如何编写串口(uart)驱动-基于linux310

来源:互联网 发布:僵直脊柱炎周杰伦知乎 编辑:程序博客网 时间:2024/05/22 12:39

Linux uart驱动编写

本篇在介绍Linux下串口驱动程序编写,为了让让读者能够明白串口驱动如何编写、为什么这么编写,本文讲述了串口驱动编写的流程,此外还介绍了编写的串口驱动在内核态下是如何被使用的,此外还介绍了其和应用的交互等。
在介绍串口驱动的编写时,按照先介绍其在Linux操作系统下的架构,然后重点介绍了其在内核下的数据结构的组织结构,在介绍组织结构的同时也介绍了uart驱动是如何和内核的一些底层构建交互的,最后介绍了串口的读写的执行流程,从最后的执行流程中能够看到编写的串口驱动是如何被内核的构建调用的。
由于本文没有介绍字符设备驱动的概念,所以如果没有字符设备驱动的基础,建议先看LDD中关于字符设备驱动的相关章节。

uart驱动概览

首先来看一张图,该图显示了从应用程序到驱动底层的过程,在这张图即为Linux操作系统的uart整个架构,在该图的右侧红色方框内有三个数据结构,uart_driver是uart驱动的描述,串口驱动的核心实际上是tty,所以这里当注册该驱动时,该注册函数内部会构建一个tty结构体,来表示该串口驱动。uart_port对应的是一个端口的表示,这正如同pc端的CM0、CM1等等

uart_ops是该端口可用的操作集,串口驱动的主要工作就是集中在这里,该操作集将实际操作底层的硬件设备。



数据结构流

有了上面的概念之后,下面来看看内核中串口的数据结构是如何组织的。


这张图涵盖了3.10内核下uart涉及的相关数据结构,和相关的函数操作集,编写一个串口驱动,图中三个黄色的部分即为需要实现的部分,很有可能,串口驱动编写人员还会实现深黄色部分的,有了整体架构和整个数据结构流图,下面我们一步步讲述串口驱动的工作过程,非常感谢内核驱动的编写者,让串口编写的工作变得如此简单。
上图中虚线左边的黄色部分uart_driver代表一个串口驱动,会调用uart_register_driver对该串口进行注册,该字段需要的相关成员需要自己实现;
static struct uart_driver serial8250_reg = {
.owner = THIS_MODULE,
.driver_name = "serial",
.dev_name = "ttyS",
.major = TTY_MAJOR,
.minor = 64,
.cons = SERIAL8250_CONSOLE,
};
注册uart串口驱动,可以发现,后边会调用tty相关的一些东西,其实在内核中,串口实际上是一个tty设备,但是由于串口的通用性,内核工作着将串口的底层封装在了tty_driver里了,所以在串口注册函数uart_register_driver,通常我们能看见alloc_tty_driver函数申请struct tty_driver一个结构的变量,并用uart_driver的相关的成员对其初始化,这时uart_driver的结构体会变成tt_driver的结构体,接着调用tty_register_driver注册上图中的tty_driver结构体表示的tty驱动,这些驱动工作者已经为我们实现了,通常我们并不关心uart_drvier的注册过程。
在上图中的tty_driver的*op字段中,可以看到对(*open)成员,被赋值为uart_open,其它的函数指针被类似的赋值了,值得一说的是这里的uart_open抑或uart_ioctl也好,这些内核工作者为我们编写好了,它们最终会调用我们编写的端口驱动,这个过程在读写流程中有叙述。
一个uart驱动注册好了以后,下面就要将对应的port和其进行绑定了,绑定之前,需要初始化uart_port,设置其相关的成员,比如终端号,*ops等,这里的ops非常重要,在应用程序调用open、read等操作,到系统调用后,首先调用uart_ops,然后会调用这里的端口的ops,在Soc情况下,这里ops一般是对片上系统的串口控制器寄存器相关的设置,比如工作模式等等,这和数据手册和体系结构关系非常大,不同的芯片的地址范围和控制方法以及串口能力都有较大的差别。
来看一看port添加到驱动函数的原型,该函数有两个参数,其中一个是uart_driver,这个就是就是上面调用uart_register_driver注册的串口驱动,第二个参数就是这里的uart_port了,int uart_add_one_port(struct uart_driver *drv, struct uart_port *uport),这里我们来看看8250的添加方法。

serial8250_register_ports(struct uart_driver *drv, struct device *dev)
{
int i;
struct uart_8250_port *up = &serial8250_ports;
uart_add_one_port(drv, &up->port);
}


这里做了简化,可以看到,serial8250_ports结构体,该结构体对应上面图中的深黄色部分,一般Soc下也会建立该结构体,这样方便管理,但是该结构的内容获得的方法使用的是devicetree方法,有点区别。上图中红色1的线标识了这一过程。
uart_add_one_port函数我们这里走深一点看看,该函数调用uart_configure_port配置端口,注册的实际工作调用tty_port_register_device_attr()完成,该函数又调用tty_register_device_attr(),该函数将tty设备注册到系统中,如果有sysfs文件系统,则udev机制会创建设备节点。

串口的读写流程

open操作,读写操作前会调用open打开串口,open时
1、根据tty的driver_state字段找到uart_driver,
2、根据uart_driver以及tty的index字段,index表示的端口号,找到对应的uart_state,tty的index在tty_open时由tty_init_dev调用initialize_tty_struct设置
3、uart_state保存在tty和uart_port中
4、tty_driver的ports字段指向tty_port中
5、调用uart_start_up进一步完成uart的初始化,以使得串口可用
read操作:
1、对设备来说就是接收数据,使用中断方法,在start_up()函数中,即open一个端口时会注册接收数据的中断服务函数,这样可以最大限度的节约中断资源。注册中断的函数类似于下面:
2、reques_irq(port->irq,serial_8250_irq,...),当Soc设备的串口有数据来时,产生中断,中断服务程序判断是接收数据中断标志,那么调用uart_insert_char将数据放入tty的缓存中
3、调用tty_insert_flip_char/tty_flip_buffer_push将数据刷入到line discipline中,后tty读取line discipline返回给用户
write操作:
1、uart_write将数据放入circ_buf中,函数调用的过程write=>uart_write()(tty_driver)=>port->ops->start_tx();
2、uart_start,即是用户编写的发送驱动程序。
其它操作(设置波特率等):
调用iotcl方法完成,修改 **termios;字段完成。

串口驱动编写流程:
1、编写一个uart_driver结构体,并调用uart_register_driver注册该驱动到tty核心。
2、编写My_uart_port和uart_ops
3、编写 platform_driver,然后解析设备树,获得串口的相关信息,关于devicetree可以参看《Linux启动那些事-基于310》然后调用platform_device_register注册该Soc下的平台设备,在platform_driver中的probe调用前述的 uart_add_one_port完成端口的添加工作,正确返回后串口驱动可以正常工作了。
4、可以参考drivers/tty/serial目录下已有的一些驱动。
0 0
原创粉丝点击