news 2026/11/6 11:40:58

双MCU SPI主从通信实战:atmega8535与atmega8寄存器配置与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
双MCU SPI主从通信实战:atmega8535与atmega8寄存器配置与避坑指南

简介:这份资源提供ATmega8535作为SPI主机、ATmega8作为从机的完整通信程序,面向正在学习AVR单片机与同步串行总线的嵌入式开发者及电子类专业学生。内容围绕SPI四线制展开,涵盖SCK时钟、MOSI与MISO数据线以及SS片选信号的配置,并涉及主机时钟频率、CPOL与CPHA数据帧格式设定、从机响应时序和中断处理等关键环节,可帮助读者理解主从机初始化、数据收发与多从机选择的实现思路。压缩包共111个文件,约99KB,以c与h源码、asm汇编、hex烧录文件、cof与map编译输出、prj工程文件及doc说明为主,兼顾源码阅读与直接烧录验证。目前已有234人学习下载,适合作为SPI通信入门与主从机联调的参考范例。

1. 双 MCU 的 SPI 主从通信:为什么你的第一版程序总是收不到数据

两块单片机之间用 SPI 传数据,是嵌入式开发里最经典也最容易翻车的场景之一。atmega8535 做主机、atmega8 做从机,这个组合在早期的工业控制板、数据采集模块里非常常见——主机负责采集和决策,从机负责驱动外设或做前端预处理。但很多人第一次写这个程序时,主机发出去的数据从机收不到,或者从机回传的数据主机读出来全是 0xFF,调了半天以为是硬件坏了,其实问题往往出在 SPI 的时序配置和片选逻辑上。

这个方案适合谁?如果你手上有两块 AVR 单片机,需要通过短距离高速链路交换数据,又不想用 UART 那种异步方式(波特率误差、帧格式协商麻烦),SPI 是首选。它全双工、速率高、协议简单,但前提是你得把主从两端的寄存器配置对齐。下面我从寄存器层面把这件事拆开讲,每一步都给可复现的代码和参数说明。

2. SPI 主从模式的核心寄存器配置:把时钟极性和相位对齐

2.1 为什么 SPCR 的 CPOL 和 CPHA 是第一个要锁死的参数

SPI 通信的本质是一个移位寄存器环:主机的 8 位寄存器和从机的 8 位寄存器通过 MOSI 和 MISO 连成一个 16 位环路,每来一个时钟脉冲,数据就移一位。问题在于,时钟空闲时是高电平还是低电平(CPOL),数据在时钟的哪个边沿采样(CPHA),这两个参数如果主从两端不一致,收到的数据就会整体错位。

atmega8535 和 atmega8 的 SPI 寄存器结构基本一致,都有 SPCR(控制寄存器)、SPSR(状态寄存器)和 SPDR(数据寄存器)。SPCR 里的 CPOL 是 bit3,CPHA 是 bit2。常见做法是主机设 CPOL=0、CPHA=0,也就是时钟空闲低电平、上升沿采样。从机必须设成完全一样的值,否则第一个 bit 就丢了。

我一般会先把两端都设成 Mode 0(CPOL=0,CPHA=0),这是最通用的模式,绝大多数 SPI 外设都支持。如果你接的是某个特定型号的传感器或 Flash 芯片,再去查它的手册确认模式。

2.2 主机 atmega8535 的初始化代码与参数逐行说明

// atmega8535 主机 SPI 初始化 // 假设 F_CPU = 8000000UL,SPI 时钟 = F_CPU / 16 = 500kHz void spi_master_init(void) { // PB4=SS, PB5=MOSI, PB6=MISO, PB7=SCK 设为输出 // 注意:主机模式下 SS 必须设为输出,否则硬件可能自动切换到从机模式 DDRB |= (1 << PB4) | (1 << PB5) | (1 << PB7); // MISO (PB6) 保持输入 DDRB &= ~(1 << PB6); // 使能 SPI,主机模式,时钟极性 0,相位 0,时钟分频 16 // SPIE=0 关闭中断(轮询方式),SPE=1 使能,DORD=0 高位先发 // MSTR=1 主机模式,CPOL=0,CPHA=0,SPR1:SPR0=01 分频 16 SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR0); // SPSR 的 SPI2X 保持 0,实际分频 = 16 SPSR &= ~(1 << SPI2X); // 主机拉低 SS 作为片选(如果从机 SS 接在 PB4 上) PORTB &= ~(1 << PB4); }

