usbhost驱动相关疑问与调试记录

描述

前言

rtt 里的 usbhost 驱动有问题这是众所周知的事情了,很多人在论坛上提问,也出现了各种解决方案。这里做个汇总,同时把我最终的解决方法说一下。

平台环境:STM32F429 正点原子阿波罗

usbhost 驱动相关疑问

第一个疑问

https://club.rt-thread.org/ask/question/430499.html

关于这里的延时,好像 stm32 官方的某个手册或者 usb 规范里有讲,因为这里 https://bbs.21ic.com/icview-106567-1-1.html 也提到了这个 1ms 延时是必需的。

我曾经去掉过这个延时,去掉是有严重问题的。识别不出 U盘还是小事儿,还可能严重的搞坏系统,这一点儿下面细讲。

第二个疑问

https://club.rt-thread.org/ask/question/425072.html

出现死循环的原因只有一个,usb 控制器出现 nak 并且自己不可恢复。原驱动中相关代码如下:

if (HAL_HCD_HC_GetState(&stm32_hhcd_fs, pipe->pipe_index) == HC_NAK)
{
   RT_DEBUG_LOG(RT_DEBUG_USB, ("nak\n"));
   if (pipe->ep.bmAttributes == USB_EP_ATTR_INT)
   {
       rt_thread_delay((pipe->ep.bInterval * RT_TICK_PER_SECOND / 1000) > 0 ? (pipe->ep.bInterval * RT_TICK_PER_SECOND / 1000) : 1);
   }
   HAL_HCD_HC_Halt(&stm32_hhcd_fs, pipe->pipe_index);
   HAL_HCD_HC_Init(&stm32_hhcd_fs,
                   pipe->pipe_index,
                   pipe->ep.bEndpointAddress,
                   pipe->inst->address,
                   USB_OTG_SPEED_FULL,
                   pipe->ep.bmAttributes,
                   pipe->ep.wMaxPacketSize);
   continue;
}

即便这里有初始化操作,但是实际上并不能恢复,也不能 continue 实现重新提交请求。

其它疑问

不识别,枚举设备失败,无法挂载。。。
其它各种问题都和 drv_usbh.c 的 `drv_pipe_xfer` 函数有千丝万缕的联系。

调试记录

删 `drv_pipe_xfer` 中的延时

这个延时应该是硬件的硬性要求,去掉它会很容易 nak,然后进入上面的死循环里面。我去掉了延时,所以在 nak 死循环了。接下来去掉 nak 的死循环。

修改一个 bug

原代码是这么写的 `else if (HAL_HCD_HC_GetState(&stm32_hhcd_fs, pipe->pipe_index) == URB_ERROR)` ,`HAL_HCD_HC_GetState` 返回值是 `HCD_HCStateTypeDef` 类型,而 `URB_ERROR` 是 `HCD_URBStateTypeDef` 类型的,明显调用的函数和比较值类型不一样。
修改后 `else if (HAL_HCD_HC_GetURBState(&stm32_hhcd_fs, pipe->pipe_index) == URB_ERROR)`。

重写任何可能引起**死循环**的代码

死循环可不是什么好东西,如果有这个可能,就必须改掉它,比如通过计数,重试有限次之后退出尝试。

nak retry

把 `continue` 改成重试 10 ,感觉把错误转移了,也没见有多少改善。

nak 退出

去掉 `continue` ,去掉 retry,直接返回错误退出 `drv_pipe_xfer` 函数。那么,问题来了。

首先说明,`drv_pipe_xfer` 函数被 `rt_usb_hcd_setup_xfer` 和 `rt_usb_hcd_pipe_xfer` 两个函数直接调用,然后被其它几十个函数间接调用。
`drv_pipe_xfer` 函数的第三个参数 `void *buffer`,用于传递输入输出数据缓存地址指针。输出数据过程没有多大问题,当它扮演接收数据缓存时就存在潜在的隐患。

因为接收数据的内存缓存多半不是全局的,而是临时申请的内存,或者是从栈上分配的。假如 `drv_pipe_xfer` 函数“假”失败返回,而 stm32 的 usb 控制器还在工作。比如上面的 nak,当我直接错误返回退出 `drv_pipe_xfer` 函数后,开始发现各种内存异常修改。比如返回上层调用函数的过程中发现局部变量(栈)莫名变成其它随机值。从这里我猜测虽然是 nak,但是并不一定表示有什么严重的问题。既然 usb 控制器仍然使用传递给他的寄存器地址,如果再稍微等待一下是不是变成完成状态了?

nak 超时

处理 HC 状态和 URB 状态前先判断是否是 nak,如果是 nak 就等待,等待 timeout 个 tick 超时,然后交给下面处理;不是 nak, ok 的可能性很大,直接交给下面处理。

       tick = rt_tick_get();
       while(HAL_HCD_HC_GetState(&stm32_hhcd_fs, pipe->pipe_index) == HC_NAK)
       {
           if ((rt_tick_get() - tick) >= timeout)
           {
               break;
           }
           else
           {
               rt_thread_yield();
           }
       }

这么处理以后,nak 少多了,但是还是有,而且一经出现就很难再继续。

阶段小结

修改了状态比较的错误,添加上 `drv_pipe_xfer` 中的 1ms 延时,同时添加了 nak 状态等待。去掉任何死循环操作,如果 nak 等待超时后还是 nak 就错误退出。到此,U 盘失败和 sd 卡识别已经有明显的改观了,大多时候可以走到 df_mount 之前(修改前走到 rt_udisk_run 就很难,rt_udisk_run 和 df_mount 中间有几个地方都可能出错终止)。

下面继续 debug。

上层操作 retry

`drv_pipe_xfer` 里 retry 的操作尝试过了,经过测试才知道,底层的工作原理不允许这么暴力 retry,这样可能引起 usb 控制器工作异常。
换个思路 retry,把 retry 往上层移动,找最容易出错误的函数调用路径中的某个接口,比如 `rt_usbh_storage_read10` 或 `rt_usbh_storage_write10` ,读操作最多返回错误,应用层操作失败。写操作失败意味着可能写 U 盘的任意节点出现问题了,这个时候放任不管可能会丢失数据的。
于是 `rt_usb_bulk_only_xfer(intf, &cmd, buffer, timeout);`  返回值不是 OK 就重试。这种尝试好像有效果,但是还是有多次重试后失败的。

继续查找原因

最底层的和比较上层的部分都使用各种方法尝试过了,问题还是存在。而且,让人头疼的是出现问题后 U 盘的文件系统有被损坏的概率。经过多次格式化 U 盘后发现也只有 `rt_usbh_storage_write10` -> `rt_usb_bulk_only_xfer` 出错概率最大,写坏 U 盘的操作也出现在这里。
那么,进入 `rt_usb_bulk_only_xfer` 函数寻找机会。

rt_usb_bulk_only_xfer

这个函数大概率出错,肯定有它自己的独特的操作。 `rt_usbh_storage_read10` 也调用了这个函数但是不出错,由此,我把可疑范围缩小到

   if(cmd->xfer_len != 0)
   {
       ...
       size = rt_usb_hcd_pipe_xfer(pipe->inst->hcd, pipe, (void*)buffer,
           cmd->xfer_len, timeout);
       ...
   }

`cmd->xfer_len` 比较大的时候,也就是说读写数据量大的时候,`rt_usb_hcd_pipe_xfer` 函数执行就容易出错。

为什么呢?数据量多少直接影响了出错概率。其它地方调用 `drv_pipe_xfer` 能正常工作,这里出错,能说明硬件配置有问题吗?

`rt_usb_hcd_pipe_xfer` 里面把大数据分包,分成 64 字节的小包一次次发送,这个包能改大一点儿吗?发大包会有影响吗?

多处判断包大小和 wMaxPacketSize 的关系, wMaxPacketSize 能修改大一点吗?

加延时,还是 retry ?哪种解决方法更高效?

最终方案

调用 `rt_usb_hcd_pipe_xfer` 之前加个短延时,比如 3ms 的延时。因为其它方法都尝试过,暂时没找到能解决问题的。
如果对延时引起的性能降低比较在意,先判断发送的包大小有多大,如果一包处理不完,延时一下;如果一包就可以处理完不需要延时。
读和写延时也不一样,写操作要求延时时间长,读操作可能不延时也没问题。
当这里加延时后,手头的 U 盘和读卡器识别和读写文件都正常了。
因为这里的延时是经验值,有的需要延时时间短,有的需要延时时间长。不确定延时多少怎么办?可以如下动态调整延时时间。

   ...
   if(cmd->xfer_len > pipe->ep.wMaxPacketSize)
   {
       rt_thread_mdelay(wr_delay);
   }
   size = rt_usb_hcd_pipe_xfer(pipe->inst->hcd, pipe, (void*)buffer,
       cmd->xfer_len, timeout);
   if(size != cmd->xfer_len)
   {
       if (wr_delay < 5)
       {
           wr_delay += 2;
       }
       ...
   }

其它

不知道这个算不算问题,无论从理论上还是实际应用中,读写 U 盘的时候是禁止拔掉 U 盘的。
不说写 U 盘,假设读 U 盘的时候拔掉 U 盘,原驱动有一定的机率在读操作过程中清理掉了对应通道的设备。因为,监测 usb 端口在一个独立线程,然后读写接口被文件系统调用,肯定是另外的应用层线程操作的了。两个不同的线程,一个使用 usb 通道时,另一个清理掉了它!
所以,在 `rt_usbh_hub_port_change` `rt_udisk_read` `rt_udisk_write` 等接口出添加互斥操作避免上述情况出现。这个对读写都是有效的。

> 本文所有提到的更改已经提交到 gitee ,欢迎大家测试 https://gitee.com/thewon/rt_thread_repo

  审核编辑:汤梓红

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分