尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

OTG主机库中NAK处理机制深度解析与工程实践

OTG主机库中NAK处理机制深度解析与工程实践 1. 一次U盘读写卡死之后我开始认真对待NAK去年调试一块RK3568开发板的Type-C OTG接口往板载eMMC里烧写系统镜像。电笔试了十几次每次都在同一个地方卡死设备枚举一切正常固件传输到一半进度条突然不动过几十秒报“下载失败”。一开始我怀疑是烧写工具问题、数据线问题、甚至是开发板供电不足全换了一遍问题依旧。后来用逻辑分析仪去抓DP/DM线上的低速信号才看到真相主机一遍遍发起批量写入令牌设备端在擦除Flash期间根本不接收数据每个事务都返回NAK而我们的主机库对这个端点没有任何NAK超时与恢复策略NAK就这样一直刷下去最后把整个USB总线的调度搅成一团。这个案例让我彻底改变了看待NAK的方式。很多搞USB驱动和主机协议栈的人一上来就背协议规范知道NAK是设备“没准备好”的握手响应知道硬件控制器一般会自动重试但真正到了自研OTG主机库、或者要排查设备兼容性问题时对NAK的理解往往停留在“硬件自动处理”的层面。这篇文章就围绕OTG主机库对NAK的处理展开把我这几年的实际案例、主机库源码分析方法和工程取舍都摊开来聊。不管你是做嵌入式设备端固件、写USB主机驱动还是调试Android系统的OTG外设兼容性这篇内容应该都能给你一点参考。2. 先弄清NAK到底是什么为什么主机库不能装看不见2.1 一次USB事务里的“忙”信号USB总线通信的最小单位是事务一次事务由令牌包、数据包、握手包组成。主机向某个端点发出IN或OUT令牌后设备必须在规定时间内回应握手包。三种常见响应ACK数据已经正确接收或者数据已经准备好送出。NAK设备暂时无法接收或提供数据但功能正常请主机稍后再试。STALL设备无法支持这个请求或者端点已被halt请主机停止访问。NAK就是字面意思的“Not Acknowledged”不过它跟网络协议里的NACK不太一样。USB协议中的NAK不是否定应答更像一个“我正忙着你过会再来”的排队信号。打个比方你去窗口打饭师傅说“稍等正在备餐”你退回去重新排队过一会再问一次。师傅没有说“我管不了你这单”只是让你等。NAK出现在哪些传输类型里很关键。控制传输的Data和Status阶段设备可以NAK批量输入的IN令牌设备没有数据时NAK批量输出的OUT令牌设备内部缓冲区满时NAK中断传输的IN令牌设备没有中断事件要上报时也NAK。等时传输不握手没有NAK这回事因为等时传输本身就是“不保证送达”的实时数据通道。2.2 设备能连续NAK多少次协议规范里没有限制设备连续NAK的次数。主机的批量端点如果一直往一个没就绪的设备发数据设备可以无限回复NAK只要它还没准备好。这个问题在纯协议层面无解必须靠主机库的策略来终结。一次NAK事务本身要消耗总线时间。拿高速设备来说一个成功的批量IN事务大概要几百纳秒到几微秒而一个被NAK掉的批量IN事务同样要消耗令牌包和NAK握手的时间。如果一个端点上每秒产生几万次NAK主线带宽被白白浪费一大截。更严重的是NAK事务会不断打断调度器对其他端点的服务造成“一个忙设备拖死整条总线”的现象。但这并不意味着NAK是坏事。NAK是USB流控机制里最基础的手段设备端背压能力不足时正是靠NAK保证数据不乱。主机库要做的不是消灭NAK而是给NAK建立一套有边界的处理机制允许它发生但要知道它持续了多久、频次有多高、什么时候该放弃、放弃之后怎么办。2.3 四种传输类型对NAK的容忍度完全不同控制传输主要用于设备枚举、获取描述符、设置配置等管理类操作。设备在控制传输过程中NAK是正常现象但控制管道的NAK不能无限等下去。Linux usbcore中很多控制URB都有超时参数比如usb_control_msg的timeout常见默认值是5秒或30秒。超过时间主机库就返回-ETIMEDOUT由驱动上层决定是重试还是复位设备。批量传输对NAK的容忍度最高。U盘里的Flash在做擦写时一个扇区的写操作可能要好几十毫秒期间设备一直NAK主机控制器通常会在硬件层面自动重试。软件层不需要每次NAK都跑一遍但这不意味着软件可以完全不设防。块存储协议SCSI/UFI虽然有自己的命令超时但USB层如果没有兜底I/O请求可能挂得比你还急。中断传输的NAK是常态。一个HID键盘在没有键按下的时间里主机每次轮询都会收到NAK。这种NAK不能算错误甚至不能影响错误计数。主机库只需要按照设备描述符里的bInterval和对应速度下的轮询间隔按部就班地发起IN事务就好。中断端点的处理原则是“NAK当空气”既不重试也不报错。2.4 为什么NAK对OTG主机库尤其麻烦OTG设备通常既是主机又是外设同一颗芯片要能在host和device之间切换。相比普通PC上的纯主机控制器OTG主机库还多了不少麻烦事。配套的协议栈往往运行在资源受限的MCU或嵌入式SoC上CPU主频不高内存也不大。如果NAK完全交给软件处理每个NAK都触发一次中断中断风暴能把CPU打满。如果完全交给硬件处理又没有可以感知NAK状态的手段出问题时只能干瞪眼。角色切换时原有的URB和端点调度队列可能还没清空。主机库从host切到device后如果残留了一批正在等待重试的NAK队列等角色切回来时这些URB可能还卡在“等待设备就绪”的状态里导致外设无法正常工作。我自己就遇到过一次OTG口先接了U盘当主机用拔掉U盘后切换到device模式连电脑发现电脑端总是枚举失败。后来查下来是主机模式下某个批量端点还在等NAK重试切到device时没有把该端点的调度队列清干净。2.5 NAK和STALL要分清很多Bug就出在这里排查NAK问题时最容易混淆的就是NAK和STALL。NAK是“暂时忙”STALL是“我没法做这件事别来烦我”。主机协议栈对两者的处理完全相反NAK应该继续等待或重试STALL则应该立即终止传输并向驱动返回错误。很多驱动开发者在日志里看到设备一直回NAK以为设备坏了直接复位端口。实际上NAK持续期间设备可能只是正在擦写Flash一旦擦完就能正常接收。反过来有些设备对不支持的命令直接回STALL驱动如果把它当成NAK反复重试不仅浪费时间还可能让设备端的主控状态错乱。所以在主机库里设计NAK处理逻辑时事件上报来源必须能区分NAK与STALL这是第一优先级。3. 主机库是怎么处理NAK的从Linux到自研栈的几种典型方案3.1 先问清楚NAK的处理到底在哪一层USB主机系统的分层大致是这样USB控制器硬件负责发送令牌、收数据包、响应NAK主机控制器驱动HCD负责管理URB、端点调度、中断处理USB核心层usbcore负责设备枚举、标准请求、设备驱动管理。再往上才是块存储、HID、网卡这些具体类驱动。NAK最先发生在控制器硬件这一层。不同控制器硬件对NAK的干预程度完全不同。EHCI/xHCI这类高性能控制器通常会在硬件调度器里自动重试批量和控制传输NAK不会频繁打断CPU而很多OTG芯片自带的控制器比如DWC2、DWC3、CH9、OTG IP核硬件自动重试的深度有限有时需要软件介入。主机库的工作就是在“硬件能自动处理的就交给硬件硬件处理不了的要能及时感知并接管”之间做平衡。3.2 Linux主机栈的做法靠URB状态机兜底Linux内核里NAK通常不会直接映射为URB的错误状态。USB HCD在硬件层面完成NAK重试URB只有在超时、取消、设备断开或者控制器错误时才改变状态。驱动开发者能看到的是一个URB长期处于“pending”状态最后可能返回-ETIMEDOUT或-ESHUTDOWN。Linux usbcore对控制传输的超时管理最明确。usb_control_msg()的最后一个参数就是超时时间很多驱动会传5000或者30000毫秒。批量URB则没有通用的超时机制一旦提交就无限期等待直到设备完成传输、出错或者驱动主动取消。所以“NAK无限重试导致卡死”在Linux上不容易被主机栈自己发现通常要上层驱动或应用程序主动设置超时。usbmon在Linux调试中很有用但必须说明一点usbmon只能看到URB层面的提交与完成事件看不到物理层每个事务是否NAK。你可以在usbmon日志里看到一个批量URB提交后过了很久才完成却看不到期间到底发生了多少次NAK。想看到NAK计数得用协议分析仪或者支持调试寄存器读取的控制器驱动。这不是usbmon的缺陷而是USB协议栈分层设计的结果NAK本来就不属于URB这个概念能表达的层级。硬件自动重试的大前提下Linux主机栈真正要管的是“重试到什么时候为止”。HCD的调度器会按带宽预算和端点队列轮询各个端点假如某个批量端点长期占用async队列其它端点可能被饿死。某些控制器驱动里会有中断延迟检查但整体来说Linux把NAK的“最终超时”责任交给了驱动和上层软件。这对PC Linux没什么问题因为xHCI硬件本身足够强但对OTG嵌入式场景直接套用Linux这套思路就不一定合适了。3.3 自研OTG主机库的软件轮询模型很多嵌入式项目不用完整Linux内核而是用自研的USB主机协议栈基于FreeRTOS、RT-Thread、Zephyr这类RTOS环境。我自己维护过一段时间的自研OTG主机库这里把我们对NAK的处理模型拆开讲比较有参考价值。核心思路是每个端点维护一个“传输上下文”里面包含当前URB状态、重试计数器、首次NAK时间戳、最后NAK时间戳。控制器在完成中断里上报事务结果时如果结果是NAK主控驱动并不把NAK当成中断风暴而是只更新上下文里的统计字段然后把该端点重新挂到调度队列尾部。伪代码如下void host_channel_xfer_complete(endpoint_t *ep, xfer_result_t result) { if (result XFER_NAK) { ep-ctx-nak_count; if (ep-ctx-first_nak_ticks 0) { ep-ctx-first_nak_ticks now_ms(); } ep-ctx-last_nak_ticks now_ms(); if (ep_type_is_bulk(ep) || ep_type_is_control(ep)) { uint32_t elapsed ep-ctx-last_nak_ticks - ep-ctx-first_nak_ticks; if (elapsed ep-ctx-nak_timeout_ms) { abort_urb(ep-ctx-urb, -ETIMEDOUT); reset_endpoint(ep); return; } } // 中断端点不重试不统计直接等待下一次调度 if (ep_type_is_interrupt(ep)) { return; } queue_endpoint_for_retry(ep); } }这套模型有几个关键决策。批量端点的NAK要有时间预算而不是计数预算。连续NAK 1000次和分散NAK 1000次是两回事时间维度更能反映设备是否真的卡死。中断端点的NAK直接忽略不重试、不计时间、不报错等调度器下一轮再查。控制端点的NAK超时要设得短一些控制管道是管理通道不能让它被一个外设的忙碌状态拖死。3.4 从TinyUSB和Zephyr主机栈中能学到的设计TinyUSB主机模式的实现也值得参考。TinyUSB在底层驱动里会把NAK结果解析出来传给上层usbh驱动模块。但不同控制器在NAK事件暴露上差别很大有的控制器在完成中断里明确给出NAK状态位有的控制器只给一个“传输未完成”事件需要驱动自己判断是不是NAK。TinyUSB面临的核心挑战跟我们自研栈一样如何用一套统一的上层接口屏蔽不同控制器的NAK表达差异。Zephyr的USB主机栈更偏重设备侧其host控制器抽象层在NAK处理上同样要分别考虑轮询型和中机型控制器。轮询型控制器要求HCD在NAK后把端点重新放回调度队列中机型控制器则要小心处理中断风暴。这类设计里最值得借鉴的一点是把“NAK是否可上报”做成控制器能力标志。支持NAK事件上报的控制器主机栈可以精确统计NAK频率不支持的控制器就只能靠“URB完成时延”这个间接指标来推断设备是否处于反复NAK状态。3.5 软件层NAK统计不打断中断但是要记账我见过不少OTG主机库设计完全没有NAK统计这回事。控制器返回NAK要么直接重试要么忽略代码里连一个计数器都没有。这样带来的问题是设备兼容性出了问题你根本没有数据支撑去判断是设备端太忙还是主机调度策略有问题。我觉得最低限度也要维护三个字段端点NAK总次数持续累加用于观察设备在某段时间内的忙碌程度。当前URB的首次NAK时间用于判断当前传输是否卡死。连续无进展时间上次成功传输到当前的时间间隔。有了这三个字段主机库可以在NAK率过高时打印警告日志或者通过调试接口导出。没有硬性要求每个NAK都进中断可以在完成中断里顺手更新一下计数器累计到CPU能接受的程度。统计的意义不在于报警而在于让你在复现问题时有一组可对比的数据。很多“灵异故障”只要看到底层的NAK计数整个排查方向就清晰了。4. 实测复盘OTG场景下三类由NAK引发的“灵异故障”4.1 案例一RK3566 Type-C OTG烧写镜像中断这个案例就是文章开头提到的场景。开发板通过Type-C OTG口连接到PCPC端用烧写工具把固件发送到板子板子端进入下载模式接收数据并写入Flash。问题现象是每次写入到某个固定位置附近卡死进度条不再前进设备枚举仍然存在但主机和板子之间就像失去联系一样。用逻辑分析仪抓DP/DM信号后发现主机在反复发送批量OUT令牌板子端回复的是清一色NAK。进一步对照Flash操作的时序卡死位置正好对应着Flash在进行大块擦除。设备端固件在处理擦除操作时USB中断被长时间屏蔽导致端点的FIFO没有被及时读取和释放。主机的批量OUT事务因此只能收到NAK。问题根源不在设备端而在主机侧。烧写工具使用的批量URB没有设置超时主机控制器硬件一直在重试。重试了大约几十秒后控制器内部状态出现异常整个端口需要复位才能恢复。当时的OTG主机库没有“NAK时间预算”机制也没有在NAK重试超时后主动复位端点的逻辑。解决办法分两端处理。设备端固件在擦除Flash期间不能完全屏蔽USB中断至少要周期性地服务一下端点FIFO哪怕只是把数据收到RAM里暂存。主机端则给批量传输增加了NAK超时阈值例如连续NAK超过5秒就主动终止该URB并复位端点而不是让硬件无限重试。这样即使设备端响应不及时主机侧也能快速恢复不会拖死整条总线。这个案例让我意识到一个很反直觉的事实USB设备“卡死”很多时候不是死机而是设备处于一个无法及时响应的正常忙状态NAK处理策略决定了主机是选择等待、放弃还是把整条总线都拖下水。4.2 案例二U盘通过OTG连接手机后变成“写保护”这个故障在论坛里见过很多人问。U盘通过OTG口插到手机上复制照片或文件复制到一半弹窗提示“U盘写保护”拔下来插到电脑上一看U盘里的文件系统甚至出现了只读属性。很多人第一反应是U盘坏了或者OTG转接头有问题。但把U盘插到电脑上用专业工具一查硬件本身完全正常。这类问题的本质我分析下来是这样手机端的主机栈在批量写传输中遇到设备反复NAK持续一段时间后达到某种超时或错误阈值。Android系统的卷管理服务vold或者文件系统驱动在检测到块设备I/O错误后会把挂载状态切换为只读防止数据进一步损坏。用户在UI上看到的“写保护”其实是系统自我保护后呈现的结果不是U盘物理写保护。为什么U盘会长时间NAK常见原因有两个。一是OTG口供电不足U盘主控在写操作时供电电压跌落Flash写操作失败主控反复重试对应的USB端点表现为长时间NAK。二是U盘本身的Flash写速度跟不上或者文件系统碎片太多导致写放大严重设备端忙不过来。这两种情况下NAK是结果不是原因。从这个案例可以学到主机库在处理NAK时不能只盯着USB层还要考虑上层文件系统策略。一旦你决定在NAK超时后返回I/O错误文件系统就有权将设备切换为只读。如果产品不希望看到“写保护”这种用户感知强烈的错误就得从更早的层面做文章比如检测到供电不足时主动提示、降低写入速率、或者在NAK率升高时先尝试重试而不是直接报错。4.3 案例三Android OTG网络共享RNDIS/ECM吞吐量异常有段时间我在调一块开发板希望通过OTG口连接手机用手机共享网络给板子。设备枚举正常RNDIS网卡也能起来但ping外网时延很高下载速度只有几百Kbps。折腾了很多网络参数都不行回到USB层才发现问题。手机通过USB共享网络时上行和下行数据分别走不同的批量端点。当板子向手机请求大量数据时手机的RNDIS驱动如果来不及从USB控制器读取数据就会在批量IN端点上返回大量NAK。板子端的主机库如果只用固定的调度策略比如仍然以很高的频率轮询这个批量端点这些NAK事务就会占满总线把其它端点的服务全挤掉。更隐蔽的一个坑是USB 3.0和USB 2.0之间的切换。现在很多手机在开发者选项里有一个“USB速度/配置”的选项可以选USB 3.0或USB 2.0。某些OTG线材和连接器本身是USB 2.0的焊盘主板只接了USB 2.0信号但手机侧被切到USB 3.0模式以后链路训练不完整设备枚举后链路状态不稳定导致批量传输时频繁出现链路层重试端点上表现为大量NAK和NRDY。当时排查到这个原因后把手机端的USB配置固定到USB 2.0并把主机库对批量端点的调度间隔做了一点降频吞吐量立刻恢复正常。这个案例的教训是OTG主机库不能假设设备端永远处于“即问即答”的状态。NAK的密度和分布往往能反映链路质量和设备端背压。如果主机库能把NAK率过高的批量端点降权把总线调度让给中断端点比如HID吞云吞吐问题就能缓解很多。4.4 案例四USB HID外设枚举后随机断连这类故障在运行自研主机库的嵌入式设备上很常见。一个USB鼠标或键盘插上去能识别但用着用着突然断连过几秒又恢复。排查之后发现中断端点在无事件时本来就该持续NAK按照协议这是常态。但某些设备对中断查询的间隔非常敏感如果主机调度器轮询间隔短于设备端实际处理能力设备端会一直NAK甚至可能触发设备端的错误恢复机制。问题出在主机库对中断端点的间隔配置上。很多初版自研主机库会把所有中断端点都按1ms轮询但对全速设备来说USB描述符里的bInterval表示的是帧数对高速设备则是2^(bInterval-1)微帧对无线USB等特殊设备还有不同的解析方式。如果解析错误轮询间隔过短NAK风暴就会出现。我们把描述符解析逻辑修正按设备速度正确计算轮询间隔后断连问题彻底消失。5. 排查NAK问题主机库层面有哪些实用工具和手段5.1 先做可复现性测试把“灵异”变“必然”排查NAK问题前有一件很重要的事情确认问题能不能稳定复现。不能稳定复现的问题后面所有分析都可能建立在偶然性上。我在调试NAK相关故障时通常先按这个顺序走固定使用同一个USB设备、同一根OTG线、同一个供电方案先把“设备相关变量”压到最小。用不同的主机侧控制器模式各测一遍比如同一颗芯片分别跑DWC2和DWC3控制器驱动。在同一台设备上连续跑100次压力测试记录每次失败时的URB状态、耗时、dmesg日志。如果问题只在特定设备上复现那大概率是设备端兼容性问题如果多台设备都复现那主机库或硬件设计的嫌疑更大。这个分类能帮你省下大量时间。5.2 usbmon、dmesg和ftrace的配合打法Linux环境下usbmon是最容易上手的工具。开启方式加载usbmon模块后抓取总线1上的URB记录。输出会包含URB的提交、完成、错误事件。重点关注以下几类信息完成状态s -110代表-ETIMEDOUT说明某个URB被超时终止背后很可能存在长时间的NAK重试。长时间pending的批量URB时间戳跨度异常大说明设备端可能处于忙碌状态。连续多个URB以错误完成且错误码相同说明设备或控制器可能已经进入了非正常状态。usbmon看不到物理层NAK所以当usbmon显示“URB长时间pending”时配合dmesg看控制器驱动是否上报了异常事件。例如xHCI驱动在响应超时时可能打印类似“ERROR Transfer event TRB DMA ...”的信息这类信息往往和端点的NAK重试超时有关。ftrace用来跟踪hcd驱动内部的端点调度函数耗时和中断处理时间可以定位NAK风暴时CPU是否被中断处理占满。5.3 逻辑分析仪和USB协议分析仪怎么选逻辑分析仪适合抓低速和中速信号。USB 2.0全速和低速信号用24MHz以上采样率的逻辑分析仪就能抓配合sigrok/PulseView这类开源工具可以直接解码URB层的token、data、handshake。看到NAK包连续出现时可以统计NAK的间隔和数量但受限于采样率和解码深度不适合长时间大数据量抓取。USB协议分析仪则是专业设备比如TotalPhase Beagle 480、Ellisys USB Explorer之类。它们能深度解码USB 2.0/USB 3.x协议直接给出NAK计数、总线利用率、错误统计。调试NAK风暴时这类工具能直观地告诉你某个端点上每秒产生了多少NAK占用了多少总线时间哪些事务在NAK期间被挤掉。如果公司预算允许这类设备值得备一台。我个人的习惯是先用usbmon和dmesg把问题压缩到某个端点再用协议分析仪做一轮短时间抓取确认NAK的分布和频率。逻辑分析仪在涉及低速信号质量、VBUS压降、D/D-上拉下拉时序的问题时更有用。5.4 在自己的主机库里增加NAK健康度接口如果主机库是自研的强烈建议在早期就加上NAK统计和导出接口。具体做法在每个端点结构体里维护nak_count、last_nak_time、total_retry_counter。在完成中断里更新这些字段不打印、不打断正常流程。提供一个调试CLI或API随时可以查询某个端点上最近一段时间的NAK率。当NAK率超过预设阈值时在日志里输出警告但不主动改变传输策略。有了这些数据排查兼容性问题时就能直接回答“这个设备到底有多忙”“它的NAK是持续性的还是突发性的”。没有数据支撑全靠猜效率差太多。6. 关于NAK重试策略我的工程取舍和最终体会做OTG主机库这几年踩过不少坑也积累了一些很实际的取舍经验这里一次性分享完。控制传输的NAK我倾向于“短等待、快失败、再恢复”。控制管道是USB设备的生命线设备枚举、标准请求、类请求都走这条管道。如果控制端点一直NAK先别急着无限重试给一个比较保守的超时阈值比如2到5秒超时后直接复位设备并重新枚举往往比重试更靠谱。因为控制管道不通通常意味着设备端固件状态异常再等下去也只是浪费时间。批量传输的NAK采用“硬件重试软件护栏”的双层策略。硬件负责正常的NAK重试软件只管一件事设定一个合理的总超时时间。块存储设备写入时Flash擦写导致的NAK可能要持续几十毫秒甚至上百毫秒超时设太短会误杀正常操作设太长又可能出现URB挂死。我用下来比较合适的是给批量URB设30秒到60秒的总超时具体数值还要根据产品场景调节。如果NAK持续时间超过了总超时立刻终止URB、复位端点并向上层返回I/O错误让上层决定是否重试或降级。中断传输的NAK核心原则是“不计错、不重试、按间隔轮询”。HID设备没有事件时本就应该NAK这是正常工作状态。唯一要细心的是轮询间隔的解析正确全速设备bInterval的单位是1ms帧高速设备用的是2^(bInterval-1)微帧搞错了就会出现NAK风暴。对于OTG角色切换场景务必在切换前清空所有正在等待NAK重试的端点队列。切换后主机库要处于一个干净状态不能带着旧端点的“残留记忆”进入新角色。我的做法是在角色切换函数里显式调用一个endpoint_flush_all()把所有URB终止清空调度队列再重新初始化主机控制器。调试工具方面usbmon负责快速定位URB层问题协议分析仪负责确认NAK的分布和频率逻辑分析仪负责排查信号质量相关的问题。三者不是替代关系而是互补关系。如果你只靠usbmon去查NAK大概率查不出结果因为NAK层面根本不在usbmon的视野里。最后说一点个人体会。很多USB主机栈的设计者把NAK当成一个协议层的小细节觉得交给硬件控制器自动重试就万事大吉。但真正到了OTG嵌入式场景你会发现NAK处理策略直接决定了设备的兼容性上限和故障恢复速度。我们自研主机库在加入NAK统计、超时预算、端点复位这三板斧之后兼容性表格从十几款设备扩展到上百款设备烧写工具再也没有出现那种位置随机、原因不明的卡死。如果你也正在被“USB设备随机卡死”“OTG外设兼容性差”这类问题困扰不妨先去端点上挂个NAK计数器。大概率你会看到一些意料之外的数据而这些数据往往就是问题答案的一半。
返回列表