嵌入式常用通信协议总结:UART、I2C、SPI、CAN 原理对比与 STM32 模

前言

在 STM32、单片机、机器人和嵌入式项目中,最常见的通信方式主要有 UART 串口、I2C、SPI 和 CAN。它们都属于串行通信,但适用场景差别很大。

本文从通信特点、接线方式、优缺点、典型应用和 STM32 HAL 模板代码几个方面进行总结,适合用于快速复习和项目选型。

一、四种通信方式总览

通信方式 常用引脚 通信类型 主从关系 典型速率 通信距离 典型应用
UART 串口 TX、RX、GND 异步串行 点对点为主 9600、115200、1Mbps 短到中等 调试、蓝牙、GPS、串口屏、上位机
I2C / IIC SCL、SDA、GND 同步串行 一主多从 100k、400k、1MHz 短距离 OLED、EEPROM、RTC、低速传感器
SPI SCK、MOSI、MISO、CS、GND 同步串行 一主多从 几 MHz 到几十 MHz 短距离 Flash、屏幕、ADC、IMU
CAN CAN_H、CAN_L、GND 差分总线 多主通信 125k、250k、500k、1Mbps 中长距离 电机控制、汽车电子、机器人、工业控制

简单选择原则:

  • 调试、上位机、模块通信:优先选 UART。
  • 多个低速传感器、节省引脚:优先选 I2C。
  • 高速外设、大量数据传输:优先选 SPI。
  • 多节点、强干扰、远距离、可靠通信:优先选 CAN。

二、UART 串口通信

1. 基本特点

UART 是异步串行通信,不需要时钟线,通信双方只需要约定相同的波特率、数据位、停止位和校验位。

常见参数:

  • 波特率:9600、115200、500000、1000000
  • 数据位:8 bit
  • 停止位:1 bit
  • 校验位:None / Even / Odd

2. 接线方式

text 复制代码
设备A TX  -> 设备B RX
设备A RX  -> 设备B TX
设备A GND -> 设备B GND

注意:TX 和 RX 要交叉连接,GND 必须共地。

3. STM32 HAL 模板代码

c 复制代码
#include "main.h"
#include <string.h>

extern UART_HandleTypeDef huart1;

void UART_SendString(const char *str)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)str, strlen(str), 100);
}

void UART_ReceiveExample(void)
{
    uint8_t rx_data = 0;

    if (HAL_UART_Receive(&huart1, &rx_data, 1, 100) == HAL_OK)
    {
        HAL_UART_Transmit(&huart1, &rx_data, 1, 100);
    }
}

中断接收模板:

c 复制代码
#include "main.h"

extern UART_HandleTypeDef huart1;

uint8_t uart_rx_byte;

void UART_StartReceiveIT(void)
{
    HAL_UART_Receive_IT(&huart1, &uart_rx_byte, 1);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1)
    {
        HAL_UART_Transmit(&huart1, &uart_rx_byte, 1, 100);
        HAL_UART_Receive_IT(&huart1, &uart_rx_byte, 1);
    }
}

4. 优缺点

优点:

  • 使用简单,调试方便。
  • 适合和上位机、串口模块通信。
  • 硬件资源占用少。

缺点:

  • 常见情况下主要是点对点通信。
  • 抗干扰能力一般。
  • 双方波特率不一致会乱码。

三、I2C / IIC 通信

1. 基本特点

I2C 是两线同步通信,由 SCL 时钟线和 SDA 数据线组成。一个主机可以挂多个从机,每个从机通过地址区分。

I2C 常见速率:

  • 标准模式:100 kHz
  • 快速模式:400 kHz
  • 高速模式:1 MHz 及以上,具体取决于芯片支持

2. 接线方式

text 复制代码
主机 SCL -> 从机 SCL
主机 SDA -> 从机 SDA
GND      -> GND

SCL 和 SDA 通常需要上拉电阻,一般上拉到 3.3V 或 5V,取决于芯片电平。

3. STM32 HAL 模板代码

以向 I2C 设备寄存器写入和读取为例:

c 复制代码
#include "main.h"

extern I2C_HandleTypeDef hi2c1;

#define DEV_ADDR        (0x68 << 1)
#define REG_ADDR        0x75

void I2C_WriteReg(uint8_t reg, uint8_t data)
{
    HAL_I2C_Mem_Write(&hi2c1,
                      DEV_ADDR,
                      reg,
                      I2C_MEMADD_SIZE_8BIT,
                      &data,
                      1,
                      100);
}

uint8_t I2C_ReadReg(uint8_t reg)
{
    uint8_t data = 0;

    HAL_I2C_Mem_Read(&hi2c1,
                     DEV_ADDR,
                     reg,
                     I2C_MEMADD_SIZE_8BIT,
                     &data,
                     1,
                     100);

    return data;
}

扫描 I2C 地址模板:

c 复制代码
#include "main.h"
#include <stdio.h>

extern I2C_HandleTypeDef hi2c1;
extern UART_HandleTypeDef huart1;

void I2C_Scan(void)
{
    char msg[64];

    for (uint8_t addr = 1; addr < 127; addr++)
    {
        if (HAL_I2C_IsDeviceReady(&hi2c1, addr << 1, 2, 10) == HAL_OK)
        {
            snprintf(msg, sizeof(msg), "I2C device found: 0x%02X\r\n", addr);
            HAL_UART_Transmit(&huart1, (uint8_t *)msg, strlen(msg), 100);
        }
    }
}

4. 优缺点

优点:

  • 只占用两根信号线。
  • 一个总线上可以挂多个设备。
  • 很适合低速传感器。

缺点:

  • 距离短。
  • 总线速度不高。
  • 需要上拉电阻。
  • 某个设备异常时可能导致总线被拉死。

四、SPI 通信

1. 基本特点

SPI 是高速同步串行通信,通常由主机提供时钟,从机通过片选信号 CS 选择。

常用信号线:

  • SCK:时钟线
  • MOSI:主机输出,从机输入
  • MISO:主机输入,从机输出
  • CS / NSS:片选线
  • GND:共地

2. 接线方式

text 复制代码
主机 SCK  -> 从机 SCK
主机 MOSI -> 从机 MOSI
主机 MISO -> 从机 MISO
主机 CS   -> 从机 CS
GND       -> GND

多个 SPI 从设备通常共用 SCK、MOSI、MISO,每个从设备单独使用一个 CS。

3. STM32 HAL 模板代码

c 复制代码
#include "main.h"

extern SPI_HandleTypeDef hspi1;

#define SPI_CS_GPIO_Port    GPIOA
#define SPI_CS_Pin          GPIO_PIN_4

static void SPI_CS_LOW(void)
{
    HAL_GPIO_WritePin(SPI_CS_GPIO_Port, SPI_CS_Pin, GPIO_PIN_RESET);
}

static void SPI_CS_HIGH(void)
{
    HAL_GPIO_WritePin(SPI_CS_GPIO_Port, SPI_CS_Pin, GPIO_PIN_SET);
}

void SPI_WriteRead(uint8_t *tx_buf, uint8_t *rx_buf, uint16_t len)
{
    SPI_CS_LOW();
    HAL_SPI_TransmitReceive(&hspi1, tx_buf, rx_buf, len, 100);
    SPI_CS_HIGH();
}

SPI 写寄存器模板:

c 复制代码
void SPI_WriteReg(uint8_t reg, uint8_t data)
{
    uint8_t tx_buf[2];

    tx_buf[0] = reg;
    tx_buf[1] = data;

    SPI_CS_LOW();
    HAL_SPI_Transmit(&hspi1, tx_buf, 2, 100);
    SPI_CS_HIGH();
}

SPI 读寄存器模板:

c 复制代码
uint8_t SPI_ReadReg(uint8_t reg)
{
    uint8_t tx_buf[2];
    uint8_t rx_buf[2];

    tx_buf[0] = reg | 0x80;
    tx_buf[1] = 0xFF;

    SPI_CS_LOW();
    HAL_SPI_TransmitReceive(&hspi1, tx_buf, rx_buf, 2, 100);
    SPI_CS_HIGH();

    return rx_buf[1];
}

说明:不同 SPI 设备的读写位定义不同,reg | 0x80 只是常见写法,具体要看芯片手册。

4. 优缺点

优点:

  • 速度快。
  • 协议简单。
  • 支持全双工通信。

缺点:

  • 占用引脚较多。
  • 多个从机需要多个 CS。
  • 不适合远距离通信。

五、CAN 通信

1. 基本特点

CAN 是一种差分总线通信方式,常用于汽车电子、机器人底盘、电机控制和工业控制。它不是通过设备地址通信,而是通过消息 ID 区分数据类型和优先级。

CAN 的核心特点:

  • 多主通信。
  • 差分传输,抗干扰能力强。
  • 支持仲裁,ID 越小优先级越高。
  • 适合多节点总线。

2. 接线方式

text 复制代码
CAN_H -> CAN_H
CAN_L -> CAN_L
GND   -> GND

注意事项:

  • MCU 通常需要外接 CAN 收发器,例如 TJA1050、SN65HVD230。
  • 总线两端需要各接一个 120Ω 终端电阻。
  • CAN_H 和 CAN_L 不要接反。

