STM32F4驱动USB实现虚拟串口
实现目的
使用Dap-link和stlink的时候,就发现这些仿真器上并没有USB转TTL芯片,就可以实现USB转串口,实现虚拟串口,非常方便。这里实测得出,使用USB虚拟串口,可以轻松达到921600波特率,接近1M/s,因为这个虚拟串口实际就是USB通讯,使用USB通讯,模拟COM类通讯端口协议,实现串口通讯。
这个功能主要用于实现单片机通过USB线同上位机通讯,实现速率高(1M/s),稳定性强(USB线+差分信号),操作简单(串口通讯效果)的效果。
最终实现了单片机同上位机进行串口通讯,并编写了类似于HAL库串口通讯的USB串口通信操作函数,包括数据发送,printf发送,堵塞接收,中断接收等函数
注意,此方案单片机作为USB从机,同上位机(主机)通讯,不能够使用USB同使用了USB串口的其他设备通讯,因为他们也是从机。
cubemx初始化
STM32F407VET6+CubeMx+MDK5
系统时钟初始化
修改debug方式
GPIO配置
USB外设初始化
啥都不用改,把中断打开记得
工程配置
keil5修改
初始化完成
实现HAL库uart通讯功能
简单使用系统函数 uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len)
进行通讯
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
led_GPIO_Port->ODR^=led_Pin;
CDC_Transmit_FS(Tx_Buffer,strlen((char*)Tx_Buffer));
HAL_Delay(100);
}
设计USB_CDC_printf格式化输出函数
/**
* @brief USB虚拟串口格式化输出printf实现
* @param 格式化输入
* @retval 无
*/
void usb_printf(const char *format, ...)
{
va_list args;
uint32_t length;
va_start(args, format);
length = vsnprintf((char *)UserTxBufferFS, APP_TX_DATA_SIZE, (char *)format, args);
va_end(args);
CDC_Transmit_FS(UserTxBufferFS, length);
}
USB虚拟串口主要使用CDC通讯协议,在usbd_cdc_if.c文件中,有相关函数描述,其中数据中断接收回调函数需要重点关注
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
/* USER CODE BEGIN 6 */
/* 定义外部变量 */
extern uint16_t Rx_Date_Num,RX_goal_num;
extern uint8_t UserRxBuffer[APP_RX_DATA_SIZE];
extern uint8_t Rx_status;
extern uint8_t* p;
/* 保存接收到的数据 */
Rx_date_save(Buf,UserRxBuffer,*Len);
/* 如果接收到的数据量小于或等于缓冲区大小,增加接收数据的数量 */
if(Rx_Date_Num<=APP_RX_DATA_SIZE)
Rx_Date_Num+=*Len;
/* 如果接收到的数据量大于缓冲区大小,将接收数据的数量设置为缓冲区大小 */
else
Rx_Date_Num=APP_RX_DATA_SIZE;
/* 如果接收状态为0 */
if(Rx_status==0)
{
/* 如果接收到的数据量大于或等于目标数据量 */
if(Rx_Date_Num>=RX_goal_num)
{
/* 将用户接收缓冲区的数据复制到p指向的位置 */
Rx_buffer_copy(p,UserRxBuffer,RX_goal_num);
/* 减少接收数据的数量 */
Rx_Date_Num-=RX_goal_num;
/* 将接收状态设置为1 */
Rx_status=1;
}
}
/* 设置USB设备的接收缓冲区 */
USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
/* 接收USB数据包 */
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
/* 返回操作结果 */
return (USBD_OK);
/* USER CODE END 6 */
}
虚拟串口的接收方式是覆盖式的,相关缓存区大小由宏定义 APP_RX_DATA_SIZE 确定
比方说,本次接受了8个字节数据,分别是,“12345678”,然后发送了4个字节数据,分别是“abcd”,则缓存区数据变为“abcd5678”,原数据会被覆盖
这样是不利于我们接收数据的,如果我要接收128个字节的数据,为防止数据丢失,我会设置256个字节宽度的缓存区,原系统的接受到的数据会被及时转存到用户自定义的缓存区内,随取随用。
所以代码里我们定义了uint8_t UserRxBuffer[APP_RX_DATA_SIZE];
用于存储用户想要接收的信息,放置被覆盖,并定义了相关函数,操作读取数据
堵塞型数据接收函数*
/**
* @brief 这个函数用于接收USB虚拟串口的数据
* @param Rx_Buffer: 接收缓冲区
* @param num: 需要接收的数据数量
* @param overtime: 超时时间
* @retval 如果接收成功,返回1,如果超时,返回0
*/
uint8_t usb_vbc_Receive(uint8_t* Rx_Buffer,uint16_t num,uint32_t overtime)
{
uint32_t time=0;
overtime=overtime/2;
if(Rx_Date_Num>=num)
{
Rx_buffer_copy(Rx_Buffer,UserRxBuffer,num);
Rx_Date_Num-=num;
return 1;
}
else
{
while(1)
{
if(Rx_Date_Num>=num)
{
Rx_buffer_copy(Rx_Buffer,UserRxBuffer,num);
Rx_Date_Num-=num;
return 1;
}
else
time++;
if(time>overtime)
return 0;
HAL_Delay(1);
}
}
}
中断型数据接收函数
内容较少,仅改变几个全局标志位的值,主要操作内容在中断回调函数里
/**
* @brief 开启接收数据,不堵塞,完成接收任务后,全局变量Rx_status置一,否则为0
* @param Rx_Buffer: 接收缓冲区
* @param num: 需要接收的数据数量
* @retval 无
*/
void usb_vbc_Receive_It(uint8_t* Rx_Buffer,uint16_t num)
{
p=Rx_Buffer;
RX_goal_num=num;
Rx_status=0;
}
中断回调函数内的操作
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
/* USER CODE BEGIN 6 */
/* 定义外部变量 */
extern uint16_t Rx_Date_Num,RX_goal_num;
extern uint8_t UserRxBuffer[APP_RX_DATA_SIZE];
extern uint8_t Rx_status;
extern uint8_t* p;
/* 保存接收到的数据 */
Rx_date_save(Buf,UserRxBuffer,*Len);
/* 如果接收到的数据量小于或等于缓冲区大小,增加接收数据的数量 */
if(Rx_Date_Num<=APP_RX_DATA_SIZE)
Rx_Date_Num+=*Len;
/* 如果接收到的数据量大于缓冲区大小,将接收数据的数量设置为缓冲区大小 */
else
Rx_Date_Num=APP_RX_DATA_SIZE;
/* 如果接收状态为0 */
if(Rx_status==0)
{
/* 如果接收到的数据量大于或等于目标数据量 */
if(Rx_Date_Num>=RX_goal_num)
{
/* 将用户接收缓冲区的数据复制到p指向的位置 */
Rx_buffer_copy(p,UserRxBuffer,RX_goal_num);
/* 减少接收数据的数量 */
Rx_Date_Num-=RX_goal_num;
/* 将接收状态设置为1 */
Rx_status=1;
}
}
/* 设置USB设备的接收缓冲区 */
USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
/* 接收USB数据包 */
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
/* 返回操作结果 */
return (USBD_OK);
/* USER CODE END 6 */
}
其他相关数据操作函数
/**
* @brief 这个函数用于复制接收缓冲区的内容,并将缓存区数据移位
* @param Buffer_get: 获取缓冲区
* @param Buffer_put: 放置缓冲区
* @param num: 要复制的元素数量
* @retval 无
*/
void Rx_buffer_copy(uint8_t* Buffer_get,uint8_t* Buffer_put,uint16_t num)
{
uint16_t i=0;
for(i=0;i<num;i++)//复制数据
{
Buffer_get[i]=Buffer_put[i];
}
for(i=0;i<Rx_Date_Num-num;i++)//剩余数据移位
{
Buffer_put[i]=Buffer_put[i+num];
}
}
/**
* @brief 这个函数用于将一个数组的内容复制到另一个数组中,而不会丢失接收数组中的原始数据
* @param src: 源数组
* @param dest: 目标数组
* @param n: 源数组中的元素数量
* @retval 无
*/
void Rx_date_save(uint8_t* src, uint8_t* dest, uint16_t n)
{
uint16_t i=0,num=Rx_Date_Num;
if(num+n>APP_RX_DATA_SIZE)
return;//超出缓存区大小,这里直接停止。
for(i=0;i<n;i++)
dest[i+num]=src[i];
}
/**
* @brief 这个函数用于获取USB接收缓存区的数据数量
* @param 无
* @retval 返回接收的数据数量
*/
uint16_t usb_Rx_Get_Num(void)
{
return Rx_Date_Num;
}
main函数
int main(void)
{
/* USER CODE BEGIN 1 */
uint8_t Rx_Buffer[32];
uint8_t Tx_Buffer[32]="灵遨老六\n";
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USB_DEVICE_Init();
/* USER CODE BEGIN 2 */
usb_vbc_Receive_It(Rx_Buffer,16);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
led_GPIO_Port->ODR^=led_Pin;
// time=HAL_GetTick();
// CDC_Transmit_FS((uint8_t*)str, strlen(str));
// if(usb_vbc_Receive(Rx_Buffer,16,500)==0)
// usb_printf("超时:%d\n",HAL_GetTick()-time);
// else
// CDC_Transmit_FS((uint8_t*)Rx_Buffer, 16);
if(Rx_status==1)
{
CDC_Transmit_FS(Rx_Buffer, 16);
usb_vbc_Receive_It(Rx_Buffer,16);
}
// CDC_Transmit_FS(Tx_Buffer,strlen((char*)Tx_Buffer));
HAL_Delay(100);
}
/* USER CODE END 3 */
}
总结
使用USB虚拟串口,用起来很爽,波特率能跑很高,主要可以应用在同ROS主机通讯上;具体细致学习,可以参考开源Dap-link的代码。
另外想使用DMA的话,F4的还没实现,H7的可以,速度应该可以跑很高。