RS422 异步 UART 通信测试全方案(基于 TI TMS320C6678)

UART 与 RS422 完全入门指南(给嵌入式新手)

适合人群:刚学单片机、第一次接触串口通信、看到“UART”“RS422”就头晕的新手
目标:读完后你能回答——

  • UART 是协议还是电线?
  • RS422 跟普通串口有什么不同?
  • 为什么我的开发板要加一个“转换芯片”才能连电脑?

一、先说结论(一句话记住)

  • UART 是一种 通信规则(协议),规定数据怎么打包、怎么发送。
  • RS422 是一种 物理电平标准,规定用多少电压、几根线来传输信号。
  • 它们的关系

    UART 是“说什么语言”,RS422 是“用什么嗓门喊”

就像:

  • 你说中文(UART 协议)
  • 但你可以小声说(TTL 电平),也可以用喇叭喊(RS422 电平)

二、UART 是什么?—— 通信的“语法”

1. 全称

Universal Asynchronous Receiver/Transmitter
(通用异步收发器)

2. 核心特点

特点解释
异步没有时钟线!靠双方提前约定“语速”(波特率)
串行数据一位一位地传(像排队过独木桥)
全双工有两条线:一条发(TX),一条收(RX),能同时说话和听

3. 一帧数据长什么样?

UART 把每个字节(8 位)包装成一个“数据帧”:


text

编辑

[起始位] [数据位] [校验位] [停止位]
   0     D0~D7     (可选)     1

