Linux串口应用编程

一、Linux串口驱动框架

串口通信协议介绍可参照STM32串口通信(寄存器与hal库实现)

1. 硬件层:串口通信的物理基础

"Hardware" 部分对应串口的物理传输链路:

Terminal:终端设备(如串口调试工具、嵌入式开发板的串口接口);

Physical line:物理传输介质(如 RS-232 线缆);

UART:通用异步收发器(硬件模块,负责串口数据的串并转换、波特率控制等)。

2. 内核驱动层:Linux 串口的核心处理流程

"Software" 的 "Kernel" 部分是串口驱动的核心,包含三个关键模块:

  • UART driver(UART 驱动)
    直接与硬件 UART 交互,负责底层硬件操作(如初始化 UART 寄存器、设置波特率 / 数据位 / 校验位、收发原始字节)。
  • Line discipline(行规程)
    对 UART 驱动收发的原始字节进行上层协议处理(如换行符转换、回显控制、特殊字符解析)。
    是 "原始字节" 到 "终端命令" 的中间层。例如,将串口收到的\r自动转换为\n,或处理Ctrl+C这类中断信号。
  • TTY driver(TTY 驱动)
    向上提供统一的终端设备接口(如 /dev/ttyS0),让用户进程可以像操作普通文件一样读写串口。
    将 UART 硬件和行规程的复杂逻辑封装,对外暴露简单的文件操作接口(read/write/ioctl等)。

3. 用户层:进程与串口的交互

图中 "User process" 是用户空间的应用程序,通过文件系统接口与串口交互:

应用程序通过 open/read/write/close 等系统调用操作 /dev/ttyS0 这类设备文件,底层由 TTY 驱动转发到行规程和 UART 驱动,最终完成数据收发。

4. 数据流向示例(以 "用户进程发送数据" 为例)

用户进程调用 write("/dev/ttyS0", "hello", 5),数据进入 TTY 驱动;

TTY 驱动将数据传递给行规程,行规程根据配置(如是否需要添加\r)处理字节;

处理后的数据传递给 UART 驱动,UART 驱动将字节发送到硬件 UART;

硬件 UART 将字节通过物理线路发送到终端设备。

二、串口API

Linux 系统中,操作设备的统一接口就是:open/ioctl/read/write。

对于 UART,封装了相关API用来设置行规程和相关参数,所以对于 UART,编程的套路就是:

  • open;
  • 通过结构体termios设置行规程和相关参数,比如波特率、数据位、停止位、检验位、RAW 模式、一有数据就返回;
  • read/write;
c 复制代码
struct termios
  {
    tcflag_t c_iflag;		/* input mode flags */
    tcflag_t c_oflag;		/* output mode flags */
    tcflag_t c_cflag;		/* control mode flags */
    tcflag_t c_lflag;		/* local mode flags */
    cc_t c_line;			/* line discipline */
    cc_t c_cc[NCCS];		/* control characters */
    speed_t c_ispeed;		/* input speed */
    speed_t c_ospeed;		/* output speed */
#define _HAVE_STRUCT_TERMIOS_C_ISPEED 1
#define _HAVE_STRUCT_TERMIOS_C_OSPEED 1
  };

struct termios 是一个 "统一的配置接口",它包含了两类参数 ------硬件相关参数(波特率、数据位等)和行规程相关参数(回显、规范模式等)。

设置 termios 时,硬件参数会被转发给串口驱动(UART 驱动)处理,而行规程参数才由行规程模块处理。

相关函数

函数名 作用
tcgetattr get terminal attributes,获得终端的属性
tcsetattr set terminal attributes,修改终端参数
tcflush 清空终端未完成的输入/输出请求及数据
cfsetispeed sets the input baud rate,设置输入波特率
cfsetospeed sets the output baud rate,设置输出波特率
cfsetspeed 同时设置输入、输出波特率

三、串口收发实验

将开发板串口的发送、接收引脚短接,实现自发自收:使用 write 函数发出字符,使用 read 函数读取字符。

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

