个人主页:
个人专栏:
C语言
嵌入式小白启动!
重要OJ算法题详解
蓝桥杯备战
C++从菜鸟到强手
python启航
AI大模型Agent:拥抱未来,赋能自己
文章目录
- 嵌入式小白第三站:UART、I2C、SPI、ADC 怎么学?从传感器读数到完整小项目
- 1) 先建立一个大局观:外设通信不是背协议,是搭数据管道
- 2) UART:最朴素、最常用、最值得先学的通信方式
- 2.1 UART 的核心概念
- 2.2 TTL 串口、RS232、RS485 不是一回事
- 2.3 为什么串口会乱码?
- 2.4 串口接收:别让数据把你冲晕
- 3) I2C:两根线挂一串设备,简单外表下很讲规矩
- 3.1 I2C 的几个核心动作
- 3.2 7 位地址和 8 位地址:I2C 新手大坑
- 3.3 I2C 为什么必须上拉?
- 3.4 I2C 自救清单
- 4) SPI:线多一点,速度快很多,屏幕和 Flash 很爱它
- 4.1 CS 片选:你先点名,它才说话
- 4.2 CPOL/CPHA:SPI 的四种模式
- 4.3 SPI 读写不总是“发什么就收什么”
- 5) ADC:把模拟世界切成数字格子
- 5.1 分辨率:12 位 ADC 到底代表什么?
- 5.2 采样时间和输入阻抗:为什么 ADC 值会飘?
- 5.3 软件滤波不是补锅神器
- 5.4 电池电压检测:为什么要分压?
- 6) 定时器、中断、DMA:让系统从“排队干活”升级为“有节奏地协作”
- 6.1 定时器:给系统一个节拍器
- 6.2 中断:处理突发事件,但不要在里面安家
- 6.3 DMA:让外设自己搬数据
- 7) 调试工具:不要只靠眼神和感觉
- 7.1 串口日志:最便宜的系统旁白
- 7.2 逻辑分析仪:通信协议的照妖镜
- 7.3 示波器:看模拟、电源和边沿
- 7.4 SWD/JTAG:进 MCU 里面看看
- 8) 把知识串成项目:桌面环境监测小站
- 8.1 项目功能
- 8.2 模块分层
- 8.3 主循环框架
- 8.4 串口命令设计
- 8.5 I2C 传感器驱动思路
- 8.6 OLED 显示:别一秒刷一万次
- 8.7 报警逻辑:用状态机,不要用一堆 if 硬堆
- 9) 常见协议问题对照表
- 10) 新手学习顺序:从能看见结果到能解释原因
- 11) 本篇最终总结:通信协议是嵌入式项目的血管
嵌入式小白第三站:UART、I2C、SPI、ADC 怎么学?从传感器读数到完整小项目

如果第二篇的关键词是“控制一根引脚”,那第三篇的关键词就是“组织一条数据流”。
点亮 LED、读取按键之后,你已经能让 MCU 对外部世界做出最简单的反应了。但真正的嵌入式项目不会只停在“灯亮不亮、按键按没按”。它通常要做这些事:
- 从温湿度、姿态、电流、电压、距离等传感器读取数据;
- 把数据显示到 OLED、LCD 或上位机;
- 通过串口接收命令;
- 通过 Wi-Fi、蓝牙、CAN、RS485 等方式和其他设备通信;
- 按固定周期采样;
- 在异常时报警;
- 在资源有限的 MCU 上让多个任务同时看起来“井井有条”。
你会发现,嵌入式项目的难点开始从“某一行代码怎么写”变成“数据怎么来、怎么走、怎么处理、怎么证明它没错”。
这一篇我们就讲新手必须跨过去的几座桥:UART、I2C、SPI、ADC、定时器、中断、DMA、调试工具,以及如何把它们串成一个真正能跑的小项目。
1) 先建立一个大局观:外设通信不是背协议,是搭数据管道
很多人学通信协议时会陷入一种痛苦:UART 有波特率,I2C 有地址,SPI 有 CPOL/CPHA,ADC 有采样时间,定时器还有分频和溢出。每个词都像认识,但凑一起就开始头疼。
我们换个角度。
一个嵌入式系统里的数据通常会经历这样的路径:
你学 UART、I2C、SPI、ADC,并不是为了背名词,而是为了回答四个问题:
- 数据从哪里来?
- 数据以什么电气方式传过来?
- MCU 哪个外设负责接收它?
- 程序如何把原始数据变成有意义的信息?
比如温湿度小系统:
- SHT30 传感器通过 I2C 输出温湿度原始数据;
- MCU 通过 I2C 外设读取传感器寄存器;
- 驱动层把原始字节换算成摄氏度和百分比;
- 应用层判断是否超温;
- OLED 显示结果;
- UART 打印日志或接收命令;
- 蜂鸣器或 LED 负责报警。
这就是数据流。协议只是数据流中某一段路的交通规则。
【干货】学嵌入式通信时,不要只问“这个协议怎么配”。要问“这段数据从哪来、到哪去、用什么证据证明它到了”。
2) UART:最朴素、最常用、最值得先学的通信方式
UART 是新手最应该优先掌握的通信方式。原因很简单:它既能做设备通信,又是调试神器。
你写的第一句嵌入式日志,大概率是从 UART 出来的:
printf("system init ok\r\n");当屏幕还没亮、传感器还没通、网络还没连上时,串口日志就像系统给你递出来的一张小纸条:我跑到这里了,我读到这个值,我刚才出错了。
2.1 UART 的核心概念
UART 常见连接至少需要三根线:
- TX:发送;
- RX:接收;
- GND:共同参考地。
两个设备连接时,一般是交叉接:
- A 的 TX 接 B 的 RX;
- A 的 RX 接 B 的 TX;
- GND 接 GND。
如果 TX 接 TX、RX 接 RX,两个设备就像两个人都对着麦克风讲话,但没人把耳朵靠过去听。
UART 是异步通信,没有单独时钟线。双方靠约定好的波特率和帧格式来对齐。常见配置是115200 8N1:
- 115200:每秒大约传 115200 个符号位;
- 8:8 个数据位;
- N:无校验;
- 1:1 个停止位。
只要双方波特率、数据位、校验位、停止位不一致,就可能乱码。
2.2 TTL 串口、RS232、RS485 不是一回事
新手还容易把“串口”这两个字混着用。
MCU 引脚上的 UART 通常是 TTL/CMOS 电平,比如 3.3V 或 5V。电脑老式 DB9 那种 RS232 电平范围不同,不能直接硬接 MCU。工业现场常见 RS485,本质上也不是 UART 本身,而是用差分电气层承载串口数据,抗干扰更强、距离更远、可多点总线。
所以你看到“串口模块”时要问:
- 它是 USB-TTL?
- 还是 USB-RS232?
- 还是 USB-RS485?
- 电平是 3.3V 还是 5V?
【干货】UART 是通信外设/数据格式,TTL/RS232/RS485 是电气层。名字都叫串口,但线不能乱接。
2.3 为什么串口会乱码?
串口乱码是新手最常见的心理测试。常见原因有:
- PC 端波特率设置和 MCU 不一致;
- MCU 系统时钟配置错,导致实际波特率偏了;
- 数据位、校验位、停止位不一致;
- TX/RX 接反或没共地;
- 用 5V USB-TTL 接了只能承受 3.3V 的芯片;
- 打印二进制数据,却用文本方式查看。
排查顺序建议:
- 固定使用
115200 8N1或9600 8N1; - 确认系统时钟配置;
- TX/RX 交叉接,GND 共地;
- 用逻辑分析仪抓 UART,直接解码看波特率和字节;
- 先发固定字符串,比如
ABC\r\n,不要一上来发复杂结构体。
2.4 串口接收:别让数据把你冲晕
发送日志很简单,接收命令就会复杂一点。因为数据不是你想什么时候来就什么时候来,它可能半包到、粘包到、带噪声到。
新手常见错误是:在主循环里阻塞等待串口接收。这样做会让系统像坐在门口等快递,快递不来就什么事也不干。
更好的思路:
- 串口中断或 DMA 接收字节;
- 放入环形缓冲区;
- 主循环从缓冲区取数据;
- 按协议解析完整命令。
一个简单的文本协议可以这样设计:
LED ON\r\n LED OFF\r\n TEMP?\r\n MODE AUTO\r\n更工程化的二进制协议可能包含:
- 帧头;
- 长度;
- 命令字;
- 数据;
- 校验;
- 帧尾。
【干货】UART 入门先用文本协议建立信心;项目变复杂后,再考虑二进制帧和 CRC。
3) I2C:两根线挂一串设备,简单外表下很讲规矩
I2C 常用于连接低速传感器、EEPROM、RTC、OLED、小型 ADC/DAC 等器件。它的标志性特点是两根线:
- SDA:数据线;
- SCL:时钟线。
再加上电源和地,一个 I2C 模块通常四根线就能工作。
这也是 I2C 很适合新手做传感器项目的原因:线少,模块多,资料多。
3.1 I2C 的几个核心动作
I2C 通信里有几个关键词:
- Start:起始条件;
- Stop:停止条件;
- Address:设备地址;
- R/W:读写方向;
- ACK/NACK:应答/不应答;
- Register Address:很多传感器内部寄存器地址;
- Data:读写的数据字节。
一个典型的 I2C 读寄存器过程可以理解成:
- 主机发起 Start;
- 主机发送设备地址 + 写方向;
- 从机 ACK;
- 主机发送要读取的寄存器地址;
- 从机 ACK;
- 主机再次 Start;
- 主机发送设备地址 + 读方向;
- 从机 ACK;
- 主机读取数据;
- 主机发送 NACK 并 Stop。
你可以把它想成去柜台取资料:
- 先告诉柜台“我要找 0x44 号窗口”;
- 再告诉它“我要读 0x00 号文件夹”;
- 然后切换成读取模式;
- 对方把文件夹里的内容递给你。
3.2 7 位地址和 8 位地址:I2C 新手大坑
I2C 地址常见有 7 位表示法。比如某传感器地址是0x68。但有些资料会把读写位也算进去,写成:
- 写地址:
0xD0; - 读地址:
0xD1。
它们其实对应同一个 7 位地址0x68,因为0x68 << 1 = 0xD0,最低位再表示读写。
不同库函数要求不同。有的函数要你传 7 位地址,有的函数要你传左移后的 8 位地址。传错后,现象通常是一直 NACK。
【干货】I2C 读不到设备时,第一件事不是改代码逻辑,而是确认库函数要的是 7 位地址还是 8 位地址。
3.3 I2C 为什么必须上拉?
I2C 总线通常采用开漏/开集结构。设备可以把线拉低,但不会主动强推高电平。高电平依靠上拉电阻。
这带来两个好处:
- 多个设备可以安全共享总线;
- 任何设备拉低都能被总线看到。
但也带来一个要求:没有上拉,线就高不起来;上拉太弱,速度快时上升沿太慢;线太长、设备太多、电容太大,也会让波形变差。
常见模块板上已经带了 4.7k 或 10k 上拉电阻,但不是所有模块都有。多个模块都带上拉时,等效阻值会变小,也要注意。
3.4 I2C 自救清单
当 I2C 设备读不到时,按这个顺序查:
- 电源电压是否正确;
- GND 是否共地;
- SDA/SCL 有没有接反;
- SDA/SCL 是否有上拉;
- 地址是 7 位还是 8 位;
- 设备地址引脚是否改变了默认地址;
- 总线速度是否太快,先降到 100kHz;
- 有没有别的设备占用相同地址;
- 用 I2C scanner 扫描总线;
- 用逻辑分析仪看 Start、地址、ACK/NACK。
如果逻辑分析仪显示根本没有波形,先查 MCU 配置和引脚复用。如果有地址但 NACK,重点查地址、电源、上拉和设备状态。
4) SPI:线多一点,速度快很多,屏幕和 Flash 很爱它
SPI 常用于 OLED/LCD 屏幕、Flash 存储器、无线模块、高速 ADC、某些传感器。它通常有四类线:
- SCLK:时钟;
- MOSI:主机输出、从机输入;
- MISO:主机输入、从机输出;
- CS:片选。
SPI 的特点是主机提供时钟,从机按时钟收发数据。它通常比 I2C 快,结构也更直接,但线更多。
4.1 CS 片选:你先点名,它才说话
SPI 可以挂多个从设备,但每个从设备通常要有自己的 CS。主机要和某个设备通信时,先把它的 CS 拉到有效电平,通信结束再释放。
如果 CS 没拉对,设备可能完全不理你。屏幕白屏、Flash 读 ID 全 0xFF 或 0x00,很可能就和 CS、复位脚、模式、时序有关。
4.2 CPOL/CPHA:SPI 的四种模式
SPI 有四种常见模式,由 CPOL 和 CPHA 组合决定:
- CPOL:时钟空闲时是低还是高;
- CPHA:在第几个时钟边沿采样数据。
模式不对时,波形看起来有,数据却全错。这种错误非常迷惑,因为你会觉得“线在动啊,为什么读不到?”
答案是:对方在上升沿放数据,你在下降沿读;或者对方刚准备好,你已经读完了。
【干货】SPI 调不通时,第一步把速度降下来,第二步确认 mode,第三步用逻辑分析仪看数据边沿。
4.3 SPI 读写不总是“发什么就收什么”
SPI 是同步全双工,主机发送的同时也在接收。但很多设备协议会规定:
- 先发命令;
- 再发地址;
- 可能有 dummy cycles;
- 再读数据。
比如读 Flash ID,你可能需要先发一个读 ID 命令,然后继续发送空字节来产生时钟,设备才会把 ID 从 MISO 线上吐出来。
新手常犯的错是:只调用一次 receive,却没有提供时钟。SPI 从机不会自己说话,主机不给时钟,它就没有节奏输出数据。
5) ADC:把模拟世界切成数字格子
GPIO 只能读高低电平,但真实世界很多量不是非黑即白的:
- 电池电压从 4.2V 慢慢掉到 3.7V;
- 电位器输出 0 到 3.3V 的连续电压;
- 光敏电阻随光照变化;
- 麦克风、压力传感器、电流采样都可能是模拟信号。
这时就需要 ADC,把模拟电压转换成数字。
5.1 分辨率:12 位 ADC 到底代表什么?
如果一个 ADC 是 12 位,它能把输入范围分成2^12 = 4096个等级,结果通常是 0 到 4095。
如果参考电压是 3.3V,那么每一格大约是:
3.3V / 4096 ≈ 0.000805V也就是约 0.805mV。
如果 ADC 读数是 2048,理想情况下对应电压约:
2048 / 4095 * 3.3V ≈ 1.65V注意这是理想计算。实际系统里还有参考电压精度、噪声、输入阻抗、采样时间、PCB 布局、传感器误差等因素。
5.2 采样时间和输入阻抗:为什么 ADC 值会飘?
MCU 内部 ADC 采样时,通常会通过一个采样电容短暂接到外部输入。如果外部信号源阻抗太大,采样时间又太短,电容还没充到真实电压,ADC 就开始转换,结果当然不准。
所以 ADC 读数飘时,不要只想着软件滤波。先问:
- 信号源阻抗高不高?
- 采样时间够不够?
- 参考电压稳不稳?
- 模拟地和数字地处理是否合理?
- 线是否太长、旁边是否有 PWM/电机干扰?
- 有没有合适的 RC 滤波?
5.3 软件滤波不是补锅神器
常见滤波方法有:
- 多次采样取平均;
- 去掉最大最小后平均;
- 滑动平均;
- 一阶低通滤波;
- 中值滤波。
它们能降低随机噪声,但不能修复接线错误、参考电压乱飘、采样时间严重不足这类根因。
【干货】ADC 调试顺序:先让硬件信号稳定,再用软件滤波变漂亮。不要反过来。
5.4 电池电压检测:为什么要分压?
很多 MCU ADC 不能直接测超过参考电压的输入。比如 MCU 工作在 3.3V,你想测一节锂电池满电 4.2V,就不能直接把电池正极接 ADC。
常见做法是电阻分压:
电池正极 -- R1 -- ADC点 -- R2 -- GNDADC 点电压为:
Vadc = Vbat * R2 / (R1 + R2)然后软件再反推:
Vbat = Vadc * (R1 + R2) / R2这里还要考虑分压电阻耗电、ADC 输入阻抗、采样时间和校准。低功耗项目里,电池分压还可能通过 MOS 管控制,只在测量时打开。
6) 定时器、中断、DMA:让系统从“排队干活”升级为“有节奏地协作”
你可以用 delay 写出很多入门实验,但真正的项目不能只靠 delay。
假设你要做一个温湿度显示系统:
- 每 1 秒读一次温湿度;
- 每 200ms 刷新一次按键;
- 每 50ms 更新一次 LED 状态;
- 串口随时可能收到命令;
- OLED 需要定期刷新;
- 超温时蜂鸣器要按节奏响。
如果你到处写 delay,系统就会像一条单车道:前面有人慢吞吞,后面全部堵死。
6.1 定时器:给系统一个节拍器
定时器可以产生周期中断,也可以输出 PWM、测量频率、捕获脉冲。入门阶段先把它当“系统节拍器”。
比如每 1ms 产生一次 tick,然后任务用时间戳判断是否该执行:
voidapp_loop(void){uint32_tnow=millis();if(now-last_key_scan>=5){last_key_scan=now;key_scan();}if(now-last_sensor_read>=1000){last_sensor_read=now;sensor_read_request();}if(now-last_led_update>=50){last_led_update=now;led_update();}}这种写法的本质是非阻塞调度。它不要求你立刻学 RTOS,但已经在培养 RTOS 思维。
6.2 中断:处理突发事件,但不要在里面安家
中断适合处理“不能错过”的事件:
- 串口收到字节;
- 定时器到点;
- 外部引脚边沿触发;
- ADC 转换完成;
- DMA 传输完成。
中断函数里要遵守几条纪律:
- 尽量短;
- 不要 delay;
- 不要做大段计算;
- 尽量不要 printf;
- 只置标志、搬少量数据、清中断标志;
- 复杂逻辑交给主循环或任务。
【干货】ISR 不是办公室,是前台。它负责接电话、记信息、叫人处理,不负责把整个项目做完。
6.3 DMA:让外设自己搬数据
DMA 可以在外设和内存之间搬运数据,减少 CPU 参与。比如:
- UART DMA 接收一大段数据;
- SPI DMA 刷屏;
- ADC DMA 连续采样;
- I2C/SPI DMA 读传感器数据。
新手不必一上来就用 DMA,但你要知道它解决的问题:当数据量变大、频率变高、CPU 忙不过来时,让硬件搬运比 CPU 一字节一字节搬更稳定。
学习顺序建议:
- 先轮询跑通;
- 再中断优化响应;
- 最后 DMA 优化吞吐。
别第一天就把 DMA、中断、缓存、半传输回调全搅在一起。那不是学习,是给自己开困难模式。
7) 调试工具:不要只靠眼神和感觉
嵌入式调试最痛苦的地方是:很多错误发生在你看不见的电信号里。
所以工具很重要。
7.1 串口日志:最便宜的系统旁白
串口日志适合回答:
- 程序有没有跑到这里;
- 变量当前是多少;
- 状态机切到哪一步;
- 传感器返回了什么;
- 错误码是什么。
但注意,串口打印也会占时间。不要在高频中断里疯狂 printf,也不要在实时性强的地方输出长日志。
7.2 逻辑分析仪:通信协议的照妖镜
逻辑分析仪可以抓数字波形,并解码 UART、I2C、SPI 等协议。它能回答:
- 线上有没有波形;
- 波特率是否正确;
- I2C 地址有没有 ACK;
- SPI 模式是否像预期;
- CS 时序有没有拉对;
- 数据有没有粘包或丢包。
几十块到几百块的逻辑分析仪,对新手来说非常值。它能把“我觉得发了”变成“线上确实发了 0x55”。
7.3 示波器:看模拟、电源和边沿
示波器适合看:
- PWM 波形;
- 电源纹波;
- 按键抖动;
- ADC 输入;
- I2C/SPI 边沿质量;
- 电机启动时电压下跌;
- 复位脚是否被干扰。
逻辑分析仪告诉你 0 和 1,示波器告诉你这个 0 和 1 背后的电压到底长什么样。
7.4 SWD/JTAG:进 MCU 里面看看
SWD/JTAG 调试器可以让你:
- 打断点;
- 单步执行;
- 查看变量;
- 查看寄存器;
- 看调用栈;
- 观察内存;
- 定位 HardFault。
新手不要害怕断点调试。串口日志适合看流程,断点调试适合看瞬间状态。两个搭配使用,效率会高很多。
【调试四证据法】一个外设调不通时,尽量拿到四类证据:
- 代码证据:程序执行到了正确位置;
- 寄存器证据:外设配置和状态正确;
- 波形证据:线上真的有符合协议的信号;
- 物理证据:电源、地线、电平、连接都正确。
只有代码证据是不够的。嵌入式 bug 很多时候藏在另外三类证据里。
8) 把知识串成项目:桌面环境监测小站
现在我们设计一个适合新手进阶的小项目:桌面环境监测小站。
目标不是做一个商业产品,而是在一个项目里把核心外设串起来。
8.1 项目功能
硬件可以这样选:
- MCU:STM32、ESP32 或其他你熟悉的开发板;
- 温湿度传感器:SHT30、AHT20、BME280 等 I2C 传感器;
- OLED:I2C 或 SPI 接口;
- 一个按键;
- 一个 LED;
- 一个蜂鸣器;
- 一个电位器或电池分压输入,练 ADC;
- USB-TTL 串口调试。
功能:
- 每 1 秒读取温湿度;
- OLED 显示温度、湿度、运行状态;
- 串口输出日志;
- 串口输入命令设置报警阈值;
- 按键切换显示页面;
- 温度超过阈值时 LED 闪烁、蜂鸣器报警;
- ADC 读取电位器,用来模拟阈值调节或电池电压;
- 系统不使用长时间阻塞 delay。
8.2 模块分层
建议文件结构:
app/ app_main.c app_state.c drivers/ bsp_led.c bsp_key.c bsp_oled.c sensor_sht30.c uart_protocol.c adc_voltage.c platform/ board.c board.h不要把所有代码都塞进main.c。main.c只负责初始化和调度,具体硬件动作放驱动层,业务逻辑放应用层。
8.3 主循环框架
主循环可以这样组织:
intmain(void){board_init();app_init();while(1){uint32_tnow=millis();key_task(now);sensor_task(now);display_task(now);alarm_task(now);uart_protocol_task(now);}}每个 task 都自己判断时间到了没有,不到了就立刻返回。这样系统不会被某一个模块拖住。
8.4 串口命令设计
先用文本命令,简单好调:
TEMP? HUMI? THR 30.5 LED ON LED OFF BEEP OFF STATUS?返回可以这样:
OK TEMP=26.4 OK HUMI=52.1 OK THR=30.5 ERR CMD这个小协议能练到命令解析、字符串处理、状态反馈。等你以后做更复杂的设备,再升级成二进制帧、长度字段和 CRC。
8.5 I2C 传感器驱动思路
以常见 I2C 传感器为例,驱动层要做几件事:
- 初始化传感器;
- 发送测量命令;
- 等待转换完成;
- 读取原始数据;
- 校验数据;
- 换算成实际单位;
- 返回错误码。
应用层不应该关心 I2C 具体怎么读写,它只需要调用:
sensor_data_tdata;if(sensor_read(&data)==SENSOR_OK){app_update_environment(data.temperature,data.humidity);}这就是分层的好处:底层协议细节变化时,应用层不至于被牵着重写。
8.6 OLED 显示:别一秒刷一万次
OLED 刷新也要有节奏。温湿度 1 秒更新一次,显示 5 到 10 次每秒已经很顺眼了。没必要主循环跑多快就刷多快。
显示层建议维护一个“当前页面”:
- 页面 1:温湿度;
- 页面 2:阈值和报警状态;
- 页面 3:ADC 电压和系统运行时间;
- 页面 4:调试信息。
按键短按切换页面,长按复位统计数据或静音报警。
8.7 报警逻辑:用状态机,不要用一堆 if 硬堆
报警可以设计成几个状态:
- NORMAL:正常;
- WARNING:接近阈值;
- ALARM:超过阈值;
- SILENCED:用户临时静音;
- SENSOR_ERROR:传感器错误。
状态机的好处是:你能清楚地定义从一个状态到另一个状态的条件。
例如:
- 温度超过阈值 3 次连续采样,进入 ALARM;
- 用户长按按键,进入 SILENCED;
- 静音 60 秒后,如果温度仍超限,重新进入 ALARM;
- 传感器连续读取失败,进入 SENSOR_ERROR;
- 温度恢复到阈值以下并留有回差,回到 NORMAL。
这里提到“回差”很重要。比如阈值 30 度,如果刚到 30.0 就报警,降到 29.9 就解除,温度在 29.9 和 30.1 之间抖动时,报警会来回跳。可以设置:
- 30.0 度以上报警;
- 28.5 度以下解除。
这就是回差,能让系统更稳定。
9) 常见协议问题对照表
| 协议/模块 | 现象 | 常见原因 | 排查动作 |
|---|---|---|---|
| UART | 乱码 | 波特率/时钟/帧格式不一致 | 固定 115200 8N1,抓逻辑分析仪 |
| UART | 收不到 | TX/RX 没交叉、没共地 | 交叉接线,测 TX 是否有波形 |
| UART | 收半包 | 接收方式阻塞或无缓冲 | 中断/DMA + 环形缓冲 |
| I2C | 一直 NACK | 地址错、没上拉、没供电 | 扫描地址,查 7/8 位,测 SDA/SCL |
| I2C | 偶尔失败 | 速度过快、线长、干扰 | 降到 100kHz,缩短线,加合适上拉 |
| SPI | 读 ID 全 0xFF | MISO 上拉、从机没响应 | 查 CS、模式、复位脚、电源 |
| SPI | 屏幕花屏 | CPOL/CPHA 或初始化序列错 | 对 datasheet,降速抓波形 |
| ADC | 数值乱跳 | 参考电压/输入阻抗/采样时间 | 延长采样,加 RC,检查 Vref |
| ADC | 数值比例不对 | 分压系数或参考电压算错 | 用万用表测输入点,重新标定 |
| PWM | 舵机乱动 | 频率/脉宽不对或供电不足 | 确认 50Hz 和脉宽,独立供电共地 |
这张表的价值不在于背,而在于提醒你:每个协议都有自己的“物理层脾气”。调试时要尊重它。
10) 新手学习顺序:从能看见结果到能解释原因
建议你按这个顺序练:
- UART 打印日志。先让 MCU 能和电脑说话。
- UART 接收命令。练输入、缓冲、协议解析。
- I2C 读一个传感器。练地址、ACK/NACK、寄存器读写。
- I2C 或 SPI 驱动 OLED。练显示和数据组织。
- ADC 读电位器。练模拟量换算、滤波、标定。
- 定时器非阻塞调度。练系统节奏。
- 中断接收串口或按键。练事件处理。
- 逻辑分析仪抓 UART/I2C/SPI。练证据链。
- 把所有模块整合成一个小项目。
- 最后再考虑 DMA、RTOS、低功耗和更复杂协议。
每一步都要有可见成果。不要只看教程,不上手验证。嵌入式知识如果没有落到电路和波形上,很容易看起来懂了,做起来全忘。
【干货】每学一个外设,都用这五问验收:
- 我知道它用哪些引脚吗?
- 我知道它的电气要求吗?
- 我知道初始化参数为什么这样配吗?
- 我能用工具看到它的信号吗?
- 我能把它封装成一个稳定的驱动接口吗?
五问都能答上来,这个外设才算真的入门。
11) 本篇最终总结:通信协议是嵌入式项目的血管
读到这里,你应该已经建立起这样的认识:
- UART 是入门最重要的通信方式,也是调试第一工具;
- I2C 适合连接低速多设备传感器,但要注意地址、ACK 和上拉;
- SPI 适合高速设备,重点关注 CS、模式、时钟和读写时序;
- ADC 把模拟电压变成数字,精度不仅取决于位数,还取决于参考电压、采样时间和硬件设计;
- 定时器让系统有节奏,中断处理突发,DMA 优化大量数据搬运;
- 调试不能只靠代码,要结合串口、逻辑分析仪、示波器、SWD/JTAG;
- 小项目要分层,主循环要非阻塞,业务逻辑最好用状态机。
一句话总结:
嵌入式通信不是把协议背熟,而是让数据从真实世界可靠地走到程序里,再从程序可靠地走回真实世界。
当你完成一个“温湿度传感器 + OLED 显示 + 串口命令 + ADC 输入 + 报警状态机”的小项目时,你已经不只是会点灯了。你已经开始具备嵌入式工程的基本形状:会接硬件、会读手册、会调协议、会分层、会验证、会把一堆零散外设组织成一个系统。
这时再学 RTOS、网络协议、低功耗、驱动框架、Bootloader,就不再是空中楼阁。你脚下已经有地面了。