目录
一、什么是串口通信
串口通信(Serial Communication)是指数据一位一位地按顺序传输的通信方式。与之相对的是并行通信,多位数据同时通过多根线传输。
串口是嵌入式开发中最基础、最常用的通信接口。从 Arduino 的 Serial.print(),到工业 PLC 的 Modbus,再到你电脑曾经的 COM 口,底层都是串口通信。
① 协议层:UART(帧格式、波特率约定)
② 电气层:TTL / RS-232 / RS-485 / RS-422(电平标准)
③ 应用层:Modbus / AT 指令 / 自定义协议
同一个 UART 协议,搭不同电气层 = 不同串口标准。这是理解串口的关键。
二、串行 vs 并行:为什么串口赢了
早期计算机用并行口(LPT 打印口、IDE 硬盘)因为看起来"8 根线同时传 8 位 = 8 倍速度"。但并行最终被串行取代,原因有三:
① 线少 → 串扰小:1~2 根线没有线间干扰
② 差分 → 抗扰强:RS-485/USB 用差分信号,不怕共模噪声
③ 频率可拉满:不用等 8 根线同步到达,单线可跑极高频率
三、通信三要素:方向、时钟、电平
3.1 方向:单工 / 半双工 / 全双工
3.2 时钟:同步 vs 异步
3.3 电平:TTL / RS-232 / RS-485 / RS-422
这是串口种类最多的维度。同一个 UART 协议,配上不同电平标准,就变成了不同"串口":
| 电平标准 | 逻辑 1 | 逻辑 0 | 传输方式 | 典型电压 |
|---|---|---|---|---|
| TTL | 高电平 | 低电平 | 单端 | 0V / 3.3V 或 5V |
| RS-232 | 负电压 | 正电压 | 单端 | -12V / +12V |
| RS-485 | A<B | A>B | 差分 | ±2V 差分 |
| RS-422 | A<B | A>B | 差分(双对) | ±2V 差分 |
UART ≠ RS-232 ≠ RS-485。UART 是协议(帧格式),RS-232/485 是电平标准。一个 MCU 的 UART 引脚输出的是 TTL 电平,需要 MAX232 芯片转成 RS-232 电平,或用 MAX485 芯片转成 RS-485 差分信号。
四、UART:串口的大脑
UART(Universal Asynchronous Receiver/Transmitter,通用异步收发器)是串口通信的协议核心。它不是一种电平,而是规定了数据如何打包成帧、如何收发。
4.1 UART 硬件结构
- 发送器 TX:将并行数据转成串行位流,自动加起始位/停止位
- 接收器 RX:检测起始位,按约定波特率采样,还原并行数据
- 波特率发生器:产生收发时钟,决定"语速"
- FIFO(可选):缓冲收发数据,减少 CPU 中断频率
五、串口帧格式:数据是怎么排队的
UART 的核心是帧格式。一个 UART 帧由 4 部分组成:
5.1 各字段详解
| 字段 | 长度 | 说明 |
|---|---|---|
| 起始位 | 1 bit | 固定为 0(下降沿),标志帧开始。空闲时总线为高电平 |
| 数据位 | 5~8 bit | 实际数据,低位(LSB)先发。常用 8 位(1 字节) |
| 校验位 | 0~1 bit | 奇校验/偶校验/无校验。用于简单错误检测 |
| 停止位 | 1~2 bit | 固定为 1(高电平),标志帧结束。常用 1 位 |
总线空闲时一直是高电平。当接收方检测到下降沿(高→低跳变),就知道"有数据来了",然后按约定波特率在每个位的中点采样。这就是异步通信无需时钟线的秘密。
5.2 常见配置
串口配置通常写成 波特率-数据位-校验-停止位 格式:
- 115200-8-N-1:最常用。115200 波特率,8 数据位,无校验,1 停止位
- 9600-8-E-1:工业常用。9600 波特率,偶校验
- 9600-7-E-1:Modbus RTU 标准配置
六、波特率:通信的"语速"
波特率(Baud Rate)是每秒传输的码元数。在 UART 中,1 个码元 = 1 bit,所以波特率 = 比特率。常见值:9600、115200、921600 等。
收发双方波特率必须一致。如果差超过 3%,采样会"错位"导致乱码。这就是为什么不能用 RC 振荡器(误差 5%+),必须用晶振(误差 < 0.1%)。
6.1 常见波特率与场景
| 波特率 | 字节/秒 | 典型场景 |
|---|---|---|
| 2400 | 240 | 老式 GPS 模块、低速传感器 |
| 9600 | 960 | 工业仪表、Modbus 从站默认 |
| 19200 | 1920 | 蓝牙模块 AT 配置 |
| 38400 | 3840 | 部分 RFID 读卡器 |
| 115200 | 11520 | 调试输出、高精度 GPS |
| 921600 | 92160 | 固件烧录、高速数据采集 |
七、TTL 串口:MCU 的母语
TTL 串口是 MCU(单片机)直接输出的串口电平。0V 表示逻辑 0,3.3V 或 5V 表示逻辑 1。
7.1 TTL 串口的三种电压
| MCU 电压 | 逻辑 0 | 逻辑 1 | 典型芯片 |
|---|---|---|---|
| 5V TTL | 0V | 5V | Arduino UNO / 传统 51 |
| 3.3V TTL | 0V | 3.3V | STM32 / ESP32 / ESP8266 |
| 1.8V TTL | 0V | 1.8V | 部分低功耗 MCU |
5V MCU 的 TX 输出 5V 高电平,直接接到 3.3V MCU 的 RX 会烧毁后者。需要电平转换(如 TXS0108E)或串联电阻限流。
八、RS-232:经典的 DB9 串口
RS-232是 1960 年代的标准,用 负逻辑:-3V~-15V 表示逻辑 1,+3V~+15V 表示逻辑 0。采用 DB9 接口(9 针 D 型连接器)。
8.1 DB9 接口引脚
两台电脑用 RS-232 通信,TXD 对 RXD 交叉连接:
A 的 TXD(3) → B 的 RXD(2)
A 的 RXD(2) ← B 的 TXD(3)
A 的 GND(5) ↔ B 的 GND(5)
这就是"串口交叉线"的由来。
九、RS-485:工业总线的王者
RS-485采用差分信号传输,用两根线 A 和 B,信号靠电压差判断。最多 32~256 节点,传输距离 1200 米。
RS-232 = 单端(参考地)+ 全双工 + 点对点 + 15 米
RS-485 = 差分(A-B)+ 半双工 + 总线多节点 + 1200 米
软件层完全一样(都是 UART),只是电气层不同。
十、RS-422:全双工差分方案
RS-422是 RS-485 的"全双工版本"。用两对差分线(4 根),一对发(TX+/TX-),一对收(RX+/RX-),同时收发。
| 维度 | RS-485 | RS-422 |
|---|---|---|
| 线数 | 2 线(A/B) | 4 线(TX+/TX-/RX+/RX-) |
| 双工 | 半双工 | 全双工 |
| 拓扑 | 总线,多节点 | 点对点/一点对多点 |
| 节点数 | 32~256 | 10(标准) |
| 距离 | 1200m | 1200m |
| 成本 | 低 | 中(多一对线) |
| 典型 | 工业总线 | 全双工仪表 |
十一、全家族对比与选型
11.1 选型决策树
| 需求 | 推荐 | 理由 |
|---|---|---|
| MCU 与模块近距离通信 | TTL | 无需电平芯片,直连即可 |
| PC 调试输出 | RS-232 / USB转TTL | PC 有 USB,转 TTL 最方便 |
| 工业多节点总线 | RS-485 | 差分抗扰 + 多节点 + 远距离 |
| 全双工远距离 | RS-422 | 同时收发 + 差分 |
| 高实时 + 多主 + 安全 | CAN | 仲裁 + 硬件 CRC + 自动降级 |
十二、串口硬件:接口与线缆
12.1 常见物理接口
12.2 电平转换芯片
| 芯片 | 转换 | 供电 | 特点 |
|---|---|---|---|
| MAX232 | TTL ↔ RS-232 | 5V | 经典 RS-232 转换,内置电荷泵 |
| MAX3232 | TTL ↔ RS-232 | 3.3V | 3.3V 版本,低功耗 |
| MAX485 | TTL ↔ RS-485 | 5V | 半双工,最常用 485 芯片 |
| MAX488 | TTL ↔ RS-422 | 5V | 全双工,4 线制 |
| SP3485 | TTL ↔ RS-485 | 3.3V | 3.3V 低功耗 485 |
| ADM2483 | TTL ↔ RS-485 | 5V | 隔离型,5000V 隔离 |
| CH340 | USB ↔ TTL | 5V | USB 转串口,便宜好用 |
| CP2102 | USB ↔ TTL | 3.3V | Silicon Labs,更稳定 |
十三、Qt/C++ 实战代码
13.1 Qt 串口通信基础
// SerialPort.h —— Qt 串口封装 #ifndef SERIALPORT_H #define SERIALPORT_H #include <QObject> #include <QSerialPort> #include <QSerialPortInfo> #include <QByteArray> class SerialPort : public QObject { Q_OBJECT public: explicit SerialPort(QObject *parent = nullptr); ~SerialPort(); // 列举可用串口 static QStringList listPorts(); // 打开串口 bool open(const QString &portName, qint32 baudRate = 115200, QSerialPort::DataBits dataBits = QSerialPort::Data8, QSerialPort::Parity parity = QSerialPort::NoParity, QSerialPort::StopBits stopBits = QSerialPort::OneStop); void close(); bool isOpen() const; qint64 write(const QByteArray &data); signals: void dataReceived(const QByteArray &data); void errorOccurred(const QString &error); private slots: void onReadyRead(); void onErrorOccurred(QSerialPort::SerialPortError error); private: QSerialPort *m_serial; QByteArray m_buffer; // 接收缓冲区 }; #endif
// SerialPort.cpp —— 实现 #include "SerialPort.h" #include <QDebug> QStringList SerialPort::listPorts() { QStringList ports; for (const QSerialPortInfo &info : QSerialPortInfo::availablePorts()) { ports << QString("%1 (%2 - %3)") .arg(info.portName()) .arg(info.description()) .arg(info.manufacturer()); } return ports; } SerialPort::SerialPort(QObject *parent) : QObject(parent), m_serial(new QSerialPort(this)) { connect(m_serial, &QSerialPort::readyRead, this, &SerialPort::onReadyRead); connect(m_serial, &QSerialPort::errorOccurred, this, &SerialPort::onErrorOccurred); } SerialPort::~SerialPort() { if (m_serial->isOpen()) m_serial->close(); } bool SerialPort::open(const QString &portName, qint32 baudRate, QSerialPort::DataBits dataBits, QSerialPort::Parity parity, QSerialPort::StopBits stopBits) { m_serial->setPortName(portName); m_serial->setBaudRate(baudRate); m_serial->setDataBits(dataBits); m_serial->setParity(parity); m_serial->setStopBits(stopBits); m_serial->setFlowControl(QSerialPort::NoFlowControl); if (!m_serial->open(QIODevice::ReadWrite)) { emit errorOccurred(m_serial->errorString()); return false; } return true; } void SerialPort::close() { m_serial->close(); } bool SerialPort::isOpen() const { return m_serial->isOpen(); } qint64 SerialPort::write(const QByteArray &data) { return m_serial->write(data); } void SerialPort::onReadyRead() { QByteArray data = m_serial->readAll(); m_buffer.append(data); // 按换行符分包(常见协议以 \n 或 \r\n 分隔) int idx; while ((idx = m_buffer.indexOf('\n')) >= 0) { QByteArray line = m_buffer.left(idx + 1); m_buffer.remove(0, idx + 1); emit dataReceived(line.trimmed()); } } void SerialPort::onErrorOccurred(QSerialPort::SerialPortError error) { if (error != QSerialPort::NoError) { emit errorOccurred(m_serial->errorString()); } }
13.2 实战:NMEA GPS 数据解析
// main.cpp —— 读取 GPS 模块 NMEA 数据 #include <QCoreApplication> #include "SerialPort.h" #include <QDebug> #include <QRegularExpression> int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); SerialPort serial; // GPS 模块通常 9600 波特率 if (!serial.open("COM3", 9600)) { qDebug() << "打开串口失败"; return -1; } QObject::connect(&serial, &SerialPort::dataReceived, [](const QByteArray &line) { QString msg = QString::fromUtf8(line); // 解析 NMEA GPRMC 语句:$GPRMC,时间,状态,纬度,N,经度,E,速度,航向,日期 if (msg.startsWith("$GPRMC")) { QStringList parts = msg.split(','); if (parts.size() >= 10) { QString status = parts[2]; // A=有效, V=无效 QString lat = parts[3]; // 纬度 ddmm.mmmm QString lon = parts[5]; // 经度 dddmm.mmmm QString speed = parts[7]; // 速度(节) if (status == "A") { qDebug() << "GPS有效:" << "Lat=" << lat << "Lon=" << lon << "Speed=" << speed << "knots"; } } } }); QObject::connect(&serial, &SerialPort::errorOccurred, [](const QString &err) { qDebug() << "串口错误:" << err; }); qDebug() << "GPS 监听已启动..."; return app.exec(); }
13.3 RS-485 方向控制
// RS-485 半双工需要控制收发方向 // DE/RE 引脚:高=发送,低=接收 void sendRs485(QSerialPort *serial, const QByteArray &data) { // 1. 拉高 RTS(DE=1),进入发送模式 serial->setRequestToSend(true); // 2. 发送数据 serial->write(data); serial->flush(); // 3. 等待最后一个字节发送完毕 while (serial->bytesToWrite() > 0) { serial->waitForBytesWritten(100); } // 4. 关键延时:等待最后一个位真正离开移位寄存器 QThread::msleep(2); // 5. 拉低 RTS(DE=0),切回接收模式 serial->setRequestToSend(false); } // 完整的 Modbus RTU 主站轮询示例 class ModbusMaster : public QObject { Q_OBJECT public: explicit ModbusMaster(QSerialPort *s, QObject *p = nullptr) : QObject(p), m_serial(s) { connect(m_serial, &QSerialPort::readyRead, this, &ModbusMaster::onRead); } void readHoldingRegisters(quint8 slave, quint16 addr, quint16 count) { QByteArray req; req.append(char(slave)); // 从站地址 req.append(char(0x03)); // 功能码:读保持寄存器 req.append(char((addr >> 8) & 0xFF)); req.append(char(addr & 0xFF)); req.append(char((count >> 8) & 0xFF)); req.append(char(count & 0xFF)); quint16 crc = crc16Modbus(req); req.append(char(crc & 0xFF)); req.append(char((crc >> 8) & 0xFF)); sendRs485(m_serial, req); // 485 方向控制发送 } private slots: void onRead() { QByteArray resp = m_serial->readAll(); if (resp.size() >= 5) { quint8 slave = resp[0]; quint8 func = resp[1]; quint8 byteCount = resp[2]; qDebug() << "从站" << slave << "功能码" << QString::number(func, 16); } } private: QSerialPort *m_serial; // Modbus CRC16 计算 quint16 crc16Modbus(const QByteArray &data) { quint16 crc = 0xFFFF; for (int i = 0; i < data.size(); i++) { crc ^= (quint8)data[i]; for (int j = 0; j < 8; j++) { if (crc & 1) crc = (crc >> 1) ^ 0xA001; else crc >>= 1; } } return crc; } };
① 异步接收 + 缓冲区:串口数据可能分多次到达,必须缓冲累积后按帧分隔
② 485 方向延时:发送后等 2ms 再切接收,防止最后一个字节被截断
③ 超时处理:Modbus 等请求-响应协议必须有超时机制,不能死等
十四、应用场景全景
十五、调试技巧与常见问题
15.1 调试三板斧
- 先确认物理层:万用表测 TX/RX 引脚有无电压跳变,排除接线问题
- 用串口助手回环测试:TX 短接 RX,发什么收什么 → 证明本机串口正常
- 抓包对比:用逻辑分析仪或串口监控工具,看实际波形和数据
15.2 常见问题速查
| 现象 | 原因 | 解决 |
|---|---|---|
| 收到乱码 | 波特率不匹配 | 双方波特率必须一致 |
| 收到部分数据 | 缓冲区未分包 | 按帧分隔符拆包 |
| 485 收不到 | A/B 接反 | 对调 A/B 线 |
| 485 尾字节丢失 | 方向切换过早 | 发送后延时 2ms 再切接收 |
| TTL 直连烧 MCU | 5V 接 3.3V | 加电平转换 |
| 偶发误码 | 线路干扰/波特率容差 | 用双绞线/晶振 |
| 跨平台端口名变 | USB 转串口每次枚举不同 | 用序列号固定端口 |
① 波特率容差:收发差超过 3% 就乱码,别用内部 RC 振荡器
② 共地:TTL/RS-232/RS-485 都要共地,不共地通信不稳定
③ 485 终端电阻:总线两端各 120Ω,不是每个节点都接
15.3 常用调试工具
| 工具 | 平台 | 用途 |
|---|---|---|
| 串口调试助手 | Win/Linux | 手动收发测试 |
| putty / minicom | 跨平台 | 终端式串口交互 |
| 逻辑分析仪 | 硬件 | 看实际波形,排查时序 |
| Modbus Poll/Slave | Win | Modbus 主站/从站模拟 |
| Virtual Serial Port | Win | 虚拟串口对,无硬件测试 |
十六、总结
本文知识全景
- 串口本质:数据一位一位顺序传输,1 根线即可通信
- 三层结构:协议层(UART) + 电气层(TTL/232/485/422) + 应用层(Modbus/AT)
- 串行 vs 并行:线少→无串扰→频率高→串行赢了
- 通信三要素:方向(单工/半双工/全双工) + 时钟(同步/异步) + 电平
- UART 帧格式:起始位(1) + 数据位(5~8) + 校验位(0~1) + 停止位(1~2)
- 波特率:每秒传输位数,双方必须一致(容差 < 3%)
- TTL:0V/3.3V 正逻辑,MCU 母语,< 1m,无抗扰
- RS-232:±12V 负逻辑,单端全双工,15m,DB9 接口
- RS-485:差分半双工,1200m,32~256 节点,工业总线王者
- RS-422:差分全双工,4 线制,1200m,全双工仪表
- 电平转换:MAX232(TTL→232) / MAX485(TTL→485) / CH340(USB→TTL)
- Qt 编程:QSerialPort + readyRead 异步 + 缓冲区拆帧 + 485 方向控制
- 调试要点:共地 + 波特率一致 + 485 终端电阻 + 方向延时
串口通信是嵌入式工程师的"ABC":看起来简单(两根线),但背后藏着异步同步、单端差分、帧格式、波特率、电平转换等一整套知识体系。
理解串口后,再看 CAN、USB、I2C、SPI 都会有"似曾相识"的感觉——它们都在解决同一类问题:如何在两个或多个设备之间可靠地传输比特。差别只在于"怎么打包、怎么传输、怎么纠错"。
动手建议:买一个 USB-TTL 模块(CH340 或 CP2102,十元一个),用 Qt 串口示例在自己电脑上收发数据,比读百遍文章更有效。