这段代码的关键点有三个。第一,DDRB 的方向设置:主机模式下 SS 必须配置为输出,哪怕你不用它做片选,也要么拉低要么拉高,不能悬空。atmega8535 的硬件在 SS 被外部拉低时会自动把 MSTR 清零,切到从机模式,这是很多人调试时发现“主机突然不发了”的元凶。第二,SPCR 的赋值我用了或运算,SPR0 置 1 配合 SPI2X=0 得到 16 分频。如果你需要更快的速率,可以把 SPR0 清零、SPI2X 置 1,得到 2 分频,但要注意从机的响应速度。第三,片选信号我用 PB4 手动控制,每次传输前拉低、传输后拉高,这样从机才能正确复位它的移位计数器。

2.3 从机 atmega8 的初始化代码与必须注意的 SS 引脚行为

// atmega8 从机 SPI 初始化 void spi_slave_init(void) { // PB4=SS, PB5=MOSI, PB6=MISO, PB7=SCK // 从机模式下 MOSI、SCK、SS 为输入,MISO 为输出 DDRB |= (1 << PB6); // MISO 输出 DDRB &= ~((1 << PB4) | (1 << PB5) | (1 << PB7)); // 其余输入 // 使能 SPI,从机模式(MSTR=0),CPOL=0,CPHA=0 // 从机的时钟分频位无效,但建议清零 SPCR = (1 << SPE); SPSR &= ~(1 << SPI2X); // 确保 SS 引脚有上拉,防止悬空误触发 PORTB |= (1 << PB4); }

从机这边最容易踩的坑是 SS 引脚的处理。atmega8 的 SPI 硬件要求 SS 在每次字节传输前被拉低、传输后拉高,否则移位寄存器不会加载新数据。如果你把 SS 直接接地,从机确实能收到第一个字节,但后续字节会错位——因为硬件需要 SS 的上升沿来复位接收逻辑。我一般会在从机端给 SS 加上拉电阻,主机端用普通 IO 控制片选,每次传输前拉低、传输后拉高,中间保持至少一个时钟周期的间隔。

另外,从机的 SPCR 里 MSTR 位必须为 0,否则它会试图当主机,和 atmega8535 抢时钟线,结果就是两边都收不到正确数据。这个错误在示波器上看很明显:SCK 线上会出现两个不同频率的时钟叠加。

3. 主机与从机的收发程序:轮询方式下的完整数据交换流程

3.1 主机发送一个字节并读取从机回传的字节

// 主机发送一个字节,同时返回从机回传的字节 uint8_t spi_master_transfer(uint8_t data) { // 拉低片选,选中从机 PORTB &= ~(1 << PB4); // 写入数据到 SPDR,启动传输 SPDR = data; // 等待传输完成(SPIF 置位) // 注意:SPIF 在 SPSR 中,读 SPSR 再读 SPDR 才能清除 while (!(SPSR & (1 << SPIF))) ; // 读取接收到的数据 uint8_t received = SPDR; // 拉高片选,结束本次传输 PORTB |= (1 << PB4); return received; }

这个函数是主机侧的核心。SPDR 写入后,硬件自动开始产生 8 个时钟脉冲,MOSI 上逐位输出数据,同时 MISO 上逐位采样从机回传的数据。SPIF 标志位置位表示一个字节传输完毕。注意读 SPDR 之前必须先读 SPSR,这是 AVR 的硬件要求,否则 SPIF 不会被清除,下一次循环会立刻通过。

片选的控制时机很关键。我在 SPDR 赋值之前拉低 SS,在读取完 SPDR 之后拉高。如果你在 SPDR 赋值之后才拉低 SS,从机可能已经错过了第一个时钟沿。如果你在 SPIF 置位之前就拉高 SS,从机的移位寄存器可能还没完成加载。

3.2 从机的中断方式接收与回传准备

