本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:在Linux系统中,串口通信是嵌入式和物联网开发的基础。本项目基于C语言实现串口的打开、配置与数据读写操作,并通过队列机制提升数据处理效率。项目还集成WebSocket协议,实现将串口接收的数据实时上传至服务器,具备完整的错误处理与多线程并发机制。适用于物联网设备通信、嵌入式系统数据上传等实际应用场景。
LINUX c serial port_serialport

1. Linux串口通信基础

Linux系统中,串口通信是设备间数据交互的重要方式,广泛应用于工业控制、物联网、嵌入式系统等领域。串口通信通过异步方式传输数据,具备结构简单、成本低廉、传输距离远等优点。其基本通信要素包括起始位、数据位、校验位和停止位,构成了串行数据帧的完整格式。

常见的串口标准如 RS-232 RS-485 在电气特性和通信距离上有显著区别:RS-232适用于短距离点对点通信,而RS-485支持多点通信和更长的传输距离,适合工业现场环境。理解这些标准及其适用场景是进行串口编程与系统集成的基础。

2. C语言串口设备文件操作(open/read/write)

在Linux系统中,串口通信的本质是通过文件操作接口实现的。由于Linux将所有设备都抽象为文件,因此对串口的操作本质上就是对设备文件的读写。本章将从C语言的角度深入探讨如何使用标准的文件操作函数(如 open read write )来控制串口设备,并结合实际代码示例展示串口通信的底层实现机制。

2.1 Linux文件操作基础

Linux系统中的文件操作是所有设备通信的基础,串口设备也不例外。理解文件描述符、 open 系统调用以及设备文件路径是掌握串口通信的第一步。

2.1.1 文件描述符与open系统调用

在Linux中,文件操作是通过文件描述符(File Descriptor)来实现的。文件描述符是一个非负整数,用于标识打开的文件或设备。通过 open() 系统调用可以获取一个文件描述符。

#include <fcntl.h>
#include <unistd.h>

int fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_NDELAY);

代码解释:

  • "/dev/ttyS0" :这是串口设备文件的路径,表示第一个串口。
  • O_RDWR :以可读可写方式打开设备。
  • O_NOCTTY :防止设备成为控制终端。
  • O_NDELAY :非阻塞方式打开设备,避免程序在设备不可用时卡死。

逐行分析:

  1. 引入 fcntl.h 是为了使用 open 函数。
  2. 调用 open() ,传入设备路径和标志位。
  3. 返回值 fd 即为文件描述符,后续操作都基于此值。

2.1.2 串口设备文件的路径与权限配置

Linux系统中常见的串口设备文件路径如下:

设备类型 路径名 描述
RS-232 /dev/ttyS0 第一个串口
USB转串口 /dev/ttyUSB0 第一个USB转串口设备
Bluetooth串口 /dev/rfcomm0 蓝牙串口设备

权限配置说明:

默认情况下,普通用户没有权限访问串口设备。需使用以下命令修改权限:

sudo chmod 666 /dev/ttyS0

或添加用户到 dialout 组:

sudo usermod -a -G dialout $USER

2.1.3 常见错误码与处理方式

使用 open() 打开串口时可能遇到以下常见错误码:

错误码 含义 处理方式
EACCES 权限不足 修改设备权限或添加用户组
ENOENT 设备文件不存在 检查设备路径或驱动是否加载
EBUSY 设备被其他程序占用 结束占用进程或更换设备路径
EIO I/O错误 检查硬件连接或更换设备

错误处理示例:

#include <errno.h>
#include <stdio.h>

if (fd == -1) {
    perror("Failed to open serial port");
    switch(errno) {
        case EACCES:
            printf("Error: Permission denied\n");
            break;
        case ENOENT:
            printf("Error: Device file not found\n");
            break;
        default:
            printf("Error code: %d\n", errno);
    }
    return -1;
}

2.2 串口设备的打开与关闭操作

正确地打开和关闭串口设备是确保通信稳定的基础。本节将详细介绍 open() close() 的使用方法及其注意事项。

2.2.1 open函数的参数设置(O_RDWR、O_NOCTTY、O_NDELAY)

open() 的参数决定了串口的访问模式和行为:

