
简介基于STM8系列微控制器的SPI从机通讯程序面向嵌入式开发者和电子爱好者解决STM8与主控设备之间高速、低引脚数同步通信问题适合需要学习SPI协议、外设驱动及中断开发的初中级工程师。RAR压缩包共88个文件大小仅262KB包含31个.h头文件、28个.c源文件、链接脚本、调试映射、烧录镜像等工程文件这些文件共同构成一套可编译验证的完整开发工程。目前已有411人浏览学习。程序实现覆盖SPI从机模式的关键环节SPI_CR1/CR2寄存器参数配置、SS信号激活与监控、中断服务程序响应、SPI_DR数据收发、主从时钟同步匹配以及溢出错误处理。通过阅读源码与调试文件可掌握STM8 SPI模块从机模式的完整配置流程理解主从设备数据交换机制并参考其中断与错误处理的设计思路快速迁移到温度传感器、显示屏或ADC采集等实际项目中。 做嵌入式这几年SPI 从机通讯的需求其实比想象中多。前阵子一个项目里主控板用 STM32但要额外接几路传感器采集和 IO 控制单独为了这点功能升级主控板又不划算我就在板子上加了一颗 STM8S003 当协处理器。STM32 通过 SPI 总线访问它读写寄存器、取数据、下控制命令整个链路跑通之后调试效率提升得非常明显。这篇就聊一下 STM8 做 SPI 从机时从协议要点、寄存器配置、中断收发到排错经验的完整思路给同样要写 STM8 SPI 从机通讯程序的朋友一个可以直接“抄作业”的参考。SPI 总线在嵌入式系统里的地位不用多说四线搞定全双工同步通信速度快、时序简单特别适合做主控和协处理器之间的高速数据交换。但越是看起来简单的协议做从机时越容易踩坑不是收不到数据就是字节错位甚至时序稍有问题就整包乱掉。这篇内容我会尽量把每一个坑都摊开讲清楚。1. 项目概述与方案选型1.1 这个程序解决什么问题SPI 从机程序的核心任务很明确让 STM8 在 SPI 总线上配合主机工作主机发来命令从机解析并应答必要时返回数据状态。典型场景有两种。一种是主控板加协处理器。主控 MCU 资源紧张或引脚不够时挂一颗 STM8 专门做 IO 扩展、PWM 输出、ADC 采集甚至低功耗唤醒管理等杂活。主控通过 SPI 直接读写这颗 STM8 的寄存器不需要额外跑一套复杂协议整个系统结构被拆得很干净。另一种是数据采集节点。STM8 作为从机挂在总线上定时采集传感器数据主控按需读取。和 I2C 相比SPI 没有 7 位寻址限制不需要 ACK速度上限高得多关键是全双工主控发指令的同时就能拿到从机返回的数据一个周期完成一次握手非常适合高频轮询场景。顺带说一句SPI 这条总线不仅可以连协处理器也可以挂 SPI Flash、LCD 屏、SD 卡这类常用外设。从机程序写到后来基本能迁移到这些设备上因为底层数据收发、片选管理、中断处理这些知识是通用的。1.2 为什么选 STM8 而不是其他 MCU选 STM8 做从机最直接的原因是成本低、货源稳。STM8S003 这类芯片在消费电子和工控领域出货量巨大几块钱一颗资源也完全够用。做协处理器时对主频和算力要求本来就不高STM8 的 16MHz 主频配合硬件 SPI 外设跑点到点通信毫无压力。另外STM8 的历史代码积累丰富很多老工程师手里都有现成的寄存器级操作模板。它的标准外设库虽然也能用但实际调试中发现寄存器直接操作反而更好分析问题因为库函数封装了一层SPI 时序又那么敏感单步调试时根本看不清到底哪一步出了问题。这个选择在我项目里是经过实测的后面代码也全部走寄存器路线。还有一个次要原因STM8 从机方案可以把主控的无谓开销降到最低。主机只需要操作 SPI 外设不需要维护复杂的状态机整个系统稳定性更容易保证。读热词里看到很多人也会在“STM32 三线 SPI”“C# 上位机”这类场景里遇到类似问题本质上都是从机这端没把时序理顺才导致上位机和主控层反复排查无效。2. SPI 从机通讯核心机制解析2.1 从 STM8 角度看 SPI 协议要点SPI 是同步串行协议对外四根线SCK时钟、MOSI主出从入、MISO主入从出、NSS片选。主机的移位寄存器发出时钟带动整个链路一个时钟沿发送一位另一个时钟沿采样一位。之所以强调“从机角度”是因为从机没法控制时钟它只能被动跟随主机的节奏。STM8 的 SPI 外设在从机模式下MSTR 位必须为 0。此时 BR[2:0] 波特率配置位会被忽略因为时钟全部由主机提供这是新手最容易误解的地方——从机代码里拼命调 BR 位调了半天根本不生效属实白费功夫。真正需要对齐的是 SCK 空闲电平CPOL和数据采样沿CPHA这四个组合也就是 SPI 的 mode 0 到 mode 3。SPI ModeCPOLCPHA特征Mode 000SCK 空闲低电平上升沿采样Mode 101SCK 空闲低电平下降沿采样Mode 210SCK 空闲高电平下降沿采样Mode 311SCK 空闲高电平上升沿采样主机和从机的 Mode 必须完全一致否则数据会整体偏移一位甚至错乱。实际调试时最有效的办法是抓逻辑分析仪看 SCK 与 MOSI 信号之间的相对时序然后回头核对 CPOL/CPHA 寄存器配置基本两三分钟就能定位。还要注意 LSBFIRST 位。默认是 MSB First也就是先发高位如果主机端配置成了 LSB First但从机还是默认值那收到的字节就是比特序完全颠倒的。这种问题最阴险因为波形看起来一切正常只是数据值不对。2.2 硬件 NSS 与软件 NSS 的选择片选信号在从机通讯里几乎是灵魂。STM8 的 SPI 外设支持两种 NSS 管理方式硬件 NSS 和软件 NSS。硬件 NSS 模式SSM0下外部 NSS 引脚直接控制从机选中状态。NSS 拉低从机被选中MISO 正常输出NSS 拉高从机释放 MISO进入高阻态。这种模式非常适合多从机挂在同一根总线上的场景每个从机靠各自的 NSS 区分互不干扰。STM8 的硬件 NSS 引脚通常会对应到一个普通 IO需要在 GPIO 初始化时把这个引脚设为输入带上拉。软件 NSS 模式SSM1下外部 NSS 引脚功能失效从机是否被选中完全由内部的 SSI 位决定。SSI1 时从机始终处于选中状态SSI0 时从机被禁止。这种模式适合点对点通信省一条片选线也省去主机端控制 NSS 的麻烦。网上经常有人问“三线 SPI 怎么接”其实就是把 NSS 去掉主机和从机都跑软件 NSS 模式强行用软件把片选固定住。我个人的建议是如果板子上只有一台从机软件 NSS 最省事只要保证主机和从机的 CPOL/CPHA 一致数据就能通。如果总线上可能挂多个从机或者未来要扩展设备老老实实走硬件 NSS。还有一点硬件 NSS 高电平期间从机的 MISO 是高阻态这条特性在排查“总线回读异常”时特别有用——如果从机没被选中但 MISO 还在输出主机读到的数据就可能是多设备冲突的结果。2.3 从机模式的波特率与时序参数前面说了从机忽略 BR 位但这不是说波特率就可以完全不管。主机端 SPI 时钟频率不能超过从机允许的上限否则从机跟不上采样节奏数据必然出错。STM8S003 在 16MHz 主频下SPI 时钟最高可以到 8MHzfMASTER/2这是理论上限实际项目里我通常建议主机降一档跑到 4MHz 左右毕竟 SPI 从机还要处理中断和协议太高的时钟会挤压 CPU 时间。数据位长度也是对齐项。STM8 的 SPI 默认 8 位数据帧主机一定要也配成 8 位否则双方对数据帧的边界理解不一致从机收到的会是一堆拼错的字节。网上有些 16 位帧场景比如音频采样也能工作但两边必须同时配置 FRF 相关逻辑复杂度直接上一个台阶。时序上还有一个小细节主机在 NSS 拉低之后最好等一个极短的延时再开始发 SCK让从机有反应时间。真机联调时很多“首字节丢失”问题就是主机在片选拉低的瞬间立刻打时钟从机还没来得及准备 MISO 数据导致第一个字节丢位。解决方案很简单主机侧在 NSS 拉低后加几微秒延时或者从机侧提前把第一个应答字节写入 DR 寄存器都能缓解。3. 核心代码实现与逐段解析3.1 寄存器级初始化流程开发环境我用的是 IAR for STM8芯片型号 STM8S003F3SPI 外设是 SPI1引脚分配为PA5-SCK、PA6-MISO、PA7-MOSI、PA3-NSS。先把 GPIO 模式配好SCK、MOSI、NSS 是输入MISO 是推挽输出。// PA5-SCK 输入、PA7-MOSI 输入、PA3-NSS 输入 PA_DDR ~(0x20 | 0x80 | 0x08); // 带上拉输入避免浮空误触发 PA_CR1 | (0x20 | 0x80 | 0x08); PA_CR2 ~(0x20 | 0x80 | 0x08); // PA6-MISO 设为推挽输出 PA_DDR | 0x40; PA_CR1 | 0x40; PA_CR2 ~0x40;GPIO 配完之后再配 SPI。这里用软件 NSS 模式演示因为单从机场景最容易跑通也省一条线。void SPI_Slave_Init(void) { // 关闭 SPI 再配置 SPI_CR1 0x00; // CPOL0, CPHA1即 SPI Mode 1MSTR0 从机模式MSB First SPI_CR1 0x02; // SSM1, SSI1软件 NSS 且始终选中 SPI_CR2 0x03; // 清状态寄存器残留标志 (void)SPI_SR; // 使能 SPI SPI_CR1 | 0x40; // 使能 RXNE 接收中断 SPI_CR3 | 0x02; }有一个关键顺序必须先配好 CR1 和 CR2最后再置位 SPE。如果先使能 SPI 再改模式位部分寄存器状态会产生不确定行为实测中偶尔会导致第一个字节收不准。另外使能 SPI 之前先读一次 SR是为了清掉芯片上电时可能残留的 OVR/MODF 标志避免进入中断服务程序后误处理。3.2 中断收发与溢出保护从机不能用轮询收发。主机随时可能发起通信如果从机代码正在跑别的任务轮询模式下数据就会直接被下一字节覆盖从而触发溢出错误 OVR。所以从机必须开中断最好把 SPI 中断优先级放到比较靠前的位置。STM8 的中断向量号越小优先级越高SPI 中断向量是 10在常用的定时器、UART 之前够用。#define RX_BUF_SIZE 64 volatile uint8_t rx_buf[RX_BUF_SIZE]; volatile uint8_t rx_head 0; volatile uint8_t tx_buf[RX_BUF_SIZE]; volatile uint8_t tx_len 0; volatile uint8_t tx_pos 0; INTERRUPT_HANDLER(SPI_IRQHandler, 10) { uint8_t sr SPI_SR; if (sr 0x01) // RXNE接收数据非空 { uint8_t r SPI_DR; if (rx_head RX_BUF_SIZE) { rx_buf[rx_head] r; } // 有没有要回的数据有就提前写入 DR if (tx_pos tx_len) { SPI_DR tx_buf[tx_pos]; } } if (sr 0x08) // OVR溢出错误 { (void)SPI_DR; (void)SPI_SR; } }SPI 是全双工协议主机发一个字节的同时从机也必须在 MISO 上移出一个字节。所以从机程序一个最常见的游戏规则是接收中断 RXNE 触发时要立刻判断下一轮要不要回数据如果需要回就把应答字节写进 DR这样等到主机发下一个字节时从机已经准备好了要发送的内容。很多从机通讯的“数据迟一拍”问题就是因为应答数据写入太晚主机读到的永远是上一个字节甚至空数据。还有一点要注意读取 SPI_DR 时RXNE 标志会被自动清除但如果从机是等到溢出错误发生了才去读 DR旧数据其实已经被丢掉了。所以从机代码里 OVR 处理不是“恢复数据”而是“清错误标志防止状态机卡住”。读 SR再读 DR这个顺序是固定的不能反。3.3 与 STM32 主机的联调实例纸上谈兵没意思我直接给一个真实跑通的例子。STM32 侧发送 0x55STM8 从机收到后原样返回然后 STM32 发 0xAA从机同样回 0xAA。这个测试虽然简单但足以验证 SCK、MOSI、MISO 三条线全部工作正常NSS 管理也没问题。STM32 侧用 HAL 库配置比较简单SPI_HandleTypeDef hspi1; uint8_t tx_data 0x55; uint8_t rx_data 0; // 片选拉低选中 STM8 HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_RESET); // 发送并接收一个字节 HAL_SPI_TransmitReceive(hspi1, tx_data, rx_data, 1, 100); // 片选拉高释放总线 HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_SET);STM8 从机侧主循环里做协议处理void main(void) { SPI_Slave_Init(); enableInterrupts(); while (1) { if (rx_head 1) { uint8_t cmd rx_buf[0]; // 简单回显测试 tx_buf[0] cmd; tx_len 1; tx_pos 0; // 提前写入第一个应答字节 SPI_DR tx_buf[0]; tx_pos 1; rx_head 0; } } }这里有个细节主循环里写第一个应答字节时可能中断里 TXE 还没置位直接写 DR 会覆盖当前正在发送的字节。所以更稳妥的做法是在中断中判断 tx_pos 与 tx_len如果还有数要发就写主循环里只在无数据时预填充第一字节。我上面的简化代码在低速测试下没问题但正式项目里建议做双缓冲甚至环形缓冲避免主循环和中断同时抢 DR 导致的数据竞争。跑通回显之后就可以扩展成正式协议了比如主机发“命令 寄存器地址 数据长度”从机按约定格式返回“状态 数据 校验”。我在实际项目里就是这么做的STM8 内置一小段命令解析程序跟上位机或主控配合非常顺。4. 从机通讯常见问题排查4.1 收不到数据或者首字节丢失这个问题在论坛里被问烂了但成因往往就那么几种。先确认主机和从机的 SPI Mode 是否一致这是第一个排查点。再查从机 SPI 是否真的使能了SPE1 且 RXIE1缺一个都不行。接着看片选状态硬件 NSS 模式下 NSS 引脚必须确认被拉低而不是悬空否则从机根本没进选中状态。首字节丢失还有一个很容易被忽略的原因从机 DR 里残留了上次的数据。当 SPI 从机被主机选中后主机打第一个时钟从机移位寄存器会把 DR 中的旧值移出去如果这个旧值是垃圾数据主机读到的自然就是垃圾。解决方法是每次通信开始前主机在 NSS 拉低后先发一个哑字节释放从机的残留数据或者从机在协议层每次 NSS 下降沿对内部缓冲和 DR 做复位。如果是软件 NSS 模式检查 SSM 和 SSI 两个位是否都置 1。只置 SSM 忘记置 SSI从机等于被内部禁用了这时候数据是绝对收不到的。4.2 数据错位、随机错误字节数据错位的典型场景是裸眼瞅波形看不出问题但收到的字节序列整体错位。大概率是 CPHA 设置反了导致数据在错误的时钟沿被采集。还有一种混合情况是主机用了 LSB First从机默认 MSB First收到的字节 bit 顺序全反。这种问题只能靠协议层加魔数头来快速感知一旦收到预期魔数说明对齐正常。随机错误字节往往和电气质量有关。SPI 通信距离拉长后信号反射、地线噪声都会影响时序。SPI 本来就不是为长距离设计的超过 20cm 就可能出问题。我建议只要板间有排线就把时钟降到 1MHz 以下MISO/MOSI 串接 33Ω 电阻SCK 上再加小电容滤波。这些操作都是低成本但对稳定性提升明显。另外如果主机在 NSS 拉高后立刻又拉低发起下一帧从机可能还没从上一帧的状态里恢复。两个帧之间留一点空闲时间主机代码里在两次传输之间加一个短延时一劳永逸。4.3 实时性优化半双工、DMA 与多从机扩展有些场景从机并不需要回数据比如只采集传感器上报给主机。这时可以配置 RXONLY 位让 SPI 工作在单工接收模式不用处理 TXE代码简化一大截也不容易产生空应答误覆盖。这个思路对应 STM8 外设支持的单工模式省掉 MISO 线协议也更简单。如果用的是 STM8L 或 STM8AF 这类支持 DMA 的型号从机接收可以配合 DMA把 SPI 接收直接搬到内存缓冲区CPU 负载几乎为零。STM8S003 没有 DMA用中断方案足够了但中断服务程序内一定不要做复杂的协议解析只做数据搬运解析交给主循环。多从机扩展时可以并列挂载每个从机独立 NSS也可以菊花链。菊花链模式下数据从一个从机的 MISO 串到下一个从机的 MOSI所有从机共用时钟协议复杂度会明显增加适合需要节省片选 IO 的场景。但菊花链的时序约束更严我不建议新手入门就从菊花链开始先跑通单从机再扩展会安全得多。5. 实操心得与扩展建议最后再分享一个我自己的调试方法STM8 做从机时如果信号层面没问题但数据一直不对我会在从机主循环里把 rx_buf 的内容通过 UART 打出来用串口接到电脑上位机观察。这样省去反复连调试器的麻烦也能快速确认是通信链路错了还是协议解析错了。这个做法在多人协作时尤其好用上位机同学直接分析串口日志比在一起抢开发板效率高多了。还有一个容易忽视的点是 STM8 的 SWIM 调试接口。SWIM 在 PA1和 SPI1 的 PA3/PA5/PA6/PA7 不冲突但如果你的板子把 SWIM 占用了比如复用成了普通 IO在线调试就可能没法正常工作。研发阶段尽量保留 SWIM 接口哪怕只是留几个测试点真出问题的时候就知道多重要了。SPI 从机通讯程序写完以后扩展空间其实很大。可以用它做传感器网关主控定期来取数也可以把它接在用户交互面板后面按键和 LED 状态全部通过 SPI 寄存器暴露给主机甚至可以把 STM8 作为一个低功耗待机模块主机需要时通过 SPI 唤醒它平时它睡在那里不耗电。每次遇到新需求只要把协议层改一改底层这套 SPI 从机框架基本都不用动。这就是前期把时序和状态管理做干净的好处后面全是省事。本文还有配套的精品资源点击获取