1. 项目概述:深入MC68SZ328的USB设备模块
如果你正在为一个基于MC68SZ328的嵌入式项目开发USB功能,或者你正在学习如何为老式但经典的Motorola(现NXP)微控制器编写底层USB设备驱动,那么这篇笔记或许能帮你避开不少弯路。MC68SZ328集成的USB设备模块,是一个功能完整但编程模型相对底层的控制器,它不像现在许多高级MCU的USB库那样提供“一键配置”的抽象层。你需要直接和它的寄存器、FIFO以及中断打交道。这种“亲密接触”虽然增加了复杂度,但也让你能对USB协议栈的底层运作,特别是数据流控制、端点管理和错误恢复机制,有更透彻的理解。
这份指南的核心,就是拆解这个模块从“上电一片空白”到“稳定收发数据”的全过程。我们会聚焦三个最关键的实战环节:设备初始化与配置下载、三种数据传输模式(控制、批量、中断)的编程实现,以及当事情不按预期发展时的异常处理策略。整个过程就像组装并调试一台精密的机械:你需要按照严格的顺序安装零件(配置寄存器),确保管道连接正确(映射端点到FIFO),然后测试它在各种负载下的运行(数据传输),最后还要知道卡壳时该怎么排查(异常处理)。我会结合手册中的寄存器描述和实际编程中容易踩坑的地方,把每个步骤背后的“为什么”讲清楚。
2. 核心硬件架构与寄存器映射解析
在动手写代码之前,我们必须先搞清楚手头的“工具箱”里有什么。MC68SZ328的USB模块可以看作是两个部分的协作:一个是处理USB协议底层信令的UDC(USB Device Controller)核心,另一个是面向程序员、用于配置和数据交换的前端逻辑。我们的编程工作主要在前端逻辑上。
2.1 端点、FIFO与缓冲区的三角关系
这是理解整个模块的基石。USB协议逻辑上支持最多31个端点(Endpoint),但MC68SZ328在硬件上只提供了5个物理FIFO缓冲区(FIFO0-FIFO4)。这就产生了一个映射问题:我们如何将USB协议中定义的多个逻辑端点(比如端点1-IN、端点1-OUT、端点2-IN等)分配到有限的5个硬件FIFO上?
答案就在端点缓冲区(EndPtBuf)这个40位的数据结构中。在初始化阶段,我们需要通过USB_DDAT寄存器,为每个想要使用的逻辑端点下载一个EndPtBuf。这个数据结构的关键字段包括:
- 逻辑端点号(EpNum):对应USB描述符里定义的端点地址。
- 配置、接口、备用设置号:将端点关联到特定的USB配置状态。
- 端点类型与方向:定义它是控制(Control)、批量(Bulk)还是中断(Interrupt)端点,以及是IN(设备到主机)还是OUT(主机到设备)。
- 最大包大小:限定每次事务传输的数据量,只能是8、16、32或64字节之一。
- 硬件FIFO编号(FifoNum):这是建立映射的关键。它指明这个逻辑端点使用哪个物理FIFO(例如FIFO3)。多个逻辑端点可以共享一个硬件FIFO,但这需要软件精心管理,避免数据冲突。
例如,你可以将端点1-IN(批量传输,64字节)和端点2-IN(中断传输,8字节)都映射到较大的FIFO3(128字节)上,通过分时复用。但你必须确保在任一时刻,只有一个端点在向主机发送数据。
2.2 关键控制与状态寄存器一览
编程的本质就是读写寄存器。以下几个寄存器组成了控制USB模块的“仪表盘”:
USB_EPn_STATCR(端点n状态/控制寄存器):这是每个端点的“大脑”。你需要在这里设置端点的类型(控制/批量/中断)、方向(IN/OUT)和最大包大小。它同时也包含重要的状态位,如
SIP(Setup In Progress,用于检测异常的重复Setup包)和FORCE_STALL(软件强制挂起端点)。USB_EPn_FCTRL(端点n FIFO控制寄存器):控制FIFO的行为模式。对于几乎所有的USB应用,你都需要将
FRAME模式置位。此寄存器中的WFR(Wait for Frame)位在程序I/O发送数据时至关重要,用于标记一个数据包的最后一个字节。USB_EPn_FALRM(端点n FIFO报警寄存器):设定FIFO的“水位线”。当FIFO中的数据量低于这个报警值时,会触发DMA请求或
FIFO_LOW中断,提示软件或DMA控制器可以继续填充数据了。一个关键技巧:对于批量传输且启用FRAME模式的情况,通常将报警值设置为一个数据包的大小。例如,如果你的最大包大小是64字节,FIFO深度是128字节(双缓冲),那么报警值设为64字节是最优的,这样能确保DMA每次正好搬移一个完整的数据包。USB_EPn_FWRP(端点n FIFO写指针寄存器):这个寄存器直接反映了FIFO的写指针位置
WP[6:0]。它的一个重要作用是调试。你可以通过读取它来了解FIFO的实时填充情况,或者在驱动异常时,通过写入特定的值来手动重置FIFO的指针状态,这在排查复杂的FIFO卡死问题时非常有用。USB_GEN_ISR 和 USB_EPn_MASK(中断寄存器):全局中断状态寄存器和端点中断屏蔽寄存器。你必须正确配置它们来使能你需要的中断,例如配置改变(
CFG_CHG)、设备请求(DEVREQ)、帧结束(EOF)、传输结束(EOT)等。初始化时,所有中断默认是被屏蔽的。
注意:寄存器访问有严格的顺序要求。特别是在初始化阶段,必须在确保
CFG位(在USB_CFGSTAT寄存器中)置位后,才能开始下载配置数据。在每次向USB_DDAT写入一个配置字节后,都必须等待BSY位清除,才能进行下一步操作。忽视这些等待会导致配置下载失败,设备无法被主机识别。
3. 设备初始化与配置下载全流程拆解
初始化过程是为USB模块“注入灵魂”的过程。手册里给出了步骤列表,但每一步背后的意图和潜在陷阱,才是实战的关键。
3.1 初始化步骤的深度解读
让我们逐条分析手册中的9个初始化步骤,并补充实战细节:
步骤1-3:复位与等待首先进行硬件或软件复位(设置USB_ENAB寄存器的RST位)。这里的一个常见误区是,复位后立即进行后续操作。你必须等待RST位被硬件自动清除,这表示复位操作真正完成。紧接着,要轮询USB_CFGSTAT寄存器的CFG位,直到它变为1。这个位是UDC模块发出的“我已准备好接收配置”的信号。没有这个信号,后续的配置下载是无效的。
步骤4:配置数据下载——最精细的操作这是初始化的核心。你需要按照EndPtBuf的格式,为每个端点构造40位(5字节)的配置数据,并通过USB_DDAT寄存器逐个字节写入。顺序是从最高字节(EPn[39:32])到最低字节(EPn[7:0])。
- 实战心得:我通常会在内存中预先构建好所有端点的
EndPtBuf数组,然后在一个循环中完成写入。循环体内必须包含对BSY位的检查。伪代码如下:
写入完成后,再次检查uint32_t endpt_bufs[NUM_ENDPOINTS][5]; // 假设已按格式填充好 for (int ep = 0; ep < NUM_ENDPOINTS; ep++) { for (int byte = 0; byte < 5; byte++) { USB_DDAT = endpt_bufs[ep][byte]; // 写入一个字节 while (USB_CFGSTAT & BSY_MASK); // 死等BSY清除,也可加超时 } }CFG位,它会从1变为0,标志着配置数据已被UDC模块成功加载。
步骤5-8:端点与中断配置
- 步骤5:使能全局USB中断。通常你需要使能
CFG_CHG(配置改变)中断,��便主机选择不同配置时你能及时响应。 - 步骤6-7:对每个端点,配置其
USB_EPn_STATCR寄存器。这里要特别注意方向。一个物理端点(FIFO)在EndPtBuf中定义了方向后,必须与STATCR寄存器中的方向设置一致。例如,一个映射到FIFO1的端点,如果在EndPtBuf中定义为IN端点,那么它的STATCR也必须设置为IN方向。 - 步骤8:配置FIFO控制器。如前述,将
USB_EPn_FCTRL的FRAME模式置1。然后根据FIFO深度和包大小计算并设置USB_EPn_FALRM报警值。双缓冲配置示例:端点最大包大小=64字节,使用的FIFO深度=128字节。理想情况下,你希望当FIFO中剩余空间>=64字节时(即已发送完一个包),DMA或软件就立即填充下一个包。因此,报警值应设为128 - 64 = 64字节(即当FIFO数据量低于64字节时触发请求)。这样能实现近乎无缝的流水线数据发送。
步骤9:最终使能设置USB_CTRL寄存器的USB_ENA位来启用整个模块。通常还需要同时设置USB_SPD(全速模式)和USB_AFE(模拟前端使能)。至此,设备从硬件角度看已经就绪,可以响应主机的总线枚举了。
3.2 配置改变(CFG_CHG)中断的处理
初始化完成后,主机开始枚举过程。主机可能会设置不同的配置(Configuration)或交替设置(Alternate Setting)。当发生这种改变时,硬件会产生CFG_CHG中断。
你必须及时响应这个中断。处理流程通常是:
- 读取
USB_GEN_ISR确认中断源。 - 根据USB协议,主机通过控制传输发送
SetConfiguration或SetInterface请求。 - 你的驱动需要解析这些请求(通过端点0的Setup包),并更新内部状态,知道当前生效的是哪个配置和接口。
- 关键点:在
CFG_CHG中断挂起期间,USB模块会暂停该端点上的所有后续通信。这是为了防止主机和设备状态不同步。因此,快速处理此中断对维持USB连接稳定性至关重要。
4. 数据传输模式编程实战
USB模块支持控制、批量和中断三种传输。从硬件角度看,中断传输被视为一种特殊的批量传输。
4.1 控制传输:设备命令的基石
控制传输用于主机对设备的配置、查询和控制。它总是发生在端点0,并且分为三个阶段:Setup、Data(可选)、Status。
软件处理流程如下:
- 接收Setup包:当主机发送Setup包时,对应端点的
DEVREQ和EOF中断会同时置位。DEVREQ表示这是一个设备请求,EOF表示一个数据包(此处是8字节的Setup数据)已在FIFO中可用。 - 读取并解码:从端点0的FIFO数据寄存器(
USB_EP0_FDAT)中读取8字节的Setup数据。这8字节包含了bmRequestType、bRequest、wValue、wIndex和wLength字段,你需要解析它们来确定主机的意图。 - 清除中断:读取数据后,清除
DEVREQ和EOF中断标志。 - 执行数据阶段(如果需要):如果
wLength不为0,表示有数据阶段。如果是IN方向(设备给主机数据),你需要准备数据并通过FIFO发送;如果是OUT方向,你需要从FIFO读取主机发来的数据。务必严格遵守wLength,不要发送比请求更多的数据,硬件不会帮你检查。 - 状态阶段:数据阶段完成后,通过设置
USB_EPn_STATCR寄存器中的CMD_OVER位(正常结束)或CMD_ERROR位(出错)来进入状态阶段。硬件会根据这个位生成相应的握手包(ACK或STALL)给主机。设置后,需要等待CMD_OVER位被硬件自动清除,这表示整个控制传输已彻底完成。
4.2 批量传输:大块数据的主力
批量传输用于大量、非实时性但要求可靠的数据传输,如文件传输。
批量OUT(主机到设备)流程:
- 主机发送数据包到设备的OUT端点。
- 硬件自动将数据存入对应的FIFO,并在收完一个完整的数据包后触发
EOF中断。 - 你的中断服务程序(ISR)或DMA控制器从FIFO中读取该数据包。
- 当整个传输(可能由多个包组成)结束时,硬件会触发
EOT中断。 - 重要机制:在
EOT中断被服务(即软件清除中断标志)之前,该端点会对主机的后续请求回复NAK。这保证了不同传输的数据绝不会在FIFO中混合,为软件提供了清晰的传输边界。
批量IN(设备到主机)流程:
- 你的软件或DMA将数据按最大包大小写入FIFO。
- 标记包结束:这是最容易出错的地方。对于程序I/O,在写入一个包的最后一个字节之前,需要先设置
USB_EPn_FCTRL寄存器的WFR位,然后再写入这最后一个字节。对于DMA,则需要DMA控制器在传输最后一个字节时,通过专用信号线通知USB模块。 - 短包与零长度包:如果一次传输的数据总量不是最大包大小的整数倍,最后一个包就是“短包”,它标识了传输结束。如果数据总量恰好是整数倍,则需要在发送完所有数据包后,额外发送一个零长度包(ZLP)来标识结束。发送ZLP的方法是设置
USB_EPn_STATCR寄存器的ZLPS位,硬件会自动发送一个零长度包并在成功后清除该位。 - 传输完成后,会触发
EOT中断。
4.3 中断传输:周期性的小数据
中断传输在协议上保证了固定的查询间隔,用于传输少量、需及时响应的数据,如HID设备的按键状态。
从设备驱动角度看,其处理与批量IN传输极其相似:也是准备数据包,标记帧结束,等待发送。唯一的关键区别在于:每次中断传输(无论包长多少)都会触发EOT中断。这意味着每个中断事务都被硬件视为一个独立的传输。
这就带来了一个重要的时序约束:你的设备驱动程序必须在主机下一次查询该中断端点之前,服务完本次的EOT中断,并准备好新的数据(如果需要)。如果没能及时准备好,设备会回复NAK,主机将在下一个查询间隔重试。但如果连续多次NAK,可能会被主机认为设备故障。
4.4 程序I/O与DMA模式选择
- 程序I/O:通过CPU直接读写
USB_EPn_FDAT寄存器。实现简单,但占用大量CPU时间,适合低速或数据量小的端点(如控制端点0)。 - DMA模式:将数据搬移工作交给DMA控制器,极大解放CPU。需要正确配置DMA通道的源/目标地址、传输量,并连接USB模块的DMA请求线。在批量传输中,为了与USB的包结构对齐,DMA传输的触发最好与FIFO报警值(
FALRM)配合。设置为一个包大小,可以确保DMA每次被触发时,正好搬移一个完整的数据包到FIFO,效率最高。
5. 异常处理与调试技巧
即使代码逻辑正确,在实际的USB通信中也会遇到各种异常。模块的硬件处理了一部分,但有些需要软件干预。
5.1 无法完成的设备请求
当你的驱动收到一个无法识别或不支持的Setup请求时,不能置之不理。正确的做法是:在完成对Setup包的读取后,同时设置CMD_ERROR和CMD_OVER位。这会导致硬件对该控制端点返回STALL握手信号给主机。STALL是一个明确的错误指示,主机会感知到并采取相应措施(如重置该端点)。之后,你需要等待CMD_OVER位被清除,这表示主机已经清除了这个STALL状态,端点可以���复通信。
5.2 中止的设备请求(重复Setup包)
这是一个经典的USB通信问题。主机发送一个Setup包,设备回复ACK,但这个ACK可能在总线上丢失。主机因未收到ACK而超时重发相同的Setup包。此时,设备的FIFO里就会有两个相同的Setup包。
如何检测?
- 检查
MDEVREQ中断(多个设备请求)。 - 或检查端点0的
USB_EPn_STATCR寄存器中的SIP(Setup In Progress)位是否在未处理完第一个请求时就再次被置位。
如何处理?无论通过哪种方式检测到,处理方法一致:丢弃FIFO中的第一个Setup包,然后处理第二个。你需要从FIFO中读取8字节数据(但不处理),然后通过设置CMD_OVER(无数据阶段)或进行一个完整的控制传输来“清除”这个被丢弃的请求,等待状态阶段完成,再处理下一个真正的请求。
5.3 FIFO溢出/下溢等临时性问题
例如,由于系统负载过高,CPU未能及时服务FIFO_LOW中断,导致主机在IN事务中尝试读取数据时,设备FIFO为空(下溢);或者在OUT事务中,主机发送数据过快,导致设备FIFO满(溢出)。
处理方法是软件发起STALL:设置对应端点的FORCE_STALL位。这会立即中止当前传输,并让该端点进入STALL状态。主机随后会感知到错误,并通过标准的USB错误恢复流程(如获取状态、清除特性)来尝试清除这个STALL。FORCE_STALL位会在STALL生效后自动清除。你的驱动需要在主机清除端点STALL后,重新准备好该端点(可能需要重置FIFO指针)以继续通信。
5.4 灾难性错误与复位
当遇到无法恢复的通信错误、驱动程序状态机混乱或总线长时间无响应时,最后的恢复手段就是复位。
- 软件复位:设置
USB_ENAB寄存器的RST位。这会复位大部分逻辑,但寄存器配置可能保留。 - UDC复位:设置
USB_CTRL寄存器的UDC_RST位。这主要复位UDC核心逻辑,通常在总线连接/断开事件后使用。 - 硬复位:最彻底的方式,通常由外部电路或看门狗触发。设备需要重新执行完整的初始化流程。
在进行任何复位操作前,如果可能,应尝试通过USB_EPn_FWRP等寄存器读取并保存FIFO的残余数据,或者至少记录下错误发生时的状态寄存器信息,这对于后期调试分析非常有价值。复位后,设备会等待主机重新进行枚举,一切从头开始。