2. 串口协议回顾:UART帧结构
讲状态机之前,咱们得先把串口协议本身捋清楚。很多新手一上来就写代码,结果波形出来对不上,查半天发现是协议理解有偏差。我个人习惯是,写代码前先在纸上把帧结构画一遍,心里有数了再动手。
2.1 UART帧长什么样?
UART通信,说白了就是一根线发数据,一根线收数据。没有时钟线,全靠双方约定好的节奏来采样。一个完整的UART帧,通常包含这几部分:
- 起始位:拉低电平,告诉接收方“我要开始发数据了”
- 数据位:5~8位,最常见的是8位
- 校验位:可选,奇校验或偶校验
- 停止位:拉高电平,表示这一帧结束
嗯,这里要注意:空闲时总线是高电平。所以起始位是下降沿,停止位是上升沿。这个细节在状态机设计里非常关键,我见过有人把空闲和起始搞反了,结果整个状态机跑飞。
2.2 波特率与采样时钟的关系
波特率就是每秒传输的比特数。比如9600波特,意味着每bit持续约104.167微秒。但FPGA里用的是系统时钟,比如50MHz,周期20ns。怎么用20ns的时钟去采样104us的bit?
答案很简单:分频计数。
我曾经在项目里遇到过波特率不匹配的问题。当时用115200波特率,结果接收端算错了分频值,数据全是乱码。查了两天才发现是计数器少加了1。从那以后,我每次算分频都会手算一遍,再用仿真确认。
分频值的计算公式:
分频值 = 系统时钟频率 / 波特率
例如:50MHz / 9600 ≈ 5208
但这里有个坑:采样点选在bit的中间位置最稳。所以实际设计中,我们会把分频值再除以2,得到半个bit的时间。状态机在检测到起始位后,先等半个bit,再从中间位置开始采样。
2.3 帧结构图解
光说可能不够直观,我画了一张UART帧结构的时序图,你看看就明白了:
你看,起始位拉低后,数据位从LSB开始依次发送。每个bit持续的时间由波特率决定。采样点选在bit中间,就是为了避开电平跳变的不稳定区域。
2.4 常见参数速查表
这里我整理了一份常用波特率对应的分频值,方便你查:
| 波特率 (bps) | bit周期 (μs) | 50MHz分频值 | 100MHz分频值 |
|---|---|---|---|
| 9600 | 104.17 | 5208 | 10417 |
| 19200 | 52.08 | 2604 | 5208 |
| 38400 | 26.04 | 1302 | 2604 |
| 115200 | 8.68 | 434 | 868 |
| 921600 | 1.09 | 54 | 108 |
2.5 为什么状态机适合做UART接收?
你想想看,UART接收本质上就是一个按时间顺序执行的状态切换过程:
- 空闲时等待起始位
- 检测到下降沿后,确认起始位
- 依次采样每个数据位
- 可选地采样校验位
- 等待停止位,然后回到空闲
这不就是状态机的天然应用场景吗?每个状态对应一个明确的动作,状态之间的转移由计数器或信号控制。用状态机实现,代码结构清晰,调试也方便。
我个人习惯把UART接收状态机分成5个状态:IDLE、START、DATA、PARITY、STOP。每个状态里只做一件事,逻辑简单,不容易出错。下一节我们就开始具体设计这个状态机。