标志位 说明
O_RDONLY 只读方式打开
O_WRONLY 只写方式打开
O_RDWR 读写方式打开
O_NOCTTY 不将设备设置为控制终端
O_NDELAY 非阻塞模式,立即返回
O_SYNC 同步写入,确保数据写入磁盘

示例代码:

int fd = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY | O_NDELAY);

逻辑说明:

  • O_RDWR :确保可以同时进行读写。
  • O_NOCTTY :避免串口被误设为控制终端,防止程序被中断信号干扰。
  • O_NDELAY :非阻塞打开,避免在设备未连接时卡住。

2.2.2 串口设备的独占访问与阻塞控制

Linux允许串口设备被多个进程访问,但通常建议以独占方式使用串口以避免冲突。可以通过检查设备是否已被打开,或使用锁文件机制实现。

使用 fcntl 实现文件锁示例:

#include <fcntl.h>

struct flock lock;
lock.l_type = F_WRLCK;  // 写锁
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;

if (fcntl(fd, F_SETLK, &lock) == -1) {
    perror("Failed to lock device");
    close(fd);
    return -1;
}

2.2.3 close函数的正确使用与资源释放

每次打开设备后,都必须使用 close() 关闭文件描述符,释放系统资源。

#include <unistd.h>

close(fd);

注意事项:

  • 未关闭文件描述符会导致资源泄漏。
  • 多线程环境下需确保在所有线程退出后再调用 close()

2.3 数据的读取与写入操作

串口通信的核心是数据的读写。Linux中使用 read() write() 函数完成串口的数据传输。

2.3.1 read与write函数的使用方式

#include <unistd.h>

char buffer[256];
int n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0) {
    buffer[n] = '\0';  // 添加字符串结束符
    printf("Received: %s\n", buffer);
}

const char *msg = "Hello Serial\n";
write(fd, msg, strlen(msg));

逻辑说明:

  • read() 从串口读取最多255字节数据。
  • write() 发送字符串到串口。
  • n 表示实际读取的字节数。

2.3.2 阻塞与非阻塞模式下的读写行为差异

模式 read()行为 write()行为
阻塞模式 无数据时等待 数据未完全写入时等待
非阻塞模式 无数据时立即返回0 数据未完全写入时返回已写入字节数

切换非阻塞模式:

fcntl(fd, F_SETFL, O_NONBLOCK);

2.3.3 实际读写数据长度的判断与处理

串口通信中,一次 read() write() 操作不一定能完成全部数据的传输。因此需要进行循环处理:

int total_read = 0;
int bytes_to_read = 100;
char read_buf[100];

while (total_read < bytes_to_read) {
    int n = read(fd, read_buf + total_read, bytes_to_read - total_read);
    if (n <= 0) {
        // 错误或超时处理
        break;
    }
    total_read += n;
}

流程图:

graph TD
    A[开始读取] --> B{是否读取完整?}
    B -- 是 --> C[完成]
    B -- 否 --> D[继续读取]
    D --> B

2.4 串口通信的同步与异步处理

在实际应用中,串口通信可能需要处理并发或异步事件。Linux提供了 select poll 机制来实现异步通信。

2.4.1 同步通信的实现方式

同步通信是指主线程等待串口数据到达后继续执行。适用于简单通信任务。

char buffer[256];
int n = read(fd, buffer, sizeof(buffer));
if (n > 0) {
    // 处理数据
}

优点: 简单直观
缺点: 容易造成阻塞,影响程序响应

2.4.2 异步通信与select/poll机制简介

使用 select() 可以监听多个文件描述符的状态变化,适用于多路复用场景。

#include <sys/select.h>

fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fd, &readfds);

struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;

int ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
if (ret > 0 && FD_ISSET(fd, &readfds)) {
    // 有数据可读
    read(fd, buffer, sizeof(buffer));
}

表格:select 与 poll 对比

特性 select poll
最大文件描述符数 1024 无限制
性能 随文件描述符数增加而下降 性能更优
使用方式 每次调用需重新设置集合 可复用pollfd结构

2.4.3 串口数据读写的超时控制

在实际通信中,常常需要设置读写超时机制以避免无限等待。

struct timeval timeout;
timeout.tv_sec = 3;
timeout.tv_usec = 0;

setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));

注意事项:

  • setsockopt() 通常用于网络套接字,对于串口需使用 termios 设置超时时间。
  • 更推荐使用 select() poll() 实现超时控制。

下一章预告: 在掌握了串口的基本文件操作后,我们将深入探讨如何使用 termios 接口对串口通信的参数进行精确配置,包括波特率、数据位、校验位等关键参数。

3. termios串口参数配置(波特率、校验位等)

在Linux系统中,串口通信的配置不仅限于打开和读写设备文件,更重要的是对串口的通信参数进行精细控制。这些参数包括波特率、数据位、停止位、校验方式等,直接影响数据传输的稳定性和效率。 termios 结构体是Linux中用于配置终端(包括串口)行为的核心数据结构,它允许开发者对输入输出模式、控制选项、本地行为等进行灵活设置。

本章将深入解析 termios 结构体的组成与使用方式,详细讲解波特率、数据格式、通信模式等关键配置项的设置方法,并通过代码示例说明如何在C语言中进行串口参数配置,以满足不同应用场景的需求。

3.1 termios结构体详解

在Linux系统中, termios 结构体用于控制终端设备的输入输出行为。该结构体定义在头文件 <termios.h> 中,是串口编程中最核心的数据结构之一。

3.1.1 termios结构体的四个主要成员(c_iflag、c_oflag、c_cflag、c_lflag)

termios 结构体由四个主要标志字段组成:

成员字段 作用
c_iflag 输入模式标志,控制输入数据的处理方式
c_oflag 输出模式标志,控制输出数据的处理方式
c_cflag 控制模式标志,设置波特率、字符大小、停止位、校验等
c_lflag 本地模式标志,控制终端的本地行为(如回显、规范模式等)

此外,还有一个用于控制特殊字符的数组 c_cc ,用于定义如 EOF、INTR、QUIT 等特殊字符的映射。

示例代码:初始化termios结构体
#include <termios.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    int fd = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY | O_NDELAY);
    if (fd == -1) {
        perror("open");
        return -1;
    }

    struct termios options;
    tcgetattr(fd, &options);  // 获取当前终端属性

    // 输出原始结构体中的标志值
    printf("c_iflag: %x\n", options.c_iflag);
    printf("c_oflag: %x\n", options.c_oflag);
    printf("c_cflag: %x\n", options.c_cflag);
    printf("c_lflag: %x\n", options.c_lflag);

    close(fd);
    return 0;
}

代码逻辑分析:

  • open() 打开串口设备文件。
  • tcgetattr() 获取当前串口的属性配置。
  • 输出 termios 结构体中的各字段值,用于查看当前配置状态。

3.1.2 输入、输出、控制、本地模式标志的含义与设置方法

输入模式标志(c_iflag)

用于控制输入数据的处理方式,常见的标志包括:

标志名 含义
IGNBRK 忽略BREAK条件
BRKINT BREAK条件产生SIGINT信号
IGNPAR 忽略帧错误和奇偶校验错误
PARMRK 标记奇偶错误
INPCK 启用奇偶校验
ISTRIP 剥离第8位
IXON 启用输出流控制(XON/XOFF)
IXOFF 启用输入流控制
输出模式标志(c_oflag)

控制输出数据的处理方式,常见标志:

标志名 含义
OPOST 启用后处理
ONLCR 将换行符转换为CR+LF
OCRNL 将CR转换为NL
ONOCR 不在列0输出CR
ONLRET 不输出CR
控制模式标志(c_cflag)

控制波特率、字符大小、停止位、校验等:

标志名 含义
CBAUD 波特率掩码
CSIZE 字符大小掩码(CS5~CS8)
CSTOPB 使用两个停止位
PARENB 启用奇偶校验
PARODD 奇校验(否则为偶校验)
HUPCL 关闭时挂断
CLOCAL 忽略调制解调器状态线
CREAD 启用接收器
本地模式标志(c_lflag)

控制终端的本地行为:

标志名 含义
ISIG 启用信号处理(如INTR、QUIT)
ICANON 启用规范模式(行缓冲)
ECHO 启用输入字符回显
ECHOE 回显擦除字符
ECHOK 回显KILL字符
NOFLSH 禁用刷新输入/输出队列

