一、概念
串口通信(Serial Communication)是指通过串行接口进行数据传输的方式。串行接口指的是数据一位一位地按顺序传输,而不是并行传输。常见的串行通信接口包括 RS-232、RS-485、USB 等。
在 C++ 编程中,串口通信通常用于计算机与外部设备(如传感器、微控制器、调制解调器等)之间的数据交换。
二、原理
串口通信基于 UART(Universal Asynchronous Receiver/Transmitter),UART 是一种硬件设备,它将并行数据转换为串行数据并进行传输,接收端的 UART 将串行数据还原为并行数据。串口通信的关键参数有:
- 波特率(Baud Rate):每秒传输的位数。例如,9600 波特率表示每秒传输 9600 位。
- 数据位(Data Bits):每个字符的位数,通常为 7 或 8 位。
- 停止位(Stop Bits):用于标识数据帧结束,通常为 1 位或 2 位。
- 奇偶校验位(Parity Bit):用于错误检测,可以是无校验(None)、奇校验(Odd)或偶校验(Even)。
数据传输分为异步传输和同步传输:
- 异步传输:每个数据帧独立传输,帧之间不需要同步时钟信号。波特率、数据位、停止位和校验位等参数需要在双方进行通信前设定一致。
- 同步传输:发送和接收设备使用共同的时钟信号。同步传输速度快,适合长距离传输,但设备间需要保持时钟同步。
三、应用领域
串口通信被广泛应用于各种领域,主要包括:
-
嵌入式系统:
- 与传感器、微控制器(如 Arduino、Raspberry Pi)等设备进行通信。
- 读取传感器数据,控制执行器(如电机、LED 等)。
-
工业自动化:
- 连接 PLC(可编程逻辑控制器)和 HMI(人机界面)。
- 控制工业设备和生产线,实现数据采集和远程监控。
-
通信设备:
- 与调制解调器、GPS 模块等设备进行数据交换。
- 实现远程数据传输和定位服务。
-
医疗设备:
- 监控患者生命体征数据,如心电图、血压等。
- 与其他医疗设备进行数据交换和控制。
-
消费电子:
- 家用电器、音频设备的控制和数据传输。
- 智能家居设备之间的通信和控制。
四、使用场景
-
数据采集与监控:
- 通过串口通信采集多个传感器的数据,并将数据传输到计算机或云端进行处理和监控。例如,环境监测系统通过串口与传感器连接,实时采集温度、湿度、空气质量等数据。
-
设备控制:
- 通过串口向外部设备发送指令,控制其行为。例如,通过串口通信控制工业机器人或自动化生产线的运作,提升生产效率和安全性。
-
调试和测试:
- 在嵌入式系统开发中,串口常用于输出调试信息或进行系统测试。开发者通过串口获取系统运行状态、调试信息和错误日志,有助于快速定位和解决问题。
-
远程通信:
- 使用串口通信模块(如无线调制解调器)实现远程数据传输和控制。例如,通过 GSM 模块实现远程监控和控制设备,适用于智能电网、远程抄表等应用场景。
五、实现方式
1. Linux 环境下的串口通信
在 Linux 系统中,可以使用 POSIX 标准 API 进行串口通信。以下是一个简单示例,展示如何打开串口、配置参数、读取和写入数据。
#include <iostream>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
// 配置串口参数
void configure_serial_port(int fd) {
struct termios tty;
// 获取当前串口设置
if (tcgetattr(fd, &tty) != 0) {
std::cerr << "Error from tcgetattr" << std::endl;
return;
}
// 设置输入输出波特率为9600
cfsetospeed(&tty, B9600);
cfsetispeed(&tty, B9600);
// 禁用校验位
tty.c_cflag &= ~PARENB;
// 设置一个停止位
tty.c_cflag &= ~CSTOPB;
// 设置每个字节8位
tty.c_cflag &= ~CSIZE;
tty.c_cflag |= CS8;
// 禁用硬件流控制
tty.c_cflag &= ~CRTSCTS;
// 开启接收使能和本地模式(忽略调制解调器)
tty.c_cflag |= CREAD | CLOCAL;
// 将终端设置为原始模式
cfmakeraw(&tty);
// 刷新输入缓冲区
tcflush(fd, TCIFLUSH);
// 设置新的串口参数
if (tcsetattr(fd, TCSANOW, &tty) != 0) {
std::cerr << "Error from tcsetattr" << std::endl;
}
}
int main() {
const char *portname = "/dev/ttyS1"; // 串口设备文件的名称
// 以读写、非阻塞和同步模式打开串口
int fd = open(portname, O_RDWR | O_NOCTTY | O_SYNC);
if (fd < 0) {
std::cerr << "Error opening " << portname << std::endl;
return 1;
}
configure_serial_port(fd); // 配置串口参数
const char *msg = "Hello, Serial Port!"; // 要发送的消息
// 向串口写入数据
int n_written = write(fd, msg, strlen(msg));
if (n_written < 0) {
std::cerr << "Error writing to " << portname << std::endl;
}
char buf[100]; // 缓冲区用于读取数据
// 从串口读取数据
int n_read = read(fd, buf, sizeof(buf));
if (n_read < 0) {
std::cerr << "Error reading from " << portname << std::endl;
} else {
std::cout << "Read: " << std::string(buf, n_read) << std::endl; // 打印读取到的消息
}
close(fd); // 关闭串口
return 0;
}
关键注释解释
configure_serial_port(int fd)
:
- 函数用于配置串口的参数,包括波特率、数据位、停止位、校验位和流控制等。
tcgetattr(fd, &tty)
:
- 获取当前串口设置,填充到
termios
结构体中。
cfsetospeed(&tty, B9600)
和cfsetispeed(&tty, B9600)
:
- 设置串口的输入和输出波特率为 9600。
配置
tty.c_cflag
:
tty.c_cflag
用于控制串口的各种选项:
~PARENB
:禁用校验位。~CSTOPB
:设置一个停止位。~CSIZE
和CS8
:设置每个字节 8 位。~CRTSCTS
:禁用硬件流控制。CREAD | CLOCAL
:启用接收使能和本地模式(忽略调制解调器)。
cfmakeraw(&tty)
:
- 将终端设置为原始模式。
tcflush(fd, TCIFLUSH)
:
- 刷新输入缓冲区,丢弃所有的数据。
tcsetattr(fd, TCSANOW, &tty)
:
- 设置新的串口参数,立即生效。
main()
函数:
open()
:以读写、非阻塞和同步模式打开串口。configure_serial_port(fd)
:配置串口参数。write(fd, msg, strlen(msg))
:向串口写入数据。read(fd, buf, sizeof(buf))
:从串口读取数据。close(fd)
:关闭串口。
2. Windows 环境下的串口通信
在 Windows 系统中,可以使用 Win32 API 进行串口通信。以下是简单示例。
方法一:使用宽字符字符串
#include <iostream>
#include <windows.h>
// 配置串口参数
void configure_serial_port(HANDLE hSerial) {
DCB dcbSerialParams = {0}; // 初始化 DCB 结构
dcbSerialParams.DCBlength = sizeof(dcbSerialParams);
// 获取当前串口状态
if (!GetCommState(hSerial, &dcbSerialParams)) {
std::wcerr << L"Error getting state: " << GetLastError() << std::endl;
return;
}
// 配置波特率、数据位、停止位和校验位
dcbSerialParams.BaudRate = CBR_9600; // 设置波特率为 9600
dcbSerialParams.ByteSize = 8; // 设置数据位为 8
dcbSerialParams.StopBits = ONESTOPBIT; // 设置一个停止位
dcbSerialParams.Parity = NOPARITY; // 设置无校验位
// 设置串口状态
if (!SetCommState(hSerial, &dcbSerialParams)) {
std::wcerr << L"Error setting state: " << GetLastError() << std::endl;
return;
}
// 设置超时参数
COMMTIMEOUTS timeouts = {0};
timeouts.ReadIntervalTimeout = 50; // 读取间隔超时
timeouts.ReadTotalTimeoutConstant = 50; // 读取总超时常量
timeouts.ReadTotalTimeoutMultiplier = 10; // 读取总超时乘数
timeouts.WriteTotalTimeoutConstant = 50; // 写入总超时常量
timeouts.WriteTotalTimeoutMultiplier = 10; // 写入总超时乘数
// 设置串口超时
if (!SetCommTimeouts(hSerial, &timeouts)) {
std::wcerr << L"Error setting timeouts: " << GetLastError() << std::endl;
return;
}
// 清理串口缓冲区
if (!PurgeComm(hSerial, PURGE_RXCLEAR | PURGE_TXCLEAR)) {
std::wcerr << L"Error purging comm: " << GetLastError() << std::endl;
return;
}
}
int main() {
const wchar_t *portname = L"COM1"; // 使用宽字符字符串表示串口名称
// 打开串口
HANDLE hSerial = CreateFileW(portname, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
// 检查串口是否成功打开
if (hSerial == INVALID_HANDLE_VALUE) {
std::wcerr << L"Error opening port: " << GetLastError() << std::endl;
return 1;
}
configure_serial_port(hSerial); // 配置串口参数
const char *msg = "Hello, Serial Port!"; // 要发送的消息
DWORD bytes_written;
// 写入数据到串口
if (!WriteFile(hSerial, msg, strlen(msg), &bytes_written, NULL)) {
std::wcerr << L"Error writing to port: " << GetLastError() << std::endl;
CloseHandle(hSerial); // 关闭串口
return 1;
}
char buf[100] = {0}; // 缓冲区用于读取数据
DWORD bytes_read;
// 从串口读取数据
if (!ReadFile(hSerial, buf, sizeof(buf), &bytes_read, NULL)) {
std::wcerr << L"Error reading from port: " << GetLastError() << std::endl;
} else {
std::wcout << L"Read: " << std::string(buf, bytes_read).c_str() << std::endl; // 打印读取到的消息
}
CloseHandle(hSerial); // 关闭串口
return 0;
}
关键注释解释
configure_serial_port(HANDLE hSerial)
:
- 函数用于配置串口的参数,包括波特率、数据位、停止位、校验位和超时设置等。
DCB dcbSerialParams = {0};
:
- 初始化一个
DCB
结构,用于存储串口配置参数。
GetCommState(hSerial, &dcbSerialParams)
:
- 获取当前串口的状态,并存储到
dcbSerialParams
结构中。设置串口参数:
- 波特率:
dcbSerialParams.BaudRate = CBR_9600;
- 数据位:
dcbSerialParams.ByteSize = 8;
- 停止位:
dcbSerialParams.StopBits = ONESTOPBIT;
- 校验位:
dcbSerialParams.Parity = NOPARITY;
SetCommState(hSerial, &dcbSerialParams)
:
- 设置新的串口状态(参数)。
设置超时参数:
- 读取间隔超时:
timeouts.ReadIntervalTimeout = 50;
- 读取总超时常量:
timeouts.ReadTotalTimeoutConstant = 50;
- 读取总超时乘数:
timeouts.ReadTotalTimeoutMultiplier = 10;
- 写入总超时常量:
timeouts.WriteTotalTimeoutConstant = 50;
- 写入总超时乘数:
timeouts.WriteTotalTimeoutMultiplier = 10;
SetCommTimeouts(hSerial, &timeouts)
:
- 设置串口的超时参数。
PurgeComm(hSerial, PURGE_RXCLEAR | PURGE_TXCLEAR)
:
- 清理串口缓冲区。
主函数
main
:
CreateFileW()
:以读写模式打开串口。configure_serial_port(hSerial)
:配置串口参数。WriteFile()
:将消息写入串口。ReadFile()
:从串口读取数据。CloseHandle(hSerial)
:关闭串口。
方法二:使用 ANSI 版本的 Windows API 函数
#include <iostream>
#include <windows.h>
// 配置串口参数
void configure_serial_port(HANDLE hSerial) {
DCB dcbSerialParams = {0}; // 初始化 DCB 结构
dcbSerialParams.DCBlength = sizeof(dcbSerialParams);
// 获取当前串口状态
if (!GetCommState(hSerial, &dcbSerialParams)) {
std::cerr << "Error getting state: " << GetLastError() << std::endl;
return;
}
// 配置波特率、数据位、停止位和校验位
dcbSerialParams.BaudRate = CBR_9600; // 设置波特率为 9600
dcbSerialParams.ByteSize = 8; // 设置数据位为 8
dcbSerialParams.StopBits = ONESTOPBIT; // 设置一个停止位
dcbSerialParams.Parity = NOPARITY; // 设置无校验位
// 设置串口状态
if (!SetCommState(hSerial, &dcbSerialParams)) {
std::cerr << "Error setting state: " << GetLastError() << std::endl;
return;
}
// 设置超时参数
COMMTIMEOUTS timeouts = {0};
timeouts.ReadIntervalTimeout = 50; // 读取间隔超时
timeouts.ReadTotalTimeoutConstant = 50; // 读取总超时常量
timeouts.ReadTotalTimeoutMultiplier = 10; // 读取总超时乘数
timeouts.WriteTotalTimeoutConstant = 50; // 写入总超时常量
timeouts.WriteTotalTimeoutMultiplier = 10; // 写入总超时乘数
// 设置串口超时
if (!SetCommTimeouts(hSerial, &timeouts)) {
std::cerr << "Error setting timeouts: " << GetLastError() << std::endl;
return;
}
// 清理串口缓冲区
if (!PurgeComm(hSerial, PURGE_RXCLEAR | PURGE_TXCLEAR)) {
std::cerr << "Error purging comm: " << GetLastError() << std::endl;
return;
}
}
int main() {
const char *portname = "COM1"; // 使用 ANSI 字符串表示串口名称
// 打开串口
HANDLE hSerial = CreateFileA(portname, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
// 检查串口是否成功打开
if (hSerial == INVALID_HANDLE_VALUE) {
std::cerr << "Error opening port: " << GetLastError() << std::endl;
return 1;
}
configure_serial_port(hSerial); // 配置串口参数
const char *msg = "Hello, Serial Port!"; // 要发送的消息
DWORD bytes_written;
// 写入数据到串口
if (!WriteFile(hSerial, msg, strlen(msg), &bytes_written, NULL)) {
std::cerr << "Error writing to port: " << GetLastError() << std::endl;
CloseHandle(hSerial); // 关闭串口
return 1;
}
char buf[100] = {0}; // 缓冲区用于读取数据
DWORD bytes_read;
// 从串口读取数据
if (!ReadFile(hSerial, buf, sizeof(buf), &bytes_read, NULL)) {
std::cerr << "Error reading from port: " << GetLastError() << std::endl;
} else {
std::cout << "Read: " << std::string(buf, bytes_read) << std::endl; // 打印读取到的消息
}
CloseHandle(hSerial); // 关闭串口
return 0;
}
关键注释解释
configure_serial_port(HANDLE hSerial)
:
- 函数用于配置串口的参数,包括波特率、数据位、停止位、校验位和超时设置等。
DCB dcbSerialParams = {0};
:
- 初始化一个
DCB
结构,用于存储串口配置参数。
GetCommState(hSerial, &dcbSerialParams)
:
- 获取当前串口的状态,并存储到
dcbSerialParams
结构中。设置串口参数:
- 波特率:
dcbSerialParams.BaudRate = CBR_9600;
- 数据位:
dcbSerialParams.ByteSize = 8;
- 停止位:
dcbSerialParams.StopBits = ONESTOPBIT;
- 校验位:
dcbSerialParams.Parity = NOPARITY;
SetCommState(hSerial, &dcbSerialParams)
:
- 设置新的串口状态(参数)。
设置超时参数:
- 读取间隔超时:
timeouts.ReadIntervalTimeout = 50;
- 读取总超时常量:
timeouts.ReadTotalTimeoutConstant = 50;
- 读取总超时乘数:
timeouts.ReadTotalTimeoutMultiplier = 10;
- 写入总超时常量:
timeouts.WriteTotalTimeoutConstant = 50;
- 写入总超时乘数:
timeouts.WriteTotalTimeoutMultiplier = 10;
SetCommTimeouts(hSerial, &timeouts)
:
- 设置串口的超时参数。
PurgeComm(hSerial, PURGE_RXCLEAR | PURGE_TXCLEAR)
:
- 清理串口缓冲区。
主函数
main
:
CreateFileA()
:以读写模式打开串口。configure_serial_port(hSerial)
:配置串口参数。WriteFile()
:将消息写入串口。ReadFile()
:从串口读取数据。CloseHandle(hSerial)
:关闭串口。
3. 使用 Boost.Asio 进行跨平台串口通信
Boost.Asio 是一个跨平台的库,提供了用于网络和低级 I/O 编程的广泛功能,包括串口通信。以下是一个使用 Boost.Asio 进行串口通信的示例。
#include <iostream>
#include <boost/asio.hpp>
int main() {
boost::asio::io_service io; // 创建 io_service 对象,用于管理 I/O 操作
boost::asio::serial_port serial(io, "/dev/ttyS1"); // 创建 serial_port 对象并打开串口
// 配置串口参数
serial.set_option(boost::asio::serial_port_base::baud_rate(9600)); // 设置波特率为 9600
serial.set_option(boost::asio::serial_port_base::character_size(8)); // 设置数据位为 8
serial.set_option(boost::asio::serial_port_base::parity(boost::asio::serial_port_base::parity::none)); // 设置无校验位
serial.set_option(boost::asio::serial_port_base::stop_bits(boost::asio::serial_port_base::stop_bits::one)); // 设置一个停止位
serial.set_option(boost::asio::serial_port_base::flow_control(boost::asio::serial_port_base::flow_control::none)); // 设置无流控制
const std::string msg = "Hello, Serial Port!"; // 要发送的消息
boost::asio::write(serial, boost::asio::buffer(msg)); // 将消息写入串口
char buf[100]; // 缓冲区用于读取数据
boost::asio::read(serial, boost::asio::buffer(buf, msg.size())); // 从串口读取数据到缓冲区
std::cout << "Read: " << std::string(buf, msg.size()) << std::endl; // 打印读取到的消息
return 0;
}
关键注释解释
boost::asio::io_service io
:
- 创建一个
io_service
对象,它是 Boost.Asio 中 I/O 操作的核心对象,负责管理所有异步操作。
boost::asio::serial_port serial(io, "/dev/ttyS1")
:
- 创建一个
serial_port
对象并打开指定的串口。- 在 Linux 系统中,串口设备通常表示为
/dev/ttyS1
(或其他名称),在 Windows 中,你可以使用"COM1"
等名称。设置串口参数:
- 波特率:
serial.set_option(boost::asio::serial_port_base::baud_rate(9600));
- 设置串口通信的波特率为 9600。
- 数据位:
serial.set_option(boost::asio::serial_port_base::character_size(8));
- 设置数据位为 8。
- 校验位:
serial.set_option(boost::asio::serial_port_base::parity(boost::asio::serial_port_base::parity::none));
- 设置无校验位。
- 停止位:
serial.set_option(boost::asio::serial_port_base::stop_bits(boost::asio::serial_port_base::stop_bits::one));
- 设置一个停止位。
- 流控制:
serial.set_option(boost::asio::serial_port_base::flow_control(boost::asio::serial_port_base::flow_control::none));
- 设置无流控制。
发送数据:
const std::string msg = "Hello, Serial Port!";
- 要发送的消息。
boost::asio::write(serial, boost::asio::buffer(msg));
- 使用
boost::asio::write
函数将消息写入串口。读取数据:
char buf[100];
- 定义一个缓冲区用于读取数据。
boost::asio::read(serial, boost::asio::buffer(buf, msg.size()));
- 使用
boost::asio::read
函数从串口读取数据到缓冲区。std::cout << "Read: " << std::string(buf, msg.size()) << std::endl;
- 打印读取到的数据。
六、小结
C++ 串口通信涉及了解串口的基本概念和原理,掌握如何在不同操作系统环境下进行串口操作。无论是使用原生 API 还是第三方库,串口通信在嵌入式系统、工业自动化、通信设备等领域都有广泛的应用。通过学习和掌握这些技术,开发者可以实现设备之间的高效数据传输和控制。