UART偶发乱码:波特率分频误差之外,还要算采样漂移

UART偶发乱码:波特率分频误差之外,还要算采样漂移

上一篇UART收发回显跑通后,常见的下一步是把9600改成115200,再提高到921600。此时若偶发乱码,只看"分频误差不到百分之几"并不足以判断问题。接收端从起始沿建立时间基准,随后靠自己的时钟安排采样;真正需要核算的是,整帧中这些采样点是否仍留在可靠窗口里。

1. 先把两种误差分开

假设FPGA每隔N个系统时钟产生一个采样使能,每位使用M个使能,则实际接收波特率为:

B_RX = f_CLK / (N × M)

这里N是实际周期数。如果计数器从0数到N−1,寄存器或比较常量未必就叫N。下面只讨论均匀整数分频;相位累加器产生的不等间隔使能,还需要单独核算相位抖动。

本地分频误差是B_RX/B_target−1。50 MHz、115200、16倍采样时,N=27,实际为115740.740741,误差+0.469393%。程序比较理想分频值两侧的整数,按实际波特率误差选较优者,不直接截断。

但线路上发送器也有自己的误差。定义:

r = T_RX/T_TX = B_TX/B_RX

若两端相对共同目标的总误差为e_TX、e_RX,则r=(1+e_TX)/(1+e_RX)。必须保留方向,不能把本地+0.47%直接当成链路总误差。晶振初始偏差、温漂和老化还要叠加到各端实际时钟;独立容差做最坏情况检查时,应检查两端一快一慢的组合。

2. 从起始沿算到最后一个检查点

以发送端位周期T_TX为单位,起始沿为0。8N1的数据中心依次在1.5、2.5......8.5,首个停止位中心在9.5。带一位校验的8E1,首个停止位中心变为10.5。每帧重新识别起始沿的接收器,漂移在一帧内累积,不应把整包字节数乘进这个系数。ADI的时钟精度说明也采用逐帧分析,并指出有效采样窗口受线路条件影响。

现在明确一个可计算的接收器假设:起始沿由M倍采样时钟捕获,未补偿的检测延迟δ在0到r/M之间;检测后不再做帧内相位修正;起始位确认也使用同样的中心及三点结构。理想中心为a的采样点,其中心误差就是:

E(a) = a × (r−1) + δ

上一篇RTL用系统时钟检测低电平,采样使能自由运行,还存在输入同步延迟;本节的默认假设并非那份RTL的逐周期模型。若同步链、滤波器或计数起点还引入额外延迟,必须按真实时序修改δ范围,不能认为本程序已经包含它们。

令可靠窗口为中心两侧各w个发送位周期。示例取w=0.375,即中间75%,这是预算假设,并非电气接口保证。单点采样只检查中心;三点投票若采样间隔为一个过采样周期,额外位置为−r/M、0、+r/M。于是全部三点的最早、最晚误差分别为:

E_min = a(r−1) − r/M

E_max = a(r−1) + 2r/M

要求它们严格落在(−w,+w),得到该检查点的余量min(w+E_min, w−E_max)。程序遍历起始位、数据、可选校验及指定停止位,取全帧最小值。

3. 三点投票、停止位都要写清楚

这里要求三个采样点全部处于可靠窗口,是保守充分条件。真实多数判决只需其中两点正确,程序没有模拟其误码行为;负余量表示本预算无法保证,不能直接说硬件必然收错。

提高过采样倍数能缩短本模型中的起始沿量化误差,但同时改变整数分频粒度,未必总能得到更好的最终预算。Microchip的Error Tolerance说明按采样模式及数据加校验长度列出不同范围,推荐接收误差还假定两端分摊总误差。因此,离开具体接收结构和帧格式,单独宣称"UART允许±2%"或"±5%"都不严谨。

停止位也不能机械地多算一个周期。Infineon UART组件手册明确其接收器不对第二停止位做帧错误检查。本程序默认只检查第一个停止位;只有实际设计检查第二个中心,才把stops_checked改为2。额外停止位可能给下一帧留出时间,却不会把此前数据位的漂移倒回去。

4. 用结果指导排查

下面程序只依赖Python 3标准库,保存为demo.py后执行python3 demo.py。输出是实际运行记录。默认16倍采样、8N1、全部三点入窗;发送端先按目标波特率计算。

