目录
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应用编程的套路就是:
- open;
- 设置行规程,比如波特率、数据位、停止位、检验位、RAW 模式(ioctl);
- 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: // 默认9600cfsetispeed(&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;
}