/* set_opt(fd,115200,8,'N',1) */
int set_opt(int fd, int nSpeed, int nBits, char nEvent, int nStop)
{
    struct termios newtio, oldtio;

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

    // 初始化新配置
    bzero(&newtio, sizeof(newtio));

    // 设置串口基本通信属性
    // CLOCAL:忽略调制解调器(Modem)的状态线(串口通常直接连接设备,无需 Modem 控制)
    // 确保串口 "本地拥有",不受其他设备控制;
    // CREAD:启用接收功能(允许从串口读取数据)
    newtio.c_cflag |= CLOCAL | CREAD;
    // 清除数据位掩码
    // CSIZE 是数据位的 "掩码标志"(包含 CS5/CS6/CS7/CS8,分别对应 5/6/7/8 位数据)
    newtio.c_cflag &= ~CSIZE;

    // 输入模式:关闭规范模式、回显、中断信号响应
    newtio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); /*Input*/
    // 输出模式:关闭输出处理(不修改输出数据)
    newtio.c_oflag &= ~OPOST; /*Output*/

    // 设置数据位
    switch (nBits)
    {
    case 7:
        newtio.c_cflag |= CS7;
        break;
    case 8:
        newtio.c_cflag |= CS8;
        break;
    }

    // 设置校验位(奇 / 偶 / 无校验)
    switch (nEvent)
    {
    // 奇校验
    case 'O':
        newtio.c_cflag |= PARENB;
        newtio.c_cflag |= PARODD;
        newtio.c_iflag |= (INPCK | ISTRIP);
        break;
    // 偶校验
    case 'E':
        newtio.c_iflag |= (INPCK | ISTRIP);
        newtio.c_cflag |= PARENB;
        newtio.c_cflag &= ~PARODD;
        break;
    // 无校验
    case 'N':
        newtio.c_cflag &= ~PARENB;
        break;
    }

    // 设置波特率
    switch (nSpeed)
    {
    case 2400:
        cfsetispeed(&newtio, B2400);
        cfsetospeed(&newtio, B2400);
        break;
    case 4800:
        cfsetispeed(&newtio, B4800);
        cfsetospeed(&newtio, B4800);
        break;
    case 9600:
        cfsetispeed(&newtio, B9600);
        cfsetospeed(&newtio, B9600);
        break;
    case 115200:
        cfsetispeed(&newtio, B115200);
        cfsetospeed(&newtio, B115200);
        break;
    default:
        cfsetispeed(&newtio, B9600);
        cfsetospeed(&newtio, B9600);
        break;
    }

    // 设置停止位
    if (nStop == 1)
        newtio.c_cflag &= ~CSTOPB;
    else if (nStop == 2)
        newtio.c_cflag |= CSTOPB;

    // 设置读取阻塞参数
    // 至少读到1个字节才返回
    newtio.c_cc[VMIN] = 1; /* 读数据时的最小字节数: 没读到这些数据我就不返回! */
    // 不设置超时(若没有数据,read 会一直等,直到有数据到达)
    newtio.c_cc[VTIME] = 0; /* 等待第1个数据的时间:
                             * 比如VMIN设为10表示至少读到10个数据才返回,
                             * 但是没有数据总不能一直等吧? 可以设置VTIME(单位是10秒)
                             * 假设VTIME=1,表示:
                             *    10秒内一个数据都没有的话就返回
                             *    如果10秒内至少读到了1个字节,那就继续等待,完全读到VMIN个数据再返回
                             */

    tcflush(fd, TCIFLUSH);

    // 应用新配置
    // TCSANOW 表示 "立即生效"
    if ((tcsetattr(fd, TCSANOW, &newtio)) != 0)
    {
        perror("com set error");
        return -1;
    }
    // printf("set done!\n");
    return 0;
}

int open_port(char *com)
{
    int fd;

    // O_NOCTTY不要让设备成为控制终端
    fd = open(com, O_RDWR | O_NOCTTY);
    if (-1 == fd)
    {
        return -1;
    }

    // 恢复串口为阻塞状态
    if (fcntl(fd, F_SETFL, 0) < 0)
    {
        printf("fcntl failed!\n");
        return -1;
    }

    return fd;
}

/*
 * ./serial_send_recv <dev>
 */
int main(int argc, char **argv)
{
    int fd;
    int iRet;
    char c;

    if (argc != 2)
    {
        printf("Usage: \n");
        printf("%s </dev/ttySAC1 or other>\n", argv[0]);
        return -1;
    }

    fd = open_port(argv[1]);
    if (fd < 0)
    {
        printf("open %s err!\n", argv[1]);
        return -1;
    }

    iRet = set_opt(fd, 115200, 8, 'N', 1);
    if (iRet)
    {
        printf("set port err!\n");
        return -1;
    }

    printf("Enter a char:");
    while (1)
    {
        scanf("%c", &c);
        iRet = write(fd, &c, 1);
        iRet = read(fd, &c, 1);
        if (iRet == 1)
        {
            printf("get: %02x %c\n", c, c);
        }
        else
        {
            printf("can not get data\n");
        }
    }

    return 0;
}

编译执行

shell 复制代码
arm-buildroot-linux-gnueabihf-gcc serial_send_recv.c -o serial_send_recv 

./serial_send_recv /dev/ttymxc5

对于开发板100ASK_IMX6ULL_mini