50 MHz/115200得到正余量;12 MHz/115200的整数分频误差则达到−6.994048%,超出本窗口。再把前一组接收时钟设为±100 ppm、发送时钟设为±10000 ppm,检查四个容差角点,余量缩为0.173544个发送位周期。同样的2%周期比偏差下,8E1比8N1少0.02个位周期余量,正是多一位带来的漂移。

读输出时还有两个容易忽略的地方。第一,margin_TXbit的单位是发送端位周期,不是百分比;余量为0也不算通过,因为窗口边界没有留出保护;代码用1e−12个位周期作浮点判定保护,并非物理容差。要换成时间,可除以当前发送波特率。第二,50 MHz例子的标称余量略高于完全同频时的0.25,这不代表增加频偏会普遍改善接收。原因是本模型的起始延迟只向后偏,而较快的接收时钟会把后续采样往前拉;两者在这个方向上部分抵消。反向频偏会加剧偏移,所以只测试一个方向容易误判。

修改参数时,vote_radius=0表示单点检查,parity=1表示加入一位校验,half_window控制可靠区间。发送端若也采用整数分频,应先算出它的实际波特率,再传给worst_margin;其中ppm只添加尚未包含的时钟容差,避免重复累计。若两端共用同一参考源,其误差可能相关,四角独立扫描会偏保守,应改成符合实际相关性的组合。

工程排查可以依次记录:实际分频周期、两端时钟范围、起始检测与采样拍号、帧格式以及可靠窗口。最后再用连续帧、长短帧和有跳变的数据检查边界。这个计算器验证的是确定性时间预算,不是RTL仿真或板级测试;电平、噪声、边沿失真及帧重启行为仍需在真实接口上验证。

完整代码

python 复制代码
#!/usr/bin/env python3
"""Conservative UART timing-window calculator. Python 3, standard library only."""
from itertools import product
from math import floor, isclose, isfinite

EPS = 1e-12  # numerical guard in TX-bit units, not a physical tolerance


def divider(clock, target, oversample):
    """N clock cycles per sample tick; compare both adjacent integer divisors."""
    if not all(isfinite(v) and v > 0 for v in (clock, target, oversample)):
        raise ValueError("positive parameters required")
    ideal = clock / (target * oversample)
    candidates = {max(1, floor(ideal)), max(1, floor(ideal) + 1)}
    n = min(candidates, key=lambda n: (abs(clock / (n * oversample) / target - 1), n))
    actual = clock / (n * oversample)
    return n, actual, actual / target - 1


def margin(tx, rx, m=16, data=8, parity=0, stops_checked=1,
           half_window=0.375, vote_radius=1):
    """Units: TX bit periods. All sample footprint points must be in window.

    Edge delay is [0, T_RX/m]; vote_radius=0 is one central sample,
    vote_radius=1 requires ALL three samples (not exact majority decoding).
    Only the explicitly checked stop centers are included.
    """
    if not (isfinite(tx) and isfinite(rx) and tx > 0 and rx > 0 and m in (8, 16)
            and 5 <= data <= 9 and parity in (0, 1)
            and stops_checked in (1, 2) and 0 < half_window <= 0.5
            and vote_radius in (0, 1)):
        raise ValueError("unsupported receiver assumptions")
    r = tx / rx  # T_RX / T_TX, NOT RX/TX baud-rate ratio
    centers = [0.5] + [1.5 + k for k in range(data + parity + stops_checked)]
    result = float('inf')
    for a in centers:
        early = a * (r - 1) - vote_radius * r / m
        late = a * (r - 1) + (1 + vote_radius) * r / m
        result = min(result, half_window + early, half_window - late)
    return result


def worst_margin(tx, rx, tx_ppm=0, rx_ppm=0, **kwargs):
    if not (0 <= tx_ppm < 1e6 and 0 <= rx_ppm < 1e6):
        raise ValueError("invalid oscillator tolerance")
    # Linear sampling coordinates in r; extrema are at oscillator corners.
    return min(margin(tx * (1 + st * tx_ppm / 1e6),
                      rx * (1 + sr * rx_ppm / 1e6), **kwargs)
               for st, sr in product((-1, 1), repeat=2))


