ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

嵌入式通信底层逻辑:传输方式与协议规则详解(UART/SPI/I2C/CAN)

嵌入式通信底层逻辑:传输方式与协议规则详解(UART/SPI/I2C/CAN) 1. 先弄明白为什么说嵌入式通信的本质是“把两个设备拉到一个频道上”很多刚接触嵌入式的朋友面对UART、SPI、I2C、CAN这一堆术语时第一反应是背协议、记引脚、抄初始化代码。但这都是在“术”的层面打转学的越快忘的越快。我当年刚入行的时候也是这样直到被一个老工程师点醒通信这件事说白了就是让两个设备在同一个频道上用同一种语言对话。你不需要畏惧这些名词你只需要先把“对话”这件事拆开看明白。这里我用一个生活场景来类比。你让一个只讲粤语的人和一个只讲普通话的人聊天他们需要什么第一两个人得愿意在同一个时刻说话或者约定好你说完我再说这叫做时序。第二就算都用普通话一个人语速极快、一个语速极慢也聊不下去这叫做速率匹配。第三你说“零”他理解成“一”那就全乱套了这叫做电平标准。第四你们得有开始说话和结束说话的信号否则永远不知道对方是没说还是说完了这叫做帧格式。嵌入式里的两个单片机、一个MCU和一个传感器、一个MCU和一个上位机之间的通信本质就是在解决上面这堆“聊天问题”。而所谓的传输方式解决的是“信号怎么从A走到B”所谓的协议规则解决的是“信号到了B之后B怎么理解它”。这两个层面缺一不可也刚好就是很多人最容易混淆的地方。这篇文章里我会把这两层东西拆开讲清楚从最底层的物理传输开始到逻辑协议和分层设计再到具体总线的选型和调试经验。目标读者是刚接触嵌入式通信、或者搞了一段时间但总觉得哪儿差口气的朋友。看完之后你再去看数据手册、官方驱动代码会轻松很多因为底层逻辑已经通了。2. 底层传输方式你发的“1”和“0”到底是怎么从一个引脚跑到另一个引脚的先明确一个容易忽略的事实通信的物理本质是电压变化。MCU引脚上输出高电平就是“1”低电平就是“0”接收端通过采样引脚上的电压把这些电平重新翻译回二进制数据。听起来很简单但这里面藏了几个关键分支直接决定了你后面选择用哪种接口。2.1 单端传输与差分传输一根线传和两根线传的差异单端传输是最常见、最容易理解的——发送端在一个引脚上输出某个电压接收端把这个电压跟自己的GND地做比较。高电平是1、低电平是0完事。UART、SPI、I2C都属于单端传输方式它们的共同点是结构简单、引脚少、适合短距离板内或短距离设备间通信。差分传输是另外一条路线典型代表是CAN总线物理层和RS485。它的思路是数据不是拿一根线对地来判定而是用两根线CANH和CANL之间的电压差来判定。A线电压减去B线电压是正的就是显性位大致对应逻辑0是负的就是隐性位大致对应逻辑1。这种设计的最大好处是抗干扰能力强。因为外部电磁干扰通常同时作用在两根线上两根线被抬高了或者拉低了多少它们之间的差值基本不变。我给新手一个非常实用的建议只要通信距离超过一米或者环境里有电机、继电器这类强干扰源优先考虑差分传输接口比如CAN、RS485。别图省事去拉一根长长的UART线调试的时候能把你折磨到怀疑人生。2.2 并行传输与串行传输我们为什么最终都选串行早期的通信经常用并行方式比如老式并口打印机、早年的IDE硬盘接口一次传8位或16位速度快是快但代价是要拉一大堆线。现在嵌入式领域除了片内总线比如MCU内部连接Flash和SRAM的总线之外几乎所有的对外通信接口都走向了串行——一次只传一位通过时间累积成多个bit。为什么两个原因一是引脚成本封装越来越小根本放不下那么多并行引脚二是高频并行信号之间的串扰极难处理8根线并排跑高频互相干扰反而限制了速度上限。串行通信靠提高时钟频率来弥补位宽劣势比如现在常见的SPI轻松跑几十MHzUART跑几Mbps也没压力板级场景已经完全够用。所以你现在看到的I2C、SPI、UART、CAN、Ethernet无一例外都是串行。2.3 同步通信与异步通信一根时钟线和三根时钟线的差别这是底层传输里最重要的一条分叉路也是面试最爱问的点。我之前见过很多初学者把“同步”和“异步”跟“阻塞”和“非阻塞”混为一谈完全是两回事必须掰开。同步通信的意思是发送端和接收端共享同一个时钟信号。发送方不仅发出数据线还发出一根时钟线SCLK/SCK接收方在这个时钟信号的上升沿或下降沿去采样数据线。数据在时钟边沿被稳定取样所以只要时序对了传多快都可以上限取决于电路和布线水平。SPI和I2C就是非常典型的同步通信。异步通信的意思是没有共享时钟线。双方各自用自己的时钟比如都按115200bps的速率发送端按这个节奏把数据bit挨个发出来接收端靠自己内部时钟去采样这根数据线。因为两边时钟存在微小的偏差所以异步通信必须做“帧同步”最常见的方法就是起始位和停止位。平时说的UART串口就是这个路子。它的优点是引脚少只需要TX和RX缺点是速率做不高因为太高的速率下双方时钟哪怕只有万分之几的偏差累积起来也会导致采样错位。关于UART我多说一句它虽然叫异步串口但在MCU层面发送端和接收端如果共用同一个晶振或参考时钟也能配合得很默契。实际做低功耗设计的时候UART的速率上限一般控制在几Mbps以内超过这个数误码率会明显变高。3. 协议规则从“电平出来了”到“数据被懂”中间还有三层台阶底层传输解决的是物理层问题但真正让两个设备能沟通工作还需要协议。凡是你看到的“帧格式”“地址”“校验”“ACK应答”全属于协议层面。很多新手把电平标准比如TTL电平和RS232电平当成协议这也是个常见的误区。电平标准是物理层的“传输方式”协议规定的是数据组织方式。我习惯把嵌入式通信协议拆成三个层面来理解虽然不完全等同于经典的OSI七层模型但拿来入门特别顺。3.1 第一层字节怎么框出来——帧同步问题收端拿到一串bit流之后第一件事是知道“从哪儿到哪儿算一个完整的数据单元”。这个数据单元就叫帧Frame。不同的接口处理帧同步的方式不一样。比如UART它靠起始位Start Bit来“框”当前帧的开始。平时数据线处于空闲态高电平发送端拉低一个bit的时间低电平接收端检测到这个下降沿就知道“要开始了”然后按约定的波特率去采样后续的8个bit的数据位再检查停止位高电平。这就是为什么UART帧格式里含有起始位和停止位的原因。SPI和UART又不一样它不需要起始位因为它有独立的时钟线。只要CS片选被拉低主设备开始产生时钟脉冲从设备就在SCK的边沿上同步采样MOSI数据线。所以SPI的帧同步靠的是“时钟是否在走”简称“有时钟就是有效数据”。CAN的帧同步更讲究它除了有SOFStart of Frame之外还采用了位填充Bit Stuffing机制——连续发送5个相同电平的bit后强制插入一个反向bit。这是为了保证接收端能从足够多的电平跳变中恢复出时钟信息而不会因为长时间没有跳变导致失步。所以你看CAN总线的帧波形非常“活跃”连续高电平或低电平不会超过5个bit周期这是很多人用示波器看CAN波形时应该有的预期。3.2 第二层数据怎么组织——协议帧里到底要放什么一旦帧边界确定了接下来就是帧内部怎么排。大部分通信协议的数据帧都会包含这几个要素地址域给谁、控制域干什么、数据域内容、校验域验证、结束标志帧结束。这么设计不是为了炫技。嵌入式环境往往存在噪声、信号反射、时钟抖动一帧数据里可能坏掉某一位如果没有校验机制收端把错数据全盘接收轻则数据错误重则设备误动作引发事故。所以校验是通信协议里必不可少的一环。常见的校验方式有三种简单介绍一下第一种是奇偶校验在数据位后面加一位让整帧里“1”的个数为奇数或偶数实现简单但只能检测单bit错误而且如果恰好两位反转就失效了。第二种是累加和校验Checksum把数据逐字节求和取低八位很多私有协议爱用实现简单但强度一般。第三种是CRC校验循环冗余校验用多项式除法对数据做计算检测能力很强可以捕捉突发错误CAN、Ethernet、Modbus这些正经工业协议全都用CRC只是宽度和多项式各不相同比如CAN用的是CRC15CRC-16和CRC-32在别处也很常见。3.3 第三层双方怎么配合——握手与应答机制有了帧格式之后A把数据发出去B怎么知道该不该回话如果B正在忙A一直发数据是不是会丢这就是流控制和握手机制要解决的问题。最简单的配合方式是“发完就等确认”。Modbus的一条查询从站收到之后必须回一条响应帧主站如果超时没有收到响应会重发或者报错。这种方式可靠性高但效率偏低因为每一来一回都要等。复杂一点的方式是在数据链路层就建立流控。比如在UART上常用的RTS/CTS硬件流控发送之前先查一下对方“是否准备好接收”准备好了才发硬件流控比软件流控XON/XOFF更适合大数据量传输因为软件流控把控制信息和数据混在一条线上一旦数据量大了控制字符可能穿插在数据流里解析起来很麻烦。CAN总线不太一样它天生自带优先级仲裁。多个节点同时发数据时总线会根据ID标识符逐位仲裁ID越小优先级越高仲裁失败的节点主动退出发送改成接收。这就是为什么CAN特别适合传感器多、实时性要求高的场合——你不需要人为管理“谁先谁后”协议自己就解决了。4. 从传输方式到协议规则的完整落地UART、I2C、SPI、CAN各自代表哪条路前面几章把底层逻辑拆开讲了现在我们把UART、I2C、SPI、CAN这四个最常用的接口放进这个框架里对比一下。你会发现看似完全不同的四个总线其实都只是“传输方式 协议规则”的不同组合。4.1 UART最朴素的异步点对点UART是异步、单端的串行通信物理上只需要TX和RX两根信号线外加共地。协议上靠起始位和停止位框定帧靠波特率约定采样节奏协议本身很薄校验方式也简单可要可不要。开发里最简单的就是串口调试——你要看某个传感器的数据直接把传感器的TX接到MCU的RXMCU的TX再回给上位机一打开串口助手就能看到数据。UART最大的优势是通用性和简单性几乎每个MCU都带好几路UART没有时钟线只有两根数据线线序接错一换就完事。最大的短板是通信双方必须事先约定好波特率而且一旦波特率不一致屏幕上就是一坨乱码。低级错误往往就出在这儿——你代码里的波特率是115200对方配置的是9600数据自然对不上。除了跟传感器和调试器通信之外UART还是很多模组比如蓝牙模组、4G模组、GPS模组的标准控制口。很多时候我们不用管模组内部怎么实现的只要按照模组的AT指令手册通过UART发个“ATXXX”它就会回你一个“OK”。这种“外部交互协议”往往跑在“轻型物理层”之上非常适合快速集成设备功能。4.2 I2C双线总线里的“寻址专家”I2C是同步、单端、半双工的串行通信物理上只需要两根线SCL时钟线和SDA数据线。它的亮点在协议层——总线上的每个从设备都有唯一的7位或10位地址主设备发起通信时先发送起始条件然后发地址和读写位符合地址的从设备会回ACK然后开始数据交换。新手拿到I2C传感器模块最容易被驱动的“寄存器地址”绕晕。事实上一颗I2C芯片的内部往往有很多寄存器你要往寄存器0x10写入0x55就得先发寄存器地址再发数据某些芯片还要求先发送“设备地址写位”再发寄存器地址再发数据流程一乱数据就写不进去。所以I2C编程的本质其实就是“按照数据手册里的时序图和寄存器表一步一步来”。I2C在板内器件连接上占据了统治地位EEPROM、温湿度传感器、加速度计、OLED屏控制芯片基本都是I2C接口。I2C的好处是引脚少、可挂多设备不足是速度相对较慢标准模式100kHz快速模式400kHz高速模式也不过3.4MHz不适合大批量连续传输。而且I2C总线有严格的上拉电阻要求忘加上拉通信就会时好时坏这也是排查I2C故障时第一个要检查的点。4.3 SPI四根线全双工速度拉满SPI是同步、单端、全双工的串行通信典型的四根线是SCLK时钟、MOSI主出从入、MISO主入从出、CS片选。它的哲学和I2C正好相反不靠地址来选设备而是靠一根CS线单独选中从设备要跟哪个设备通信就拉低哪个设备的CS。这样做的结果是你省掉了寻址的协议开销每次通信都是纯数据搬运速度相当快。SPI的模式Mode 0到Mode 3常常是新手掉坑的地方。模式由时钟极性CPOL和时钟相位CPHA组合而来CPOL决定空闲时SCK是高还是低电平CPHA决定数据是在时钟上升沿采样还是下降沿采样。主设备和从设备必须配置成相同模式否则数据会错位。我调试SPI Flash时遇到过好几次这种情况代码看起来毫无问题但读出来的ID全是0xFFFFFFFF最后拿示波器一看模式对不上把CPHA改一下立刻就好了。SPI适合对吞吐量有要求的场景SD卡、Flash芯片、显示屏、ADC采样数据高速输出基本都走SPI。因为全双工和独立CS它吞吐能力很强但代价是引脚多、只能一主多从、而且不能像CAN那样远距离传。4.4 CAN走向多节点与抗干扰的不二之选CAN是差分、异步、半双工的串行通信跟前面三种完全不一样的点在于它本来就是为“多主多从、共享总线”设计的。所有节点挂在一对双绞线上任意节点都可以主动发起发送优先级靠ID仲裁冲突时低优先级的自动让路整个过程不需要主机调度。这种机制非常“民主”也决定了CAN被汽车、工业自动化、医疗器械这些高可靠场景反复选用。CAN的协议层比UART/I2C/SPI复杂得多。一帧CAN报文除了Data数据段之外还有很详细的标识符、控制段、CRC段和ACK槽。如果自己造轮子去实现CAN协议会很吃力但好在现在的MCU基本都集成了CAN控制器甚至是CAN-FD控制器你只需要配置好波特率和过滤器就能通过硬件收发报文。用CAN做开发时有两个地方是新手最容易出问题的一个是波特率和采样点配置。CAN不是简单按一个波特率定死就行的采样点位置很关键推荐设置在75%到85%之间这样才能在长距离、多节点时保证采样正确。另一个是验收过滤器配置CAN控制器收到的每一帧报文都会先过一遍滤波器不匹配的帧硬件直接丢弃不占CPU。如果你开启了过滤器但没配好ID你会发现自己永远收不到数据排查半天还以为总线坏了。这种硬件层面的“静默淘汰”机制恰好解释了为什么很多人看CAN总线波形明明有数据但MCU却一个中断都不进。5. 协议分层品性下标、中断、回调与状态机——代码层面的通信落地手段原理讲了一大堆最后总归要落到代码上。很多人的困惑是“我知道了UART是什么但到了写代码的时候还是一头雾水。”因为原理到代码之间还隔着几个常见的软件设计套路。最基础的套路是轮询。MCU主循环里不停查询接收标志位有数据就处理没数据就继续跑。优点是代码简单、适合教学和低频通信缺点是接收数据时CPU必须一直等容易造成丢帧。如果你的系统里同时有好几个通信接口轮询就是灾难因为CPU只有一个不可能同时盯着三个接口。第二种是中断接收。UART收到一个字节触发一次中断在中断里把数据放到环形缓冲区。主循环在另一个时间片里消费这个缓冲区。我强烈建议新手尽快掌握这个套路因为它是现代嵌入式通信编程的地基。环形缓冲区不仅能削峰填谷还能让通信模块和业务逻辑解耦不至于串口数据一多就把主循环卡死。第三种是状态机解析。对于那种“帧头长度数据校验帧尾”的自定义协议不要用阻塞等待的方式去等整帧数据而是每收到一个字节就驱动状态机跳转一步。比如状态定义成等待帧头、接收长度、接收数据、校验、接收帧尾。收到任何一字节都先喂给状态机切换状态直到完整收完一帧再触发整帧处理。这种设计在通信模块里非常常见好处是你不需要知道一帧什么时候来只需要保证每个字节都有地方安放。第四种是DMA。DMA可以在不需要CPU干预的情况下把外设收到的数据直接搬到内存里。整帧收完后触发一次完成中断CPU只需处理一帧数据效率极高。缺点是DMA的缓冲区管理比中断稍微复杂一点需要自己设计“半满全满”或者“多缓冲乒乓”策略。对于SPI传输LCD刷屏、UART高波特率打印这类大批量数据场景DMA几乎是必选项。6. 调通信查Bug的实战思路为什么波形正常但数据就是不出来说到最后介绍一点通信调试的实战排查方法。通信问题排查是有套路的我总结成几条顺序原则按照这个顺序去排查大部分问题半小时内都能定位。第一先怀疑物理层再看协议层。用示波器或者逻辑分析仪去抓引脚上的波形看有没有电平跳变波形幅值对不对信号有没有严重变形。我遇到过好几回通信不正常的根因就是某个引脚的上下拉电阻没配好或者导线太长产生了信号反射。物理层一乱协议层再对也是白搭。第二确认波特率或时钟极性。UART乱码就先查波特率SPI读数不对就试试切换Mode 0到Mode 3I2C通信时好时坏就查上拉电阻和速率。这些都是典型的“原子级”检查点不需要复杂分析却经常是问题真凶。第三用逻辑分析仪代替“人肉眼力”。如果手里只有示波器看协议数据比较吃力强烈建议准备一个逻辑分析仪配合免费软件比如PulseView或者Saleae Logic能直接把UART、I2C、SPI、CAN的帧解析出来比对着波形数高低电平方便太多。我一般排查顺序都是逻辑分析仪抓波形看帧是否完整然后再看代码里校验和为什么对不上。第四最后再怀疑MCU外设配置。确认外设时钟有没有打开、引脚有没有正确复用AF、中断有没有使能、DMA通道有没有配错。STM32用户尤其要仔细对照参考手册的Alternate Function Mapping表好多人通信不通不是因为协议有问题而是因为GPIO的复用功能选错了。最后说一个很多人都会忽略的细节电平标准的一致性。MCU的UART引脚默认是TTL电平3.3V或5V如果你接了一个RS232电平的设备必须加MAX3232之类的电平转换芯片否则收到的全是乱码或者干脆没反应。同理CAN收发器、RS485收发器本质上都是在做“逻辑电平/差分电平”之间的转换不要以为MCU自带CAN控制器就能直接把引脚怼到总线上。通信这块理论再多都不如亲手调一次。建议新手找一块带多个通信接口的开发板先跑通一个传感器或者一个EEPROM的读写再用逻辑分析仪抓波形看看把抽象的概念跟真实的波形对上号。这一步打通之后你会发现自己对嵌入式通信的理解会上一个台阶——不再是照着例程抄而是真真正正知道每一行配置在干什么。
返回列表