当前位置: 首页 > news >正文

嵌入式Linux——8 串口

目录

1.终端(tty)

/dev/tty*:物理/虚拟终端

/dev/pts/*:伪终端

/dev/tty:当前进程的控制终端

/dev/tty0:当前活动的虚拟控制台

2.行规程模式(line discipline)

比较行规程和原始模式:

1. 行规程模式

2.原始模式

 3.串口API

termios 结构体

基本的API

4.串口实验(看看代码怎么写)

4.1 串口回环实验(传出去马上传回来)

自定义 set_opt 设置串口参数函000数:

自定义 open_port 打开串口设备函数 

main函数示例

4.2 GPS 模块实验 (不看也行,差不多的,就是加了点应用性)


1.终端(tty)

/dev/tty*:物理/虚拟终端

/dev/ttyS*:物理串口(如 RS-232)

/dev/ttyUSB*:USB 转串口设备

/dev/tty1 ~ tty63:本地虚拟控制台(通过 Ctrl+Alt+F1~F12 切换)


/dev/pts/*:伪终端

特点
        动态创建:由终端模拟器或 SSH 会话按需生成,退出后自动消失。

        无硬件关联:完全由软件模拟,用于多用户会话管理。


典型用途

        SSH 远程连接

        图形界面中的终端模拟器(就是Ubuntu图形化界面的终端,虽然它是tty2,但它是运行在 X Server/Wayland 上的,而不是原生的文本控制台,是通过 伪终端(/dev/pts/*) 实现的,与 /dev/tty0 无直接关联)


/dev/tty:当前进程的控制终端

        指向当前会话实际使用的终端设备

切换到 tty3,执行su root之后,执行下面命令观察现象:

while [ 1 ]; do echo msg_from_tty3 > /dev/tty; sleep 3; done

现象:只有执行命令的那个终端会收到并打印信息 

/dev/tty0:当前活动的虚拟控制台

代表当前前台虚拟终端,不适用于伪终端(如 SSH 或图形终端

切换到 tty3,执行su root之后,执行下面命令观察现象:

while [ 1 ]; do echo msg_from_tty3 > /dev/tty0; sleep 3; done

现象:把哪个终端切换到前台,那个终端就会收到并打印信息

2.行规程模式(line discipline)

在终端和串口通信的过程中,设备和程序之间有一个行规程模式处理方式

韦的图,大致意思就是说 pc 在调试开发板的串口终端上输入一个字符 “a” ,通过串口传到开发板后,先不传入程序(app)中,而是保存在行规程中,然后行规程会把当前字符回传给 pc,所以串口终端界面上会出现我们输入的字符,这个过程叫做 “回显”,需要退格删掉一个字符也是把退格传到行规程,行规程删除字符后再把现存字符回显到 pc 上。直到行规程收到 “回车键” ,才会把保存的字符都发送给程序(app)处理

后面需要把设备的行规程模式设置为原始模式,是因为需要把信息的处理全权交给程序

比较行规程原始模式:

1. 行规程模式

特点:

        行缓冲:数据按行处理(遇到\n或EOF才提交给程序)

        字符回显:输入字符会显示在终端上

        特殊字符处理:支持Ctrl+C(中断)、Ctrl+Z(暂停)等控制功能


典型场景:

        用户交互式终端(如SSH会话、本地Shell)

        需要逐行输入的命令行工具

示例:在行规程模式下,输入 hello 后按回车,程序才会收到完整字符串

2.原始模式

特点:

        无缓冲:数据立即传递给程序,无需等待行结束符

        无回显:输入字符不自动显示

        禁用控制字符:Ctrl+C等被视为普通数据

        完全控制:可精确设置数据位、超时等参数

典型场景:

        串口通信(如与单片机、传感器通信)

        需要实时响应的应用(如游戏、网络协议栈)

        二进制数据传输

示例:在原始模式下,每次接收到1个字节就会立即触发读取操作。

可以看看下面原因:

 3.串口API

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

对于 UART,又在 ioctl 之上封装了很多函数,主要是用来设置行规程等参数

所以UART应用编程的套路就是:

  1. open;
  2. 设置行规程,比如波特率、数据位、停止位、检验位、RAW 模式(ioctl)
  3. read/write;

termios 结构体

struct termios 是 Linux 系统中用于终端 I/O 控制的关键数据结构,定义在 <termios.h> 头文件中。它包含了终端设备的全部控制参数,用于配置串口、控制台等设备的通信行为。

基本的API

tc:terminal contorl
cf:control flag

tcgetattr:获取终端的属性
tcsetattr:修改终端参数
tcflush:清空终端未完成的输入/输出请求及数据
cfsetispeed: 设置输入波特率
cfsetospeed: 设置输出波特率
cfsetspeed: 同时设置输入、输出波特率

这些API其实就是修改上面的 termios 结构体,这些函数更底层其实就是调用 ioctl 修改 termios 结构体

4.串口实验(看看代码怎么写)

4.1 串口回环实验(传出去马上传回来)

自定义 set_opt 设置串口参数函000数:

/**
 * 设置串口参数
 * @param fd      串口文件描述符
 * @param nSpeed  波特率(2400/4800/9600/115200)
 * @param nBits   数据位(7或8)
 * @param nEvent  校验方式(N:无校验,O:奇校验,E:偶校验)
 * @param nStop   停止位(1或2)
 * @return        成功返回0,失败返回-1
 */
int set_opt(int fd, int nSpeed, int nBits, char nEvent, int nStop) 
{
    struct termios newtio, oldtio;

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

    /* 2. 初始化新配置结构体 */
    bzero(&newtio, sizeof(newtio));

    /* 3. 设置控制模式标志 */
    newtio.c_cflag |= CLOCAL | CREAD;  // 保持本地连接和启用接收
    newtio.c_cflag &= ~CSIZE;          // 清除数据位掩码

    /* 4. 设置输入/输出模式 */
    newtio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);  // 原始输入模式(非规范模式)
    newtio.c_oflag &= ~OPOST;                           // 原始输出模式(无处理)

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

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

    /* 7. 设置波特率 */
    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:  // 默认9600
        cfsetispeed(&newtio, B9600);
        cfsetospeed(&newtio, B9600);
        break;
    }

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

    /* 9. 设置非规范模式下的读取参数 */
    newtio.c_cc[VMIN]  = 1;   // 最小读取字节数:至少读取1字节才返回
    newtio.c_cc[VTIME] = 0;   // 超时时间(单位:0.1秒),0表示无限等待
                              //等待第1个数据的时间
                              //比如VMIN设为10表示至少读到10个数据才返回,但是没有数据总不能一直等吧? 可以设置VTIME,如果超时时间内至少读到了1个字节,那就继续等待,完全读到VMIN个数据再返回
    

    /* 10. 清空输入缓冲区 */
    tcflush(fd, TCIFLUSH);

    /* 11. 应用新配置(立即生效) */
    if (tcsetattr(fd, TCSANOW, &newtio) != 0) {
        perror("tcsetattr failed");
        return -1;
    }

    return 0;
}

自定义 open_port 打开串口设备函数 

/**
 * 打开并初始化串口设备
 * @param com 串口设备路径(如 "/dev/ttyS0")
 * @return 成功返回文件描述符,失败返回-1
 */
int open_port(char *com) 
{
    int fd;

    /* 以读写模式打开串口设备,并确保不被用作控制终端 */
    fd = open(com, O_RDWR | O_NOCTTY);
    if (fd == -1) {
        perror("open serial port failed");
        return -1;
    }

    /* 显式设置文件状态标志为阻塞模式,也就是当程序无法读/写数据,程序会休眠*/
    if (fcntl(fd, F_SETFL, 0) < 0) {
        perror("fcntl F_SETFL failed");
        close(fd);  // 失败时关闭文件描述符
        return -1;
    }

    return fd;  // 返回有效的文件描述符
}

main函数示例

/**
 * 串口通信测试程序
 * 功能:打开串口,配置参数(115200,8N1),实现简单的回显测试
 */
int main(int argc, char **argv) 
{
    int fd;      // 串口文件描述符
    int iRet;    // 操作返回值
    char c;      // 读写数据的缓冲区

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

    /* 1. 打开串口 */
    fd = open_port(argv[1]);
    if (fd < 0) {
        printf("open %s err!\n", argv[1]);
        return -1;
    }

    /* 2. 配置串口参数(115200波特率,8数据位,无校验,1停止位)*/
    iRet = set_opt(fd, 115200, 8, 'N', 1);
    if (iRet) {
        printf("set port err!\n");
        close(fd);  // 配置失败时关闭串口
        return -1;
    }

    /* 3. 串口读写测试 */
    printf("Enter a char: ");
    while (1) {
        /* 从标准输入获取字符 */
        scanf("%c", &c);

        /* 写入串口 */
        iRet = write(fd, &c, 1);
        if (iRet != 1) {
            printf("write failed\n");
            continue;
        }

        /* 从串口读取回显数据(非阻塞模式立即返回)*/
        iRet = read(fd, &c, 1);
        if (iRet == 1) {
            printf("get: %02x %c\n", c, c);  // 打印十六进制和ASCII格式
        } else {
            printf("can not get data\n");
        }
    }

    close(fd);  // 理论上不会执行到这里
    return 0;
}

因为有可能会出现读取串口数据时,由于串口回环设备传输太慢,导致发送后回环读取时会出现读取失败的情况,是因为数据还没传到。

set_opt 中 :

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

这两段代码就设置了等待数据时间(这里设置为0,即无限等待)

这就解决了问题,把数据等到程序才继续运行,否则就一直阻塞

4.2 GPS 模块实验 (不看也行,差不多的,就是加了点应用性)

使用串口接收数据,收到的数据包含:$GPGGA(GPS 定位数据)、$GPGLL (地理定位信息)、$GPGSA(当前卫星信息)、$GPGSV(可见卫星状态信息)、 $GPRMC(推荐最小定位信息)、$GPVTG(地面速度信息)

