2. 串口协议回顾:UART帧结构

讲状态机之前,咱们得先把串口协议本身捋清楚。很多新手一上来就写代码,结果波形出来对不上,查半天发现是协议理解有偏差。我个人习惯是,写代码前先在纸上把帧结构画一遍,心里有数了再动手。

2.1 UART帧长什么样?

UART通信,说白了就是一根线发数据,一根线收数据。没有时钟线,全靠双方约定好的节奏来采样。一个完整的UART帧,通常包含这几部分:

  • 起始位:拉低电平,告诉接收方“我要开始发数据了”
  • 数据位:5~8位,最常见的是8位
  • 校验位:可选,奇校验或偶校验
  • 停止位:拉高电平,表示这一帧结束

嗯,这里要注意:空闲时总线是高电平。所以起始位是下降沿,停止位是上升沿。这个细节在状态机设计里非常关键,我见过有人把空闲和起始搞反了,结果整个状态机跑飞。

核心记忆点:起始位=低电平,停止位=高电平,数据位LSB先发。

2.2 波特率与采样时钟的关系

波特率就是每秒传输的比特数。比如9600波特,意味着每bit持续约104.167微秒。但FPGA里用的是系统时钟,比如50MHz,周期20ns。怎么用20ns的时钟去采样104us的bit?

答案很简单:分频计数。

我曾经在项目里遇到过波特率不匹配的问题。当时用115200波特率,结果接收端算错了分频值,数据全是乱码。查了两天才发现是计数器少加了1。从那以后,我每次算分频都会手算一遍,再用仿真确认。

分频值的计算公式:

分频值 = 系统时钟频率 / 波特率
例如:50MHz / 9600 ≈ 5208

但这里有个坑:采样点选在bit的中间位置最稳。所以实际设计中,我们会把分频值再除以2,得到半个bit的时间。状态机在检测到起始位后,先等半个bit,再从中间位置开始采样。

我的经验:采样时钟最好用波特率的16倍频。这样每个bit可以采16个点,用多数表决的方式判断电平,抗干扰能力更强。我在工业现场的项目里一直用这个方案,效果很好。

2.3 帧结构图解

光说可能不够直观,我画了一张UART帧结构的时序图,你看看就明白了:

UART帧结构时序图(8位数据,无校验,1位停止位) 起始位 (低电平) D0 D1 D2 D3 D4 D5 停止位 (高电平) ▲ 采样点(每个bit的中间位置) 空闲 (高)

你看,起始位拉低后,数据位从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%以内。我曾经用115200波特率配50MHz时钟,分频值434.03,取整434后误差约0.007%,完全没问题。但如果用921600,分频值54.25,取整54后误差0.46%,还在容忍范围内。再高就不建议了。

2.5 为什么状态机适合做UART接收?

你想想看,UART接收本质上就是一个按时间顺序执行的状态切换过程:

  • 空闲时等待起始位
  • 检测到下降沿后,确认起始位
  • 依次采样每个数据位
  • 可选地采样校验位
  • 等待停止位,然后回到空闲

这不就是状态机的天然应用场景吗?每个状态对应一个明确的动作,状态之间的转移由计数器或信号控制。用状态机实现,代码结构清晰,调试也方便。

我个人习惯把UART接收状态机分成5个状态:IDLE、START、DATA、PARITY、STOP。每个状态里只做一件事,逻辑简单,不容易出错。下一节我们就开始具体设计这个状态机。

小结:UART帧结构是状态机设计的基础。搞懂了起始位、数据位、停止位的时序关系,以及波特率与采样时钟的分频计算,后面写代码就是水到渠成的事。