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 的优势
| 特性 | RS422 | TTL / 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 | ≤115kbps | 3(TX/RX/GND) |
| RS422 | 电平标准 | 工业长距离通信 | 差分 ±2V~±6V | ≤1200m | ≤10Mbps | 4(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 总线)
九、给初学者的建议
- 先掌握 UART 协议:理解起始位、数据位、波特率
- 再学电平转换:知道 TTL 传不远,工业要用 RS422/RS485
- 动手接一次线:用 USB 转 RS422 模块连开发板,亲眼看到数据
- 别怕看芯片手册:找一个 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–15 | Payload | 0x01, 0x02, ..., 0x10 |
| 16 | CRC | 累加和校验值 = 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_TX | DI(Driver Input) |
| GPIO[17] | UART1_RX | RO(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_BAUD、TEST_PARITY、TEST_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:接收模式(验证核心板接收能力)
- 上位机启动自动发送脚本:每 100ms 发 1 帧,共 1000 帧
- 核心板上电,选择 模式 1
- 程序结束后,查看打印:
>>> TOTAL VALID FRAMES RECEIVED: 1000 <<< - 比对:该数值应等于上位机“已发送帧数”
场景 B:发送模式(验证核心板发送能力)
- 核心板上电,选择 模式 2
- 程序开始发送 1000 帧(耗时 ≈ 100 秒)
- 上位机串口助手显示 接收计数
- 比对:助手计数应为 1000
六、典型测试用例表
| 测试编号 | 波特率 | 奇偶校验 | 停止位 | 模式 | 预期结果 |
|---|---|---|---|---|---|
| TC-01 | 9600 | None | 1 | Receive | 收到 1000 帧 |
| TC-02 | 19200 | Even | 1 | Receive | 收到 1000 帧 |
| TC-03 | 115200 | Odd | 2 | Receive | 收到 1000 帧 |
| TC-04 | 115200 | None | 1 | Send | 上位机收到 1000 帧 |
✅ 每次测试前 复位核心板,确保状态干净。
七、注意事项
- 时钟源确认:代码假设 UART 时钟为 100 MHz,若实际不同,请调整
divisor计算。 - 中断映射:
UART1_INT_NUM = 13为示例,需根据 C6678 EVM 实际中断分配修改。 - 延时精度:
delay_100ms()为粗略延时,高精度场景建议使用 Timer。 - 缓冲区溢出:若通信速率极高,可增大
RX_BUF_SIZE。 - CRC 一致性:确保上位机使用 相同累加和算法,否则校验恒失败。
八、结论
本方案提供了一套 简洁、可靠、可复现 的 RS422 UART 通信测试体系,完全满足以下需求:
- ✅ 用户交互选择 发送/接收模式
- ✅ 接收模式 仅输出有效帧计数
- ✅ 发送模式 仅发送,不判断
- ✅ 比对 核心板日志 与 上位机计数
- ✅ 支持 任意波特率、校验、停止位组合
可直接用于 TMS320C6678 平台的产品验证、出厂测试或通信调试。
🌟 TMS320C6678 核心板串口通信测试程序 —— 初学者完全指南
适合人群:刚学 C 语言、第一次接触 DSP(数字信号处理器)、想用 TMS320C6678 做串口通信的新手
目标:让你看懂、能改、会用这段代码,完成一次完整的“发送/接收”测试!
一、我们到底要做什么?
想象你在调试一块工业控制板(比如 TI 的 TMS320C6678 开发板),它通过 RS422 接口 和上位机(你的电脑)通信。
你希望验证:
- 当电脑发数据给核心板时,核心板能不能正确收到?
- 当核心板发数据给电脑时,电脑能不能完整接收到?
但你不希望程序自动判断“对不对”,而是你自己看着串口助手数一数——这就是“人工比对”。
所以,我们需要一个程序,它能做到两件事:
- 接收模式:核心板等电脑发 1000 帧数据,每帧校验一下,统计成功收到多少帧。
- 发送模式:核心板自己发 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 是差分信号,抗干扰强,适合工业环境。但我们这里只用它的“全双工”模式,当成普通串口用。
四、程序整体结构(像搭积木)
我们的程序分成几块:
- 用户配置区:你改几个数字,就能换波特率、校验方式。
- 寄存器定义:告诉 C 语言“UART 寄存器在哪”。
- 中断 + 缓冲区:防止高速通信丢字节。
- 主函数:打印菜单,让你选“发”还是“收”。
- 两个测试函数:
run_receive_test():收数据、校验、计数run_send_test():发 1000 帧
- 工具函数:发字节、算 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:准备上位机
- 安装串口助手(如 SSCOM、AccessPort)
- 插上 RS422 转 USB 模块,记下 COM 号(比如 COM5)
步骤 2:编译烧录
- 用 TI Code Composer Studio (CCS) 打开工程
- 把上面代码保存为
main.c - 编译 → 加载到 C6678 开发板
步骤 3:运行测试
场景 A:测“接收能力”
- 串口助手设置:115200, 无校验, 1 停止位
- 打开串口,看到菜单
- 在串口助手的“发送区”输入
1并发送 - 核心板进入接收模式
- 另开一个串口发送工具(或脚本),每 100ms 发一帧
01 02 ... 10 88,共 1000 次 - 核心板打印:
总共接收到的有效帧数量: 1000 - ✅ 你确认:电脑发了 1000,核心板收到了 1000 → 通信 OK!
场景 B:测“发送能力”
- 串口助手设置同上
- 输入
2进入发送模式 - 核心板开始发数据
- 串口助手会显示一堆十六进制数据,并有“接收计数”
- 等 100 秒后,计数应为 1000
- ✅ 通信 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,现在我们来认识另外两种在嵌入式系统中极其常用的串行通信协议:SPI 和 I²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):
| 信号线 | 全称 | 方向(从主设备看) | 作用 |
|---|---|---|---|
| SCLK | Serial Clock | 输出 | 时钟信号,由主设备产生 |
| MOSI | Master Out Slave In | 输出 | 主设备 → 从设备 的数据线 |
| MISO | Master In Slave Out | 输入 | 从设备 → 主设备 的数据线 |
| CS/SS | Chip Select / Slave Select | 输出 | 片选信号,低电平有效 |
⚠️ 关键:每增加一个从设备,就要多一根 CS 线!
比如接 3 个传感器,就需要 3 根 CS 线(共 6 根线:SCLK+MOSI+MISO+CS1+CS2+CS3)
4. 工作流程(以读取 Flash 为例)
- 主设备拉低 Flash 的 CS 线 → “你,准备干活!”
- 主设备通过 SCLK 打拍子,同时在 MOSI 发送“读命令”
- Flash 在 MISO 上返回数据(同时 MOSI 可继续发地址)
- 通信结束,主设备拉高 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 根!
| 信号线 | 全称 | 作用 |
|---|---|---|
| SDA | Serial Data | 双向数据线(所有设备共享) |
| SCL | Serial Clock | 时钟线(由主设备控制) |
✅ 优势:无论接多少设备,都只用这 2 根线!
(当然,每个设备还需接电源和地)
4. 工作流程(以读取温度传感器为例)
- 主设备发出 起始信号(SCL 高时,SDA 从高→低)
- 主设备发送 从设备地址 + 读写位(如
0x90表示写) - 对应地址的从设备 拉低 SDA 回应 ACK(确认收到)
- 主设备发送寄存器地址(如
0x00) - 主设备再发 重复起始,然后发
0x91(读) - 传感器通过 SDA 返回温度值
- 主设备发 停止信号(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 终极对比表
| 特性 | SPI | I²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)
一、核心差异速览表
| 特性 | UART | SPI | I²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
五、给初学者的建议
- 先掌握 UART:代码最简单,用于调试,建立信心。
- 再学 SPI:理解“片选”概念,适合驱动显示屏、SD卡。
- 最后攻克 I²C:重点理解“起始/停止”和“地址”,多看波形图。
- 善用逻辑分析仪:亲眼看到 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)
| 特性 | UART | SPI | I²C | CAN |
|---|---|---|---|---|
| 拓扑 | 点对点 | 星型(主从) | 总线(多从) | 总线(多主) |
| 地址 | 无 | CS 片选 | 7/10 位地址 | ID = 优先级(非地址) |
| 同步 | 异步 | 同步 | 同步 | 异步(但有位同步) |
| 错误处理 | 无 | 无 | ACK/NACK | CRC + 自动重传 + 故障隔离 |
| 物理层 | 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 位显性) |
| ID | 11 位标识符(决定优先级,数值越小优先级越高) |
| RTR | 远程请求位(0=数据帧,1=请求数据) |
| DLC | 数据长度(0~8 字节) |
| Data | 实际数据(最多 8 字节!) |
| CRC | 15 位校验码(硬件自动生成/校验) |
| 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 被无关消息淹没 |
九、给初学者的建议
- 先理解“ID 优先级”和“广播”概念,再看代码
- 用 CAN 分析仪或逻辑分析仪看真实波形(显性/隐性电平)
- 从 STM32 HAL 库示例入手(如
CAN_Networking) - 不要试图用 GPIO 模拟 CAN(时序要求太高,必须用硬件控制器)
🚗 终极比喻:
- UART = 两个人打电话
- SPI = 老板对员工下命令(点名)
- I²C = 会议室按工号发言
- CAN = 工厂广播系统:
“注意!生产线紧急停机(ID=0x100)!” —— 所有工人都听到,但只有产线工人行动!
现在,你已经掌握了嵌入式四大串行通信协议(UART/SPI/I²C/CAN)的核心思想!
663

被折叠的 条评论
为什么被折叠?



