Linux外设之 串口(UART)的使用

串口(UART)C语言调用方式

目录

引言

串口(UART - Universal Asynchronous Receiver Transmitter)是嵌入式系统和Linux应用中最常用的外设之一,广泛应用于:

  • 与微控制器通信
  • 连接GPS模块、蓝牙模块等外设
  • 调试和日志输出
  • 工业设备通信
  • 传感器数据采集

本文将详细介绍在Linux系统中使用C语言调用串口的方法,包括核心API、代码示例。

核心API

Linux系统中,串口设备(实际上所有的字符设备和块设备都被表示为文件)被表示为文件(如 /dev/ttyS0/dev/ttyUSB0 等),可以使用标准的文件操作函数进行访问。但为了配置串口参数,需要使用 termios 库。

1. 主要头文件

c 复制代码
#include <stdio.h>      // 标准输入输出
#include <stdlib.h>     // 标准库
#include <unistd.h>     // UNIX标准函数
#include <fcntl.h>      // 文件控制
#include <termios.h>    // 终端控制
#include <string.h>     // 字符串处理
#include <errno.h>      // 错误处理

2. 核心函数

打开串口
c 复制代码
int open(const char *pathname, int flags);

参数

  • pathname:串口设备路径,如 "/dev/ttyS0"
  • flags:打开方式,如 O_RDWR | O_NOCTTY | O_NDELAY

返回值:成功返回文件描述符,失败返回 -1

配置串口
c 复制代码
int tcgetattr(int fd, struct termios *termios_p);
int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);

参数

  • fd:文件描述符
  • optional_actions:生效时机,如 TCSANOW(立即生效)
  • termios_p:termios 结构体指针

返回值:成功返回 0,失败返回 -1

设置波特率
c 复制代码
speed_t cfgetispeed(const struct termios *termios_p);
speed_t cfgetospeed(const struct termios *termios_p);
int cfsetispeed(struct termios *termios_p, speed_t speed);
int cfsetospeed(struct termios *termios_p, speed_t speed);
int cfsetspeed(struct termios *termios_p, speed_t speed);

参数

  • termios_p:termios 结构体指针
  • speed:波特率,如 B9600B115200

返回值:成功返回 0,失败返回 -1

读写操作
c 复制代码
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

参数

  • fd:文件描述符
  • buf:数据缓冲区
  • count:数据长度

返回值:成功返回读写的字节数,失败返回 -1

关闭串口
c 复制代码
int close(int fd);

参数

  • fd:文件描述符

返回值:成功返回 0,失败返回 -1

3. termios 结构体

c 复制代码
struct termios {
    tcflag_t c_iflag;    // 输入模式标志
    tcflag_t c_oflag;    // 输出模式标志
    tcflag_t c_cflag;    // 控制模式标志
    tcflag_t c_lflag;    // 本地模式标志
    cc_t     c_cc[NCCS]; // 控制字符
    speed_t  c_ispeed;   // 输入波特率
    speed_t  c_ospeed;   // 输出波特率
};

4. termios核心API详细说明

