开发板是用的stm32f103c8t6的核心板淘宝上最便宜最常见的那种(还是上图吧)
stm32f1系列的hal驱动库中把usb驱动放在了“Middlewares(中间件)”文件夹下,且有“STM32_USB_Device_Library”和“STM32_USB_Host_Library”两个驱动库。若是想直接利用驱动库新建工程可在这两个库的目录下复制Core文件夹和Class文件夹下所需文件。
先勾选Peripherals的USB->Device才能在MiddleWares下的USB_Devics下选择IP核,下拉后有六个选项,分别对应USB设备的六大分类:
分类 | 功能 |
---|---|
AUDIO | USB音频设备 |
CDC(communication device class) | 虚拟串口 |
Download Firmware Update | 固件更新 |
Human Interface Device | 人机接口 |
Custom Human Interface Device | 传统人机接口(键盘鼠标类) |
Mass Storage Class | 大容量存储设备 |
此处选择CDC设备。需要注意的是,在RCC分类下的HSE必须被使能成外部晶振->Crystal/ceramic Resonator。要开启USB核主时钟必须达到48M,因为USB区域的时钟为48M。这点可在时钟树的配置中很清楚看到。不过,若主时钟没有配置成48M或以上,在STM32Cube的Clock Configuartion界面会直接报错,点进去看会提示是否自动设置时钟,直接确认就OK。
PLL倍频数位6的话,USB分频设为1即可,若要开到72M的最高频率,分频数则要设为1.5。其余配置均保持默认即可。
最后点击生成代码,工程名称为:USBCDC 使用MDK-ARM/V5。下面的Stack Size可以改大些,USB CDC的RX和TX的Buffer默认大小为2048,只是单纯的收发测试的话,默认大小也够用,为避免造成内存溢出导致的跑飞,此处设为0x1000。在Code generator界面最好勾选->为每个外设生成单独的.c和.h文件 这个选项。这样生成的外设驱动能比较方便的移植,另外分类清晰,可读性也较好,否则全部代码都堆在main文件中。
Middlewares分类下为USB内核代码,这些文件都不用改动。
Application下的stm32f1xx_it.c文件中含有目前工程中所开启的所有中断处理函数代码,打开翻到这个文件的最下面可以看到,STM32Cube默认为USB开启了中断:
/**
* @brief This function handles USB low priority or CAN RX0 interrupts.
*/
void USB_LP_CAN1_RX0_IRQHandler(void)
{
/* USER CODE BEGIN USB_LP_CAN1_RX0_IRQn 0 */
/* USER CODE END USB_LP_CAN1_RX0_IRQn 0 */
HAL_PCD_IRQHandler(&hpcd_USB_FS);
/* USER CODE BEGIN USB_LP_CAN1_RX0_IRQn 1 */
/* USER CODE END USB_LP_CAN1_RX0_IRQn 1 */
}
从中断函数函数名可以看出,USB和CAN是公用一个中断向量的。在f1系列的参考文档中也可以查到,CAN和USB占用同一段RAM,这两个外设不能同时使用。
stm32f1xx_hal_msp.c文件只含有HAL_MspInit()
这一个函数,实现NVIC分组和系统中断优先级的设定(都是最高优先级),并禁用JTAG和SWD使能(若在Cube配置中没有开启SYS->Serial Wire,则不会有这一句)。
usb_device.c文件初始化了一个USB句柄。由于是HAL库的驱动,整体的代码对于外设初始化和调用都是通过句柄。
/* USB Device Core handle declaration */
USBD_HandleTypeDef hUsbDeviceFS;
/* init function */
void MX_USB_DEVICE_Init(void)
{
USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS);
USBD_RegisterClass(&hUsbDeviceFS, &USBD_CDC);
USBD_CDC_RegisterInterface(&hUsbDeviceFS, &USBD_Interface_fops_FS);
USBD_Start(&hUsbDeviceFS);
}
去掉多余部分注释(实话说Cube生成的工程注释太多挺烦的),可以看出这是USB外设的初始化函数;USBD_Init()里面包含了,检测主机是否存在,复位USB类,设定USB设备描述符,还有USB设备状态几个步骤,最后初始化了单片机上面的USB底层驱动:端点号啊,全速模式啊,失能低功耗,外部供电关闭啊,blabalbal…然后会跑到USBD_LL_MspInit()中去开启USB的引脚。USB引脚比较特殊,只要单片机内的USB区域时钟开启,那PA12和PA11就只能作为USB的数据引脚使用。
USBD_RegisterClass()会连接CDC类驱动到USB核,这点在HAL固件库里有所体现,USB的设备库分成了两层,一层为内核文件,就是只要开启了USB就要调用的,还有一层为“类”,对应它的不同功能,详见上面那个表格,只需根据需要将一类代码加入工程即可。至于函数内具体的细节,则需要对应驱动库的说明看了(不知是我人品不好还是怎么,在哪都找不到hal库 usb的说明文档,只能找到以前的标准库的)。
USBD_CDC_RegisterInterface()则是不知道要干嘛了,进入函数定义只能看到简短的描述,@brief USBD_CDC_RegisterInterface
(找不到文档的痛(ㄒ_ㄒ))。最后一个倒是很容易看懂了‘USBD_Start()’开启外设。
usb_conf.c文件则是关于USB的配置,这里除开HAL库的部分,还有部分代码是使用LL库写的(底层驱动部分)。但USB协议部分都是包含在stm32f1x_hal_pcd.c和stm32f1x_hal_pcd_ex.cz这两组文件中(为嘛不用stm32f1x_hal_usb.c文件名害得我一顿好找)。有兴趣的可以详细看看分析,只是要使用这个接口的话,也不用深究了。
然后usb_desc.c文件也是关于协议这一块(目前学的不深就不多说了)。但有一点要补充的,这个两个文件里的代码——不要动!不要动!不要动!
usb_cdc_if.c这里就是对与USB的CDC类的直接调用了。文件不是很大,里面定义了用户的收发缓存,都是2048byte。然后有4个静态函数和一个可供外部调用的函数:
static int8_t CDC_Init_FS (void);
static int8_t CDC_DeInit_FS (void);
static int8_t CDC_Control_FS (uint8_t cmd, uint8_t* pbuf, uint16_t length);
static int8_t CDC_Receive_FS (uint8_t* pbuf, uint32_t *Len);
uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len)
见名生意,全速设备CDC类初始化、CDC类复位、CDC类控制、CDC类接收还有CDC类发送函数。初始化复位和控制这3个函数都没什么好说的。
重点是CDC_Receive_FS
和CDC_Transmit_FS
这两个函数。这里必须强调CDC_Receive_FS
是USB接收中断回调函数。有必要提一下HAL库和标准库的一个区别:使用标准库时,在配置中断时,一般都是在启动文件中寻找中断向量名,以此名字编写中断处理函数。函数里面有两个重要的部分,一是清除中断标志位,二是实现用户需要的功能。内容部分都是要自己写的。HAL库中,对于中断处理函数做了封装,函数名和函数体都已经写好,进入中断处理函数就判断中断源,然后清除中断标志位,接着进入中断回调函数。唯一允许改动的则是这个中断回调函数,它在库中使用weak修饰,即允许user在工程的任何地方对该函数重定义,以实现所需的功能。CDC_Receive_FS则是CDC类对应USB库的中断回调函数。
/**
* @brief CDC_Receive_FS
* Data received over USB OUT endpoint are sent over CDC interface
* through this function.
*
* @note
* This function will block any OUT packet reception on USB endpoint
* untill exiting this function. If you exit this function before transfer
* is complete on CDC interface (ie. using DMA controller) it will result
* in receiving more data while previous ones are still not sent.
*
* @param Buf: Buffer of data to be received
* @param Len: Number of data received (in bytes)
* @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL
*/
static int8_t CDC_Receive_FS (uint8_t* Buf, uint32_t *Len)
{
/* USER CODE BEGIN 6 */
USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
return (USBD_OK);
/* USER CODE END 6 */
}
从注释也能看出,在从USB虚拟串口接收到任何数据后,都会将接收到的数组的指针和数组长度传回此函数。若需要实现某些功能,只需在重设接收缓存前新增函数或者直接处理即可。
而允许外部调用的那个CDC_Transmit_FS(),则是像串口发送函数那样直接调用即可,输入要发送的数组的指针和要发送的长度,然后就可以在电脑端的虚拟串口处直接接收到所发送的数据。
我有尝试过像串口输出重定向那样,使用CDC_Transmit_FS()函数重定向fputc欲实现printf函数通过虚拟串口的打印。但实测每次发送字符串或者长一点的符号什么的,就只能接收到第一个字符。重定向代码还是贴出来吧,思路应该是正确的,肯定还有哪些点没注意到…如果有大佬看到的话,还劳烦指点迷津,不胜感激。
int fputc(int ch, FILE *f)
{
CDC_Transmit_FS((uint8_t *)&ch, 1);
return ch;
}
回到正题,剩下gpio.c文件还没说,这里面只有一个MX_GPIO_Init()
函数,用来开启GPIOA和GPIOD两组引脚的时钟,GPIOA是USB的D+和D-引脚以及下载口SWDIO和SWCLK的时钟,GPIOD则是HSE主时钟的晶振引脚的时钟了。
工程内的主要代码说完,再看主函数main()(依旧去除烦人的注释)
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USB_DEVICE_Init();
while (1)
{
}
}
使用STM32Cube生成的代码,都是一个样:首先初始化hal库,然后配置系统时钟,开启要用的GPIO口时钟,然后初始化外设,再加一个空的while循环,只是帮助user初始化要用到的外设,要实现其他功能,还要自己往里加。MX_USB_DEVICE_Init()
函数在前面说usb_device.c的时候讲过,不再重复。
这里实现一个简单的功能:把从虚拟串口接收到的数据,原样从虚拟串口输出。只需在usb接收中断回调函数中做简单的处理,修改代码如下:
static int8_t CDC_Receive_FS (uint8_t* Buf, uint32_t *Len)
{
/* USER CODE BEGIN 6 */
CDC_Transmit_FS(Buf,*Len);
CDC_Transmit_FS((uint8_t *)"\n",sizeof("\n"));
USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
return (USBD_OK);
/* USER CODE END 6 */
}
然后编译。由于STM32Cube生成的代码默认调试工具为ST-LINK如果使用J-LINK,还需在Options中更改调试工具,并选成SW模式。下载完成后用一根micro-B接口的数据线连到电脑。补充一点,需要先装stm32的虚拟串口驱动,电脑才能识别(安装完后重新插拔USB上电)。
这时电脑上就会识别到虚拟串口(我也不知道我电脑为什么不是显示stmicroxxx visual com port的标志,可能win10系统的原因吧)。打开任意串口调试助手(波特率什么的随意,因为实际情况是通过usb总线传输的数据,与波特率什么的无关)
任意发送即可看到回复。
[注]:在使用CDC_Transmit_FS函数发送数据时,若len为64的整数倍,则需要在发送完成后在发送一个空帧;否则电脑端会直接丢去此次发送的数据,这是由usb的协议所决定的,有兴趣的可以看看官方这里给出的解释在进行USB CDC类开发时,无法发送64整数倍的数据,或详细分析usb传输协议。