3.1.3 特殊字符控制(如EOF、INTR等)

c_cc 数组用于定义特殊字符,如:

字符名 说明
VINTR 中断字符(默认Ctrl+C)
VQUIT 退出字符(默认Ctrl+\)
VERASE 删除字符(默认Backspace)
VKILL 删除行(默认Ctrl+U)
VEOF 文件结束符(默认Ctrl+D)
VTIME 非规范模式下等待时间(单位:0.1秒)
VMIN 非规范模式下最小读取字符数

例如,设置 VEOF 为Ctrl+Z:

options.c_cc[VEOF] = 26;  // ASCII码26对应Ctrl+Z

3.2 波特率的设置与串口速率匹配

波特率是串口通信中最重要的参数之一,表示每秒传输的比特数。Linux中通过 termios 结构体的 c_cflag 字段设置波特率。

3.2.1 波特率常量定义(B9600、B115200等)

Linux预定义了一系列波特率常量,例如:

常量 波特率
B300 300 bps
B1200 1200 bps
B9600 9600 bps
B19200 19200 bps
B38400 38400 bps
B57600 57600 bps
B115200 115200 bps

设置波特率的示例代码:

cfsetispeed(&options, B115200);  // 设置输入波特率
cfsetospeed(&options, B115200);  // 设置输出波特率

3.2.2 自定义波特率的实现方式

在某些特殊设备中,可能需要设置非标准波特率。可以通过 cfsetspeed() 函数或直接修改 c_cflag 字段实现。

示例:设置自定义波特率
options.c_cflag &= ~CBAUD;          // 清除波特率掩码
options.c_cflag |= B38400 | B57600; // 设置自定义波特率组合(部分平台支持)

⚠️ 注意:并非所有系统和硬件都支持自定义波特率,需查阅具体设备手册或驱动支持情况。

3.2.3 波特率设置的注意事项与常见错误

  • 波特率必须一致 :发送端与接收端的波特率必须完全一致,否则会导致数据乱码。
  • 波特率过高 :可能超出设备支持范围,导致通信失败。
  • 波特率设置失败 :检查是否正确调用 cfsetispeed() cfsetospeed() ,并使用 tcsetattr() 更新配置。

3.3 数据格式的配置(数据位、停止位、校验位)

串口通信的数据格式由数据位、停止位和校验位组成,决定了数据帧的结构。

3.3.1 CSIZE位宽设置(CS5~CS8)

使用 CSIZE 宏配合 CS5 ~ CS8 设置数据位长度。

示例代码:
options.c_cflag &= ~CSIZE;  // 清除数据位掩码
options.c_cflag |= CS8;     // 设置8位数据位

3.3.2 停止位设置(CSTOPB)

CSTOPB 标志用于设置停止位:

  • 未设置:1个停止位
  • 设置:2个停止位
options.c_cflag &= ~CSTOPB;  // 1个停止位
// options.c_cflag |= CSTOPB; // 2个停止位

3.3.3 校验位的启用与类型选择(PARODD、PARENB)

启用校验位需设置 PARENB ,奇校验则加 PARODD

示例代码:
options.c_cflag |= PARENB;   // 启用校验位
options.c_cflag |= PARODD;   // 奇校验(偶校验则不加)

⚠️ 注意:启用校验位时,数据位应为7位(CS7),否则可能导致通信失败。

3.4 串口通信模式的切换(原始模式与规范模式)

Linux串口通信支持两种主要模式:规范模式(Canonical Mode)和原始模式(Raw Mode)。

3.4.1 规范模式下的行缓冲机制

规范模式下,输入数据以行为单位进行处理,直到遇到换行符( \n )或EOF(Ctrl+D)才会返回数据。适合交互式终端操作。

设置规范模式:
options.c_lflag |= ICANON;   // 启用规范模式

3.4.2 原始模式的配置方法与数据处理特点

原始模式下,输入数据立即返回,不进行任何处理,适用于串口通信等实时数据传输。

设置原始模式:
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);  // 禁用规范模式、回显、信号处理
options.c_oflag &= ~OPOST;                            // 禁用输出处理

3.4.3 屏蔽回显与控制字符处理