控制模式标志 (c_cflag)
标志 描述
CLOCAL 本地连接,忽略调制解调器状态线
CREAD 启用接收器
CSIZE 数据位掩码
CS5 5位数据位
CS6 6位数据位
CS7 7位数据位
CS8 8位数据位
CSTOPB 2位停止位(默认1位)
PARENB 启用奇偶校验
PARODD 使用奇校验(默认偶校验)
HUPCL 关闭时挂断调制解调器连接
CRTSCTS 启用硬件流控制(RTS/CTS)
输入模式标志 (c_iflag)
标志 描述
IGNBRK 忽略中断条件
BRKINT 中断条件产生SIGINT信号
IGNPAR 忽略奇偶校验错误
PARMRK 标记奇偶校验错误
INPCK 启用输入奇偶校验
ISTRIP 剥离8位数据的第8位
INLCR 将NL转换为CR
IGNCR 忽略CR
ICRNL 将CR转换为NL
IXON 启用XON/XOFF流控制
IXOFF 启用XOFF输入流控制
IXANY 任何字符都可以重启输入
输出模式标志 (c_oflag)
标志 描述
OPOST 启用输出处理
ONLCR 将NL转换为CR-NL
OCRNL 将CR转换为NL
ONOCR 第一列不输出CR
ONLRET NL执行CR功能
OFILL 使用填充字符而不是延迟
OFDEL 填充字符是DEL而不是NUL
本地模式标志 (c_lflag)
标志 描述
ISIG 启用信号产生
ICANON 启用规范模式
ECHO 回显输入字符
ECHOE 擦除字符时回显
ECHOK 回显KILL字符
ECHONL 回显NL
NOFLSH 不刷新输入队列
TOSTOP 后台进程尝试写入时发送SIGTTOU
控制字符 (c_cc)
索引 描述
VINTR 中断字符
VQUIT 退出字符
VERASE 擦除字符
VKILL 杀死字符
VEOF 文件结束字符
VTIME 读取超时(0.1秒为单位)
VMIN 最小读取字符数
VSWTC 交换字符
VSTART 开始字符
VSTOP 停止字符
VSUSP 挂起字符
波特率常量
常量 波特率
B0 0(挂断)
B50 50
B75 75
B110 110
B134 134.5
B150 150
B200 200
B300 300
B600 600
B1200 1200
B1800 1800
B2400 2400
B4800 4800
B9600 9600
B19200 19200
B38400 38400
B57600 57600
B115200 115200
B230400 230400
B460800 460800
B500000 500000
B576000 576000
B921600 921600
B1000000 1000000
串口控制函数
函数 描述 参数 返回值
tcgetattr 获取终端属性 int fd, struct termios *termios_p 成功返回0,失败返回-1
tcsetattr 设置终端属性 int fd, int optional_actions, const struct termios *termios_p 成功返回0,失败返回-1
tcflush 刷新输入/输出队列 int fd, int queue_selector 成功返回0,失败返回-1
tcflow 控制终端流 int fd, int action 成功返回0,失败返回-1
tcsendbreak 发送中断 int fd, int duration 成功返回0,失败返回-1
tcdrain 等待输出完成 int fd 成功返回0,失败返回-1
tcsetattr的optional_actions参数
参数 描述
TCSANOW 立即生效
TCSADRAIN 等待输出完成后生效
TCSAFLUSH 等待输出完成,刷新输入缓冲区后生效
tcflush的queue_selector参数
参数 描述
TCIFLUSH 刷新输入队列
TCOFLUSH 刷新输出队列
TCIOFLUSH 刷新输入和输出队列
串口配置流程
  1. 打开串口设备文件
  2. 使用tcgetattr获取当前配置
  3. 修改termios结构体中的配置项
  4. 使用tcsetattr应用新配置
  5. 使用tcflush清除缓冲区
  6. 进行读写操作
  7. 关闭串口设备文件

代码示例

1. 串口初始化与配置

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <string.h>
#include <errno.h>

/**
 * 打开并配置串口
 * @param port 串口设备路径
 * @param baud_rate 波特率
 * @return 成功返回文件描述符,失败返回 -1
 */
int uart_open(const char *port, int baud_rate)
{
    int fd;
    struct termios options;
    speed_t speed;

    /**
     * 打开串口设备文件
     * 
     * @details 打开指定路径的串口设备文件,获取文件描述符用于后续操作
     * @param pathname 串口设备路径,如 "/dev/ttyS0"
     * @param flags 打开方式标志组合
     *              O_RDWR: 读写模式
     *              O_NOCTTY: 不使设备成为控制终端
     *              O_NDELAY: 非阻塞模式
     * @return 成功返回文件描述符(非负整数),失败返回-1并设置errno
     * @see man 2 open
     */
    fd = open(port, O_RDWR | O_NOCTTY | O_NDELAY);
    if (fd < 0) {
        perror("open serial port failed");
        return -1;
    }

    // 获取当前配置
    if (tcgetattr(fd, &options) != 0) {
        perror("tcgetattr failed");
        close(fd);
        return -1;
    }

    // 设置波特率
    switch (baud_rate) {
        case 9600:
            speed = B9600;
            break;
        case 19200:
            speed = B19200;
            break;
        case 38400:
            speed = B38400;
            break;
        case 57600:
            speed = B57600;
            break;
        case 115200:
            speed = B115200;
            break;
        default:
            speed = B9600;
            printf("Unsupported baud rate, using 9600\n");
    }

    // 设置输入输出波特率
    if (cfsetispeed(&options, speed) != 0 || cfsetospeed(&options, speed) != 0) {
        perror("cfsetispeed/cfsetospeed failed");
        close(fd);
        return -1;
    }

    // 设置控制模式
    options.c_cflag |= (CLOCAL | CREAD);  // 启用接收器,设置本地模式
    options.c_cflag &= ~CSIZE;             // 清除数据位设置
    options.c_cflag |= CS8;                // 8位数据位
    options.c_cflag &= ~PARENB;            // 无校验位
    options.c_cflag &= ~CSTOPB;            // 1位停止位
    options.c_cflag &= ~CRTSCTS;           // 无硬件流控制

    // 设置输入模式
    options.c_iflag &= ~(IXON | IXOFF | IXANY);  // 无软件流控制
    options.c_iflag &= ~(INLCR | ICRNL | ISTRIP);  // 禁用特殊字符处理

    // 设置输出模式
    options.c_oflag &= ~OPOST;  // 原始输出

    // 设置本地模式
    options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);  // 原始输入

    // 设置超时
    options.c_cc[VTIME] = 10;  // 读取超时时间(0.1秒为单位)
    options.c_cc[VMIN] = 0;    // 最小读取字符数

    // 应用配置
    if (tcsetattr(fd, TCSANOW, &options) != 0) {
        perror("tcsetattr failed");
        close(fd);
        return -1;
    }

    // 清除缓冲区
    tcflush(fd, TCIOFLUSH);

    return fd;
}

