RS-232协议与RS485协议详解
RS-232协议与RS485协议详解
1. RS-232 协议详解
RS-232 (Recommended Standard 232) 是一个历史悠久且非常普及的串行通信接口标准。它定义了信号的电气特性、时序、引脚功能以及物理连接器。
主要特点:
- 通信模式:点对点 (Point-to-Point)。这意味着一个 RS-232 端口只能与另一个 RS-232 端口直接通信。
- 通信方式:全双工 (Full-Duplex)。数据可以同时发送和接收。
- 信号电平:非逻辑电平(与微控制器常用的 TTL/CMOS 电平不同)。
- 逻辑 ‘1’ (Mark): -3V 到 -15V
- 逻辑 ‘0’ (Space): +3V 到 +15V
- 这种较大的电压摆幅是为了在一定程度上抵抗噪声,但在现代标准看来,它的抗扰性较差。
- 物理连线:最简单的连接只需要三根线:
- TxD (Transmit Data): 发送数据线
- RxD (Receive Data): 接收数据线
- GND (Ground): 信号地
- 复杂的连接还包括 RTS (Request To Send), CTS (Clear To Send) 等硬件流控制线。
- 通信距离:较短。标准规定在典型波特率(如 9600 bps)下,最大距离约为 15米 (50英尺)。距离越长,可靠的通信速率越低。
- 常见应用:
- 个人电脑的 COM 口(现已少见)
- 调制解调器 (Modem)
- 工业设备、网络设备的配置/调试接口 (Console Port)
- 一些简单的仪器仪表通信
在嵌入式开发中,微控制器(MCU)本身的 UART(通用异步收发器)使用的是 TTL/CMOS 电平(如 0V 和 3.3V/5V)。要与 RS-232 设备通信,必须使用一个电平转换芯片,例如 MAX232 或其变种。
2. RS-485 协议详解
RS-485 (Recommended Standard 485) 是为了克服 RS-232 的局限性(如距离短、点对点、抗扰性差)而设计的。它在工业自动化、楼宇控制等领域应用极为广泛。
主要特点:
- 通信模式:多点 (Multi-point)。它允许在一条总线上连接多个设备(标准规定最多可达 32 个,使用中继器或高阻抗收发器可连接更多)。
- 通信方式:通常是半双工 (Half-Duplex)。使用一对双绞线进行发送和接收,因此在任何时刻,总线上的设备要么在发送,要么在接收。需要协议来控制谁可以发送数据。
- 信号电平:差分信号 (Differential Signaling)。这是 RS-485 的核心优势。
- 它使用两根线(通常标记为 A 和 B)来传输信号。
- 逻辑状态由 A 和 B 之间的电压差决定:
- 逻辑 ‘1’: V_A - V_B < -200mV
- 逻辑 ‘0’: V_A - V_B > +200mV
- 由于它检测的是电压差,而不是对地的绝对电压,因此能有效抵抗共模噪声,抗干扰能力非常强。
- 物理连线:最简单的半双工连接只需要两根线(一对双绞线)。
- 通信距离:非常长。在较低速率下,最大距离可达 1200米 (4000英尺)。
- 总线拓扑:必须是线性拓扑(菊花链),不能是星形或环形。为了防止信号反射,总线的两个物理末端通常需要并联一个 120Ω 的终端电阻。
- 常见应用:
- 工业控制总线(如 Modbus RTU, Profibus-DP 都常基于 RS-485 物理层)
- 楼宇自动化系统
- 舞台灯光控制 (DMX512)
- 安防监控系统 (PTZ 云台控制)
同样,MCU 的 UART 也需要通过一个 RS-485 收发器芯片(如 MAX485, SP3485)来连接到 RS-485 总线。
3. RS-232 vs. RS-485 对比总结
特性 | RS-232 | RS-485 | 备注 |
---|---|---|---|
通信模式 | 点对点 (1 对 1) | 多点总线 (最多 32 个标准节点) | RS-485 的核心优势 |
通信距离 | 短 (约 15 米) | 长 (约 1200 米) | RS-485 的核心优势 |
抗干扰能力 | 较差 (单端信号) | 极强 (差分信号) | RS-485 的核心优势 |
数据传输方式 | 全双工 (通常 3 线以上) | 半双工 (2 线) 或 全双工 (4 线) | 2 线半双工是 RS-485 最常见的用法 |
信号电平 | 单端信号, 高电平 (+3~15V), 低电平 (-3~-15V) | 差分信号 (A、B 线压差) | 需要专门的电平转换/收发器芯片 |
总线拓扑 | 点对点 | 线性总线,两端需加终端电阻 | RS-485 组网比 RS-232 复杂 |
驱动能力 | 驱动 1 个接收器 | 驱动 32 个标准接收器 | 允许多个设备挂载在同一总线 |
结论与建议
- 何时选择 RS-232?
- 当你需要一个简单的、一对一的、短距离的通信连接时。
- 最常见的场景是作为设备的“后台”或“调试”接口,用一根线直连到电脑或另一个主控设备进行配置和监控。
- 何时选择 RS-485?
- 当你需要在同一条通信线上连接多个设备(一主多从或多主多从)时。
- 当通信距离要求很长(几十米到上千米)时。
- 当应用环境中有较强的电磁干扰(如工厂车间、电机附近)时。
RS-232 协议详解
RS-232 (Recommended Standard 232) 是一种用于异步串行通信的点对点协议。它定义了物理层的电气、机械和功能特性。
A. 协议细节
- 电气特性 (Physical Layer)
- 电平标准:它使用与微控制器 (TTL/CMOS) 不同的负逻辑电平。
- 逻辑 ‘1’ (Mark): 表示为 -3V 到 -15V 的负电压。线路空闲时处于此状态。
- 逻辑 ‘0’ (Space): 表示为 +3V 到 +15V 的正电压。
- 连接方式:点对点,即一个设备连接一个设备。
- 通信模式:全双工,发送 (TxD) 和接收 (RxD) 有独立的线路,可以同时进行。
- 电平标准:它使用与微控制器 (TTL/CMOS) 不同的负逻辑电平。
- 数据帧结构 (Data Link Layer) 由于是异步通信,没有时钟信号线。通信双方依靠固定的波特率 (Baud Rate) 和数据帧结构来同步。一个典型的数据帧(一个字节)包含以下部分:
- 起始位 (Start Bit): 总是 1 位 逻辑 ‘0’ (Space)。它标志着一个新数据帧的开始,并使接收方与发送方同步。
- 数据位 (Data Bits): 通常为 5, 6, 7 或 8 位。最常见的是 8 位。数据从最低有效位 (LSB) 开始发送。
- 校验位 (Parity Bit): (可选) 用于简单的错误校验。可以是:
- 无校验 (None): 不使用。
- 奇校验 (Odd): 确保数据位和校验位中 ‘1’ 的总数是奇数。
- 偶校验 (Even): 确保数据位和校验位中 ‘1’ 的总数是偶数。
- 停止位 (Stop Bit(s)): 总是 逻辑 ‘1’ (Mark)。可以是 1, 1.5 或 2 位。它标志着数据帧的结束,并确保总线在下一个起始位到来前有足够时间恢复到空闲状态。
- 波特率 (Baud Rate) 指每秒传输的码元(在这里等同于比特)数。通信双方必须设置为相同的波特率才能正确解码。常见值有 9600, 19200, 38400, 115200 等。
B. 时序图
下图展示了以 “8-N-1” 格式(8个数据位,无校验,1个停止位)发送 ASCII 字符 ‘A’ 的时序。
- ASCII ‘A’ = 65 (十进制) =
0100 0001
(二进制)。 - LSB 优先发送,所以发送顺序是
1, 0, 0, 0, 0, 0, 1, 0
。
Idle Start Data Bits Stop Idle
Logic Lvl: _ ___ _ ___ ___ ___ ___ ___ _ ___ ___ ___ _| | | | | | | | | | | | | | | | | | | | | | | | | |
Line State: |_________| |__|____|___|____|___|____|___|____|___|____|___|____|___|____|__|_| |_______| | | | | | | | | | |
Bit: Start D0(1) D1(0) D2(0) D3(0) D4(0) D5(0) D6(1) D7(0) Stop (Next Start...)<-------------------------------------- One Character Frame ---------------------------------------->
时序解释:
- Idle: 线路空闲,保持在逻辑 ‘1’ (Mark, 负电压)。
- Start Bit: 线路被拉到逻辑 ‘0’ (Space, 正电压) 并保持一个比特时间,通知接收方数据即将到来。
- Data Bits: 按照 LSB 优先的顺序,依次发送 8 个数据位。‘1’ 为 Mark 状态,‘0’ 为 Space 状态。
- Stop Bit: 发送 1 位逻辑 ‘1’ (Mark),结束本次传输。
- Idle: 线路返回空闲状态,等待下一个字符的起始位。
C. C/C++ 代码示例 (Linux)
在 PC 上通过串口与 RS-232 设备通信,最常见的是操作系统的串行端口(在 Linux 中是 /dev/ttyS*
或 /dev/ttyUSB*
)。下面的例子展示了如何配置和使用串口。
#include <iostream>
#include <string>
#include <cstring>// POSIX 串口编程所需的头文件
#include <fcntl.h> // 包含文件控制定义,例如 O_RDWR
#include <termios.h> // 包含 POSIX 终端控制定义
#include <unistd.h> // 包含 write(), read(), close() 函数
#include <cerrno> // 包含 errno 和 strerror 函数,用于错误报告int main() {// 打开串口设备。请将 "/dev/ttyUSB0" 更改为你的实际串口设备文件。// O_RDWR: 以读写方式打开// O_NOCTTY: 防止该设备成为进程的控制终端int serial_port = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY);// 检查串口是否成功打开if (serial_port < 0) {std::cerr << "打开串口错误 " << errno << ": " << strerror(errno) << std::endl;return 1;}// 创建一个新的 termios 结构体,按惯例命名为 ttystruct termios tty;memset(&tty, 0, sizeof(tty));// 读取现有的串口设置,并处理可能发生的错误if (tcgetattr(serial_port, &tty) != 0) {std::cerr << "读取串口属性错误 " << errno << ": " << strerror(errno) << std::endl;return 1;}// --- 配置串口参数 ---// 设置波特率 (例如 9600)cfsetispeed(&tty, B9600); // 设置输入波特率cfsetospeed(&tty, B9600); // 设置输出波特率// 设置数据帧格式 (8-N-1)tty.c_cflag &= ~PARENB; // 清除校验位,禁用校验 (N)tty.c_cflag &= ~CSTOPB; // 清除停止位字段,设置一个停止位 (1)tty.c_cflag &= ~CSIZE; // 清除所有设置数据大小的位tty.c_cflag |= CS8; // 设置数据位为8位 (8)// 禁用硬件流控tty.c_cflag &= ~CRTSCTS; // 开启接收者,并设置为本地模式(忽略调制解调器控制线)tty.c_cflag |= CREAD | CLOCAL;// 设置为原始输入模式 (Raw Mode)tty.c_lflag &= ~ICANON; // 禁用规范模式(行缓冲)tty.c_lflag &= ~ECHO; // 禁用回显tty.c_lflag &= ~ECHOE; // 禁用擦除字符tty.c_lflag &= ~ECHONL; // 禁用换行回显tty.c_lflag &= ~ISIG; // 禁用中断、退出、暂停等信号// 输入模式标志tty.c_iflag &= ~(IXON | IXOFF | IXANY); // 关闭软件流控tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL); // 禁用所有输入处理// 输出模式标志tty.c_oflag &= ~OPOST; // 禁用所有输出处理(例如,不转换换行符)tty.c_oflag &= ~ONLCR; // 禁止将换行符转换为回车换行// 设置读取超时// VTIME: 等待时间,单位为 1/10 秒。// VMIN: 读取的最小字符数。// VMIN = 0, VTIME > 0: read() 是一个定时读取,最多等待 VTIME*0.1 秒。如果无数据,read()返回0。tty.c_cc[VTIME] = 10; // 设置超时为 1 秒 (10 * 0.1s)tty.c_cc[VMIN] = 0;// 保存 tty 设置,并检查错误if (tcsetattr(serial_port, TCSANOW, &tty) != 0) {std::cerr << "保存串口属性错误 " << errno << ": " << strerror(errno) << std::endl;return 1;}std::cout << "串口配置成功。" << std::endl;// --- 读写数据 ---unsigned char msg[] = "Hello RS232\n";write(serial_port, msg, sizeof(msg) - 1); // 写入数据,不包括字符串末尾的 '\0'std::cout << "已发送: " << msg;char read_buf[256];memset(&read_buf, '\0', sizeof(read_buf));int num_bytes = read(serial_port, &read_buf, sizeof(read_buf)); // 从串口读取数据if (num_bytes > 0) {std::cout << "接收到 " << num_bytes << " 字节: " << read_buf << std::endl;}// 关闭串口close(serial_port);return 0;
}
RS-485 协议详解
RS-485 是一种多点差分信号传输的物理层标准。它本身不定义数据帧格式,而是经常用于承载像 RS-232 那样的 UART 数据帧。
A. 协议细节
- 电气特性 (Physical Layer)
- 电平标准: 差分信号。使用两根线(通常为 A 和 B)传输。
- 逻辑 ‘1’ (Mark): B 线电压比 A 线电压高 >200mV。
- 逻辑 ‘0’ (Space): A 线电压比 B 线电压高 >200mV。
- 这种差分方式使其具有极强的抗共模噪声能力,支持长距离传输(最长可达1200米)。
- 连接方式: 多点总线。允许在一条总线上连接多个设备(标准为32个单元负载)。
- 通信模式: 常用 2 线制半双工。发送和接收共用一对线,因此任何时候只能有一个设备发送。
- 总线终端: 在总线的两个物理末端,必须各接一个 120Ω 的终端电阻,以消除信号反射。
- 电平标准: 差分信号。使用两根线(通常为 A 和 B)传输。
- 方向控制 (Direction Control) 由于是半双工,收发器芯片(如 MAX485)需要一个控制信号来切换工作模式:
- 发送模式: 将数据从 MCU 的 TX 引脚发送到 A/B 总线上。
- 接收模式: 监听 A/B 总线,将数据发送到 MCU 的 RX 引脚。 这个切换通常由一个 GPIO 引脚控制(常称为 DE/RE,Driver Enable/Receiver Enable)。这是 RS-485 编程与 RS-232 最大的不同点。
B. 时序图
下图展示了在 RS-485 总线上发送一个数据帧的典型时序,关键在于 DE/RE 信号的管理。
<------------------- Transmit Duration -------------------->
Direction Control __________________________________________________________
(DE/RE Pin) | |_____| |_____| | <--> | | <--> |
MCU Data (TXD) | | T1 | <---------- UART Frame on TXD Pin --------> | T2 || | |___________________________________________| |___|__|______|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_|______|___| | S| D0 ... |SP| || | T| | | |
RS-485 Bus (A/B) | | A| VALID DATA ON BUS | | |
(Logical state) _ _ _ |_ _ _ |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _High-Z | | High-Z(Listening) | <--------- Driver is Active -----------> | (Listening)
时序解释:
- 拉高 DE/RE: 在发送数据前,程序必须先将 DE/RE 引脚置为高电平,使能发送器。
- 等待 T1: 留出微小延时 (T1),确保收发器已完全切换到发送模式。
- 发送数据: MCU 通过 UART 发送完整的数据帧(起始位…停止位)。
- 等待 T2: 发送完最后一个停止位后,必须再等待一小段时间 (T2),确保最后一个比特的电平已经在总线上稳定传输。
- 拉低 DE/RE: 将 DE/RE 引脚置为低电平,关闭发送器,切换回收听模式,并释放总线给其他设备使用。
C. C/C++ 代码示例 (Linux with ioctl
)
许多 USB-to-RS485 转换器支持通过 RTS
信号进行自动方向控制。Linux 内核提供了 ioctl
接口来启用此功能,避免了手动 GPIO 操作。
#include <iostream>
#include <string>
#include <cstring>#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <cerrno>
#include <linux/serial.h> // 用于 struct serial_rs485 结构体int main() {// 打开串口设备int serial_port = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY);if (serial_port < 0) {std::cerr << "打开串口错误" << std::endl;return 1;}// 1. 配置标准的串口参数 (波特率, 数据位等)// (这部分与 RS-232 示例完全相同)struct termios tty;if (tcgetattr(serial_port, &tty) != 0) return 1;cfsetispeed(&tty, B9600);cfsetospeed(&tty, B9600);tty.c_cflag &= ~PARENB;tty.c_cflag &= ~CSTOPB;tty.c_cflag |= CS8;tty.c_cflag |= CREAD | CLOCAL;tty.c_lflag &= ~ICANON & ~ECHO & ~ECHOE & ~ISIG;tty.c_oflag &= ~OPOST;tty.c_cc[VMIN] = 0;tty.c_cc[VTIME] = 10;if (tcsetattr(serial_port, TCSANOW, &tty) != 0) {std::cerr << "保存串口属性错误" << std::endl;return 1;}// 2. 使用 ioctl 配置 RS-485 特定参数struct serial_rs485 rs485conf;// 获取当前的 RS-485 配置if (ioctl(serial_port, TIOCGRS485, &rs485conf) < 0) {perror("ioctl 获取 RS485 配置失败");close(serial_port);return 1;}// 启用 RS-485 模式rs485conf.flags |= SER_RS485_ENABLED;// 设置 RTS 在发送数据时为高电平 (RTS_ON_SEND)rs485conf.flags |= SER_RS485_RTS_ON_SEND;// 设置 RTS 在发送数据后为低电平 (RTS_AFTER_SEND)rs485conf.flags &= ~(SER_RS485_RTS_AFTER_SEND);// 设置发送前后 RTS 信号的延时时间 (单位: 毫秒)// 用于确保收发器有足够的时间切换状态rs485conf.delay_rts_before_send = 5; // 发送前延时 5msrs485conf.delay_rts_after_send = 5; // 发送后延时 5ms// 应用新的 RS-485 配置if (ioctl(serial_port, TIOCSRS485, &rs485conf) < 0) {perror("ioctl 设置 RS485 配置失败");close(serial_port);return 1;}std::cout << "RS-485 模式已启用,串口配置完毕。" << std::endl;// --- 模拟主站发送请求并等待从站响应 ---unsigned char request[] = "GET_DATA\n";write(serial_port, request, sizeof(request) - 1);std::cout << "已发送请求: " << request;// 留出时间给从站设备响应// 在实际应用中,你可能会使用 select() 或 poll() 来进行更高效的事件处理usleep(200 * 1000); // 等待 200 毫秒char response[256];memset(&response, '\0', sizeof(response));int bytes_read = read(serial_port, &response, sizeof(response));if (bytes_read > 0) {std::cout << "收到响应: " << response << std::endl;} else {std::cout << "未收到从站响应。" << std::endl;}// 在关闭串口前禁用 RS-485 模式rs485conf.flags &= ~SER_RS485_ENABLED;if (ioctl(serial_port, TIOCSRS485, &rs485conf) < 0) {perror("ioctl 禁用 RS485 模式失败");}// 关闭串口close(serial_port);return 0;
}