1. 项目概述:深入CMX USB栈与HNP协议

在嵌入式USB开发中,我们常常面临两个核心挑战:一是如何在资源受限的MCU上高效、稳定地运行USB协议栈;二是如何实现像手机连接U盘、平板连接键盘这样灵活的主从角色切换。后者正是USB On-The-Go(OTG)规范中主机协商协议(HNP)要解决的问题。今天,我想结合一份经典的Freescale(现NXP)应用笔记AN3492中的实测数据,和大家深入聊聊CMX USB栈的资源占用细节,并拆解HNP协议背后的交互逻辑。这份资料虽然年代稍早,但其揭示的内存优化方法和协议实现思想,对于当今仍在大量使用的Cortex-M系列等资源敏感型芯片开发,依然具有极高的参考价值。无论你是在为一个自定义的HID设备精简代码,还是在设计一个支持双角色(DRD)的OTG产品,理解栈的“胃口”和协议“握手”的细节,都能让你在调试时心里更有底,在设计时做出更合理的取舍。

2. CMX USB栈内存占用深度解析

当我们选定一个USB协议栈,第一个要问的问题往往是:“它要吃掉我多少内存?”对于嵌入式系统,尤其是RAM可能只有几十KB的微控制器,每一字节都弥足珍贵。CMX USB栈提供了一份非常实在的“账单”,它没有给出模糊的理论值,而是直接列出了几个典型演示项目编译后的MAP文件数据,这让我们能直观地看到不同应用场景下的资源消耗。

2.1 基准内存占用数据解读

原始资料中的表格清晰地展示了五种典型配置下的内存占用,我们可以将其分为两大类:设备类(Device)和主机类(Host)。

对于设备类应用:

  • hid-demo-flash (HID设备) :这是最常见的应用之一,比如键盘、鼠标、游戏手柄。它的Flash占用为22960字节,RAM总占用为7680字节。值得注意的是,这里的RAM又细分为三部分: stack (调用栈)5120字节、 bss (未初始化数据区)1621字节、 bdt+align (缓冲区描述符表及对齐)939字节。这个HID演示项目同时包含了键盘、鼠标和一个通用HID设备的支持,这意味着它存储了三套配置描述符。
  • cdc-demo-flash (CDC设备) :模拟串口(USB转串口)是嵌入式调试和通信的利器。它的资源消耗相对更友好,Flash 18832字节,RAM总计7168字节(stack 5120, bss 1224, bdt+align 824)。通常CDC设备的描述符比复合HID设备简单,所以Flash和bss区占用会更少。
  • otg-app (OTG双角色设备) :这是功能最复杂的配置,支持在主机(Host)和设备(Device)角色间动态切换。其资源消耗也显著上升,Flash高达54128字节,RAM总计11264字节(stack 7168, bss 3338, bdt+align 758)。栈空间的激增主要是因为OTG状态机、主机控制器驱动以及可能同时运行的两套协议处理逻辑更为复杂。

对于主机类应用:

  • host-hid-demo (HID主机) :例如,一个嵌入式系统去读取USB键盘的数据。其占用与CDC设备类似,Flash 23904字节,RAM总计7168字节。
  • mass-storage-demo (大容量存储主机) :实现U盘读写功能。Flash占用35728字节,RAM总计7680字节。Flash占用较大是因为需要实现FAT文件系统等上层协议。

注意 :表格中 bdt+align 的占用相对固定且较小,这是因为USB端点缓冲区(BDT)的大小主要取决于你使能了哪些端点、以及为每个端点分配的缓冲区大小。这部分通常在配置头文件中定义,是可以根据实际吞吐量需求精细调整的。

2.2 关键优化策略与实践

这份数据最有价值的地方,在于它明确指出了优化方向,而不仅仅是罗列数字。

