前言
串口设备(serial or uart,后面不再区分)是TTY设备的一种,Linux kernel为了方便串口驱动的开发,在TTY framework的基础上,封装了一层串口框架(serial framework)。该框架尽可能的屏蔽了TTY有关的技术细节(比较难懂),驱动工程师在编写串口驱动的时候,只需要把精力放在串口以及串口控制器本身即可。
本文将通过对serial framework的简单分析,理解上面的概念,并掌握基于该框架编写串口驱动的方法和步骤。
软件架构
Linux kernel serial framework位于“drivers/tty/serial”目录中,其软件架构(如下面图片1所示)比较简单:
Serial core是Serial framework的核心实现,对上封装、屏蔽TTY的技术细节,对下为具体的串口驱动提供简单、统一的编程API。
earlycon(early console)是serial framework中比较新的一个功能,它基于Kernel system console的框架,提供了一种比较简单的控制台实现方式。最后就是具体的串口驱动(Serial drivers)了,不再详细介绍。
serial core
功能介绍
serial core主要实现如下三类功能(任何一个framework的core模块都提供类似的功能,这就是套路!~):
1)将串口设备有关的物理对象(及其操作方法)封装成一个一个的数据结构,以达到用软件语言描述硬件的目的。
2)向底层driver提供串口驱动的编程接口。
3)基于TTY framework所提供的TTY driver的编写规则,将底层driver看到的serial driver,转换为TTY driver,并将所有的serial操作,转换为对应的tty操作。
本文将重点介绍1)和2)两类功能,第3)类,属于TTY core的内部逻辑,由于比较简单就不多说了。
关键数据结构
数据结构是一个软件模块的灵魂和骨架,而对设备驱动来说,数据结构一般和具体的硬件实体对应,例如:
假如一个soc中有5个串口控制器(也可称作uart控制器,后面我们不再区分),每个uart控制器都可引出一个串口(uart port)。那么:
每个uart控制器,都是一个platform device,它们可由同一个patform driver驱动;
相对于uart控制器实实在在的存在,我们更为熟悉的串口(uart port),可以看作虚拟的设备,serial core将它们抽象为“struct uart_port”,并在platform driver的probe接口中,注册到kernel;
和platform device类似,这些虚拟的串口设备,也可由同一个虚拟的driver驱动,这就是serial core中的“struct uart_driver”。
下面我们将跟随上面的思路,介绍serial core提供的数据结构(具体可参考“include/linux/serial_core.h”)。
struct uart_port
在serial framework中,struct uart_port抽象虚拟的串口设备(具体的串口控制器,则为实实在在的硬件设备),这是一个庞大的数据结构,存放了五花八门的、各式各样的、有新有旧的、有用没有的和串口设备有关的信息,例如(不能全部罗列,大家在写driver的时候,可以有事没事去看看,说不定就有惊喜):
1)最基本、最必须的,需要驱动工程师根据实际的硬件自行填充的字段
dev,父设备的指针,通常是串口控制器所对应的platform device;
type,该串口的类型,是以PORT_为前缀的一个宏定义,可以根据需要在include/uapi/linux/serial_core.h中定义;
ops,该串口的操作函数集(struct uart_ops类型的指针),具体可参考3.2.3;
iotype,该串口的I/O类型,例如UPIO_MEM32(常用的通过寄存器访问的uart控制器);
mapbase,对应MEM类型的串口,保存它的寄存器基址(物理地址),一般是从DTS中解析得到的
membase,从mapbase ioremap得来(虚拟地址);
irq、irqflags,该串口对应的中断号(以及相应的终端flags), 一般是从DTS中解析得到的;
line,该串口的编号,和字符设备的次设备号等有关。
和运行时状态有关的字段
lock,一个自旋锁(spinlock_t类型),用于对该数据结构进行访问保护;
icount,一个struct uart_icount类型变量,用于保存该串口的统计信息,例如收发数据的统计等;
cons,console指针,如果该串口被注册为system console的话,将对应的console指针保存在这里;
state,struct uart_state指针,具体请参考3.2.2。
一些有用的函数指针(driver实现,serial core调用),例如
serial_in,读取该串口的某个寄存器;
serial_out,向该串口的某个寄存器写入某一value;
最后,serial core根据这两个函数指针,封装出两个公共的寄存器访问接口:serial_port_in和serial_port_out,以方便driver使用。
注1:struct uart_port中的内容非常多,因此在编写串口驱动的时候,要把握一个原则:不到万不得已的时候,不要新定义变量,多去struct uart_port找找,很有可能就找到了你想要的东西。
struct uart_state
struct uart_state中保存了串口使用过程中的动态信息(它们的生命周期是串口被打开到被关闭的过程),包括:
port,对应的struct tty_port变量(uart port是tty port的一个特例);
pm_state,电源管理有关的状态,例如UART_PM_STATE_ON、UART_PM_STATE_OFF和UART_PM_STATE_UNDEFINED三种;
xmit,用于保存TX数据的环形缓冲区(struct circ_buf,具体可参考include/linux/circ_buf.h);
uart_port,struct uart_port类型的指针,指向所属的串口。
struct uart_ops
使用struct uart_port抽象串口的同时,serial core将串口有关的操作函数封装在struct uart_ops中,底层驱动根据实际硬件情况,填充这些函数,serial core在合适的时候,帮忙调用。因为在历史上,串口设备是一种非常复杂的设备,因此,和struct uart_port类似,struct uart_ops结构也非常庞大,包罗万象,这里简单介绍一些常用的(其它在用到的时候再关注,或者可参考kernel的帮助文档----Documentation/serial/driver):
startup,打开串口设备的时候,serial core会调用该接口,driver可以在这里进行串口的初始化操作,例如申请中断资源、使能clock、使能接收,等等;
shutdown,startup的反操作,在串口设备被关闭的时候调用;
start_tx,每当有一笔新的数据需要通过串口发送出去的时候,serial core会先把数据保存在TX的buffer中(参考3.2.2的介绍),然后调用start_tx通知driver。driver需要在该接口中,根据当前的状态(TX是否正在进行),决定是否需要发起一次传输;
stop_tx,停止正在进行中的TX;
stop_rx,停止RX;
tx_empty,判断硬件的TX FIFO是否为空,如果是,则返回TIOCSER_TEMT,否则返回0;
.set_mctrl,设置modem的control line,可以留空;
.set_termios,设置串口的termios(例如波特率、数据位、停止位等)。
struct uart_driver
上面介绍的几个数据结构,都在竭力描述串口设备,相应的,按照设备模型的惯例,需要一个和设备对应的、抽象driver数据结构,就是struct uart_driver:
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;
};
该数据结构非常简单,一般情况下,只要关注如下的字段:
driver_name,driver的名称;
dev_name,对应的设备名,例如“ttyS”;
nr,该驱动可以支持的串口的数量(serial core会根据这个值为每一个串口分配一些内部使用资源);
major、minor,主、次设备号,可以不指定(这样的话,TTY core会帮忙动态分配);
cons,如果需要将某一个串口当作system console,可以在driver中定义struct console变量,并将它的指针保存在这里,serial core会在注册串口的时候帮忙将console注册到系统中。
向具体driver提供的用于编写串口驱动的API
数据结构抽象完毕后,serial core向下层的driver提供了方便的编程API,主要包括:
1)uart driver有关的API
int uart_register_driver(struct uart_driver *uart);
void uart_unregister_driver(struct uart_driver *uart);
uart_register_driver,将定义并填充好的uart driver注册到kernel中,一般在驱动模块的init接口中被调用。
uart_unregister_driver,注销uart driver,在驱动模块的exit接口中被调用。
2)uart port有关的API
int uart_add_one_port(struct uart_driver *reg, struct uart_port *port);
int uart_remove_one_port(struct uart_driver *reg, struct uart_port *port);
int uart_match_port(struct uart_port *port1, struct uart_port *port2);
int uart_suspend_port(struct uart_driver *reg, struct uart_port *port);
int uart_resume_port(struct uart_driver *reg, struct uart_port *port);
static inline int uart_tx_stopped(struct uart_port *port)
extern void uart_insert_char(struct uart_port *port, unsigned int status,
unsigned int overrun, unsigned int ch, unsigned int flag);
uart_add_one_port、uart_remove_one_port,添加/删除一个uart port,一般在platform driver的probe/remove中被调用。
uart_suspend_port、uart_resume_port,suspend/resume uart port,在电源管理状态切换的时候被调用。
uart_tx_stopped,判断某一个uart port的tx是否处于停止状态。
uart_insert_char,驱动从串口接收到一个字符之后,可以调用该接口把该字符放到RX buffer中(相比tty_insert_flip_char,可以进行一些overrun的处理)。
3)system console有关的API
struct tty_driver *uart_console_device(struct console *co, int *index);
void uart_console_write(struct uart_port *port, const char *s,
unsigned int count,
void (*putchar)(struct uart_port *, int));
uart_console_device,通过console指针获取tty driver指针的帮助函数,通常情况下会把该函数赋值给串口console的.device指针,例如:
static struct console xxx_console = {
…
.device = uart_console_device,
…
};
uart_console_write,向串口发送一个字符串的辅助接口,通常在console的.write接口中被调用。
4)其它辅助类的API
#define uart_circ_empty(circ) ((circ)->head == (circ)->tail)
#define uart_circ_clear(circ) ((circ)->head = (circ)->tail = 0)
#define uart_circ_chars_pending(circ) \
(CIRC_CNT((circ)->head, (circ)->tail, UART_XMIT_SIZE))
#define uart_circ_chars_free(circ) \
(CIRC_SPACE((circ)->head, (circ)->tail, UART_XMIT_SIZE))
为了方便driver操作环形缓冲区,serial core定义了一些状态判断的宏,例如是否为空(uart_circ_empty)、是否为初始状态(uart_circ_clear)、缓冲区中有效数据的个数(uart_circ_chars_pending)、缓冲区中空闲空间的个数(uart_circ_chars_free)、等等。
earlycon
early console是linux serial framework提供的一个可在kernel启动早期使用的console,最早可以在“start_kernel-->setup_arch-->parse_early_param”之后就可使用,对于kernel早期的debug很有帮助。
根据复杂程度,serial framework提供了两种API,供底层的driver使用。