在串口通信中,通常不希望看到回显和控制字符干扰数据流。可通过以下方式禁用:

options.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL);  // 禁用回显
options.c_lflag &= ~ICANON;                            // 禁用规范模式
options.c_lflag &= ~ISIG;                              // 禁用信号字符(如Ctrl+C)

总结与流程图

下面通过Mermaid流程图总结串口参数配置的基本流程:

graph TD
    A[打开串口设备] --> B[获取termios结构]
    B --> C[设置波特率]
    C --> D[设置数据格式]
    D --> E[设置通信模式]
    E --> F[更新串口属性]
    F --> G[进行数据读写]

通过本章的学习,读者应掌握如何使用 termios 结构体配置串口的关键参数,并能够在实际项目中灵活应用,为后续章节中的队列管理和多线程通信打下坚实基础。

4. 串口数据队列管理(queue.h实现FIFO)

4.1 队列管理在串口通信中的作用

4.1.1 数据缓冲的必要性与队列结构设计

在串口通信中,数据的发送和接收通常是以异步方式进行的。由于串口通信速率较低、数据包到达时间不规律,若不进行缓冲处理,很容易造成数据丢失或覆盖。此时,引入队列结构作为中间缓冲机制,能够有效解决这一问题。

一个典型的队列设计包括入队(enqueue)和出队(dequeue)操作。队列可以是固定大小的数组结构,也可以是链表结构,根据应用场景选择不同的实现方式。

在Linux环境下,队列的实现可以借助于 queue.h 头文件提供的宏定义,例如 TAILQ (Tail Queue)结构,它支持高效的链表操作,并且易于在多线程环境中使用。

4.1.2 队列在多线程通信中的协调作用

当串口通信被封装在独立的线程中时,主程序或其它线程可能需要访问这些数据。使用队列作为线程间通信的桥梁,可以避免直接操作共享内存带来的同步问题。

例如,在一个典型的串口数据采集系统中,一个线程负责从串口读取数据并放入队列,另一个线程则从队列中取出数据进行处理或转发。通过队列的中间缓冲,两个线程无需频繁地等待对方,从而提高系统吞吐量和响应速度。

4.1.3 FIFO与LIFO结构的适用场景对比

FIFO(First In First Out)与LIFO(Last In First Out)是两种常见的数据结构。在串口通信中,数据是按照到达顺序处理的,因此FIFO结构更为合适。

结构类型 特点 适用场景
FIFO 先入先出,顺序处理 串口数据缓存、消息队列
LIFO 后入先出,栈式处理 函数调用栈、命令回退机制

在串口应用中,如果采用LIFO结构,可能导致后到达的数据被优先处理,破坏数据的时序性。因此,FIFO结构是更合理的选择。

4.2 使用queue.h头文件实现链式队列

4.2.1 queue.h中TAILQ宏的使用方法

Linux内核提供的 queue.h 头文件定义了多种链表结构宏,其中 TAILQ 是最常用于队列管理的宏集合。它提供了队列的初始化、插入、删除等基本操作。

主要宏定义如下:

  • TAILQ_HEAD(name, type) :定义队列头部结构
  • TAILQ_ENTRY(type) :在结构体中添加队列节点链接字段
  • TAILQ_INIT(head) :初始化队列
  • TAILQ_INSERT_TAIL(head, elm, field) :尾部插入元素
  • TAILQ_REMOVE(head, elm, field) :删除指定元素
  • TAILQ_FOREACH(var, head, field) :遍历队列

4.2.2 队列节点结构体的定义与初始化

为了使用 TAILQ 宏,首先需要定义一个节点结构体,并在其中包含 TAILQ_ENTRY 字段:

#include <sys/queue.h>

typedef struct data_node {
    char data[256];  // 假设每个节点存储最多256字节数据
    TAILQ_ENTRY(data_node) entries;  // 队列链接字段
} DataNode;

接下来定义队列头部结构:

TAILQ_HEAD(data_queue_head, data_node);

初始化队列:

struct data_queue_head head;
TAILQ_INIT(&head);

4.2.3 入队与出队操作的实现与线程安全性

入队操作示例:

DataNode *node = (DataNode *)malloc(sizeof(DataNode));
strcpy(node->data, "Hello Serial");

