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; }
遇到的问题
- Hard Fault崩溃:传入的是函数栈上的局部分配内存,而
CDC_Transmit_FS是异步执行——函数返回时数据可能还没完成传输,当USBD_CDC_TransmitPacket实际使用缓冲区时,栈内存已被释放,导致崩溃。 - 连续发送失效:改成全局缓冲区后单次发送成功,但连续调用
CDC_Transmit_FS时,第二条消息会覆盖全局缓冲区,因为前一次传输还没完成。
我的思路疑问
- 考虑用全局缓冲区排队待发送数据,但不清楚
CDC_Init_FS中USBD_CDC_SetTxBuffer(&hUsbDeviceFS, UserTxBufferFS, 0)的长度是否应该设为APP_TX_DATA_SIZE而非0;也不理解为何CDC_Transmit_FS中要反复调用USBD_CDC_SetTxBuffer。 - 尝试给缓冲区追加数据排队,但不知道何时传输完成以复用缓冲区,同时存在并发写入的数据丢失风险。
可行解决方案
方案一:环形缓冲区+传输完成回调(推荐)
这是最通用的解决方式,能处理任意数量的连续发送请求:
- 定义全局环形缓冲区:
#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; - 实现环形缓冲区操作函数:
// 计算环形缓冲区可用空间 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; } - 修改
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; } - 利用传输完成回调触发下一次发送:
在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(); } }
方案二:双缓冲区切换(简化版)
如果发送数据量不大,双缓冲区足够满足需求:
- 定义双缓冲区:
#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; - 修改
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; } - 传输完成回调切换缓冲区:
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
相关产品推荐
相关产品推荐

