多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:通信异常 A50001seqCRC 读传感器请求 A50781seqdata...CRC 应答+传感器数据 A50403seqmotor_dataCRC 电机控制命令 A50083seqCRC 执行成功应答 A50005seqCRC 心跳请求 A50085seqCRC 心跳应答 A50001seqCRC 读传感器请求 超时无应答 A50001seqCRC 重试(最多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:

相关推荐
国科安芯1 天前
航天器多路并联大功率电源系统设计与ASP4644均流特性分析
单片机·嵌入式硬件·fpga开发·安全性测试
茯苓gao1 天前
嵌入式开发笔记:CANopen相关移位运算与通信协议术语详解
网络·嵌入式硬件·学习·信息与通信
梁朝辉1 天前
STM32H750VBT6中ADCINP与INN什么区别
stm32·单片机·嵌入式硬件
省四收割者1 天前
一文详解信号完整性(1)
python·嵌入式硬件·数学建模·信息与通信·信号处理·智能硬件
崇山峻岭之间1 天前
单片机无感FOC驱动之ADC
单片机·嵌入式硬件
Ligocious1 天前
1.点亮一颗小小的LED
单片机·嵌入式硬件
KaifuZeng2 天前
通信与接口协议面试八、CAN通信
单片机·嵌入式硬件·面试·通信与接口协议
智源单片机设计2 天前
基于单片机的直流电机双闭环调速系统设计
单片机·嵌入式硬件
kebidaixu2 天前
两轮车换电 BMS 的模拟前端中颖 SH367306 AFE 芯片详解
stm32
kebidaixu2 天前
两轮车换电 BMS 的模拟前端中颖 SH367306 AFE 芯片调试实录:四个关键问题与解决方案
stm32