AVR单片机SPI主从通信实战:从引脚配置到帧协议设计
2026/9/9 0:04:46 网站建设 项目流程

简介:一份基于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是SS,PB5是MOSI,PB6是MISO,PB7是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空闲高电平,数据在上升沿采样

主机和从机必须设置成相同模式,否则两边采样点完全错开,收上来的数据就是乱的。我调试时最喜欢用模式0,AVR默认不开CPOL和CPHA就是模式0,代码最少,示波器看波形也直观。

2.2 寄存器位拆解:SPCR、SPSR、SPDR

AVR的SPI其实只需要管三个寄存器:

  • SPCR(SPI控制寄存器):SPE是SPI总开关,SPIE是中断使能,MSTR决定主机还是从机,DORD控制数据位顺序(0为MSB先发,1为LSB先发),CPOL/CPHA决定模式,SPR1/SPR0配合SPSR里的SPI2X设置SCK频率。
  • SPSR(SPI状态寄存器):SPIF是传输完成标志;WCOL是写冲突标志,传输没结束就往SPDR里写数据会置1;SPI2X是时钟倍频位。
  • SPDR(SPI数据寄存器):收发共用一个寄存器,写它等于启动一次传输并清除SPIF。

主机SCK频率选择是实际调试里很关键的一步。SCK = F_CPU / 分频系数,分频系数由SPR1:SPR0和SPI2X共同决定:

SPI2XSPR1:SPR0分频F_CPU=8MHz时的SCK
00042MHz
00116500kHz
01064125kHz
01112862.5kHz
10024MHz
10181MHz
11032250kHz
11164125kHz

我建议先用低速率通信,比如选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位自动清0,SPI主机功能失效。解决办法就是我在主机初始化里强调的:把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两个信号,多半就能找到答案。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询