1. 裁剪未使用的描述符以节省Flash 资料中特别强调:“如果只构建鼠标设备、或只构建键盘设备、或只构建通用设备,可以从 hid-demo-flash 项目的Flash占用数字中减去大约600字节。” 这是一个非常具体的指导。三套描述符约300字节一套,保留一套,裁剪掉两套,就能节省600字节。在实际项目中,我们的产品功能是确定的,一个键盘设备绝不需要鼠标的描述符。因此,在 usb_descriptor.c 这类文件中,务必只保留与本产品相关的配置描述符、接口描述符、端点描述符和报告描述符,并相应调整描述符的总长度和接口数量等字段。这是最直接、最有效的Flash优化手段。

2. 评估与缩减调用栈(Stack)大小 原始配置中,调用栈预留了相当大的空间(5120字节),这主要是为了确保在各种嵌套函数调用和中断服务例程(ISR)场景下都不会溢出。但对于一个功能确定的稳定系统,这个值往往有压缩空间。资料中的第二张表——“减少调用栈后的内存占用”——正是展示了这种优化的成果。

他们采用了一种工程上非常可靠的方法进行验证: 在栈的末尾放置一个标记(例如,一个特殊的魔数 0xDEADBEEF ,然后让演示程序进行全功能、高负荷的测试(“running the demo through its paces”)。测试完成后,检查这个标记是否被覆盖。如果标记完好,证明在最恶劣的执行路径下,栈使用也未触及边界,那么当前栈大小就是安全的。

通过这种方法,他们将HID设备、CDC设备、HID主机、大容量存储主机的栈大小从5120字节成功降低到了2048字节,RAM总占用也从7KB左右降至4KB左右。这是一个将近60%的RAM节省!而OTG应用的栈因为其复杂性,仍需要7168字节。

实操建议 :在你的项目中,一定要进行类似的栈压力测试。你可以使用编译器提供的栈使用分析工具(如GCC的 -fstack-usage ),但最终必须通过运行时标记法来验证。测试场景要尽可能覆盖所有功能分支和中断并发情况。

3. 理解RAM构成的优化启示

  • bss段 :存放未初始化的全局和静态变量。这部分的大小直接由你的代码决定。减少不必要的全局变量,特别是大型数组,是缩小bss的关键。
  • 堆(Heap) :在表格中没有单独列出,可能因为CMX栈在默认演示中未使用动态内存分配,或者将其包含在 bss stack 中。在嵌入式USB开发中,我强烈建议避免使用 malloc/free ,因为内存碎片化在长期运行的系统里是致命风险。所有内存都应静态分配。
  • 栈(Stack) :如上所述,通过测试来最小化。
  • BDT缓冲区 :根据端点实际数据流量调整。对于仅传输少量报告数据的HID设备,端点缓冲区可以设得很小(如8-64字节)。对于批量传输的CDC或大容量存储,则需要根据最大包长度(通常512字节用于全速USB)来设置。

3. 主机协商协议(HNP)实现机制详解

HNP是USB OTG规范中的精华所在,它让两个支持OTG的设备(比如两部手机)在只有一根USB线连接的情况下,智能地决定谁当“主机”(负责供电和调度),谁当“从设备”。这个过程完全由硬件和底层固件自动完成,对用户透明。理解其步骤,对于调试OTG功能异常(比如角色切换失败)至关重要。

3.1 HNP交互流程全步骤拆解

假设有两个设备:OTG-A(初始角色为主机)和OTG-B(初始角色为设备),它们通过Micro-AB接口连接(这是支持HNP的前提,因为Micro-AB接口有ID引脚用于初始角色判定)。

步骤1:能力宣告与使能 OTG-A作为初始主机,通过控制传输向OTG-B发送一个特殊的 SetFeature 请求。这个请求的“特性选择符”(Feature Selector)就是 b_hnp_enable 。这相当于主机A对设备B说:“我支持HNP,我也允许你将来扮演主机角色,你准备好了吗?” 如果OTG-B也支持HNP,它必须成功应答(ACK)这个请求,而不能返回STALL(失败)信号。这个“使能”动作是整个HNP流程的开关。

步骤2:主机发起切换准备 OTG-A在使能了OTG-B的HNP能力后,主动挂起(Suspend)USB总线。在USB协议中,主机可以通过一段时间(如3ms)不在总线上产生任何活动(SOF包)来进入挂起状态。这个动作是一个明确的信号:“我现在要暂停一下作为主机的职责。”

步骤3:设备检测与信号释放 OTG-B(设备)持续监测总线状态。当它检测到总线进入挂起状态(无SOF包)超过一定时间后,它就明白角色切换的“发令枪”响了。此时,OTG-B会主动关闭其D+线上的上拉电阻(对于全速设备)。在USB协议中,设备端的上拉电阻是用于向主机宣告自身存在和速度的。关闭上拉电阻,意味着OTG-B在电气上“断开”了与总线的设备连接。

步骤4:角色反转——设备变主机 OTG-A(初始主机)虽然在逻辑上挂起了,但它仍在监测总线。当它检测到设备端的上拉电阻消失(即D+线电压变低),它会将其解读为:“OTG-B请求成为主机”。于是,OTG-A立即开启自己的上拉电阻(注意:此时它连接到作为“设备”角色的端口),将自己配置为USB设备。同时,OTG-B在断开上拉电阻后,会延迟一个很短的时间(SRP协议定义的时间),然后开始以主机身份枚举总线,它会检测到OTG-A(现在作为设备)的上拉电阻,从而开始枚举这个新设备。至此,角色完成第一次切换:B成了主机,A成了设备。

步骤5:逆向切换的发起 当OTG-B(当前主机)需要将控制权交回时(例如,数据传输完成或根据应用逻辑),它不能直接“下台”。正确的做法是:OTG-B首先停止一切总线活动,包括停止发送SOF包,并主动将自己切换回设备模式(关闭主机控制器,启用设备控制器并连接上拉电阻)。这相当于它主动“放弃”了主机身份,重新变回一个等待被连接的设备。

步骤6:角色恢复——主机归位 OTG-A(当前设备)检测到总线长时间无活动(无SOF包),它会认为“主机消失了”。根据OTG协议,一个支持HNP的设备在检测到总线长时间空闲后,会断开连接(断开自己的上拉电阻),然后尝试恢复自己初始的主机角色。OTG-A断开连接后,会重新作为主机去检测总线,此时它会发现OTG-B(已变回设备)的上拉电阻,于是重新枚举OTG-B,角色切换回初始状态。

3.2 协议实现中的核心要点与避坑指南

  1. 时序是关键 :HNP的每一步都有严格的时序要求,例如检测挂起的超时时间( a_bus_suspend )、断开上拉后的等待时间( a_aidl_bdis )等。这些时间参数在OTG规范中有明确定义,通常在协议栈的底层驱动或状态机中配置。如果时序不对,角色切换就会失败,表现为设备无法识别或枚举错误。

  2. ID引脚是起点 :HNP的初始角色由Micro-AB插座中的ID引脚连接决定。ID脚接地(通过Micro-A插头)的设备初始化为A设备(主机);ID脚悬空(通过Micro-B插头)的设备初始化为B设备(设备)。你的硬件设计必须确保ID引脚连接正确。

  3. VBUS管理是前提 :资料中提到一个关键约束:“如果OTG-B设备没有 stall SetFeature(b_hnp_enable) 命令,那么OTG-A主机在关闭VBUS电源之前,必须给OTG-B设备一个成为主机的机会。” 这意味着,作为初始主机的A设备,在决定断电结束会话前,必须检查HNP是否已被使能。如果已使能,它必须通过挂起总线来发起HNP,让B有机会成为主机去取回所需数据,而不是直接断电导致数据丢失。VBUS的开关必须与HNP状态机协同工作。

  4. 调试手段 :调试HNP问题,一个逻辑分析仪或支持USB协议解码的示波器几乎是必不可少的。你需要观察 SetFeature 请求的传输、总线挂起事件、D+线上拉电阻的通断变化。从软件层面,确保OTG控制器的双重角色(DRD)配置正确,并且HNP使能位被正确设置。

4. 从数据到设计:工程实践指南

了解了资源占用和HNP原理,我们如何将其应用到实际项目中?这里分享一些从这些数据中提炼出的设计思路和调试经验。

4.1 基于资源占用的选型与配置策略

  1. 项目选型决策树

    • 如果你的设备只是简单的USB从设备(如键盘) :选择 HID Device 配置。仔细裁剪描述符,将栈大小初步设为2KB左右,然后进行压力测试。预计Flash占用可控制在22KB以内,RAM在4-5KB。
    • 如果需要USB转串口功能 :选择 CDC Device 配置。这是资源消耗最少的通信类设备之一,优化后RAM可望低于4KB。
    • 如果需要连接U盘或读卡器 :选择 Mass Storage Host 配置。注意,除了栈本身的7-8KB RAM,你还需要为文件系统(如FAT)和磁盘缓冲区分配额外的RAM,总内存需求可能达到10KB以上,需提前规划。
    • 如果需要双角色功能(如设备既能被电脑读取,又能读取U盘) :必须选择 OTG 配置。这是最吃资源的模式,务必选用Flash > 64KB,RAM > 16KB的MCU,并为栈预留足够的空间(至少7KB以上)。
  2. 内存优化检查清单

    • [ ] 审查并删除所有未使用的描述符。
    • [ ] 在配置头文件中,禁用所有未使用的USB端点。
    • [ ] 根据端点实际传输的最大包长,减小端点缓冲区大小。
    • [ ] 将全局变量数组(如数据缓冲区)的大小调整到够用即可,避免“以防万一”的过度分配。
    • [ ] 进行栈边界标记测试,逐步减小栈大小至安全极限。
    • [ ] 检查编译器优化等级, -Os (优化大小)通常比 -O2 更能减少代码体积。

4.2 HNP功能开发与调试常见问题

  1. 角色切换完全不发生

    • 检查硬件 :确认使用的是Micro-AB接口和带ID引脚连接的OTG电缆。用万用表测量ID引脚电平。
    • 检查软件使能 :确认在代码中同时使能了OTG核心的HNP功能(通常需要设置某个寄存器位)并且成功响应了 SetFeature(b_hnp_enable) 请求。
    • 查看协议分析仪 :检查初始主机是否发出了 SetFeature 请求,设备是否回复了ACK。
  2. 角色切换后枚举失败

    • 检查电源 :角色切换后,新的主机(原设备)必须能为VBUS供电。确保其OTG控制器支持并已启用作为主机时的VBUS放电功能。
    • 检查状态机 :OTG驱动中的HNP状态机逻辑可能存在问题。确保在角色切换后,USB核心模式(主机/设备)的切换、上下拉电阻的控制与协议步骤严格同步。
    • 时序问题 :尝试微调HNP相关的延时参数(需查阅具体OTG控制器数据手册),这可能与电缆长度或PCB布局引起的信号延迟有关。
  3. 系统在HNP过程中不稳定或死机

    • 栈溢出 :HNP状态机可能增加了函数调用深度。如果之前裁剪栈空间过于激进,此时可能发生溢出。重新进行栈压力测试,重点测试角色切换路径。
    • 中断冲突 :角色切换涉及控制器模式切换,可能会影响USB相关中断的使能和优先级。确保在切换过程中,关键中断不被错误地屏蔽或丢失。

这份来自Freescale的实测数据,就像一份老工程师的笔记,它没有华丽的辞藻,但给出了实实在在的优化基准和验证方法。而HNP协议的拆解,则揭示了USB设备间那场静默而有序的“权力交接”仪式。在实际项目中,我习惯在项目初期就根据类似表格估算内存需求,选择合适的芯片型号;在调试OTG时,则会把HNP的六个步骤写在便签上,对照协议分析仪的波形一个一个核对。这种把原理和实测数据结合起来的做法,往往能让你在遇到问题时,更快地定位到是硬件连接、软件配置还是协议逻辑上的偏差。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