您的位置:首页 > 其它

《圈圈教你玩USB》 第三章 USB鼠标的实现——看书笔记(1)

2017-03-22 22:43 344 查看
前言:
    本章以一个实际的例子——USB鼠标来讲述如何设计一个USB设备。

    本章将穿插USB标准请求、各种标准描述符、报告描述符等重要知识。

    本章是本书中最长一章,以后的实例程序都是在此基础上修改而来。

3.1 USB鼠标工程的建立

    将第二章中实例复制一份,将文件夹名改为UsbMouse。然后进入UsbMouse文件夹中,将工程名从
TestBoard.uv2改为UsbMouse.uv2。工程的时钟频率改为22.1184。实现了一个程序的基本框架,并且
含有串口、键盘和LED等驱动。
    再修改main.c文件,将显示的信息头修改一下,将死循环中的代码删除,只剩下一些初始化代码。

    在前的工程中缺少一个写1字节数据的函数。在PDIUSBD12.c文件中,增加一个写1字节数据的函数。

//函数功能:写一字节D12数据。

//入口参数:Value:要写的一字节数据。

void D12WriteByte(uint8 Value)

{

D12SetDataAddr();     //设置为数据地址

D12ClrWr();           //WR置低

D12SetPortOut();      //将数据口设置为输出状态(注意这里为空宏,移植时可能有用)

D12SetData(Value);    //写出数据

D12SetWr();           //WR置高

D12SetPortIn();       //将数据口设置为输入状态,以备后面输入使用

}


3.2 USB的断开和连接


1. 如何断开和连接USB

       当按下复位按键后,程序重新运行,这时须模拟一个USB拔下的动作,因此在程序的开始处,需要将D12内部
的上拉电阻断开。这可以通过D12的设置模式命令来实现。将上拉电阻断开后,需要再延迟一段时间,以便主机确
认设备已经断开连接。然后再将D12的上拉电阻连上,这时主机就会检测到设备的插入。
   D12的设置模式命令(Set Mode):

    Set Mode命令的代码是0xF3,它后面跟2字节数据的写入。第一字节是配置字节,第二字节是时钟分频系数。

    第一字节和第二字节的详细结构图如下所示。

            


    1)Set Mode命令的第一字节各位介绍:

    Bit0:保留,置0。

    Bit1:无赖时钟(低频时钟)模式。
           该位设置为1表示时钟输出端CLKOUT不会切换到懒时钟模式;
           该位设置为0表示时钟输出端将在Suspend引脚变高后1ms切换到懒时钟模式。
           懒时钟的频率为30x(1±40%)kHz。该位在USB总线复位时不会被改变。
    Bit2:时钟运行。
           该位设置为1,表示即使在USB挂起状态下,内部时钟和PLL也保持运行状态;
           该位设置为0,表示当时钟不再需要时,内部时钟、晶体振荡器和PLL都将停止运行。

            为了能达到USB协议中对总线挂起时严格的电流限制,该位应该设置为0,以节省在挂起状态下的
      电流消耗。该位在USB总线复位时不会被改变。
    Bit3:中断模式。

            该位置1时,表示所有的错误和NAK都将产生中断请求;

            该位置0时,表示只有传输正确(对于输出端点,正确接收到数据;对于输入端点,成功发送出数据)

      时,才产生中断请求。该位在USB总线复位时,不会被改变。

    Bit4:软连接控制。

            该位置1,并且Vbus有效(前面说过,Vbus是通过EOT_N检测的)时,就会将上拉电阻连通;

            该位置0时,上拉电阻被断开。该位在USB总线复位时,不会被改写。

    Bit5:保留,置0。

    Bit7~6:端点配置选择。

            可以选择模式0~3。这三种具体的模式请参看D12数据手册。

            模式0为无等时端点,即端点1和端点2都是普通端点,可作为中断或批量端点。

    2)Set Mode命令的第二字节各位介绍:

    Bit3~0:时钟分频系数。

                假设该值为N,那么CLKOUT端的频率值就是48MHz除以N+1。通过对该值的设置,可以获得不同
           频率的CLKOUT时钟输出。USB总线复位不会影响该值。

    Bit5~4:保留,置0。

    Bit6    :该位必须置1。

    Bit7    :仅在SOF时产生中断。该位置1,只有当帧起始(SOF)时,中断信号才产生。

   3) 程序中两个字节如何设置:

    ① 为了方便调试,不考虑节电,时钟设置为使能状态;
    ② 中断模式选择只有正确传输才产生中断,即成功发送或成功接收到数据后才产生中断;

    ③ 端点配置选择为模式0,即端点1和端点2都为普通模式,因为这里不需要等时传输;

    所以,第一个字节的值在USB连接断开时为0x06;连接时为0x16。
    ④ 将分频系数设置为最大,即8分频,从而在CLKOUT端得到6MHz的时钟频率;

    ⑤ 中断可以在任何时刻产生,不需要仅在SOF时产生。

    所以,第二字节的值得Bit7为0,因此得出第二字节的值为0x47。


2.如何用代码实现USB断开和连接

    增加一个UsbCore.c和UsbCore.h文件,大部分根USB协议相关的代码都放在这里。增加一些调试信息,并用宏
打开和关闭,该宏的定义在config.h中。

//函数功能:USB断开连接函数。

void UsbDisconnect(void)

{

#ifdef DEBUG0

Prints("断开USB连接。\r\n");

#endif

D12WriteCommand(D12_SET_MODE);  //写设置模式命令

D12WriteByte(0x06); //设置模式的第一字节

D12WriteByte(0x47); //设置模式的第二字节

DelayXms(1000);  //延迟1秒

}


//函数功能:USB连接函数。

void UsbConnect(void)

{

#ifdef DEBUG0

Prints("连接USB。\r\n");

#endif

D12WriteCommand(D12_SET_MODE);  //写设置模式命令

D12WriteByte(0x16); //设置模式的第一字节

D12WriteByte(0x47); //设置模式的第二字节

}


    进入主函数,完成各种初始化后,先调用断开连接函数来断开USB连接,再调用USB连接函数,将上拉电阻连
通,此时就检测到设备已经插入了。

3.3 USB中断的处理

1. 哪些事件会导致D12中断请求

    USB总线复位;D12进入挂起状态;成功接收或发送完数据等。

    注:这里在主程序中一直查询中断引脚的电平状态来判断D12是否有中断发生,当然也可以改为终端方式。

2. 如何判断中断源

    通过读取D12的中断寄存器来获取。

    读中断寄存器的命令为Read Interrupt Register,代码为0xF4。

    发送该命令后,可以读取两个字节的数据,第一字节中的内容是端点和总线状态的中断,第二字节的内容只有一
位有效,是与DMA有关的。
    本程序不用DMA,所以只保存第一个字节。第一字节详细结构如下图所示。
    


     其中,某位为1,表示该中断源发出了中断请求。

3. 如何处理中断信号

   通过判断该寄存器中每一位的值,可以写8个对应的处理函数来处理它们。这8个函数都放在UsbCore.c中。

    对中断源的处理代码如下:

while(1)  //死循环

{

if(D12GetIntPin()==0)                       //如果有中断发生

{

D12WriteCommand(READ_INTERRUPT_REGISTER);  //写读中断寄存器的命令

InterruptSource=D12ReadByte();             //读回第一字节的中断寄存器

if(InterruptSource&0x80)UsbBusSuspend();   //总线挂起中断处理

if(InterruptSource&0x40)UsbBusReset();     //总线复位中断处理

if(InterruptSource&0x01)UsbEp0Out();       //端点0输出中断处理

if(InterruptSource&0x02)UsbEp0In();        //端点0输入中断处理

if(InterruptSource&0x04)UsbEp1Out();       //端点1输出中断处理

if(InterruptSource&0x08)UsbEp1In();        //端点1输入中断处理

if(InterruptSource&0x10)UsbEp2Out();       //端点2输出中断处理

if(InterruptSource&0x20)UsbEp2In();        //端点2输入中断处理

}

}


    然后,每个函数中写上一句输出调试信息。例如在总线复位中增加一条“ USB总线复位”:

#ifdef DEBUG0

Prints("USB总线复位。\r\n");

#endif


4. 如何分析串口调试信息

 接着把程序下载到开发板上,通电测试,看具体发生了哪些中断。

    串口显示的信息如下:



    调试信息分析:

        1)从上面显示信息看到,在连接USB之后,主机对设备进行了几次复位操作;

        2)然后向端点发送了数据,因为端点0输出已经产生了中断。

        3)至于端点0输出了什么数据,需要接收过来看看。

    注:右下角弹出了无法识别USB设备的对话框,这是因为程序未返回描述符。



3.4 读取从主机发送到端点0的数据

1. 何时读取端点数据

    3.3节中端点0发送了数据过来,并引发了中断,在端点0输出中断处理函数UsbEp0Out()中调用读取端点缓冲区函数。

2. 如何选择读哪个端点的缓冲区数据

    D12的选择端点(select endpoint)命令。

    选择端点命令共有6个,分别对应3额端点的输出和输入,命令代码实0x00~0x05,发送哪个命令就选择了哪个端点。

//函数功能:选择端点的函数,选择一个端点后才能对它进行数据操作。

//入口参数:Endp:端点号。

void D12SelectEndpoint(uint8 Endp)

{

D12WriteCommand(0x00+Endp); //选择端点的命令

}



3. 如何读取特定端点的数据   

    读取D12的数据缓冲区,使用D12的读缓冲(read buffer)命令,它的代码是0xF0,。发送该命令后,就可以连续读数据了。

    数据传输协议:1(reserved)+1(len)+n(data)

       读取的第一字节是保留位,没有意义,不用理会它。

        第二字节的值是接收到的数据的字节数,读取它之后就知道缓冲区内实际接收到了多少字节数据。

       第三字节开始是真正的USB数据,将其读出并保存到自己的Buf中。

    注:该读取函数有一个入口参数len,表示想要读取的字节数。如果len比实际接收的字节数小,则只读取前面len
字节;如果比实际接收的字节数大,则只读取实际接收的数据。

    读取端点数据的代码实现:

//函数功能:读取端点缓冲区函数。

//入口参数:Endp:端点号;Len:需要读取的长度;Buf:保存数据的缓冲区。

//返    回:实际读到的数据长度。

uint8 D12ReadEndpointBuffer(uint8 Endp, uint8 Len, uint8 *Buf)

{

uint8 i,j;

D12SelectEndpoint(Endp);             //选择要操作的端点缓冲

D12WriteCommand(D12_READ_BUFFER);    //发送读缓冲区的命令

D12ReadByte();                       //该字节数据是保留的,不用。

j=D12ReadByte();                     //这里才是实际的接收到的数据长度

if(j>Len)                            //如果要读的字节数比实际接收到的数据长

{

j=Len;                              //则只读指定的长度数据

}

#ifdef DEBUG1                         //如果定义了DEBUG1,则需要显示调试信息

Prints("读端点");

PrintLongInt(Endp/2);                //端点号。由于D12特殊的端点组织形式,

              //这里的0和1分别表示端点0的输出和输入;

              //而2、3分别表示端点1的输出和输入;

              //3、4分别表示端点2的输出和输入。

              //因此要除以2才显示对应的端点。

Prints("缓冲区");

PrintLongInt(j);                     //实际读取的字节数

Prints("字节。\r\n");

#endif

for(i=0;i<j;i++)

{

//这里不直接调用读一字节的函数,而直接在这里模拟时序,可以节省时间

D12ClrRd();                         //RD置低

*(Buf+i)=D12GetData();              //读一字节数据

D12SetRd();                         //RD置高

#ifdef DEBUG1

PrintHex(*(Buf+i));                 //如果需要显示调试信息,则显示读到的数据

if(((i+1)%16)==0)Prints("\r\n");    //每16字节换行一次

#endif

}

#ifdef DEBUG1

if((j%16)!=0)Prints("\r\n");          //换行。

#endif

return j;                             //返回实际读取的字节数。

}



4. 如何清除中断标志

    为什么要清除中断标志:

        防止一直提示中断发生。

    如何清除端点中断标志:
        用Read Last Transaction Status命令读取端点最后传输状态后,各端点的中断标志(Bit0~5)被清零。

    
        该命令代码为0x40~0x45,分别对应着3个端点的输出和输入。
        发生该命令后,可以读1字节数据,数据内容为该端点传输的最后状态。详细结构如下图所示:



         Bit0:该位为1表示数据成功接收或发送。
    
    Bit1~4:出错代码,可以用来调试,知道当前的芯片处于怎样的状态,具体参看数据手册。
         Bit5:该位为1,表示收到的是建立(setup)过程的数据包。
         Bit6:该位为0,表示收到的是DATA0数据包;该位为1表示收到的是DATA1数据包。

         Bit7:该位为1,表示前一次状态没有读取,前面的状态已经被覆盖。

         其中Bit5在控制传输中很有用,由此可知当前收到的是建立过程的数据包。建立包是控制传输第一个过
