Linux驱动开发——串口设备驱动
一、串口简介
串口全称叫做串行接口,通常也叫做 COM 接口,串行接口指的是数据一个一个的顺序传输,通信线路简单。使用两条线即可实现双向通信,一条用于发送,一条用于接收。串口通信距离远,但是速度相对会低,串口是一种很常用的工业接口。
I.MX6U 自带的 UART 外设就是串口的一种,UART 全称是 Universal Asynchronous Receiver/Trasmitter,也就是异步串行收发器。UART 作为串口的一种,其工作原理也是将数据一位一位的进行传输,
发送和接收各用一条线,因此通过 UART 接口与外界相连最少只需要三条线:TXD(发送)、RXD(接收)和 GND(地线)
空闲位:数据线在空闲状态的时候为逻辑“1”状态,也就是高电平,表示没有数据线空闲,没有数据传输。
起始位:当要传输数据的时候先传输一个逻辑“0”,也就是将数据线拉低,表示开始数据传输。
数据位:数据位就是实际要传输的数据,数据位数可选择 5~8 位,我们一般都是按照字节传输数据的,一个字节 8 位,因此数据位通常是 8 位的。低位在前,先传输,高位最后传输。
奇偶校验位:这是对数据中“1”的位数进行奇偶校验用的,可以不使用奇偶校验功能。
停止位:数据传输完成标志位,停止位的位数可以选择 1 位、1.5 位或 2 位高电平,一般都选择 1 位停止位。
波特率:波特率就是 UART 数据传输的速率,也就是每秒传输的数据位数,一般选择 9600、19200、115200 等。
二、Linux下串口驱动框架
Linux 提供了串口驱动框架,我们只需要按照相应的串口框架编写驱动程序即可。串口驱动没有什么主机端和设备端之分,就只有一个串口驱动,而且这个驱动也已经由 NXP 官方已经编写好了,
我们真正要做的就是在设备树中添加所要使用的串口节点信息。当系统启动以后串口驱动和设备匹配成功,相应的串口就会被驱动起来,生成/dev/ttymxcX(X=0….n)文件。
uart_driver 结构体
uart_driver 结构体表示 UART 驱动,uart_driver 定义在 include/linux/serial_core.h 文件中 struct uart_driver { struct module *owner; /* 模块所属者 */ const char *driver_name; /* 驱动名字 */ const char *dev_name; /* 设备名字 */ int major; /* 主设备号 */ int minor; /* 次设备号 */ int nr; /* 设备数 */ struct console *cons; /* 控制台 */ /* * these are private; the low level driver should not * touch these; they should be initialised to NULL */ struct uart_state *state; struct tty_driver *tty_driver; };
1. 加载驱动的时候通过 uart_register_driver 函数向系统注册这个 uart_driver,此函数原型如下:
int uart_register_driver(struct uart_driver *drv)
函数参数和返回值含义如下:
drv :要注册的 uart_driver。
返回值:0,成功;负值,失败。
2.注销驱动的时候也需要注销掉前面注册的 uart_driver,需要用到 uart_unregister_driver 函数,函数原型如下:
void uart_unregister_driver(struct uart_driver *drv)
函数参数和返回值含义如下:
drv :要注销的 uart_driver。
返回值:无。
uart_port 的添加与移除
uart_port 表示一个具体的 port,uart_port 定义在 include/linux/serial_core.h 文件
117 struct uart_port { 118 spinlock_t lock; /* port lock */ 119 unsigned long iobase; /* in/out[bwl] */ 120 unsigned char __iomem *membase; /* read/write[bwl] */ ...... 235 const struct uart_ops *ops; 236 unsigned int custom_divisor; 237 unsigned int line; /* port index */ 238 unsigned int minor; 239 resource_size_t mapbase; /* for ioremap */ 240 resource_size_t mapsize; 241 struct device *dev; /* parent device */ ...... 250 };
uart_port 中最主要的就是第 235 行的 ops,ops 包含了串口的具体驱动函数,UART 驱动编写人员需要实现 uart_ops,因为 uart_ops 是最底层的 UART 驱动接口,是实实在在的和 UART 寄存器打交道的。
1. 那么 uart_port 是怎么和 uart_driver 结合起来,用到 uart_add_one_port 函数函数原型如下:
int uart_add_one_port(struct uart_driver *drv, struct uart_port *uport)
函数参数和返回值含义如下:
drv:此 port 对应的 uart_driver。
uport :要添加到 uart_driver 中的 port。
返回值:0,成功;负值,失败。
2.卸载 UART 驱动的时候也需要将 uart_port 从相应的 uart_driver 中移除,需要用到uart_remove_one_port 函数,函数原型如下:
int uart_remove_one_port(struct uart_driver *drv, struct uart_port *uport)
函数参数和返回值含义如下:
drv:要卸载的 port 所对应的 uart_driver。
uport :要卸载的 uart_port。
返回值:0,成功;负值,失败。
三、Linux下串口驱动工作流程
UART 本质上是一个 platform 驱动
platform 驱动框架结构体 serial_imx_driver
在驱动入口函数中调用uart_register_driver 函数向 Linux 内核注册 uart_driver
在驱动出口函数中调用uart_unregister_driver 函数注销掉前面注册的 uart_driver
UART 设备和驱动匹配成功以后 serial_imx_probe 函数就会执行,此函数的重点工作就是初始化 uart_port,然后将其添加到对应的 uart_driver 中
在初始化uart_port过程中,设置 uart_ops 为 imx_pops。imx_pops 就是 I.MX6ULL 最底层的驱动函数集合。
四、Linux下串口应用开发
串口的应用编程就是通过 ioctl()对串口进行配置,调用 read()读取串口的数据、调用 write()向串口写入数据。
Linux 为上层用户做了一层封装,将这些 ioctl()操作封装成了一套标准的 API,这些 API 其实是 C 库函数。
这一套接口并不是针对串口开发的,而是针对所有的终端设备,串口是一种终端设备,计算机系统本地连接的鼠标、键盘也是终端设备,通过 ssh 远程登录连接的伪终端也是终端设备
使用 termios API,需要在我们的应用程序中包含 termios.h 头文件
终端工作模式
规范模式
基于行进行处理的。在用户输入一个行结束符(回车符、EOF 等)之前,系统调用 read()函数是读不到用户输入的任何字符的
非规范模式
所有的输入是即时有效的,不需要用户另外输入行结束符,而且不可进行行编辑
原始模式
是一种特殊的非规范模式。在原始模式下,所有的输入数据以字节为单位被处理。在这个模式下,终端是不可回显的,并且禁用终端输入和输出字符的所有特殊处理。调用 cfmakeraw()函数将终端设置为原始模式
多线程例程
#define _GNU_SOURCE //在源文件开头定义_GNU_SOURCE宏 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include <errno.h> #include <string.h> #include <signal.h> #include <termios.h> #include <pthread.h> typedef struct uart_hardware_cfg { unsigned int baudrate; /* 波特率 */ unsigned char dbit; /* 数据位 */ char parity; /* 奇偶校验 */ unsigned char sbit; /* 停止位 */ } uart_cfg_t; static struct termios old_cfg; //用于保存终端的配置参数 static int fd; //串口终端对应的文件描述符 /** ** 串口初始化操作 ** 参数device表示串口终端的设备节点 **/ static int uart_init(const char *device) { /* 打开串口终端 */ fd = open(device, O_RDWR | O_NOCTTY); if (0 > fd) { fprintf(stderr, "open error: %s: %s\n", device, strerror(errno)); return -1; } /* 获取串口当前的配置参数 */ if (0 > tcgetattr(fd, &old_cfg)) { fprintf(stderr, "tcgetattr error: %s\n", strerror(errno)); close(fd); return -1; } return 0; } /** ** 串口配置 ** 参数cfg指向一个uart_cfg_t结构体对象 **/ static int uart_cfg(const uart_cfg_t *cfg) { struct termios new_cfg = {0}; //将new_cfg对象清零 speed_t speed; /* 设置为原始模式 */ cfmakeraw(&new_cfg); /* 使能接收 */ new_cfg.c_cflag |= CREAD; /* 设置波特率 */ switch (cfg->baudrate) { case 1200: speed = B1200; break; case 1800: speed = B1800; break; case 2400: speed = B2400; break; case 4800: speed = B4800; break; 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; case 230400: speed = B230400; break; case 460800: speed = B460800; break; case 500000: speed = B500000; break; default: //默认配置为115200 speed = B115200; printf("default baud rate: 115200\n"); break; } if (0 > cfsetspeed(&new_cfg, speed)) { fprintf(stderr, "cfsetspeed error: %s\n", strerror(errno)); return -1; } /* 设置数据位大小 */ new_cfg.c_cflag &= ~CSIZE; //将数据位相关的比特位清零 switch (cfg->dbit) { case 5: new_cfg.c_cflag |= CS5; break; case 6: new_cfg.c_cflag |= CS6; break; case 7: new_cfg.c_cflag |= CS7; break; case 8: new_cfg.c_cflag |= CS8; break; default: //默认数据位大小为8 new_cfg.c_cflag |= CS8; printf("default data bit size: 8\n"); break; } /* 设置奇偶校验 */ switch (cfg->parity) { case 'N': //无校验 new_cfg.c_cflag &= ~PARENB; new_cfg.c_iflag &= ~INPCK; break; case 'O': //奇校验 new_cfg.c_cflag |= (PARODD | PARENB); new_cfg.c_iflag |= INPCK; break; case 'E': //偶校验 new_cfg.c_cflag |= PARENB; new_cfg.c_cflag &= ~PARODD; /* 清除PARODD标志,配置为偶校验 */ new_cfg.c_iflag |= INPCK; break; default: //默认配置为无校验 new_cfg.c_cflag &= ~PARENB; new_cfg.c_iflag &= ~INPCK; printf("default parity: N\n"); break; } /* 设置停止位 */ switch (cfg->sbit) { case 1: //1个停止位 new_cfg.c_cflag &= ~CSTOPB; break; case 2: //2个停止位 new_cfg.c_cflag |= CSTOPB; break; default: //默认配置为1个停止位 new_cfg.c_cflag &= ~CSTOPB; printf("default stop bit size: 1\n"); break; } /* 将MIN和TIME设置为0 */ new_cfg.c_cc[VTIME] = 0; new_cfg.c_cc[VMIN] = 0; /* 清空缓冲区 */ if (0 > tcflush(fd, TCIOFLUSH)) { fprintf(stderr, "tcflush error: %s\n", strerror(errno)); return -1; } /* 写入配置、使配置生效 */ if (0 > tcsetattr(fd, TCSANOW, &new_cfg)) { fprintf(stderr, "tcsetattr error: %s\n", strerror(errno)); return -1; } /* 配置OK 退出 */ return 0; } /*** --dev=/dev/ttymxc2 --brate=115200 --dbit=8 --parity=N --sbit=1 --type=read ***/ /** ** 打印帮助信息 **/ static void show_help(const char *app) { printf("Usage: %s [选项]\n" "\n必选选项:\n" " --dev=DEVICE 指定串口终端设备名称, 譬如--dev=/dev/ttymxc2\n" "\n可选选项:\n" " --brate=SPEED 指定串口波特率, 譬如--brate=115200\n" " --dbit=SIZE 指定串口数据位个数, 譬如--dbit=8(可取值为: 5/6/7/8)\n" " --parity=PARITY 指定串口奇偶校验方式, 譬如--parity=N(N表示无校验、O表示奇校验、E表示偶校验)\n" " --sbit=SIZE 指定串口停止位个数, 譬如--sbit=1(可取值为: 1/2)\n" " --help 查看本程序使用帮助信息\n\n", app); } /** ** 信号处理函数,当串口有数据可读时,会跳转到该函数执行 **/ static void io_handler(int sig, siginfo_t *info, void *context) { unsigned char buf[10] = {0}; int ret; int n; if(SIGRTMIN != sig) return; /* 判断串口是否有数据可读 */ if (POLL_IN == info->si_code) { ret = read(fd, buf, 8); //一次最多读8个字节数据 printf("[ "); for (n = 0; n < ret; n++) printf("0x%hhx ", buf[n]); printf("]\n"); } } /** ** 异步I/O初始化函数 **/ static void async_io_init(void) { struct sigaction sigatn; int flag; /* 使能异步I/O */ flag = fcntl(fd, F_GETFL); //使能串口的异步I/O功能 flag |= O_ASYNC; fcntl(fd, F_SETFL, flag); /* 设置异步I/O的所有者 */ fcntl(fd, F_SETOWN, getpid()); /* 指定实时信号SIGRTMIN作为异步I/O通知信号 */ fcntl(fd, F_SETSIG, SIGRTMIN); /* 为实时信号SIGRTMIN注册信号处理函数 */ sigatn.sa_sigaction = io_handler; //当串口有数据可读时,会跳转到io_handler函数 sigatn.sa_flags = SA_SIGINFO; sigemptyset(&sigatn.sa_mask); sigaction(SIGRTMIN, &sigatn, NULL); } static void *read_thread(void *arg) { printf("read_thread , process id=%d , thread id= %lu\n",getpid(),pthread_self()); async_io_init(); //我们使用异步I/O方式读取串口的数据,调用该函数去初始化串口的异步I/O while(1) sleep(1); //进入休眠、等待有数据可读,有数据可读之后就会跳转到io_handler()函数 return (void *)0; } static void *write_thread(void *arg) { unsigned char w_buf[8] = {0}; printf("write_thread , process id=%d , thread id= %lu\n",getpid(),pthread_self()); while(1) { scanf("%s",w_buf); write(fd, w_buf, 8); //一次向串口写入8个字节 memset(w_buf, 0, sizeof w_buf); } return (void *)0; } int main(int argc, char *argv[]) { uart_cfg_t cfg = {0}; char *device = NULL; int rw_flag = -1; int n; /* 解析出参数 */ for (n = 1; n < argc; n++) { if (!strncmp("--dev=", argv[n], 6)) device = &argv[n][6]; else if (!strncmp("--brate=", argv[n], 8)) cfg.baudrate = atoi(&argv[n][8]); else if (!strncmp("--dbit=", argv[n], 7)) cfg.dbit = atoi(&argv[n][7]); else if (!strncmp("--parity=", argv[n], 9)) cfg.parity = argv[n][9]; else if (!strncmp("--sbit=", argv[n], 7)) cfg.sbit = atoi(&argv[n][7]); else if (!strcmp("--help", argv[n])) { show_help(argv[0]); //打印帮助信息 exit(EXIT_SUCCESS); } } /* 串口初始化 */ if (uart_init(device)) exit(EXIT_FAILURE); /* 串口配置 */ if (uart_cfg(&cfg)) { tcsetattr(fd, TCSANOW, &old_cfg); //恢复到之前的配置 close(fd); exit(EXIT_FAILURE); } pthread_t tid1,tid2; int ret = pthread_create(&tid1,NULL,read_thread,NULL); if(ret != 0) { fprintf(stderr,"error:%s\n",strerror(ret)); exit(-1); } ret = pthread_create(&tid2,NULL,write_thread,NULL); if(ret != 0) { fprintf(stderr,"error:%s\n",strerror(ret)); exit(-1); } while(1); /* 退出 */ tcsetattr(fd, TCSANOW, &old_cfg); //恢复到之前的配置 close(fd); exit(EXIT_SUCCESS); }
————————————————
版权声明:本文为CSDN博主「拉依达不拉胯」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_44814825/article/details/129661547