// 从机 SPI 接收中断服务程序 volatile uint8_t slave_rx_data = 0; volatile uint8_t slave_tx_data = 0x55; // 默认回传值 volatile uint8_t slave_rx_flag = 0; ISR(SPI_STC_vect) { // 读取主机发来的数据 slave_rx_data = SPDR; slave_rx_flag = 1; // 准备下一次要回传的数据 // 注意:必须在本次中断内写入 SPDR,否则下次传输会回传旧值 SPDR = slave_tx_data; } void spi_slave_init_interrupt(void) { DDRB |= (1 << PB6); DDRB &= ~((1 << PB4) | (1 << PB5) | (1 << PB7)); PORTB |= (1 << PB4); SPCR = (1 << SPE) | (1 << SPIE); // 使能 SPI 和 SPI 中断 SPSR &= ~(1 << SPI2X); // 预加载第一个回传字节 SPDR = slave_tx_data; sei(); // 开启全局中断 }

从机用中断方式比轮询更可靠,因为从机无法预知主机什么时候发起传输。SPI_STC_vect 在每次字节传输完成后触发。这里有一个血泪经验:从机必须在中断里尽快把下一个要发送的字节写入 SPDR。如果你等到主程序里再写,主机下一次传输时从机回传的就是上一次的旧数据,表现为“数据延迟一拍”。我在中断里直接赋值,保证时序。

另外,从机初始化时预加载 SPDR 是必须的。第一个字节传输时,从机的移位寄存器里必须已经有数据,否则 MISO 上输出的是未定义值。我一般预加载 0x55 或 0xAA 作为握手标志,主机收到这个值就知道从机已经就绪。

3.3 一次完整的主从数据交换示例

// 主机侧:发送命令 0xA1,读取从机状态 void master_exchange_demo(void) { uint8_t status; // 发送命令字节,同时读取从机回传的第一个字节(可能是预加载的握手值) status = spi_master_transfer(0xA1); // 如果从机回传的是 0x55,说明从机在线 if (status == 0x55) { // 再发一个 dummy 字节,读取从机对 0xA1 的响应 uint8_t response = spi_master_transfer(0x00); // response 里就是从机对 0xA1 的处理结果 } }

这个交换流程体现了 SPI 全双工的特点:主机每发一个字节,必然收到一个字节。从机的响应会延迟一个字节周期,因为从机在第一个字节传输中断里才能读到命令,然后准备响应数据,主机需要再发一个字节才能把响应读回来。这个“延迟一拍”是 SPI 从机通信的固有特性,不是 bug。如果你需要从机立即响应,可以把命令和响应放在同一个字节里,比如高 4 位是命令、低 4 位是响应,但这会限制数据宽度。

4. 避坑与排查:主从通信失败的 5 个典型现象

4.1 主机收到的全是 0xFF 或 0x00

现象:主机发送数据后,spi_master_transfer 返回的 received 始终是 0xFF 或 0x00,从机那边看起来毫无反应。

原因:最常见的是 MISO 线没接好,或者从机的 MISO 引脚没有配置为输出。atmega8 的 MISO 是 PB6,如果 DDRB 的 PB6 没置 1,从机就无法驱动 MISO 线,主机读到的就是浮空电平,通常表现为 0xFF 或 0x00。另一个可能是从机的 SS 引脚一直为高,SPI 硬件处于复位状态,移位寄存器不工作。

解决:用万用表或示波器确认 MISO 线在主从两端都有信号。检查从机 DDRB 的 PB6 是否为输出。确认主机在传输期间把从机 SS 拉低,传输结束后拉高。如果从机 SS 直接接地,改成接主机的一个 IO 口,由主机控制片选。

4.2 数据整体错位,收到的字节是发送字节的移位版本

现象:主机发 0x01,从机收到 0x02 或 0x80;主机发 0x80,从机收到 0x01。数据看起来像是被左移或右移了一位。

原因:CPOL 或 CPHA 不匹配。主机和从机如果一个是 Mode 0(CPOL=0,CPHA=0),另一个是 Mode 1(CPOL=0,CPHA=1),采样边沿就差半个时钟周期,数据会整体偏移一位。另一个可能是 DORD 位不一致,一个高位先发,一个低位先发。

解决:把主从两端的 SPCR 配置打印出来对比,确保 CPOL、CPHA、DORD 三个位完全一致。我一般会在初始化代码里把 SPCR 的完整值写成宏,两端引用同一个宏定义,避免手误。

4.3 第一个字节正确,后续字节全部错乱

