AVR单片机SPI主从通信实战:从引脚配置到帧协议设计 简介一份基于ATmega8535主机与ATmega8从机之间SPI通信的完整工程源码面向嵌入式初学者及AVR开发者用于理解同步串行通信的协议实现与双机协作流程。包内共111个文件以C源码、头文件、汇编文件、HEX烧录文件及工程配置文件为主并附带文档说明便于对照学习SPI主机模式与从机模式的初始化、数据收发及从机选择逻辑。程序同时提供主机端与从机端两套完整工程包含寄存器配置、时钟极性与相位设定以及SS引脚拉低的交互流程可直接参考移植。资源包仅99KB轻量易用已有233人学习下载。通过阅读这两套代码可掌握SCK/MOSI/MISO/SS四线工作机制并理解多从机扩展方法由此还能延展到传感器采集、显示模块扩展等实际项目。 搞嵌入式的朋友应该都遇到过这种场景两块单片机之间要互相甩数据测个温度、传个控制指令、同步个状态。串口最常用但总得两边约定波特率I2C又要惦记地址和应答。我最近正好用ATmega8535当主机、ATmega8当从机把整套SPI通信程序从头到尾调通了从引脚规划到寄存器配置、帧协议设计到现场踩坑全流程跑了一遍。这篇文章就是给打算在AVR平台上做SPI主从通信的开发者一份可以直接抄作业的参考已经摸过单片机但第一次碰SPI的人也能根据这里的接线和代码把通信跑起来。1. 整体设计思路为什么用SPI做双机通信1.1 三种常见总线怎么选先回答很多新手会问的问题两块AVR之间传数据UART、I2C、SPI到底选哪个UART实现最简单TX和RX两根线就行但异步串行必须约定波特率数据帧也得自己处理收发缓冲没管好就容易丢数据。I2C两根线SDA、SCL支持多主机多从机但每个从机都要有地址每次收发还有应答和起始/停止条件速度一般到100k或400k比SPI慢一个量级。SPI四根线MOSI、MISO、SCK、SS全双工主机负责产生时钟靠片选信号区分从机。单主机单从机时最简单连地址都不需要写。我这次就是两块AVR点对点通信没有挂多个从机的需求所以SPI性价比最高速度快、全双工、代码量还小。如果后面要扩展多从机SPI靠SS线也能继续加暂时不需要引入更复杂的协议。1.2 硬件连接与管脚规划ATmega8535和ATmega8的SPI硬件都挂在PORTB上引脚定义完全一致PB4是SSPB5是MOSIPB6是MISOPB7是SCK。两块芯片都是5V逻辑直接连不用电平转换但一定要注意共地别让两块板子各接各的电源地不连通信必乱。实际接线很简单正因为主机从机引脚同名连接完全不用交叉同名引脚一一对接就行主机PB5(MOSI) → 从机PB5(MOSI)主机PB6(MISO) → 从机PB6(MISO)主机PB7(SCK) → 从机PB7(SCK)主机PB4(SS) → 从机PB4(SS)GND → GND这里我踩过坑。刚开始我以为SS可以随便接个IO口甚至悬空后来才意识到SPI模式下从机的SS引脚必须处理它决定从机是否被选中。单从机场景可以直接把从机SS接地让从机永远处于选中状态也可以让主机用普通IO口软件控制。最标准的做法还是把主机SS也配成输出并主动拉高避免硬件误触发主机模式故障。1.3 为什么主机选8535、从机选8这个组合没什么玄学纯粹手边有什么用什么。ATmega8535是40脚芯片IO多、Flash也大适合做主控跑人机交互、采集逻辑都宽裕ATmega8是28脚芯片体积小做从机执行专项任务很合适。两块芯片内核基本一样SPI寄存器完全兼容代码可以互相抄只要你清楚当前芯片工作在主机还是从机模式就行。2. SPI核心概念与寄存器配置2.1 四线时序和数据模式SPI是同步通信主机负责产生SCK时钟。每次SCK跳变主机和从机通过MOSI/MISO同时移出一位数据、同时移入一位数据所以它是全双工发一字节的同时必然收一字节。这个特点在收发指令和应答时特别方便但也带来一个新手容易忽略的问题你发送一次其实有数据回来如果不读SPDR返回值会在下一轮传输时被覆盖。通信模式由CPOL和CPHA组合决定也就是时钟空闲电平和数据采样沿的区别模式CPOLCPHA说明模式000SCK空闲低电平数据在上升沿采样模式101SCK空闲低电平数据在下降沿采样模式210SCK空闲高电平数据在下降沿采样模式311SCK空闲高电平数据在上升沿采样主机和从机必须设置成相同模式否则两边采样点完全错开收上来的数据就是乱的。我调试时最喜欢用模式0AVR默认不开CPOL和CPHA就是模式0代码最少示波器看波形也直观。2.2 寄存器位拆解SPCR、SPSR、SPDRAVR的SPI其实只需要管三个寄存器SPCRSPI控制寄存器SPE是SPI总开关SPIE是中断使能MSTR决定主机还是从机DORD控制数据位顺序0为MSB先发1为LSB先发CPOL/CPHA决定模式SPR1/SPR0配合SPSR里的SPI2X设置SCK频率。SPSRSPI状态寄存器SPIF是传输完成标志WCOL是写冲突标志传输没结束就往SPDR里写数据会置1SPI2X是时钟倍频位。SPDRSPI数据寄存器收发共用一个寄存器写它等于启动一次传输并清除SPIF。主机SCK频率选择是实际调试里很关键的一步。SCK F_CPU / 分频系数分频系数由SPR1:SPR0和SPI2X共同决定SPI2XSPR1:SPR0分频F_CPU8MHz时的SCK00042MHz00116500kHz01064125kHz01112862.5kHz10024MHz10181MHz11032250kHz11164125kHz我建议先用低速率通信比如选F_CPU/16逻辑跑通后再慢慢拉高。SPI虽然能跑到几兆但杜邦线长了、地线不好高速传输出错率会明显上升。3. 主机端程序初始化、发送与协议封装3.1 主机初始化步骤主机端初始化核心就三件事把MOSI、SCK、SS配成输出MISO保持输入再打开SPI并把MSTR置1。我用的是AVR-GCC引入avr/io.h就行。#include avr/io.h void SPI_MasterInit(void) { // PB5(MOSI)、PB7(SCK)、PB4(SS) 设为输出 DDRB | (1 PB5) | (1 PB7) | (1 PB4); // PB6(MISO) 保持输入 DDRB ~(1 PB6); // 主机SS输出高电平防止模式故障 PORTB | (1 PB4); // 使能SPI、主机模式、MSB先发、模式0、F_CPU/16 SPCR (1 SPE) | (1 MSTR) | (1 SPR0); }很多教程里主机初始化不会去动SS但在AVR上这是一个隐患。如果SS被误判为输入且被拉低主机SPI会认为自己变成了从机MSTR位被硬件清掉通信直接停摆。所以只要你想稳就把主机SS配成输出并拉高一遍。3.2 收发一字节的基础函数SPI收发一字节其实就是三步把要发的数据写进SPDR等SPIF置位再把SPDR读回来。注意即使你只想发不想收也最好把SPDR读掉否则接收缓冲里的残留值会影响后续读数的正确性。uint8_t SPI_MasterTransfer(uint8_t data) { SPDR data; // 等待传输完成 while (!(SPSR (1 SPIF))); // 返回收到的数据 return SPDR; }这个函数是主机端所有SPI操作的基础。给从机发一字节控制指令同时从从机那边收到一字节状态逻辑上就是一次SPI_MasterTransfer调用的事。3.3 数据帧设计别裸发字节只发一串裸字节的demo确实能跑但一旦从机要做多指令解析你就明白帧协议有多重要了。我的做法是给通信包加固定帧头、命令字、长度、数据和校验这样从机解析时不至于收到半个包就懵掉。一个简单帧结构可以设计成这样帧头0xAA 0x55连续两个字节用来对齐数据流命令字1字节比如0x01读温度、0x02写电机转速数据长度1字节说明后面数据域有几个字节数据域0~255字节实际要传的内容校验和1字节从命令字开始到数据域最后一位的异或结果发送端代码大概是这个逻辑void SPI_MasterSendFrame(uint8_t cmd, uint8_t *data, uint8_t len) { uint8_t sum cmd ^ len; SPI_MasterTransfer(0xAA); SPI_MasterTransfer(0x55); SPI_MasterTransfer(cmd); SPI_MasterTransfer(len); for (uint8_t i 0; i len; i) { SPI_MasterTransfer(data[i]); sum ^ data[i]; } SPI_MasterTransfer(sum); }有人觉得校验是多此一举但现场跑起来电磁环境一乱一个错误字节就可能让从机执行错误动作。加一个校验和只是多几行代码排查问题的效率能提升一大截。3.4 带应答的交互流程SPI是全双工主机发完命令后可以继续发起一次“空写”来获取从机应答。具体做法是主机再调用一次SPI_MasterTransfer参数随便填真正要用的是返回值。这个流程在设计主机菜单交互、状态轮询时非常够用。比如主机会每秒查询一次从机温度寄存器从机在收到查询命令后的下一个字节位置把温度值回传两边配合好节奏整个系统就像一个闭环流水线。4. 从机端程序从模式配置与中断处理4.1 从机初始化步骤从机初始化和主机最大的区别就是MSTR不置位同时MOSI、SCK、SS配置为输入只有MISO是输出。AVR从机模式不用管时钟频率时钟完全由主机提供。#include avr/io.h void SPI_SlaveInit(void) { // PB6(MISO) 设为输出 DDRB | (1 PB6); // MOSI、SCK、SS 保持输入 DDRB ~((1 PB5) | (1 PB7) | (1 PB4)); // 使能SPI、从机模式、MSB先发、模式0 SPCR (1 SPE); }从机初始化时同样要注意SS引脚。硬件SS输入为低时从机被选中为高时从机不参与通信。单从机场景如果不想管片选逻辑可以把SS直接接地等于永远选中系统但为了扩展性我一般还是把SS接到主机的PB4让主机主动控制。4.2 轮询方式和中断方式怎么选从机处理SPI数据有两种风格。轮询方式适合简单场景主循环一直查SPIF标志有了就处理。中断方式适合主机随时会发指令的场景尤其是从机同时还要干别的活比如控制电机、扫描按键。下面是一个典型的中断处理框架static volatile uint8_t rx_buf[32]; static volatile uint8_t rx_len 0; ISR(SPI_STC_vect) { uint8_t received SPDR; // 这里根据你的帧协议解析received rx_buf[rx_len] received; // 准备从机应答数据写回SPDR后由主机SCK自动移出 SPDR response_data; }中断方式要注意的是ISR里动作越短越好帧解析和业务处理最好拆到主循环里不要在中断里做大量计算。另外必须记得开全局中断否则SPI中断不会触发。4.3 从机的完整解析流程从机收到主机数据后最关键的是状态机解析。因为SPI是一字节一字节到的只有收满整个帧才真正构成一条指令。我的解析状态机一般分四步找帧头、读命令、读数据、验证校验和。核心逻辑用伪代码表达switch (state) { case 0: if (byte 0xAA) state 1; break; case 1: if (byte 0x55) state 2; else state 0; break; case 2: cmd byte; state 3; break; case 3: len byte; index 0; sum cmd ^ len; state 4; break; case 4: data[index] byte; sum ^ byte; if (index len) state 5; break; case 5: if (byte sum) 执行命令; state 0; break; }这种状态机写出来不复杂却能避免一字节干扰导致整个接收流错位。我第一次做双机通信时没加帧头主机发0x01从机莫名其妙多收到几个0x00排查了一晚上最后才明白是主机在空闲时多调了传输函数从机把无关字节也当成命令了。5. 踩坑实录常见问题与排查技巧5.1 数据全乱、偶发错位先查模式和速度SPI通信最典型的问题就是CPOL/CPHA不匹配。主机设了模式0从机却忘了配置两边采样点完全错开你看到的就是乱码。另一个常见原因是SCK频率太高。杜邦线超过20厘米或者地线回路太长高速SPI信号边沿变缓误码率直线上升。建议先用500kHz跑通再逐步提速。排查手段很简单有逻辑分析仪就抓SCK、MOSI、MISO三根线没有就用示波器看。看完判断SCK每个周期是否规则跳变再看MOSI数据是否符合预期。别瞎调代码先把电平波形喂到眼睛再说。5.2 从机收不到任何数据重点查SS和DDR从机收不到数据十有八九是SS没被拉低。主机如果只靠硬件SS从机SS还悬空那从机永远不会被选中。先用万用表量从机SS对地电压或者直接临时把它接地验证。另一个容易忽略的点是DDRB配置我见过有人从机初始化时把MOSI设置成了输出相当于两个输出端直接对打通信必挂。顺便提一句如果硬件SPI引脚被别的功能占用了也可以IO口模拟三线SPI但那是最后手段。硬件SPI有四线模拟得自己控SCK和片选时序代码量和时序风险都大能复用硬件模块就别自己造轮子。5.3 主机MSTR被清掉多半是SS惹的祸这个问题很有迷惑性程序跑着跑着主机就“失联”了重新上电又能好一会儿。后来查数据手册才发现AVR主机模式下如果SS被配置为输入且被拉低硬件会触发模式故障MSTR位自动清0SPI主机功能失效。解决办法就是我在主机初始化里强调的把SS配成输出并拉高。如果实在要用软件片选也一定保证主机SS是输出状态。5.4 电平不匹配和外接干扰ATmega8535和ATmega8都是5V器件直接连没压力但如果你混用3.3V的从机或者传感器5V电平打到3.3V芯片上就可能烧引脚或者误码。这时候要么加电平转换芯片要么在数据线上串电阻分压别硬怼。现场干扰方面可以在SCK线附近少走大电流线给两块板子共用一个电源地必要时在电源两端加10uF加100nF电容滤波。5.5 读写缓冲区被覆盖的坑SPI是同时收发很多人只写SPDR不读SPDR结果第二次传输的返回值其实是上一帧数据逻辑上就全都对不上了。而且发送完一定要等SPIF置位否则写SPDR会产生WCOL写冲突。我现在的习惯是无论发什么函数都要返回SPDR的值用不上就丢弃但一定要读一次。5.6 调试前先做一次回环测试这个习惯我建议你从一开始就养成。新板子焊接完、第一行SPI代码还没写之前先把主机MISO和MOSI用一根杜邦线短接然后发一个固定值比如0xA5看能不能原样收回来。能回来说明SPI模块、引脚、时钟链路都是通的收不回来大概率是硬件问题跟你的从机程序没半毛钱关系。等回环过了再接上从机再去查线序、模式和片选排查范围一下就缩小了。SPI这套东西一旦玩明白后面接SD卡、Flash芯片、屏幕驱动全是同样的套路寄存器都大同小异。真调不通的时候别急着换芯片、换方案示波器量一量SCK和SS两个信号多半就能找到答案。本文还有配套的精品资源点击获取