TAILQ_INSERT_TAIL(&head, node, entries);  // 插入到队列尾部

出队操作示例:

DataNode *first = TAILQ_FIRST(&head);
if (first) {
    printf("Dequeued data: %s\n", first->data);
    TAILQ_REMOVE(&head, first, entries);  // 从队列中移除
    free(first);  // 释放内存
}
逻辑分析与参数说明:
  • TAILQ_INSERT_TAIL() :将节点插入到队列尾部,保证FIFO顺序。
  • TAILQ_FIRST() :获取队列的第一个节点。
  • TAILQ_REMOVE() :从队列中移除指定节点,注意必须传入正确的 field 字段名。

⚠️注意:以上操作在单线程下是安全的,但在多线程环境下,必须配合互斥锁使用,以防止数据竞争。

4.3 队列的同步与并发控制

4.3.1 互斥锁(pthread_mutex_t)在队列操作中的应用

在多线程环境中,多个线程可能同时访问队列,导致数据竞争。为了解决这个问题,可以使用互斥锁( pthread_mutex_t )来保护队列操作。

示例代码如下:

#include <pthread.h>

pthread_mutex_t queue_mutex = PTHREAD_MUTEX_INITIALIZER;

void enqueue_safe(struct data_queue_head *head, DataNode *node) {
    pthread_mutex_lock(&queue_mutex);
    TAILQ_INSERT_TAIL(head, node, entries);
    pthread_mutex_unlock(&queue_mutex);
}

DataNode* dequeue_safe(struct data_queue_head *head) {
    pthread_mutex_lock(&queue_mutex);
    DataNode *node = TAILQ_FIRST(head);
    if (node) {
        TAILQ_REMOVE(head, node, entries);
    }
    pthread_mutex_unlock(&queue_mutex);
    return node;
}
逻辑分析:
  • pthread_mutex_lock() :锁定互斥锁,防止其他线程同时操作队列。
  • pthread_mutex_unlock() :释放互斥锁,允许其他线程继续操作。

该方法可以确保队列的线程安全,但可能会引入性能瓶颈,特别是在高并发场景下。

4.3.2 条件变量(pthread_cond_t)实现的等待/通知机制

为了提高队列的并发效率,可以在队列为空时让线程等待,直到有新数据到来。此时可以使用条件变量配合互斥锁实现。

pthread_cond_t queue_not_empty = PTHREAD_COND_INITIALIZER;

DataNode* dequeue_with_wait(struct data_queue_head *head) {
    pthread_mutex_lock(&queue_mutex);
    while (TAILQ_EMPTY(head)) {
        pthread_cond_wait(&queue_not_empty, &queue_mutex);  // 等待数据
    }
    DataNode *node = TAILQ_FIRST(head);
    TAILQ_REMOVE(head, node, entries);
    pthread_mutex_unlock(&queue_mutex);
    return node;
}

void enqueue_with_signal(struct data_queue_head *head, DataNode *node) {
    pthread_mutex_lock(&queue_mutex);
    TAILQ_INSERT_TAIL(head, node, entries);
    pthread_cond_signal(&queue_not_empty);  // 通知等待线程
    pthread_mutex_unlock(&queue_mutex);
}
逻辑分析:
  • pthread_cond_wait() :释放锁并等待条件满足,当被唤醒时重新获取锁。
  • pthread_cond_signal() :唤醒一个等待的线程。

这种方式避免了忙等待,提高了线程效率。

4.3.3 多线程环境下队列的性能优化策略

在高并发场景下,以下策略可用于优化队列性能:

  • 使用无锁队列 :如CAS(Compare and Swap)实现的环形缓冲区,避免锁的开销。
  • 限制队列长度 :设置最大容量,防止内存溢出。
  • 使用读写锁 :允许并发读取,提高读操作性能。
  • 批量操作 :将多个数据一次性入队/出队,减少锁竞争。

⚙️建议:在实际开发中,结合性能测试选择最优方案。

4.4 队列的异常处理与内存管理

4.4.1 内存泄漏预防与节点释放机制

在使用动态内存分配的队列中,必须确保每个节点在出队后被正确释放,否则将导致内存泄漏。