3. STM32 HAL 模板代码

CAN 启动和滤波器配置:

c 复制代码
#include "main.h"

extern CAN_HandleTypeDef hcan1;

void CAN_FilterInit(void)
{
    CAN_FilterTypeDef filter;

    filter.FilterBank = 0;
    filter.FilterMode = CAN_FILTERMODE_IDMASK;
    filter.FilterScale = CAN_FILTERSCALE_32BIT;
    filter.FilterIdHigh = 0x0000;
    filter.FilterIdLow = 0x0000;
    filter.FilterMaskIdHigh = 0x0000;
    filter.FilterMaskIdLow = 0x0000;
    filter.FilterFIFOAssignment = CAN_RX_FIFO0;
    filter.FilterActivation = ENABLE;
    filter.SlaveStartFilterBank = 14;

    HAL_CAN_ConfigFilter(&hcan1, &filter);
    HAL_CAN_Start(&hcan1);
    HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);
}

CAN 发送模板:

c 复制代码
void CAN_SendData(uint32_t std_id, uint8_t *data, uint8_t len)
{
    CAN_TxHeaderTypeDef tx_header;
    uint32_t tx_mailbox;

    tx_header.StdId = std_id;
    tx_header.ExtId = 0;
    tx_header.IDE = CAN_ID_STD;
    tx_header.RTR = CAN_RTR_DATA;
    tx_header.DLC = len;
    tx_header.TransmitGlobalTime = DISABLE;

    HAL_CAN_AddTxMessage(&hcan1, &tx_header, data, &tx_mailbox);
}

CAN 接收中断模板:

c 复制代码
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    CAN_RxHeaderTypeDef rx_header;
    uint8_t rx_data[8];

    if (hcan->Instance == CAN1)
    {
        HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx_header, rx_data);

        switch (rx_header.StdId)
        {
        case 0x201:
            /* 处理 ID 为 0x201 的数据 */
            break;

        case 0x202:
            /* 处理 ID 为 0x202 的数据 */
            break;

        default:
            break;
        }
    }
}

4. 优缺点

优点:

  • 抗干扰强。
  • 适合多节点通信。
  • 总线仲裁机制可靠。
  • 非常适合电机、车载、机器人底盘等场景。

缺点:

  • 配置比 UART、I2C、SPI 复杂。
  • 需要 CAN 收发器。
  • 波特率、采样点、终端电阻配置错误时容易通信失败。

六、实际项目中的选型建议

1. 连接上位机调试

推荐 UART。

原因是简单、直观,配合 USB 转 TTL 和串口助手即可快速调试。

2. 连接 OLED、EEPROM、RTC、气压计等低速外设

推荐 I2C。

原因是省引脚,一个总线可以挂多个设备。

3. 连接 Flash、TFT 屏幕、高速 ADC、IMU

推荐 SPI。

原因是速度高,时序简单,适合连续读写大量数据。

4. 连接电机控制器、机器人底盘、多个控制节点

推荐 CAN。

原因是抗干扰强,支持多节点,可靠性高。

七、常见问题总结

1. UART 收到乱码

常见原因:

  • 波特率不一致。
  • TX/RX 接反或没有共地。
  • 电平不匹配,例如 3.3V 和 RS232 电平直接连接。

2. I2C 找不到设备

常见原因:

  • SDA/SCL 没有上拉电阻。
  • 地址写错,注意 HAL 库中地址通常需要左移一位。
  • 设备没有供电。
  • SDA/SCL 接反。

3. SPI 读出来全是 0x00 或 0xFF

常见原因:

  • CS 没有正确拉低。
  • SPI 模式 CPOL/CPHA 配错。
  • MOSI/MISO 接反。
  • 设备读写命令格式不对。

4. CAN 无法通信

常见原因:

  • CAN_H 和 CAN_L 接反。
  • 没有 120Ω 终端电阻。
  • 波特率不一致。
  • CAN 收发器没有供电。
  • 滤波器配置过严,导致消息被过滤。

八、总结

UART、I2C、SPI、CAN 都是嵌入式开发中非常重要的通信方式:

  • UART:简单、常用于调试和模块通信。
  • I2C:省引脚、适合低速多外设。
  • SPI:速度快、适合高速外设。
  • CAN:可靠、抗干扰、适合多节点控制系统。

项目中不要只看"能不能通信",还要考虑通信距离、速率、抗干扰能力、节点数量和硬件成本。掌握这四种通信方式,基本可以覆盖大多数 STM32 和嵌入式项目的外设通信需求。