单片机通信协议怎么学?UART、I2C、SPI核心要点与调试实战
“你学单片机不学通信协议”这句话虽然刺耳但确实点破了很多人的学习困境单片机入门时写点流水灯、按键扫描、数码管显示一切都很顺利可一旦想把传感器数据读回来、和上位机对话、接个屏幕就发现代码莫名其妙。有人读温湿度传感器全是 0xFF有人 OLED 花屏有人串口收到的数据全是乱码。这些问题十有八九都出在通信协议上。这篇文章不是只丢一张协议速查表给你而是从“为什么学”讲到“怎么用”再给可以直接改的代码示例和排查思路。适合正在学 51、STM32、GD32或者准备做课设、电子设计竞赛、毕业设计的开发者。看完之后你会明白 UART、I2C、SPI、单总线这类协议到底在解决什么问题也能自己在项目里组织一套可靠的数据通信逻辑。1. 你知道学单片机不学通信协议最常卡在哪里吗1.1 很多疑难杂症最终根源都在通信先看几个常见的现象用 I2C 接口的 OLED 显示屏幕上什么都没有或者偶尔出来一个点。用 SPI 读写 Flash读到的一直是 0xFF或者前 3 个字节对、后面全错。用串口把单片机数据发给电脑乱码波特率明明改过了还是乱码。读 DHT11 温湿度持续超时代码看了三遍也不知道问题在哪。这些问题有一个共同点开发板本身没问题代码编译也没报错连 LED 都能闪但外设就是“不理你”。为什么因为从传感器、屏幕、存储芯片到单片机每一方都有自己的“说话方式”。你的代码只是瞎发了一通电平信号对方根本不知道你在说什么自然也就不会给你想要的回应。这就是通信协议的重要性。1.2 不会通信协议往往有这几种典型表现第一种完全复制例程但换个引脚、换个芯片就废了。原因是只能看到代码里一堆寄存器赋值不理解“先拉低 CS、再发指令、再收数据”这个完整过程。第二种只敢用官方库封装好的 API。一旦没有库或者要适配一个不常见的传感器就不知道如何用 GPIO 手动模拟时序。第三种自己做板间通信或者和上位机联调时只会把所有数据裸发出去没有帧头、校验、重传的概念。偶尔能通但不稳定。第四种学习路径混乱。今天听说 SPI 快就去学 SPI明天听说 CAN 很强大就去学 CAN结果哪个都只学了个皮毛。学通信协议的目的不是背下来一串寄存器名字而是建立一种能力给你一份芯片数据手册你能看懂时序图给你一个陌生传感器你能把它的数据读回来给你一套通信需求你能设计出不会丢帧、不会乱码的传输格式。2. 通信协议到底是什么为什么不是“发高低电平”就够了2.1 用一句话理解通信协议你可以把两台设备之间的通信理解成两个人打电话。两个人必须约定同一门语言。A 说“你好”B 如果只懂日语那就没法交流。设备也是一样单片机发送一串高低电平传感器必须能把这串电平解释成“地址”“命令”“数据”“校验”然后才能给出响应。所以通信协议本质上是一套约定通常包含四部分电平规则什么电压代表 0什么代表 1。时序规则信号什么时候变化双方按什么节奏采样。帧格式一帧里谁在前谁在后哪个是命令、哪个是数据、哪个是校验。错误处理数据错了怎么发现甚至怎么重传。很多初学者只关注“怎么把 GPIO 拉高拉低”忽略后三部分这是后面出问题的根源。2.2 串行、并行、同步、异步先把维度分清单片机通信最常听到的几个词UART、SPI、I2C、CAN、USB、以太网。想要不容易混淆先记住两个维度。第一个维度并行还是串行。并行通信一次同时传多位数据速度快但占用的引脚多。现在大部分单片机项目更愿意用串行通信一根或几根线把数据按位依次传过去。所谓 USB、SPI、I2C、UART虽然在电气上有很大差异但本质都是串行通信。第二个维度同步还是异步。同步通信会额外提供一根时钟线。比如 I2C 有 SCLSPI 有 SCK。接收方跟着发送方的时钟去采样不需要提前约定波特率。异步通信没有专门时钟线。例如 UART通信双方必须提前约定一个波特率靠起始位和停止位来对齐边界。把这两个维度记清楚后面看任何协议都会轻松很多。2.3 学协议要抓四个抓手遇到一个陌生协议先问四件事它用几根线线的名字和功能是什么有没有时钟线如果没有双方靠什么同步一帧数据的顺序是什么起始位、地址位、数据位、停止位、校验位分别在哪里多设备挂在同一总线上时靠什么区分“这条消息是发给我的”这四点搞清楚再去看数据手册里的时序图基本就不慌了。3. 单片机通信协议学习路线先学哪些再学哪些3.1 五个最该上手的通信协议对于绝大多数单片机项目下面的协议覆盖了 90% 的使用场景协议典型速度引脚数量常见使用场景易错重点UART/USART常见 9600/115200bps2~4调试串口、GPS、蓝牙模块、上位机通信波特率一致、帧格式I2C100k/400k/1M2温湿度传感器、OLED、EEPROM、触摸芯片地址、上拉电阻、ACKSPIMHz 级3~4 每从机一条 CSFlash、SD 卡、LCD、高速 ADC时钟极性和相位1-Wire较低1DHT11/DHT22、DS18B20严格时序和超时CAN常见 125k~1M2车载、工业控制、多机通信终端电阻、报文滤波除了这些你可能还会在物联网项目里遇到以太网 MAC/PHY、BLE、Wi-Fi 模块等。但它们的底层已经做成模块或协议栈更多是“调用接口”的问题。裸机单片机阶段优先把上表前五个弄清楚。3.2 选协议不只看速度更看场景很多新手会问I2C 和 SPI 哪个好只看速度SPI 上限更高只看接线数量I2C 更省引脚但如果设备本身没有对应硬件外设或者你想灵活移植也可以用 GPIO 模拟。更合理的思路是让场景决定协议调试输出、接 GPS、接串口屏、和上位机聊天用 UART。挂多个传感器且速率要求不高用 I2C。读写 Flash、SD 卡、大屏这类高吞吐场景用 SPI。DHT11、DS18B20 属于单总线设备老老实实按单总线时序读。多节点工业控制、车规场景用 CAN。不要拿着一块 SPI 接口的 Flash 去问“能不能用 I2C 读”。芯片支持什么协议硬件上已经决定了。你在 I2C 总线上模拟 SPI 也许能“凑合”但不稳定也不专业。3.3 学习环境与必备工具准备单片机通信协议的调试不能只靠眼睛看代码。至少需要准备一块常见开发板。无论是 STC 系列、STM32F103C8T6、GD32 还是更专业的板子学习思路一致区别只是寄存器或者库函数不同。一个 USB 转 TTL 串口模块。用于 UART 通信几乎每个项目都会用到。一个逻辑分析仪。不需要很贵能抓几十 MHz 信号的那种入门型号即可观察 I2C、SPI、单总线时序非常方便。一个万用表。检查电压、通断、上拉电阻是否正常。对应芯片的数据手册和传感器的数据手册。版本以你手上模组为准不要只看网上零散代码。开发环境方面有人用 Keil有人用 STM32CubeIDE有人用 SDCC还有人用 Arduino IDE。工具不决定你能否学会协议但至少需要你具备一种能烧录并调试代码的环境。版本不需要追求最新能配合你的芯片和调试器即可。此外强烈建议学会看编译输出信息。比如 Keil 编译后会有 Program Size 信息如果 code 段大小超过芯片 ROM链接时就会报溢出。遇到这类问题可以先检查优化等级、是否有未用的大数组、代码段是不是选错了芯片型号而不是一头扎进代码里找 bug。4. UART/USART最基础的异步通信串口调试离不开它4.1 为什么叫“异步”通信UART 全称是 Universal Asynchronous Receiver/Transmitter通用异步收发器。异步意味着通信双方没有共同的时钟线。发送方按约定的波特率把每个 bit 依次放到 TX 引脚接收方也要按同样的波特率去采样 RX 引脚。一个典型的 UART 帧是这样的起始位(1bit) 数据位(5~9bit常见8bit) 校验位(可不选) 停止位(1~2bit)平时配置串口时说的“9600, 8, N, 1”意思就是波特率 9600bps8 位数据位无校验1 位停止位。双方只要有一个参数不一致收到的就是乱码。4.2 最基础的数据收发代码思路如果使用 STM32 HAL 库初始化时一般会通过 CubeMX 配置串口参数。核心代码如下// 示例思路以常见 HAL 库风格为例函数名会随版本/芯片不同而变化 void uart_config_example(void) { // 配置 UART 参数 huart.Instance USART1; huart.Init.BaudRate 115200; huart.Init.WordLength UART_WORDLENGTH_8B; huart.Init.StopBits UART_STOPBITS_1; huart.Init.Parity UART_PARITY_NONE; huart.Init.Mode UART_MODE_TX_RX; // HAL_UART_Init(huart) 等函数由实际工程生成 }如果你用的是 51 单片机的串口本质也是配置波特率发生器、设置串口模式、使能收发中断。无论底层怎么封装思路都一样波特率一致帧格式一致数据才能正确流动。发送数据也很直接// 向串口发送一字节 // printf 重定向或直接调用底层写函数均可 void uart_send_byte(uint8_t data) { // 等待发送数据寄存器为空然后写入数据 // 不同库 API 名称不一但都是“等待-写数据” }很多新手容易犯一个错在中断里收发数据时把所有解析逻辑都写到中断函数里。正确思路是“中断只负责收字节业务解析放在主循环或任务里”避免长时间占用中断。4.3 接收不定长数据不要用 delay 等待实际项目里串口发送的数字往往不是一个固定字节上位机可能发来AA 55 01 02 03 04 校验这样的帧。如果只调用一个“阻塞接收”函数在没收到数据时会一直卡住严重影响单片机处理其他任务。更好的做法是用状态机逐字节解析// 状态机解析示意从串口接收缓冲区读一个字节边收边解析 enum FrameState { FRAME_IDLE 0, FRAME_HEADER1, FRAME_HEADER2, FRAME_LENGTH, FRAME_DATA, FRAME_CHECK }; uint8_t frame_buf[64]; uint32_t frame_len 0; uint8_t received 0; void protocol_parse(uint8_t byte) { static enum FrameState state FRAME_IDLE; static uint8_t data_len 0; static uint8_t sum 0; switch (state) { case FRAME_IDLE: if (byte 0xAA) // 帧头第 1 个字节 state FRAME_HEADER1; break; case FRAME_HEADER1: if (byte 0x55) // 帧头第 2 个字节 state FRAME_HEADER2; else state FRAME_IDLE; break; case FRAME_HEADER2: frame_len 0; data_len byte; // 负载长度 sum byte; state FRAME_LENGTH; break; case FRAME_LENGTH: // 暂不处理扩展字段 state FRAME_DATA; break; case FRAME_DATA: if (frame_len data_len) { frame_buf[frame_len] byte; sum byte; if (frame_len data_len) state FRAME_CHECK; } else { state FRAME_IDLE; } break; case FRAME_CHECK: if (sum byte) // 校验通过 received 1; state FRAME_IDLE; break; } }每次串口中断收到一个字节就把这个字节交给protocol_parse。主循环检测到received 1再去处理完整的帧。这样既不会阻塞也方便后续加超时处理。5. I2C两根线连接一排传感器靠地址区分设备5.1 I2C 的“两条线 地址”设计I2C 总线由 SCL时钟线和 SDA数据线两根线组成。典型速率有标准模式 100kbit/s、快速模式 400kbit/s、快速模式 1Mbit/s 等不同芯片和设备支持的上限不同具体速率要查数据手册。和 UART 不同I2C 是同步通信SCL 提供时钟它也是多主总线总线上可以挂多个设备。每个设备有一个器件地址主机通信时先发地址从机判断“这帧是不是发给我的”。I2C 的引脚大多是开漏结构所以需要外接上拉电阻阻值常见 2.2kΩ~10kΩ。如果总线上有设备没有被上拉SDA 可能一直拉不高通信就卡在起始条件或者 ACK 上。5.2 最短的 I2C 写数据流程以常见的 EEPROM 芯片 AT24C02 为例它的器件地址中写地址常见为 0xA0。一次写单字节的流程是主机发送起始条件。主机发送器件地址 写标志。从机返回 ACK。主机发送目标存储地址。从机返回 ACK。主机发送要写入的数据。从机返回 ACK。主机发送停止条件。下面这段是软件模拟 I2C 的核心框架。需要你提供i2c_scl_write和i2c_sda_write这类 GPIO 操作函数不同平台引脚不同替换即可。// 软件模拟 I2C 主机底层函数GPIO 操作请按平台替换 void i2c_start(void) { i2c_sda_write(1); i2c_scl_write(1); delay_us(5); i2c_sda_write(0); // SCL 高电平时 SDA 产生下降沿 起始 delay_us(5); i2c_scl_write(0); } void i2c_stop(void) { i2c_sda_write(0); i2c_scl_write(1); delay_us(5); i2c_sda_write(1); // SCL 高电平时 SDA 产生上升沿 停止 delay_us(5); } uint8_t i2c_wait_ack(void) { uint8_t ack; i2c_sda_write(1); // 释放 SDA 由从机控制 i2c_scl_write(1); delay_us(5); ack i2c_sda_read(); i2c_scl_write(0); return ack; } void i2c_write_byte(uint8_t dat) { uint8_t i; for (i 0; i 8; i) { i2c_sda_write((dat 0x80) ? 1 : 0); dat 1; i2c_scl_write(1); delay_us(5); i2c_scl_write(0); delay_us(5); } }读单字节和写单字节类似区别是主机在最后一个字节读完后要发送 NACK告诉从机“不要再发了”。这段代码看着简单但它已经完整体现了 I2C 最重要的起始、停止、ACK 机制。如果总线上有硬件 I2C 外设也可以使用硬件外设的库函数不过理解这套时序能帮你解决很多“硬件 I2C 卡死”的疑难问题。5.3 I2C 调试的常见坑找不到设备先确认地址。7 位地址和 8 位地址经常因为“左移一位”导致错误。SDA 一直为高或一直为低查上拉查线序是否接反。卡在等待 ACK从机没工作、地址错、总线被死锁可以尝试给 SCL 连续发送几个脉冲复位从机状态机。代码一上电就死机有些 I2C 从机上电状态不确定需要先软件复位总线。6. SPI快但别忽略时钟极性和相位6.1 SPI 的四根线SPI 通常使用四根信号线SCLK串行时钟由主设备产生。MOSI主出从入。MISO主入从出。CS/SS片选低电平有效选择当前要通信的从设备。多一个从设备通常就要多一根 CS 线。主机想和哪个设备通信就把哪个设备的 CS 拉低通信结束后再拉高。这也是 SPI 和 I2C 一个明显区别I2C 靠地址SPI 靠片选。6.2 CPOL 和 CPHA 为什么折磨人SPI 有四种模式由时钟极性 CPOL 和时钟相位 CPHA 决定CPOL 决定空闲时 SCLK 是高还是低。CPHA 决定数据在 SCLK 的上升沿采样还是下降沿采样。如果主设备和从设备配置的 SPI 模式不一致数据就会错位。表现往往是“第一个字节看起来对后面全乱”或者“读回的数据二进制位反了”。遇到 SPI 通信异常先看数据手册上从机要求的 CPOL/CPHA 是哪个模式再检查主设备配置比盲目改代码有效得多。6.3 SPI 读 Flash ID 是个典型入门实验以常见 SPI Flash 为例读 JEDEC ID 的指令是 0x9Fvoid spi_read_flash_id(void) { uint8_t cmd 0x9F; uint8_t id[3] {0, 0, 0}; flash_cs_low(); // CS 拉低选中 Flash spi_transfer(cmd, 1); // 发送读 ID 指令 spi_transfer(id, 3); // 连续读 3 个字节 flash_cs_high(); // CS 拉高结束传输 printf(Flash ID: %02X %02X %02X\n, id[0], id[1], id[2]); }核心逻辑就是CS 拉低、发命令、读数据、CS 拉高。任何 SPI 设备几乎都遵守这种“片选有效后先发命令再读写”的结构。SPI 没有 ACK 机制所以从机是否正确收到数据通常只能通过读回的内容验证。7. 从单总线 1-Wire 到 CAN按需求补充协议版图7.1 1-Wire一根线也要讲时序DHT11、DHT22、DS18B20 这类器件使用 1-Wire 单总线数据线和时钟合并成一根线。主机发送起始信号之后从机回响应然后开始按位返回数据。读取“0”和“1”的区别主要靠测量高电平持续时间的长短。DHT11 这类传感器的驱动看似简单实际很容易踩坑主机起始信号的低电平要保持足够长不同器件的参数有差异。读位时要精确延时延时太久可能导致后面位错乱。传感器上电后需要稳定时间不能一上电立刻读。最好设置超时退出避免传感器没接好时程序卡死在读数循环。下面是一个 DHT11 读取的核心思路// 返回 1 表示读到高电平返回 0 表示读到低电平 uint8_t dht11_read_bit(void) { uint8_t bit 0; // 等待低电平结束 while (dht11_pin_read() 0) { if (timeout_expired()) return 0xFF; } delay_us(40); // 延时后采样根据数据手册调整 if (dht11_pin_read()) bit 1; // 等待当前位结束 while (dht11_pin_read() 1) { if (timeout_expired()) return 0xFF; } return bit; }所有单总线驱动不建议只用“网上的模板”最好对照你手中传感器数据手册的时序图把每一段高低电平时间核对一遍。因为不同批次、不同厂家的 DHT11时序参数也存在差异。7.2 CAN面向多节点和强干扰场景如果你做的是车载通信或者工业多机控制CAN 总线会非常常见。CAN 使用差分信号传输抗干扰能力强支持多主机节点可以通过报文 ID 决定是否接收。CAN 驱动调试时除了波特率还要重点检查总线两端是否有终端电阻。终端电阻一般取 120Ω用于匹配传输线阻抗。少了终端电阻总线信号容易反射通信质量会明显下降。7.3 协议栈只是另一层“封装”当你逐渐接触蓝牙 BLE、Wi-Fi、以太网之后会发现很多模块内部已经实现了完整的协议栈。单片机只需要通过 UART 或 SPI 给模块发 AT 指令或特定格式数据模块回传结果即可。此时你要做的不是把 TCP/IP 协议栈手写一遍而是先会看懂“模块指令手册”里的通信帧格式。这和单片机里的底层协议有相通之处发什么命令、参数用什么字节序、返回帧如何校验、发生错误时错误码是什么。如果你的项目还要设计设备端和服务器的通信比如智能硬件上报数据到云平台一般会涉及 token、签名等认证字段。这块属于应用层安全设计和单片机底层的 I2C/SPI 不是同一个层次但仍然要记住任何协议都应包含鉴权与越权防护意识不要把敏感接口裸奔在公网上。8. 实战UART 指令 DHT11 单总线 I2C OLED 组合验证8.1 项目需求拆解把前面知识点串起来设计一个最简单的温湿度采集器DHT11 通过单总线接口接到单片机。OLED 屏通过 I2C 接口显示温湿度。上位机通过 UART 发送指令查询当前温湿度。整体数据流可以画成这样上位机 --UART-- MCU 协议解析 DHT11 --1-Wire-- MCU 温度读取 MCU --I2C-- OLED 显示这个项目的核心不在于把某个驱动抄通而在于“三种通信方式同时协作时系统设计如何不混乱”。8.2 先把硬件相关函数隔离出来为了不让协议代码和具体芯片绑定可以使用几个底层接口// hardware_interface.h void hal_dht11_init(void); uint8_t hal_dht11_read_bit(void); void hal_i2c_write_byte(uint8_t dat); void hal_i2c_start(void); void hal_i2c_stop(void); void hal_uart_send(uint8_t *buf, uint16_t len);这样无论是 STM32、STC 还是 GD32只要把这些函数用对应平台的 GPIO、UART、硬件 I2C 实现上层的传感器读取逻辑和协议解析逻辑就不用大改。8.3 编写协议分发函数上位机不一定只发“读取温湿度”一个指令。为了演示我们约定一个简单自定义帧帧头: 0xAA 命令: 0x01(查询) 校验: 0x01实际项目里更复杂的协议可以在此基础上扩展长度、数据区、CRC。示例解析函数如下void handle_uart_command(uint8_t cmd) { switch (cmd) { case 0x01: { uint8_t humi, temp; if (dht11_read_data(humi, temp) 0) { // 通过串口回传结果 uint8_t resp[6] {0xAA, 0x81, humi, temp, 0x00, 0x00}; resp[4] resp[0] resp[1] resp[2] resp[3]; hal_uart_send(resp, 5); } break; } default: break; } }注意这里用了“加和校验”。比如前 4 个字节的和放在第 5 个字节接收方用同样算法加一遍如果结果不匹配这帧数据就丢弃。虽然求和校验简单但已经能拦截很多传输错误。要求更高的场景应换成 CRC16 或 CRC32。8.4 读取 DHT11 后通过 I2C 送到 OLEDDHT11 读取出来的温度、湿度是整数还是小数和器件型号与输出格式有关。下面只展示“单总线读取后调用 I2C 发送数据到 OLED”的衔接逻辑不展开 OLED 完整驱动void update_oled_with_dht11(void) { uint8_t humi 0, temp 0; if (dht11_read_data(humi, temp) 0) { // 将数值转成字符串后通过 I2C 写入 OLED 显示 RAM char line[32]; // snprintf 具体实现取决于你的编译环境 // oled_show_string(0, 0, line) 内部会使用 I2C 发送帧 } }OLED 指令虽然多但每一个功能都对应一个或几个 I2C 写帧。很多初学者花屏不是 OLED 芯片不支持而是发送的命令顺序不对或者初始化流程不完整。遇到这类问题不要急着改代码用逻辑分析仪抓一次 I2C 波形再对照数据手册检查每个字节是否符合预期通常五分钟就能定位。8.5 验证与现象确认把程序烧录后用串口助手发送AA 01或者直接观察 OLED 每秒刷新数据。正常现象应该是OLED 能稳定显示温度、湿度数值不会闪烁、花屏或间歇性显示 0xFF。串口助手能收到单片机回传的数据帧校验值正确。拔掉 DHT11 连接线后程序不会死等而是报错或显示上一次有效数据。如果出现问题优先检查逻辑分析仪的波形而不是凭感觉改延时参数。9. 通信异常排查思路与高频问题清单调试通信协议时有一个适用的排查顺序先看硬件连接。供电是否正常共地有没有接线有没有接反。再看时序波形。用逻辑分析仪抓引脚确认是否有起始信号、时钟、ACK。然后看数据内容。收到的字节是否符合协议格式和手册里的示例有没有出入。最后看代码逻辑。确认状态机是否进入错误分支有没有被非阻塞逻辑卡住。9.1 常见问题对照表问题现象常见原因解决思路串口收到乱码波特率、校验位、停止位不一致确认两端参数一致检查晶振频率是否引起误差过大I2C 卡在 ACK从机地址错误、上拉缺失、从机未上电用逻辑分析仪抓地址检查上拉电阻I2C SDA 一直为低总线死锁或从机异常拉低连续切换 SCL让从机恢复状态SPI 数据错位CPOL/CPHA 配置与从机不一致查看从机手册修改 SPI 模式SPI 读回全 0xFFCS 没控制好、电源不稳、Flash 未复位检查 CS 时序、供电和复位DHT11 读取超时起始时序不够、引脚不对、延时不准对照手册检查高低电平时间设置超时通信偶尔丢帧没有帧头/校验、中断频率过高增加帧格式降低波特率关闭无关中断编译提示程序超出 ROM芯片型号选错、代码优化等级过低、数组过大检查工程配置使用更高容量芯片9.2 “程序超出内存”是什么问题很多 STC 用户在编译时会看到类似data segment too large或code address space overflow的报错。这不是通信协议问题但它经常在加入协议解析代码后出现。原因是单片机内部 RAM/ROM 空间有限。比如 51 单片机普通变量的 data 段、堆栈、数组都可能占用 RAM函数代码则占据 ROM。解决方案有确认工程里选择的芯片型号和实际芯片一致。把大数组放到 code 区而不是 RAM 区。降低编译优化等级或者提高优化等级都试一下。精简代码去掉无用缓冲区。如果还不够换更大容量的芯片。9.3 带硬件 FIFO 的外设到底一次写多少数据STM32、GD32 等芯片的 UART、SPI、I2C 外设内部可能带有硬件 FIFO。此时“一次写入多少个字节”并不是拍脑袋决定的而是要看外设状态寄存器标志。发送前要检查“发送数据寄存器为空”或“TX FIFO 未满”。接收时如果 FIFO 有数据要读取硬件标志位判断还剩多少字节。使用 DMA 时需要配置传输长度并在完成中断里处理后续数据。不少新手的疑问是“为什么我一次性写 16 字节结果丢了几个”。大概率是写入时没有等待 FIFO 可写标志或者芯片发送太快外设还没把上一批字节发送完又强行写入导致数据覆盖。严格按型号的数据手册操作不要盲目套用别人的寄存器代码。10. 工程里的通信协议不只是让“波形对”这么简单10.1 能读通协议不等于能设计好协议学习别人的协议时你可能只需要复现时序。但到了项目里设备之间可能没有统一协议标准需要自己定义一套帧格式。这时候要考虑的东西远比“高低电平”多。一套比较可靠的自定义协议至少包含帧头用于接收方找到一帧的起点可以设计为两个固定字节。长度字段防止粘包和半包时不知道边界。命令字说明这一帧是查询、写入还是应答。设备地址或路由信息多设备通信时用于寻址。数据区真正要传的内容。校验和校验只适合要求不高的场景重要数据建议使用 CRC。超时机制如果接收端只收到半个帧超过一定时间就放弃避免状态机永远卡住。字段顺序还涉及字节序问题。单片机常用小端有些网络协议则用大端。多字节数值通信时必须显式约定字节序否则换个平台数据含义就错了。10.2 用状态机解析而不是阻塞等待很多初学通信协议的人会写这样的代码先死等一个字节再死等下一个字节。这在单任务的裸机程序里极容易导致主循环被阻塞。如果对方忽然不发数据你的单片机就卡死了。更好的做法就是把收到的一帧数据当成一个“事件流”用状态机逐步处理。这样无论对方发得快还是慢只要最终帧完整解析结果就是一致的。如果一帧数据中间断了也能靠超时机制自动复位。10.3 联调时多用“先固定后调试”的思路和上位机联调或者两个单片机之间互联时先不要急着直接跑真实业务数据。可以先用一个固定测试帧比如固定发 100 次AA 55 01 00 10 00观察接收方解析成功多少次。如果发现偶发失败再考虑是不是干扰、波特率误差、校验设计或电源波动造成的。这种方式在通信调试里非常高效。它把复杂的业务逻辑暂时卸掉只保留传输通道方便快速定位问题发生在物理层还是协议层。10.4 写驱动前的三条工程建议用逻辑分析仪保存一份“正常波形”后面代码改坏了拿出来对比就知道哪里不一样。不要只注释“这里发送温度”要写下帧格式和每个字段的含义方便两个月后的自己看懂。在仿真环境或开发板上先做小范围验证再考虑接入真实生产设备。尤其是涉及电机、强电、工业总线时任何协议参数的改动都要先断电验证、再小范围测试不要直接在生产环境里乱试。通信协议是单片机项目里最容易“表面会、实际不会”的部分。你把 I2C、SPI、UART、单总线这些基础协议吃透后再去看蓝牙模块、Wi-Fi 模块、CAN 总线、Modbus 协议会发现它们背后共享同一套思维物理层规定电平数据链路层规定帧格式应用层规定指令含义。学习时可以按协议类型逐个实践调试时则要多观察波形、多设计实验、多留退路。把每个传感器的手册看明白你的代码会比网上大多数例程都可靠。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →