💪 图像算法工程师,专业从事且热爱图像处理,图像处理专栏更新如下👇:
📝《图像去噪》
📝《超分辨率重建》
📝《语义分割》
📝《风格迁移》
📝《目标检测》
📝《图像增强》
📝《模型优化》
📝《模型实战部署》
📝《图像配准融合》
📝《数据集》
📝《高效助手》
📝《C++》
📝《Qt》
串口通信(Serial Communication),就是一种数据一位一位(逐位)顺序发送和接收的通信方式,是计算机和外部设备(如传感器、单片机、控制器)之间常见的通信方式之一。
目录
一、串口通信
可以把串口通信想象成:两个人通过“对讲机”说话,每次只能说一个字(1位),对方按顺序接收这些字,然后组成完整的信息。
1.1 串口通信特点
串口通信特点见下:
1.2 串口通信的基本引脚
串口通信的基本引脚,以RS-232为例:
实例引脚图片见下:
1.3 串口通信中的数据结构
一个完整的串口数据帧一般包括:
[起始位][数据位][校验位][停止位]
参数解析:
起始位:通常是一个 0,告诉对方“我要开始发数据了”
数据位:一般是 8 位(1个字节)
校验位:可选,用来检测数据有没有出错(奇偶校验)
停止位:1或2位 1,表示“数据发完了”
1.4 串口通信分类
1.4.1 同步串口通信
同步串口通信,有时钟信号(clock),发送接收都依靠统一节奏。
比如 SPI、I2C。
1.4.2 异步串口通信
用的比较多的为异步串口通信,无时钟,通过起始位、停止位等方式同步。
比如 RS-232、RS-485、TTL 串口等。
1.5 常见串口通信接口
常见串口通信接口见下:
1.6 通俗理解串口通信
结合上面的串口通信原理,下面举一个通俗理解串口通信的例子:
假设有一个串口设备(比如一个智能灯控制板),你用 C++ 程序通过串口向它发送一条指令,让它开灯。
1.6.1 设计协议
约定:每条数据格式是这样的:[帧头][命令][参数][校验]
帧头:0xAA
命令:“打开灯”是 0x01
参数:0x01 表示第1盏灯
校验:简单加法校验 = 命令 + 参数
示例指令:0xAA 0x01 0x01 0x02
1.6.2 发送数据
用串口程序发送这4个字节的数据给设备:
unsigned char buf[] = {0xAA, 0x01, 0x01, 0x02};
write(serial_fd, buf, 4); // 假设 serial_fd 是串口设备句柄
假设小明通过对讲机说:开始、打开灯、灯1、确认。
1.6.3 接收数据
设备收到数据后:
检查帧头是不是 0xAA;
读取命令 0x01,查表知道是“打开灯”;
读取参数 0x01,知道是“第1盏灯”;
校验是否正确(0x01+0x01 = 0x02),OK!
执行动作后,设备可能再通过串口回一条响应:0xAA 0x81 0x01 0x82 表示“灯1打开成功”。
二、串口通信实例
2.1 C++代码
参考了博文https://blog.csdn.net/weixin_44353958/article/details/104156394中的代码,感谢作者,结合我自己的需求修改整合成一套代码,我主要是通过发送十六进制指令到硬件板子上,控制相机的快门开和关。
2.1.2 参数修改
使用代码过程需要修改的参数见下。
2.1.2.1 端口号
注意:在 Windows 系统中,CreateFile 打开串口时:对于 COM1 到 COM9,你可以直接写成:
CreateFile(“COM1”, …),但是如果是 COM10 及以上(如 COM17),你必须写成:
CreateFile(“\\.\COM17”, …)。
下面是代码中修改端口号对应位置:
2.1.2.2 指令发送
待发送的指令,要先明确好自己需要以什么数据发送,对应的定义数据类型,见下:
2.1.2.3 数据接收
数据的接收格式和长度可以自定义调整,注意不要丢包。
2.1.3 完整代码
C++实现串口通信中数据指令的发送与接收完整代码见下:
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <WinSock2.h>
#include <windows.h>
#include <iostream>
#include <string>
#include <thread> // 包含线程相关功能,用于延时
#include <chrono> // 包含时间单位定义
#include <vector>
using namespace std;
// === WZSerialPort 类的定义 ===
#ifndef _WZSERIALPORT_H
#define _WZSERIALPORT_H
class WZSerialPort
{
public:
WZSerialPort();
~WZSerialPort();
// 打开串口,成功返回true,失败返回false
// 参数:端口号、波特率、校验位、数据位、停止位、同步/异步标志
bool open(const char* portname, int baudrate = 115200, char parity = 0, char databit = 8, char stopbit = 1, char synchronizeflag = 1);
// 关闭串口
void close();
// 发送数据,成功返回发送数据长度,失败返回0
int send(string dat);
// 接收数据,成功返回读取的实际数据,失败返回空字符串
string receive();
private:
int pHandle[16]; // 用于存储串口句柄
char synchronizeflag; // 同步或异步标志
};
#endif
// === WZSerialPort 类的实现 ===
WZSerialPort::WZSerialPort()
{
// 构造函数,初始化成员变量
}
WZSerialPort::~WZSerialPort()
{
// 析构函数,释放资源
}
// 打开串口
bool WZSerialPort::open(const char* portname, int baudrate, char parity, char databit, char stopbit, char synchronizeflag)
{
this->synchronizeflag = synchronizeflag; // 设置同步/异步标志
HANDLE hCom = NULL; // 串口句柄
if (this->synchronizeflag)
{
// 同步方式打开串口
hCom = CreateFileA(portname, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
}
else
{
// 异步方式打开串口
hCom = CreateFileA(portname, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
}
if (hCom == (HANDLE)-1)
{
// 打开串口失败
return false;
}
// 配置串口缓冲区大小
if (!SetupComm(hCom, 1024, 1024))
{
return false;
}
// 配置串口参数
DCB p;
memset(&p, 0, sizeof(p));
p.DCBlength = sizeof(p);
p.BaudRate = baudrate; // 设置波特率
p.ByteSize = databit; // 设置数据位
// 设置校验位
switch (parity)
{
case 0:
p.Parity = NOPARITY; // 无校验
break;
case 1:
p.Parity = ODDPARITY; // 奇校验
break;
case 2:
p.Parity = EVENPARITY; // 偶校验
break;
case 3:
p.Parity = MARKPARITY; // 标记校验
break;
}
// 设置停止位
switch (stopbit)
{
case 1:
p.StopBits = ONESTOPBIT; // 1位停止位
break;
case 2:
p.StopBits = TWOSTOPBITS; // 2位停止位
break;
case 3:
p.StopBits = ONE5STOPBITS; // 1.5位停止位
break;
}
// 应用串口配置
if (!SetCommState(hCom, &p))
{
// 设置参数失败
return false;
}
// 配置超时设置
COMMTIMEOUTS TimeOuts;
TimeOuts.ReadIntervalTimeout = 1000; // 读间隔超时
TimeOuts.ReadTotalTimeoutMultiplier = 500; // 读时间系数
TimeOuts.ReadTotalTimeoutConstant = 5000; // 读时间常量
TimeOuts.WriteTotalTimeoutMultiplier = 500; // 写时间系数
TimeOuts.WriteTotalTimeoutConstant = 2000; // 写时间常量
SetCommTimeouts(hCom, &TimeOuts);
// 清空串口缓冲区
PurgeComm(hCom, PURGE_TXCLEAR | PURGE_RXCLEAR);
// 保存串口句柄
memcpy(pHandle, &hCom, sizeof(hCom));
return true;
}
// 关闭串口
void WZSerialPort::close()
{
HANDLE hCom = *(HANDLE*)pHandle; // 获取串口句柄
CloseHandle(hCom); // 关闭句柄
}
// 发送数据
int WZSerialPort::send(string dat)
{
HANDLE hCom = *(HANDLE*)pHandle; // 获取串口句柄
if (this->synchronizeflag)
{
// 同步方式发送数据
DWORD dwBytesWrite = dat.length(); // 要发送的数据长度
BOOL bWriteStat = WriteFile(hCom, (char*)dat.c_str(), dwBytesWrite, &dwBytesWrite, NULL);
if (!bWriteStat)
{
// 发送失败
return 0;
}
return dwBytesWrite; // 返回成功发送的字节数
}
else
{
// 异步方式发送数据
DWORD dwBytesWrite = dat.length();
DWORD dwErrorFlags;
COMSTAT comStat;
OVERLAPPED m_osWrite;
memset(&m_osWrite, 0, sizeof(m_osWrite));
m_osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, L"WriteEvent");
ClearCommError(hCom, &dwErrorFlags, &comStat);
BOOL bWriteStat = WriteFile(hCom, (char*)dat.c_str(), dwBytesWrite, &dwBytesWrite, &m_osWrite);
if (!bWriteStat)
{
if (GetLastError() == ERROR_IO_PENDING)
{
// 等待异步操作完成
WaitForSingleObject(m_osWrite.hEvent, 1000);
}
else
{
// 发送失败
ClearCommError(hCom, &dwErrorFlags, &comStat);
CloseHandle(m_osWrite.hEvent);
return 0;
}
}
return dwBytesWrite; // 返回成功发送的字节数
}
}
// 接收数据
string WZSerialPort::receive()
{
HANDLE hCom = *(HANDLE*)pHandle; // 获取串口句柄
string rec_str = ""; // 用于存储接收的数据
char buf[1024]; // 缓冲区
DWORD wCount = 1024; // 缓冲区大小
if (this->synchronizeflag)
{
// 同步方式接收数据
BOOL bReadStat = ReadFile(hCom, buf, wCount, &wCount, NULL);
if (bReadStat)
{
for (int i = 0; i < wCount; i++) {
// 过滤 0x0D 和 0x0A
if (buf[i] == 0x0D || buf[i] == 0x0A) {
continue;
}
rec_str += buf[i];
}
}
return rec_str; // 返回接收的数据
}
else
{
// 异步方式接收数据
DWORD wCount = 1024;
DWORD dwErrorFlags;
COMSTAT comStat;
OVERLAPPED m_osRead;
memset(&m_osRead, 0, sizeof(m_osRead));
m_osRead.hEvent = CreateEvent(NULL, TRUE, FALSE, L"ReadEvent");
ClearCommError(hCom, &dwErrorFlags, &comStat);
if (!comStat.cbInQue)
return ""; // 如果输入缓冲区为空,返回空字符串
BOOL bReadStat = ReadFile(hCom, buf, wCount, &wCount, &m_osRead);
if (!bReadStat) {
if (GetLastError() == ERROR_IO_PENDING) {
GetOverlappedResult(hCom, &m_osRead, &wCount, TRUE);
}
else {
ClearCommError(hCom, &dwErrorFlags, &comStat);
CloseHandle(m_osRead.hEvent);
return "";
}
}
// 关键修改:遍历实际读取的字节数 wCount,过滤 0x0D 和 0x0A
for (DWORD i = 0; i < wCount; i++) {
// 跳过 0x0D(回车)和 0x0A(换行)
if (buf[i] == 0x0D || buf[i] == 0x0A) {
continue;
}
rec_str += buf[i];
}
// 确保释放 OVERLAPPED 事件句柄(避免内存泄漏)
CloseHandle(m_osRead.hEvent);
return rec_str;
}
}
// === 主函数 ===
int main()
{
WZSerialPort w; // 创建串口对象
// 打开串口,这里以 COM17 为例
if (w.open("\\\\.\\COM19"))
{
cout << "串口打开成功" << endl;
// 发送指令
string instruction_send = "aa 55 55 aa 80 02 02 00 04 ff";
string instruction_send = "aa 55 55 aa 80 02 02 01 00 ff"; // 关闭快门
string instruction_send = "aa 55 55 aa 80 02 02 02 00 ff"; // 打开快门
//string instruction_send = "aa 55 55 aa 81 01 00 00 00 ff"; // 发送指令后返回foc数据
/*string instruction_send = "aa 55 55 aa 80 02 02 00 04 ff";
w.send(instruction_send);
std::cout << "成功发送指令:" << instruction_send << std::endl;*/
// 十六进制指令数组
std::vector<unsigned char> instruction_send = {
0xAA, 0x55, 0x55, 0xAA, 0x81, 0x01, 0x00, 0x00, 0x00, 0xFF // 获取机芯的返回数据
//0xAA, 0x55, 0x55, 0xAA, 0x80, 0x02, 0x02, 0x01, 0x00, 0xFF // 关闭快门
//0xAA, 0x55, 0x55, 0xAA, 0x80, 0x02, 0x02, 0x02, 0x00, 0xFF // 打开快门
};
// 发送指令 将 vector 转换为 string,明确指定长度
// w.send(reinterpret_cast<const char*>(instruction_send.data())); // 原始写法
std::string data_str(reinterpret_cast<const char*>(instruction_send.data()), instruction_send.size());
w.send(data_str);
std::cout << "成功发送16进制指令" << std::endl;
}
else
{
cout << "串口打开失败" << endl;
}
// 无限循环接收数据
while (true)
{
//std::string msg = w.receive(); // 接收数据
//std::cout << "receive (" << msg.size() << " bytes): " << msg << std::endl;
延时1秒
//std::this_thread::sleep_for(std::chrono::seconds(1));
std::string msg = w.receive();
std::cout << "Received " << msg.size() << " bytes: ";
for (char c : msg) {
printf("%02X ", (unsigned char)c);
}
std::this_thread::sleep_for(std::chrono::seconds(1)); // 示延时1秒。如果需要更短的时间,可以使用std::chrono::milliseconds(100)表示延时100毫秒。
std::cout << std::endl;
}
return 0;
}
2.2 串口通信测试
2.2.1 设备准备
测试本教程代码发送指令是否成功,先准备两台电脑,一个串口模块和杜邦线,还有ATK XCOM通信软件,见下:
解释一下为什么要用两台电脑:同一台电脑上同一个端口,假设被ATK XCOM软件占用,运行代码时就找不到此端口了,串口冲突。
串口模型接线图见上面1.2小节。
2.2.2 运行代码收发指令
在第一台电脑上运行上面2.1.3中C++脚本,发送指令:aa 55 55 aa 80 02 02 01 00 ff,指令发送成功见下:
第二台电脑上接收到的指令见下:
从第二台电脑上发送直接到第一台电脑的终端接收,见下:
第一台电脑终端中接收到的指令见下:
通过上面测试方法,证明能准确发送十六进制指令,连接相机测试,通过此代码,可以准确控制相机快门开关。
三、总结
以上介绍了什么是串口通信,已经给出了详细代码和收发实例讲解C++实现串口通信,希望能帮到你!
学者在使用此代码时,要结合自己的指令需求调整修改代码,我在调试期间遇到了丢数据的问题,代码中已经解决了此问题。
感谢您阅读到最后!😊总结不易,多多支持呀🌹 点赞👍收藏⭐评论✍️,您的三连是我持续更新的动力💖
关注下面「视觉研坊」,获取干货教程、实战案例、技术解答、行业资讯!