程的令牌包,地位很特殊,控制端点必须要接收建立过程的数据包。    

    如何清除另外两位(Bit6~7):
       在读取本寄存器后,被自动清零。

5. 如何清除数据缓冲区   

    为什么要清除数据缓冲区:

        防止不能再接收数据。

        如果一个端点接收数据后没有清除端点缓冲区,对于以后发往该端点的数据包(建立过程的数据包除

外,设备必须接收它)将使用NAK来应答。

    如何清除数据缓冲区:

        命令Clear Buffer,代码是0xF2。

        对于D12的控制端点,接收到建立包后必须要使用命令Acknowledge Setup,才能让Clear Buffer命令和

Validate Buffer命令生效。Acknowledge Setup命令对控制输入和输出端点都要发送,因为Clear Buffer命令是

针对输出端点的,而Validate Buffer命令是针对输入端点的。

       这样做的目的是为了保证控制传输建立过程的数据不会丢失,且接着也不会返回错误的数据,只有等到

处理完了这个建立过程,并发送Acknowledge Setup命令后,才能使用Clear Buffer命令和Validate Buffer命令。

        因此,程序首先要判断一下,收到的这个数据包是否为建立过程的数据包;如果是,则在发送Clear

Buffer命令之前,还需要先发送Acknowledge Setup命令。

       通常,先读取端点缓冲区后再清除端点缓冲区,因此这里的清除端点缓冲区函数没有再选择端点,避免

多余的操作。

       在调用该函数前,一定要确保当前所选择的端点是需要清除的目标端点。例如,下面的

AcknowledgeSetup()函数就是先对输入端点0操作,再对输出端点0操作,以保证后面使用的清缓冲函数时当

前的目标端点是输出端点0。

    清除数据缓冲区代码实现:

//函数功能:清除接收端点缓冲区的函数。

//备    注:只有使用该函数清除端点缓冲后,该接收端点才能接收新的数据包。

void D12ClearBuffer(void)

{

D12WriteCommand(D12_CLEAR_BUFFER);

}


//函数功能:应答建立包的函数。

void D12AcknowledgeSetup(void)

{

D12SelectEndpoint(1);                     //选择端点0输入

D12WriteCommand(D12_ACKNOWLEDGE_SETUP);   //发送应答设置到端点0输入

D12SelectEndpoint(0);                     //选择端点0输出

D12WriteCommand(D12_ACKNOWLEDGE_SETUP);   //发送应答设置到端点0输出

}


6. 端点0输出中断处理函数

   如何处理端点0输出数据:

       进入端点0输出中断后,首先读取最后传输状态;

       然后检查Bit5是否为1,如果是1,则说明是建立包,此时读取数据后需要调用D12AcknowledgeSetup()函数;

                   
                    如果不是1,则说明只是普通的输出数据包,不用调用D12AcknowledegSetup()

函数,直接清除缓冲区即可。

    将端点0输出中断处理函数如下所示:

函数功能:端点0输出中断处理函数。

入口参数:无。

返    回:无。

备    注:无。

********************************************************************/

void UsbEp0Out(void)

{

#ifdef DEBUG0

Prints("USB端点0输出中断。\r\n");

#endif

//读取端点0输出最后传输状态,该操作清除中断标志

//并判断第5位是否为1,如果是,则说明是建立包

if(D12ReadEndpointLastStatus(0)&0x20)

{

D12ReadEndpointBuffer(0,16,Buffer);   //读建立过程数据

D12AcknowledgeSetup();                //应答建立包

D12ClearBuffer();                     //清缓冲区

}

else //if(D12ReadEndpointLastStatus(0)&0x20)之else 普通数据输出

{

D12ReadEndpointBuffer(0,16,Buffer);

D12ClearBuffer();

}

}


7. 分析串口调试信息

编译并下载上面程序,通过串口调试助手可以看到返回的调试信息,如下图所示:



    1)从上图可以看出,已经成功接收到主机发送过来的8字节数据。
    2)在第一次接收到数据后,会停顿一段时间。此时主机一直在请求输入。但程序目前还没有返回数据,
所以D12一直在回答NAK,即没有数据准备好。
    3)结果USB主机经过一段时间等待后,终于放弃,发送一次总线复位。
    4)然后又重新输出这8字节数据,又等待输入数据……主机共重试3次这种操作,当3次都没读到数据后,
放弃操作。

    5)USB端口上不再有数据活动,D12进入挂起状态。

    6)计算机端弹出“ 无法识别 ”对话框。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  usb 鼠标