举个例子:发送字母 'A'(ASCII = 65 = 01000001

  • 起始位:0(表示“我要开始说话了!”)
  • 数据位:从最低位开始发 → 1 0 0 0 0 0 1 0
  • 停止位:1(表示“我说完了!”)

关键:发送方和接收方必须约定好:

  • 波特率(每秒发多少位,如 9600)
  • 数据位(通常是 8 位)
  • 有没有校验位
  • 停止位是 1 位还是 2 位

这就是常说的 “8N1” 配置:

  • 8 位数据
  • N = No parity(无校验)
  • 1 位停止位

三、UART 的“电平问题”—— 为什么不能直接连电脑?

1. 芯片内部的 UART 用的是 TTL 电平

  • 逻辑 0 = 0V
  • 逻辑 1 = 3.3V 或 5V

⚠️ 问题来了:
这种电压只能传很短距离(< 0.5 米),而且抗干扰能力差
工厂里电机一开,电磁干扰一大,信号就乱了!

2. 传统电脑串口用的是 RS232 电平

  • 逻辑 0 = +3V ~ +15V
  • 逻辑 1 = -3V ~ -15V

❗ 注意:RS232 是负逻辑!高电压反而是 0!
而且它用正负电压,是为了抗干扰、传得远(可达 15 米)。

但 RS232 也有缺点:

  • 只能点对点(1 对 1)
  • 速度慢(最高约 115kbps)
  • 现在新电脑都没串口了(被 USB 取代)

四、RS422 是什么?—— 工业级的“大声喊话”

1. 为什么需要 RS422?

为了解决 TTL 传不远、RS232 速度慢的问题,工程师发明了 差分信号

2. 什么是“差分信号”?

不用一根线传 0/1,而是用两根线

  • A 线:发原始信号
  • B 线:发相反的信号
逻辑值A 线B 线电压差 (A - B)
1+2V ~ +6V
0-2V ~ -6V

好处
外界干扰(比如电机噪声)会同时影响 A 和 B 线,但电压差不变
所以接收端只看“差值”,就能准确判断是 0 还是 1。

3. RS422 的优势

特性RS422TTL / RS232
传输距离最长 1200 米< 15 米
传输速率最高 10 Mbps< 115 kbps
抗干扰极强(差分)弱(单端)
连接方式1 发多收(最多 10 个设备)仅 1 对 1

4. 接线方式

RS422 需要 4 根线(全双工):

  • TX+、TX-(核心板发)
  • RX+、RX-(核心板收)
  • GND(共地,很重要!)

💡 实际使用中,常通过 RS422 转 USB 模块 连到电脑。


五、UART 和 RS422 怎么配合?—— “翻译官”芯片

你的 TMS320C6678 芯片内部有 UART 模块,但它输出的是 TTL 电平(0V/3.3V)

而你要连的是 RS422 设备(用差分信号)。

所以中间需要一个 “翻译官” —— RS422 收发器芯片,比如:

  • SN65HVD72
  • MAX3080
  • ADM2587

工作流程:

C6678 (UART-TTL) 
    → [RS422 芯片] 
    → (差分信号: TX+, TX-, RX+, RX-) 
    → 工业设备 / 电脑(通过转接器)

🔧 芯片作用

  • 把 TTL 的 0/3.3V 转成 RS422 的 ±2V~±6V 差分信号
  • 把收到的差分信号转回 TTL 电平给 C6678

六、一张表总结所有概念

名称类型作用电平距离速度线数
UART通信协议规定数据帧格式、波特率等不定义(依赖物理层)--2(TX/RX)
TTL电平标准芯片间短距离通信0V / 3.3V 或 5V< 0.5m高(>1Mbps)2
RS232电平标准老式电脑串口±3V~±15V(负逻辑)≤15m≤115kbps3(TX/RX/GND)
RS422电平标准工业长距离通信差分 ±2V~±6V≤1200m≤10Mbps4(TX±, RX±)+GND

记住

  • UART 是“软件协议”,存在于代码和寄存器配置中
  • TTL/RS232/RS422 是“硬件电平”,决定你用什么芯片、怎么接线

七、实际应用举例

场景 1:调试你的 C6678 开发板

  • 板子上 UART 引脚(TTL 电平) → 接 USB 转 TTL 模块(如 CH340) → 电脑
  • 你在串口助手看到 printf("Hello") —— 这就是 UART + TTL

场景 2:工业现场连接 PLC

  • C6678 的 UART → 接 RS422 芯片 → 用双绞线连到 100 米外的 PLC
  • 即使旁边有大电机,通信依然稳定 —— 这就是 UART + RS422

八、常见误区澄清

误区 1:“UART 就是串口”
✅ 正解:UART 是串口的一种实现方式(还有 SPI、I2C 也是串口,但不是 UART)

误区 2:“RS422 是一种协议”
✅ 正解:RS422 只是物理层标准,它上面跑的还是 UART 协议!

误区 3:“RS422 和 RS485 一样”
✅ 正解:

  • RS422:全双工,1 发多收(不能多个设备同时发)
  • RS485:半双工,多设备可轮流发(更常用在 Modbus 总线)

九、给初学者的建议

  1. 先掌握 UART 协议:理解起始位、数据位、波特率
  2. 再学电平转换:知道 TTL 传不远,工业要用 RS422/RS485
  3. 动手接一次线:用 USB 转 RS422 模块连开发板,亲眼看到数据
  4. 别怕看芯片手册:找一个 RS422 芯片(如 SN65HVD72),看它的引脚图和典型电路

十、总结(终极记忆法)

  • UART = “怎么说话”(语法:先说“你好”,再说内容,最后说“再见”)
  • RS422 = “用喇叭喊”(嗓门大、传得远、不怕吵)
  • 没有 RS422 芯片,UART 只能在板子上“悄悄说话”
  • 有了 RS422,UART 就能在工厂里“大声广播”

现在,你再也不用混淆 UART 和 RS422 了!🎉


如果你正在用 TMS320C6678 做项目,记住:
写代码配 UART,画电路加 RS422 芯片,接线别忘 GND —— 通信稳如泰山!

适用场景:工业控制、嵌入式通信验证、多参数串口兼容性测试
核心目标:通过 比对收发帧数,验证 TMS320C6678 在不同 UART 配置下与上位机的 RS422 通信可靠性
测试模式

  • 接收模式:核心板监听上位机发送的 1000 帧,统计 CRC 校验正确的帧数
  • 发送模式:核心板主动发送 1000 帧,由上位机串口助手计数

一、通信协议与数据帧定义

1.1 数据帧格式(固定 17 字节)

字节索引内容值(Hex)
0–15Payload0x01, 0x02, ..., 0x10
16CRC累加和校验值 = 0x88

CRC 算法:简单字节累加和(mod 256)

crc = (0x01 + 0x02 + ... + 0x10) & 0xFF; // = 0x88

1.2 上位机行为规范

  • 发送节奏:每 100ms 发送 1 帧
  • 总帧数1000 帧
  • 工具要求:支持自动发送、16 进制显示、发送计数(如 AccessPort、SSCOM、Tera Term)

二、硬件连接说明

TMS320C6678 引脚功能连接至 RS422 芯片
GPIO[16]UART1_TXDI(Driver Input)
GPIO[17]UART1_RXRO(Receiver Output)
GND公共地GND
3.3V电源VCC

RS422 收发器(如 SN65HVD72)配置

  • DE = 1(驱动使能)
  • RE = 0(接收使能)
  • 全双工模式,无需方向控制
  • 终端电阻:在通信链路最远端并联 120 Ω 电阻

⚠️ 关键提示:必须确保 核心板与上位机共地,否则共模电压漂移将导致通信失败。


三、软件架构设计

3.1 统一配置区(用户只需修改此处)

// >>>>>>>>>>> 用户配置区 <<<<<<<<<<<
#define TEST_BAUD       115200   // 波特率:9600 / 19200 / 115200 等
#define TEST_PARITY     'N'      // 'N'=无校验, 'E'=偶校验, 'O'=奇校验
#define TEST_STOP_BITS  1        // 1 或 2
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

✅ 修改此区域即可同步应用于 发送接收 模式,避免参数不一致。

3.2 中断机制

  • 使用 UART 接收中断 + 环形缓冲区,防止高速通信丢字节
  • 中断服务程序(ISR)仅负责 入队,主循环负责 帧解析与 CRC 校验

3.3 验证原则

  • 自动判断收发是否一致
  • 只输出有效帧计数(接收模式)或 发送完成提示(发送模式)
  • 由用户自行比对核心板打印值 与 上位机串口助手的“发送/接收计数”

四、完整 C 语言代码(可直接编译运行)

#include <c6x.h>        // 包含 TI C6000 系列 DSP 的寄存器和中断定义
#include <stdio.h>      // 标准输入输出库(用于 printf)

// ==================== 用户配置区(只需修改此处即可切换测试参数)====================
#define TEST_BAUD       115200   // 通信波特率:可设为 9600、19200、115200 等
#define TEST_PARITY     'N'      // 奇偶校验:'N'=无校验, 'E'=偶校验, 'O'=奇校验
#define TEST_STOP_BITS  1        // 停止位数:1 或 2
// ===================================================================================

// UART1 寄存器基地址(TMS320C6678 EVM 上 UART1 的物理地址)
#define UART1_BASE      0x02530C00

// UART 寄存器映射(偏移地址参考 TI 手册)
#define UART_RBR        (*(volatile unsigned char*)(UART1_BASE + 0x00))  // 接收缓冲寄存器(只读)
#define UART_THR        (*(volatile unsigned char*)(UART1_BASE + 0x00))  // 发送保持寄存器(只写)
#define UART_IER        (*(volatile unsigned char*)(UART1_BASE + 0x04))  // 中断使能寄存器
#define UART_IIR        (*(volatile unsigned char*)(UART1_BASE + 0x08))  // 中断标识寄存器
#define UART_LCR        (*(volatile unsigned char*)(UART1_BASE + 0x0C))  // 行控制寄存器
#define UART_LSR        (*(volatile unsigned char*)(UART1_BASE + 0x14))  // 行状态寄存器
#define UART_DLL        (*(volatile unsigned char*)(UART1_BASE + 0x00))  // 波特率除数低字节(DLAB=1 时)
#define UART_DLH        (*(volatile unsigned char*)(UART1_BASE + 0x04))  // 波特率除数高字节(DLAB=1 时)

// UART1 对应的 DSP 中断号(根据 EVM 板实际连接,此处假设为 13)
#define UART1_INT_NUM   13

// 接收环形缓冲区(防止高速通信时 ISR 被主循环阻塞导致丢字节)
#define RX_BUF_SIZE     4096                    // 缓冲区大小(建议 ≥ 预期突发数据量)
volatile uint8_t rx_buffer[RX_BUF_SIZE];       // 环形缓冲区数组
volatile uint16_t rx_head = 0;                 // 写入指针(由 ISR 更新)
volatile uint16_t rx_tail = 0;                 // 读取指针(由主循环更新)

// 函数声明(提前声明,便于组织代码结构)
void uart1_init(unsigned int baud, char parity, int stop_bits);  // 初始化 UART
void uart1_putc(uint8_t c);                                      // 发送单个字节
int uart1_getc_nonblock(void);                                   // 非阻塞接收单个字节(用于菜单)
void delay_100ms(void);                                          // 粗略延时约 100 毫秒
uint8_t calc_checksum(const uint8_t *buf, int len);              // 计算累加和 CRC
int receive_frame(uint8_t *frame, int frame_len);                // 从缓冲区提取完整帧
void run_receive_test(void);                                     // 运行接收测试模式
void run_send_test(void);                                        // 运行发送测试模式

// ==================== 主函数:提供交互式菜单 ====================
int main(void) {
    int choice = 0;  // 存储用户选择

    // 初始化 UART(用于打印菜单和接收用户输入)
    uart1_init(TEST_BAUD, TEST_PARITY, TEST_STOP_BITS);

    // 打印欢迎信息和当前配置(全部使用中文提示)
    printf("\n=== TMS320C6678 RS422 UART 通信测试程序 ===\n");
    printf("当前配置: %d 波特率, ", TEST_BAUD);
    if (TEST_PARITY == 'N') printf("无校验, ");
    else if (TEST_PARITY == 'E') printf("偶校验, ");
    else if (TEST_PARITY == 'O') printf("奇校验, ");
    printf("%d 位停止位\n", TEST_STOP_BITS);
    printf("\n请选择测试模式:\n");
    printf("  1. 核心板接收模式(监听上位机发送的数据)\n");
    printf("  2. 核心板发送模式(向上位机发送测试数据)\n");
    printf("请输入 1 或 2 并回车: ");

    // 等待用户通过串口输入 '1' 或 '2'
    while (1) {
        choice = uart1_getc_nonblock();  // 尝试非阻塞读取一个字符
        if (choice == '1' || choice == '2') {  // 如果是有效选择,跳出循环
            break;
        }
    }

    // 回显用户的选择
    printf("%c\n", choice);

    // 根据用户选择进入对应测试模式
    if (choice == '1') {
        printf("\n>>> 正在进入【接收模式】<<<\n");
        run_receive_test();  // 运行接收测试
    } else {
        printf("\n>>> 正在进入【发送模式】<<<\n");
        run_send_test();     // 运行发送测试
    }

    // 测试结束提示
    printf("\n=== 测试已完成 ===\n");
    return 0;
}

// ==================== 接收测试模式实现 ====================
void run_receive_test(void) {
    const int FRAME_LEN = 17;           // 每帧固定 17 字节
    uint8_t frame[17];                  // 临时存储一帧数据
    int valid_count = 0;                // 有效帧计数器(CRC 校验通过)

    printf("正在等待上位机发送 1000 帧数据...\n");

    // 持续接收,直到收到 1000 个有效帧
    while (valid_count < 1000) {
        // 尝试从环形缓冲区中提取一整帧
        if (receive_frame(frame, FRAME_LEN) == 0) {
            // 计算前 16 字节的 CRC(累加和)
            uint8_t expected_crc = calc_checksum(frame, 16);
            // 比较计算出的 CRC 与帧中第 17 字节是否一致
            if (expected_crc == frame[16]) {
                valid_count++;  // 校验成功,计数加一
                // 每收到 100 帧打印一次进度(避免刷屏)
                if (valid_count % 100 == 0) {
                    printf("已成功接收有效帧: %d\n", valid_count);
                }
            }
            // 注意:即使 CRC 错误,也继续处理下一帧(不丢弃缓冲区)
        }
    }

    // 打印最终结果(关键输出,供比对)
    printf("\n>>> 总共接收到的有效帧数量: %d <<<\n", valid_count);
    printf("请将此数字与上位机串口助手显示的【已发送帧数】进行比对(应为 1000)。\n");
}

// ==================== 发送测试模式实现 ====================
void run_send_test(void) {
    // 定义标准测试帧:前 16 字节为 0x01~0x10,第 17 字节留空(后续填 CRC)
    uint8_t frame[17] = {
        0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
        0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x00
    };
    // 计算并填充 CRC(累加和)
    frame[16] = calc_checksum(frame, 16);  // 实际值为 0x88

    printf("开始发送 1000 帧数据(每帧间隔 100 毫秒)...\n");

    // 循环发送 1000 帧
    for (int i = 0; i < 1000; i++) {
        // 逐字节发送整帧
        for (int j = 0; j < 17; j++) {
            uart1_putc(frame[j]);
        }
        delay_100ms();  // 每帧间隔 100ms
    }

    // 发送完成提示
    printf("\n>>> 发送完成:共发送 1000 帧!<<<\n");
    printf("请检查上位机串口助手的【接收计数】是否为 1000。\n");
}

// ==================== 工具函数实现 ====================

// 初始化 UART 控制器
void uart1_init(unsigned int baud, char parity, int stop_bits) {
    unsigned int divisor;        // 波特率除数
    unsigned char lcr_val = 0x03; // 默认:8 位数据(0x03 = 0b00000011)

    UART_IER = 0;               // 先关闭所有 UART 中断
    UART_LCR |= 0x80;           // 设置 DLAB=1,允许访问 DLL/DLH

    // 计算波特率除数(假设 UART 输入时钟为 100 MHz)
    divisor = 100000000 / (16 * baud);
    UART_DLL = divisor & 0xFF;        // 写入低字节
    UART_DLH = (divisor >> 8) & 0xFF; // 写入高字节

    // 配置停止位:0 = 1 位,1 = 2 位
    if (stop_bits == 2) {
        lcr_val |= 0x04;  // 设置 STOP 位(bit2)
    }

    // 配置奇偶校验
    if (parity == 'E') {
        lcr_val |= 0x08;          // 启用偶校验(bit3=1, bit4=0)
    } else if (parity == 'O') {
        lcr_val |= 0x0A;          // 启用奇校验(bit3=1, bit4=1 → 0x08 | 0x02)
    }
    // 若 parity == 'N',则不设置校验位(默认)

    UART_LCR = lcr_val;         // 写入 LCR(同时自动清除 DLAB=0)
    UART_IER = 0x01;            // 使能接收数据可用中断(RDA)
    IER |= (1U << UART1_INT_NUM); // 使能 DSP 层面对应的中断
}

// 发送单个字节(阻塞式,等待发送缓冲区空)
void uart1_putc(uint8_t c) {
    while ((UART_LSR & 0x20) == 0); // 等待 THRE 位(bit5)为 1,表示可发送
    UART_THR = c;                   // 写入发送缓冲寄存器
}

// 非阻塞方式读取一个字节(用于菜单选择,不等待)
int uart1_getc_nonblock(void) {
    if (rx_head != rx_tail) {  // 如果缓冲区有数据
        uint8_t c = rx_buffer[rx_tail];           // 读取数据
        rx_tail = (rx_tail + 1) % RX_BUF_SIZE;    // 移动读指针(环形)
        return c;
    }
    return -1;  // 无数据可读,返回 -1
}

// 粗略延时函数(约 100 毫秒,基于空循环,实际时间依赖 CPU 频率)
void delay_100ms(void) {
    volatile int i, j;  // volatile 防止编译器优化掉空循环
    for (i = 0; i < 100; i++)
        for (j = 0; j < 30000; j++);
}

// 计算累加和 CRC(简单校验,mod 256)
uint8_t calc_checksum(const uint8_t *buf, int len) {
    uint8_t sum = 0;
    for (int i = 0; i < len; i++) {
        sum += buf[i];  // 逐字节累加
    }
    return sum;  // 自动 mod 256(因 uint8_t 范围为 0~255)
}

// 从环形缓冲区中提取一整帧(处理粘包)
int receive_frame(uint8_t *frame, int frame_len) {
    static uint8_t temp[256];  // 静态临时缓冲区(跨调用保持状态)
    static int idx = 0;        // 当前已缓存字节数

    // 从环形缓冲区中尽可能多地读取数据到 temp
    while (rx_tail != rx_head && idx < frame_len) {
        temp[idx++] = rx_buffer[rx_tail];
        rx_tail = (rx_tail + 1) % RX_BUF_SIZE;
    }

    // 如果已收集到一整帧
    if (idx >= frame_len) {
        // 拷贝完整帧到输出 buffer
        for (int i = 0; i < frame_len; i++) {
            frame[i] = temp[i];
        }
        // 将剩余未处理的数据前移(简单处理粘包)
        for (int i = frame_len; i < idx; i++) {
            temp[i - frame_len] = temp[i];
        }
        idx -= frame_len;  // 更新剩余字节数
        return 0;  // 成功返回
    }
    return -1;  // 帧未满,返回失败
}

// ==================== UART 接收中断服务程序 ====================
interrupt void uart1_isr(void) {
    uint8_t iir = UART_IIR;  // 读取中断标识寄存器

    // 判断是否有未处理的中断(bit0=0 表示有中断)
    if ((iir & 0x01) == 0) {
        // 判断是否为“接收数据可用”中断(IIR[3:1] = 010)
        if ((iir & 0x06) == 0x04) {
            uint8_t data = UART_RBR;  // 读取接收到的字节(自动清除中断)
            rx_buffer[rx_head] = data; // 存入环形缓冲区
            rx_head = (rx_head + 1) % RX_BUF_SIZE; // 移动写指针
        }
    }
    // 清除 DSP 层面的中断标志(必须!)
    ICR = (1U << UART1_INT_NUM);
}

五、测试执行流程

步骤 1:配置参数

修改代码顶部的 TEST_BAUDTEST_PARITYTEST_STOP_BITS,例如:

#define TEST_BAUD       19200
#define TEST_PARITY     'E'
#define TEST_STOP_BITS  2

步骤 2:编译烧录

  • 使用 TI Code Composer Studio (CCS) 编译
  • 加载到 TMS320C6678 EVM 开发板

步骤 3:连接上位机

  • 将 RS422 转 USB 模块 连接至 PC
  • 打开 串口调试助手,设置相同参数(波特率/校验/停止位)

步骤 4:运行测试

场景 A:接收模式(验证核心板接收能力)
  1. 上位机启动自动发送脚本:每 100ms 发 1 帧,共 1000 帧
  2. 核心板上电,选择 模式 1
  3. 程序结束后,查看打印:
    >>> TOTAL VALID FRAMES RECEIVED: 1000 <<<
  4. 比对:该数值应等于上位机“已发送帧数”
场景 B:发送模式(验证核心板发送能力)
  1. 核心板上电,选择 模式 2
  2. 程序开始发送 1000 帧(耗时 ≈ 100 秒)
  3. 上位机串口助手显示 接收计数
  4. 比对:助手计数应为 1000

六、典型测试用例表

测试编号波特率奇偶校验停止位模式预期结果
TC-019600None1Receive收到 1000 帧
TC-0219200Even1Receive收到 1000 帧
TC-03115200Odd2Receive收到 1000 帧
TC-04115200None1Send上位机收到 1000 帧

✅ 每次测试前 复位核心板,确保状态干净。


七、注意事项

  1. 时钟源确认:代码假设 UART 时钟为 100 MHz,若实际不同,请调整 divisor 计算。
  2. 中断映射UART1_INT_NUM = 13 为示例,需根据 C6678 EVM 实际中断分配修改。
  3. 延时精度delay_100ms() 为粗略延时,高精度场景建议使用 Timer
  4. 缓冲区溢出:若通信速率极高,可增大 RX_BUF_SIZE
  5. CRC 一致性:确保上位机使用 相同累加和算法,否则校验恒失败。

八、结论

本方案提供了一套 简洁、可靠、可复现 的 RS422 UART 通信测试体系,完全满足以下需求:

  • ✅ 用户交互选择 发送/接收模式
  • ✅ 接收模式 仅输出有效帧计数
  • ✅ 发送模式 仅发送,不判断
  • ✅ 比对 核心板日志 与 上位机计数
  • ✅ 支持 任意波特率、校验、停止位组合

可直接用于 TMS320C6678 平台的产品验证、出厂测试或通信调试

🌟 TMS320C6678 核心板串口通信测试程序 —— 初学者完全指南

适合人群:刚学 C 语言、第一次接触 DSP(数字信号处理器)、想用 TMS320C6678 做串口通信的新手
目标:让你看懂、能改、会用这段代码,完成一次完整的“发送/接收”测试!


一、我们到底要做什么?

想象你在调试一块工业控制板(比如 TI 的 TMS320C6678 开发板),它通过 RS422 接口 和上位机(你的电脑)通信。

你希望验证:

  • 当电脑发数据给核心板时,核心板能不能正确收到
  • 当核心板发数据给电脑时,电脑能不能完整接收到

但你不希望程序自动判断“对不对”,而是你自己看着串口助手数一数——这就是“人工比对”。

所以,我们需要一个程序,它能做到两件事:

  1. 接收模式:核心板等电脑发 1000 帧数据,每帧校验一下,统计成功收到多少帧
  2. 发送模式:核心板自己发 1000 帧数据,让电脑去数。

而且,启动时让你按 1 或 2 来选择模式,就像点菜单一样简单!


二、通信规则:什么是“一帧数据”?

为了测试方便,我们约定每帧数据长这样(共 17 个字节):

字节位置内容示例值(十六进制)
第 1~16 字节固定数据01 02 03 ... 10
第 17 字节校验码(CRC)88

校验码怎么算?
把前 16 个字节加起来:
0x01 + 0x02 + ... + 0x10 = 0x88
所以第 17 字节必须是 0x88,才算“这帧数据没出错”。


三、硬件怎么连?

你需要:

  • 一块 TMS320C6678 EVM 开发板
  • 一个 RS422 转 USB 模块(淘宝几十元)
  • 一根 USB 线连到电脑

接线很简单:

  • 核心板的 UART1_TX → RS422 模块的 DI
  • 核心板的 UART1_RX → RS422 模块的 RO
  • GND 接 GND(非常重要!不然通信会乱)

⚠️ 注意:RS422 是差分信号,抗干扰强,适合工业环境。但我们这里只用它的“全双工”模式,当成普通串口用。


四、程序整体结构(像搭积木)

我们的程序分成几块:

  1. 用户配置区:你改几个数字,就能换波特率、校验方式。
  2. 寄存器定义:告诉 C 语言“UART 寄存器在哪”。
  3. 中断 + 缓冲区:防止高速通信丢字节。
  4. 主函数:打印菜单,让你选“发”还是“收”。
  5. 两个测试函数
    • run_receive_test():收数据、校验、计数
    • run_send_test():发 1000 帧
  6. 工具函数:发字节、算 CRC、延时等。

五、逐部分详解(带注释版代码)

🔧 第 1 步:配置你的通信参数(你唯一需要改的地方!)

#define TEST_BAUD       115200   // 波特率:9600 / 19200 / 115200 都行
#define TEST_PARITY     'N'      // 'N'=无校验, 'E'=偶校验, 'O'=奇校验
#define TEST_STOP_BITS  1        // 停止位:1 或 2

💡 新手提示
如果你用的是普通串口助手,默认就是 115200, 无校验, 1 位停止位,所以这里不用改。
如果你要测“奇校验”,就改成 'O',然后上位机也要同步设置!


🧱 第 2 步:告诉 C 语言“UART 寄存器在哪”


c

编辑

#define UART1_BASE      0x02530C00  // TMS320C6678 的 UART1 基地址
#define UART_RBR        (*(volatile unsigned char*)(UART1_BASE + 0x00))  // 读数据用
#define UART_THR        (*(volatile unsigned char*)(UART1_BASE + 0x00))  // 发数据用
// ... 其他寄存器略

📌 解释
DSP 的外设(如 UART)是通过“内存地址”控制的。
*(volatile unsigned char*) 的意思是:“这个地址里的值可能会被硬件改变,请别优化掉!”
这是嵌入式 C 的常见写法。


🔄 第 3 步:用“环形缓冲区”防丢字节

#define RX_BUF_SIZE     4096
volatile uint8_t rx_buffer[RX_BUF_SIZE];  // 存接收的数据
volatile uint16_t rx_head = 0;            // 写指针(中断用)
volatile uint16_t rx_tail = 0;            // 读指针(主程序用)

为什么需要缓冲区?
如果主程序在忙(比如算 CRC),而新数据又来了,UART 寄存器只能存 1 个字节,下一个字节就会丢失
所以我们用中断把数据先存到大数组里,主程序慢慢处理——这就是“环形缓冲区”。


🎮 第 4 步:主函数 —— 像点菜一样选模式

int main(void) {
    printf("\n请选择测试模式:\n");
    printf("  1. 核心板接收模式\n");
    printf("  2. 核心板发送模式\n");
    printf("请输入 1 或 2: ");

    while (1) {
        choice = uart1_getc_nonblock();  // 尝试读一个字符
        if (choice == '1' || choice == '2') break;
    }

    if (choice == '1') run_receive_test();
    else run_send_test();

    return 0;
}

关键点

  • printf 会通过 UART 发送到你的串口助手(显示中文!)
  • uart1_getc_nonblock() 是非阻塞读取,不会卡死程序
  • 用户输入 1 或 2 后,程序才继续

📥 第 5 步:接收模式 —— 数一数收到多少好帧

void run_receive_test(void) {
    int valid_count = 0;
    while (valid_count < 1000) {
        if (receive_frame(frame, 17) == 0) {       // 收到一整帧
            uint8_t crc = calc_checksum(frame, 16); // 算前16字节的和
            if (crc == frame[16]) {                 // 和第17字节比对
                valid_count++;                      // 对了就+1
            }
        }
    }
    printf(">>> 总共接收到的有效帧数量: %d <<<\n", valid_count);
}

📌 重点

  • 不管帧内容是不是 01 02...10,只要 长度对 + CRC 对,就算有效!
  • 最后打印一个数字,你拿去和电脑串口助手的“发送计数”比对。

📤 第 6 步:发送模式 —— 主动发 1000 帧

void run_send_test(void) {
    uint8_t frame[17] = {0x01, 0x02, ..., 0x10, 0x00};
    frame[16] = calc_checksum(frame, 16); // 自动填 0x88

    for (int i = 0; i < 1000; i++) {
        for (int j = 0; j < 17; j++) {
            uart1_putc(frame[j]);  // 逐字节发送
        }
        delay_100ms();  // 每帧间隔 100ms
    }
    printf(">>> 发送完成:共发送 1000 帧!<<<\n");
}

💡 注意
delay_100ms() 是用空循环实现的,不精确但够用。
实际产品中建议用硬件定时器。


⚙️ 第 7 步:中断服务程序(ISR)——默默收数据

interrupt void uart1_isr(void) {
    if ((UART_IIR & 0x06) == 0x04) {  // 是“有数据来了”中断?
        uint8_t data = UART_RBR;       // 读走数据(否则中断停不下来!)
        rx_buffer[rx_head] = data;     // 存到缓冲区
        rx_head = (rx_head + 1) % RX_BUF_SIZE; // 环形移动
    }
    ICR = (1U << UART1_INT_NUM); // 清除 DSP 中断标志(必须!)
}

⚠️ 重要原则
ISR 要越快越好!所以只做“存数据”这一件事,复杂处理留给主循环。


六、怎么用?一步一步操作

步骤 1:准备上位机

  • 安装串口助手(如 SSCOMAccessPort
  • 插上 RS422 转 USB 模块,记下 COM 号(比如 COM5)

步骤 2:编译烧录

  • 用 TI Code Composer Studio (CCS) 打开工程
  • 把上面代码保存为 main.c
  • 编译 → 加载到 C6678 开发板

步骤 3:运行测试

场景 A:测“接收能力”
  1. 串口助手设置:115200, 无校验, 1 停止位
  2. 打开串口,看到菜单
  3. 在串口助手的“发送区”输入 1 并发送
  4. 核心板进入接收模式
  5. 另开一个串口发送工具(或脚本),每 100ms 发一帧 01 02 ... 10 88,共 1000 次
  6. 核心板打印:总共接收到的有效帧数量: 1000
  7. ✅ 你确认:电脑发了 1000,核心板收到了 1000 → 通信 OK!
场景 B:测“发送能力”
  1. 串口助手设置同上
  2. 输入 2 进入发送模式
  3. 核心板开始发数据
  4. 串口助手会显示一堆十六进制数据,并有“接收计数”
  5. 等 100 秒后,计数应为 1000
  6. ✅ 通信 OK!

七、常见问题解答(FAQ)

Q1:为什么用中断,不用 while 循环查 UART 状态?

A:轮询(polling)会占用 CPU,且高速时容易丢字节。中断更高效可靠。

Q2:CRC 为什么不用标准 CRC16?

A:本测试只需简单校验,累加和足够。且上位机容易实现。

Q3:中文 printf 能正常显示吗?

A:只要串口助手用 UTF-8 或 GBK 编码(大多数默认支持),中文就能正常显示。

Q4:波特率不准怎么办?

A:检查 divisor = 100000000 / (16 * baud) 中的 100000000 是否等于你板子的 UART 时钟频率。


八、总结

你现在拥有了一个:

  • ✅ 交互式(可选模式)
  • ✅ 带中文提示
  • ✅ 防丢字节(中断+缓冲区)
  • ✅ 支持任意串口参数
  • ✅ 便于人工验证

的完整 RS422 测试程序!

即使你是第一次接触 DSP 或嵌入式开发,只要按步骤操作,也能成功跑起来。理解每一行代码的作用,比直接复制更重要

🎯 下一步建议
尝试修改 TEST_BAUD 为 9600,再跑一次测试,看看低速下是否更稳定!

祝你调试顺利,早日成为嵌入式高手!

太好了!你已经理解了 UART 和 RS422,现在我们来认识另外两种在嵌入式系统中极其常用的串行通信协议:SPII²C(读作 “I-squared-C” 或 “I-two-C”)

下面这篇讲解专为初学者设计,用生活化比喻、清晰对比和实际例子,帮你彻底搞懂它们是什么、怎么工作、有什么区别,以及什么时候该用哪个。


🌟 SPI 与 I²C 完全入门指南(给嵌入式新手)

适合人群:刚学单片机、第一次接触传感器/存储器通信的新手
目标:读完后你能回答——

  • SPI 和 I²C 是用来干什么的?
  • 它们需要几根线?谁更快?谁更省引脚?
  • 为什么我的 OLED 屏用 I²C,而 SD 卡用 SPI?

一、先说结论(一句话记住)

  • SPI 像 工厂流水线:高速、全双工、一对一“点名”通信。
  • I²C 像 会议室圆桌讨论:低速、半双工、靠“地址”喊人。

✅ 它们都是 同步串行通信协议(有主设备提供时钟),用于芯片之间短距离通信,比如:

  • 单片机 ↔ 温度传感器
  • MCU ↔ EEPROM 存储器
  • 主控 ↔ OLED 显示屏

二、SPI 是什么?—— “高速流水线”

1. 全称

Serial Peripheral Interface
(串行外设接口)

2. 核心特点

特性说明
同步由主设备提供时钟(SCLK),收发双方严格同步
全双工能同时发送和接收数据(像对讲机)
主从架构1 个主设备(Master),可带多个从设备(Slave)
无地址靠“片选线”(CS)直接选中某个从设备

3. 需要几根线?

最少 4 根(标准 SPI):

信号线全称方向(从主设备看)作用
SCLKSerial Clock输出时钟信号,由主设备产生
MOSIMaster Out Slave In输出主设备 → 从设备 的数据线
MISOMaster In Slave Out输入从设备 → 主设备 的数据线
CS/SSChip Select / Slave Select输出片选信号,低电平有效

⚠️ 关键:每增加一个从设备,就要多一根 CS 线
比如接 3 个传感器,就需要 3 根 CS 线(共 6 根线:SCLK+MOSI+MISO+CS1+CS2+CS3)

4. 工作流程(以读取 Flash 为例)

  1. 主设备拉低 Flash 的 CS 线 → “你,准备干活!”
  2. 主设备通过 SCLK 打拍子,同时在 MOSI 发送“读命令”
  3. Flash 在 MISO 上返回数据(同时 MOSI 可继续发地址)
  4. 通信结束,主设备拉高 CS → “干完了,休息!”

5. 优点 vs 缺点

优点缺点
✅ 速度极快(可达 10~100 Mbps)❌ 引脚多(每从机需独立 CS)
✅ 协议简单,无复杂握手❌ 不支持多主设备
✅ 全双工,效率高❌ 通信距离短(< 0.5 米)

6. 典型应用

  • SD 卡、Flash 存储器(高速读写)
  • TFT/OLED 显示屏(快速刷屏)
  • 高速 ADC/DAC(模数转换器)

三、I²C 是什么?—— “地址喊话会议”

1. 全称

Inter-Integrated Circuit
(集成电路间总线)

2. 核心特点

特性说明
同步由主设备提供时钟(SCL)
半双工同一时间只能单向传输(像对讲机按住说话)
多主多从支持多个主设备(但通常只用一个)
地址寻址每个从设备有唯一地址(7 位或 10 位)

3. 需要几根线?

只需要 2 根

信号线全称作用
SDASerial Data双向数据线(所有设备共享)
SCLSerial Clock时钟线(由主设备控制)

优势:无论接多少设备,都只用这 2 根线!
(当然,每个设备还需接电源和地)

4. 工作流程(以读取温度传感器为例)

  1. 主设备发出 起始信号(SCL 高时,SDA 从高→低)
  2. 主设备发送 从设备地址 + 读写位(如 0x90 表示写)
  3. 对应地址的从设备 拉低 SDA 回应 ACK(确认收到)
  4. 主设备发送寄存器地址(如 0x00
  5. 主设备再发 重复起始,然后发 0x91(读)
  6. 传感器通过 SDA 返回温度值
  7. 主设备发 停止信号(SCL 高时,SDA 从低→高)

5. 关键机制

  • 起始/停止条件:界定一次通信的开始和结束
  • ACK/NACK:每传 1 字节,接收方必须回应确认
  • 开漏输出 + 上拉电阻:避免多个设备同时驱动总线冲突

💡 为什么需要上拉电阻?
因为 I²C 设备只能“拉低”总线,不能“输出高电平”。
所以靠外部 上拉电阻 把 SDA/SCL 拉到高电平,实现“线与”逻辑。

6. 优点 vs 缺点

优点缺点
✅ 引脚极少(仅 2 根线)❌ 速度较慢(标准 100kbps,快模式 400kbps)
✅ 支持多设备(靠地址区分)❌ 协议复杂(起始/停止/ACK/仲裁)
✅ 适合连接大量低速传感器❌ 总线负载有限(设备太多会失败)

7. 典型应用

  • 温湿度传感器(DHT20, SHT30)
  • 实时时钟(DS1307)
  • 小容量 EEPROM(AT24C02)
  • IO 扩展芯片(PCF8574)

四、SPI vs I²C 终极对比表

特性SPII²C
线数≥4 根(SCLK, MOSI, MISO, CS×N)仅 2 根(SCL, SDA)
速度极快(10~100 Mbps)较慢(100kbps ~ 3.4Mbps)
通信方式全双工(同时收发)半双工(同一时间单向)
设备选择硬件片选(CS 拉低)软件地址(广播地址)
主从结构单主多从多主多从(支持仲裁)
协议复杂度简单(无起始/停止/ACK)复杂(需处理 ACK、仲裁等)
引脚占用多(每从机需 1 根 CS)极少(所有设备共享 2 线)
典型应用SD卡、显示屏、高速ADC传感器、RTC、EEPROM

五、生活化比喻(加深理解)

🏭 SPI = 工厂流水线

  • 主设备是 厂长
  • SCLK 是 流水线节拍器
  • MOSI 是 厂长发指令:“装轮胎!”
  • MISO 是 工人回话:“装好了!”
  • CS 是 点名器:“3号工位,准备干活!”

✅ 优点:效率高、速度快
❌ 缺点:工位越多,点名线越长(引脚多)

🪑 I²C = 圆桌会议

  • 主设备是 主持人
  • SCL 是 敲钟:“下一个发言!”
  • SDA 是 唯一麦克风(所有人共用)
  • 每个设备有 工号(地址)
  • 主持人喊:“0x50号,请报温度!”
  • 对应设备拿起麦克风回答,其他人沉默

✅ 优点:节省麦克风(引脚少)
❌ 缺点:不能抢话,速度慢


六、如何选择?—— 一句话口诀

🔥 高速选 SPI,设备多选 I²C

  • 你要连 OLED 屏、SD 卡、Flash? → 用 SPI
  • 你要连 10 个温湿度传感器、RTC、EEPROM? → 用 I²C
  • 引脚紧张? → 优先 I²C
  • 要传大量数据(如图像)? → 必须 SPI

七、常见问题解答(FAQ)

Q1:SPI 有没有地址?

A:没有!靠 CS 线硬件选中。所以不能“广播”。

Q2:I²C 地址冲突怎么办?

A:每个 I²C 芯片出厂有固定地址,部分支持通过引脚修改(如 A0/A1)。务必查 datasheet!

Q3:能不能用 GPIO 模拟 SPI/I²C?

A:可以!叫“软件模拟”(bit-banging),但速度慢。硬件 SPI/I²C 更高效。

Q4:为什么 I²C 需要上拉电阻,SPI 不需要?

A:I²C 用开漏输出,靠上拉提供高电平;SPI 用推挽输出,能自己输出高低电平。


八、总结

协议本质适用场景
UART异步串口(无时钟)调试打印、连蓝牙/WiFi 模块
SPI同步高速全双工显示屏、存储器、高速外设
I²C同步低速地址总线传感器、RTC、多设备低速通信

✅ 记住:

  • UART:连电脑、调程序
  • SPI:要速度、不怕线多
  • I²C:省引脚、接一堆小设备

现在,你已经掌握了嵌入式三大串行通信协议的核心思想!🎉
下次看到电路板上的传感器,你就知道它大概率是用 I²C 还是 SPI 了!


💡 动手建议
用 Arduino 或 STM32 开发板,分别试试:

  • 用 I²C 读取一个温湿度传感器(如 DHT20)
  • 用 SPI 驱动一个 OLED 屏幕

眼见为实,一试就懂!

他们的代码有什么区别?

Qwen3-Max

这是一个非常关键的问题!UART、SPI 和 I²C 的代码区别,本质上反映了它们通信机制的根本不同。下面我将用 清晰的对比 + 简化代码示例(基于 STM32 风格伪代码),从初始化、发送、接收三个维度,为你逐行解析它们的代码差异,并解释为什么这样写

前提说明

  • 所有代码均为简化教学版,省略错误处理和寄存器细节
  • 假设使用硬件外设(非 GPIO 模拟)
  • 目标平台:STM32(但逻辑适用于大多数 MCU)

一、核心差异速览表

特性UARTSPII²C
同步/异步异步(无时钟线)同步(主设备提供 SCLK)同步(主设备提供 SCL)
数据线TX, RX(独立)MOSI, MISO(独立)SDA(双向共享)
设备选择无(点对点)CS 片选(硬件控制)地址(软件发送)
通信流程发就完事先拉低 CS → 发数据 → 拉高 CS起始 → 地址 → 数据 → 停止
典型函数uart_send(data)spi_write(addr, data)i2c_write(slave_addr, reg, data)

二、代码对比详解

📌 场景设定

我们要向一个外设写入一个字节 0x55

  • UART:直接发给串口助手(点对点)
  • SPI:写入 Flash 芯片的某个寄存器
  • I²C:写入 EEPROM 的某个地址

1. 初始化代码对比

🔹 UART 初始化

c

编辑

void uart_init(void) {
    // 1. 使能时钟(GPIO + USART)
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    
    // 2. 配置 TX/RX 引脚为复用推挽
    GPIOA->CRH &= ~0xFF0;          // 清除 PA9, PA10 配置
    GPIOA->CRH |= 0x4B0;           // PA9=AF_PP, PA10=IN_FLOATING
    
    // 3. 设置波特率(115200)、8N1
    USART1->BRR = 0x271;           // 72MHz / 16 / 115200 ≈ 39 = 0x27
    USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE; // 使能收发
}

关键点

  • 只需配置波特率(双方必须一致)
  • 无时钟线,所以不涉及 SCLK/SCL
  • 引脚方向固定:TX 输出,RX 输入

🔹 SPI 初始化

c

编辑

void spi_init(void) {
    // 1. 使能时钟(GPIO + SPI)
    RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
    
    // 2. 配置 SCLK/MOSI/MISO 为复用推挽(PA5/6/7)
    GPIOA->CRL &= ~0xFFF00000;
    GPIOA->CRL |= 0xB4400000;      // SCK=AF_PP, MISO=IN_FLOAT, MOSI=AF_PP
    
    // 3. 配置 CS 引脚(PA4)为普通推挽输出
    GPIOA->CRL &= ~0xF0000000;
    GPIOA->CRL |= 0x30000000;      // PA4=OUT_PP
    GPIOA->BSRR = GPIO_BSRR_BS4;   // 默认拉高(不选中)
    
    // 4. 配置 SPI 主机模式、时钟极性等
    SPI1->CR1 = SPI_CR1_MSTR |     // 主机模式
                 SPI_CR1_BR_1 |    // 分频 fPCLK/8
                 SPI_CR1_SSM |     // 软件管理 NSS
                 SPI_CR1_SSI;      // 内部 NSS 高
    SPI1->CR1 |= SPI_CR1_SPE;      // 使能 SPI
}

关键点

  • 必须配置 SCLK 时钟参数(分频、极性 CPOL/相位 CPHA)
  • CS 引脚由 GPIO 控制(不是 SPI 外设自动管)
  • MOSI/MISO 方向固定

🔹 I²C 初始化

c

编辑

void i2c_init(void) {
    // 1. 使能时钟(GPIO + I2C)
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
    
    // 2. 配置 SCL/SDA 为开漏输出 + 上拉
    GPIOB->CRH &= ~0xFF000000;
    GPIOB->CRH |= 0x88000000;      // PB6/7 = AF_OD (开漏)
    // 注意:外部需接上拉电阻!
    
    // 3. 配置 I2C 时钟频率(100kHz)
    I2C1->CR2 = 36;                // PCLK1 = 36MHz
    I2C1->CCR = 180;               // 标准模式: T_low = T_high = 5us
    I2C1->TRISE = 37;              // 最大上升时间 = 1000ns
    
    // 4. 使能 I2C
    I2C1->CR1 |= I2C_CR1_PE;
}

关键点

  • 必须配置时钟频率(通过 CCR/TRISE 计算)
  • GPIO 必须设为 开漏(Open-Drain)
  • 依赖外部上拉电阻(代码无法体现,但硬件必须有)

2. 发送数据代码对比

🔹 UART 发送(最简单)

c

编辑

void uart_send(uint8_t data) {
    while (!(USART1->SR & USART_SR_TXE)); // 等待发送缓冲区空
    USART1->DR = data;                    // 写入数据,自动发送
}
// 使用:uart_send(0x55);

特点

  • 无需握手,写入即发
  • 无目标地址(默认发给对方)

🔹 SPI 发送(需控制 CS)

c

编辑

void spi_write(uint8_t data) {
    while (!(SPI1->SR & SPI_SR_TXE)); // 等待发送缓冲区空
    SPI1->DR = data;                  // 写入数据
    while (SPI1->SR & SPI_SR_BSY);    // 等待传输完成
}

// 完整写操作(以 Flash 为例)
void flash_write_enable(void) {
    GPIOA->BRR = GPIO_BRR_BR4;  // 拉低 CS(选中 Flash)
    spi_write(0x06);            // 发送 "Write Enable" 命令
    GPIOA->BSRR = GPIO_BSRR_BS4; // 拉高 CS(结束通信)
}
// 使用:flash_write_enable();

特点

  • 必须手动控制 CS(拉低开始,拉高结束)
  • 全双工:每发一个字节,MISO 也会返回一个字节(即使不用)

🔹 I²C 发送(需起始/地址/ACK/停止)

c

编辑

void i2c_start(void) {
    I2C1->CR1 |= I2C_CR1_START;        // 产生起始条件
    while (!(I2C1->SR1 & I2C_SR1_SB)); // 等待起始完成
}

void i2c_stop(void) {
    I2C1->CR1 |= I2C_CR1_STOP;         // 产生停止条件
}

uint8_t i2c_write_byte(uint8_t data) {
    I2C1->DR = data;                   // 写入数据
    while (!(I2C1->SR1 & I2C_SR1_BTF)); // 等待传输完成
    return I2C1->SR1 & I2C_SR1_AF ? 1 : 0; // 检查 ACK(0=ACK, 1=NACK)
}

// 完整写操作(以 EEPROM 为例)
void eeprom_write(uint8_t slave_addr, uint8_t reg, uint8_t data) {
    i2c_start();
    i2c_write_byte(slave_addr << 1);   // 发送 7 位地址 + 写标志(bit0=0)
    i2c_write_byte(reg);               // 发送寄存器地址
    i2c_write_byte(data);              // 发送数据
    i2c_stop();
}
// 使用:eeprom_write(0xA0, 0x00, 0x55);

特点

  • 必须发送从设备地址(7 位左移 + R/W 位)
  • 每字节后需检查 ACK(硬件自动处理,但可读状态)
  • 起始/停止条件是协议核心

三、为什么代码会这样设计?—— 回到协议本质

协议代码特点原因
UART直接写 DR 寄存器异步通信,无时钟、无地址、点对点
SPI手动控制 CS + 全双工收发同步通信,靠 CS 选设备,MOSI/MISO 独立
I²C起始/地址/ACK/停止共享总线,靠地址寻址,需仲裁和确认

四、总结:一眼识别代码类型

当你看到一段通信代码,可以这样判断:

  • 如果只有 send(data),没有地址、没有 CS、没有起始信号 → UART
  • 如果有 CS_LOW() / CS_HIGH() 包裹发送函数 → SPI
  • 如果函数里有 start()stop()addr<<1、检查 ACK → I²C

五、给初学者的建议

  1. 先掌握 UART:代码最简单,用于调试,建立信心。
  2. 再学 SPI:理解“片选”概念,适合驱动显示屏、SD卡。
  3. 最后攻克 I²C:重点理解“起始/停止”和“地址”,多看波形图。
  4. 善用逻辑分析仪:亲眼看到 SDA/SCL 或 MOSI/MISO 的波形,代码瞬间变直观!

💡 记住
代码是协议的影子
理解了 UART/SPI/I²C 的物理连接和时序图,代码自然就懂了!

希望这篇对比能帮你彻底分清三者的代码差异!如有具体芯片(如 STM32、ESP32、Arduino)的代码需求,也可以告诉我,我可以给出更针对性的例子。

还有can协议呢?

Qwen3-Max

18:46:17

太好了!你已经掌握了 UART、SPI、I²C,现在我们来认识工业与汽车电子领域的“通信王者”——CAN 协议(Controller Area Network)

下面这篇讲解专为初学者设计,用清晰的逻辑、生活化比喻和代码对比,帮你彻底搞懂:

  • CAN 是什么?为什么汽车和工厂都用它?
  • 它和 UART/SPI/I²C 有什么本质区别?
  • CAN 的代码长什么样?为什么更复杂?

🌟 CAN 协议完全入门指南(给嵌入式新手)

适合人群:学过 UART/SPI/I²C,想了解工业/汽车通信的新手
目标:读完后你能回答——

  • CAN 为什么比 UART 更可靠?
  • “ID 不是地址”是什么意思?
  • 为什么 CAN 代码要配“滤波器”和“邮箱”?

一、先说结论(一句话记住)

CAN 不是点对点通信,而是一个“广播会议”
所有设备都在听,但只处理自己关心的消息。
消息用 ID 标识优先级(不是地址),自动仲裁 + 错误重传,确保关键数据永不丢失!


二、为什么需要 CAN?—— UART 的致命缺陷

想象一辆汽车:

  • 发动机 ECU 要发“当前转速”给仪表盘
  • ABS 系统要发“车轮打滑”给车身控制模块
  • 空调要发“温度设定”给中控

如果用 UART

  • 需要无数根线(发动机→仪表、ABS→车身…)
  • 如果一根线断了,整个系统瘫痪
  • 多个设备同时发?数据冲突!

CAN 的解决方案

  • 所有设备挂到同一对双绞线(CAN_H / CAN_L)
  • 所有消息广播发送
  • 每个消息带一个 ID(如 0x201)
  • ID 越小,优先级越高(0x100 比 0x200 先发)
  • 自动检测错误并重发

结果

  • 线少、成本低
  • 抗干扰强(差分信号)
  • 关键消息(如刹车)永远优先

三、CAN 的核心特点(vs UART/SPI/I²C)

特性UARTSPII²CCAN
拓扑点对点星型(主从)总线(多从)总线(多主)
地址CS 片选7/10 位地址ID = 优先级(非地址)
同步异步同步同步异步(但有位同步)
错误处理ACK/NACKCRC + 自动重传 + 故障隔离
物理层TTL/RS232单端开漏+上拉差分(CAN_H/CAN_L)
典型速率≤115kbps≤100Mbps≤3.4Mbps≤1Mbps(经典 CAN)

🔥 最大区别

  • UART/SPI/I²C:“我要发给谁”
  • CAN:“这是什么类型的消息”(所有节点自己判断要不要)

四、CAN 帧结构(以标准帧为例)

一帧 CAN 数据包含:


text

编辑

[SOF] [ID(11bit)] [RTR] [IDE] [DLC] [Data(0~8字节)] [CRC] [ACK] [EOF]
字段说明
SOF帧起始(1 位显性)
ID11 位标识符(决定优先级,数值越小优先级越高)
RTR远程请求位(0=数据帧,1=请求数据)
DLC数据长度(0~8 字节)
Data实际数据(最多 8 字节!)
CRC15 位校验码(硬件自动生成/校验)
ACK接收方应答(正确接收则拉低)

关键限制每帧最多 8 字节数据
(所以不适合传大文件,但非常适合传传感器状态、控制指令)


五、CAN 代码 vs UART/SPI/I²C —— 为什么更复杂?

📌 场景:发送一条“车速 = 60 km/h”的消息

🔹 UART 代码(简单粗暴)

c

编辑

uart_send(60); // 直接发,对方必须知道这是车速
🔹 SPI 代码(需选设备)

c

编辑

cs_low();
spi_write(CMD_SET_SPEED);
spi_write(60);
cs_high();
🔹 I²C 代码(需地址)

c

编辑

i2c_write(0x50, SPEED_REG, 60);
🔹 CAN 代码(完整流程)

c

编辑

// 1. 定义消息头(重点:ID 和 DLC)
CAN_TxHeaderTypeDef tx_header;
tx_header.StdId = 0x201;        // 车速消息 ID(行业约定)
tx_header.IDE = CAN_ID_STD;     // 标准帧(11位ID)
tx_header.RTR = CAN_RTR_DATA;   // 数据帧
tx_header.DLC = 1;              // 1字节数据

// 2. 准备数据
uint8_t data = 60;

// 3. 发送到发送邮箱(硬件自动处理总线仲裁)
uint32_t mail_box;
if (HAL_CAN_AddTxMessage(&hcan, &tx_header, data, &mail_box) != HAL_OK) {
    Error_Handler(); // 发送失败(邮箱满等)
}

关键差异

  • 不指定“发给谁”,只指定 “这是什么消息”(ID=0x201)
  • 硬件自动处理:仲裁、CRC、ACK、重传
  • 发送成功 ≠ 对方收到(因为是广播)

📌 接收代码:靠“滤波器”筛选消息

CAN 节点会收到所有消息,但通常只关心几类。
所以需要配置 硬件滤波器,避免 CPU 被无关消息打扰。


c

编辑

// 配置滤波器:只接收 ID=0x201(车速)和 0x202(油量)的消息
CAN_FilterTypeDef filter;
filter.FilterBank = 0;
filter.FilterMode = CAN_FILTERMODE_IDMASK;      // ID + 掩码模式
filter.FilterScale = CAN_FILTERSCALE_32BIT;
filter.FilterIdHigh = ((0x201 << 5) & 0xFFFF);  // ID 左移 5 位(STM32要求)
filter.FilterMaskIdHigh = ((0x7FF << 5) & 0xFFFF); // 掩码:全匹配
filter.FilterFIFOAssignment = CAN_RX_FIFO0;
filter.FilterActivation = ENABLE;

HAL_CAN_ConfigFilter(&hcan, &filter);

// 启动接收中断
HAL_CAN_Start(&hcan);
HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);

// 回调函数(当收到匹配消息时触发)
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
    CAN_RxHeaderTypeDef rx_header;
    uint8_t rx_data[8];
    
    // 从 FIFO 读取消息
    HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx_header, rx_data);
    
    if (rx_header.StdId == 0x201) {
        printf("收到车速: %d km/h\n", rx_data[0]);
    }
}

