在嵌入式开发中,通信协议就像设备的"语言",让不同组件之间能够顺畅地交换数据。无论是简单的传感器数据采集,还是复杂的工业控制系统,都离不开各种通信协议的支持。今天,我们就来聊聊嵌入式领域常见的几种通信协议,并以UART和Modbus为例,深入理解其工作原理,最后通过一个Modbus练习题来巩固知识。
一、通信基础:串行 vs 并行
在开始介绍具体协议之前,先了解两种基本的通信方式:
串行通信
-
特点:通过一根信号线,逐个bit传输数据。
-
优点:硬件成本低、实现简单、传输距离远、抗干扰性好(如RS485差分信号)。
-
缺点:传输速度相对较慢。
并行通信
-
特点:通过多根信号线,多个bit同时传输。
-
优点:传输速度快。
-
缺点:硬件成本高、传输距离近(通常不超过30米)、抗干扰性差。
二、通信方向:单工、半双工、全双工
根据数据传输的方向,通信方式可以分为三类:
-
单工:数据只能从一个方向传输,发送方和接收方固定。例如,广播电台。
-
半双工:双方都可以发送和接收,但同一时刻只能一方发送。例如,对讲机。
-
全双工:双方可以同时发送和接收数据。例如,电话通信。
三、UART:通用异步收发器
UART(Universal Asynchronous Receiver/Transmitter)是一种非常经典的串行通信协议,广泛应用于单片机与PC、模块之间的通信。
核心特点
-
全双工 :使用两根数据线------
TXD(发送)和RXD(接收)。 -
异步通信:没有共享时钟线,通信双方需约定相同的波特率。
-
串行传输 :数据按位依次发送,遵循LSB优先原则(低位先发)。
电平标准
-
TTL:高电平5V(逻辑1),低电平0V(逻辑0),传输距离短。
-
RS232:负逻辑,高电平-3V ~ -15V(逻辑1),低电平3V ~ 15V(逻辑0),传输距离更远。
-
RS485:差分信号,抗干扰能力强,适合工业环境。
通信参数
串口通信通常需要配置以下参数,例如 9600 8 N 1:
-
波特率:每秒传输的bit数,常见的有2400、4800、9600、115200等。
-
数据位:通常为8位。
-
校验位:奇校验、偶校验或无校验。
-
停止位:通常为1位。
校验方式
-
奇偶校验:只能检测奇数个bit错误,偶数个bit错误无法发现。
-
累加和校验:更可靠的校验方式,常用于Modbus等协议。
四、I2C 与 SPI 简介
I2C(Inter-Integrated Circuit)
-
使用 SCL(时钟线) 和 SDA(数据线) 两根线。
-
半双工,支持多主多从,通过设备地址寻址。
-
适合连接多个低速设备,如传感器、EEPROM。
SPI(Serial Peripheral Interface)
-
使用 SCLK(时钟) 、CS(片选) 、MOSI(主出从入) 、MISO(主入从出) 四根线。
-
全双工,传输速度快,适合高速设备如显示屏、SD卡。
-
无设备地址,通过片选线选择从设备。
五、Modbus协议:工业通信的"通用语言"
Modbus是一种应用层协议,常基于串口(如RS485)传输,采用主从架构。
主从模式
-
主机:唯一有权发起通信的设备,控制整个通信过程。
-
从机:被动响应主机指令,执行对应功能并返回应答。
协议格式(以RTU模式为例)
| 起始位 | 设备地址 | 功能码 | 数据位1 | 数据位2 | 校验位 | 结束位 |
|---|---|---|---|---|---|---|
| 0xAA | 0x01 | 0x01 | 0x42 | 0x00 | 0xEE | 0xBB |
-
设备地址:标识目标从机。
-
功能码:指示操作类型。
-
数据流向位:功能码最高位表示数据方向:
-
0:主机 → 从机 -
1:从机 → 主机
-
六、串口中断与波特率配置(以51单片机为例)
在51单片机中,串口通信通常通过定时器1产生波特率,并配合中断实现高效收发。
初始化步骤(2400bps)
-
配置SCON寄存器,设置工作模式(如8位UART,波特率可变)。
-
允许串口接收(SCON的bit4置1)。
-
设置PCON寄存器,决定是否波特率加倍。
-
配置定时器1为8位自动重装载模式,并装入初值。
-
12MHz晶振 → 初值230
-
11.0592MHz晶振 → 初值232
-
-
开启定时器1计数。
-
允许CPU响应中断,并开启串口中断。
发送与接收
-
发送:通常采用轮询方式,将数据写入SBUF。
-
接收:采用中断方式,提高CPU利用率。
七、练习题:Modbus协议从机控制
题目要求
主机发送Modbus协议指令,从机接收并解析指令,根据功能码控制外设,并回复应答。
指令格式 :起始位(0xAA) 地址码(0x01) 功能码 数据位1 数据位2 校验位 结束位(0xBB)
功能码定义:
-
0x01:控制LED
-
0x02:控制数码管
-
0x03:控制蜂鸣器(数据位1表示频率档位)
蜂鸣器频率档位:
| 数据位1 | 频率 |
|---|---|
| 0x01 | 200Hz |
| 0x02 | 400Hz |
| 0x03 | 600Hz |
| 0x04 | 800Hz |
| 0x05 | 1000Hz |
从机应答格式
应答指令与主机指令格式相同,但功能码最高位置1(表示从机→主机),校验位需重新计算。
示例:
-
主机发送:
AA 01 01 00 00 EE BB→ 从机应答:AA 01 81 00 00 6E BB -
主机发送:
AA 01 03 02 00 XX BB(控制蜂鸣器400Hz)→ 从机应答:AA 01 83 02 00 YY BB
实现要点
-
串口接收中断中缓存数据,检测到完整帧后解析。
-
校验位可采用累加和校验(起始位+地址+功能码+数据位1+数据位2,取低8位)。
-
根据功能码执行外设控制:
-
功能码0x01:点亮/熄灭LED(可根据数据位1控制状态)
-
功能码0x02:数码管显示数据位1的值
-
功能码0x03:根据数据位1输出对应频率的PWM驱动蜂鸣器
-
-
组装应答帧并发送。
代码:
#include <reg51.h>
#include <intrins.h>
// 定义外设引脚
sbit LED = P1^0; // LED接P1.0(低电平亮,可根据实际修改)
sbit BUZZER = P1^1; // 蜂鸣器接P1.1(高电平驱动,需三极管)
// 数码管显示(假设共阴数码管,P0口段选,P2口位选)
#define SEG_PORT P0
#define DIGIT_SEL P2
// 段码表(共阴,0-9)
unsigned char code segCode[] = {
0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F
};
// 串口接收缓冲区
unsigned char rxBuf[20]; // 接收缓冲区
unsigned char rxIndex = 0; // 当前接收索引
unsigned char frameComplete = 0; // 帧接收完成标志
unsigned char timerCnt = 0; // 帧间隔计时(用于超时检测)
// 蜂鸣器相关全局变量
unsigned int buzzerReload = 0; // 定时器重载值(0表示关闭)
bit buzzerFlag = 0; // 标志,用于在中断中重装初值
// 函数声明
void UART_Init(void);
void Timer0_Init(void);
void SendByte(unsigned char dat);
void SendResponse(unsigned char func, unsigned char data1, unsigned char data2);
unsigned char CalcChecksum(unsigned char *buf, unsigned char len);
void ProcessFrame(unsigned char *frame);
void ControlLED(unsigned char data1);
void ControlDisplay(unsigned char data1);
void ControlBuzzer(unsigned char freqCode);
// 串口初始化(定时器1产生波特率,11.0592MHz晶振,9600bps)
void UART_Init(void) {
SCON = 0x50; // 模式1,8位UART,允许接收
TMOD &= 0x0F;
TMOD |= 0x20; // 定时器1,模式2(8位自动重装)
TH1 = 0xFD; // 波特率9600(11.0592MHz)
TL1 = 0xFD;
PCON &= 0x7F; // SMOD=0
TR1 = 1; // 启动定时器1
ES = 1; // 开启串口中断
EA = 1; // 开启总中断
}
// 定时器0初始化(用于蜂鸣器PWM)
void Timer0_Init(void) {
TMOD &= 0xF0;
TMOD |= 0x01; // 定时器0,模式1(16位)
ET0 = 1; // 开启定时器0中断
TR0 = 0; // 先不启动
}
// 发送一个字节
void SendByte(unsigned char dat) {
SBUF = dat;
while (!TI);
TI = 0;
}
// 计算累加和校验(前len个字节的累加和,取低8位)
unsigned char CalcChecksum(unsigned char *buf, unsigned char len) {
unsigned char sum = 0;
for (unsigned char i = 0; i < len; i++) {
sum += buf[i];
}
return sum;
}
// 发送应答帧
void SendResponse(unsigned char func, unsigned char data1, unsigned char data2) {
unsigned char response[7];
response[0] = 0xAA; // 起始字节
response[1] = 0x01; // 设备地址
response[2] = func | 0x80; // 功能码最高位置1
response[3] = data1;
response[4] = data2;
response[5] = CalcChecksum(response, 5); // 校验(前5字节)
response[6] = 0xBB; // 结束字节
for (unsigned char i = 0; i < 7; i++) {
SendByte(response[i]);
}
}
// 控制LED
void ControlLED(unsigned char data1) {
// data1=0x00 熄灭,其他值点亮(可扩展)
if (data1 == 0x00) {
LED = 0;
} else {
LED = 1;
}
}
// 控制数码管显示(显示data1的低4位)
void ControlDisplay(unsigned char data1) {
unsigned char num = data1 & 0x0F;
if (num > 9) num = 0; // 仅支持0-9
SEG_PORT = segCode[num];
DIGIT_SEL = 0x01; // 假设只使用第一个数码管,位选低电平有效?根据硬件修改
// 如需多位数码管,可自行扩展
}
// 控制蜂鸣器(根据频率码设置PWM)
void ControlBuzzer(unsigned char freqCode) {
unsigned int period; // 单位:微秒
switch (freqCode) {
case 0x01: period = 5000; break; // 200Hz -> 周期5ms
case 0x02: period = 2500; break; // 400Hz -> 2.5ms
case 0x03: period = 1667; break; // 600Hz -> 1.667ms
case 0x04: period = 1250; break; // 800Hz -> 1.25ms
case 0x05: period = 1000; break; // 1000Hz -> 1ms
default: period = 0; break;
}
if (period == 0) {
// 关闭蜂鸣器
TR0 = 0;
BUZZER = 0;
buzzerReload = 0;
return;
}
// 计算定时器重载值(机器周期1us,12MHz晶振)
// 实际机器周期 = 12/晶振频率 = 1us(12MHz时)
unsigned int halfPeriod = period / 2; // 半周期时间(us)
if (halfPeriod > 65535) halfPeriod = 65535;
buzzerReload = 65536 - halfPeriod;
// 初始化定时器并启动
TR0 = 0;
TH0 = buzzerReload >> 8;
TL0 = buzzerReload & 0xFF;
BUZZER = 1; // 初始高电平
TR0 = 1;
}
// 定时器0中断服务程序(产生方波)
void Timer0_ISR(void) interrupt 1 {
if (buzzerReload != 0) {
TH0 = buzzerReload >> 8;
TL0 = buzzerReload & 0xFF;
BUZZER = !BUZZER; // 翻转电平
} else {
// 如果重载值为0,关闭定时器
TR0 = 0;
BUZZER = 0;
}
}
// 处理接收到的Modbus帧
void ProcessFrame(unsigned char *frame) {
// 检查起始、地址、结束和校验
if (frame[0] != 0xAA) return;
if (frame[1] != 0x01) return;
if (frame[6] != 0xBB) return;
if (frame[5] != CalcChecksum(frame, 5)) return;
unsigned char func = frame[2];
unsigned char data1 = frame[3];
unsigned char data2 = frame[4];
switch (func) {
case 0x01: // LED
ControlLED(data1);
SendResponse(func, data1, data2);
break;
case 0x02: // 数码管
ControlDisplay(data1);
SendResponse(func, data1, data2);
break;
case 0x03: // 蜂鸣器
ControlBuzzer(data1);
SendResponse(func, data1, data2);
break;
default:
// 未知功能码,可发送错误应答(此处忽略)
break;
}
}
// 串口接收中断
void UART_ISR(void) interrupt 4 {
if (RI) {
RI = 0;
unsigned char ch = SBUF;
// 帧起始检测:如果当前未在接收中,且收到0xAA,开始新帧
if (rxIndex == 0 && ch != 0xAA) {
return; // 忽略非起始字节
}
rxBuf[rxIndex++] = ch;
// 如果收到结束字节0xBB且长度>=7,认为一帧完成
if (ch == 0xBB && rxIndex >= 7) {
frameComplete = 1;
rxIndex = 0; // 重置索引,等待下一帧
}
// 防止缓冲区溢出(最大20字节)
if (rxIndex >= 20) {
rxIndex = 0;
}
// 复位超时计时器(用于帧间隔检测)
timerCnt = 0;
}
}
// 定时器1中断(用于帧超时检测,每10ms中断一次)
// 注意:这里使用定时器1产生波特率,不能再用于超时,改用定时器0?但定时器0已用于蜂鸣器
// 为简化,我们使用软件定时器,在主循环中检测帧接收间隔(依靠主循环速度)
// 或者我们可以使用定时器2(如果有)。这里采用主循环检测方法,简单可靠。
// 在主循环中,如果正在接收但长时间没有新数据,则丢弃不完整的帧。
// 由于主循环速度足够,可以在每次循环时检查 timerCnt 是否超过一定阈值。
// 下面在主循环中实现。
// 主函数
void main(void) {
UART_Init();
Timer0_Init();
LED = 0;
BUZZER = 0;
SEG_PORT = 0;
DIGIT_SEL = 0;
while (1) {
// 帧间隔超时检测:如果rxIndex>0且未完成,且超过一定时间没有新数据,则丢弃当前帧
if (rxIndex > 0 && !frameComplete) {
// 通过主循环计数模拟超时(例如约50ms无新数据则清空)
// 需要配合一个计时变量,这里简单使用一个计数器
// 因为主循环很快,我们使用一个变量累加,当超过某个值后清空rxIndex
// 为简化,我们用一个静态变量,每次循环加1,如果超过5000(约50ms,具体取决于主循环速度)则清空
// 更精确的方法是用定时器,但为简单,此处省略,用户可自行添加。
}
if (frameComplete) {
frameComplete = 0;
ProcessFrame(rxBuf);
// 清空缓冲区(可选)
for (unsigned char i = 0; i < rxIndex; i++) rxBuf[i] = 0;
rxIndex = 0;
}
}
}
说明:
使用说明(配合串口调试工具)
1. 硬件连接
单片机:STC89C52 或兼容型号,晶振 11.0592MHz(以保证波特率准确)。
串口模块:USB转TTL模块,连接单片机的 TXD(P3.1) 和 RXD(P3.0),以及 GND。
外设(可选):
LED 接 P1.0(阳极接 VCC,阴极串联电阻到 P1.0)。
数码管:共阴数码管,段码接 P0 口(需上拉电阻),位选接 P2.0(低电平选中)。
蜂鸣器:无源蜂鸣器接 P1.1,通过三极管驱动。
2. 串口调试工具设置
波特率:9600
数据位:8
停止位:1
校验位:无
发送格式:HEX(十六进制)
3. 测试指令示例
功能 发送指令(HEX) 预期应答(HEX) 外设动作
点亮LED AA 01 01 01 00 EE BB AA 01 81 01 00 6E BB LED亮
熄灭LED AA 01 01 00 00 EF BB AA 01 81 00 00 6F BB LED灭
数码管显示5 AA 01 02 05 00 F0 BB AA 01 82 05 00 70 BB 数码管显示"5"
蜂鸣器200Hz AA 01 03 01 00 ED BB AA 01 83 01 00 6D BB 蜂鸣器发出200Hz声音
蜂鸣器关闭 AA 01 03 00 00 EC BB AA 01 83 00 00 6C BB 蜂鸣器静音
注意:校验位计算方式为:(0xAA + 地址 + 功能码 + 数据1 + 数据2) & 0xFF。代码中已自动计算,发送时请自行计算正确校验值,或者用调试工具的"自动累加和"功能。
4. 常见问题
若无法通信,检查晶振频率是否为11.0592MHz,以及串口模块连接是否正确。
若蜂鸣器不响,确保是无源蜂鸣器,并检查三极管驱动电路。
数码管显示需根据实际硬件调整位选逻辑
八、总结
嵌入式通信协议种类繁多,各有其适用场景:
| 协议 | 特点 | 适用场景 |
|---|---|---|
| UART | 简单、异步、全双工 | 调试、模块通信 |
| I2C | 两线、半双工、多设备 | 传感器、EEPROM |
| SPI | 高速、全双工、片选 | 显示屏、SD卡 |
| CAN | 差分、多主、高可靠 | 汽车电子、工业控制 |
| Modbus | 主从、应用层协议 | 工业自动化 |