从乱码里把波特率找回来
目标
亲手从采集到的采样序列中解码 UART 帧,并在发送时钟错位的采集中重新测出真实的位时间,让可读的句子重新出现。在这个过程中,用数字统计奇偶校验和帧错误各自抓住了什么、漏掉了什么。
为什么重要
UART 没有时钟线。双方必须信任同一个位时间,而验证这种信任的手段,只有每个帧一次的起始位。因此时钟一旦错位,帧就会从后半部分开始崩溃,而症状只会以“字符乱码”的形式上报。本实验就是练习把这类报告变成错位了百分之几这样的数字。关键在于,修正的不是值,而是时间轴,而这条时间轴的依据,已经包含在采集文件里了。
步骤
- 为采样序列建立时间轴 — 用头部的采样率把采样点序号换算成时间,并留下概览。
- 提高阈值,边沿就会移动 — 实现 rising_edges,并测量两个阈值下检出时间的差。
- 编写帧解码器 — 实现 decode,读取距起始沿 (k + 0.5) 个位时间处的采样点。
- 读取正常采集 — 在帧错误为 0 的情况下得到句子。
- 抽稀采样点,找出判读崩溃的位置 — 以 1、4、10、25、50 的间隔重新解码。
- 用标称波特率读取时的症状 — 测量错位采集的帧错误和最短脉冲。
- 重新测出位时间并还原句子 — 从粗略值开始,用较长的基线逐步收窄。
- 统计奇偶校验漏掉的部分 — 数出悄悄出错的帧有多少个。
参考
- 素材是
/opt/fixtures/serialbus/uart/下的三份文件:hello-9600.csv(正常)、drift-8n1.csv、drift-8e1.csv。它们是只读的,不要修改。格式说明在上一级文件夹的/opt/fixtures/serialbus/README.md中。 - 读取采集文件的函数请放在
capture.py这样的单个文件里,不要每一步都重写。参考答案也是这样做的。 - 文件的
n列是采样点序号。时间是n / sample_rate_hz秒,文件中没有时间列。 - 常见错误 1:按从高位开始的顺序收集数据位。UART 是从低位开始的。
- 常见错误 2:把每个位的采样点数四舍五入成整数。以 500 kHz 接收 9600 Bd 时是 52.083,取整之后在帧的末尾就会错位。
- 不需要额外安装,也不需要联网,只使用 python3。
- 预计需要 70 分钟,所以请在默认的 60 分钟结束之前,用“+时间”延长(最多 180 分钟)。会话结束后
/root会消失,想保留的内容请另行保存。
为采样序列建立时间轴
用 mkdir -p /root/bus-uart 创建工作文件夹,读取 /opt/fixtures/serialbus/uart/hello-9600.csv,把 sample_rate_hz(从头部读到的值)、sample_count(采样行数)、duration_us(采样点数 x 1,000,000 / 采样率)、min_v、max_v、idle_level(第一个采样点不低于 1.65 V 为 "high",否则为 "low")保存到 /root/bus-uart/survey.json。
头部是以 # 开头的行,sample_rate_hz 就在其中。下一行是列名,再下面是采样点。时间不在文件里,要用采样点序号和采样率计算。
提高阈值,边沿就会移动
在 /root/bus-uart/edges.py 中实现 rising_edges(volts, threshold_v)。返回满足 volts[i-1] 低于阈值且 volts[i] 不低于阈值的所有 i,按升序排列成列表(i 从 1 开始)。然后对 hello-9600.csv 分别应用 1.65 V 和 2.90 V 两个阈值,把 count_1v65、count_2v90、first_1v65、first_2v90、shift_samples(first_2v90 减去 first_1v65)、shift_us 保存到 /root/bus-uart/threshold.json。
如果边沿的斜率是有限的,较高的阈值会更晚才被越过。请确认两个阈值得到的个数相同,只是时间不同。评分器也会用与采集文件无关的简短表格来测试这个函数。
编写帧解码器
在 /root/bus-uart/uart.py 中实现 decode(volts, sample_rate_hz, baud, threshold_v=1.65, parity=None)。找出从空闲(高)落到起始位的边沿 i,把距 i 有 (k + 0.5) 个位时间的采样点用 int() 取整,读作第 k 位。数据为 8 位,从低位开始。把每个帧装入 {"start": i, "value": 字节, "framing_ok": 停止位是否为 1, "parity_ok": 奇偶校验是否匹配} 后以列表返回,并从读取停止位的采样点的下一个位置开始寻找下一个边沿。parity 是 None 或 "even"。
每个位的采样点数是 sample_rate_hz / baud,它不是整数。parity 为 None 时,parity_ok 始终为真。评分器会先用手工构造的测试表(正常帧、停止位为 0 的帧、只翻转了奇偶位的帧)来确认。
读取正常采集
把 hello-9600.csv 按 9600 Bd 解码,把 frame_count、framing_errors、bytes(整数列表)、text(把字节原样连成字符的字符串)保存到 /root/bus-uart/message.json。
帧错误为 0 才算正常。如果不是 0,请重新检查每个位的采样点数或寻找起始沿的条件。
抽稀采样点,找出判读崩溃的位置
把 hello-9600.csv 的采样序列按 1、4、10、25、50 的间隔抽稀(volts[::k]),把各自的采样率设为 rate // k,再按 9600 Bd 重新解码。在 /root/bus-uart/sampling.json 中按此顺序放入 5 个 cases,每项包含 decimate、sample_rate_hz、samples_per_bit、text_ok(字符串是否与未抽稀的结果相同)。并把 text_ok 第一次变为假的间隔写入 first_failing_decimate。
当每个位的采样点数接近 1 时,解码器不会报错,而是给出另一个字符。text_ok 必须是布尔值,而不是字符串 "true"。
用标称波特率读取时的症状
把 /opt/fixtures/serialbus/uart/drift-8n1.csv 按 9600 Bd 解码,把 frame_count、framing_errors、bytes 保存到 /root/bus-uart/drift.json。此外,收集电平发生变化的采样点序号,求出相邻间隔的最小值,写作 shortest_pulse_us,把 1,000,000 / 9600 写作 nominal_bit_time_us,二者之比写作 ratio。
最短的间隔是一个位。这个值与标称位时间相差百分之几,就是这份采集的诊断结果。请确认帧数与正常采集相近,而只有帧错误增加了。
重新测出位时间并还原句子
从 drift-8n1.csv 重新测出位时间。先取边沿间隔的最小值作为粗略值,再逐步拉长基线来收窄——在从第一个边沿起(当前估计值 x 16)个采样点之内,选出最远的边沿,把这个距离除以估计值并四舍五入得到整数,再用这个整数去除,然后把倍数每次扩大 4 倍,重复到最后一个边沿。把结果保存到 /root/bus-uart/measured.json,包含 bit_time_us、baud(1,000,000 除以 bit_time_us)、用该波特率重新解码的 text,以及 framing_errors。
不能一次就按整个长度取整——如果粗略值的误差乘以位数超过 0.5 位,就会选出错误的整数。仅凭粗略值也能读出字符,但评分要求误差在 0.2% 以内,所以不能跳过收窄的步骤。
统计奇偶校验漏掉的部分
把 /opt/fixtures/serialbus/uart/drift-8e1.csv 按偶校验解码两次——一次用重新测出的位时间(这一次是真值),一次用 9600 Bd。把 frames、parity_errors、framing_errors、flagged(两者之一被抓到的帧数)、wrong_bytes(值与真值不同的帧数)、silently_wrong(值不同,但奇偶校验和帧检查都通过的帧数)、text_at_measured 写入 /root/bus-uart/parity.json。
奇偶校验只看 1 的个数的奇偶性。翻转偶数个时会照样通过,所以 flagged 可能小于 wrong_bytes。请把两次解码的结果按帧的顺序配对比较。