问题 :
某客户工程师在其产品的设计中,使用了 STM32F205VET6。据其工程师讲述:他使用 ST 的 USB 固件库中的 VCP 例程来实现虚拟串口的功能,但是他发现虚拟串口一次输出的数据(从串口到上位机)如果超过 2Kbytes 就会造成数据丢失,只输出尾部的 2Kbytes。客户工程师检查代码发现 USB 的 FIFO 大小由宏定义 APP_RX_DATA_SIZE 决定,而 APP_RX_DATA_SIZE 的大小刚好为 2Kbytes。所以他认为此 FIFO设计太小而造成的,于是他将 FIFO 的大小改成 5Kbyte,不过修改后并不能解决问题。 调研 :
1. 打开“STM32F105/7, STM32F2 and STM32F4 USB on-the-go Host and device library (UM1021)”库里边的 VCP 例程,位于“...STM32_USB-Host-
Device_Lib_V2.1.0ProjectUSB_Device_ExamplesVCP”中。对其进行测试,并没有出现所说的问题,APP_RX_DATA_SIZE 的大小仍然为 2Kbytes,不管传输的数据是 2Kbytes 还是 5Kbytes 甚至是25Kbytes,完全没有问题。
2. 了解客户程序 UART 所设置的波特率,为 115200,与原 VCP 例程一致。USB 采用的是 Full Speed,全速 USB 总线的帧周期为 1ms。
3. 在 u***d_conf.h 中可以看到 APP_RX_DATA_SIZE 的定义在这里,为 2048,它定义了 APP_Rx_Buffer的大小。APP_Rx_Buffer 其实是一个循环缓冲区APP_Rx_ptr_in 指明了其数据进来的位置,当USART 接收到数据时,将数据存储于 APP_Rx_ptr_in 指定的位置;APP_Rx_ptr_out 指明其数据取出的位置,当 USB 到 FIFO 中取出数据时,起始地址由 APP_Rx_ptr_out 决定。
4. 打开 VCP 项目,观察其程序
通信部分。当 USART 每接收到字节时,进入 EVAL_COM_IRQHandler 函数,调用 VCP_Tx(0,0)函数,将收到的字节存储于 APP_Rx_Buffer[APP_Rx_ptr_in]中,在APP_Rx_Buffer 中的位置由 APP_Rx_ptr_in 指定。在 VCP_Tx 函数中,
if(APP_Rx_ptr_in == APP_RX_DATA_SIZE)
{
APP_Rx_ptr_in = 0;
}
可以看到当 APP_Rx_ptr_in 达到 APP_RX_DATA_SIZE 时,将其置 0,也就是在循环缓冲区中绕了一圈回到缓冲区起始地址。
5. 再来看 APP_Rx_Buffer 是如何被 USB 取走,并送到上位机的。在 u***d_cdc_core.c 中,我们在u***d_cdc_SOF 函数中看到:
if (FrameCount++ == CDC_IN_FRAME_INTERVAL)
{
FrameCount = 0;
Handle_USBAsynchXfer(pdev);
}
可以看到,USB 每 CDC_IN_FRAME_INTERVAL 个帧调用一次 Handle_USBAsynchXfer 到 APP_Rx_Buffer中去取数据。CDC_IN_FRAME_INTERVAL 同样定义在 u***d_conf.h 中,全速的时候,其值为 5。在定义的这边,我们可以看到:
APP_RX_DATA_SIZE*8/MAX_BAUDARATE*1000 should be > CDC_IN_FRAME_INTERVAL
其目的是在于告诉我们 APP_RX_DATA_SIZE、MAX_BAUDARATE 和 CDC_IN_FRAME_INTERVAL 的关系,以保证 APP_Rx_Buffer 是够用的。
6. 接着看 Handle_USBAsynchXfer 函数,同样,我们可以看到:
if (APP_Rx_ptr_out == APP_RX_DATA_SIZE)
{
APP_Rx_ptr_out = 0;
}
也就是当 APP_Rx_ptr_out 达到 APP_RX_DATA_SIZE 时,将其置 0,也就是在循环缓冲区中绕了一圈回到缓冲区起始地址。
if (APP_Rx_prt_out == APP_Rx_ptr_in)
{
USB_Tx_State = 0;
return;
}
当 APP_Rx_prt_out 赶上 APP_Rx_ptr_in 时,证明 Buffer 里边的数据已经发送完毕,返回。再往下看:
if (APP_Rx_ptr_out > APP_Rx_ptr_in) /* rollback */
{
APP_Rx_length = APP_RX_DATA_SIZE - APP_Rx_ptr_out; //①
}
else
{
APP_Rx_length = APP_Rx_ptr_in - APP_Rx_ptr_out; //②
}
第① 种情况为 APP_Rx_ptr_out 比 APP_Rx_ptr_in 大,也就是说 APP_Rx_ptr_in 已经绕了一圈,而APP_Rx_ptr_out 还没有绕一圈,比如下面情况:
这种情况下:APP_Rx_length 设置为 APP_Rx_ptr_out 当前圈里还剩下数据长度;
第② 种情况为 APP_Rx_ptr_in 比 APP_Rx_ptr_out 大,也就是 APP_Rx_ptr_in 和 APP_Rx_ptr_out 处于同一圈,于是数据情况比如下面情况:
这种情况下:APP_Rx_length 设置为 APP_Rx_ptr_in 减去 APP_Rx_ptr_out,也就是当前所有数据长度。
程序的后面就是对 USB 包的设置,然后发送数据。
7. 结合 EVAL_COM_IRQHandler 和 Handle_USBAsynchXfer 函数对 APP_Rx_Buffer 的处理,我们可以发现,有一种情况在程序中是没有做处理的:当 APP_Rx_ptr_in 绕了一圈回来,并追上 APP_Rx_ptr_out 时,APP_Rx_Buffer 的数据已满,这个时候,若 USART 继续接收到数据,APP_Rx_ptr_in 指针继续增长,就会造成数据溢出,新来的数据冲掉还没被 USB 取走的旧数据。但是,需要注意的一点是,此 USB库的 VCP 例程是实时的,也就是 USART 收进来,USB 会取走送到上位机。而 USB 将数据送往上位机的速率是肯定大于 USART 接收数据的速率的。也就是说,这个例程可以保证“当 APP_Rx_ptr_in 绕了一圈回来,并追上 APP_Rx_ptr_out 时,APP_Rx_Buffer 的数据已满,这个时候,若 USART 继续接收到数据,APP_Rx_ptr_in 指针继续增长,就会造成数据溢出,新来的数据冲掉还没被 USB 取走的旧数据。”这种情况不会发生!
之前所分析的:
APP_RX_DATA_SIZE*8/MAX_BAUDARATE*1000 should be > CDC_IN_FRAME_INTERVAL
这个要求,正是保证 APP_Rx_Buffer 安全的重要条件。
8. 至此,我们怀疑客户并不是照搬 VCP 例程,而是对其做了修改。拜访客户,了解到确实如此。客户由于其应用需要,将 VCP 程序拆成两部分,先是用 USART 把所有的数据接收进来,放到 RAM 中,然后再将数据送到 APP_Rx_Buffer,由 USB 将数据送往上位机。这样,问题就来了,由于客户 USART先从外部接收到并保存于 RAM 的数据大于 5Kbytes,而 CPU 将 RAM 中的数据搬往 APP_Rx_Buffer 的速率远大于 USB 将 APP_Rx_Buffer 送往上位机的速率。这样就造成了 APP_Rx_ptr_in 绕了一圈回来追上 APP_Rx_ptr_out 并造成溢出的情况,甚至可能是 APP_Rx_ptr_in 绕了几圈,而APP_Rx_ptr_out 却还未开始动,因为 USB 每 CDC_IN_FRAME_INTERVAL*1ms 才送一次数据。
9. 建议客户修改其程序,在将数据从 RAM 中搬往 APP_Rx_Buffer 的时候,不能采用原 VCP 例程中USART 一样的操作方式,也就是存一个字节到 APP_Rx_Buffer,APP_Rx_ptr_in 指针加 1,并绕圈。
新的程序需要在此基础上判断,当 APP_Rx_ptr_in 指针加 1 后,若等于 APP_Rx_ptr_out,置
“APP_Rx_Buffer 满”标志位,并停止将 RAM 中的数据搬往 APP_Rx_Buffer。等 USB 从
APP_Rx_Buffer 中取走数据,再清“APP_Rx_Buffer 满”标志位,允许将 RAM 中的数据继续搬往APP_Rx_Buffer。
10. 客户修改程序,问题解决。
结论 :
修改 VCP 例程时,没有对 APP_Rx_Buffer 的操作有足够的了解,造成在其特定应用中产生了数据溢出问题。
处理 :
在将数据存入 APP_Rx_Buffer 时,对 APP_Rx_ptr_in 指针在循环缓冲区中绕一圈回来后,是否会追上APP_Rx_ptr_out 指针进行监控,以避免数据溢出。
建议 :
在对例程进行修改程序以供自己的应用来使用的时候,我们不仅需要把接口看清楚,更需要把程序的流程看明白。