Linux下C语言实现串口通讯例程

一、分析需求

        我们的目标,是在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

可以读取到我用串口屏发送的程序

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值