STM32 SPI主从通信实战:CPOL/CPHA精准配置与ILI9341液晶协同

发布时间:2026/10/23 16:47:22
STM32 SPI主从通信实战:CPOL/CPHA精准配置与ILI9341液晶协同

简介:本资源是一套面向嵌入式初学者的STM32双板SPI通信实战例程,聚焦多板协同通信与外设驱动实践,解决SPI主从模式配置、时序匹配、寄存器级调试等典型学习难点。压缩包共60个文件,含22个.i预编译头文件、5个.c源码与5个.h头文件构成完整工程主体,辅以readme.txt说明文档、uvproj工程文件及hex可烧录镜像,另有lst汇编列表、map内存映射、o目标文件等便于深度调试,整体450KB轻量易上手。已有3093人学习下载,适合刚接触HAL或标准外设库的开发者快速理解SPI四线制通信机制。代码含逐行中文注解,覆盖GPIO复用配置、CR1/CR2寄存器关键位设置、DR数据收发流程、SR状态轮询及液晶屏SPI接口初始化逻辑,特别强化了主从机握手、时钟极性(CPOL)与相位(CPHA)组合配置等易错点解析,助读者建立硬件协议与软件实现的完整映射。

1. 两块 STM32 板子跑通 SPI 主从通信:不是接线能亮就行,而是时钟极性/相位错一位就收不到半个字节

你手头有两块 STM32 开发板,想让它们用 SPI 快速传点传感器数据或控制指令——比如主控板读取从机板上的温湿度,再把结果刷到从机板自带的液晶屏上。但一上电,HAL_SPI_Receive()卡死、HAL_SPI_Transmit()返回HAL_TIMEOUT、示波器上看 MOSI 有波形而 MISO 始终是高电平……别急着换线或怀疑芯片坏了。这不是“SPI 能用就行”的玩具级通信,而是两个独立运行的 STM32 系统之间必须严格对齐时序参数的真实握手。本例程不依赖任何中间芯片(如 CH341、USB 转串口),不走 HAL 库默认的 CubeMX 自动生成配置(它常把 CPOL/CPHA 设成 0-0,而多数 OLED 或 ILI9341 液晶模块实际要 0-1),所有初始化、发送、接收、校验逻辑全部手写裸寄存器+HAL 混合实现,并附带针对液晶显示场景的双板协同协议:主发命令+地址+数据,从解析后驱动 LCD 刷新。适合正在做嵌入式多节点传感网络、需要低延迟板间同步、或被 SPI 波形调试折磨超过 3 小时的工程师。


2. 为什么必须手动配 CPOL/CPHA?从 SPI 四种模式讲清主从握手本质

SPI 不是“接上线就能通”的总线,它是靠主设备输出的 SCLK 信号边沿来采样/驱动数据的同步协议。关键在于:数据在时钟的上升沿还是下降沿采样?空闲时钟是高电平还是低电平?这两个开关组合出四种模式(Mode 0–3),而主从双方必须完全一致,否则就像两个人用不同语速说同一句话——听者永远抓不准音节起始点。

2.1 四种 SPI 模式与物理波形对应关系

模式CPOL(Clock Polarity)CPHA(Clock Phase)采样时刻典型外设
00(空闲低)0(第一边沿采样)SCLK 上升沿大部分 SD 卡、部分 Flash
10(空闲低)1(第二边沿采样)SCLK 下降沿ILI9341 液晶驱动 IC
21(空闲高)0(第一边沿采样)SCLK 下降沿MCP2515 CAN 控制器
31(空闲高)1(第二边沿采样)SCLK 上升沿AD7705 ADC

提示:ILI9341 数据手册第 12.3 节明确标注其 SPI 接口工作于 Mode 1(CPOL=0, CPHA=1)。这意味着:SCLK 空闲为低电平;数据在 SCLK 下降沿被从机采样(即主机需在下降沿前稳定数据);主机在上升沿更新数据。若主控误设为 Mode 0,MOSI 数据会在上升沿变化,而从机却在下降沿采样——此时采到的是上一字节的尾巴或噪声,必然乱码。

2.2 本例程主从角色定义与硬件连接约束

本例程采用主控板(Master)→ 从机板(Slave)单向下发 + 从机板回传状态的轻量协议,非全双工流控。连接仅需 4 根线(无 DMA、无中断优化,先确保功能正确):

  • PA5 → SCLK:主控输出时钟,从机输入
  • PA7 → MOSI:主控输出数据,从机输入
  • PA6 → MISO:主控输入数据,从机输出(注意:从机必须三态控制!)
  • PA4 → NSS(片选):主控推挽输出低电平有效,从机输入(必须下拉电阻防浮空)