DataNode *node = dequeue_safe(&head);
if (node) {
    free(node);  // 必须手动释放
}

建议封装一个队列销毁函数,释放所有节点:

void destroy_queue(struct data_queue_head *head) {
    DataNode *node;
    while ((node = TAILQ_FIRST(head)) != NULL) {
        TAILQ_REMOVE(head, node, entries);
        free(node);
    }
}

4.4.2 队列满溢与空读的处理逻辑

当队列已满时,继续入队可能导致数据丢失或程序崩溃。因此,应在入队前检查队列状态:

#define MAX_QUEUE_SIZE 100
int queue_size = 0;

void enqueue_with_check(struct data_queue_head *head, DataNode *node) {
    pthread_mutex_lock(&queue_mutex);
    if (queue_size >= MAX_QUEUE_SIZE) {
        printf("Queue full, dropping data\n");
        free(node);
    } else {
        TAILQ_INSERT_TAIL(head, node, entries);
        queue_size++;
    }
    pthread_mutex_unlock(&queue_mutex);
}

DataNode* dequeue_with_check(struct data_queue_head *head) {
    pthread_mutex_lock(&queue_mutex);
    if (TAILQ_EMPTY(head)) {
        pthread_mutex_unlock(&queue_mutex);
        return NULL;
    }
    DataNode *node = TAILQ_FIRST(head);
    TAILQ_REMOVE(head, node, entries);
    queue_size--;
    pthread_mutex_unlock(&queue_mutex);
    return node;
}

4.4.3 队列大小的动态调整与限制策略

为了适应不同负载场景,可以实现动态调整队列大小的功能。例如,当系统负载高时,自动增加队列容量;当负载低时减少容量,释放内存。

graph TD
    A[开始] --> B{队列是否满?}
    B -->|是| C[尝试扩容]
    B -->|否| D[正常入队]
    C --> E{扩容是否成功?}
    E -->|是| D
    E -->|否| F[丢弃数据或返回错误]
    D --> G[结束]

该策略通过监控队列使用情况,动态调整资源,提升系统适应性和稳定性。

5. 多线程并发处理(串口与WebSocket通信)

在现代工业控制和物联网应用中,串口通信往往需要与网络通信(如WebSocket)结合使用,实现远程数据采集与控制。为了提升系统并发处理能力与响应效率,采用多线程机制来分别处理串口通信与网络通信是常见的架构设计。本章将深入探讨如何使用C语言结合 pthread 库与 libwebsockets 库,实现串口与WebSocket的多线程协同处理。

5.1 多线程编程基础

5.1.1 pthread库的基本使用方法

POSIX线程库( pthread )是Linux下标准的线程处理接口,支持线程的创建、同步、互斥与销毁等操作。核心函数包括:

  • pthread_create() :创建线程
  • pthread_join() :等待线程结束
  • pthread_mutex_lock/unlock() :互斥锁操作
  • pthread_cond_wait() :条件变量等待

5.1.2 线程的创建与销毁

线程创建的基本示例代码如下:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

void* thread_func(void* arg) {
    printf("Thread is running...\n");
    return NULL;
}

int main() {
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, thread_func, NULL);
    if (ret != 0) {
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }

    pthread_join(tid, NULL); // 等待线程结束
    printf("Thread exited.\n");
    return 0;
}

5.1.3 线程间通信与数据共享机制

线程之间通常通过共享内存进行通信。例如,定义一个全局结构体变量用于保存串口数据,并在采集线程中更新、在通信线程中读取。

typedef struct {
    char data[256];
    size_t len;
    pthread_mutex_t lock;
} shared_data_t;

shared_data_t g_serial_data = {0};
pthread_mutex_init(&g_serial_data.lock, NULL);

5.2 串口数据采集线程的设计与实现

5.2.1 串口数据采集线程的生命周期管理

串口采集线程通常是一个循环线程,持续读取串口数据并缓存至共享队列中。其生命周期包括:

  • 初始化串口设备(open + termios配置)
  • 进入循环读取数据
  • 检测退出条件(如标志位)