/**
 * 关闭串口
 * @param fd 文件描述符
 */
void uart_close(int fd)
{
    if (fd >= 0) {
        close(fd);
    }
}

/**
 * 读取串口数据
 * @param fd 文件描述符
 * @param buf 缓冲区
 * @param len 缓冲区长度
 * @return 读取的字节数,失败返回 -1
 */
int uart_read(int fd, char *buf, int len)
{
    return read(fd, buf, len);
}

/**
 * 写入串口数据
 * @param fd 文件描述符
 * @param buf 缓冲区
 * @param len 数据长度
 * @return 写入的字节数,失败返回 -1
 */
int uart_write(int fd, const char *buf, int len)
{
    return write(fd, buf, len);
}

int main()
{
    int fd;
    char buf[256];
    int len;

    // 打开串口
    fd = uart_open("/dev/ttyS0", 115200);
    if (fd < 0) {
        printf("Failed to open serial port\n");
        return -1;
    }

    printf("Serial port opened successfully\n");

    // 发送数据
    const char *msg = "Hello, Serial Port!\n";
    len = uart_write(fd, msg, strlen(msg));
    if (len < 0) {
        perror("uart_write failed");
    } else {
        printf("Sent %d bytes: %s", len, msg);
    }

    // 读取数据(非阻塞)
    len = uart_read(fd, buf, sizeof(buf) - 1);
    if (len > 0) {
        buf[len] = '\0';
        printf("Received %d bytes: %s", len, buf);
    } else if (len == 0) {
        printf("No data received\n");
    } else {
        perror("uart_read failed");
    }

    // 关闭串口
    uart_close(fd);
    printf("Serial port closed\n");

    return 0;
}

2. 带超时的串口读取

c 复制代码
/**
 * 带超时的串口读取
 * @param fd 文件描述符
 * @param buf 缓冲区
 * @param len 缓冲区长度
 * @param timeout_ms 超时时间(毫秒)
 * @return 读取的字节数,失败返回 -1
 */
int uart_read_timeout(int fd, char *buf, int len, int timeout_ms)
{
    fd_set read_fds;
    struct timeval timeout;
    int ret;

    // 设置文件描述符集
    FD_ZERO(&read_fds);
    FD_SET(fd, &read_fds);

    // 设置超时时间
    timeout.tv_sec = timeout_ms / 1000;
    timeout.tv_usec = (timeout_ms % 1000) * 1000;

    // 等待数据
    ret = select(fd + 1, &read_fds, NULL, NULL, &timeout);
    if (ret < 0) {
        perror("select failed");
        return -1;
    } else if (ret == 0) {
        // 超时
        return 0;
    }

    // 有数据可读
    if (FD_ISSET(fd, &read_fds)) {
        return read(fd, buf, len);
    }

    return 0;
}

3. 串口配置示例

c 复制代码
/**
 * 配置串口参数
 * @param fd 文件描述符
 * @param baud_rate 波特率
 * @param data_bits 数据位(5-8)
 * @param parity 校验位(0: 无, 1: 奇, 2: 偶)
 * @param stop_bits 停止位(1-2)
 * @return 成功返回 0,失败返回 -1
 */