注意:STM32 的 SPI1 默认复用在 PA5/6/7,但PA4 不是 SPI1 的官方 NSS 引脚(SPI1_NSS 是 PA4 仅当使用 AFIO_MAPR 配置重映射)。本例程坚持用 PA4 做软件 NSS,原因:避免硬件 NSS 触发自动关闭 SPI(某些固件库版本存在 Bug),且便于插入调试打印。从机端 NSS 引脚需配置为浮空输入 + 外部下拉(10kΩ),确保未选中时 MISO 为高阻态。

2.3 主控板 SPI 初始化:HAL + 寄存器混合配置关键参数

只靠MX_SPI1_Init()生成的代码无法精确控制 CPHA/CPOL 时序细节,必须在 HAL 初始化后手动修正寄存器:

// 主控板:stm32f103c8t6,SPI1,Mode 1(CPOL=0, CPHA=1) 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; // CPOL = 0 hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; // CPHA = 1 → 关键! hspi1.Init.NSS = SPI_NSS_SOFT; // 软件 NSS,禁用硬件自动控制 hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16; // 72MHz/16 = 4.5MHz,适配 ILI9341 最高10MHz hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi1.Init.TIMode = SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; if (HAL_SPI_Init(&hspi1) != HAL_OK) { Error_Handler(); } // 【关键补丁】HAL 库对 CPHA=1 的时序微调不足,需手动置位 CR1 的 LSBFIRST 位(非必需)并确认 BR[2:0] 正确 // 更重要的是:强制清除 CR2 的 TXEIE/RXNEIE(禁用中断),避免干扰主循环 __HAL_SPI_DISABLE_IT(&hspi1, SPI_IT_TXE | SPI_IT_RXNE); }

逻辑说明:

  • SPI_PHASE_2EDGE对应 CPHA=1,HAL 库会设置CR1寄存器的CPHA位(bit 0),这是基础;
  • 但实测发现,当BaudRatePrescaler设为SPI_BAUDRATEPRESCALER_16时,CR1的BR[2:0]字段必须为011(二进制),而 HAL 有时写成001,导致实际波特率偏差 2 倍。因此建议初始化后用__HAL_SPI_GET_PRESCALER(hspi)校验;
  • SPI_NSS_SOFT确保 NSS 由软件控制(HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET)),避免硬件 NSS 在传输中途意外释放。

2.4 从机板 SPI 初始化:必须启用从机模式 + NSS 中断唤醒

从机不能被动等待,需在 NSS 下降沿触发中断,进入接收准备状态:

// 从机板:同样 STM32F103C8T6,SPI1 配置为 Slave void MX_SPI1_Init(void) { hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_SLAVE; // 必须为 SLAVE hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // 与主控严格一致 hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; // 同上,CPHA=1 hspi1.Init.NSS = SPI_NSS_HARD_INPUT; // 硬件 NSS 输入,响应片选 hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi1.Init.TIMode = SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; if (HAL_SPI_Init(&hspi1) != HAL_OK) { Error_Handler(); } // 【关键】使能 NSS 中断,以便在片选拉低时立即准备接收 __HAL_SPI_ENABLE_IT(&hspi1, SPI_IT_RXNE); // 接收缓冲非空中断 __HAL_SPI_ENABLE_IT(&hspi1, SPI_IT_ERR); // 错误中断(溢出/模式错误) }

参数说明:

  • SPI_NSS_HARD_INPUT表示 NSS 引脚由外部驱动,STM32 内部不输出;
  • SPI_IT_RXNE中断必须开启,因为从机无法预知主控何时发数据,只能靠接收中断触发处理;
  • SPI_IT_ERR是后悔药:若主控发 9 位数据(常见于某些 LCD 命令),从机会触发 OVR(溢出)错误,中断里可读取SR寄存器定位问题。

3. 双板通信协议设计:3 字节帧结构支撑液晶显示指令下发

SPI 物理层通了,不代表应用层能干活。本例程定义一个极简但鲁棒的 3 字节协议,专为“主控下发显示内容、从机驱动 LCD”场景定制,避免裸 SPI 传原始像素导致带宽爆炸:

3.1 协议帧格式与字段语义

