一、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;
}