只分析$GPGGA (Global Positioning System Fix Data)即可, 它包含了 GPS 定位经纬度、质量因子、HDOP、高程、参考站号等字段。

数据标准格式:
$GPGGA,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,M,<10>,M,<11>,<12>*hh

其他关于gps的介绍看手册吧,这里只放代码

沿用上面的 set_opt 和 open_port 函数

/**
 * GPS数据读取与解析程序
 * 功能:从串口读取GPS模块的NMEA数据并解析位置信息
 */

/**
 * 从串口读取一行GPS原始数据
 * @param fd 串口文件描述符
 * @param buf 存储读取数据的缓冲区
 * @return 成功返回0,失败返回-1
 */
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);  // 每次读取1个字符
        if (iRet == 1) {
            if (c == '$') {  // NMEA语句起始符
                start = 1;
                i = 0;       // 重置缓冲区索引
            }
            
            if (start) {
                buf[i++] = c;  // 存储有效数据
            }
            
            // 遇到换行符表示一行数据结束
            if (c == '\n' || c == '\r') {
                buf[i] = '\0';  // 添加字符串结束符
                return 0;
            }
        } else {
            return -1;  // 读取失败
        }
    }
}

/**
 * 解析GPS原始数据(GPGGA格式)
 * @param buf 原始数据缓冲区
 * @param time 存储时间信息
 * @param lat 存储纬度
 * @param ns 存储南北半球
 * @param lng 存储经度
 * @param ew 存储东西半球
 * @return 成功返回0,失败返回-1
 */
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) {  // 必须是GPGGA语句
        return -1;
    } else if (strstr(buf, ",,,,,")) {  // 无效定位数据
        printf("Place the GPS to open area\n");
        return -1;
    } else {
        // 解析关键字段
        sscanf(buf, "%[^,],%[^,],%[^,],%[^,],%[^,],%[^,]",
               tmp, time, lat, ns, lng, ew);
        return 0;
    }
}

/*
 * 主函数
 * 用法:./gps_reader </dev/ttySAC1 or other>
 */
int main(int argc, char **argv)
{
    int fd;              // 串口文件描述符
    int iRet;            // 函数返回值
    char buf[1000];      // 原始数据缓冲区
    char time[100];      // 时间字段
    char Lat[100];       // 纬度字段
    char ns[100];        // 南北半球标识
    char Lng[100];       // 经度字段
    char ew[100];        // 东西半球标识
    float fLat, fLng;    // 转换后的经纬度

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

    /* 2. 打开串口 */
    fd = open_port(argv[1]);
    if (fd < 0) {
        printf("open %s err!\n", argv[1]);
        return -1;
    }

    /* 3. 配置串口(9600波特率,8N1)*/
    iRet = set_opt(fd, 9600, 8, 'N', 1);
    if (iRet) {
        printf("set port err!\n");
        close(fd);
        return -1;
    }

    /* 4. 主循环:读取并解析GPS数据 */
    while (1) {
        /* 读取一行NMEA数据 */
        iRet = read_gps_raw_data(fd, buf);
        
        /* 解析GPGGA数据 */
        if (iRet == 0) {
            iRet = parse_gps_raw_data(buf, time, Lat, ns, Lng, ew);
        }
        
        /* 打印解析结果 */
        if (iRet == 0) {
            printf("\n------ GPS Data ------\n");
            printf("Time : %s\n", time);
            printf("Lat  : %s %s\n", Lat, ns);
            printf("Lng  : %s %s\n", Lng, ew);

            /* 转换纬度格式:ddmm.mmmm → 十进制 */
            sscanf(Lat+2, "%f", &fLat);
            fLat = fLat / 60;
            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("Decimal Coordinates:\n");
            printf("Lng,Lat: %.06f,%.06f\n", fLng, fLat);
        }
    }

    close(fd);  // 理论上不会执行到这里
    return 0;
}

相关文章:

  • pytorch查询字典、列表维度
  • 应急响应排查系统技术解析
  • Ingress蓝绿发布
  • Redis 特性和应用场景
  • 测试模板1
  • python高级编程一(生成器与高级编程)
  • arcpy基础之环境搭建
  • 【QT】学习笔记1
  • leetcode刷题日记——赎金信
  • 【PyTorch项目实战】反卷积(Deconvolution)
  • 解决 weditor 报错AttributeError: ‘Device‘ object has no attribute ‘address’
  • Java对象内存结构详解
  • 文献总结:AAAI2025-UniV2X-End-to-end autonomous driving through V2X cooperation
  • linux RCU技术
  • Android11车载WiFi热点默认名称及密码配置
  • 蓝桥杯嵌入式考前模块总结
  • CMake中add_custom_command用法详解
  • Kubernetes安装与集群构建详细过程
  • 【STM32标准库】--新建工程
  • redis之缓存击穿
  • 做app的模板下载网站有哪些/如何优化企业网站
  • 企点网络科技骗局/李飞seo
  • 做网站公司东莞/百度账号登录个人中心
  • 软件开发模型螺旋模型/朝阳seo排名
  • 快速做网站企业/免费com域名注册永久
  • 企业网站手机端跳转设置/湖南正规seo公司