常见串口通信协议
串口是一种数据传输接口,通常用于在计算机和外部设备之间传输数据。串口有多种通信协议,其中最常用的是RS232、RS485和UART等协议。
- 不同的串口通信协议的区别主要是通信的距离不一样!
TTL 接口
数码设备和单片机中的TTL电平如下:
- 输入: 高电平: 大于2V 低电平: 小于1.2V
- 输出: 高电平: 大于2.4V 低电平: 小于0.8V
所以接台式机RS232接口与设备连接需要用RS232转TTL电路,或者现在最常用的USB转TTL
RS232接口
特性 | 描述 |
---|---|
类型 | 异步串行通信 |
通信模式 | 点对点(1 对 1) |
电压标准 | ±12V(高)、-12V(低) |
传输距离 | 最长 15 米 |
信号线 | TX(发送)、RX(接收)、RTS、CTS、DTR、DSR、GND |
- 常用于计算机串口(COM 口)通信,如老式鼠标、调制解调器、医疗设备等。
以前的台式机,在机箱背面都有九针DB9接口,信号电平逻辑遵照RS-232规则。
台式机九针D型接口中,虽然有九针实际用到的只有3个,2脚RXD,3脚TXD ,5是GND。
而各种数码设置中的逻辑信号与RS232高低不同,因此不能直接对接。
右图为例针的用途说明如下(1号针位于端口左上角)
RS-485(工业常用)
特性 | 描述 |
---|---|
类型 | 异步串行通信 |
通信模式 | 多点通信(1 对多) |
电压标准 | -7V ~ +12V |
传输距离 | 最大 1200 米 |
信号线 | A(+)、B(-)、GND |
RS-485 适用于:
- 工业自动化(PLC、传感器)
- 远距离通信
- 多设备总线
串口通信引脚连接图
UART引脚连接方法
- RXD:数据输入引脚,数据接受。
- TXD:数据发送引脚,数据发送。
开发板 与 RFID 读卡模块连接
对于两个芯片之间的连接,两个芯片GND共地,同时TXD和RXD交叉连接。这里的交叉连接的意思就是,芯片1的RxD连接芯片2的TXD,芯片2的RXD连接芯片1的TXD。这样,两个芯片之间就可以进行TTL电平通信了。
QT 串口编程
Qt串口模块提供了两个类一个是QSerialPort
提供访问串口的函数,另外一个类QSerialPortInfo
提供有关现有串行端口的信息。如果要在你的项目中使用QSerialPort
和QSerialPortInfo
要包含下面两个头文件
#include <QSerialPort>
#include <QSerialPortInfo>
编译的时候需要连接串口模块,可以在qmake.pro(项目工程文件中添加模块)
QT += serialport
QSerialPort 串口类
构造函数
QSerialPort(const QSerialPortInfo &serialPortInfo, QObject *parent = nullptr)
//name:串口驱动设备路径名 parent:父类指针
QSerialPort(const QString &name, QObject *parent = nullptr)
QSerialPort(QObject *parent = nullptr)
常用函数
bool QSerialPort::open(QIODevice::OpenMode mode) //打开串口驱动
void QSerialPort::close() //关闭串口
--------------------示例---------------------
QSerialPort *port = new QSerialPort("/dev/ttySAC2",this);
if(port->open(QIODevice::ReadWrite))
{
//打开串口成功
}else
{
//打开串口失败
}
串口参数配置枚举
//波特率
enum BaudRate { Baud1200, Baud2400, Baud4800, Baud9600, Baud19200, …, UnknownBaud }
//数据位
enum DataBits { Data5, Data6, Data7, Data8, UnknownDataBits }
//流控制
enum FlowControl { NoFlowControl, HardwareControl, SoftwareControl, UnknownFlowControl }
//校验
enum Parity { NoParity, EvenParity, OddParity, SpaceParity, MarkParity, UnknownParity }
//串口错误
enum SerialPortError { NoError, DeviceNotFoundError, PermissionError, OpenError, NotOpenError, …, UnknownError }
//停止位
enum StopBits { OneStop, OneAndHalfStop, TwoStop, UnknownStopBits }
串口参数配置函数
//设置串口名称
setPortName(const QString &name)
//设置波特率
setBaudRate(qint32 baudRate, QSerialPort::Directions directions = AllDirections)
//设置数据位
setDataBits(QSerialPort::DataBits dataBits)
//设置停止位
setStopBits(QSerialPort::StopBits stopBits)
//设置校验位
setParity(QSerialPort::Parity parity)
//设置流控制
setFlowControl(QSerialPort::FlowControl flowControl)
//向串口发送数据
write(const char *data, qint64 maxSize)
//从串口接收数据
read(char *data, qint64 maxSize)
//获取串口缓冲区中的可用字节数
bytesAvailable() const
QSerialPortInfo串口信息类
获取当前系统所有可用串口的信息
//获取当前系统所有可以使用的串口
[static] QList<QSerialPortInfo> QSerialPortInfo::availablePorts()
//串口描述信息
QString QSerialPortInfo::description() const;
//串口名
QString QSerialPortInfo::portName() const;
综合示例
需求:设置一个串口助手
UI 界面设计
代码实现
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QSerialPort>
#include <QSerialPortInfo>
QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void on_pushButton_openclose_clicked();
void on_pushButton_send_clicked();
private:
Ui::MainWindow *ui;
QSerialPort *serial; // 串口对象
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>
#include <QMessageBox>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
setWindowTitle("串口助手"); // 设置窗口标题
//获取系统所有可用接口显示到comboBox上
for(QSerialPortInfo &info:QSerialPortInfo::availablePorts())
{
qDebug() << info.portName() << info.description();
QSerialPort port(info);//创建串口对象
if(port.open(QIODevice::ReadWrite))//空闲
{
ui->comboBox_port->addItem(port.portName());
//关闭
port.close();
}else//被占用
{
ui->comboBox_port->addItem(port.portName() + "(被占用)");
}
}
//初始化串口对象
serial = new QSerialPort(this);
//绑定可读信号
//连接接收数据的信号和槽
QObject::connect(serial,&QSerialPort::readyRead,this,[&](){
//接收所有数据
QByteArray arr = serial->readAll();
//显示
if(!arr.isEmpty()){
ui->textBrowser->append(QString(arr));
}
});
}
MainWindow::~MainWindow()
{
delete ui;
}
//打开/关闭串口
void MainWindow::on_pushButton_openclose_clicked()
{
if(ui->pushButton_openclose->text() == "打开串口")//打开
{
QString portName = ui->comboBox_port->currentText();//获取名字
serial->setPortName(portName);//设置名字
if(!serial->open(QIODevice::ReadWrite))
{
QMessageBox::warning(this,"提示","打开失败!");
serial->deleteLater();//关闭串口
return;
}
//设置波特率
switch(ui->comboBox_baudrate->currentIndex()){
case 0:
serial->setBaudRate(QSerialPort::Baud2400);
break;
case 1:
serial->setBaudRate(QSerialPort::Baud4800);
break;
case 2:
serial->setBaudRate(QSerialPort::Baud9600);
break;
case 3:
serial->setBaudRate(QSerialPort::Baud38400);
break;
case 4:
serial->setBaudRate(QSerialPort::Baud115200);
break;
}
//设置数据位
switch(ui->comboBox_databits->currentIndex()){
case 0:
serial->setDataBits(QSerialPort::Data5);
break;
case 1:
serial->setDataBits(QSerialPort::Data6);
break;
case 2:
serial->setDataBits(QSerialPort::Data7);
break;
case 3:
serial->setDataBits(QSerialPort::Data8);
break;
}
//设置校验位
switch(ui->comboBox_parity->currentIndex()){
case 0:
serial->setParity(QSerialPort::NoParity);
break;
case 1:
serial->setParity(QSerialPort::OddParity);
break;
case 2:
serial->setParity(QSerialPort::EvenParity);
break;
}
//设置停止位
switch(ui->comboBox_stopbits->currentIndex()){
case 0:
serial->setStopBits(QSerialPort::OneStop);
break;
case 1:
serial->setStopBits(QSerialPort::OneAndHalfStop);
break;
case 2:
serial->setStopBits(QSerialPort::TwoStop);
break;
}
//关闭流控制
serial->setFlowControl(QSerialPort::NoFlowControl);
//关闭选项菜单,使能发送
ui->comboBox_port->setEnabled(false);
ui->comboBox_baudrate->setEnabled(false);
ui->comboBox_databits->setEnabled(false);
ui->comboBox_parity->setEnabled(false);
ui->comboBox_stopbits->setEnabled(false);
ui->pushButton_send->setEnabled(true);
ui->pushButton_openclose->setText("关 闭 串 口");
}else
{
serial->clear();
serial->close();
serial->deleteLater();
ui->comboBox_port->setEnabled(true);
ui->comboBox_baudrate->setEnabled(true);
ui->comboBox_databits->setEnabled(true);
ui->comboBox_parity->setEnabled(true);
ui->comboBox_stopbits->setEnabled(true);
ui->pushButton_send->setEnabled(false);
ui->pushButton_openclose->setText("打 开 串 口");
}
}
void MainWindow::on_pushButton_send_clicked()
{
//获取要发送的数据
QByteArray arr = ui->textEdit->toPlainText().toUtf8();
serial->write(arr);
}
串口模拟
安装后进入下图界面添加模拟串口
复制刚刚写的工程文件生成第二份,记得改名字及工程构建路径
效果
注意:波特率这些串口参数实际选什么取决于你的串口,请查阅厂家给的手册
LInux 串口编程
Linux提供了一系列的接口函数来访问串口,通过调用这些函数就能实现串口传输。步骤非常固定
Linux 串口设备文件
在 Linux 中,串口设备以 文件形式 存在,通常位于:
设备路径 | 设备类型 |
---|---|
/dev/ttyS0 、/dev/ttyS1 | 标准串口(COM 口) |
/dev/ttyUSB0 、/dev/ttyUSB1 | USB-串口 |
/dev/ttyAMA0 | 树莓派 UART 串口 |
/dev/ttySAC0(调试串口) 、/dev/ttySAC1 、/dev/ttySAC2 、/dev/ttySAC3 | gec6818 开发板 |
相关的头文件和数据结构
#include <termios.h>
#define NCCS 32
struct termios
{
tcflag_t c_iflag; /* 输入模式标志 */
tcflag_t c_oflag; /* 输出模式标志 */
tcflag_t c_cflag; /* 控制模式标志 */
tcflag_t c_lflag; /* 本地模式标志 */
cc_t c_line; /* 线路规程 */
cc_t c_cc[NCCS]; /* 控制属性 */
speed_t c_ispeed; /* 输入速度 */
speed_t c_ospeed; /* 输出速度 */
#define _HAVE_STRUCT_TERMIOS_C_ISPEED 1
#define _HAVE_STRUCT_TERMIOS_C_OSPEED 1
};
termios
结构体成员详解
成员 | 描述 |
---|---|
c_iflag | 输入模式标志(影响数据接收) |
c_oflag | 输出模式标志(影响数据发送) |
c_cflag | 控制模式标志(设置波特率、数据位等) |
c_lflag | 本地模式标志(影响终端行为,如回显) |
c_line | 线路规程(一般不使用) |
c_cc[NCCS] | 控制字符(如 VTIME 、VMIN ) |
c_ispeed | 输入波特率(cfsetispeed() ) |
c_ospeed | 输出波特率(cfsetospeed() ) |
c_iflag
(输入模式标志)
c_iflag
用于 控制输入数据的处理方式(如换行转换、流控)。
标志位 | 作用 |
---|---|
IGNBRK | 忽略输入的 BREAK 信号 |
BRKINT | BREAK 产生中断 |
IGNPAR | 忽略奇偶校验错误 |
PARMRK | 标记奇偶校验错误 |
INPCK | 启用输入奇偶校验 |
ISTRIP | 截断高位 bit |
IXON | 启用软件流控(XON/XOFF) |
IXOFF | 启用接收端流控 |
IXANY | 允许任何字符启动输出 |
c_oflag
(输出模式标志)
c_oflag
用于 控制数据如何发送(仅适用于终端,不影响串口)。
标志位 | 作用 |
---|---|
OPOST | 处理输出数据(默认启用) |
ONLCR | 将 \n 转换成 \r\n |
c_cflag
(控制模式标志)😍
c_cflag
设置波特率、数据位、校验位、停止位。
标志位 | 作用 |
---|---|
CS5 / CS6 / CS7 / CS8 | 设置数据位数(5、6、7、8 位) |
PARENB | 启用奇偶校验 |
PARODD | 奇校验(默认偶校验) |
CSTOPB | 2 个停止位(默认 1 个) |
CREAD | 启用接收功能 |
CLOCAL | 忽略 modem 控制信号 |
c_lflag
(本地模式标志)
c_lflag
控制 终端行为(如回显、信号处理)。
标志位 | 作用 |
---|---|
ICANON | 规范模式(启用行缓冲) |
ECHO | 输入时回显字符 |
ECHOE | ERASE 键删除前一个字符 |
ECHOK | KILL 键删除整行 |
ISIG | 启用 INTR 、QUIT 、SUSP 信号 |
c_cc[NCCS]
(控制字符)
用于 控制超时、数据接收行为。
控制字符 | 作用 |
---|---|
VTIME | 读取超时(单位 100ms) |
VMIN | 读取最小字节数 |
c_ispeed
/ c_ospeed
(波特率)
设置输入/输出波特率。
波特率 | 宏定义 |
---|---|
9600 | B9600 |
115200 | B115200 |
串口编程步骤
Linux 下的串口通信主要分为 5 个步骤:
- 打开串口(
open()
)- 配置串口参数(
termios
)- 读写数据(
read()
/write()
)- 刷新 & 控制串口(
tcflush()
/tcdrain()
)- 关闭串口(
close()
)
综合示例
#include <stdio.h> // 标准输入输出
#include <stdlib.h> // 标准库(如 malloc, free)
#include <string.h> // bzero, strlen
#include <unistd.h> // read, write, close
#include <fcntl.h> // open, O_RDWR
#include <errno.h> // 错误处理
#include <termios.h> // 串口配置(B9600, CS8, etc)
#include <pthread.h> // 线程库(pthread_t, pthread_create)
int serialfd;
//线程的任务函数,接收串口信息
void *recvserialmsg(void *arg)
{
char rbuf[100];
while(1)
{
bzero(rbuf,100);
read(serialfd,rbuf,100); //接收串口数据
printf("串口接收到的数据是: %s\n",rbuf);
}
}
//封装函数配置串口
int set_serial()
{
//声明设置串口的结构体
struct termios termios_new;
//清空结构体
bzero(&termios_new,sizeof(termios_new));
//cfmakeraw()设置终端属性,就是设置termios结构体中的各个参数
cfmakeraw(&termios_new);
//设置波特率,双向通信
cfsetispeed(&termios_new,B9600);
cfsetospeed(&termios_new,B9600);
//CLOCAL 和 CREAD分别用于本地连接和接受使能 因此 首先要通过位掩码的方式激活这两个选项
termios_new.c_cflag |= CLOCAL | CREAD;
//通过位掩码设置数据为8位 linux中的串口编程和QT一样(代码都是固定写法)
termios_new.c_cflag &= ~CSIZE; //清空标志位
termios_new.c_cflag |= CS8; // CS7 CS6 CS5
//设置无奇偶校验
termios_new.c_cflag &= ~PARENB; //PARENB:是否使用奇偶校验位,若使用,则设置奇偶校验位类型
//设置停止位为1
termios_new.c_cflag &= ~CSTOPB;
//可设置接受字符和等待时间 无特殊要求可以将其设置为 0
termios_new.c_cc[VTIME] = 2;//等待时间
termios_new.c_cc[VMIN] = 1;//接受字符
//用于清空输入/输出缓冲区
tcflush(serialfd,TCIFLUSH); //TCIFLUSH:刷新输入缓冲区
//完成配置后,可以使用以下函数激活串口设置
tcsetattr(serialfd,TCSANOW,&termios_new); //TCSANOW:立即执行
return 0;
}
int main()
{
pthread_t id;//定义线程id
char sbuf[100];
//打开串口驱动
serialfd = open("/dev/ttySAC1",O_RDWR);
if (serialfd == -1)
{
perror("打开串口失败");
return -1;
}
//配置串口
set_serial();
//收发信息
//创建线程专门接收信息
pthread_create(&id,NULL,recvserialmsg,NULL);
//主函数专门发送信息
while(1)
{
bzero(sbuf,100);
printf("请输入要发送的数据!\n");
scanf("%s",sbuf);
write(serialfd,sbuf,strlen(sbuf));
}
//关闭串口
close(serialfd);
return 0;
}
参考资料
RFID 模块的开发
RFID(Radio Frequency Identification,射频识别) 是一种
无线通信技术,通过 射频信号 实现
无接触式数据传输和识别,可用于
物联网(IoT)、物流、门禁、支付系统等。工作原理
- RFID 读写器 发出 无线射频信号。
- RFID 标签(Tag) 内部的 芯片+天线 接收信号,并返回数据。
- RFID 读写器 解析数据,进行存储或操作。
RFID 组成部分
1️⃣ RFID 标签(RFID Tag)
作用:存储数据,贴在 物品、卡片、设备 上。
- 无源标签(Passive RFID):无电池,靠读写器供电,廉价,适合 门禁卡、商品管理。
- 有源标签(Active RFID):内置电池,发射信号,适合 远距离追踪(如车辆定位)。
2️⃣ RFID 读写器(RFID Reader)
作用:发射射频信号,并读取标签数据。
- 固定式读写器:安装在固定位置,如 门禁、仓库出入口。
- 手持式读写器:移动式,如 快递扫描枪。
3️⃣RFID 天线
作用:用于无线数据传输,决定通信距离。
4️⃣ RFID 数据管理系统
作用:处理 RFID 读取的数据,连接 数据库、物联网系统、ERP。
RFID 读卡流程
UART 接口一帧的数据格式为 1 个起始位,8 个数据位、无奇偶校验位、1 个停止位,波特率固定为 9600。
数据帧
字段 | 长度 | 说明 |
---|---|---|
FrameLen | 1 byte | 整个数据帧的总长度(从 FrameLen 到 ETX 的字节数) |
SEQ/CmdType | 1 byte | 包号(SEQ)或命令类型(CmdType),标识命令类别(如设备控制或 ISO14443A) |
Cmd/Status | 1 byte | 具体命令(主机发送时)或状态(从机应答时) |
Length | 1 byte | Info 字段的长度(N bytes) |
Info | N bytes | 可变长度的信息内容(如参数或返回数据) |
BCC | 1 byte | 校验和,用于验证数据完整性 |
ETX | 1 byte | 结束符,固定为 0x03 ,标志数据帧结束 |
校验和规则
请求数据帧设计
char Request[8]={0};
Request[0] = 0x07; //帧长
Request[1] = 0x02; //命令类型
Request[2] = 0x41; //命令
Request[3] = 0x01; //数据长度
Request[4] = 0x52; //数据信息
//计算校验和
int BCC = 0;
for(int i=0;i<Request[0]-2;i++)
{
BCC ^= Request[i];
}
Request[5] = ~BCC; //校验和
Request[6] = 0x03; //结束标记
//发送数据帧给 RFID 模块
port->write(Request,7);
应答数据的处理
void MainWindow::read_rfid()
{
QByteArray bye = port->readAll();
// //遍历数据帧的每一位
// for(int i=0;i<bye.length();i++)
// {
// qDebug() << (int)bye.at(i);
// }
//判断状态位,确定是否请求成功!
if(bye.length() == 0x08 && bye.at(2) == 0x00)
{
qDebug() << "Request Successful";
//设计防碰撞数据帧
//发送防碰撞协议
}else
{
qDebug() << "Request fail";
}
}
防碰撞数据帧设计
//设计防碰撞协议
char AntiCollision[9]={0};
AntiCollision[0] = 0x08; //帧长
AntiCollision[1] = 0x02; //命令类型
AntiCollision[2] = 0x42; //命令
AntiCollision[3] = 0x02; //数据长度
AntiCollision[4] = 0x93;
AntiCollision[5] = 0x00;
//计算校验和
int BCC = 0;
for(int i=0;i<Request[0]-2;i++)
{
BCC ^= Request[i];
}
AntiCollision[6] = ~BCC;
AntiCollision[7] = 0x03;
//发送防碰撞协议
port->write(AntiCollision,8);
应答数据的处理
f(bye.length() == 0x0a && bye.at(2) == 0x00)
{
qDebug() << "AntiCollision Successful";
//卡号 4 5 6 7
QString id = QString::asprintf("%d%d%d%d",bye.at(7),bye.at(6),bye.at(5),bye.at(4));
qDebug() << "ID:" << id;
}else
{
qDebug() << "RFID fail";
}