为什么需要滤波器?
想象总线上有 100 个设备,每秒发 1000 条消息。
如果不滤波,CPU 会被中断打爆!
硬件滤波器在数据进 RAM 前就过滤掉无关消息,极大减轻 CPU 负担。


六、CAN 的独特机制(代码背后的原理)

1. 非破坏性仲裁

  • 多个节点同时发?ID 小的胜出
  • 仲裁过程中,输的一方自动停止发送,转为接收
  • 无数据冲突,且高优先级消息零延迟

2. 错误检测与恢复

  • 每个节点监控总线
  • 发现错误(位错误、CRC 错误等)→ 发送 错误帧
  • 发送方自动重发,直到成功

3. 故障封闭

  • 如果某个节点持续出错,会被自动离线(Bus Off)
  • 不影响其他节点通信

七、CAN vs 其他协议 —— 何时使用?

场景推荐协议
调试打印、连电脑UART
高速读写 Flash/SD卡SPI
连多个传感器(温湿度、RTC)I²C
汽车电子、工业控制、多节点实时通信✅ CAN

💡 记住

  • 高可靠、多节点、抗干扰 → 选 CAN
  • 传大量数据 → CAN 不合适(只有 8 字节/帧),考虑 CAN FD(64 字节)或 Ethernet

八、总结:CAN 代码的核心思想

概念说明
ID 不是地址是消息类型 + 优先级
广播通信所有节点都能收到,靠滤波器筛选
硬件处理底层仲裁、CRC、ACK 由 CAN 控制器自动完成
邮箱/FIFO发送用“邮箱”,接收用“FIFO 队列”
滤波器必备避免 CPU 被无关消息淹没

九、给初学者的建议

  1. 先理解“ID 优先级”和“广播”概念,再看代码
  2. 用 CAN 分析仪或逻辑分析仪看真实波形(显性/隐性电平)
  3. 从 STM32 HAL 库示例入手(如 CAN_Networking
  4. 不要试图用 GPIO 模拟 CAN(时序要求太高,必须用硬件控制器)

🚗 终极比喻

  • UART = 两个人打电话
  • SPI = 老板对员工下命令(点名)
  • I²C = 会议室按工号发言
  • CAN = 工厂广播系统
    “注意!生产线紧急停机(ID=0x100)!” —— 所有工人都听到,但只有产线工人行动!

现在,你已经掌握了嵌入式四大串行通信协议(UART/SPI/I²C/CAN)的核心思想!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小范好好学习

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值