一、分析需求
我们的目标,是在Linux系统中实现一个C语言的串口通讯程序,能与插入的串口通讯设备进行通讯。所以代码层面的目标可以拆解为三个:
1.正确配置串口。
2.实现写入功能。
3.实现读取功能
在分化需求之后,我们来从代码层面逐个实现。
二、串口配置及初始化
“Linux中一切皆文件”想必大家耳熟能详。随着不断的学习和运用,对这句话的理解也逐渐加深。"Linux下一切皆文件" 是对Linux操作系统中一种重要设计理念的简化描述。这个理念强调了Linux系统中几乎所有对象和资源都可以被抽象为文件,并通过文件系统接口进行访问和操作。
在Linux中,硬件设备(如硬盘、串口、USB设备等)通常被表示为设备文件,位于/dev
目录下。例如,/dev/sda
可能代表第一个SCSI硬盘。通过读写这些设备文件,用户空间程序可以与硬件设备进行交互。而我们的串口USB在插入并经过Linux驱动识别后,会以/dev/ttyUSB*的格式出现:
root@dlpprinter:/# ls /dev/ttyUSB*
/dev/ttyUSB0
通过操作这个文件,我们可以对串口进行一系列配置,以下是一个串口初始化的函数:
int serial_init(const char *port, speed_t baudrate) {
int fd = open(port, O_RDWR | O_NOCTTY); // 打开串口设备文件
if (fd == -1) {
perror("open_port: Unable to open serial port"); // 输出错误信息
return -1;
}
struct termios tty;
memset(&tty, 0, sizeof(tty));
if (tcgetattr(fd, &tty) != 0) { // 获取串口属性
perror("tcgetattr");
close(fd);
return -1;
}
cfsetispeed(&tty, baudrate); // 设置输入波特率
cfsetospeed(&tty, baudrate); // 设置输出波特率
tty.c_cflag &= ~PARENB; // 禁用奇偶校验
tty.c_cflag &= ~CSTOPB; // 设置停止位为1
tty.c_cflag &= ~CSIZE; // 清除数据位设置
tty.c_cflag |= CS8; // 设置数据位为8位
tty.c_cflag &= ~CRTSCTS; // 禁用硬件流控
tty.c_cflag |= CREAD | CLOCAL; // 启用接收器,本地连接
tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // 设置输入模式
tty.c_iflag &= ~(IXON | IXOFF | IXANY); // 禁用软件流控
tty.c_oflag &= ~OPOST; // 设置输出模式
tty.c_cc[VMIN] = 1; // 读取一个字符
tty.c_cc[VTIME] = 0; // 无超时
if (tcsetattr(fd, TCSANOW, &tty) != 0) { // 设置串口属性
perror("tcsetattr");
close(fd);
return -1;
}
tcflush(fd, TCIFLUSH); // 刷新输入缓冲区
return fd;
}
这个函数通用性比较良好,传入的参数为串口设备文件名和波特率。经过以上操作,即可对串口进行初始化配置。
三、串口读写
“Linux下一切皆文件”,既然我们已经对串口设备文件进行了正确的初始化配置,接下里便可以直接用write()和read()这样的文件io函数进行读写,我们来看串口写入函数:
int port_write(int port_fd, const unsigned char* data, int length) {
int written = write(port_fd, data, length); // 写入数据到串口
if (written == -1) {
perror("write");
return -1;
}
return written;
}
其实只是将写入操作和报错简单的封装了一下,提高复用性和代码简洁性,读取函数同理:
int port_read(int port_fd, unsigned char* buffer, int length) {
int bytes_read = read(port_fd, buffer, length); // 从串口读取数据
if (bytes_read == -1) {
perror("read");
return -1;
}
return bytes_read;
}
四、程序设计
在实际运用中,我们的读写操作不可能只进行一次,写入操作可以单方面直接运行,但是读取操作需要考虑到缓冲区以及读取的实时性。比较简单的做法是用一个while循环去对设备文件进行无限循环读取。但是while循环运行到read函数会阻塞,直到串口有数据可读。我个人不喜欢这种写法,选择使用select函数来进行io复用,这样程序就可以非阻塞的进行其他操作,通过直接访问读取到的buffer即可直到读取到的数据。我们直接上完整程序:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <string.h>
#include <sys/select.h>
#include <errno.h>
#define SERIAL_PORT "/dev/serial-screen-device"
#define SERIAL_BAUDRATE B115200 // 串口波特率
#define READ_BUF_SIZE 256 // 读取缓冲区大小
int serial_init(const char *port, speed_t baudrate) {
int fd = open(port, O_RDWR | O_NOCTTY); // 打开串口设备文件
if (fd == -1) {
perror("open_port: Unable to open serial port"); // 输出错误信息
return -1;
}
struct termios tty;
memset(&tty, 0, sizeof(tty));
if (tcgetattr(fd, &tty) != 0) { // 获取串口属性
perror("tcgetattr");
close(fd);
return -1;
}
cfsetispeed(&tty, baudrate); // 设置输入波特率
cfsetospeed(&tty, baudrate); // 设置输出波特率
tty.c_cflag &= ~PARENB; // 禁用奇偶校验
tty.c_cflag &= ~CSTOPB; // 设置停止位为1
tty.c_cflag &= ~CSIZE; // 清除数据位设置
tty.c_cflag |= CS8; // 设置数据位为8位
tty.c_cflag &= ~CRTSCTS; // 禁用硬件流控
tty.c_cflag |= CREAD | CLOCAL; // 启用接收器,本地连接
tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // 设置输入模式
tty.c_iflag &= ~(IXON | IXOFF | IXANY); // 禁用软件流控
tty.c_oflag &= ~OPOST; // 设置输出模式
tty.c_cc[VMIN] = 1; // 读取一个字符
tty.c_cc[VTIME] = 0; // 无超时
if (tcsetattr(fd, TCSANOW, &tty) != 0) { // 设置串口属性
perror("tcsetattr");
close(fd);
return -1;
}
tcflush(fd, TCIFLUSH); // 刷新输入缓冲区
return fd;
}
int port_write(int port_fd, const unsigned char* data, int length) {
int written = write(port_fd, data, length); // 写入数据到串口
if (written == -1) {
perror("write");
return -1;
}
return written;
}
int port_read(int port_fd, unsigned char* buffer, int length) {
int bytes_read = read(port_fd, buffer, length); // 从串口读取数据
if (bytes_read == -1) {
perror("read");
return -1;
}
return bytes_read;
}
//后续主程序需改为select接收
int main() {
int fd = serial_init(SERIAL_PORT, SERIAL_BAUDRATE); // 初始化串口
if (fd == -1) {
fprintf(stderr, "Failed to initialize serial port.\n");
return 1;
}
const char* message = "Hello, Serial Port!";
int message_len = strlen(message);
if (port_write(fd, (const unsigned char*)message, message_len) == -1) { // 写入数据到串口
fprintf(stderr, "Failed to write to serial port.\n");
close(fd);
return 1;
}
fd_set readfds;
unsigned char read_buffer[256];
int ret;
int bytes_read;
while (1) {
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
ret = select(fd + 1, &readfds, NULL, NULL, NULL); // 使用select函数等待串口数据
if (ret == -1) {
perror("select");
break;
} else if (ret > 0) {
if (FD_ISSET(fd, &readfds)) {
memset(read_buffer, 0, sizeof(read_buffer));
bytes_read = port_read(fd, read_buffer, sizeof(read_buffer)); // 从串口读取数据
if (bytes_read == -1) {
fprintf(stderr, "Failed to read from serial port.\n");
break;
} else {
read_buffer[bytes_read] = '\0'; // 添加字符串结束符
printf("Received: %s\n", read_buffer); // 打印接收到的数据
}
}
}
}
close(fd); // 关闭串口
return 0;
}
以上代码只是展示了select的具体写法,并没有展现出io复用的效果,建议读者去详细了解一下io复用机制。我们来看一下程序的通讯效果
root@dlpprinter:/boa/cgi-bin# gcc -o ttl-screen ttl-screen.c
root@dlpprinter:/boa/cgi-bin# ttl-screen
Received: ><<>o
Received: ><<>o
Received: ><<>o
Received: ><<>o
Received: ><<>o
Received: hello
Received: hello
Received: hello
Received: hello
可以读取到我用串口屏发送的程序