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

前言:
    本章以一个实际的例子——USB鼠标来讲述如何设计一个USB设备。
    本章将穿插USB标准请求、各种标准描述符、报告描述符等重要知识。
    本章是本书中最长一章,以后的实例程序都是在此基础上修改而来。

3.1 USB鼠标工程的建立

    将第二章中实例复制一份,将文件夹名改为UsbMouse。然后进入UsbMouse文件夹中,将工程名从
TestBoard.uv2改为UsbMouse.uv2。工程的时钟频率改为22.1184。实现了一个程序的基本框架,并且
含有串口、键盘和LED等驱动。
    再修改main.c文件,将显示的信息头修改一下,将死循环中的代码删除,只剩下一些初始化代码。
    在前的工程中缺少一个写1字节数据的函数。在PDIUSBD12.c文件中,增加一个写1字节数据的函数。
    
    
  1. //函数功能:写一字节D12数据。
  2. //入口参数:Value:要写的一字节数据。
  3. void D12WriteByte(uint8 Value)
  4. {
  5. D12SetDataAddr();    //设置为数据地址
  6. D12ClrWr();        //WR置低
  7. D12SetPortOut();    //将数据口设置为输出状态(注意这里为空宏,移植时可能有用)
  8. D12SetData(Value);   //写出数据
  9. D12SetWr();          //WR置高
  10. D12SetPortIn();      //将数据口设置为输入状态,以备后面输入使用
  11. }

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中。
   
   
  1. //函数功能:USB断开连接函数。
  2. void UsbDisconnect(void)
  3. {
  4. #ifdef DEBUG0
  5. Prints("断开USB连接。\r\n");
  6. #endif
  7. D12WriteCommand(D12_SET_MODE); //写设置模式命令
  8. D12WriteByte(0x06); //设置模式的第一字节
  9. D12WriteByte(0x47); //设置模式的第二字节
  10. DelayXms(1000); //延迟1秒
  11. }
  12. //函数功能:USB连接函数。
  13. void UsbConnect(void)
  14. {
  15. #ifdef DEBUG0
  16. Prints("连接USB。\r\n");
  17. #endif
  18. D12WriteCommand(D12_SET_MODE); //写设置模式命令
  19. D12WriteByte(0x16); //设置模式的第一字节
  20. D12WriteByte(0x47); //设置模式的第二字节
  21. }
    进入主函数,完成各种初始化后,先调用断开连接函数来断开USB连接,再调用USB连接函数,将上拉电阻连
通,此时就检测到设备已经插入了。

3.3 USB中断的处理

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

    USB总线复位;D12进入挂起状态;成功接收或发送完数据等。
    注:这里在主程序中一直查询中断引脚的电平状态来判断D12是否有中断发生,当然也可以改为终端方式。

2. 如何判断中断源

    通过读取D12的中断寄存器来获取。
    读中断寄存器的命令为Read Interrupt Register,代码为0xF4。
    发送该命令后,可以读取两个字节的数据,第一字节中的内容是端点和总线状态的中断,第二字节的内容只有一
位有效,是与DMA有关的。
    本程序不用DMA,所以只保存第一个字节。第一字节详细结构如下图所示。
    
     其中,某位为1,表示该中断源发出了中断请求。

3. 如何处理中断信号

   通过判断该寄存器中每一位的值,可以写8个对应的处理函数来处理它们。这8个函数都放在UsbCore.c中。
    对中断源的处理代码如下:
   
   
  1. while(1) //死循环
  2. {
  3. if(D12GetIntPin()==0)                      //如果有中断发生
  4. {
  5. D12WriteCommand(READ_INTERRUPT_REGISTER); //写读中断寄存器的命令
  6. InterruptSource=D12ReadByte();            //读回第一字节的中断寄存器
  7. if(InterruptSource&0x80)UsbBusSuspend();  //总线挂起中断处理
  8. if(InterruptSource&0x40)UsbBusReset(); //总线复位中断处理
  9. if(InterruptSource&0x01)UsbEp0Out(); //端点0输出中断处理
  10. if(InterruptSource&0x02)UsbEp0In(); //端点0输入中断处理
  11. if(InterruptSource&0x04)UsbEp1Out(); //端点1输出中断处理
  12. if(InterruptSource&0x08)UsbEp1In(); //端点1输入中断处理
  13. if(InterruptSource&0x10)UsbEp2Out(); //端点2输出中断处理
  14. if(InterruptSource&0x20)UsbEp2In(); //端点2输入中断处理
  15. }
  16. }
    然后,每个函数中写上一句输出调试信息。例如在总线复位中增加一条“ USB总线复位”:
    
    
  1. #ifdef DEBUG0
  2. Prints("USB总线复位。\r\n");
  3. #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,发送哪个命令就选择了哪个端点。
   
   
  1. //函数功能:选择端点的函数,选择一个端点后才能对它进行数据操作。
  2. //入口参数:Endp:端点号。
  3. void D12SelectEndpoint(uint8 Endp)
  4. {
  5. D12WriteCommand(0x00+Endp); //选择端点的命令
  6. }

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

    读取D12的数据缓冲区,使用D12的读缓冲(read buffer)命令,它的代码是0xF0,。发送该命令后,就可以连续读数据了。
     数据传输协议:1(reserved)+1(len)+n(data)
       读取的第一字节是保留位,没有意义,不用理会它。
        第二字节的值是接收到的数据的字节数,读取它之后就知道缓冲区内实际接收到了多少字节数据。
       第三字节开始是真正的USB数据,将其读出并保存到自己的Buf中。
    注:该读取函数有一个入口参数len,表示想要读取的字节数。如果len比实际接收的字节数小,则只读取前面len