void* serial_reader_thread(void* arg) {
    int fd = open_serial_port("/dev/ttyUSB0", B115200);
    if (fd < 0) {
        perror("open_serial_port");
        return NULL;
    }

    char buffer[256];
    while (!g_stop_flag) {
        ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
        if (bytes_read > 0) {
            pthread_mutex_lock(&g_serial_data.lock);
            memcpy(g_serial_data.data, buffer, bytes_read);
            g_serial_data.len = bytes_read;
            pthread_mutex_unlock(&g_serial_data.lock);

            // 可通知WebSocket线程有新数据
        }
    }
    close(fd);
    return NULL;
}

5.2.2 数据采集线程与主控线程的交互

主控线程通过设置 g_stop_flag 标志来通知采集线程终止运行:

volatile int g_stop_flag = 0;

void signal_handler(int sig) {
    g_stop_flag = 1;
}

5.2.3 采集频率与数据丢失的控制策略

采集频率可通过 usleep() 或定时器控制,避免过快采集导致资源浪费或数据丢失。建议设置合理缓冲区,并使用队列结构缓存数据。

5.3 WebSocket通信线程的设计与实现

5.3.1 libwebsockets库的基本使用流程

libwebsockets 是一个高性能的WebSocket库,支持客户端与服务端通信。基本流程如下:

  1. 初始化上下文
  2. 创建客户端或服务端协议
  3. 主循环中处理事件
  4. 发送与接收消息

5.3.2 客户端与服务器端的连接建立与维护

客户端连接示例代码(简化版):

struct lws_client_connect_info info;
memset(&info, 0, sizeof(info));
info.context = context;
info.address = "192.168.1.100";
info.port = 8080;
info.path = "/";
info.host = info.address;
info.origin = "origin";
info.protocol = protocols[0].name;

struct lws* wsi = lws_client_connect_via_info(&info);
if (!wsi) {
    fprintf(stderr, "Failed to connect to server\n");
}

5.3.3 消息收发机制与异步处理

WebSocket通信线程通常在一个循环中等待事件并处理数据:

while (!g_stop_flag) {
    lws_service(context, 50);
}

发送数据可通过如下方式:

unsigned char buf[LWS_PRE + 256];
unsigned char *p = &buf[LWS_PRE];
size_t len = prepare_data(p, sizeof(buf) - LWS_PRE);
lws_write(wsi, p, len, LWS_WRITE_TEXT);

5.4 多线程协同与资源调度

5.4.1 串口采集与WebSocket上传的协同流程

协同流程如下图所示(mermaid格式):

sequenceDiagram
    participant SerialThread
    participant MainThread
    participant WebSocketThread
    participant Server

    MainThread->>SerialThread: 启动采集线程
    MainThread->>WebSocketThread: 启动WebSocket线程
    SerialThread->>SerialThread: 循环读取串口数据
    SerialThread->>SharedQueue: 存入数据
    WebSocketThread->>SharedQueue: 提取数据
    WebSocketThread->>Server: 发送数据
    MainThread->>WebSocketThread: 接收控制指令
    MainThread->>SerialThread: 发送控制指令

5.4.2 CPU资源的合理分配与调度优先级

通过 pthread_setschedparam() 可设置线程优先级:

struct sched_param param;
param.sched_priority = 10;
pthread_setschedparam(tid, SCHED_FIFO, &param);

建议为采集线程设置较高优先级以保证实时性,WebSocket线程可适当降低优先级。

5.4.3 多线程程序的调试与性能优化方法

  • 调试工具 :使用 gdb 多线程调试、 strace 跟踪系统调用
  • 日志记录 :添加线程ID、时间戳、操作类型等信息
  • 性能分析 :使用 perf valgrind 进行性能瓶颈分析
  • 内存管理 :注意线程间共享结构体的生命周期管理,避免内存泄漏

本章内容展示了多线程如何在串口通信与WebSocket通信中协同工作,通过合理设计线程结构与资源共享机制,可以构建高效、稳定的数据采集与传输系统。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:在Linux系统中,串口通信是嵌入式和物联网开发的基础。本项目基于C语言实现串口的打开、配置与数据读写操作,并通过队列机制提升数据处理效率。项目还集成WebSocket协议,实现将串口接收的数据实时上传至服务器,具备完整的错误处理与多线程并发机制。适用于物联网设备通信、嵌入式系统数据上传等实际应用场景。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