You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

STM32F4 CubeIDE中CDC_Transmit_FS的正确实现及问题解决

STM32F4 USB CDC 异步发送问题的解决方案

我在CubeIDE中创建了针对STM32F4的项目,通过USB CDC与主机通信,使用自动生成的CDC_Transmit_FS函数发送消息,代码如下:

uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len)
{
  uint8_t result = USBD_OK;
  /* USER CODE BEGIN 7 */
  USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData;
  if (hcdc->TxState != 0){
    return USBD_BUSY;
  }
  USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len);     // 1
  result = USBD_CDC_TransmitPacket(&hUsbDeviceFS);   // 2
  /* USER CODE END 7 */
  return result;
}

遇到的问题

  1. Hard Fault崩溃:传入的是函数栈上的局部分配内存,而CDC_Transmit_FS是异步执行——函数返回时数据可能还没完成传输,当USBD_CDC_TransmitPacket实际使用缓冲区时,栈内存已被释放,导致崩溃。
  2. 连续发送失效:改成全局缓冲区后单次发送成功,但连续调用CDC_Transmit_FS时,第二条消息会覆盖全局缓冲区,因为前一次传输还没完成。

我的思路疑问

  • 考虑用全局缓冲区排队待发送数据,但不清楚CDC_Init_FS中USBD_CDC_SetTxBuffer(&hUsbDeviceFS, UserTxBufferFS, 0)的长度是否应该设为APP_TX_DATA_SIZE而非0;也不理解为何CDC_Transmit_FS中要反复调用USBD_CDC_SetTxBuffer。
  • 尝试给缓冲区追加数据排队,但不知道何时传输完成以复用缓冲区,同时存在并发写入的数据丢失风险。

可行解决方案

方案一:环形缓冲区+传输完成回调(推荐)

这是最通用的解决方式,能处理任意数量的连续发送请求:

  1. 定义全局环形缓冲区:
    #define APP_TX_DATA_SIZE 512
    uint8_t UserTxRingBuffer[APP_TX_DATA_SIZE];
    volatile uint16_t tx_read_ptr = 0;
    volatile uint16_t tx_write_ptr = 0;
    volatile uint8_t tx_in_progress = 0;
    
  2. 实现环形缓冲区操作函数:
    // 计算环形缓冲区可用空间
    uint16_t tx_buffer_available(void) {
      if (tx_write_ptr >= tx_read_ptr) {
        return APP_TX_DATA_SIZE - (tx_write_ptr - tx_read_ptr) - 1;
      } else {
        return tx_read_ptr - tx_write_ptr - 1;
      }
    }
    
    // 向环形缓冲区写入数据
    uint16_t tx_buffer_write(uint8_t* data, uint16_t len) {
      uint16_t writable = tx_buffer_available();
      if (len > writable) len = writable;
      if (len == 0) return 0;
    
      __disable_irq(); // 临界区保护,防止USB中断打断
      // 处理缓冲区尾部空间不足的情况
      if (tx_write_ptr + len > APP_TX_DATA_SIZE) {
        uint16_t tail_len = APP_TX_DATA_SIZE - tx_write_ptr;
        memcpy(&UserTxRingBuffer[tx_write_ptr], data, tail_len);
        memcpy(UserTxRingBuffer, data + tail_len, len - tail_len);
        tx_write_ptr = len - tail_len;
      } else {
        memcpy(&UserTxRingBuffer[tx_write_ptr], data, len);
        tx_write_ptr += len;
      }
      __enable_irq();
      return len;
    }
    
  3. 修改CDC_Transmit_FS:
    不再直接发送,而是把数据写入环形缓冲区,若当前无传输任务则启动第一次发送:
    uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len)
    {
      uint16_t written = tx_buffer_write(Buf, Len);
      if (written != Len) {
        return USBD_FAIL; // 缓冲区满,可根据需求处理
      }
    
      if (!tx_in_progress) {
        tx_in_progress = 1;
        uint16_t send_len;
        __disable_irq();
        if (tx_write_ptr >= tx_read_ptr) {
          send_len = tx_write_ptr - tx_read_ptr;
        } else {
          send_len = APP_TX_DATA_SIZE - tx_read_ptr;
        }
        USBD_CDC_SetTxBuffer(&hUsbDeviceFS, &UserTxRingBuffer[tx_read_ptr], send_len);
        __enable_irq();
        USBD_CDC_TransmitPacket(&hUsbDeviceFS);
      }
      return USBD_OK;
    }
    
  4. 利用传输完成回调触发下一次发送:
    在CDC_TransmitCplt_FS的用户代码区添加逻辑:
    void CDC_TransmitCplt_FS(uint8_t *Buf, uint32_t *Len)
    {
      uint8_t result = USBD_OK;
      /* USER CODE BEGIN 13 */
      __disable_irq();
      tx_read_ptr += *Len;
      if (tx_read_ptr >= APP_TX_DATA_SIZE) {
        tx_read_ptr = 0;
      }
      // 检查是否还有数据要发送
      if (tx_read_ptr != tx_write_ptr) {
        uint16_t send_len;
        if (tx_write_ptr >= tx_read_ptr) {
          send_len = tx_write_ptr - tx_read_ptr;
        } else {
          send_len = APP_TX_DATA_SIZE - tx_read_ptr;
        }
        USBD_CDC_SetTxBuffer(&hUsbDeviceFS, &UserTxRingBuffer[tx_read_ptr], send_len);
        result = USBD_CDC_TransmitPacket(&hUsbDeviceFS);
      } else {
        tx_in_progress = 0;
      }
      __enable_irq();
      /* USER CODE END 13 */
      if(result != USBD_OK){
        Error_Handler();
      }
    }
    

