USB2.0 UTMI收发器RTL实现:NRZI、位填充与状态机 简介围绕UTMI协议展开的USB2.0收发器逻辑电路设计资料内容来自中国科学技术大学王永、白雪飞、方毅撰写并发表于《微型机与应用》的正式论文适合数字IC设计、FPGA/ASIC验证方向的学习者以及从事USB接口电路与通信协议开发的工程师、高年级本科生参考。全文以UTMI规范为约束逐层拆解收发器的数字逻辑与模拟接口包括串并/并串转换、位填充与剔除、非归零编解码、弹性缓冲器、同步字包头包尾生成与检测、比特填充错误与挂起唤醒检测、时钟与数据恢复电路以及满足差分信号、对地电阻、电缆断连检测等电气要求的接口电路并给出功能模块图、发送状态机转移图与比特填充实现流程图。资源包仅含1个PDF文件约3.84MB篇幅紧凑便于通读也方便按模块检索复用。目前已有238人学习下载可在指定工艺库下对照仿真、综合与布局布线流程快速理解高速与全速双模收发器的设计要点与排错思路。1. 先把速率这件事说清楚UTMI 到底解决什么问题很多人第一次接手 USB2.0 收发器的活会默认它就是 480Mbps翻完协议才发现「USB2.0 可以全速传输吗 480M」这个问题本身就带着误解USB2.0 协议里高速 480Mbps、全速 12Mbps、低速 1.5Mbps 是三档并列的速率全速和 480M 不是一回事。UTMIUSB 2.0 Transceiver Macrocell Interface的价值恰好在这里它把三档速率切换、差分线驱动、以及位级的 NRZI 编码与位填充打包成一个同步并行接口数字侧只要写状态机和 FIFO就能把收发器接进 SoC 或 FPGA不必碰模拟。接下来拆的是这套接口背后的逻辑电路信号怎么接、状态机怎么画、RTL 怎么写、怎么用 testbench 把一个包发出去再原样收回来。适合做数字前端、FPGA 原型验证或者需要自研 PHY 数字部分的工程师模拟前端只做边界交代。2. UTMI 接口信号与 USB2.0 收发器的位级数据通路2.1 UTMI、UTMI 与 ULPI 三种接口的取舍同一颗模拟前端外面能挂三种不同的数字接口选错了后面每一步都别扭。UTMI 是最直接的一种时钟由 PHY 给出数据线双向控制信号全部平铺没有间接寻址。UTMI 在 Level 0 到 Level 3 之间逐级加上 OTG 支持、ID/VBUS 检测、DP/DM 直通。ULPI 则把 22 根信号压缩到 12 根代价是寄存器访问要一拍一拍地发命令字。接口数据宽度时钟信号数访问方式典型场景UTMI 1.08/16 bit60/30 MHz约 22直连FPGA 原型、SoC 内部集成UTMI L38/16 bit60/30 MHz约 28直连 OTG带 OTG 的便携设备ULPI8 bit60 MHz12寄存器间接pin 敏感的移动芯片我一般会在 FPGA 原型阶段优先选 UTMI原因很土但很实在所有信号都是并行的ILA 一抓就能看到 TxValid 和 TxReady 的握手关系出了问题能直接定位到哪一拍。ULPI 省下的那 10 根线换来的是调试时要在脑子里模拟一遍命令时序原型阶段不划算。2.2 从差分线到 8bit 并行四层拆分把收发器拆开看数字侧接触到的只是最上面一层下面三层在模拟或混合信号域模拟前端。负责 HS 的电流驱动、FS/LS 的电压驱动、终端电阻投切以及 DM/DP 上的差分信号整形。时钟与数据恢复。HS 模式下从 480Mbps 的位流里恢复出位时钟这一级通常和 SERDES 绑在一起。位级处理。NRZI 编解码、位填充与去填充、SYNC 和 EOP 的生成识别这是纯数字逻辑也是自研时最值得自己写的一段。协议接口。弹性缓冲加 UTMI 的收发状态机把位流按字节或字地交给上层。自研的范围通常落在第 3、4 层第 1、2 层买 IP 或用现成硬核。这个边界要在项目启动时就划清楚不然接口对不上波形永远对不齐。2.3 速率选择XcvrSelect、TermSelect 与时钟的对应关系速率不是靠数据内容判断的是靠配置引脚提前选定的。这条是新手最容易忽略的点一个包发到一半不能切速率XcvrSelect 必须在包间隙改。模式XcvrSelectTermSelectUTMI Clk数据宽度线速率高速 8bit00160 MHz8 bit480 Mbps高速 16bit00130 MHz16 bit480 Mbps全速01148 MHz8 bit12 Mbps低速1006 MHz8 bit1.5 MbpsTermSelect 控制内部终端低速设备的上拉电阻在 DM 上全速和高速在 DP 上切错的表现是枚举阶段根本不产生连接事件。低速模式的 6MHz 时钟很容易被忽略如果接收状态机里写了固定周期超时低速下会全线失效。2.4 一个最小 UTMI 发送通路的端口骨架先定端口再谈逻辑。下面这个骨架只保留 HS 8bit 模式必须的信号已经够跑通一个令牌包module usb2_utmi_tx_path #( parameter DATA_W 8 // HS 8bit 模式固定 816bit 模式改成 16 )( input wire clk_utmi, // HS 8bit: 60MHz, FS: 48MHz, LS: 6MHz input wire rst_n, // ---- UTMI 接口侧面向协议层---- input wire txvalid_i, // 上层数据有效 output wire txready_o, // PHY 可接收低时形成背压 input wire [DATA_W-1:0] txdata_i, // 待发送的并行数据 // ---- 配置 ---- input wire [1:0] xcvrselect_i, // 00HS 01FS 10LS input wire [1:0] opmode_i, // 10 可关闭 NRZI 与位填充仅调试用 // ---- 送往模拟侧的串行位流 ---- output reg tx_serial_o, // 已完成 NRZI 编码与位填充 output reg tx_en_o // 有效期间才驱动差分线 );opmode_i等于2b10时绕过 NRZI 和位填充这条通路在调试时非常值钱可以直接往差分线上打已知图案用示波器看眼图把数字侧的错误和模拟侧的问题彻底分开。txready_o必须做成一拍内可撤销的组合或寄存输出上层 FIFO 靠它控制出队节奏迟一拍就会多送一个字节。3. NRZI 编解码、位填充与 SYNC/EOP 的 RTL 实现3.1 NRZI 的编码规则与两行 RTLNRZI 的规则只有一句数据位为 0 时电平翻转为 1 时电平保持。好处是接收侧只要看有没有跳变就能恢复时钟不需要单独的时钟线。// NRZI 编码0 翻转1 保持。初始电平取 J 状态高 always (posedge clk_utmi or negedge rst_n) begin if (!rst_n) nrzi_state 1b1; else if (bit_en) // 每个有效位推进一次 nrzi_state tx_bit ? nrzi_state : ~nrzi_state; end assign tx_serial_o nrzi_state;解码就是同一个表达式rx_bit (rx_serial nrzi_state_prev)。注意初始状态发送侧和接收侧必须约定同一个起始电平否则整包数据全部反相校验永远不过。3.2 位填充连续 6 个 1 之后必须补一个 0位填充是为了防止数据里出现长串的 1导致接收侧长时间没有跳变而丢失时钟同步。计数到 6 个连续的 1就强行插入一个 0。reg [2:0] ones_cnt; always (posedge clk_utmi or negedge rst_n) begin if (!rst_n) ones_cnt 3d0; else if (!bit_en) ones_cnt 3d0; else if (insert_stuff) // 插入的 0 不参与计数 ones_cnt 3d0; else if (tx_bit) ones_cnt (ones_cnt 3d6) ? 3d0 : ones_cnt 3d1; else ones_cnt 3d0; end assign insert_stuff bit_en (ones_cnt 3d6) tx_bit;阈值选 6 而不是 7是因为接收侧要在第 7 个 1 到来之前就把 0 插进去留一拍给比较器的时序余量。插入的 0 本身不能计入下一次计数否则遇到长串 1 会出现连续插入接收侧直接错位。3.3 接收侧去填充与位滑移容错去填充是纯计数收到 6 个连续 1 之后紧跟的那个 0 直接丢弃不进移位寄存器。// 接收侧去填充连续 6 个 1 之后的那一位 0 丢弃 always (posedge clk_utmi or negedge rst_n) begin if (!rst_n) begin ones_cnt_rx 3d0; drop_bit 1b0; end else if (rx_bit_en) begin if (ones_cnt_rx 3d6 rx_bit_raw 1b0) begin ones_cnt_rx 3d0; drop_bit 1b1; // 本拍数据不写入 end else begin ones_cnt_rx rx_bit_raw ? ones_cnt_rx 3d1 : 3d0; drop_bit 1b0; end end end比去填充更难处理的是位滑移。时钟恢复的抖动会让采样点落在位边界附近出现偶发的误判。常见做法是让模拟侧提供过采样数据数字侧做三取二表决如果 PHY 只给单路恢复时钟就只能依赖 RxValid 和 RxActive 这两个信号来框定包的起止越界立刻报 RxError 并丢弃整包。宁可丢包重传也别把错位的数据当成有效负载往上送。3.4 SYNC、EOP 的生成与识别以及 RxActive/RxValid 的时序包的开头和结尾不靠数据内容区分靠固定的位型。全速和低速的 SYNC 是 8 位高速的 SYNC 是 32 位发送侧按速率选长度接收侧我一般用一个固定位长的移位比较器来匹配比逐位状态机省资源也更好约束时序。EOP 高速下常见做法是发一串连续 0 位接收侧用长度阈值判定全速和低速的 EOP 是 SE0 持续两个位时间由 LineState 指示。包阶段LineStateRxActiveRxValidRxErrorSYNCJ100数据J 或 K110EOPSE0100空闲J000异常任意101工程上最容易写错的是 RxActive 的撤销时机。它在 EOP 检测到的那一拍就要拉低晚了会把下一包的 SYNC 吞掉半个字节表现为接收数据整体右移。4. UTMI 收发状态机与弹性缓冲的跨时钟域设计4.1 发送状态机TxValid/TxReady 的握手与背压发送侧的骨架就四个状态写清楚比写复杂更重要localparam TX_IDLE 2d0, TX_SYNC 2d1, TX_DATA 2d2, TX_EOP 2d3; reg [1:0] tx_state; reg [4:0] sync_cnt; always (posedge clk_utmi or negedge rst_n) begin if (!rst_n) begin tx_state TX_IDLE; tx_en_o 1b0; end else case (tx_state) TX_IDLE: if (txvalid_i) begin tx_state TX_SYNC; // 先发 SYNC sync_cnt 5d0; tx_en_o 1b1; end TX_SYNC: begin sync_cnt sync_cnt 5d1; if (sync_cnt sync_len - 1) tx_state TX_DATA; end TX_DATA: if (!txvalid_i) tx_state TX_EOP; TX_EOP: begin if (eop_done) begin tx_state TX_IDLE; tx_en_o 1b0; end end default: tx_state TX_IDLE; endcase end握手的关键在txready_o它代表 PHY 还能不能再吞一个字节。上层 FIFO 的读使能必须写成txvalid_i txready_o只判断 txvalid 会导致数据被重复发送。背压持续时间超过一个包间隔时主机侧会判超时所以弹性缓冲的深度至少要能吸收两到三个字节的突发。4.2 接收状态机从 SYNC 检测到 RxActive 撤销接收侧比发送侧多一层麻烦状态推进是由数据驱动的不是由本地计数器驱动的。我的做法是保持 SYNC 匹配器在空闲态常开一旦匹配成功就进 RX_DATA同时把 RxActive 拉高、RxValid 按字节节拍输出。RxError 的触发条件主要有三条去填充计数异常、CRCs 不匹配、以及 EOP 之前出现长度不足的包。任何一条成立整包丢弃并上报上层。4.3 弹性缓冲60MHz 与 480MHz 之间的水位控制高速模式下写侧是 480Mbps 的恢复位时钟域读侧是 60MHz 的 UTMI 时钟域两边频率比正好是 8但存在 ppm 级偏差缓冲就是用来吸收这个偏差的。我一般用 16 深度的双口 RAM指针用格雷码跨域同步。localparam EB_DEPTH 16; // 格雷码指针跨域同步后比较避免多比特同时翻转带来的亚稳态 wire [4:0] wr_gray_sync; wire [4:0] rd_gray_sync; sync_2ff #(.W(5)) u_wr_sync (.clk(clk_utmi), .din(wr_gray), .dout(wr_gray_sync)); sync_2ff #(.W(5)) u_rd_sync (.clk(clk_480), .din(rd_gray), .dout(rd_gray_sync)); wire eb_empty (rd_gray wr_gray_sync); wire eb_full (wr_gray {~rd_gray_sync[4:3], rd_gray_sync[2:0]}); // 留一格余量水位低于 4 时说明读侧太快继续读会空高于 EB_DEPTH-2 时说明写侧太快只能靠背压让上游等一拍。这里的亚稳态处理不能省二进制指针跨域是新手最常见的翻车点。4.4 用 testbench 灌包验证不用等整机联调先在一个模块级 testbench 里把包发出去initial begin rst_n 0; xcvrselect_i 2b00; opmode_i 2b00; txvalid_i 0; #200 rst_n 1; (posedge clk_utmi); txvalid_i 1; for (int i 0; i 8; i) begin txdata_i pkt[i]; (posedge clk_utmi); while (!txready_o) (posedge clk_utmi); // 严格按握手推进 end txvalid_i 0; repeat (20) (posedge clk_utmi); $finish; end这个激励的重点是while (!txready_o)那一行它模拟了真实上层 FIFO 的行为。去掉它跑一遍波形上看不出问题真机上就是偶发的数据错位。5. 时序收敛、速率回退与三个必看的断言5.1 480MHz 域的处理恢复时钟域里的组合逻辑深度要压到最低NRZI 解码和去填充的比较器尽量做成一级寄存输出。如果综合后这条路径的 slack 是负的优先砍组合深度而不是加流水线加流水线会和弹性缓冲的写指针算在一起反而更容易出错。跨域信号除了指针其余一律单比特。5.2 速率回退与 USB2.0 开关芯片场景很多设计在高速握手失败后回退到全速回退逻辑要重发一次复位序列而不是直接把速率配置改掉就继续发数据。另外当收发器前面串了一颗 USB2.0 开关芯片做通道切换时开关的导通电阻和寄生电容会直接反映到高速眼图上回退概率明显升高。判断方法很直接旁路开关再跑一遍同样的包如果回退消失问题就在开关而不在 PHY。5.3 三个必看的断言写完不要只跑仿真看打印加三条断言能省掉大量调试时间// 1. RxValid 不允许在 RxActive 为低时拉高 assert property ((posedge clk_utmi) disable iff (!rst_n) rxvalid !rxactive |- 0); // 2. 发送态下 SYNC 不能被上层数据打断 assert property ((posedge clk_utmi) disable iff (!rst_n) tx_state TX_SYNC |- $stable(txdata_i)); // 3. 弹性缓冲不允许同时读写同一地址 assert property ((posedge clk_utmi) disable iff (!rst_n) !(eb_full eb_empty));第一条盯的是状态机边界第二条盯的是同步阶段的输入稳定性第三条盯的是指针同步逻辑。覆盖率上把三种速率、两种数据宽度、以及 opmode 的旁路模式各跑一遍交叉覆盖能覆盖到的组合基本就覆盖了实际项目中会踩的坑。本文还有配套的精品资源点击获取