现象:主机发第一个字节时从机收到正确数据,第二个字节开始从机收到的就是垃圾值。

原因:从机的 SS 引脚没有在每次传输后拉高。atmega8 的 SPI 硬件需要 SS 的上升沿来复位接收逻辑,如果 SS 一直保持低电平,从机的移位计数器不会复位,第二个字节会从第一个字节结束的位置继续移位,导致错位。另一个可能是从机中断里没有及时写入下一个回传字节,主机读到的 MISO 数据是旧值。

解决:主机端确保每次 spi_master_transfer 调用时,SS 在传输前拉低、传输后拉高,中间保持至少一个时钟周期的间隔。从机端在 SPI_STC_vect 中断里第一时间写入 SPDR,不要在主循环里写。

4.4 通信速率一高就出错,降低分频后正常

现象:SPI 时钟分频设为 2 或 4 时,数据偶尔出错;设为 16 或 32 时稳定。

原因:从机的处理速度跟不上。atmega8 在 8MHz 晶振下,SPI 从机模式最高支持 F_CPU/4 的时钟,但那是理想值。实际使用中,如果从机中断里有其他高优先级中断,或者从机主频较低(比如用内部 1MHz RC 振荡器),SPI 时钟太快会导致从机来不及在下一个时钟沿之前把数据写入 SPDR。

解决:先降低 SPI 时钟到 F_CPU/16 或 F_CPU/32,确认通信稳定后再逐步提高。如果必须高速通信,从机端用 DMA 或硬件 SPI 的预加载机制,避免在中断里做耗时操作。另外检查从机的熔丝位,确认时钟源和主频设置正确。

4.5 主机突然停止发送,MSTR 位被硬件清零

现象:程序运行一段时间后,主机不再产生 SCK 时钟,SPCR 的 MSTR 位读出来是 0。

原因:atmega8535 的 SS 引脚被外部拉低时,硬件会自动清除 MSTR 位,把主机切换到从机模式。如果你的 SS 引脚配置为输入且没有上拉,或者被其他电路拉低,就会触发这个行为。另一个可能是电源波动导致 SPI 寄存器复位。

解决:主机模式下 SS 引脚必须配置为输出,或者至少使能内部上拉。如果你不需要硬件片选,把 SS 设为输出并拉高,用其他 IO 口做片选。检查电源是否稳定,SPI 通信时电流突变可能导致电压跌落,在 VCC 和 GND 之间加 100nF 去耦电容。

5. 进阶技巧:用示波器验证时序与提升通信可靠性的三个习惯

5.1 用示波器抓 SCK 和 MOSI 的对应关系

如果你有示波器,把探头接在 SCK 和 MOSI 上,触发方式设为 SCK 上升沿。你应该能看到:SCK 空闲时为低电平,第一个上升沿到来时 MOSI 上的数据已经稳定。如果 MOSI 的数据变化和 SCK 的上升沿同时发生,说明 CPHA 设错了,数据在采样边沿才变化,从机会采到不确定值。正确的时序是数据在 SCK 的下降沿变化、上升沿采样(Mode 0)。把示波器的时间轴展开到 1us/div,能清楚看到 8 个时钟脉冲和对应的数据位。

5.2 在 SS 和 SCK 之间加一个小延时

很多人在主机端拉低 SS 后立刻写 SPDR,从机可能还没检测到 SS 变低。我一般会在拉低 SS 后加一个_delay_us(1),给从机一点反应时间。同样,在 SPIF 置位后、拉高 SS 之前,也加一个_delay_us(1),确保从机完成移位寄存器的加载。这个延时在低速通信时无所谓,但在高速通信时能显著降低误码率。注意不要用_delay_ms,那太长了,会拖慢整体吞吐。

5.3 用固定帧头和校验字节过滤错误数据

SPI 本身没有协议层,没有地址、没有校验。我习惯在数据帧里加一个固定帧头(比如 0xAA)和一个校验字节(比如前几个字节的异或和)。主机发送时按帧头发送,从机收到帧头后才开始解析后续数据。如果从机收到的第一个字节不是帧头,就丢弃并等待下一次传输。这个习惯在电磁干扰较强的工业现场特别有用,能过滤掉大部分偶发误码。

