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

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

————————————————————————————————
现开通针对在校生嵌入式学习咨询服务,学习路线可见下文:
拉依达的嵌入式学习和秋招经验-CSDN博客
咨询详情请私信,或者加vx:songwei4615,加vx请备注CSDN咨询
————————————————————————————————

一、串口简介

串口全称叫做串行接口,通常也叫做 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,成功;负值,失败。
  1. 注销驱动的时候也需要注销掉前面注册的 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,成功;负值,失败。
  1. 卸载 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下串口驱动工作流程

  1. UART 本质上是一个 platform 驱动
  2. platform 驱动框架结构体 serial_imx_driver
  3. 驱动入口函数中调用uart_register_driver 函数向 Linux 内核注册 uart_driver
  4. 驱动出口函数中调用uart_unregister_driver 函数注销掉前面注册的 uart_driver
  5. UART 设备和驱动匹配成功以后 serial_imx_probe 函数就会执行,此函数的重点工作就是初始化 uart_port,然后将其添加到对应的 uart_driver 中
  6. 在初始化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);
}

  • 6
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
### 回答1: Linux字符设备驱动开发是指在Linux系统中编写驱动程序,使得用户可以通过字符设备接口来访问硬件设备。这种驱动程序通常用于控制串口、并口、USB设备等。开发Linux字符设备驱动需要掌握Linux内核的基本知识,包括进程管理、内存管理、中断处理、设备驱动等方面。此外,还需要了解字符设备驱动的编写流程、驱动程序的结构和接口等。开发Linux字符设备驱动需要使用C语言和Linux内核编程接口。 ### 回答2: Linux字符设备驱动开发Linux系统中的一部分,它允许开发人员在Linux系统上使用字符设备,这些字符设备可以包括串口、USB口、网卡等。Linux字符设备驱动开发可帮助开发人员实现各种各样的设备驱动,从而增强Linux系统的功能。 在Linux字符设备驱动开发过程中,需要注意以下几点: 1. 实现设备驱动的一个基本框架,包括注册设备设备的初始化,以及对设备进行读写操作等。 2. 开发人员不仅需要熟悉驱动程序开发技术,还需要了解Linux内核系统的相关知识,例如进程、中断、内存管理等。 3. 应该在代码注释中提供详细的文档,以方便其他开发人员进行维护和修改。 4. 在实现字符设备驱动过程中,必须保证安全性和可靠性,防止设备出现故障或者损坏用户的数据。 5. 在测试和维护设备驱动时,需要使用一些常见的工具和技术,例如devfs、udev等。 总之,Linux字符设备驱动开发是一个需要熟练技能和丰富经验的过程。开发人员需要有足够的专业知识和经验来确保设备驱动的高效和稳定性。通过精心设计和开发Linux字符设备驱动可以提供高性能、高可靠性、易于使用的设备驱动,从而大大增强了Linux系统的功能和灵活性。 ### 回答3: Linux字符设备驱动开发Linux系统中的一个重要领域。其主要任务是开发一些支持字符设备驱动程序,从而使用户能够在Linux系统中使用各种不同类型的字符设备,例如串口、打印机、读卡器和磁盘等。同时,这些驱动程序还要保证设备完全可靠和高效地工作,确保系统的安全性和性能。 Linux字符设备驱动开发需要掌握以下基本知识: 1.了解Linux系统体系结构和内核架构 Linux系统由内核和用户空间组成,内核作为系统的核心组件,是实现系统功能的主要部分,因此了解内核体系结构和架构是开发Linux字符设备驱动所必须掌握的知识。 2.熟悉字符设备的相关知识 字符设备Linux系统中的一种重要的设备类型,它与其他类型设备不同之处在于它只能逐个字符地进行读写操作。因此需要深入了解字符设备的相关知识,例如驱动的主要功能、驱动程序与设备的交互方式、设备控制结构等。 3.熟练掌握C语言及Linux内核编程技术 编写Linux字符设备驱动程序需要掌握良好的C语言编程知识以及熟练的Linux内核编程技术,包括内存管理、进程管理、文件系统、中断处理等。同时,还需要了解Linux内核代码的结构和代码的编写规范,以便于编写出符合内核标准的驱动程序。 4.掌握Linux驱动框架的使用方法 为了简化Linux驱动开发流程,Linux提供了一些驱动框架,这些框架定义了一些驱动程序中常用的接口和函数,能够方便驱动程序的开发和调试。因此,Linux字符设备驱动开发者需要掌握其中的一些驱动框架,如字符驱动框架。 5.熟悉Linux字符设备驱动开发过程 Linux字符设备驱动开发过程主要包括驱动程序的初始化、驱动程序的主要功能实现、驱动程序的卸载等环节。在开发过程中,需要合理使用系统提供的工具和调试手段,如gdb、strace、make等,以便于分析和排查驱动程序出现的问题,确保驱动程序的稳定和可靠性。 总之,在Linux字符设备驱动开发过程中,开发者需要掌握相关的知识和技能,以实现对字符设备的编程和调试,开发出满足用户需求的高质量驱动程序。同时,Linux字符设备驱动开发也是一项长期持续的工作,开发者需要时刻关注最新的技术发展和硬件设备变化,才能更好地适应市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

拉依达的嵌入式小屋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值