
简介STM8 SPI从机通讯程序是一份基于STM8系列微控制器的SPI从机通信实现面向嵌入式开发者和学习STM8外设编程的工程师解决STM8作为从设备与SPI主控之间高速同步数据传输的问题。压缩包共88个文件大小262KB以c/h源文件和头文件为主另含o目标文件、ls列表、lkf链接文件及txt说明文档便于查看代码结构并直接导入开发环境编译。目前已有411人浏览学习。程序覆盖SPI寄存器初始化、时钟极性和相位CPOL/CPHA配置、SS从选信号监测、中断服务程序编写以及通过SPI_DR寄存器收发数据等完整流程并带工程文件与编译调试产物可帮助读者掌握STM8 SPI从机模式下主从同步、数据接收和错误处理等核心细节适合快速移植到传感器、显示屏、A/D转换器等外设通信场景。 最近在做一套多板卡协同的数据采集设备主控板用STM32底下挂了三个采集小板小板用的是STM8S103主控和小板之间用SPI通信STM8全程只扮演从机角色。这个活儿做完之后我最大的感受是很多人写SPI都是从主机角度写从机程序虽然代码量不大但有一个算一个的坑全部踩在时序细节和寄存器配合上。这篇文章就把STM8 SPI从机通讯程序从方案设计、寄存器配置、中断处理到联调排错完整复盘一遍给正在做类似主从板卡通信的朋友一个可以直接抄的参考。项目本身不复杂但涉及到的知识点比较细SPI协议的主从机制、STM8的SPI外设寄存器、从机模式下数据收发的本质、与STM32主机联调时的波形核对以及最常见的几个通信故障。我会把代码和排查思路全放出来适合三类人看一是刚接触STM8、想搞懂从机模式怎么配置的初学者二是正在做多板卡SPI通信但被时序问题卡住的项目开发者三是想拿STM8做传感器采集节点、通过SPI上报给主控的嵌入式爱好者。1. 为什么选STM8当SPI从机方案选型与整体设计1.1 场景需求与从机角色定位先交代一下项目背景。整套设备有一个主控板和一个采集板采集板上带了几路模拟量传感器和一路PWM输出需要实时响应主控的命令。选型的时候考虑过两个方案一是采集板上单独放一颗带ADC的MCU通过SPI和主控通信二是直接用SPI扩展芯片加模拟开关但灵活性太差后面想加协议逻辑就很痛苦。最后选了STM8S103作为采集板的核心理由很直接价格便宜、单片机资源够用、ADC和PWM都有、IAR和STVD开发环境成熟而且SPI从机模式是硬件外设实现的不占CPU太多资源。STM8在这个架构里的角色非常清晰它不主动发起任何传输只等主控通过SPI总线发来命令然后解析命令、执行采集、把数据填回SPI数据寄存器让主控在下一个时钟周期把数据读走。整个交互过程是典型的“一问一答”主机发命令字节从机回数据字节全双工同时完成。1.2 SPI总线的天然主从特性与从机约束SPI和UART、I2C最大的不同在于SPI没有独立的时钟同步机制时钟线SCK完全由主机控制。这意味着从机永远不能主动说话哪怕从机有数据要上报也只能等着主机把SCK拉起来再借主机的时钟把数据一位一位移出去。这个特性决定了从机程序的整体设计思路——所有发送操作本质上是“预写数据等待被读取”而不是“主动发送”。从机程序里有一个很容易忽略的点SPI是全双工主机每发出一个时钟脉冲从机同时移出一位数据所以从机在接收主机数据的同时也在向主机发送数据。很多人写从机代码时只关注了接收忘了在接收之前把回复数据预先写入数据寄存器结果主机读回来的永远是垃圾数据或者全0xFF。这个问题我在调试时踩过一次后面会专门讲。2. STM8 SPI从机核心机制寄存器、引脚与时序细节2.1 STM8 SPI外设概述与引脚映射STM8S系列的SPI是一个完整的外设模块支持主机模式和从机模式数据位可配时钟极性/相位可配。以常见的STM8S103F3P6为例SPI引脚在PC口上引脚功能从机模式下方向PC3NSS从机选择输入PC5SCK串行时钟输入PC6MISO主入从出输出PC7MOSI主出从入输入注意一个细节从机模式下MISO是输出由从机驱动MOSI和SCK是输入由主机驱动。如果不把MISO配置为推挽输出从机就拉不动这根线主机端会读到杂乱的电平。很多初学者在这里翻车引脚模式配错SPI配置看起来全对但通信就是不通。2.2 SPI_CR1/CR2关键位配置从机模式、软硬件片选、极性与相位STM8的SPI配置集中在SPI_CR1和SPI_CR2两个寄存器。CR1里最关键的几个位MSTR主从选择、CPOL时钟极性、CPHA时钟相位、SPESPI使能、BR[2:0]波特率分频。从机模式下MSTR必须为0BR位实际上不影响从机的通信速率因为SCK是外部输入的。CPOL和CPHA决定了SPI的四种模式必须和主机完全一致否则数据会整体错位SPI模式CPOLCPHA数据采样时机模式000第一个边沿上升沿采样模式101第二个边沿下降沿采样模式210第一个边沿下降沿采样模式311第二个边沿上升沿采样CR2里最重要的是SSM和SSI两位也就是软件NSS管理。SPI的片选信号有两种管理方式硬件NSS和软件NSS。硬件NSS模式下从机的NSS引脚必须被主机拉低才会参与通信一旦NSS悬空或者被拉高从机对SCK上的时钟脉冲直接无视。软件NSS模式下NSS引脚的功能被旁路只要SPE置1从机就一直处于选中状态。三线SPI场景只有SCK、MOSI、MISO三根线必须用软件NSS否则从机根本不会工作。2.3 从机模式下数据收发的本质STM8的SPI数据收发共用一个数据寄存器SPI_DR往SPI_DR里写数据就是发送从SPI_DR里读数据就是接收。但寄存器的物理结构是双缓冲的发送缓冲区和接收缓冲区独立存在只是共用一个地址。读操作返回接收缓冲区的内容写操作写入发送缓冲区。从机模式下写入SPI_DR的数据并不会立刻出现在MISO引脚上而是要等主机产生SCK时钟数据才随着时钟一位一位移出去。如果从机在主机开始传输之前没有把数据预写到DR里主机读到的就是上一次传输遗留的数据或者默认电平通常是0xFF。反过来如果从机在主机传输过程中才写DR前面几个位可能已经移出去了结果就是数据错位。3. 从零手写STM8 SPI从机通讯程序3.1 初始化代码时钟、引脚、寄存器先给一份完整的从机初始化代码基于寄存器操作环境是IAR for STM8主频16MHz。不同的STM8型号寄存器布局基本一致但有些位的位置可能有差异建议以你的具体型号参考手册为准。// STM8S103F3P6 SPI从机初始化 // SPI模式0CPOL0, CPHA0软件NSS管理波特率分频不关心 void SPI_Slave_Init(void) { // 1. 使能SPI外设时钟 // STM8S的SPI时钟使能位在CLK_PCKENR1中不同型号位号可能不同 CLK_PCKENR1 | 0x04; // 2. 配置引脚模式 // PC6(MISO)推挽输出PC5(SCK)/PC7(MOSI)/PC3(NSS)带上拉输入 PC_DDR | 0x40; // PC6 设为输出 PC_CR1 | 0x40; // PC6 推挽输出 PC_DDR ~0xA8; // PC3、PC5、PC7 设为输入 PC_CR1 | 0xA8; // PC3、PC5、PC7 上拉输入 // 3. 配置SPI控制寄存器 // 从机模式MSTR0 // 模式0CPOL0, CPHA0 // BR位不关心留默认值 SPI_CR1 0x00; // 全部清零默认从机、模式0 // 4. 软件NSS管理SSM1SSI1 // 从机模式下SSI必须为1否则SPI被置为禁止状态 SPI_CR2 | 0x03; // 5. 使能SPI外设 SPI_CR1 | 0x40; // SPE1 }初始化顺序有讲究先开时钟再配引脚再配SPI控制寄存器最后使能SPE。如果先使能SPE再改CR1/CR2的配置可能会导致外设处于不稳定状态建议按这个顺序来。3.2 查询方式收发查询方式代码最简单适合对实时性要求不高、数据量小的场景。核心思想就是轮询SPI_SR寄存器里的RXNE和TXE标志位。// SPI状态寄存器标志位 #define SPI_SR_RXNE 0x01 #define SPI_SR_TXE 0x02 // 从机读取一个字节主机发起传输后此函数才会返回 uint8_t SPI_Slave_ReadByte(void) { while (!(SPI_SR SPI_SR_RXNE)); // 等待接收缓冲区非空 return SPI_DR; // 读取数据硬件自动清除RXNE } // 从机预写一个字节等主机SCK移出 void SPI_Slave_WriteByte(uint8_t data) { SPI_DR data; // 写入发送缓冲区 while (!(SPI_SR SPI_SR_TXE)); // 等待发送缓冲区空 }这里有一个非常重要的执行顺序问题。如果你要在一个通信周期内回复主机必须先写SPI_DR再等待RXNE。比如主机发来0x01读采集数据命令从机要把0x2A回给主机正确顺序是void SPI_Slave_Run(void) { uint8_t cmd; SPI_DR 0xAA; // 先预写一个默认回复值 while (!(SPI_SR SPI_SR_RXNE)); // 等待命令到达 cmd SPI_DR; // 读取主机命令 if (cmd 0x01) { SPI_DR sensor_value; // 预写下一次要回复的数据 } }如果顺序颠倒先等RXNE再写SPI_DR那么主机在这一次传输中读到的数据就是旧数据只有下一次传输才能读到正确值这在协议上会造成一个字节的延迟。3.3 中断方式收发查询方式有一个问题如果主机连续高速发送数据从机在主循环里轮询可能会来不及读取导致接收溢出OVR错误。更稳妥的做法是用中断接收每条数据到达自动进中断处理。STM8的SPI有独立的中断源RXNE和TXE共用一个中断向量需要在中断服务函数里区分。// SPI接收缓冲区和索引 #define RX_BUF_SIZE 32 uint8_t g_spiRxBuf[RX_BUF_SIZE]; volatile uint8_t g_spiRxIdx 0; volatile uint8_t g_spiReplyData 0x00; // SPI中断服务函数IAR环境具体中断向量号以芯片头文件为准 #pragma vector0x14 __interrupt void SPI_IRQHandler(void) { uint8_t sr SPI_SR; // 接收完成 if (sr SPI_SR_RXNE) { uint8_t rxData SPI_DR; // 读取数据自动清除RXNE if (g_spiRxIdx RX_BUF_SIZE) { g_spiRxBuf[g_spiRxIdx] rxData; } // 根据收到的命令预写回复数据 if (rxData 0x01) { g_spiReplyData sensor_value; } SPI_DR g_spiReplyData; } // 错误标志处理溢出等 if (sr 0x80) { SPI_SR 0x00; // 清除错误标志具体方式以手册为准 } } // 中断初始化 void SPI_Slave_Init_IT(void) { SPI_Slave_Init(); SPI_ICR | 0x02; // 使能RXIEN接收中断Bit1 __enable_interrupt(); }注意这里只开启了RXNE中断没有开TXE中断。虽然SPI是全双工但每次主机发来一个字节从机也同时发一个字节所以在RXNE中断里预写回复数据就够了。如果你把TXE中断也打开发送缓冲区一空就会触发中断而且没数据可写时还会反复进入很容易把程序卡死在中断里。这个坑我劝你别踩除非你的协议里有明确的“多字节连续发送”需求。4. 主机侧配合与联调实录以STM32为例4.1 主机配置要点模式、速率、片选从机写好了主机这边也得配合好。我用的是STM32F103作为主机HAL库配置如下SPI_HandleTypeDef hspi1; void MX_SPI1_Init(void) { hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; // CPOL0 hspi1.Init.CLKPhase SPI_PHASE_1EDGE; // CPHA0 hspi1.Init.NSS SPI_NSS_SOFT; // 软件片选 hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_32; // 4.5MHz72MHz hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi1); // CS片选引脚配置为普通GPIO手动拉低/拉高 GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_4; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // CS默认高电平 }主机发送和接收用HAL_SPI_TransmitReceive这个函数同时完成发送和接收正好对应SPI全双工的特性。片选操作顺序是拉低CS - 传输 - 拉高CS。注意CS拉低后建议留一点延时至少几个微秒让从机有时间准备。4.2 联调时抓到的波形分析与时序核对联调时最有力的工具是逻辑分析仪我直接把CS、SCK、MOSI、MISO四根线全部抓出来看波形非常直观。正常的一次字节传输时序大致是这样的CS |‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|______ SCK |__|‾|__|‾|__|‾|__|‾|__|‾|__|‾|__|‾|__|‾|__|‾|__|‾|__|‾|__ MOSI |D7|D6|D5|D4|D3|D2|D1|D0| 主机发出 MISO |D7|D6|D5|D4|D3|D2|D1|D0| 从机回复MISO在SCK下降沿前稳定我调试时遇到一个很典型的问题从机回复的数据在逻辑分析仪上看起来是有的但STM32读回来的数组里全是0xFF。后来用逻辑分析仪逐位对比才发现从机MISO的数据变化发生在SCK的上升沿而主机在上升沿采样正好采到数据线上还没稳定的跳变。原因就是我把CPHA配成了1和从机的模式0不匹配。把主机改成CPHA0之后数据完全正常。所以联调时如果数据不对第一件事就是核对两边的CPOL和CPHA不要先怀疑代码逻辑。速率方面STM32的SPI1挂在APB2总线上72MHz主频分频32得到2.25MHz这个速率对STM8从机来说完全没问题。如果通信距离超过10厘米建议把分频调大到64或128降低速率换取稳定性。5. 常见问题与排查技巧实录5.1 常见问题速查表把我在实际调试中遇到的问题和排查思路整理成一个速查表方便定位现象可能原因排查与解决主机读回全是0xFF从机没预写回复数据MISO引脚没配置成推挽输出在接收前先写SPI_DR检查PC6输出配置数据错位、乱码CPOL/CPHA主从不一致逻辑分析仪抓波形核对采样沿连续传输偶发错误从机接收溢出OVR改用中断接收及时读取SPI_DRCS拉低后第一个字节丢失从机还没准备好主机就开始传输主机拉低CS后延时几个微秒再发数据从机死机或卡死NSS引脚意外拉低导致模式错误检查NSS配置使用软件NSSSSM1,SSI1通信距离稍长就不稳定SPI不适合远距离传输降低速率缩短连线用屏蔽线或加缓冲器5.2 从机发不出数据/主读全FF的处理这个现象出现频率最高原因有两个层面。第一层是配置问题从机的MISO引脚如果没有配置为推挽输出引脚处于高阻态主机端读到的是上拉电阻拉高的电平也就是全10xFF。第二层是时序问题从机在主机传输期间才写SPI_DR这时候前面的位已经移出去了主机读到的数据一半是旧数据一半是新数据表现为乱码。所以从机必须在CS拉低后的第一个SCK上升沿之前就把回复数据预写到DR里。如果做不到可以在主机侧加一个“先发一个同步字节”的机制从机收到同步字节后才准备正式回复数据。5.3 三线SPI场景与软件NSS有些项目为了省一根线只接SCK、MOSI、MISO三根线不接CS。这种场景下从机的NSS引脚悬空如果还是用硬件NSS模式SSM0NSS引脚电平不确定从机可能永远处于未选中状态完全不响应主机。解决办法就是把CR2的SSM和SSI都置1启用软件NSS管理让SPI外设忽略NSS引脚只要SPE使能就认为一直选中。这样即使NSS引脚悬空通信也能正常进行。5.4 SPI通信距离与布线注意事项SPI的通信距离非常有限实测下来板级通信同一块PCB或板间飞线在10厘米以内比较可靠超过15厘米就开始出现偶发错误。如果要从机板放到较远的位置有几种改善手段降低SCK速率到1MHz以下、使用双绞线或者屏蔽线、在接收端加施密特触发器整形。但说实话SPI天生就不是为远距离设计的如果设备间距超过30厘米建议换RS485或者CAN总线别硬扛。6. 扩展方向与个人经验如果做完基础的从机收发还想继续深入有几个方向可以参考。一个是给从机加上在线升级功能STM8支持通过SWIM接口在线编程也可以用自举程序实现应用层升级但要注意SPI通信和Flash擦写不能同时进行需要设计好时序另一个是配合上位机让STM32主机把从机数据通过串口转发给电脑再用C#或者Python写个上位机显示曲线这样就形成了“传感器从机-主控-上位机”的完整数据链路。还有多个从机并联的场景每个从机分配一个独立的CS引脚或者用菊花链把多个从机串起来后者对SPI协议的理解要求更高。最后分享一个我实际调试中得到的经验SPI从机程序看起来简单但每一个环节都有坑从引脚模式、寄存器配置、预写数据的时机到主机那边的极性和速率任何一环对不上表现出来的都是数据错误。调试SPI一定要用逻辑分析仪看波形不要只盯着代码猜。先把波形抓稳确认SCK、MOSI、MISO每一位都对得上再回头优化代码逻辑效率会高很多。另外从机的回复数据一定要“预写”不要“现写”这可能是SPI从机通信最容易忽略、也最容易出问题的细节。本文还有配套的精品资源点击获取