做过嵌入式的人,多少都和这三个词打过交道:串并行、同步异步、单工双工。尤其在ARM平台上写驱动、调外设时,这几个概念绕不开。通信协议的名目再多,真正要做的判断,就是给一对设备之间约定一种“说法”,让双方在电信号层面能互相读懂0和1。这篇文章把ARM环境里最常用的通信方式拆开讲,包括它们怎么选、怎么配、怎么调,以及实际项目中容易踩的坑。
适合两类人看:一类是刚接触嵌入式、想理顺UART、SPI、I2C之间关系的初学者;另一类是已经能跑例程,但遇到乱码、时钟对不齐、总线冲突时,想系统排查的开发者。内容不绑定特定开发板,所有配置以寄存器级伪代码为主,换了芯片也能参考同样的思路。
1. 先建立全局:串并行、同步异步、单工双工到底在说什么
很多教程一上来就列协议,但协议只是表面,底层是三类通信维度的不同组合。先把这三个维度单独理解透,后面看UART、SPI、I2C这些名字,就不会再晕。
1.1 串行与并行:用时间换引脚,还是用引脚换速度
串行通信只有一根数据线(或者一组差分线),数据一位一位按顺序发。并行通信同时用多根数据线,一起发多个位,比如8位并行就是8条数据线同时各发一个bit。
为什么不全部用并行?因为并行看似快,代价是引脚数量、PCB走线面积、信号干扰全部成倍增加。8位并行需要8根数据线再加时钟和控制线,数据线之间还有信号偏移问题,频率高了就非常头疼。ARM芯片的引脚是宝贵资源,一个LQFP封装也就几十到一百多个引脚,要供电、要接地、要连接无数外设,不可能每个外设都用8条线传输。串行通信用一根线换低速,但能省下大量引脚、减少干扰,还能做得更远,所以在芯片间、板级甚至长距离通信里,串行成了绝对主流。
1.2 同步与异步:有没有一根专门的时钟线
同步通信的特点是收发双方共享同一个时钟信号。主机发送数据的同时,也输出时钟沿,从设备在这条时钟的上升沿或者下降沿去采样数据线。因为时钟是同步的,双方不需要对速度做精确约定,理论上速率可以做到很高,数据线电平变化和时钟沿的对应关系由硬件保证。
异步通信就没有这根时钟线。收发双方各用自己的时钟,数据线上一开始是空闲电平,发送端要发一个起始位,告诉对方“我开始了”,然后按双方约定的比特率把一帧数据发出去,最后还要加停止位表示结束。由于没有共同时钟,双方必须提前把波特率设成一致,否则采样位置会越来越偏,最后收到的就是乱码。
生活里打比方:同步通信像两个人中间放了一个节拍器,跟着同一节奏说话;异步通信像两个人没节拍器,只能约定“每秒说几个字”,然后开场先说一句“请注意,我要说了”,说完还得喊“我说完了”示意结束。
1.3 单工、半双工、全双工:数据能不能同时双向流通
单工通信是单向的,发送方永远是发送方,接收方永远是接收方。广播、遥控器与接收机这类场景,数据天生只朝一个方向流。
半双工是双方都能发,但不能同时发,同一时刻只能有一个方向在占线。对讲机就是典型,你说完我才能说,中间要有一句“完毕”来切换方向。I2C总线也是半双工,一根SDA线既负责发送又负责接收,主机要读数据时,必须先发地址和寄存器地址,然后释放总线,再等从设备把数据放上去。
全双工是收发可以同时进行,双方各用一条独立的数据线。UART的TX和RX是分开的,SPI的MOSI和MISO也是分开的,所以这两类协议天然支持全双工。全双工不只是“速度快一点”那么简单,它在交互类协议里意义很大,因为接收数据和发送数据不互相阻塞,可以实现边发边收的处理逻辑。
1.4 三个维度不是三选一,而是同一协议的三张标签
一个具体的通信协议,可以同时用这三组维度来描述。比如UART就是“串行 + 异步 + 全双工”,SPI是“串行 + 同步 + 全双工”,I2C是“串行 + 同步 + 半双工”。有些老式并行液晶屏接口则是“并行 + 同步 + 单工/读写分开”。
三张标签一贴,协议的定位就很清楚了。选型时不用死记,而是看场景缺什么:引脚少选串行,要速度快选同步,需要双向实时交互选全双工。后面所有ARM外设配置,本质都是去实现这三个标签对应的硬件逻辑。
2. 在ARM开发里最常见的组合与选型
ARM内核本身只是计算单元,真正和外界打交道全靠外设接口控制器。你写代码时通常不会直接操作GPIO电平来模拟协议,而是配置某个通信外设的寄存器,由硬件模块自动完成时序。了解典型组合,能帮你少走很多弯路。
2.1 UART:异步串行全双工,嵌入式里最像“万能胶”的接口
UART的全称是通用异步收发器,ARM芯片里通常叫USART,多了一个“S”代表支持同步模式,但绝大多数场景用的是异步模式。
UART的典型接法是TX接对方RX,RX接对方TX,双发共用GND。因为异步通信需要双方时钟大体一致,所以波特率是核心参数。常用波特率有9600、115200等,115200是很多蓝牙模块、GPS模块、调试串口的默认值。为什么常用115200?因为它近似于约11万bit每秒,换算成字节每秒约11520字节,在1MHz以下的老式晶振体系里比较容易分频,后来又成了行业惯例。
在实际ARM项目中,UART承担了大量“简单但必须”的工作:系统日志打印、AT指令控制无线模块、与单片机之间交换状态数据。很多工程师对UART的印象是“慢”,但它兼容性极好、接线最少、调试最直观。我曾经用UART连接一个外部传感器的调试口,只靠两线就把日志数据全部拉出来,排查问题非常顺手。
2.2 SPI:同步串行全双工,追求速率和确定性时的首选
SPI通常有4条线:SCLK时钟、MOSI主出从入、MISO主入从出、CS片选。它是同步串行通信,主设备主动产生时钟,因此速率上限可以比UART高一大截。ARM芯片上的SPI外设,分频后往往能跑到几十兆甚至上百兆赫兹,这已经接近不少并行接口的速度了。
SPI特别适合高速数据流场景:外部Flash读取、LCD驱动、SD卡、ADC采集、音频Codec等。SPI还有非常灵活的“规则”:时钟极性和相位都可以配,多个从设备可以挂在不同片选线上,同一时刻只选中一个,天然避免了总线冲突。
不过SPI也有自己的麻烦,就是线数多、没有标准化协议,先发地址还是先发命令、字节高位先行还是低位先行,完全看从设备手册。这在选型和写驱动时容易踩坑,后面第三部分会专门讲。
2.3 I2C:同步串行半双工,用最少的线解决大量低速设备连接
I2C只需要SCL时钟线和SDA数据线,挂在上面的每个设备有自己的地址。ARM作为主机时,先发起START信号和7位从机地址,再从机应答后才能继续读写。由于SDA是双向的,I2C是半双工,主机在读数据时,必须把发送控制权让给从机,软件上要注意方向切换时机。
I2C的好处是接线少,多个设备并联很轻松,特别适合板内传感器、EEPROM、实时钟芯片这类低速设备。坏处是速率相对较低,常见有标准模式100kbit/s、快速模式400kbit/s、快速+模式1Mbit/s,再高的模式对PCB布局和上拉电阻要求就高了。另外I2C是开漏结构,需要外接上拉电阻,阻值会影响上升沿时间和最大速率,这个细节经常被忽略。
2.4 并行接口在ARM时代还剩下什么用武之地
很多人觉得并行接口过时了,其实没有。ARM处理器访问外部NOR Flash、SDRAM、SRAM时,用的还是并行数据总线。带并行RGB接口的LCD屏,一条条像素数据都要靠并行线传过去,才能满足刷新率要求。工业总线里也有并行方式,但由于引脚多、抗干扰差,慢慢被串行取代。
相比串行,并行通信的编程模型反而简单,因为地址线、数据线、读写控制线一一对应,写一个地址再传一个数据,时序非常直观。ARM芯片里的外部存储控制器就是干这个的,它会自动完成地址建立、数据建立、保持时间等时序参数。选并行接口的代价是引脚占用和PCB连线成本,设计时要先算清楚是否值得。
3. 拿UART和SPI做例子:波形、参数计算与初始化步骤
只记住协议特点还不够,真正落地时要面对寄存器配置。我用ARM环境下的UART和SPI初始化为例,把参数怎么算、时序怎么对、波形怎么理解说清楚。代码以寄存器伪代码形式写,目的是讲原理,不是让你不读芯片手册就照抄。
3.1 UART异步帧格式与波特率计算
UART一帧数据通常包含:1位起始位(低电平)、5到8位数据位(常见8位)、1位可选校验位、1位停止位(高电平)。空闲时TX线保持高电平,起始位的下降沿就是接收方启动采样的触发点。
波特率计算需要先知道外设时钟和采样倍率。很多ARM芯片的串口模块是16倍过采样,也就是每个数据位被采样16次,因此分频系数为:
分频系数 = 外设时钟 / (16 × 目标波特率)举例:外设时钟60MHz,目标波特率115200。
分频系数 = 60_000_000 / (16 × 115200) ≈ 32.55硬件寄存器通常把分频系数拆成整数部分和小数部分。整数部分是32,小数部分0.55要根据芯片手册换算成对应的分数段。有些芯片直接用一个寄存器表示,有些则分开。写代码之前,必须先确认外设时钟具体是多少,因为实际波特率误差会直接影响通信稳定性。
一个常见误区是“目标波特率=分频数×16”,实际上那个16已经在公式里了。我在项目里就见过有人少除了16,导致实际波特率疯狂偏高,串口助手怎么调都不对。
寄存器初始化伪代码大致这样:
// 打开串口外设时钟 RCC->ENABLE |= USART_CLK; // 引脚配置为复用功能,TX输出、RX输入 GPIO->MODE = AF; GPIO->AFRX = USART_AF; // 设置波特率,PCLK=60MHz,BAUD=115200 USART->BRR = PCLK / 16 / BAUD; // 使能发送、接收、串口 USART->CR1 = USART_UE | USART_TE | USART_RE; // 1个停止位 USART->CR2 &= ~USART_STOP_MASK;配置完成后,发送一个字节通常要检查发送数据寄存器空标志,等待数据完整移出时还要检查传输完成标志。异步通信没有时钟线,这两个标志就是软件与硬件之间唯一的“握手”。
3.2 SPI的时钟极性和相位,配错了必然失败
SPI模式由CPOL(时钟极性)和CPHA(时钟相位)组合成4种模式。CPOL决定空闲时SCLK电平是低还是高,CPHA决定数据采样发生在第一个还是第二个边沿。选哪一种,不是看主机心情,而是严格看从设备数据手册里对时序图的要求。
以最常见的模式0为例:CPOL=0,空闲时钟低电平;CPHA=0,在第一个边沿(上升沿)采样数据。从设备手册往往会画一张时序图,数据在SCLK上升沿被锁存。如果主机配成了模式1,采样边沿整体往后移半个周期,高速通信时几乎所有字节都会错位,表现出来要么全FF,要么间隔乱码。
SPI主设备初始化时,要把波特率配置为不超过从设备上限的值。比如外设时钟72MHz,预分频系数16,则SPI时钟为:
SPI时钟 = 72MHz / 16 = 4.5MHz如果从设备最高支持10MHz,4.5MHz完全够用;如果从设备最高只有1MHz,就必须把预分频调大。这个参数不是越大越好,也不是越快越好,而是越接近从设备能力且留有余量越好。
// SPI主设备初始化伪代码 SPI->CR1 = SPI_MODE_MASTER | SPI_BAUD_DIV_16 | SPI_CPOL_0 | SPI_CPHA_0; SPI->CR1 |= SPI_ENABLE;在SPI传输时还有一个隐性要求:很多从设备对片选信号有要求,要么高有效要么低有效,有的还要求片选提前于时钟建立一段时间。主机的GPIO操作顺序很重要,通常流程是拉低片选、等待从设备准备好、发数据、读数据、最后拉高片选。很多人直接在寄存器示例里复制片选拉高拉低的顺序,结果换了一款从设备就不工作,本质上就是没看时序图里的建立时间和保持时间。
3.3 同步和异步在ARM调试时的直观区别
如果手头有逻辑分析仪,异步协议看的是起始位和停止位,同步协议看的是SCLK边沿和数据线状态。UART波形上每一帧有清晰的低电平起始位,然后8个数据位,最后高电平停止位。SPI波形上SCLK一直翻转,MOSI和MISO的电平只要在采样沿稳定即可。
我自己调试时有个习惯:先把逻辑分析仪的采样率设置成远高于通信速率,先看总体波形是否符合协议框架,再放大到单个字节看数据位。UART乱码时,重点看起始位到停止位的宽度,如果一帧的时间明显和配置的波特率对不上,就能判断是波特率算错了还是时钟源没选对。SPI数据错乱时,重点看采样沿和数据变化沿之间的距离,如果数据刚好在采样沿附近跳变,大概率是CPHA配反了。
4. 实操中容易踩的坑与排查技巧
协议配置看起来就几个寄存器,真正联调时问题五花八门。这几年我调过不少ARM外设,很多故障并不是芯片坏了,而是几个细节没对齐。这里把高频问题整理成速查表,并展开讲几个典型场景。
4.1 异步通信双方配置不一致,乱码是常态
UART最经典的问题就是两边参数不完全一样。常见的参数组合是115200、8数据位、无校验、1停止位,简写115200-8-N-1。只要有一边把数据位配成7位,或者停止位配成2位,接收数据就会出现偶发错位。更隐蔽的是波特率“看起来一样”,实际上两边时钟误差不同步。
许多ARM芯片内部RC振荡器精度在±1%到±2%,温度变化还会漂移。两个设备各自的时钟误差方向相反,累计误差就可能超过UART容限。UART对波特率误差的容忍度通常要求在±2%以内,超过之后帧同步会丢失。排查时不要只盯着配置界面,还要看板子上的晶振或芯片内部时钟源是否精准。如果条件允许,优先使用外部晶振作为UART外设时钟,稳定度会好很多。
4.2 同步通信主从时钟配置不对,接收方采错沿
SPI和I2C都属于同步协议,对时钟边沿极其敏感。SPI常见故障是主机把CPOL/CPHA配置得和从设备不匹配,现象很迷惑:有时候第一个字节能收到,后面全乱。因为首字节可能正好没有影响,但连续传输后采样点越来越靠近翻转沿,噪声一叠加就出错。
排查办法很简单:先把SPI时钟降到很低,比如几百kHz,再用逻辑分析仪抓波形,把每个SCLK沿对应的MOSI/MISO电平逐位分析。如果低速率下数据正确,说明问题在时序裕量不足,而不是协议模式错误。如果低速率下依然错,就要回头检查CPOL和CPHA以及数据位顺序了。
4.3 半双工方向切换,少一个延时就是冲突
I2C这种半双工协议,最微妙的地方是方向切换。主机写完寄存器地址后,要释放SDA,转换成输入方向,再从设备把数据放到总线上。代码上如果切换方向后马上开始读,会给一个短暂的“总线争用”窗口,可能读到0xFF,也可能让从设备应答异常。
我踩过一次很深的坑:在某ARM芯片上做I2C读取温度传感器,读出来的数据偶尔是上一帧的旧值。后来查逻辑分析仪,发现主机释放SDA后没有延迟,SDA电平还没来得及被上拉电阻稳定,从设备就已经开始驱动数据了。后来我在每次方向切换后加了一个微秒级延时,问题立刻消失。不要小看这几微秒,对低速I2C来说,它是对方准备好数据的前提条件。
4.4 信号完整性问题:线太长、电平不匹配、上拉太小
串行通信虽然抗干扰比并行强,但也不是无限度地拉长。UART线超过几十厘米时,如果布线不规范,可能出现偶发错帧。SPI在高速率高边沿陡峭时,如果走线过长,反射会导致电平振铃。I2C的上拉电阻太小,低电平灌电流过大;上拉电阻太大,上升沿变慢,高速模式直接失败。
简单经验值:3.3V系统的I2C上拉电阻常用2.2kΩ到10kΩ,总线上挂的设备越多,阻值可以适当调小。UART做板间连接时,电平标准必须一致,3.3V设备接到5V设备上要加电平转换或限流保护。SPI高速模式下,串接一个几十欧姆到一百欧姆左右的电阻能抑制振铃,前提是亲自量过波形再做决定。
这里给出一个高频问题速查表,方便现场排查:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| UART收到乱码 | 波特率不一致、数据位/校验位配置不同、时钟源偏差大 | 核对双方参数,用示波器量一帧宽度 |
| UART偶尔丢帧 | 接收缓冲区溢出、主循环处理不及时、线缆干扰 | 开启硬件流控或DMA,降低波特率,检查接地 |
| SPI第一个字节错后续正常 | 片选建立时间不够、从设备准备时间不足 | 拉低片选后延时,再看从设备时序图 |
| SPI所有字节全出差错 | CPOL/CPHA不匹配、MISO/MOSI接到 | 对照从设备时序图,检查硬件接线 |
| I2C读回0xFF | 方向未切换、上拉电阻过大、时钟拉伸未处理 | 抓波形,加方向切换延时,核对设备地址 |
| I2C总线卡死SDA一直低 | 某个从设备异常拉低总线、主机中途异常停止 | 复位从设备,初始化时多发送几次START和STOP恢复 |
4.5 调试工具的选择与使用技巧
调通信协议,示波器和逻辑分析仪几乎必备。示波器适合看波形形状、噪声、边沿质量;逻辑分析仪适合看协议帧结构、数据内容。对ARM初学者,先用逻辑分析仪抓数据,再用示波器确认信号质量,是非常高效的组合。
串口调试工具也很常用,但要注意显示格式。发送端的“文本模式”和“Hex模式”经常被搞混:发送字符'A',如果用Hex模式发送的是0x41,接收端如果按Hex显示看到41,按文本显示看到A,这是正常的。很多“乱码”其实是格式没对齐,不是协议错误。用逻辑分析仪抓串口波形,直接按波特率解码,比肉眼判断靠谱得多。
5. 从协议维度反推嵌入式系统设计
看懂串并行、同步异步、单工双工,对项目选型会有质的帮助。我一般在确定通信方案前,会先把三组维度过一遍,形成一张需求清单。
先问引脚预算:主控引脚多,可以用并行或SPI;引脚紧张,就I2C或UART。再问通信速率:数据量大、实时性要求高,选SPI这类同步通信;只传状态和命令,UART就够。最后问双向性:双方需要频繁互发数据,选全双工更省事;主机只读从机的传感器数据,半双工也完全胜任。
举个例子,做一个低功耗温湿度采集节点,MCU与传感器之间距离很短、数据量很小,I2C是很好的选择,因为省引脚、支持多设备挂载。换到高速数据采集场景,ADC采样率每秒几百万点,I2C的1Mbit/s根本不够用,这时候SPI或者并行接口才是正解。再比如调试日志,数据方向是单向的,掉电也不在意,UART就是最稳的方案,因为工具链成熟、接收端随处可寻。
把这些维度想清楚,再去查芯片手册、查开发板例程,会比直接搜“某某外设初始化代码”高效得多。例程只是给你看寄存器写法,而方案选型决定了项目是否稳定、是否好维护。
6. 最后再分享一个实际教训
串并行、同步异步、单工双工,说到底就是通信双方的“约定”。我在一次做两个ARM板之间数据同步时,两片板子用的是同一个型号的芯片,却怎么调都调不通。后来查示波器才发现,两块板子的复位时间不一样,上电后初始化顺序不同,导致其中一块板子的UART在另一块还没配好时就发了几个垃圾字节,接收方又把垃圾字节当成数据帧的开头,后面所有解析全部错位。
从那以后,我所有通信协议初始化都遵循一个固定顺序:先确保双方上电完成,再配置GPS之类的外部引脚,最后再使能收发器和中断。如果双方可能异步上电,就在通信帧里加帧头、帧尾和校验,软件上做超时重发。细节决定成败,这句话在通信协议上真的不是套话。希望这篇内容能帮你把三组概念理清楚,后续写驱动时少一些“为什么别人能通我通不了”的困惑。