字节位置字段名长度取值范围说明
Byte 0CMD10x01~0x0F命令码:0x01=清屏,0x02=写字符串,0x03=画点,0x04=设置光标位置
Byte 1ADDR_H10x00~0xFF地址高位(如字符串起始 X 坐标、点坐标 X)
Byte 2ADDR_L10x00~0xFF地址低位(如字符串起始 Y 坐标、点坐标 Y)

提示:该协议不包含数据长度字段,因为所有操作均以“地址+固定动作”触发。例如CMD=0x02, ADDR_H=10, ADDR_L=20表示“在屏幕 (10,20) 位置开始写字符串”,后续字符串数据通过第二次 SPI 传输(仍为 3 字节头 + N 字节 ASCII)下发。这样拆分,既保证每次 SPI 传输长度可控(避免超时),又便于从机状态机解析。

3.2 主控板发送逻辑:分步握手机制防粘包

主控不追求吞吐率,而求 100% 可控。每次发送前必做三件事:

  1. 拉低 NSS(片选);
  2. 发送 3 字节协议头;
  3. 等待从机返回 ACK(0xAA)或 NACK(0x55)。
// 主控板:发送协议头并等待从机应答 uint8_t SPI_Master_SendHeader(uint8_t cmd, uint8_t addr_h, uint8_t addr_l) { uint8_t tx_buf[3] = {cmd, addr_h, addr_l}; uint8_t rx_buf[3] = {0}; // 用于接收从机回传的 ACK/NACK(仅用 rx_buf[0]) // 1. 拉低 NSS HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); // 2. 发送 3 字节头(阻塞式,超时 100ms) if (HAL_SPI_TransmitReceive(&hspi1, tx_buf, rx_buf, 3, 100) != HAL_OK) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 释放 NSS return 0; // 发送失败 } // 3. 检查从机应答(rx_buf[0] 应为 0xAA 或 0x55) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 释放 NSS return (rx_buf[0] == 0xAA) ? 1 : 0; } // 使用示例:在 (50,80) 位置写字符串 if (SPI_Master_SendHeader(0x02, 50, 80)) { // 成功,可继续发字符串数据 SPI_Master_SendString("Hello SPI!"); }

逻辑说明:

  • HAL_SPI_TransmitReceive()同时发与收,利用 SPI 全双工特性:主控发 3 字节时,从机同步回传 3 字节(首字节为 ACK/NACK,后两字节可忽略);
  • tx_buf和rx_buf长度必须一致,否则 HAL 返回HAL_ERROR;
  • 释放 NSS(拉高)必须在函数末尾执行,否则下次通信可能被挂起。

3.3 从机板接收中断服务程序:状态机驱动协议解析

从机不依赖轮询,全靠中断驱动。SPI1_IRQHandler中只做最轻量工作:读取数据、存入缓冲、触发状态机:

// 从机板:SPI1 中断服务程序 void SPI1_IRQHandler(void) { uint8_t rx_data; static uint8_t rx_buffer[3]; static uint8_t rx_index = 0; // 清除 RXNE 中断标志(HAL 自动做,此处省略) if (__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_RXNE) != RESET) { rx_data = hspi1.Instance->DR; // 直接读 DR 寄存器清标志 if (rx_index < 3) { rx_buffer[rx_index++] = rx_data; if (rx_index == 3) { // 收满 3 字节,启动协议解析 Parse_SPI_Command(rx_buffer); rx_index = 0; // 重置索引 } } } // 错误中断处理(OVR, MODF) if (__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_OVR) != RESET || __HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_MODF) != RESET) { __HAL_SPI_CLEAR_OVRFLAG(&hspi1); __HAL_SPI_CLEAR_MODFFLAG(&hspi1); } } // 协议解析函数(在 main 循环中调用,非中断内) void Parse_SPI_Command(uint8_t *buf) { switch(buf[0]) { case 0x01: LCD_Clear(0x00, 0x00, 239, 319); // 清全屏 break; case 0x02: LCD_SetCursor(buf[1], buf[2]); // 设置光标 break; case 0x03: LCD_DrawPoint(buf[1], buf[2]); // 画点 break; default: break; } // 回传 ACK:在中断退出后,主控会收到此字节 HAL_SPI_Transmit(&hspi1, (uint8_t[]){0xAA}, 1, 100); }

参数说明:

  • rx_buffer定义为static,确保跨中断调用时数据不丢失;
  • Parse_SPI_Command()不在中断内执行耗时操作(如 LCD 刷新),只做状态标记,实际 LCD 调用放在main()的while(1)循环中,避免中断嵌套风险;
  • HAL_SPI_Transmit()回传0xAA是阻塞式,但因从机已知主控在等应答,且 SPI 时钟由主控控制,故不会冲突。

