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配置

按键led配置
按键led配置

USB外设初始化

全速USB外设初始化
全速USB外设初始化

啥都不用改,把中断打开记得

中断配置
中断配置

工程配置

修改时钟
修改时钟

配置工程
配置工程

目录修改
目录修改

keil5修改

勾选Micolib
勾选Micolib

初始化完成

实现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的代码。

dap-link

另外想使用DMA的话,F4的还没实现,H7的可以,速度应该可以跑很高。

欢迎访问我的博客

posted @ 2024-06-30 13:16  Sparkle-now  阅读(9)  评论(1编辑  收藏  举报