☰
STM8 SPI从机通讯全解析:协议、寄存器、中断与排错实战
2026/10/17 6:38:09 网站建设 项目流程

简介:基于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 模式(SSM=0)下,外部 NSS 引脚直接控制从机选中状态。NSS 拉低,从机被选中,MISO 正常输出;NSS 拉高,从机释放 MISO,进入高阻态。这种模式非常适合多从机挂在同一根总线上的场景,每个从机靠各自的 NSS 区分,互不干扰。STM8 的硬件 NSS 引脚通常会对应到一个普通 IO,需要在 GPIO 初始化时把这个引脚设为输入带上拉。

软件 NSS 模式(SSM=1)下,外部 NSS 引脚功能失效,从机是否被选中完全由内部的 SSI 位决定。SSI=1 时从机始终处于选中状态,SSI=0 时从机被禁止。这种模式适合点对点通信,省一条片选线,也省去主机端控制 NSS 的麻烦。网上经常有人问“三线 SPI 怎么接”,其实就是把 NSS 去掉,主机和从机都跑软件 NSS 模式,强行用软件把片选固定住。

我个人的建议是:如果板子上只有一台从机,软件 NSS 最省事,只要保证主机和从机的 CPOL/CPHA 一致,数据就能通。如果总线上可能挂多个从机,或者未来要扩展设备,老老实实走硬件 NSS。还有一点,硬件 NSS 高电平期间从机的 MISO 是高阻态,这条特性在排查“总线回读异常”时特别有用——如果从机没被选中但 MISO 还在输出,主机读到的数据就可能是多设备冲突的结果。

2.3 从机模式的波特率与时序参数

前面说了从机忽略 BR 位,但这不是说波特率就可以完全不管。主机端 SPI 时钟频率不能超过从机允许的上限,否则从机跟不上采样节奏,数据必然出错。STM8S003 在 16MHz 主频下,SPI 时钟最高可以到 8MHz(fMASTER/2),这是理论上限;实际项目里我通常建议主机降一档,跑到 4MHz 左右,毕竟 SPI 从机还要处理中断和协议,太高的时钟会挤压 CPU 时间。

数据位长度也是对齐项。STM8 的 SPI 默认 8 位数据帧,主机一定要也配成 8 位,否则双方对数据帧的边界理解不一致,从机收到的会是一堆拼错的字节。网上有些 16 位帧场景,比如音频采样,也能工作,但两边必须同时配置 FRF 相关逻辑,复杂度直接上一个台阶。

时序上还有一个小细节:主机在 NSS 拉低之后,最好等一个极短的延时再开始发 SCK,让从机有反应时间。真机联调时,很多“首字节丢失”问题就是主机在片选拉低的瞬间立刻打时钟,从机还没来得及准备 MISO 数据,导致第一个字节丢位。解决方案很简单,主机侧在 NSS 拉低后加几微秒延时,或者从机侧提前把第一个应答字节写入 DR 寄存器,都能缓解。

3. 核心代码实现与逐段解析

3.1 寄存器级初始化流程

开发环境我用的是 IAR for STM8,芯片型号 STM8S003F3,SPI 外设是 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; // CPOL=0, CPHA=1,即 SPI Mode 1;MSTR=0 从机模式;MSB First SPI_CR1 = 0x02; // SSM=1, SSI=1,软件 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 侧发送 0x55,STM8 从机收到后原样返回;然后 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 是否真的使能了,SPE=1 且 RXIE=1,缺一个都不行。接着看片选状态,硬件 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 从机框架基本都不用动。这就是前期把时序和状态管理做干净的好处,后面全是省事。

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

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

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

立即咨询