字节;如果比实际接收的字节数大,则只读取实际接收的数据。
     读取端点数据的代码实现:
   
   
  1. //函数功能:读取端点缓冲区函数。
  2. //入口参数:Endp:端点号;Len:需要读取的长度;Buf:保存数据的缓冲区。
  3. //返 回:实际读到的数据长度。
  4. uint8 D12ReadEndpointBuffer(uint8 Endp, uint8 Len, uint8 *Buf)
  5. {
  6. uint8 i,j;
  7. D12SelectEndpoint(Endp);            //选择要操作的端点缓冲
  8. D12WriteCommand(D12_READ_BUFFER);   //发送读缓冲区的命令
  9. D12ReadByte();                    //该字节数据是保留的,不用。
  10. j=D12ReadByte();                    //这里才是实际的接收到的数据长度
  11. if(j>Len)                           //如果要读的字节数比实际接收到的数据长
  12. {
  13. j=Len;                            //则只读指定的长度数据
  14. }
  15. #ifdef DEBUG1                        //如果定义了DEBUG1,则需要显示调试信息
  16. Prints("读端点");
  17. PrintLongInt(Endp/2);               //端点号。由于D12特殊的端点组织形式,
  18.               //这里的0和1分别表示端点0的输出和输入;
  19.               //而2、3分别表示端点1的输出和输入;
  20.               //3、4分别表示端点2的输出和输入。
  21.               //因此要除以2才显示对应的端点。
  22. Prints("缓冲区");
  23. PrintLongInt(j);               //实际读取的字节数
  24. Prints("字节。\r\n");
  25. #endif
  26. for(i=0;i<j;i++)
  27. {
  28. //这里不直接调用读一字节的函数,而直接在这里模拟时序,可以节省时间
  29. D12ClrRd();                     //RD置低
  30. *(Buf+i)=D12GetData();         //读一字节数据
  31. D12SetRd();                     //RD置高
  32. #ifdef DEBUG1
  33. PrintHex(*(Buf+i));             //如果需要显示调试信息,则显示读到的数据
  34. if(((i+1)%16)==0)Prints("\r\n"); //每16字节换行一次
  35. #endif
  36. }
  37. #ifdef DEBUG1
  38. if((j%16)!=0)Prints("\r\n");     //换行。
  39. #endif
  40. return j;                            //返回实际读取的字节数。
  41. }

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。
     清除数据缓冲区代码实现:
    
    
  1. //函数功能:清除接收端点缓冲区的函数。
  2. //备 注:只有使用该函数清除端点缓冲后,该接收端点才能接收新的数据包。
  3. void D12ClearBuffer(void)
  4. {
  5. D12WriteCommand(D12_CLEAR_BUFFER);
  6. }
  7. //函数功能:应答建立包的函数。
  8. void D12AcknowledgeSetup(void)
  9. {
  10. D12SelectEndpoint(1);                    //选择端点0输入
  11. D12WriteCommand(D12_ACKNOWLEDGE_SETUP);  //发送应答设置到端点0输入
  12. D12SelectEndpoint(0);                    //选择端点0输出
  13. D12WriteCommand(D12_ACKNOWLEDGE_SETUP); //发送应答设置到端点0输出
  14. }

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

   如何处理端点0输出数据:
       进入端点0输出中断后,首先读取最后传输状态;
       然后检查Bit5是否为1,如果是1,则说明是建立包,此时读取数据后需要 调用D12AcknowledgeSetup()函数;
                                                     如果不是1,则说明只是普通的输出数据包,不用调用D12AcknowledegSetup()
函数, 直接清除缓冲区即可。
    将端点0输出中断处理函数如下所示:
    
    
  1. 函数功能:端点0输出中断处理函数。
  2. 入口参数:无。
  3. 回:无。
  4. 注:无。
  5. ********************************************************************/
  6. void UsbEp0Out(void)
  7. {
  8. #ifdef DEBUG0
  9. Prints("USB端点0输出中断。\r\n");
  10. #endif
  11. //读取端点0输出最后传输状态,该操作清除中断标志
  12. //并判断第5位是否为1,如果是,则说明是建立包
  13. if(D12ReadEndpointLastStatus(0)&0x20)
  14. {
  15. D12ReadEndpointBuffer(0,16,Buffer); //读建立过程数据
  16. D12AcknowledgeSetup();             //应答建立包
  17. D12ClearBuffer();                    //清缓冲区
  18. }
  19. else //if(D12ReadEndpointLastStatus(0)&0x20)之else 普通数据输出
  20. {
  21. D12ReadEndpointBuffer(0,16,Buffer);
  22. D12ClearBuffer();
  23. }
  24. }

7. 分析串口调试信息

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

    1)从上图可以看出,已经成功接收到主机发送过来的8字节数据。
    2)在第一次接收到数据后,会停顿一段时间。此时主机一直在请求输入。但程序目前还没有返回数据,
所以D12一直在回答NAK,即没有数据准备好。
    3)结果USB主机经过一段时间等待后,终于放弃,发送一次总线复位。
    4)然后又重新输出这8字节数据,又等待输入数据……主机共重试3次这种操作,当3次都没读到数据后,
放弃操作。
    5)USB端口上不再有数据活动,D12进入挂起状态。
    6)计算机端弹出“ 无法识别 ”对话框。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值