def self_test():
    n, b, e = divider(50_000_000, 115_200, 16)
    assert n == 27 and isclose(b, 115740.74074074074)
    assert isclose(e, 0.00469393004115215)
    assert isclose(margin(1, 1), 0.25)
    assert isclose(margin(1, 1, vote_radius=0), 0.3125)
    assert isclose(margin(1, 1, m=8), 0.125)
    assert margin(115200, b) > 0
    assert margin(115200, divider(12_000_000, 115200, 16)[1]) < 0
    assert margin(1.02, 1, parity=1) < margin(1.02, 1)
    assert margin(1.02, 1, stops_checked=2) < margin(1.02, 1)
    assert isclose(worst_margin(115200, b), margin(115200, b))
    assert worst_margin(115200, b, 10000, 100) < margin(115200, b)
    # Verify endpoint envelope against a phase and frequency grid.
    for r in (0.97, 1.0, 1.03):
        analytic = margin(r, 1)
        brute = min(0.375 - abs(a * (r - 1) + phase * r / 16 + j * r / 16)
                    for a in [0.5] + [1.5 + k for k in range(9)]
                    for phase in (0, .25, .5, .75, 1) for j in (-1, 0, 1))
        assert isclose(analytic, brute)
    assert margin(9.125 / 9.375, 1, m=8) <= EPS
    print('SELF_TEST: 15 checks passed (including 3 phase-grid cases)')


if __name__ == '__main__':
    self_test()
    print('clock_Hz target N actual_baud error_pct margin_TXbit verdict')
    for clk, target in ((50_000_000, 115200), (48_000_000, 115200),
                        (12_000_000, 115200), (50_000_000, 921600)):
        n, baud, error = divider(clk, target, 16)
        budget = margin(target, baud)
        print(f'{clk} {target} {n} {baud:.6f} {100*error:+.6f} '
              f'{budget:+.6f} {"PASS" if budget > EPS else "OUTSIDE"}')
    b = divider(50_000_000, 115200, 16)[1]
    print(f'50MHz RX +/-100ppm, TX +/-10000ppm: '
          f'margin={worst_margin(115200, b, 10000, 100):+.6f} TXbit')
    for parity in (0, 1):
        print(f'TX/RX baud=1.02, 8{"E" if parity else "N"}1: '
              f'margin={margin(1.02, 1, parity=parity):+.6f} TXbit')

实际运行输出

text 复制代码
SELF_TEST: 15 checks passed (including 3 phase-grid cases)
clock_Hz target N actual_baud error_pct margin_TXbit verdict
50000000 115200 27 115740.740741 +0.469393 +0.252920 PASS
48000000 115200 26 115384.615385 +0.160256 +0.251000 PASS
12000000 115200 7 107142.857143 -6.994048 -0.473800 OUTSIDE
50000000 921600 3 1041666.666667 +13.028067 -0.775304 OUTSIDE
50MHz RX +/-100ppm, TX +/-10000ppm: margin=+0.173544 TXbit
TX/RX baud=1.02, 8N1: margin=+0.057500 TXbit
TX/RX baud=1.02, 8E1: margin=+0.037500 TXbit
相关推荐
hexiaoyan82710 小时前
271-基于JFM7VX690T36 的四路40Gbps的PCIeX8 全国产化卡
fpga开发·软件无线电处理平台·硬件加速器·jfm7vx690t36·全国产化卡·7vx690t36
Terasic友晶科技1 天前
答疑解惑|Agilex 5如何指定Boot Core及设定Core的运行频率?
fpga开发·hps·terasic·cortex-a55·cortex-a76·boot core
辰丁电子1 天前
allegro入门教程-菜单栏Route
fpga开发·pcb工艺·pcb·layout·allegro软件
吉他英雄一里1 天前
FPGA工具篇--ILA
fpga开发
鸥梨菌Honevid1 天前
UVM sequence-driver 通信模型(详细讲解)
fpga开发
博览鸿蒙1 天前
数字芯片设计实战路线:硬件工程师的 FPGA
fpga开发
zhutier-1 天前
Vivado+Artix7+W25Q128JVEIQ FPGA固化程序到FLASH里面教程
fpga开发
hexiaoyan8271 天前
FMC 子卡设计原理图:154-基于FMC 八路SFP+万兆光纤子卡
fpga开发·光纤收发·fmc 子卡·万兆光纤子卡·sfp+万兆光纤
北京青翼科技2 天前
青翼基于PCIe总线架构的4路HD-SDI/3G-SDI视频图像模拟源丨图像处理
fpga开发·图像模拟源·fpga开发板·pcie板卡