方案二:双缓冲区切换(简化版)

如果发送数据量不大,双缓冲区足够满足需求:

  1. 定义双缓冲区:
    #define APP_TX_DATA_SIZE 512
    uint8_t TxBuf1[APP_TX_DATA_SIZE];
    uint8_t TxBuf2[APP_TX_DATA_SIZE];
    volatile uint8_t current_tx_buf = 0; // 0:使用Buf1,1:使用Buf2
    volatile uint8_t tx_in_progress = 0;
    
  2. 修改CDC_Transmit_FS:
    uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len)
    {
      if (Len > APP_TX_DATA_SIZE) {
        return USBD_FAIL; // 数据超过缓冲区大小
      }
    
      __disable_irq();
      uint8_t* target_buf = (current_tx_buf == 0) ? TxBuf2 : TxBuf1;
      memcpy(target_buf, Buf, Len);
      __enable_irq();
    
      if (!tx_in_progress) {
        tx_in_progress = 1;
        current_tx_buf = (current_tx_buf == 0) ? 0 : 1;
        USBD_CDC_SetTxBuffer(&hUsbDeviceFS, (current_tx_buf == 0) ? TxBuf1 : TxBuf2, Len);
        USBD_CDC_TransmitPacket(&hUsbDeviceFS);
      } else {
        static uint16_t next_tx_len = 0;
        next_tx_len = Len;
      }
      return USBD_OK;
    }
    
  3. 传输完成回调切换缓冲区:
    void CDC_TransmitCplt_FS(uint8_t *Buf, uint32_t *Len)
    {
      uint8_t result = USBD_OK;
      /* USER CODE BEGIN 13 */
      static uint16_t next_tx_len = 0;
      if (next_tx_len > 0) {
        current_tx_buf = (current_tx_buf == 0) ? 1 : 0;
        USBD_CDC_SetTxBuffer(&hUsbDeviceFS, (current_tx_buf == 0) ? TxBuf1 : TxBuf2, next_tx_len);
        result = USBD_CDC_TransmitPacket(&hUsbDeviceFS);
        next_tx_len = 0;
      } else {
        tx_in_progress = 0;
      }
      /* USER CODE END 13 */
      if(result != USBD_OK){
        Error_Handler();
      }
    }
    

关于初始化的疑问解答

  • CDC_Init_FS中USBD_CDC_SetTxBuffer(&hUsbDeviceFS, UserTxBufferFS, 0)的长度设为0是合理的,这只是初始化一个空的默认缓冲区,后续每次发送都会通过USBD_CDC_SetTxBuffer重新设置要发送的缓冲区和长度,这是USB CDC驱动的标准流程,确保每次传输使用正确的数据缓冲区。
  • 反复调用USBD_CDC_SetTxBuffer是因为每次发送的数据缓冲区和长度都可能不同,需要告诉驱动当前要传输的是哪块内存、多大的数据。

内容的提问来源于stack exchange,提问作者An Hoa

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 00:17:16