// 带帧头和校验的发送示例 void spi_send_frame(uint8_t cmd, uint8_t data) { uint8_t checksum = cmd ^ data; spi_master_transfer(0xAA); // 帧头 spi_master_transfer(cmd); // 命令 spi_master_transfer(data); // 数据 spi_master_transfer(checksum); // 校验 }

从机端在中断里维护一个状态机:收到 0xAA 后进入接收状态,依次收命令、数据、校验,校验通过后才把数据交给主程序处理。这个状态机用 switch-case 实现,每个状态对应一个字节的接收,逻辑清晰,不容易出错。

5.4 主从两端共用同一个头文件定义寄存器配置

最后一个习惯:把 SPI 的配置参数写在一个公共头文件里,主从两端都 include 这个文件。比如:

// spi_config.h #define SPI_CPOL 0 #define SPI_CPHA 0 #define SPI_DORD 0 #define SPI_SPR 0x01 // 分频 16 #define SPI_MASTER_SPCR ((SPI_CPOL << CPOL) | (SPI_CPHA << CPHA) | \ (SPI_DORD << DORD) | (SPI_SPR << SPR0) | \ (1 << SPE) | (1 << MSTR)) #define SPI_SLAVE_SPCR ((SPI_CPOL << CPOL) | (SPI_CPHA << CPHA) | \ (SPI_DORD << DORD) | (1 << SPE))

这样主机用 SPI_MASTER_SPCR,从机用 SPI_SLAVE_SPCR,CPOL、CPHA、DORD 三个位天然一致,不会因为手误写错。这个习惯帮我省掉了至少一半的调试时间。SPI 通信本身不复杂,复杂的是两端配置的对称性,把对称性用代码约束住,问题就少了一大半。

希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 20:16:48

MATLAB害虫检测识别系统:从HSV预处理到轻量网络训练全流程

简介&#xff1a;本资源是一套面向高校学生与科研初学者的MATLAB植物虫害检测识别系统&#xff0c;可用于毕业设计、课程设计或图像处理入门实践。系统基于颜色特征分析&#xff0c;将植物叶片划分为正常、轻度虫害、中度虫害与严重虫害四个等级&#xff0c;通过训练各等级叶片…

作者头像 李华
网站建设 2026/10/18 16:27:48

新能源出力场景生成与削减:时序相关性建模与工程实践

做新能源出力场景的人都知道一句话&#xff1a;场景好不好&#xff0c;不只看分布像不像&#xff0c;还要看曲线“像不像真实发生的事”。我之前在处理某个光伏基地的出力场景生成任务时&#xff0c;第一版直接用蒙特卡洛对每个时段的分布独立抽样&#xff0c;生成了1000条日曲…

作者头像 李华
网站建设 2026/10/23 15:18:30

Qt QWidget手搓小鸟飞行动画:重力、碰撞与帧动画实战

简介&#xff1a;本资源是一个基于Qt5框架实现的轻量级2D游戏——“飞行的小鸟”&#xff0c;专为Qt图形编程初学者设计&#xff0c;帮助理解QGraphicsView架构下的场景管理、动画控制与碰撞检测等核心机制。项目完整呈现了QGraphicsScene与QGraphicsItem协同工作的典型范式&am…

作者头像 李华
网站建设 2026/10/31 3:09:25

Spring Boot 3.x连接Redis失败排错指南:从Unable to connect到ACL权限

如果你也在某个Spring Boot 3.x项目里看到过这么一行日志&#xff0c;而且网上搜到的配置教程试了一圈还是连不上&#xff0c;那这篇文章应该能帮到你。先说背景&#xff1a;我这边一个内部系统最近从Spring Boot 2.7升级到3.2&#xff0c;启动看着一切正常&#xff0c;结果第一…

作者头像 李华
网站建设 2026/11/6 10:56:13

C8051F340 USB编程实例:HID/CDC设备固件开发与常见坑解决

简介&#xff1a;这是一份基于Silicon Labs C8051F340微控制器的USB编程实例&#xff0c;以工程化代码详细解析USB设备开发方法&#xff0c;面向具备一定单片机基础、希望掌握USB通信的嵌入式工程师与学习者&#xff0c;适合数据采集、嵌入式设备与PC互联等场景。实例从USB协议…

作者头像 李华