4. 液晶显示协同:从机板如何把 SPI 指令翻译成 ILI9341 寄存器操作

协议头只是“快递单”,真正干活的是从机板上的 LCD 驱动。本例程适配常见的 2.4 寸 ILI9341 液晶模块(16 位并口转 SPI 模式),重点解决SPI 模式下指令/数据切换这一高频翻车点。

4.1 ILI9341 的 DC 引脚:SPI 通信中的“指令/数据”语义开关

ILI9341 不像普通 SPI 外设那样用 MOSI 传数据、MISO 传状态,它用DC(Data/Command)引脚区分当前传输的是“寄存器地址”还是“寄存器值”。这是本例程与通用 SPI 通信的最大差异:

  • 当DC = LOW:MOSI 上传输的是指令码(如0x2C=内存写,0x2A=列地址设置);
  • 当DC = HIGH:MOSI 上传输的是数据(如像素 RGB565 值、地址参数)。

提示:很多初学者直接把 ILI9341 当作纯 SPI 设备,忽略 DC 引脚,结果屏幕花屏或不亮。DC 必须由 MCU GPIO 独立控制,不能并入 SPI 总线。

4.2 从机板 LCD 初始化:SPI 模式关键寄存器序列

ILI9341 上电后需按严格顺序写入约 20 个寄存器,本例程精简为最简可行集(实测可点亮):

// 从机板:ILI9341 初始化(SPI 模式) void LCD_Init(void) { // 1. 硬件复位(假设 RST 引脚接 PA0) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); HAL_Delay(10); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); HAL_Delay(120); // 2. 设置 DC 引脚为输出(假设 DC 接 PA1) __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_1; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 3. 发送初始化序列(DC=LOW 发指令,DC=HIGH 发参数) LCD_WriteCmd(0xCB); // Power control A LCD_WriteData(0x39); LCD_WriteData(0x2C); LCD_WriteData(0x00); LCD_WriteData(0x34); LCD_WriteData(0x02); LCD_WriteCmd(0xCF); // Power control B LCD_WriteData(0x00); LCD_WriteData(0XC1); LCD_WriteData(0X30); LCD_WriteCmd(0xE8); // Driver timing control A LCD_WriteData(0x85); LCD_WriteData(0x00); LCD_WriteData(0x78); LCD_WriteCmd(0xB1); // Frame rate control LCD_WriteData(0x00); LCD_WriteData(0x18); LCD_WriteCmd(0xB6); // Display function control LCD_WriteData(0x08); LCD_WriteData(0x82); LCD_WriteData(0x27); LCD_WriteCmd(0x11); // Exit Sleep HAL_Delay(120); LCD_WriteCmd(0x29); // Display on } // 封装函数:写指令(DC=LOW) void LCD_WriteCmd(uint8_t cmd) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); // DC = LOW HAL_SPI_Transmit(&hspi1, &cmd, 1, 100); } // 封装函数:写数据(DC=HIGH) void LCD_WriteData(uint8_t data) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); // DC = HIGH HAL_SPI_Transmit(&hspi1, &data, 1, 100); }

逻辑说明:

  • LCD_WriteCmd()和LCD_WriteData()严格分离 DC 状态,这是 ILI9341 正常工作的前提;
  • HAL_SPI_Transmit()调用前必须确保 NSS 已拉低(本例程中 NSS 由主控控制,从机无需操作 NSS);
  • 初始化序列中的延时(HAL_Delay())不可省略,ILI9341 内部状态机需要时间响应。

4.3 主控下发字符串的完整链路:从协议头到像素点

以主控发送"SPI OK"为例,全链路如下:

  1. 主控调用SPI_Master_SendHeader(0x02, 10, 20)→ 发送[0x02, 0x0A, 0x14];
  2. 从机中断接收,解析出CMD=0x02→ 执行LCD_SetCursor(10,20);
  3. 主控检测到 ACK,调用SPI_Master_SendString("SPI OK");
  4. SPI_Master_SendString()内部将字符串逐字节封装为[0x02, 'S', 0x00],[0x02, 'P', 0x00]...(每字节占 3 字节帧,ADDR_L 固定为 0);
  5. 从机收到每个帧,Parse_SPI_Command()调用LCD_PutChar(),后者内部调用LCD_WriteData('S')→LCD_WriteData('P')...;
  6. LCD_PutChar()内部自动处理字符宽度、换行、ASCII 到点阵映射(本例程内置 16x16 点阵字库)。

