目录
1、STM32_USB硬件模块简介
首先对STM32系列MCU自带的USB模块有一个概念性的认知,官方提供的培训PPT写的也很清晰,如下.
我们下面所说的全部都是F103系列的USB_FS.
在STM32F10X参考手册里面有如下两段话.
除了对USB硬件模块有了一个最基本的说明外,需要注意的就是有以下三点.
● F103系列的MCU里面CAN和USB模块不能同时使用(F105,F107不受该影响)
● USB的ACK包的发送和ACK包的处理,令牌包分组的检测,数据的发送和接收,包括USB里面的CRC校验,全部都由硬件自动做完了.
● 一共8个双向端点,可以作为16个单向端点使用
除此之外,USB提供低功耗模式(不产生任何静态电流,USB时钟减慢或停止).
2、STM32_USB硬件功能框图
各个功能框图的描述其实在ST官方的培训文档也说的很详细了,如下所示.
3、STM32_USB硬件中断
USB模块的寄存器主要分为三大类,在手册里面也说了.
先看一下通用寄存器里面最简单的两个寄存器.这部分基本没啥说的,和STM32其他模块大同小异.
寄存器 | 功能描述 |
BTABLE寄存器 | 里面存放了缓冲区描述表的起始地址 |
DADDR寄存器 | 1、USB模块的总开关 2、记录了HOST设置的设备地址 |
下面的两个寄存器都是和中断相关的.
● CNTR寄存器
● ISTR寄存器
可以很清晰的看到CNTR里面存放着USB中断的使能位,在这里我们只需要关注ISTR的CTR、WKUP、SUSP和RESET这4个bit即可.
中端标志位 | 描述 |
---|---|
CTR | 每次传输完成后都会触发这个标志位 |
WKUP | 只有在挂起的时候检测到总线的复位信号才会触发 |
SUSP | 总线无信号后,硬件自动挂起 |
RESET | 检测到复位信号后触发,触发后USB模块的发送和接收将会被禁止,直至此为被清除 |
4、STM32_USB端点相关寄存器
端点寄存器的数量由USB模块所支持的端点数目决定。每个端点都有与之对应的USB_EpnR寄存器,用于存储该端点的各种状态信息。比如F1系列一共有8个双向端点, 则0≤n≤7.具体寄存器内容如下.
这个寄存器中有 4 类标志,分别是只能清零( rc_w0),写1 翻转(t),只读( r),读写( rw).
- rc_w0 写1无效,写0清0
- t 写1翻转,写0无变化
主要关注下表的几个寄存器.
位名称 | 含义 |
CTR_RX | 正确接收到OUT或SETUP分组时由硬件置位,如果CTRM被置位,产生对应中断,以NAK或者STALL结束的分组和出错不会导致该位置位. |
STAT_RX[1:0] | 指示端点当前状态 |
EP_TYPE[1:0] | ![]() |
CTR_TX | 正确传输一个IN分组时由硬件置位,如果CTRM被置位,产生对应中断.IN分组传输完成后,如果主机响应NAK或STALL,则不会被置位. |
STAT_TX[1:0] | 指示发送数据的状态位 |
DEVICE库
4、PMA读写
在开始的时候就提到过PC和USB的交互是通过一段专属的数据缓冲区进行的,该数据缓冲区在硬件功能框图里面又被叫做Packet Buffer Memory,该区域在ST官方的USB库里面又被叫做Packet Memory Area,简称PMA.那么我们如何和这段内存进行交互呢?
首先从上面的信息里面可以知道,PMA的大小是512字节,可以通过APB1总线/USB控制器访问,且APB1的访问权限要高于USB.且软件部分通过Packet buffer interface访问(管理)PMA的存储空间,我们想使用的packet buffer的位置和大小可以随意配置,由buffer描述表指定.
buffer描述表,这个东西实在难理解,首先要明白,它本身也是占用内存的,且占用的内存也在PMA区域里面,但是这块内存的地址是由USB_BTABLE寄存器指定的!!!如下所示.
可以看到这个寄存器的功能只有一个,那就是保存了buffer描述表的起始地址.
通过上面的几段话,可以明白,想要操作这段内存空间(Packet Buffer Memory)就要对buffer描述表进行操作,但buffer描述表本身就处于这段内存中.那该如何操作这个描述表呢?这个描述表的大小又如何确定?
4.1、操作描述表
首先来看如何操作这个描述表,下面看官方提供的一段描述.
简单点说,这个描述表本身可以看成寄存器,使用操作寄存器的收发去操作.下面看一段官方的代码.
#define PCD_SET_EP_TX_ADDRESS(USBx, bEpNum, wAddr) do { \
register __IO uint16_t *_wRegVal; \
register uint32_t _wRegBase = (uint32_t)USBx; \
\
_wRegBase += (uint32_t)(USBx)->BTABLE; \
_wRegVal = (__IO uint16_t *)(_wRegBase + 0x400U + (((uint32_t)(bEpNum) * 8U) * PMA_ACCESS)); \
*_wRegVal = ((wAddr) >> 1) << 1; \
} while(0) /* PCD_SET_EP_TX_ADDRESS */
#define PCD_SET_EP_RX_ADDRESS(USBx, bEpNum, wAddr) do { \
register __IO uint16_t *_wRegVal; \
register uint32_t _wRegBase = (uint32_t)USBx; \
\
_wRegBase += (uint32_t)(USBx)->BTABLE; \
_wRegVal = (__IO uint16_t *)(_wRegBase + 0x400U + ((((uint32_t)(bEpNum) * 8U) + 4U) * PMA_ACCESS)); \
*_wRegVal = ((wAddr) >> 1) << 1; \
} while(0) /* PCD_SET_EP_RX_ADDRESS */
可以很清楚的看到,_wRegVal其实就是你要操作的ADDR_TX/RX的地址,这两段代码就是设置TX/RX缓冲区的起始地址.
这个时候已经设置好了数据存放的地址点了,但是数据交互的长度还没有设置,同理,如下所示.
#define PCD_SET_EP_TX_CNT(USBx, bEpNum, wCount) do { \
register uint32_t _wRegBase = (uint32_t)(USBx); \
register __IO uint16_t *_wRegVal; \
\
_wRegBase += (uint32_t)(USBx)->BTABLE; \
_wRegVal = (__IO uint16_t *)(_wRegBase + 0x400U + ((((uint32_t)(bEpNum) * 8U) + 2U) * PMA_ACCESS)); \
*_wRegVal = (uint16_t)(wCount); \
} while(0)
#define PCD_SET_EP_RX_CNT(USBx, bEpNum, wCount) do { \
register uint32_t _wRegBase = (uint32_t)(USBx); \
register __IO uint16_t *_wRegVal; \
\
_wRegBase += (uint32_t)(USBx)->BTABLE; \
_wRegVal = (__IO uint16_t *)(_wRegBase + 0x400U + ((((uint32_t)(bEpNum) * 8U) + 6U) * PMA_ACCESS)); \
PCD_SET_EP_CNT_RX_REG(_wRegVal, (wCount)); \
} while(0)
这个和上面的设定地址的代码其实是一样的,只是地址不一样,且在接受的时候多了一个宏,该宏的实际展开如下.
#define PCD_SET_EP_CNT_RX_REG(pdwReg, wCount) do { \
uint32_t wNBlocks; \
if ((wCount) == 0U) \
{ \
*(pdwReg) &= (uint16_t)~USB_CNTRX_NBLK_MSK; \
*(pdwReg) |= USB_CNTRX_BLSIZE; \
} \
else if((wCount) <= 62U) \
{ \
PCD_CALC_BLK2((pdwReg), (wCount), wNBlocks); \
} \
else \
{ \
PCD_CALC_BLK32((pdwReg), (wCount), wNBlocks); \
} \
} while(0) /* PCD_SET_EP_CNT_RX_REG */
其实也没什么好说的,就是根据数据量的不同,对寄存器进行设定,只是运算方法比较巧妙,有兴趣可以自己琢磨琢磨.
4.2、描述表大小的确定
上面说完了如何操作描述表,让我们可以自由的设定发送和接收的地址缓冲区和长度了,但如何确定你要使用的缓冲区地址呢,毕竟我要用的缓冲区和描述表是出于同一段内存空间的.下面看一张图.
有了上面的知识,应该已经知道能大概理解这张图的含义了,可以看出每个端点所使用的描述表大的大小为8个字节,简单点理解
描述表大小 = MAN_EP_NUM*8
USBD_StatusTypeDef USBD_LL_Init(USBD_HandleTypeDef *pdev)
{
/*省略.....*/
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x00 , PCD_SNG_BUF, 0x18);
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x80 , PCD_SNG_BUF, 0x58);
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , CDC_IN_EP , PCD_SNG_BUF, 0x98);
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , CDC_OUT_EP , PCD_SNG_BUF, 0xD8);
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , MSC_EPIN_ADDR , PCD_SNG_BUF, 0x118);
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , MSC_EPOUT_ADDR , PCD_SNG_BUF, 0x158);
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , CDC_CMD_EP , PCD_SNG_BUF, 0x198);
/*省略.....*/
}
结合例子,可以看到端点0的地址设定的是0X18(24),意思就是前24个字节用于缓冲区描述符,后续的各项间隔为0X40,其实就是端点描述符中的wMaxPacketSize.
其实我挺好奇的,复合设备的端点用的是0、1、2的双向端点,3的单向端点,其实按道理来说这地方应该是0X20,但是依旧能正常使用,后来经过实际测试,发现在通讯过程的并没有CDC_CMD_EP的传输数据.所以只使用了0,1,2三个端点.所以这个地方还是可以正常使用的.
建议使用端点的时候最好使用连续的端点,如果使用的是0,1,3.计算的时候还是需要按照4个端点进行计算的,端点2虽然没有使用,但是在实际的内存空间里面还是有端点2的描述表的.这样的话就会有空间浪费.
关于更详细的的解释可以看一下这篇文章,挺详细的.
下面说一些我整了半天才整明白的概念,可能有点简单,但是我确实很迷.
简单说一下,USB_EPnR寄存器的低四位是专门存储端点地址的,比如我们在EP1R的寄存器里面写入0X0A,那么我们代码里面使用的EP1的实际地址就是0X0A,那么如果你的实际端点号其实是0X0A,但是在代码层面8个端点被抽象成为了IN_ep[8],调用的时候使用IN_ep[1]操作的依然是0X0A端点,实现了对底层的抽象.
还有就是一直说STM32有8个双向端点,可以作为16个单向端点使用,我被这句话迷惑了好久,明明只有8个EPnrR寄存器,为什么可以使用16个单向端点,后来在上述图片内看到这句话,才明白,USB_IP最多只能使用8个端点,也就是说,我们不能同时使用15个IN/OUT端点.
他说的这16个单向端点受EPnR控制,说的就是受上图红框内的寄存器位控制.