/dev/ttymxc5对应串口6,在没有IMX6ULL扩展板情况下,串口6的收发接口:9对应TX,11对应RX。

四、GPS模块实验

使用串口进行通讯,波特率为 9600bps,1bit 停止位,无校验位,无流控,默认每秒输出一次标准格式数据。

GPS接收到数据的格式如下:

复制代码
$GPGGA ,<1> ,<2> ,<3> ,<4> ,<5> ,<6> ,<7> ,<8> ,<9> ,M ,<10> ,M ,<11> ,<12>*hh<CR><LF>

各字段含义

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

/* set_opt(fd,115200,8,'N',1) */
int set_opt(int fd, int nSpeed, int nBits, char nEvent, int nStop)
{
    struct termios newtio, oldtio;

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

    // 初始化新配置
    bzero(&newtio, sizeof(newtio));

    // 设置串口基本通信属性
    // CLOCAL:忽略调制解调器(Modem)的状态线(串口通常直接连接设备,无需 Modem 控制)
    // 确保串口 "本地拥有",不受其他设备控制;
    // CREAD:启用接收功能(允许从串口读取数据)
    newtio.c_cflag |= CLOCAL | CREAD;
    // 清除数据位掩码
    // CSIZE 是数据位的 "掩码标志"(包含 CS5/CS6/CS7/CS8,分别对应 5/6/7/8 位数据)
    newtio.c_cflag &= ~CSIZE;

    // 输入模式:关闭规范模式、回显、中断信号响应
    newtio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); /*Input*/
    // 输出模式:关闭输出处理(不修改输出数据)
    newtio.c_oflag &= ~OPOST; /*Output*/

    // 设置数据位
    switch (nBits)
    {
    case 7:
        newtio.c_cflag |= CS7;
        break;
    case 8:
        newtio.c_cflag |= CS8;
        break;
    }

    // 设置校验位(奇 / 偶 / 无校验)
    switch (nEvent)
    {
    // 奇校验
    case 'O':
        newtio.c_cflag |= PARENB;
        newtio.c_cflag |= PARODD;
        newtio.c_iflag |= (INPCK | ISTRIP);
        break;
    // 偶校验
    case 'E':
        newtio.c_iflag |= (INPCK | ISTRIP);
        newtio.c_cflag |= PARENB;
        newtio.c_cflag &= ~PARODD;
        break;
    // 无校验
    case 'N':
        newtio.c_cflag &= ~PARENB;
        break;
    }

    // 设置波特率
    switch (nSpeed)
    {
    case 2400:
        cfsetispeed(&newtio, B2400);
        cfsetospeed(&newtio, B2400);
        break;
    case 4800:
        cfsetispeed(&newtio, B4800);
        cfsetospeed(&newtio, B4800);
        break;
    case 9600:
        cfsetispeed(&newtio, B9600);
        cfsetospeed(&newtio, B9600);
        break;
    case 115200:
        cfsetispeed(&newtio, B115200);
        cfsetospeed(&newtio, B115200);
        break;
    default:
        cfsetispeed(&newtio, B9600);
        cfsetospeed(&newtio, B9600);
        break;
    }

    // 设置停止位
    if (nStop == 1)
        newtio.c_cflag &= ~CSTOPB;
    else if (nStop == 2)
        newtio.c_cflag |= CSTOPB;

    // 设置读取阻塞参数
    // 至少读到1个字节才返回
    newtio.c_cc[VMIN] = 1; /* 读数据时的最小字节数: 没读到这些数据我就不返回! */
    // 不设置超时(若没有数据,read 会一直等,直到有数据到达)
    newtio.c_cc[VTIME] = 0; /* 等待第1个数据的时间:
                             * 比如VMIN设为10表示至少读到10个数据才返回,
                             * 但是没有数据总不能一直等吧? 可以设置VTIME(单位是10秒)
                             * 假设VTIME=1,表示:
                             *    10秒内一个数据都没有的话就返回
                             *    如果10秒内至少读到了1个字节,那就继续等待,完全读到VMIN个数据再返回
                             */

    tcflush(fd, TCIFLUSH);

    // 应用新配置
    // TCSANOW 表示 "立即生效"
    if ((tcsetattr(fd, TCSANOW, &newtio)) != 0)
    {
        perror("com set error");
        return -1;
    }
    // printf("set done!\n");
    return 0;
}

int open_port(char *com)
{
    int fd;

    // O_NOCTTY不要让设备成为控制终端
    fd = open(com, O_RDWR | O_NOCTTY);
    if (-1 == fd)
    {
        return -1;
    }

    // 恢复串口为阻塞状态
    if (fcntl(fd, F_SETFL, 0) < 0)
    {
        printf("fcntl failed!\n");
        return -1;
    }

    return fd;
}