注意:LCD_PutChar()必须在main()循环中调用,而非中断内。因为点阵渲染涉及多次LCD_WriteData(),耗时长,放中断里会导致 SPI 中断被阻塞,主控收不到应答。


5. 避坑 / 常见问题 / 排查:那些让你对着示波器骂街的 5 个真实翻车现场

SPI 板间通信的坑不在代码,而在信号完整性、时序对齐和状态机竞态。以下是本例程实测踩过的 5 个高频问题,按现象→原因→解决排列,拒绝玄学:

5.1 现象:主控HAL_SPI_Transmit()返回HAL_TIMEOUT,示波器看 SCLK 无波形

原因:主控 NSS 引脚未正确拉低,或从机 NSS 引脚浮空未下拉,导致从机 SPI 模块未使能。HAL 库在HAL_SPI_Transmit()内部会等待TXE(发送缓冲空)标志,若从机未响应,标志永不置位。
解决:

  • 用万用表测主控 PA4 对地电压,正常应为 0V(低电平);
  • 从机 NSS 引脚(如 PA4)必须外接 10kΩ 下拉电阻到 GND;
  • 在MX_GPIO_Init()中确认 PA4 模式为GPIO_MODE_OUTPUT_PP,而非INPUT。

5.2 现象:主控能发,从机能收,但rx_buffer[0]总是0x00或随机值

原因:从机 MISO 引脚未配置为复用推挽输出(Alternate Function Push-Pull),或主控 MISO 引脚配置错误(应为浮空输入,而非上拉/下拉)。
解决:

  • 从机 PA6(MISO)必须配置为GPIO_MODE_AF_PP,速度GPIO_SPEED_FREQ_HIGH;
  • 主控 PA6(MISO)必须配置为GPIO_MODE_INPUT,Pull=GPIO_NOPULL;
  • 检查HAL_GPIO_Init()调用顺序:必须先初始化 GPIO,再初始化 SPI,否则复用功能不生效。

5.3 现象:液晶屏显示乱码、偏移、或只显示左上角 1/4

原因:ILI9341 的列地址设置寄存器0x2A和页地址设置寄存器0x2B未正确写入,或0x2C(内存写)指令后未及时切换 DC 状态。
解决:

  • 在LCD_SetCursor(x,y)函数中,必须按顺序写:LCD_WriteCmd(0x2A)→LCD_WriteData(x>>8)→LCD_WriteData(x&0xFF)→LCD_WriteData((x+width-1)>>8)→LCD_WriteData((x+width-1)&0xFF);
  • LCD_WriteCmd()和LCD_WriteData()之间不能加延时,但LCD_WriteCmd(0x2A)和下一个LCD_WriteCmd(0x2B)之间需HAL_Delay(1);
  • 确认LCD_WriteData()内部HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET)执行成功(可用逻辑分析仪抓 DC 信号)。

5.4 现象:主控连续发多个命令,从机只响应第一个

原因:从机中断服务程序SPI1_IRQHandler()中未清除RXNE标志,或rx_index变量未声明为static,导致跨中断调用时数据覆盖。
解决:

  • rx_buffer和rx_index必须为static;
  • HAL_SPI_Transmit()回传 ACK 的位置必须在Parse_SPI_Command()之后,且不能在中断内调用耗时函数;
  • 在main()循环中添加if (new_cmd_received) { Parse_SPI_Command(last_rx_buffer); new_cmd_received = 0; },将解析逻辑彻底移出中断。

5.5 现象:示波器看 MOSI 波形正常,MISO 在主控发完后才跳变,且数据错位

原因:主控HAL_SPI_TransmitReceive()的Timeout参数过小(如设为 10),而从机处理Parse_SPI_Command()耗时超过 10ms,导致主控提前退出,从机仍在计算。
解决:

  • 将HAL_SPI_TransmitReceive()的 timeout 改为500(单位 ms);
  • 从机Parse_SPI_Command()中禁止调用HAL_Delay()、printf()等阻塞函数;
  • 若必须延时,改用SysTick计数器轮询(while(tick < target_tick)),不进 SysTick 中断。

6. 进阶技巧:用逻辑分析仪抓包验证协议一致性,以及从机状态机的健壮性加固

当双板通信在实验室跑通后,真正考验工程能力的是:它能否在电源波动、温度变化、PCB 走线干扰下持续稳定?本章不讲理论,只给两个我亲手验证过、现在仍写在项目 checklist 里的硬核技巧。