int uart_config(int fd, int baud_rate, int data_bits, int parity, int stop_bits)
{
    struct termios options;
    speed_t speed;

    // 获取当前配置
    if (tcgetattr(fd, &options) != 0) {
        perror("tcgetattr failed");
        return -1;
    }

    // 设置波特率
    switch (baud_rate) {
        case 9600:
            speed = B9600;
            break;
        case 19200:
            speed = B19200;
            break;
        case 38400:
            speed = B38400;
            break;
        case 57600:
            speed = B57600;
            break;
        case 115200:
            speed = B115200;
            break;
        default:
            speed = B9600;
            printf("Unsupported baud rate, using 9600\n");
    }

    if (cfsetispeed(&options, speed) != 0 || cfsetospeed(&options, speed) != 0) {
        perror("cfsetispeed/cfsetospeed failed");
        return -1;
    }

    // 设置控制模式
    options.c_cflag |= (CLOCAL | CREAD);
    options.c_cflag &= ~CSIZE;

    // 设置数据位
    switch (data_bits) {
        case 5:
            options.c_cflag |= CS5;
            break;
        case 6:
            options.c_cflag |= CS6;
            break;
        case 7:
            options.c_cflag |= CS7;
            break;
        case 8:
            options.c_cflag |= CS8;
            break;
        default:
            options.c_cflag |= CS8;
            printf("Unsupported data bits, using 8\n");
    }

    // 设置校验位
    switch (parity) {
        case 0:  // 无校验
            options.c_cflag &= ~PARENB;
            break;
        case 1:  // 奇校验
            options.c_cflag |= PARENB;
            options.c_cflag |= PARODD;
            break;
        case 2:  // 偶校验
            options.c_cflag |= PARENB;
            options.c_cflag &= ~PARODD;
            break;
        default:
            options.c_cflag &= ~PARENB;
            printf("Unsupported parity, using none\n");
    }

    // 设置停止位
    if (stop_bits == 2) {
        options.c_cflag |= CSTOPB;
    } else {
        options.c_cflag &= ~CSTOPB;
    }

    // 无硬件流控制
    options.c_cflag &= ~CRTSCTS;

    // 设置输入模式
    options.c_iflag &= ~(IXON | IXOFF | IXANY);
    options.c_iflag &= ~(INLCR | ICRNL | ISTRIP);

    // 设置输出模式
    options.c_oflag &= ~OPOST;

    // 设置本地模式
    options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);

    // 设置超时
    options.c_cc[VTIME] = 10;
    options.c_cc[VMIN] = 0;

    // 应用配置
    if (tcsetattr(fd, TCSANOW, &options) != 0) {
        perror("tcsetattr failed");
        return -1;
    }

    // 清除缓冲区
    tcflush(fd, TCIOFLUSH);

    return 0;
}

常见问题与解决方案

1. 串口无法打开

问题open() 函数返回 -1,错误信息为 "Permission denied"

解决方案

  • 检查当前用户是否有串口设备的访问权限
  • 将用户添加到 dialout 组:sudo usermod -a -G dialout username
  • 重新登录后生效

问题open() 函数返回 -1,错误信息为 "No such file or directory"

解决方案

  • 检查串口设备路径是否正确
  • 确认串口设备是否存在:ls -la /dev/tty*
  • 对于 USB 转串口设备,检查设备是否正确识别

2. 数据传输问题

问题:发送数据后,接收端收到的数据不正确

解决方案

  • 检查波特率、数据位、校验位、停止位设置是否一致
  • 检查流控制设置是否正确
  • 确认线缆连接是否可靠

问题:读取数据时阻塞

解决方案

  • 检查 VMINVTIME 设置
  • 使用 select() 实现超时机制
  • 确保串口设置为非阻塞模式(如果需要)

3. 性能问题

问题:串口数据传输速度较慢

解决方案

  • 提高波特率(如果硬件支持)
  • 优化缓冲区大小
  • 减少系统调用次数,使用批量读写
  • 考虑使用 termiosRAW 模式减少处理开销
相关推荐
赛博云推-Twitter热门霸屏工具2 小时前
推特自动化营销新趋势:赛博云推如何实现热门霸屏与精准获客
运维·科技·自动化·媒体·twitter
劳埃德福杰2 小时前
Windows电脑安装双系统,如何删除其中一个系统
运维·windows·电脑·笔记本电脑
yuanmenghao2 小时前
WSL + Docker GPU 环境排查:NVIDIA-SMI couldn‘t find libnvidia-ml.so 问题分析与解决
linux·运维·服务器·docker·容器
MIXLLRED2 小时前
随笔——用指令打开与复制ubuntu的文件
linux·ubuntu
智能工业品检测-奇妙智能2 小时前
大疆无人机如何通过MQTT获取实时视频流?
运维·服务器·人工智能·mqtt·无人机
_OP_CHEN2 小时前
【MySQL数据库基础】(五)MySQL 数据类型深度解析:选对类型 = 性能拉满!
linux·开发语言·数据库·sql·mysql·数据类型·c/c++
优秀的老黄2 小时前
MySQL配置从库
linux·数据库·mysql
renhongxia12 小时前
AgentLTV:基于代理的统一搜索与演化自动化终身价值预测框架
运维·自动化
AI精钢2 小时前
WSL 磁盘清理实战:从缓存清理到 ext4.vhdx 压缩回收空间
运维·windows·缓存·docker·wsl·devops·磁盘清理