Linux驱动开发——串口设备驱动

一、串口简介

串口全称叫做串行接口,通常也叫做 COM 接口,串行接口指的是数据一个一个的顺序传输,通信线路简单。使用两条线即可实现双向通信,一条用于发送,一条用于接收。串口通信距离远,但是速度相对会低,串口是一种很常用的工业接口。


I.MX6U 自带的 UART 外设就是串口的一种,UART 全称是 Universal Asynchronous Receiver/Trasmitter,也就是异步串行收发器。UART 作为串口的一种,其工作原理也是将数据一位一位的进行传输,

发送和接收各用一条线,因此通过 UART 接口与外界相连最少只需要三条线:TXD(发送)、RXD(接收)和 GND(地线)

image.png


空闲位:数据线在空闲状态的时候为逻辑“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 结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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,此函数原型如下:

1
int uart_register_driver(struct uart_driver *drv)


函数参数和返回值含义如下:


drv :要注册的 uart_driver。

返回值:0,成功;负值,失败。

2.注销驱动的时候也需要注销掉前面注册的 uart_driver,需要用到 uart_unregister_driver 函数,函数原型如下:

1
void uart_unregister_driver(struct uart_driver *drv)

函数参数和返回值含义如下:


drv :要注销的 uart_driver。

返回值:无。

uart_port 的添加与移除

uart_port 表示一个具体的 port,uart_port 定义在 include/linux/serial_core.h 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 函数函数原型如下:

1
2
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 函数,函数原型如下:

1
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()函数将终端设置为原始模式

多线程例程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
#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