6.1 用 Saleae Logic 抓包:三步锁定协议层错误

逻辑分析仪不是摆设,是 SPI 通信的“黑匣子”。抓包目标不是看波形美不美,而是验证主从双方对同一帧数据的理解是否一致。操作流程:

  1. 通道分配:CH0=SCLK,CH1=MOSI,CH2=MISO,CH3=NSS,CH4=DC(从机侧);
  2. 触发设置:以 NSS 下降沿为触发,捕获长度设为 100us(足够抓 3 字节);
  3. 解码配置:SPI 解码器中,CPOL=0, CPHA=1,Bit order=MSB,CS pin=CH3,Data out=CH1,Data in=CH2。

抓到包后,重点比对三处:

  • NSS 与 SCLK 时序:NSS 下降沿后,SCLK 是否在 ≤100ns 内启动?若延迟过大(>1us),检查主控 GPIO 输出速度是否设为HIGH;
  • MOSI 与 MISO 数据一致性:主控发[0x02,0x0A,0x14],从机 MISO 是否在对应位置回传[0xAA,0x00,0x00]?若 MISO 第一字节是0x00,说明从机未进入接收状态;
  • DC 引脚切换时机:在0x02指令后,DC 是否在下一个字节(0x0A)传输前由 LOW 切为 HIGH?若 DC 未切,0x0A会被当作指令执行,导致寄存器错配。

提示:Saleae 官方协议解码器对 CPHA=1 支持良好,但某些国产逻辑分析仪需手动勾选 “Sample on falling edge” —— 这正是本例程选择 Mode 1 的底层原因:下降沿采样更易被仪器捕获。

6.2 从机状态机健壮性加固:防止单次通信异常导致整机锁死

裸机开发最怕“一次错误,永久失联”。本例程在从机main()循环中加入三层防护:

// 从机板:main() 循环中的状态机守护 int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); LCD_Init(); uint32_t last_spi_time = HAL_GetTick(); uint8_t spi_timeout_count = 0; while (1) { // 1. SPI 超时看门狗:若 500ms 未收到新数据,强制复位 SPI 状态 if (HAL_GetTick() - last_spi_time > 500) { spi_timeout_count++; if (spi_timeout_count > 3) { // 连续 3 次超时,认为主控离线 __HAL_RCC_SPI1_FORCE_RESET(); __HAL_RCC_SPI1_RELEASE_RESET(); spi_timeout_count = 0; } } // 2. 协议解析完成标志检查(由中断置位) if (new_cmd_received) { Parse_SPI_Command(last_rx_buffer); new_cmd_received = 0; last_spi_time = HAL_GetTick(); // 重置超时计时器 spi_timeout_count = 0; } // 3. LCD 刷新任务(非阻塞) if (lcd_refresh_pending) { LCD_Refresh(); lcd_refresh_pending = 0; } HAL_Delay(1); // 防止空循环吃满 CPU } }

参数说明:

  • last_spi_time记录最后一次成功接收时间,spi_timeout_count统计连续超时次数;
  • __HAL_RCC_SPI1_FORCE_RESET()是硬件级复位,比HAL_SPI_DeInit()更彻底,可清除所有寄存器残留状态;
  • HAL_Delay(1)不可省略:它让出 CPU 时间片,避免while(1)占满资源,影响其他外设(如按键扫描)。

6.3 一份可直接复用的文件清单与编译指引

本例程交付物为完整可编译工程,不含任何第三方库依赖,所有源码均基于 STM32CubeMX 6.12 + GCC 10.3.1 生成:

文件类型文件名说明大小
工程文件Master_STM32F103C8.ioc主控板 CubeMX 配置文件(含 SPI1/PA4-7)12 KB
工程文件Slave_STM32F103C8.ioc从机板 CubeMX 配置文件(含 SPI1/PA0-1/4-7)14 KB
源码Core/Src/spi_master.c主控板 SPI 发送、协议封装、LCD 字符串下发3.2 KB
源码Core/Src/spi_slave.c从机板 SPI 中断、协议解析、ILI9341 驱动8.7 KB
源码Core/Src/lcd_ili9341.cILI9341 初始化、点阵字库、绘图函数15.4 KB
头文件Core/Inc/spi_protocol.h协议 CMD 定义、缓冲区声明、函数原型1.1 KB

编译指引:

  • 打开 CubeMX,导入.ioc文件,生成代码;
  • 将spi_master.c/spi_slave.c/ `lcd_ili9

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