开发者用小数分频器解决Verilog UART非整数时钟倍频问题
来源标题:UART in Verilog with Fractional Clock Dividers
针对Tang Console软件栈开发中FPGA与MCU通过UART通信时遇到的非整数时钟倍频问题,开发者提出用小数时钟分频技术实现精确波特率。传统整数分频器在100MHz时钟下生成115200波特率需约868.056分频,取整会引入误差,当波特率接近系统时钟时可能导致通信错误。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
通用异步收发器(UART)模块是嵌入式系统中的基础组件,用于实现设备之间的串行通信。虽然网上有许多免费的实现方案,但我在为 Tang Console 开发独立软件栈时遇到了一个新问题:非整数时钟倍频。当运行在不同频率时钟上的 FPGA 核心需要通过 UART 与 MCU 通信时,这个问题就会浮现。与 SPI 由主机决定时钟不同,UART 要求双方遵循预先约定的波特率(我的场景中是 1Mbps)。在这种情况下,传统的整数分频器会产生不精确的波特率,导致通信错误。本文将探讨一种使用分数分频器技术的优雅解决方案。
图 1. UART 帧时序要求精确的波特率对齐
标准 UART 设计通过对系统时钟分频来产生波特率时钟。例如,100 MHz 时钟目标为 115,200 波特时,需要约 868.056 的分频系数。整数分频器会将其取整为 868 或 869,从而引入微小误差。这在许多情况下尚可接受,但当波特率接近系统时钟时,就可能引入错误。例如,21MHz 时钟驱动 2Mbps UART 时,分频系数会被设为 11 个周期。这意味着每位时间为 0.524us 而非 0.5us。经过 10 位之后,误差将累积到 (0.524-0.5)*10=0.24us,危险地接近半个位时间(0.25us)。实际测试表明,这种设置下通信并不稳定。
为了解决这个问题,一个有用的技巧是使用累加器,通过整数运算来跟踪分数间隔(Bresenham 算法)。每一步中,我们:
- 加上分数分母(DIV_DEN)
- 检查是否超过分子(DIV_NUM)而溢出
- 溢出时保留余数
这有效地近似了(生成的时钟会有一些抖动): baud_clock_period = system_clock_period * (DIV_NUM / DIV_DEN)
这种方法的好处在于完全避免了浮点运算和乘法。在 FPGA 设计中,我们通常会避免这些操作,因为它们资源消耗大且可能带来时序问题。相反,该方案仅依赖整数加法和比较,因此非常高效。
我用 Verilog 为接收器和发送器都实现了这一方案(uart_rx.v、uart_tx.v)。以下是接收器中的一段代码:
// 摘自 uart_rx.v reg [$clog2(DIV_NUM)-1:0] cnt; always @(posedge clk) begin reg cnt_overflow; cnt_next = cnt + DIV_DEN; cnt_overflow = cnt_next >= DIV_NUM; if (state != 0) cnt <= cnt_overflow ? cnt_next - DIV_NUM : cnt_next; end
这与我在之前文章中介绍的视频缩放逻辑如出一辙:
// 摘自 framebuffer.sv xcnt_next = xcnt + width; // 原始宽度:256 或 320 if (xcnt_next >= 960) // 目标宽度 960 xcnt <= xcnt_next - 960;
两者共享相同的思路:
- 累加分数步长(DIV_DEN 或 width)
- 检测是否超过目标值(DIV_NUM 或 960)而溢出
- 保留余数继续累加
状态机与整数分频 UART 基本相似。对于接收器(RX):
case (state) 0: begin // 空闲 if (!rx) begin state <= 1; cnt <= 0; bit_index <= 0; rx_data <= 0; end end 1: begin // 起始位,等待半个位时间 if (cnt_next >= DIV_NUM/2) begin state <= 2; cnt <= 0; end end 2: begin // 数据位 if (cnt_overflow) begin rx_data[bit_index] <= rx; if (bit_index == 7) state <= 3; else bit_index <= bit_index + 1; end end 3: begin // 停止位 if (cnt_overflow) begin valid <= 1; data <= rx_data; state <= 0; end end endcase
检测到起始位后,我们等待半个波特周期再采样第一位。这样正好命中波特时钟周期的中心。
DIV_NUM / DIV_DEN = clk_freq / baud_rate
例如,21.477 MHz 时钟目标为 1M 波特时,可使用 DIV_NUM=21477 和 DIV_DEN=1000。
通过采用受 Bresenham 算法启发的分数分频器技术,我们创建了一个灵活的 UART 实现,无需专用时钟硬件即可支持任意波特率。与整数分频器相比,它有若干优势:无论时钟关系如何都能支持精确的波特率,仅使用整数加法/比较运算。此外,参数支持运行时配置。
该设计以 uart_rx.v 和 uart_tx.v 形式提供。
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。