实现目的
使用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;/* 如果接收到的数据量大于缓冲区大小,将接收数据的数量设置为缓冲区大小 */elseRx_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;}elsetime++;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;/* 如果接收到的数据量大于缓冲区大小,将接收数据的数量设置为缓冲区大小 */elseRx_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的代码。
dap-link
另外想使用DMA的话,F4的还没实现,H7的可以,速度应该可以跑很高。