int read_gps_raw_data(int fd, char *buf)
{
    int i = 0;
    int iRet;
    char c;
    int start = 0;

    while (1)
    {
        iRet = read(fd, &c, 1);
        if (iRet == 1)
        {
            if (c == '$')
                start = 1;
            if (start)
            {
                buf[i++] = c;
            }
            if (c == '\n' || c == '\r')
            {
                buf[i] = '\0';
                return 0;
            }
        }
        else
        {
            return -1;
        }
    }
}

/* eg. $GPGGA,082559.00,4005.22599,N,11632.58234,E,1,04,3.08,14.6,M,-5.6,M,,*76"<CR><LF> */
int parse_gps_raw_data(char *buf, char *time, char *lat, char *ns, char *lng, char *ew)
{
    char tmp[10];

    if (buf[0] != '$')
        return -1;
    else if (strncmp(buf + 3, "GGA", 3) != 0)
        return -1;
    else if (strstr(buf, ",,,,,"))
    {
        printf("Place the GPS to open area\n");
        return -1;
    }
    else
    {
        // printf("raw data: %s\n", buf);
        //  [^,] 表示 "匹配所有 不是逗号(,) 的字符";
        sscanf(buf, "%[^,],%[^,],%[^,],%[^,],%[^,],%[^,]", tmp, time, lat, ns, lng, ew);
        return 0;
    }
}

/*
 * ./serial_send_recv <dev>
 */
int main(int argc, char **argv)
{
    int fd;
    int iRet;
    char c;
    char buf[1000];
    char time[100];
    char Lat[100];
    char ns[100];
    char Lng[100];
    char ew[100];

    float fLat, fLng;

    if (argc != 2)
    {
        printf("Usage: \n");
        printf("%s </dev/ttySAC1 or other>\n", argv[0]);
        return -1;
    }

    fd = open_port(argv[1]);
    if (fd < 0)
    {
        printf("open %s err!\n", argv[1]);
        return -1;
    }

    iRet = set_opt(fd, 115200, 8, 'N', 1);
    if (iRet)
    {
        printf("set port err!\n");
        return -1;
    }

    while (1)
    {
        /* eg. $GPGGA,082559.00,4005.22599,N,11632.58234,E,1,04,3.08,14.6,M,-5.6,M,,*76"<CR><LF>*/
        iRet = read_gps_raw_data(fd, buf);
        if (iRet == 0)
        {
            printf("GPS raw data: %s\n", buf);
            iRet = parse_gps_raw_data(buf, time, Lat, ns, Lng, ew);
        }

        if (iRet == 0)
        {
            printf("Time : %s\n", time);
            printf("ns   : %s\n", ns);
            printf("ew   : %s\n", ew);
            printf("Lat  : %s\n", Lat);
            printf("Lng  : %s\n", Lng);

            /* 纬度格式: ddmm.mmmm */
            sscanf(Lat + 2, "%f", &fLat);
            // 将分转换为度
            fLat = fLat / 60;
            // Lat[0] - '0'将字符转换为数字
            fLat += (Lat[0] - '0') * 10 + (Lat[1] - '0');

            /* 经度格式: dddmm.mmmm */
            sscanf(Lng + 3, "%f", &fLng);
            fLng = fLng / 60;
            fLng += (Lng[0] - '0') * 100 + (Lng[1] - '0') * 10 + (Lng[2] - '0');
            printf("Lng,Lat: %.06f,%.06f\n", fLng, fLat);
        }
    }

    return 0;
}
相关推荐
再睡一夏就好3 小时前
【C++闯关笔记】详解多态
c语言·c++·笔记·学习·语法·1024程序员节
Justin_193 小时前
Galera Cluster部署
linux·服务器·nginx
wanglong37133 小时前
STM32单片机PWM驱动无源蜂鸣器模块C语言程序
stm32·单片机·1024程序员节
与己斗其乐无穷3 小时前
C++学习记录(22)异常
学习·1024程序员节
云雾J视界3 小时前
开关电源拓扑工程宝典:从原理到实战的深度设计指南
gan·boost·开关电源·1024程序员节·buck·拓扑电路
FinTech老王3 小时前
国产数据库MongoDB兼容性技术分析与实践对比
mongodb·1024程序员节
小雨青年3 小时前
鸿蒙 HarmonyOS 6|ArkUI(03):状态管理
华为·harmonyos·1024程序员节
墨理学AI4 小时前
Kylin Linux Advanced Server V10 上成功安装 NVIDIA Container Toolkit
1024程序员节
御承扬4 小时前
编程素养提升之EffectivePython(Builder篇)
python·设计模式·1024程序员节