多MCU通信:STM32F1通过I2C/SPI实现数据同步与控制

文章目录

    • 一、前言
      • [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 安装与配置:

  1. 从 ARM 官网下载 Keil MDK 5 并安装
  2. 打开 Pack Installer,搜索并安装 Keil.STM32F1xx_DFP 器件支持包
  3. 验证安装:新建工程时能看到 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 TargetC/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:

相关推荐
史蒂芬_丁2 小时前
EPWM Global Load
单片机·嵌入式硬件
碎像2 小时前
单片机-数码管显示
单片机·嵌入式硬件
Wave8453 小时前
从裸机到 FreeRTOS:STM32 智能手表重构之路
stm32·重构·智能手表
LCMICRO-1331084774612 小时前
长芯微LPS123完全P2P替代ADP123,高性能、低压差的线性稳压器
单片机·嵌入式硬件·fpga开发·硬件工程·dsp开发·线性稳压器
守护安静星空13 小时前
esp32开发笔记-工程搭建
笔记·单片机·嵌入式硬件·物联网·visual studio code
ACP广源盛1392462567314 小时前
破局 Type‑C 切换器痛点@ACP#GSV6155+LH3828/GSV2221+LH3828 黄金方案
c语言·开发语言·网络·人工智能·嵌入式硬件·计算机外设·电脑
时空自由民.15 小时前
ST7701S 3.5寸显示屏
单片机
金戈鐡馬15 小时前
BetaFlight中的定时器引脚绑定详解
stm32·单片机·嵌入式硬件·无人机
Wave84516 小时前
FreeRTOS软件定时器详解
stm32·单片机·freertos