1)实验平台:正点原子stm32f103战舰开发板V4
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html#
本章,我们将向大家介绍如何利用USB FS 在正点原子战舰STM32F1开发板实现一个USB虚拟串口,通过USB与电脑数据数据交互。本章分为如下几个部分:
61.1 USB虚拟串口简介
61.2 硬件设计
61.3 软件设计
61.4 下载验证
USB虚拟串口,简称VCP,是Virtual COM Port的简写,它是利用USB的CDC类来实现的一种通信接口。CDC(Communication Device Class)类是 USB2.0 标准下的一个设备类,定义了通信相关设备的抽象集合。ST官方文档《USB CDC 类入门培训》提供了关于USB CDC的详细知识,同样的,我们把资料放到“光盘资料(A盘)→8,STM32参考资料→2,STM32 USB 学习资料”中了。
我们可以利用STM32自带的USB功能,来实现一个USB虚拟串口,从而通过USB,实现电脑与STM32的数据互传。上位机无需编写专门的USB程序,只需要一个串口调试助手即可调试,非常实用。
61.2硬件设计
图61.3.1 ST官方例程的结构
从工程结构不难找出我们需要的USB功能代码,并且这些文件是加了只读属性的,我们移植后需要把只读属性去掉才能进行我们需要的修改。我们复制之前的“内存管理实验”工程,重命名为“USB虚拟串口实验”,因为我们并不是所有例程都使用USB库驱动,故我们把USB作为一个第三方组件放到我们的“Middlewares”文件夹下,我们在该文件夹下新建一个USB文件目录,把USB相关的全部放到USB文件夹下使这部分驱动完全独立,这样可以方便我们以后事移植到其它项目中。
首先是usbd_core.c、usbd_ioreq.c、usbd_ctlreq.c这三个文件,我们查看它们所在的路径发现它们都位于“STM32_USB_Device_Library”文件夹下,所以我们可以直接把该文件夹复制到我们的“USB虚拟串口/Middlewares/USB”文件夹下,后面再考虑精简工程。复制后USB文件夹下就有“STM32_USB_Device_Library /Class”和“STM32_USB_Device_Library /Core”两个目录了。
接着同样的方法,找到usbd_cdc.c文件,文件位于“STM32_USB_Device_Library/Class”文件夹下,上一步我们已经把整个文件夹复制到我们的USB目录下了,所以这步不需要再操作。
接下来的USB应用程序usbd_cdc_interface.c、 usbd_desc.c、usbd_conf.c三个文件,源文件和头文件分别位于USB_Device\CDC_Standalone\Src和\USB_Device\CDC_Standalone\Inc下,我们在USB文件夹下新建一个“USB_APP”文件夹,把它们连同头文件都放到该文件夹下。复制后的USP_APP目录结构如图61.3.2所示。
图61.3.2 USB APP文件夹下的文件
我们需要添加的文件已经准备好了,接下来需要把这些文件添加到我们移植的MDK工程中。为了使USB的驱动更加独立,我们按原来的定义,在MDK中新建Middlewares/USB_CORE、Middlewares/USB_CLASS、Middlewares/USB_APP三个分组,把上面的文件的只读属性去掉后添加到我们的工程中,然后把相关的HAL库的驱动加到Drivers/STM32F1xx_HAL_Driver目录下,添加后的工程目录如图61.3.3所示。
图61.3.3 在MDK中添加需要的代码
为了保持USB驱动部分更少的改动,我们添加原有USB库的头文件的引用路径,结果如图61.3.4所示。
图61.3.4 在MDK中添加USB引用的头文件的路径
这时我们直接编译会报错,因为我们没有引用ST开发板的BSP文件,这时我们还需要修改相关源码以匹配我们的底层的驱动,这部分与我们开发板硬件相关,我们在程序设计的部分再讲解。
61.3.1 程序流程图
图61.3.1.1 USB虚拟串口程序流程图
我们按流程图编写的初始化顺序,在STM32注册USB内核,最后通过USB的中断和回调函数得到USB的操作状态和操作结果,主程序通过查询设定的标记变量的状态值后,在LCD上显示对应的USB操作状态。最后通过USB和电脑实现数据交互。
61.3.2 usbd_cdc_interface驱动
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码,USB虚拟串口的驱动主要包括两个文件:usbd_cdc_interface.c和usbd_cdc_interface.h。
usbd_cdc_interface.c/.h需要适配我们的硬件信息。本例中,我们需要把USB操作与我们的串口操作对应起来。为了实现我们流程图设想的功能,我们将对原例程中的这部分代码进行修改,在此之前,我们先大致了解一下需要用到的USB设备类的一些参数和操作接口。下面介绍对应接口时我们给出的是修改后的源码,详细的修改点大家可以参考对照光盘的源码即可。
USBD_CDC_ItfTypeDef USBD_CDC_fops =
{
CDC_Itf_Init,
CDC_Itf_DeInit,
CDC_Itf_Control,
CDC_Itf_Receive
};
操作描述:
CDC_Itf_Init用于初始化VCP,在初始化的时候由USB内核调用,这里我们调用函数:USBD_CDC_SetRxBuffer,设置USB接收数据缓冲区。USB虚拟串口收到的数据,会先缓存在这个buf里面,ST原来的例程同时设置了写缓冲,我们为了方便单独编写了写发送函数。
CDC_Itf_DeInit用于复位VCP,我们用不到,所以直接返回USBD_OK即可。
CDC_Itf_Control用于控制VCP的相关参数,根据cmd的不同,执行不同的操作,这里主要用到CDC_SET_LINE_CODING命令,该命令用于设置VCP的相关参数,比如波特率、数据类型(位数)、校验类型(奇偶校验)等,保存在linecoding结构体里面,在需要的时候,应用程序可以读取LineCoding结构体里面的参数,以获得当前VCP的相关信息。
CDC_Itf_Receive和VCP_DataRx,这两个函数一起,用于VCP数据接收,当STM32的USB接收到电脑端串口发送过来的数据时,由USB内核程序调用CDC_Itf_Receive,然后在该函数里面再调用VCP_DataRx函数,实现VCP的数据接收,只需要在该函数里面,将接收到的数据,保存起来即可,接收的原理同串口通信实验完全一样。
2. CDC_Itf_Init函数
CDC_Itf_Init用于初始化VCP,在初始化的时候由USB内核调用,这里我们可以编写实际的串口应用,使虚拟串口与物理串口对应上,但为了简化测试,我们没有加上这个操作,这里只为CDC设备通讯分配接收缓存:
static int8_t CDC_Itf_Init(void)
函数描述:
初始存储CDC通讯设备,这里主要包括跟开发板底层驱动相关的及CDC设备类相关的收发缓存的分配,所以在这个函数里面会调用USBD_CDC_SetRxBuffer,设置USB接收数据缓冲区。USB虚拟串口收到的数据,会先缓存在这个buf里面。
/**
* @brief 初始化 CDC
* @param 无
* @retval USB状态
* @arg USBD_OK(0) , 正常;
* @arg USBD_BUSY(1) , 忙;
* @arg USBD_FAIL(2) , 失败;
*/
static int8_t CDC_Itf_Init(void)
{
USBD_CDC_SetRxBuffer(&USBD_Device, g_usb_rx_buffer);
return USBD_OK;
}
函数返回值:
返回硬件初始化结果:USBD_OK(0):成功,其它:错误或忙。
3. CDC_Itf_Control函数
CDC_Itf_Control用于CDC设备的控制,主机可以通过这些接口配置CDC设备:
static int8_t CDC_Itf_Control(uint8_t cmd, uint8_t *pbuf, uint16_t length)
函数描述:
控制接口主要用来做设备管理和电话管理(可选),设备管理涉及到请求(request)和通知(notification),端点0一般用做请求,一般用来控制和配置设备的运行状态,而非0端点(0x82)一般用作异步事件通知,设备端通过此端点向主机端发送设备内部的一些事件,比如串口状态变化事件,电话状态改变等等
USB请求在USB器件库文档中分为三类:标准请求、特定类请求、特定厂商请求。其中标准请求由默认的控制端点0接收处理,另外两类请求由回调函数传递到特定类代码进行处理,这里的CDC_Itf_Control()函数即为回调函数最终实现者,其处理CDC类的特定请求,这些请求可以在PSTN120协议文档以及CDC120协议文档中找到。
函数形参:
形参1 cmd为控制命令,PSTN定义了三种模型:DLM(Direct Line Mode),ACM(Abstract Control Model)和TCM(Telephone Control Model),STM32的CDC设备采用的是抽象控制模型(ACM:Abstract Control Model),对这此控制指令的格式进行了定义,这些请求可以在PSTN120协议文档以及CDC120协议文档中找到,但能否实现还需要主机支持。ST文档《USB CDC类入门培训》也有对这些类进行描述,这里我们就不列举了,大家看代码实现即可。
形参2 pbuf用于需要传递数据的控制命令时使用。
形参3 length表示控制命令的传递过来的数据按字节计的数量。
/**
* @brief 控制 CDC 的设置
* @param cmd : 控制命令
* @param buf : 命令数据缓冲区/参数保存缓冲区
* @param length : 数据长度
* @retval USB状态
* @arg USBD_OK(0) , 正常;
* @arg USBD_BUSY(1) , 忙;
* @arg USBD_FAIL(2) , 失败;
*/
static int8_t CDC_Itf_Control(uint8_t cmd, uint8_t *pbuf, uint16_t length)
{
switch (cmd)
{
case CDC_SEND_ENCAPSULATED_COMMAND:
break;
case CDC_GET_ENCAPSULATED_RESPONSE:
break;
case CDC_SET_COMM_FEATURE:
break;
case CDC_GET_COMM_FEATURE:
break;
case CDC_CLEAR_COMM_FEATURE:
break;
case CDC_SET_LINE_CODING:
LineCoding.bitrate = (uint32_t) (pbuf[0] | (pbuf[1] << 8) |
(pbuf[2] << 16) | (pbuf[3] << 24));
LineCoding.format = pbuf[4];
LineCoding.paritytype = pbuf[5];
LineCoding.datatype = pbuf[6];
/* 打印配置参数 */
printf("linecoding.format:%d\r\n", LineCoding.format);
printf("linecoding.paritytype:%d\r\n", LineCoding.paritytype);
printf("linecoding.datatype:%d\r\n", LineCoding.datatype);
printf("linecoding.bitrate:%d\r\n", LineCoding.bitrate);
break;
case CDC_GET_LINE_CODING:
pbuf[0] = (uint8_t) (LineCoding.bitrate);
pbuf[1] = (uint8_t) (LineCoding.bitrate >> 8);
pbuf[2] = (uint8_t) (LineCoding.bitrate >> 16);
pbuf[3] = (uint8_t) (LineCoding.bitrate >> 24);
pbuf[4] = LineCoding.format;
pbuf[5] = LineCoding.paritytype;
pbuf[6] = LineCoding.datatype;
break;
case CDC_SET_CONTROL_LINE_STATE:
break;
case CDC_SEND_BREAK:
break;
default:
break;
}
return USBD_OK;
}
函数返回值:
返回操作结果:0(USBD_OK): 成功, 其它 : 错误或忙等状态。
4. CDC_Itf_Receive函数
CDC_Itf_Receive用于CDC数据接收处理,我们虚拟串口的数据接收通过这个接口实现。
static int8_t CDC_Itf_Receive(uint8_t *buf, uint32_t *len)
函数描述:
初始化指定编号的磁盘,磁盘所指定的存储区。
函数形参:
形参1 buf是接收数据缓冲,我们收到的数据存放在这个缓冲中。
形参2为按字节计从虚拟串口接收到的数据。
/**
* @brief CDC 数据接收函数
* @param buf : 接收数据缓冲区
* @param len : 接收到的数据长度
* @retval USB状态
* @arg USBD_OK(0) , 正常;
* @arg USBD_BUSY(1) , 忙;
* @arg USBD_FAIL(2) , 失败;
*/
static int8_t CDC_Itf_Receive(uint8_t *buf, uint32_t *len)
{
USBD_CDC_ReceivePacket(&USBD_Device);
cdc_vcp_data_rx(buf, *len);
return USBD_OK;
}
上面的cdc_vcp_data_rx(buf, *len)函数为我们编写的虚拟串口缓冲数据处理接收函数,写法与我们的串口实验相同,以全局g_usb_usart_rx_sta为接收标记和数据计数器,以“\r\n”为结束符,大家参考光盘中的代码即可。
函数返回值:
返回操作结果:0(USBD_OK): 成功, 其它 : 错误或忙等状态。
5. cdc_vcp_data_tx函数
cdc_vcp_data_tx实现USB对物理设备的读操作,其声明如下:
void cdc_vcp_data_tx(uint8_t *data, uint32_t Len)
函数描述:
用于STM32向虚拟串口发送数据。
函数形参:
形参1 data为接收的数据指针。
形参2 Len同为为按字节计的数据长度。
代码实现如下:
/**
* @brief 通过 USB 发送数据
* @param buf : 要发送的数据缓冲区
* @param len : 数据长度
* @retval 无
*/
void cdc_vcp_data_tx(uint8_t *data, uint32_t Len)
{
USBD_CDC_SetTxBuffer(&USBD_Device, data, Len);
USBD_CDC_TransmitPacket(&USBD_Device);
delay_ms(CDC_POLLING_INTERVAL);
}
为了方便实现打参数格式化和测试打印,我们编写了usb_printf这个接口用于实际的写测试,这是一个变参函数,与printf的功能类似, 本质上还是调用 cdc_vcp_data_tx接口,主要还是调用了USBD下的CDC类的发送操作接口,大家参考我们的源代码即可。
函数返回值:
无。
usbd_cdc的代码就讲到这里,usbd_conf.c的配置和USB读卡器的章节一致,我们就不重复了。
2. main.c代码
在main.c就比较简单了,按照我们的流程图的思路编写即可,我们初始化探按键,LCD和LED、SRAM等外设辅助程序显示,同样地在while循环之前调用我们之前编写的usbd_port_config接口以保证每次复位后USB都能重连成功。大家也可以尝试其它的方法。
最后,我们编写的main函数如下:
USBD_HandleTypeDef USBD_Device; /* USB Device处理结构体 */
extern volatile uint8_t g_device_state; /* USB连接 情况 */
int main(void)
{
uint16_t len;
uint16_t times = 0;
uint8_t usbstatus = 0;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
sram_init(); /* SRAM初始化 */
my_mem_init(SRAMIN); /* 初始化内部SRAM内存池 */
my_mem_init(SRAMEX); /* 初始化外部SRAM内存池 */
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "USB Virtual USART TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
/* 提示USB开始连接 */
lcd_show_string(30, 110, 200, 16, 16, "USB Connecting...", RED);
usbd_port_config(0); /* USB先断开 */
delay_ms(500);
usbd_port_config(1); /* USB再次连接 */
delay_ms(500);
USBD_Init(&USBD_Device, &VCP_Desc, 0);
USBD_RegisterClass(&USBD_Device, USBD_CDC_CLASS);
USBD_CDC_RegisterInterface(&USBD_Device, &USBD_CDC_fops);
USBD_Start(&USBD_Device);
while (1)
{
if (usbstatus != g_device_state) /* USB连接状态发生了改变 */
{
usbstatus = g_device_state; /* 记录新的状态 */
if (usbstatus == 1)
{/* 提示USB连接成功 */
lcd_show_string(30, 110, 200, 16, 16, "USB Connected ", RED);
LED1(0); /* 绿灯亮 */
}
else
{/* 提示USB断开 */
lcd_show_string(30, 110, 200, 16, 16, "USB disConnected ", RED);
LED1(1); /* 绿灯灭 */
}
}
if (g_usb_usart_rx_sta & 0x8000)
{
len = g_usb_usart_rx_sta & 0x3FFF; /* 得到此次接收到的数据长度 */
usb_printf("\r\n您发送的消息长度为:%d\r\n\r\n", len);
cdc_vcp_data_tx(g_usb_usart_rx_buffer, len);;
usb_printf("\r\n\r\n"); /* 插入换行 */
g_usb_usart_rx_sta = 0;
}
else
{
times++;
if (times % 5000 == 0)
{
usb_printf("\r\nSTM32开发板USB虚拟串口实验\r\n");
usb_printf("正点原子@ALIENTEK\r\n\r\n");
}
if (times % 200 == 0)usb_printf("请输入数据,以回车键结束\r\n");
if (times % 30 == 0)
{
LED0_TOGGLE(); /* 闪烁LED,提示系统正在运行 */
}
delay_ms(10);
}
}
}
61.4 下载验证
本例程的测试,需要在电脑上先安装ST提供的USB虚拟串口驱动软件,该软件可以在STM32论坛搜索“STM32 Virtual COM Port Driver”获取最新的虚拟串口版本。我们光盘资料提供了V1.5版本的压缩包,位于A盘6,软件资料1,软件STM32 USB虚拟串口驱动en.stsw-stm32102.zip,我们解压这个文件可以看到有多个版本的软件可选,如图61.4.1所示:
图61.4.1选择适合自己电脑的软件版本安装
由于我们电脑是系统是Win10 64位的系统,所以我们安装win7以后的版本,这里我们选择VCP_V1.5.0_Setup_W8_x64_64bits.exe这个版本,安装过程比较简单,我们填写一些简单的信息,一直点下一步安装到默认路径即可,安装完成后界面如图61.4.2所示:
图61.4.2 STM32 VCOM驱动安装完成
安装完驱动后我们就可以开始实验了。在代码编译成功之后,我们下载代码到STM32开发板上,然后将USB数据线,插入USB_SLAVE口(此时要检查P9排针是处的PA11、PA12是否与USB_D+、USB_D-正确连接),通过USB_Slave连接电脑和开发板,此时电脑会提示找到新硬件,并自动安装驱动。不过,如果自动安装不成功(有惊叹号),如图63.4.3所示:
图61.4.3自动安装驱动失败
此时,我们可手动选择驱动(以WIN7为例),进行安装,在如图61.4.3所示的条目上面,右键→更新驱动程序软件→浏览计算机以查找驱动程序软件→浏览,选择STM32虚拟串口的驱动的路径为:C:\ProgramFiles(x86)\STMicroelectronics\Software \Virtual comport driver,上述的文件夹下同样有对应对应的WIN7和WIN8版本,如果是WIN7以后的windows版本我们选择WIN8就可以了。然后点击下一步,即可完成安装。安装完成后,可以看到设备管理器里面多出了一个STM32的虚拟串口,这时我们就可以开始正常测试了,效果如图63.4.4。
图63.4.4 发现STM32USB虚拟串口
如图63.4.4,STM32通过USB虚拟的串口,被电脑识别了,端口号为:COM7(不同电脑有差异),字符串名字为:STMicroelectronics Virtual COM Port(固定)。此时,开发板的DS1常亮,同时,开发板的LCD显示USBConnected,如图63.4.5所示:
图63.4.5 USB虚拟串口连接成功
正确安装好虚拟串口驱动后,我们打开XCOM进行测试,选择电脑识别到的虚拟串口号,我们这里是COM7(需根据自己的电脑识别到的串口号选择),并打开串口(注意:波特率可以随意设置),我们例程的代码以接收到的数据中的回车换行为接收结束符,故测试时串口助手必须勾选:发送新行,STM32才会把收到的数据回发给USB口,测试结果如图63.4.6所示:
图63.4.6 STM32虚拟串口通信测试
可以看到,我们的串口调试助手,收到了来自STM32开发板的数据,同时,按发送按钮(串口助手必须勾选:发送新行),也可以收到电脑发送给STM32的数据(原样返回),说明我们的实验是成功的。实验现象同串口通讯实验完全一样。
至此,USB虚拟串口实验就完成了,通过本实验,我们就可以利用STM32的USB,直接和电脑进行数据互传了,具有广泛的应用前景。