
简介这份资源提供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 是 bit3CPHA 是 bit2。常见做法是主机设 CPOL0、CPHA0也就是时钟空闲低电平、上升沿采样。从机必须设成完全一样的值否则第一个 bit 就丢了。我一般会先把两端都设成 Mode 0CPOL0CPHA0这是最通用的模式绝大多数 SPI 外设都支持。如果你接的是某个特定型号的传感器或 Flash 芯片再去查它的手册确认模式。2.2 主机 atmega8535 的初始化代码与参数逐行说明// atmega8535 主机 SPI 初始化 // 假设 F_CPU 8000000ULSPI 时钟 F_CPU / 16 500kHz void spi_master_init(void) { // PB4SS, PB5MOSI, PB6MISO, PB7SCK 设为输出 // 注意主机模式下 SS 必须设为输出否则硬件可能自动切换到从机模式 DDRB | (1 PB4) | (1 PB5) | (1 PB7); // MISO (PB6) 保持输入 DDRB ~(1 PB6); // 使能 SPI主机模式时钟极性 0相位 0时钟分频 16 // SPIE0 关闭中断轮询方式SPE1 使能DORD0 高位先发 // MSTR1 主机模式CPOL0CPHA0SPR1:SPR001 分频 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 配合 SPI2X0 得到 16 分频。如果你需要更快的速率可以把 SPR0 清零、SPI2X 置 1得到 2 分频但要注意从机的响应速度。第三片选信号我用 PB4 手动控制每次传输前拉低、传输后拉高这样从机才能正确复位它的移位计数器。2.3 从机 atmega8 的初始化代码与必须注意的 SS 引脚行为// atmega8 从机 SPI 初始化 void spi_slave_init(void) { // PB4SS, PB5MOSI, PB6MISO, PB7SCK // 从机模式下 MOSI、SCK、SS 为输入MISO 为输出 DDRB | (1 PB6); // MISO 输出 DDRB ~((1 PB4) | (1 PB5) | (1 PB7)); // 其余输入 // 使能 SPI从机模式MSTR0CPOL0CPHA0 // 从机的时钟分频位无效但建议清零 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 0CPOL0CPHA0另一个是 Mode 1CPOL0CPHA1采样边沿就差半个时钟周期数据会整体偏移一位。另一个可能是 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_SPCRCPOL、CPHA、DORD 三个位天然一致不会因为手误写错。这个习惯帮我省掉了至少一半的调试时间。SPI 通信本身不复杂复杂的是两端配置的对称性把对称性用代码约束住问题就少了一大半。希望帮到你。本文还有配套的精品资源点击获取