文章目录
-
- 一、前言
-
- [1.1 技术背景](#1.1 技术背景)
- [1.2 本文目标与读者收获](#1.2 本文目标与读者收获)
- [1.3 系统架构总览](#1.3 系统架构总览)
- 二、环境准备
-
- [2.1 硬件清单](#2.1 硬件清单)
- [2.2 软件环境](#2.2 软件环境)
- [2.3 硬件连接](#2.3 硬件连接)
-
- [I2C 方案接线](#I2C 方案接线)
- [SPI 方案接线](#SPI 方案接线)
- [三、I2C 与 SPI 协议对比分析](#三、I2C 与 SPI 协议对比分析)
- 四、自定义通信协议设计
-
- [4.1 协议帧格式](#4.1 协议帧格式)
- [4.2 命令码定义](#4.2 命令码定义)
- [4.3 协议层实现](#4.3 协议层实现)
- [4.4 通信时序流程](#4.4 通信时序流程)
- [五、I2C 通信实现](#五、I2C 通信实现)
-
- [5.1 I2C 驱动层 --- 主机端](#5.1 I2C 驱动层 — 主机端)
- [5.2 I2C 驱动层 --- 从机端(中断方式)](#5.2 I2C 驱动层 — 从机端(中断方式))
- [六、SPI 通信实现](#六、SPI 通信实现)
-
- [6.1 SPI 驱动层 --- 主机端](#6.1 SPI 驱动层 — 主机端)
- [6.2 SPI 驱动层 --- 从机端(中断方式)](#6.2 SPI 驱动层 — 从机端(中断方式))
- [七、应用层实现 --- 数据同步与控制](#七、应用层实现 — 数据同步与控制)
-
- [7.1 主机端主程序](#7.1 主机端主程序)
- [7.2 从机端主程序](#7.2 从机端主程序)
- 八、测试验证
-
- [8.1 编译与烧录](#8.1 编译与烧录)
- [8.2 串口调试输出](#8.2 串口调试输出)
- [8.3 逻辑分析仪验证](#8.3 逻辑分析仪验证)
- 九、故障排查与问题解决
-
- [9.1 I2C 通信问题](#9.1 I2C 通信问题)
-
- [问题 1:I2C 总线锁死(SDA 被拉低无法释放)](#问题 1:I2C 总线锁死(SDA 被拉低无法释放))
- [问题 2:I2C 从机无应答(NACK)](#问题 2:I2C 从机无应答(NACK))
- [9.2 SPI 通信问题](#9.2 SPI 通信问题)
-
- [问题 3:SPI 从机接收数据全为 0x00 或 0xFF](#问题 3:SPI 从机接收数据全为 0x00 或 0xFF)
- [问题 4:SPI 从机只能收到第一帧,后续帧丢失](#问题 4:SPI 从机只能收到第一帧,后续帧丢失)
- [9.3 协议层问题](#9.3 协议层问题)
-
- [问题 5:CRC 校验始终失败](#问题 5:CRC 校验始终失败)
- [问题 6:帧序号不匹配](#问题 6:帧序号不匹配)
- [9.4 调试技巧总结](#9.4 调试技巧总结)
- 十、总结
-
- [10.1 核心知识点回顾](#10.1 核心知识点回顾)
- [10.2 扩展方向](#10.2 扩展方向)
- [10.3 学习资源](#10.3 学习资源)
一、前言
1.1 技术背景
在嵌入式系统中,单颗MCU往往难以满足复杂应用的全部需求。当系统需要同时处理电机控制、传感器采集、通信协议栈和人机交互时,将任务分配到多颗MCU协同工作是一种成熟且高效的架构方案。多MCU架构的核心挑战在于:如何在芯片之间高效、可靠地传输数据并实现协调控制。
I2C和SPI是嵌入式领域最常用的两种片间通信协议。I2C以两根线实现多设备挂载,适合低速、多节点场景;SPI以全双工高速传输见长,适合对实时性要求较高的场景。STM32F1系列(Cortex-M3内核)同时提供了I2C和SPI硬件外设,是学习和实践多MCU通信的理想平台。
1.2 本文目标与读者收获
本文将以两块STM32F103C8T6("蓝色药丸")为硬件平台,分别实现基于I2C和SPI的主从通信,涵盖数据同步与远程控制两大典型应用场景。完成本教程后,读者将掌握:STM32F1硬件I2C/SPI外设的主从配置方法、自定义通信协议帧的设计思路、多MCU间数据同步与命令控制的完整实现、通信异常检测与故障恢复策略。
本文适合已具备STM32 GPIO/UART基础、了解I2C/SPI协议基本概念的嵌入式开发者。
技术栈:
- 芯片平台:STM32F103C8T6(Cortex-M3,72MHz)
- 开发环境:Keil MDK 5 / STM32CubeIDE
- 固件库:STM32 标准外设库(StdPeriph)
- 调试工具:ST-Link V2、逻辑分析仪、串口调试助手
- 硬件:2× STM32F103C8T6 最小系统板、杜邦线、4.7kΩ 电阻
1.3 系统架构总览
从机 STM32F103
主机 STM32F103
I2C / SPI 总线
应用层
传感器采集/决策
协议层
帧封装/解析
驱动层
I2C/SPI Master
驱动层
I2C/SPI Slave
协议层
帧解析/响应
应用层
电机控制/执行
二、环境准备
2.1 硬件清单
| 序号 | 器件 | 数量 | 说明 |
|---|---|---|---|
| 1 | STM32F103C8T6 最小系统板 | 2 | 主机 + 从机 |
| 2 | ST-Link V2 调试器 | 1 | 下载与调试 |
| 3 | 4.7kΩ 电阻 | 2 | I2C 总线上拉(SPI 方案不需要) |
| 4 | 杜邦线(母对母) | 若干 | 板间连接 |
| 5 | 面包板 | 1 | 可选,方便接线 |
| 6 | 逻辑分析仪 | 1 | 可选,调试信号波形 |
2.2 软件环境
Keil MDK 5 安装与配置:
- 从 ARM 官网下载 Keil MDK 5 并安装
- 打开 Pack Installer,搜索并安装
Keil.STM32F1xx_DFP器件支持包 - 验证安装:新建工程时能看到
STM32F103C8器件选项
STM32 标准外设库配置:
从 ST 官网下载 STM32F10x_StdPeriph_Lib_V3.5.0,工程中需包含以下源文件:
Project/
├── CMSIS/
│ ├── core_cm3.c
│ ├── system_stm32f10x.c
│ └── startup_stm32f10x_md.s
├── StdPeriph_Driver/
│ ├── stm32f10x_gpio.c
│ ├── stm32f10x_rcc.c
│ ├── stm32f10x_i2c.c // I2C 方案
│ ├── stm32f10x_spi.c // SPI 方案
│ ├── stm32f10x_usart.c // 调试串口
│ └── misc.c // NVIC 中断管理
├── User/
│ ├── main.c
│ ├── comm_protocol.c // 通信协议层
│ ├── comm_protocol.h
│ ├── i2c_driver.c // I2C 驱动
│ ├── spi_driver.c // SPI 驱动
│ └── stm32f10x_conf.h
└── README.md
在 stm32f10x_conf.h 中确保包含所需头文件:
c
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_i2c.h"
#include "stm32f10x_spi.h"
#include "stm32f10x_usart.h"
#include "misc.h"
2.3 硬件连接
I2C 方案接线
主机 STM32 (Master) 从机 STM32 (Slave)
┌──────────────┐ ┌──────────────┐
│ PB6 (SCL)─────────── PB6 (SCL) │
│ PB7 (SDA)─────────── PB7 (SDA) │
│ GND ──────────────── GND │
│ PA9 (TX) ──── 串口调试 │
└──────────────┘ └──────────────┘
│ │
4.7k 4.7k ← 上拉电阻
│ │
VCC VCC (3.3V)
⚠️ 注意:I2C 总线必须接上拉电阻,推荐 4.7kΩ。两块板子必须共地(GND 互连),否则通信电平不一致会导致通信失败。
SPI 方案接线
主机 STM32 (Master) 从机 STM32 (Slave)
┌──────────────┐ ┌──────────────┐
│ PA5 (SCK) ─────────── PA5 (SCK) │
│ PA7 (MOSI)─────────── PA7 (MOSI) │
│ PA6 (MISO)─────────── PA6 (MISO) │
│ PA4 (NSS) ─────────── PA4 (NSS) │
│ GND ──────────────── GND │
│ PA9 (TX) ──── 串口调试 │
└──────────────┘ └──────────────┘
💡 提示:SPI 不需要上拉电阻,但 NSS(片选)信号线不可省略,它是主机选中从机的关键信号。
三、I2C 与 SPI 协议对比分析
在选择通信方案前,需要理解两种协议的核心差异。下表从多MCU通信的实际需求出发进行对比:
| 对比维度 | I2C | SPI |
|---|---|---|
| 信号线数量 | 2根(SCL + SDA) | 4根(SCK + MOSI + MISO + NSS) |
| 通信方式 | 半双工 | 全双工 |
| 最大速率(STM32F1) | 400kHz(快速模式) | 18MHz(APB2 分频) |
| 寻址方式 | 7/10位地址,总线自带寻址 | 硬件片选(每从机1根NSS线) |
| 多从机扩展 | 同一总线可挂128个设备 | 每增加1个从机需多1根片选线 |
| 数据可靠性 | 有ACK应答机制 | 无内建应答,需协议层保证 |
| 适用场景 | 多传感器采集、配置下发 | 高速数据流、实时控制 |
选型建议: 如果系统中从机数量多(≥3个)且数据量不大,优先选择I2C;如果对传输速率和实时性要求高,或需要全双工同时收发,选择SPI。
是
否
是
否
是
否
是
否
多MCU通信选型
从机数量 ≥ 3?
数据速率要求 > 1Mbps?
需要全双工?
SPI + 多路片选
或 SPI 菊花链
I2C 总线方案
节省IO,易扩展
SPI 方案
高速全双工
数据量大?
四、自定义通信协议设计
无论使用I2C还是SPI,裸传字节流在实际工程中都不可靠。我们需要在物理层之上设计一套轻量级的应用协议帧,实现帧定界、命令区分、数据校验和应答确认。
4.1 协议帧格式
┌───────┬───────┬────────┬──────────┬───────────────┬──────────┐
│ HEAD │ LEN │ CMD │ SEQ │ DATA │ CRC8 │
│ 0xA5 │ 1Byte │ 1Byte │ 1Byte │ 0~32 Bytes │ 1Byte │
└───────┴───────┴────────┴──────────┴───────────────┴──────────┘
各字段说明:
| 字段 | 长度 | 说明 |
|---|---|---|
| HEAD | 1B | 帧头固定 0xA5,用于帧同步定界 |
| LEN | 1B | DATA 字段长度(0~32) |
| CMD | 1B | 命令码,区分读/写/控制/应答 |
| SEQ | 1B | 帧序号(0~255循环),用于匹配请求与应答 |
| DATA | 0~32B | 有效载荷 |
| CRC8 | 1B | 对 LEN+CMD+SEQ+DATA 的 CRC-8 校验 |
4.2 命令码定义
c
/* comm_protocol.h */
#ifndef __COMM_PROTOCOL_H
#define __COMM_PROTOCOL_H
#include "stm32f10x.h"
#include <string.h>
/* 帧格式常量 */
#define FRAME_HEAD 0xA5 // 帧头标识
#define FRAME_MAX_DATA_LEN 32 // 最大数据长度
#define FRAME_OVERHEAD 5 // 帧开销: HEAD + LEN + CMD + SEQ + CRC8
/* 命令码定义 */
#define CMD_READ_SENSOR 0x01 // 主机读取从机传感器数据
#define CMD_WRITE_PARAM 0x02 // 主机下发参数到从机
#define CMD_MOTOR_CTRL 0x03 // 主机控制从机电机
#define CMD_SYNC_TIME 0x04 // 时间同步命令
#define CMD_HEARTBEAT 0x05 // 心跳检测
#define CMD_ACK_OK 0x80 // 应答成功(CMD | 0x80 表示对应命令的应答)
#define CMD_ACK_ERR 0xFF // 应答错误
/* 协议帧结构体 */
typedef struct {
uint8_t cmd; // 命令码
uint8_t seq; // 帧序号
uint8_t data[FRAME_MAX_DATA_LEN]; // 数据载荷
uint8_t data_len; // 数据长度
} CommFrame_t;
/* 传感器数据结构体(从机上报) */
typedef struct {
int16_t temperature; // 温度值 × 10(如 256 表示 25.6°C)
uint16_t humidity; // 湿度值 × 10
uint16_t adc_value; // ADC 采样值
uint8_t status; // 从机状态标志
} SensorData_t;
/* 电机控制结构体(主机下发) */
typedef struct {
uint8_t motor_id; // 电机编号
uint8_t direction; // 方向:0=停止, 1=正转, 2=反转
uint16_t speed; // 速度(PWM占空比 0~1000)
} MotorCtrl_t;
/* 函数声明 */
uint8_t CRC8_Calculate(uint8_t *data, uint8_t len);
uint8_t Frame_Pack(CommFrame_t *frame, uint8_t *buf);
int8_t Frame_Unpack(uint8_t *buf, uint8_t buf_len, CommFrame_t *frame);
#endif /* __COMM_PROTOCOL_H */
4.3 协议层实现
c
/* comm_protocol.c */
#include "comm_protocol.h"
/* CRC-8 查表法(多项式 0x07,即 x^8 + x^2 + x + 1) */
static const uint8_t CRC8_Table[256] = {
0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15,
0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65,
0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,
0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5,
0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85,
0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,
/* 省略剩余表项,实际工程中需完整256项 */
/* 可通过在线工具生成完整CRC8表 */
};
/**
* @brief 计算 CRC-8 校验值
* @param data: 待校验数据指针
* @param len: 数据长度
* @retval CRC-8 校验值
*/
uint8_t CRC8_Calculate(uint8_t *data, uint8_t len)
{
uint8_t crc = 0x00; // 初始值
while (len--) {
crc = CRC8_Table[crc ^ (*data++)];
}
return crc;
}
/**
* @brief 将协议帧打包为字节流
* @param frame: 帧结构体指针
* @param buf: 输出缓冲区(至少 FRAME_OVERHEAD + frame->data_len 字节)
* @retval 打包后的总字节数
*/
uint8_t Frame_Pack(CommFrame_t *frame, uint8_t *buf)
{
uint8_t idx = 0;
/* 帧头 */
buf[idx++] = FRAME_HEAD;
/* 数据长度 */
buf[idx++] = frame->data_len;
/* 命令码 */
buf[idx++] = frame->cmd;
/* 帧序号 */
buf[idx++] = frame->seq;
/* 数据载荷 */
if (frame->data_len > 0) {
memcpy(&buf[idx], frame->data, frame->data_len);
idx += frame->data_len;
}
/* CRC8 校验:对 LEN + CMD + SEQ + DATA 计算 */
buf[idx] = CRC8_Calculate(&buf[1], idx - 1);
idx++;
return idx; // 返回帧总长度
}
/**
* @brief 从字节流中解析协议帧
* @param buf: 输入缓冲区
* @param buf_len: 缓冲区有效数据长度
* @param frame: 输出帧结构体
* @retval 0: 解析成功
* -1: 帧头错误
* -2: 长度异常
* -3: CRC 校验失败
*/
int8_t Frame_Unpack(uint8_t *buf, uint8_t buf_len, CommFrame_t *frame)
{
/* 检查帧头 */
if (buf[0] != FRAME_HEAD) {
return -1;
}
/* 提取数据长度并检查合法性 */
uint8_t data_len = buf[1];
if (data_len > FRAME_MAX_DATA_LEN) {
return -2;
}
/* 检查缓冲区是否包含完整帧 */
uint8_t expected_len = FRAME_OVERHEAD + data_len;
if (buf_len < expected_len) {
return -2;
}
/* CRC 校验:对 LEN + CMD + SEQ + DATA 校验 */
uint8_t crc_calc = CRC8_Calculate(&buf[1], 3 + data_len);
uint8_t crc_recv = buf[4 + data_len];
if (crc_calc != crc_recv) {
return -3;
}
/* 解析各字段 */
frame->data_len = data_len;
frame->cmd = buf[2];
frame->seq = buf[3];
if (data_len > 0) {
memcpy(frame->data, &buf[4], data_len);
}
return 0; // 解析成功
}
4.4 通信时序流程
从机 Slave 主机 Master 从机 Slave 主机 Master 场景1:主机读取传感器数据 场景2:主机控制电机 场景3:心跳检测 场景4:通信异常 [A5][00][01][seq][CRC] 读传感器请求 [A5][07][81][seq][data...][CRC] 应答+传感器数据 [A5][04][03][seq][motor_data][CRC] 电机控制命令 [A5][00][83][seq][CRC] 执行成功应答 [A5][00][05][seq][CRC] 心跳请求 [A5][00][85][seq][CRC] 心跳应答 [A5][00][01][seq][CRC] 读传感器请求 超时无应答 [A5][00][01][seq][CRC] 重试(最多3次)
五、I2C 通信实现
5.1 I2C 驱动层 --- 主机端
STM32F1 的硬件 I2C 外设在工程实践中存在已知的总线锁死问题(官方勘误表 ES096 记录)。本文采用硬件 I2C 配合超时保护的方案,并在后文故障排查章节提供软件模拟 I2C 的备选方案。
c
/* i2c_driver.c --- 主机端 */
#include "stm32f10x.h"
#include "comm_protocol.h"
#define I2C_SLAVE_ADDR 0x30 // 从机7位地址(左移1位后为0x60)
#define I2C_TIMEOUT 10000 // 超时计数阈值
#define I2C_SPEED 200000 // I2C 时钟频率 200kHz
/* 超时检测宏:避免死等 */
#define I2C_WAIT_EVENT(I2Cx, event) do { \
uint32_t timeout = I2C_TIMEOUT; \
while (!I2C_CheckEvent(I2Cx, event)) { \
if (--timeout == 0) return -1; \
} \
} while(0)
/**
* @brief I2C1 主机初始化
* SCL = PB6, SDA = PB7
*/
void I2C_Master_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
I2C_InitTypeDef I2C_InitStructure;
/* 使能时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
/* 配置 PB6(SCL), PB7(SDA) 为复用开漏输出 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 复用开漏------I2C必须
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
/* I2C 参数配置 */
I2C_DeInit(I2C1);
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_OwnAddress1 = 0x00; // 主机地址(主模式下不重要)
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_ClockSpeed = I2C_SPEED;
I2C_Init(I2C1, &I2C_InitStructure);
/* 使能 I2C1 */
I2C_Cmd(I2C1, ENABLE);
}
/**
* @brief I2C 主机发送数据帧到从机
* @param buf: 已打包的帧数据
* @param len: 帧长度
* @retval 0: 成功, -1: 超时失败
*/
int8_t I2C_Master_Transmit(uint8_t *buf, uint8_t len)
{
/* 等待总线空闲 */
uint32_t timeout = I2C_TIMEOUT;
while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)) {
if (--timeout == 0) return -1;
}
/* 发送起始条件 */
I2C_GenerateSTART(I2C1, ENABLE);
I2C_WAIT_EVENT(I2C1, I2C_EVENT_MASTER_MODE_SELECT);
/* 发送从机地址 + 写方向 */
I2C_Send7bitAddress(I2C1, I2C_SLAVE_ADDR << 1, I2C_Direction_Transmitter);
I2C_WAIT_EVENT(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
/* 逐字节发送数据 */
for (uint8_t i = 0; i < len; i++) {
I2C_SendData(I2C1, buf[i]);
I2C_WAIT_EVENT(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED);
}
/* 发送停止条件 */
I2C_GenerateSTOP(I2C1, ENABLE);
return 0;
}
/**
* @brief I2C 主机从从机接收数据
* @param buf: 接收缓冲区
* @param len: 期望接收长度
* @retval 0: 成功, -1: 超时失败
*/
int8_t I2C_Master_Receive(uint8_t *buf, uint8_t len)
{
uint32_t timeout = I2C_TIMEOUT;
while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)) {
if (--timeout == 0) return -1;
}
/* 发送起始条件 */
I2C_GenerateSTART(I2C1, ENABLE);
I2C_WAIT_EVENT(I2C1, I2C_EVENT_MASTER_MODE_SELECT);
/* 发送从机地址 + 读方向 */
I2C_Send7bitAddress(I2C1, I2C_SLAVE_ADDR << 1, I2C_Direction_Receiver);
I2C_WAIT_EVENT(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);
/* 逐字节接收 */
for (uint8_t i = 0; i < len; i++) {
if (i == len - 1) {
/* 最后一个字节前关闭ACK,准备发送NACK */
I2C_AcknowledgeConfig(I2C1, DISABLE);
I2C_GenerateSTOP(I2C1, ENABLE);
}
I2C_WAIT_EVENT(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED);
buf[i] = I2C_ReceiveData(I2C1);
}
/* 重新使能 ACK,为下次通信做准备 */
I2C_AcknowledgeConfig(I2C1, ENABLE);
return 0;
}
5.2 I2C 驱动层 --- 从机端(中断方式)
从机端采用中断驱动方式接收数据,避免轮询阻塞主循环:
c
/* i2c_driver.c --- 从机端 */
#include "stm32f10x.h"
#include "comm_protocol.h"
#define I2C_OWN_ADDR 0x30 // 从机自身地址
/* 接收缓冲区 */
static volatile uint8_t i2c_rx_buf[FRAME_OVERHEAD + FRAME_MAX_DATA_LEN];
static volatile uint8_t i2c_rx_idx = 0;
static volatile uint8_t i2c_rx_complete = 0;
/* 发送缓冲区 */
static volatile uint8_t i2c_tx_buf[FRAME_OVERHEAD + FRAME_MAX_DATA_LEN];
static volatile uint8_t i2c_tx_len = 0;
static volatile uint8_t i2c_tx_idx = 0;
/**
* @brief I2C1 从机初始化(中断模式)
*/
void I2C_Slave_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
I2C_InitTypeDef I2C_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
/* 使能时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
/* GPIO 配置:PB6(SCL), PB7(SDA) 复用开漏 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
/* I2C 从机参数 */
I2C_DeInit(I2C1);
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_OwnAddress1 = I2C_OWN_ADDR << 1; // 左移1位
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_ClockSpeed = 200000;
I2C_Init(I2C1, &I2C_InitStructure);
/* 配置 I2C 事件中断 */
NVIC_InitStructure.NVIC_IRQChannel = I2C1_EV_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
/* 配置 I2C 错误中断 */
NVIC_InitStructure.NVIC_IRQChannel = I2C1_ER_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
/* 使能 I2C 中断 */
I2C_ITConfig(I2C1, I2C_IT_EVT | I2C_IT_BUF | I2C_IT_ERR, ENABLE);
/* 使能 I2C */
I2C_Cmd(I2C1, ENABLE);
}
/**
* @brief 设置从机待发送的应答数据
* @param buf: 已打包的应答帧
* @param len: 帧长度
*/
void I2C_Slave_SetTxData(uint8_t *buf, uint8_t len)
{
memcpy((uint8_t *)i2c_tx_buf, buf, len);
i2c_tx_len = len;
i2c_tx_idx = 0;
}
/**
* @brief 检查是否收到完整帧
* @retval 1: 收到完整帧, 0: 未收到
*/
uint8_t I2C_Slave_IsFrameReady(void)
{
return i2c_rx_complete;
}
/**
* @brief 获取接收到的帧数据
* @param buf: 输出缓冲区
* @retval 帧长度
*/
uint8_t I2C_Slave_GetRxData(uint8_t *buf)
{
uint8_t len = i2c_rx_idx;
memcpy(buf, (uint8_t *)i2c_rx_buf, len);
i2c_rx_idx = 0;
i2c_rx_complete = 0;
return len;
}
/**
* @brief I2C1 事件中断服务函数
*/
void I2C1_EV_IRQHandler(void)
{
uint32_t event = I2C_GetLastEvent(I2C1);
switch (event) {
/* 从机地址匹配(写方向)------ 主机要发数据过来 */
case I2C_EVENT_SLAVE_RECEIVER_ADDRESS_MATCHED:
i2c_rx_idx = 0;
i2c_rx_complete = 0;
break;
/* 从机接收到一个字节 */
case I2C_EVENT_SLAVE_BYTE_RECEIVED:
if (i2c_rx_idx < sizeof(i2c_rx_buf)) {
i2c_rx_buf[i2c_rx_idx++] = I2C_ReceiveData(I2C1);
} else {
(void)I2C_ReceiveData(I2C1); // 溢出丢弃
}
break;
/* 从机地址匹配(读方向)------ 主机要读数据 */
case I2C_EVENT_SLAVE_TRANSMITTER_ADDRESS_MATCHED:
i2c_tx_idx = 0;
I2C_SendData(I2C1, i2c_tx_buf[i2c_tx_idx++]);
break;
/* 从机发送下一个字节 */
case I2C_EVENT_SLAVE_BYTE_TRANSMITTED:
if (i2c_tx_idx < i2c_tx_len) {
I2C_SendData(I2C1, i2c_tx_buf[i2c_tx_idx++]);
} else {
I2C_SendData(I2C1, 0xFF); // 填充字节
}
break;
/* 停止条件检测 ------ 一帧接收完成 */
case I2C_EVENT_SLAVE_STOP_DETECTED:
I2C_GetFlagStatus(I2C1, I2C_FLAG_STOPF); // 清除 STOPF
I2C_Cmd(I2C1, ENABLE); // 重新使能
if (i2c_rx_idx > 0) {
i2c_rx_complete = 1; // 标记帧接收完成
}
break;
default:
break;
}
}
/**
* @brief I2C1 错误中断服务函数
*/
void I2C1_ER_IRQHandler(void)
{
/* AF 错误:主机发送 NACK(读操作结束) */
if (I2C_GetITStatus(I2C1, I2C_IT_AF)) {
I2C_ClearITPendingBit(I2C1, I2C_IT_AF);
}
/* 总线错误 */
if (I2C_GetITStatus(I2C1, I2C_IT_BERR)) {
I2C_ClearITPendingBit(I2C1, I2C_IT_BERR);
}
}
六、SPI 通信实现
6.1 SPI 驱动层 --- 主机端
SPI 通信速率远高于 I2C,但由于 SPI 协议本身没有应答机制,主机发送命令后需要等待从机准备好数据再发起读取。本文采用"命令-延时-读取"的两阶段通信模式。
c
/* spi_driver.c --- 主机端 */
#include "stm32f10x.h"
#include "comm_protocol.h"
#define SPI_TIMEOUT 5000 // SPI 超时计数
/**
* @brief SPI1 主机初始化
* SCK = PA5, MISO = PA6, MOSI = PA7, NSS = PA4(软件控制)
*/
void SPI_Master_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
/* 使能时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1 | RCC_APB2Periph_GPIOA, ENABLE);
/* 配置 SCK(PA5), MOSI(PA7) 为复用推挽输出 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 配置 MISO(PA6) 为浮空输入 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 配置 NSS(PA4) 为通用推挽输出(软件片选) */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 默认拉高 NSS(未选中从机) */
GPIO_SetBits(GPIOA, GPIO_Pin_4);
/* SPI 参数配置 */
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 空闲时SCK低电平
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 第一个边沿采样
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 软件控制片选
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16; // 72/16=4.5MHz
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStructure);
/* 使能 SPI1 */
SPI_Cmd(SPI1, ENABLE);
}
/**
* @brief SPI 收发一个字节(全双工)
* @param data: 待发送字节
* @retval 接收到的字节
*/
static uint8_t SPI_TransferByte(uint8_t data)
{
uint32_t timeout;
/* 等待发送缓冲区空 */
timeout = SPI_TIMEOUT;
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET) {
if (--timeout == 0) return 0xFF;
}
/* 发送数据 */
SPI_I2S_SendData(SPI1, data);
/* 等待接收缓冲区非空 */
timeout = SPI_TIMEOUT;
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET) {
if (--timeout == 0) return 0xFF;
}
/* 返回接收到的数据 */
return (uint8_t)SPI_I2S_ReceiveData(SPI1);
}
/* NSS 片选控制 */
static void SPI_NSS_Low(void) { GPIO_ResetBits(GPIOA, GPIO_Pin_4); }
static void SPI_NSS_High(void) { GPIO_SetBits(GPIOA, GPIO_Pin_4); }
/**
* @brief SPI 主机发送协议帧
* @param buf: 已打包的帧数据
* @param len: 帧长度
* @retval 0: 成功
*/
int8_t SPI_Master_Transmit(uint8_t *buf, uint8_t len)
{
SPI_NSS_Low(); // 拉低片选,选中从机
for (uint8_t i = 0; i < len; i++) {
SPI_TransferByte(buf[i]);
}
SPI_NSS_High(); // 释放片选
return 0;
}
/**
* @brief SPI 主机接收从机应答
* 发送 Dummy 字节(0xFF) 来产生时钟,同时接收从机数据
* @param buf: 接收缓冲区
* @param len: 期望接收长度
* @retval 0: 成功
*/
int8_t SPI_Master_Receive(uint8_t *buf, uint8_t len)
{
SPI_NSS_Low();
for (uint8_t i = 0; i < len; i++) {
buf[i] = SPI_TransferByte(0xFF); // 发送哑字节,接收有效数据
}
SPI_NSS_High();
return 0;
}
/**
* @brief SPI 主机发送命令并接收应答(完整事务)
* @param tx_buf: 发送帧缓冲区
* @param tx_len: 发送帧长度
* @param rx_buf: 接收帧缓冲区
* @param rx_len: 期望接收长度
* @retval 0: 成功
*/
int8_t SPI_Master_TransferFrame(uint8_t *tx_buf, uint8_t tx_len,
uint8_t *rx_buf, uint8_t rx_len)
{
/* 阶段1:发送命令帧 */
SPI_Master_Transmit(tx_buf, tx_len);
/* 等待从机处理(从机需要时间解析命令并准备应答) */
for (volatile uint32_t i = 0; i < 1000; i++); // 约 50us @72MHz
/* 阶段2:读取应答帧 */
SPI_Master_Receive(rx_buf, rx_len);
return 0;
}
6.2 SPI 驱动层 --- 从机端(中断方式)
c
/* spi_driver.c --- 从机端 */
#include "stm32f10x.h"
#include "comm_protocol.h"
/* 接收缓冲区 */
static volatile uint8_t spi_rx_buf[FRAME_OVERHEAD + FRAME_MAX_DATA_LEN];
static volatile uint8_t spi_rx_idx = 0;
static volatile uint8_t spi_rx_complete = 0;
/* 发送缓冲区 */
static volatile uint8_t spi_tx_buf[FRAME_OVERHEAD + FRAME_MAX_DATA_LEN];
static volatile uint8_t spi_tx_len = 0;
static volatile uint8_t spi_tx_idx = 0;
/**
* @brief SPI1 从机初始化(中断模式)
* SCK = PA5(输入), MISO = PA6(复用推挽), MOSI = PA7(浮空输入), NSS = PA4(浮空输入)
*/
void SPI_Slave_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
/* 使能时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1 | RCC_APB2Periph_GPIOA, ENABLE);
/* SCK(PA5), MOSI(PA7), NSS(PA4) 配置为浮空输入 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* MISO(PA6) 配置为复用推挽输出(从机向主机发送数据) */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* SPI 从机参数 */
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Slave; // 从机模式
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Hard; // 硬件片选
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStructure);
/* 配置 SPI 接收中断 */
NVIC_InitStructure.NVIC_IRQChannel = SPI1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
/* 使能 SPI 接收中断 */
SPI_I2S_ITConfig(SPI1, SPI_I2S_IT_RXNE, ENABLE);
/* 使能 SPI */
SPI_Cmd(SPI1, ENABLE);
}
/**
* @brief 设置从机待发送的应答数据
*/
void SPI_Slave_SetTxData(uint8_t *buf, uint8_t len)
{
memcpy((uint8_t *)spi_tx_buf, buf, len);
spi_tx_len = len;
spi_tx_idx = 0;
/* 预装载第一个字节到 SPI 数据寄存器 */
SPI_I2S_SendData(SPI1, spi_tx_buf[spi_tx_idx++]);
}
uint8_t SPI_Slave_IsFrameReady(void) { return spi_rx_complete; }
uint8_t SPI_Slave_GetRxData(uint8_t *buf)
{
uint8_t len = spi_rx_idx;
memcpy(buf, (uint8_t *)spi_rx_buf, len);
spi_rx_idx = 0;
spi_rx_complete = 0;
return len;
}
/**
* @brief SPI1 中断服务函数
* NSS 为硬件控制:NSS 拉低时 SPI 从机激活,拉高时自动停止
*/
void SPI1_IRQHandler(void)
{
if (SPI_I2S_GetITStatus(SPI1, SPI_I2S_IT_RXNE) != RESET) {
uint8_t rx_byte = (uint8_t)SPI_I2S_ReceiveData(SPI1);
/* 存储接收到的字节 */
if (spi_rx_idx < sizeof(spi_rx_buf)) {
spi_rx_buf[spi_rx_idx++] = rx_byte;
}
/* 装载下一个待发送字节 */
if (spi_tx_idx < spi_tx_len) {
SPI_I2S_SendData(SPI1, spi_tx_buf[spi_tx_idx++]);
} else {
SPI_I2S_SendData(SPI1, 0xFF); // 无数据时发送填充字节
}
/* 通过帧头+长度字段判断帧是否接收完成 */
if (spi_rx_idx >= 2) {
uint8_t expected_len = FRAME_OVERHEAD + spi_rx_buf[1];
if (spi_rx_idx >= expected_len && spi_rx_buf[0] == FRAME_HEAD) {
spi_rx_complete = 1;
}
}
}
}
七、应用层实现 --- 数据同步与控制
7.1 主机端主程序
主机端周期性地向从机发送传感器读取请求和电机控制命令,并处理从机应答。同时实现心跳检测机制,监控从机在线状态。
c
/* main.c --- 主机端 */
#include "stm32f10x.h"
#include "comm_protocol.h"
#include <stdio.h>
/* 选择通信方式:0 = I2C, 1 = SPI */
#define USE_SPI_MODE 0
#if USE_SPI_MODE
extern void SPI_Master_Init(void);
extern int8_t SPI_Master_TransferFrame(uint8_t *tx, uint8_t tx_len,
uint8_t *rx, uint8_t rx_len);
#define COMM_INIT() SPI_Master_Init()
#else
extern void I2C_Master_Init(void);
extern int8_t I2C_Master_Transmit(uint8_t *buf, uint8_t len);
extern int8_t I2C_Master_Receive(uint8_t *buf, uint8_t len);
#define COMM_INIT() I2C_Master_Init()
#endif
/* 全局变量 */
static uint8_t g_seq = 0; // 帧序号计数器
static uint8_t tx_buf[FRAME_OVERHEAD + FRAME_MAX_DATA_LEN];
static uint8_t rx_buf[FRAME_OVERHEAD + FRAME_MAX_DATA_LEN];
static SensorData_t g_sensor_data; // 最新传感器数据
static uint8_t g_slave_online = 0; // 从机在线标志
static uint32_t g_heartbeat_fail_cnt = 0; // 心跳失败计数
/* 调试串口初始化(USART1, PA9=TX, 115200bps) */
void USART1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
/* PA9 = TX 复用推挽 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
}
/* 重定向 printf 到 USART1 */
int fputc(int ch, FILE *f)
{
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, (uint8_t)ch);
return ch;
}
/* 简易延时 */
void Delay_ms(uint32_t ms)
{
for (uint32_t i = 0; i < ms; i++)
for (volatile uint32_t j = 0; j < 7200; j++);
}
/**
* @brief 发送命令并等待应答(I2C 模式)
* @retval 0: 成功, -1: 通信失败, -2: 应答校验失败
*/
int8_t Master_SendCommand(CommFrame_t *cmd_frame, CommFrame_t *ack_frame)
{
uint8_t tx_len, rx_len;
int8_t ret;
/* 打包发送帧 */
cmd_frame->seq = g_seq++;
tx_len = Frame_Pack(cmd_frame, tx_buf);
#if USE_SPI_MODE
/* SPI 模式:发送+接收一体 */
rx_len = FRAME_OVERHEAD + FRAME_MAX_DATA_LEN; // 最大长度接收
ret = SPI_Master_TransferFrame(tx_buf, tx_len, rx_buf, rx_len);
if (ret != 0) return -1;
#else
/* I2C 模式:先写后读 */
ret = I2C_Master_Transmit(tx_buf, tx_len);
if (ret != 0) return -1;
Delay_ms(2); // 等待从机处理
/* 先读取帧头和长度(2字节),再根据长度读取剩余数据 */
ret = I2C_Master_Receive(rx_buf, 2);
if (ret != 0) return -1;
if (rx_buf[0] == FRAME_HEAD) {
uint8_t remaining = 3 + rx_buf[1]; // CMD + SEQ + DATA + CRC
ret = I2C_Master_Receive(&rx_buf[2], remaining);
if (ret != 0) return -1;
rx_len = 2 + remaining;
} else {
return -1;
}
#endif
/* 解析应答帧 */
ret = Frame_Unpack(rx_buf, rx_len, ack_frame);
if (ret != 0) {
printf("[Master] ACK unpack failed, err=%d\r\n", ret);
return -2;
}
/* 检查序号匹配 */
if (ack_frame->seq != cmd_frame->seq) {
printf("[Master] SEQ mismatch: sent=%d, recv=%d\r\n",
cmd_frame->seq, ack_frame->seq);
return -2;
}
return 0;
}
/**
* @brief 读取从机传感器数据
*/
void Master_ReadSensor(void)
{
CommFrame_t cmd, ack;
int8_t ret;
cmd.cmd = CMD_READ_SENSOR;
cmd.data_len = 0;
/* 带重试的发送(最多3次) */
for (uint8_t retry = 0; retry < 3; retry++) {
ret = Master_SendCommand(&cmd, &ack);
if (ret == 0 && ack.cmd == (CMD_READ_SENSOR | 0x80)) {
/* 解析传感器数据 */
memcpy(&g_sensor_data, ack.data, sizeof(SensorData_t));
printf("[Master] Sensor: T=%.1f°C, H=%.1f%%, ADC=%d, Status=0x%02X\r\n",
g_sensor_data.temperature / 10.0f,
g_sensor_data.humidity / 10.0f,
g_sensor_data.adc_value,
g_sensor_data.status);
return;
}
printf("[Master] ReadSensor retry %d\r\n", retry + 1);
Delay_ms(10);
}
printf("[Master] ReadSensor FAILED after 3 retries\r\n");
}
/**
* @brief 发送电机控制命令
*/
void Master_ControlMotor(uint8_t motor_id, uint8_t dir, uint16_t speed)
{
CommFrame_t cmd, ack;
MotorCtrl_t ctrl;
int8_t ret;
ctrl.motor_id = motor_id;
ctrl.direction = dir;
ctrl.speed = speed;
cmd.cmd = CMD_MOTOR_CTRL;
cmd.data_len = sizeof(MotorCtrl_t);
memcpy(cmd.data, &ctrl, sizeof(MotorCtrl_t));
ret = Master_SendCommand(&cmd, &ack);
if (ret == 0 && ack.cmd == (CMD_MOTOR_CTRL | 0x80)) {
printf("[Master] Motor%d ctrl OK: dir=%d, speed=%d\r\n",
motor_id, dir, speed);
} else {
printf("[Master] Motor ctrl FAILED\r\n");
}
}
/**
* @brief 心跳检测
*/
void Master_Heartbeat(void)
{
CommFrame_t cmd, ack;
int8_t ret;
cmd.cmd = CMD_HEARTBEAT;
cmd.data_len = 0;
ret = Master_SendCommand(&cmd, &ack);
if (ret == 0 && ack.cmd == (CMD_HEARTBEAT | 0x80)) {
g_slave_online = 1;
g_heartbeat_fail_cnt = 0;
} else {
g_heartbeat_fail_cnt++;
if (g_heartbeat_fail_cnt >= 5) {
g_slave_online = 0;
printf("[Master] WARNING: Slave OFFLINE!\r\n");
}
}
}
/**
* @brief 主函数
*/
int main(void)
{
/* 系统初始化 */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
USART1_Init();
COMM_INIT();
printf("\r\n=== Multi-MCU Master Started ===\r\n");
printf("Communication Mode: %s\r\n", USE_SPI_MODE ? "SPI" : "I2C");
uint32_t loop_cnt = 0;
while (1) {
/* 每 100ms 执行一次心跳检测 */
if (loop_cnt % 10 == 0) {
Master_Heartbeat();
}
/* 每 500ms 读取一次传感器数据 */
if (loop_cnt % 50 == 0 && g_slave_online) {
Master_ReadSensor();
}
/* 每 2s 发送一次电机控制命令(示例:正转,速度500) */
if (loop_cnt % 200 == 0 && g_slave_online) {
Master_ControlMotor(1, 1, 500);
}
loop_cnt++;
Delay_ms(10);
}
}
7.2 从机端主程序
从机端在主循环中检查是否收到完整帧,解析命令并执行相应操作,然后构造应答帧返回给主机。
c
/* main.c --- 从机端 */
#include "stm32f10x.h"
#include "comm_protocol.h"
#include <stdio.h>
/* 选择通信方式:与主机保持一致 */
#define USE_SPI_MODE 0
#if USE_SPI_MODE
extern void SPI_Slave_Init(void);
extern uint8_t SPI_Slave_IsFrameReady(void);
extern uint8_t SPI_Slave_GetRxData(uint8_t *buf);
extern void SPI_Slave_SetTxData(uint8_t *buf, uint8_t len);
#define COMM_INIT() SPI_Slave_Init()
#define COMM_IS_FRAME_READY() SPI_Slave_IsFrameReady()
#define COMM_GET_RX(buf) SPI_Slave_GetRxData(buf)
#define COMM_SET_TX(buf, len) SPI_Slave_SetTxData(buf, len)
#else
extern void I2C_Slave_Init(void);
extern uint8_t I2C_Slave_IsFrameReady(void);
extern uint8_t I2C_Slave_GetRxData(uint8_t *buf);
extern void I2C_Slave_SetTxData(uint8_t *buf, uint8_t len);
#define COMM_INIT() I2C_Slave_Init()
#define COMM_IS_FRAME_READY() I2C_Slave_IsFrameReady()
#define COMM_GET_RX(buf) I2C_Slave_GetRxData(buf)
#define COMM_SET_TX(buf, len) I2C_Slave_SetTxData(buf, len)
#endif
/* 模拟传感器数据 */
static SensorData_t g_sensor = {
.temperature = 256, // 25.6°C
.humidity = 650, // 65.0%
.adc_value = 2048,
.status = 0x01 // 正常运行
};
static uint8_t rx_buf[FRAME_OVERHEAD + FRAME_MAX_DATA_LEN];
static uint8_t tx_buf[FRAME_OVERHEAD + FRAME_MAX_DATA_LEN];
/**
* @brief 处理主机命令并构造应答
*/
void Slave_ProcessCommand(CommFrame_t *req)
{
CommFrame_t ack;
uint8_t ack_len;
ack.seq = req->seq; // 应答帧序号与请求帧一致
switch (req->cmd) {
case CMD_READ_SENSOR:
/* 更新模拟传感器数据(实际工程中从ADC/传感器读取) */
g_sensor.temperature += 1; // 模拟温度变化
if (g_sensor.temperature > 400) g_sensor.temperature = 200;
ack.cmd = CMD_READ_SENSOR | 0x80; // 应答命令码
ack.data_len = sizeof(SensorData_t);
memcpy(ack.data, &g_sensor, sizeof(SensorData_t));
break;
case CMD_MOTOR_CTRL: {
MotorCtrl_t ctrl;
memcpy(&ctrl, req->data, sizeof(MotorCtrl_t));
/* 执行电机控制(实际工程中配置PWM和方向GPIO) */
printf("[Slave] Motor%d: dir=%d, speed=%d\r\n",
ctrl.motor_id, ctrl.direction, ctrl.speed);
ack.cmd = CMD_MOTOR_CTRL | 0x80;
ack.data_len = 0;
break;
}
case CMD_HEARTBEAT:
ack.cmd = CMD_HEARTBEAT | 0x80;
ack.data_len = 0;
break;
default:
ack.cmd = CMD_ACK_ERR;
ack.data_len = 0;
break;
}
/* 打包应答帧并装载到发送缓冲区 */
ack_len = Frame_Pack(&ack, tx_buf);
COMM_SET_TX(tx_buf, ack_len);
}
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
COMM_INIT();
while (1) {
/* 检查是否收到完整帧 */
if (COMM_IS_FRAME_READY()) {
uint8_t len = COMM_GET_RX(rx_buf);
/* 解析帧 */
CommFrame_t req;
int8_t ret = Frame_Unpack(rx_buf, len, &req);
if (ret == 0) {
Slave_ProcessCommand(&req);
}
}
}
}
八、测试验证
8.1 编译与烧录
分别为主机和从机创建独立的 Keil 工程,编译时注意以下配置:
在 Keil 的 Options for Target → C/C++ → Define 中添加 USE_STDPERIPH_DRIVER,确保标准外设库宏定义生效。主机和从机工程中的 USE_SPI_MODE 宏必须保持一致(同为 0 或同为 1)。
烧录步骤:先烧录从机程序并上电,再烧录主机程序。主机上电后会自动开始心跳检测和数据通信。
8.2 串口调试输出
将主机的 PA9(TX) 通过 USB 转串口模块连接到 PC,打开串口调试助手(115200, 8N1),正常运行时应看到如下输出:
=== Multi-MCU Master Started ===
Communication Mode: I2C
[Master] Sensor: T=25.6°C, H=65.0%, ADC=2048, Status=0x01
[Master] Sensor: T=25.7°C, H=65.0%, ADC=2048, Status=0x01
[Master] Motor1 ctrl OK: dir=1, speed=500
[Master] Sensor: T=25.8°C, H=65.0%, ADC=2048, Status=0x01
[Master] Sensor: T=25.9°C, H=65.0%, ADC=2048, Status=0x01
[Master] Motor1 ctrl OK: dir=1, speed=500
如果从机离线,会看到:
[Master] ReadSensor retry 1
[Master] ReadSensor retry 2
[Master] ReadSensor retry 3
[Master] ReadSensor FAILED after 3 retries
[Master] WARNING: Slave OFFLINE!
8.3 逻辑分析仪验证
使用逻辑分析仪(如 Saleae Logic)抓取 I2C/SPI 总线波形,验证通信时序是否正确:
I2C 验证要点: 检查 START/STOP 条件是否正确生成,确认从机地址(0x30)和 ACK 位,验证数据字节与协议帧格式一致,确认 SCL 频率在 200kHz 左右。
SPI 验证要点: 检查 NSS 信号在通信期间保持低电平,确认 CPOL=0, CPHA=0(空闲低电平,第一边沿采样),验证 MOSI 上的命令帧和 MISO 上的应答帧,确认 SCK 频率在 4.5MHz 左右。
九、故障排查与问题解决
9.1 I2C 通信问题
问题 1:I2C 总线锁死(SDA 被拉低无法释放)
错误现象:
主机调用 I2C_Master_Transmit 始终返回 -1(超时),用万用表测量 SDA 线电压为 0V。
原因分析:
STM32F1 的硬件 I2C 存在已知缺陷(ST 勘误表 ES096),当通信过程中被中断打断或复位时,从机可能处于发送 ACK 的状态,持续拉低 SDA 线。
解决方案:
c
/**
* @brief I2C 总线恢复:通过 GPIO 模拟发送时钟脉冲释放 SDA
* 在 I2C 初始化之前调用
*/
void I2C_Bus_Recovery(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
/* 临时将 SCL 配置为推挽输出 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; // SCL
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
/* 将 SDA 配置为浮空输入以检测状态 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; // SDA
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOB, &GPIO_InitStructure);
/* 发送最多 9 个时钟脉冲,直到 SDA 释放 */
for (uint8_t i = 0; i < 9; i++) {
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7) == Bit_SET) {
break; // SDA 已释放
}
GPIO_ResetBits(GPIOB, GPIO_Pin_6); // SCL 低
for (volatile int d = 0; d < 100; d++);
GPIO_SetBits(GPIOB, GPIO_Pin_6); // SCL 高
for (volatile int d = 0; d < 100; d++);
}
/* 发送 STOP 条件:SDA 从低到高(SCL 为高时) */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_ResetBits(GPIOB, GPIO_Pin_7); // SDA 低
for (volatile int d = 0; d < 100; d++);
GPIO_SetBits(GPIOB, GPIO_Pin_7); // SDA 高 → STOP
}
在 I2C_Master_Init() 开头调用 I2C_Bus_Recovery() 即可。
问题 2:I2C 从机无应答(NACK)
错误现象:
主机发送地址后收到 NACK,I2C_CheckEvent 返回失败。
原因分析与解决:
检查从机地址是否正确------主机发送的地址需要左移 1 位(I2C_SLAVE_ADDR << 1),而从机 I2C_OwnAddress1 也需要左移。确认两块板子共地(GND 互连)。确认上拉电阻已正确焊接(4.7kΩ 接到 3.3V)。用万用表测量 SCL/SDA 空闲时电压应为 3.3V。
9.2 SPI 通信问题
问题 3:SPI 从机接收数据全为 0x00 或 0xFF
原因分析:
主从机的 CPOL/CPHA 配置不一致,导致采样时刻错误。或者 MOSI/MISO 接线反了。
解决方案:
确保主从机的 SPI 参数完全一致:CPOL=Low, CPHA=1Edge, MSB First。用逻辑分析仪检查 MOSI 线上是否有正确的数据波形。交换 MOSI 和 MISO 接线重试。
问题 4:SPI 从机只能收到第一帧,后续帧丢失
原因分析:
从机 SPI 接收中断中未及时清除标志位,或接收缓冲区未重置。
解决方案:
确保每次帧处理完成后调用 SPI_Slave_GetRxData() 重置缓冲区索引和完成标志。检查中断优先级------SPI 中断优先级应高于其他非关键中断。
9.3 协议层问题
问题 5:CRC 校验始终失败
原因分析:
CRC 计算范围不一致。主机打包时对 buf[1] 到 buf[idx-1] 计算 CRC,从机解包时必须对相同范围计算。
解决方案:
在主从机两端分别用已知数据测试 CRC 函数输出是否一致。例如对 {0x00, 0x01, 0x00} 计算 CRC8,两端结果应相同。确保 CRC 查表数组完整(256 项)。
问题 6:帧序号不匹配
原因分析:
从机处理速度慢,主机超时重发导致序号递增,而从机仍在应答旧序号的请求。
解决方案:
适当增大主机等待应答的超时时间。在从机端增加接收缓冲区深度,支持缓存多帧。考虑在协议中增加"忙"状态应答,让主机知道从机正在处理中。
9.4 调试技巧总结
无
有
是
I2C
SPI
否
是
否
是
否
通信异常
串口有调试输出?
检查串口接线和波特率
显示超时?
I2C 还是 SPI?
检查上拉电阻
检查从机地址
执行总线恢复
检查NSS/SCK/MOSI/MISO接线
检查CPOL/CPHA配置
CRC 错误?
用逻辑分析仪抓波形
对比收发数据
SEQ 不匹配?
增大超时时间
检查从机处理速度
检查协议帧格式
逐字节对比
十、总结
10.1 核心知识点回顾
本文围绕 STM32F1 多 MCU 通信,完整实现了从协议设计到应用落地的全流程。核心要点包括:I2C 适合多节点低速场景,SPI 适合高速全双工场景,选型需根据实际需求权衡;自定义协议帧(帧头+长度+命令+序号+数据+CRC)是保证通信可靠性的关键;STM32F1 硬件 I2C 存在总线锁死隐患,必须配合超时保护和总线恢复机制;从机端采用中断驱动方式接收数据,避免阻塞主循环影响实时任务;心跳检测和重试机制是工程级通信系统的必备要素。
10.2 扩展方向
在本文基础上,可以进一步探索以下方向:DMA 传输------利用 STM32 的 DMA 控制器实现 I2C/SPI 数据的零拷贝传输,降低 CPU 占用率;多从机扩展------I2C 总线挂载 3 个以上从机节点,实现分布式传感器网络;RTOS 集成------将通信任务封装为 FreeRTOS 任务,利用消息队列和信号量实现更优雅的任务间同步;CAN 总线升级------对于工业级应用,可将通信协议迁移到 CAN 总线,获得更强的抗干扰能力和更远的通信距离。
10.3 学习资源
官方文档:
官方 GitHub: