目录
1 TTY的概念和历史
2 终端的概念
3 TTY整体框架
3.1 TTY子系统中设备节点的差别
4 UART驱动程序整体框架和注册过程
4.1 uart_register_driver(&imx_reg)函数流程
4.2 platform_driver_register(&serial_imx_driver)函数
4.3 uart驱动注册整体架构图
5 怎么编写串口驱动程序
6 串口的读和写过程是怎么一层层传递数据的
6.1 数据发送过程
6.2 数据接收过程
7 怎么调试UART驱动程序
7.1 通过得到UART硬件上收发的数据来调试UART驱动
7.2 通过proc文件调试UART驱动
7.2.1 /proc/interrupts
7.2.2 /proc/tty/drivers
7.2.3 /proc/tty/driver
7.2.4 /proc/tty/ldiscs
7.3 通过sys文件调试UART驱动程序
8 printk执行过程
8.1 printk的使用
8.1.1 printk使用示例
8.1.2 printk函数的记录级别
8.1.3 在用户空间修改printk函数的记录级别
8.1.4 printk函数记录级别的名称和使用
8.2 printk执行过程
8.2.1 函数调用过程
8.2.2 内核打印信息保存在哪里
8.2.3 printk信息从哪些设备打印出来
9 console驱动注册过程
9.1 console结构体
9.2 console驱动注册过程
9.2.1 处理命令行参数
9.2.2 register_console
9.2.3 /dev/console
10 early_printk和earlycon
10.1 内核信息的早期打印
10.2 early_printk
10.3 earlycon
10.3.1 提供硬件信息的两种方法
10.3.2 设置write函数
10.3.3 register_console
11 费曼学习法:我录制了一个UART驱动框架讲解视频
12 UART应用程序举例gps_read.c
13 参考文献:
1 TTY的概念和历史
如上图所示,其实糖糖也最开始是一个公司的名字,然后呢,这家公司之前生产的teleprinter很有名,这个所谓的远程打字机,其实就是两台打字机连接起来,然后可以相互传输电报,后来,电脑出现了,开始把其中一个打字机去掉换成电脑,然后打字机用串口连接到电脑,再往后呢,电脑还可以连接其他的一些终端设备,由于这一段历史,连接到电脑的一些其他设备也叫TTY设备,那这些设备对应的驱动也就跟TTY扯上了关系,大体了解下这段历史就可以了。
2 终端的概念
对于电脑来说,终端其实简单来说就是扮演一个人机接口的角色,能给计算机提供输入输出功能,所以像键盘和显示器属于终端,然后用串口远程连接电脑,这个中断模拟程序也可以称为终端,
然后呢,我们在电脑上打开的虚拟桌面也可以称为终端。
3 TTY整体框架
经过前面对tty的历史以及终端概念的大体介绍,现在可以引出来tty的一个大体驱动框架了,上图是TTY的一个整体框架,对于我们一个uart驱动来说,uart驱动可以由三部分组成,
- TTY层:在Linux或UNIX系统中,TTY子系统负责管理所有的终端设备,包括物理设备(如键盘和显示器),串行设备(如串口),以及虚拟设备(如SSH终端和伪终端)。TTY子系统通过TTY驱动程序在内核级别实现进程管理、行编辑和会话管理。因此,无论是物理设备、串行设备还是虚拟设备,都由TTY子系统进行管理。TTY驱动程序主要负责上层的逻辑处理,包括数据缓存、字符设备的创建和管理,以及用户空间和内核空间之间的数据交互等。
- line discipline层:大多数用户都会在输入时犯错,所以退格键会很有用。这当然可以由应用程序本身来实现,但是根据UNIX设计“哲学”,应用程序应尽可能保持简单。为了方便起见,操作系统提供了一个编辑缓冲区和一些基本的编辑命令(退格,清除单个单词,清除行,重新打印),这些命令在行规范(line discipline)内默认启用,行规程规定了键盘,串口,打印机,显示器等输入输出设备和用户态Shell等程序之间的行为规范,键盘上的按键事件被行规程解释成了Shell可以理解的输入并给出相应的输出。人们要想操作计算机,这套规程是必不可少的,它事实上规定了信息从外部进入计算机的规范。
- uart驱动层:UART驱动程序主要负责底层的硬件操作,包括数据的发送和接收,以及中断的处理等。
上图中标注了在linux内核中,tty层,line discipline层,以及uart驱动层分别在哪个文件里面。
3.1 TTY子系统中设备节点的差别
/dev/ttyS0、/dev/ttySAC0、/dev/tty、/dev/tty0、/dev/tty1、/dev/console,它们有什么差别?
内核的打印信息从哪个设备上显示出来,可以通过内核的cmdline来指定,
比如: console=ttyS0 console=tty
我不想去分辨这个设备是串口还是虚拟终端, 有没有办法得到这个设备? 有!
通过/dev/console!
- console=ttyS0时:/dev/console就是ttyS0
- console=tty时:/dev/console就是前台程序的虚拟终端
- console=tty0时:/dev/console就是前台程序的虚拟终端
- console=ttyN时:/dev/console就是/dev/ttyN
console有多个取值时,使用最后一个取值来判断
4 UART驱动程序整体框架和注册过程
在linux内核中看,以imx6ull平台为例,看一下uart驱动的整体注册过程以及整体框架,主要就是在这个函数里面
drivers/tty/serial/imx.c
static int __init imx_serial_init(void)
{
int ret = uart_register_driver(&imx_reg);
if (ret)
return ret;
ret = platform_driver_register(&serial_imx_driver);
if (ret != 0)
uart_unregister_driver(&imx_reg);
return ret;
}
接下来分两个方面看,分别看uart_register_driver(&imx_reg);函数和platform_driver_register(&serial_imx_driver);函数。
4.1 uart_register_driver(&imx_reg)函数流程
上图是我根据Linux4.9.88源码整理的uart_register_driver(&imx_reg)函数流程,
首先看一下uart_register_driver(&imx_reg)函数的参数,参数是一个uart_driver结构体,这个结构体里面又包含uart_state结构体以及tty_driver结构体。
然后tty_driver结构体里面又包含了struct cdev,这个就是要往驱动里面注册字符设备的,然后tty_driver结构体里面还有tty_port结构体以及ops结构体。
然后看一下uart_register_driver(&imx_reg)函数内部的流程,这个函数用一句话表示就是:申请了一个tty_driver结构体,然后根据传入的uart_driver结构体设置tty_driver结构体,;具体流程也可以从图中看到,函数里面首先申请了uart_state和tty_driver,然后设置这个结构体,就是给这个结构体的各个成员赋值,然后还设置他的ops成员,最后是tty_register_driver,但是这个函数进去之后发现,其实条件并不满足,并没有真正注册到内核中,真正注册内核是在下面serial_imx_driver结构体里面的probe函数里面做的。
4.2 platform_driver_register(&serial_imx_driver)函数
其实我们关注的重点不是platform_driver_register(&serial_imx_driver)函数,这个函数按照之前的套路,无非就是注册了一个platform驱动,然后当驱动和设备匹配之后,调用驱动里面的probe函数,那么重点其实是驱动结构体里面的probe函数,那么也就是int serial_imx_probe(struct platform_device *pdev)函数,
上图是probe函数的大体流程,函数内部首先是申请了一个imx_port结构体,这个imx_port结构体里面还有个uart_port成员,这个probe函数其实主要工作就是:申请imx_port结构体,设置imx_port里面的uart_port结构体,然后添加usrt_port结构体。
具体看函数内部,申请了imx_uart结构体之后,显示解析了设备树获取了寄存器信息,然后设置这个uart_port结构体,然后uart_port结构体里面的ops操作结构体对应的是uart_ops结构体,这个uart_ops结构体里面对应的就是一些imx6ull自己的一些操作函数了,这就具体到底层硬件操作函数了,设置完这些东西之后就开始调用uart_add_one_port(&imx_reg, &sport->port)函数去添加一个port。uart_add_one_port(&imx_reg, &sport->port)函数里面主要又调用了两个函数,
- tty_port_link_device(port, driver, index):这个函数其实看名字link,他内部很简单,他其实就是把uart_driver结构体里面的tty_port和tty_driver里面的tty_port关联起来。
- tty_register_device_attr(driver, index, device, drvdata,attr_grp):这个其实用来添加cdev设备节点的,前面uart_register_driver(&imx_reg)函数的时候只是设置了tty_driver结构体,并没有注册,注册是在这里注册的。这里uart_register_driver(&imx_reg)函数里面又调用了tty_cdev_add函数,然后这个函数里面有driver->cdevs[index]->ops = &tty_fops;这里面的tty_ops就是最上层应用程序调用open,read,write之后往下对应的第一个操作结构体,通过这个结构体再往下调用uart_open一类的函数,再往下调用到imx_read一类的函数,
4.3 uart驱动注册整体架构图
前面大体看了uart驱动注册的流程,看了里面的函数调用,但是太细节了,我又从里面抽出了一个整体架构图,看整体架构图更清晰,因为函数内部细节有各种结构体的名字其实有些乱,有好几个port结构体,还有好几个ops操作函数结构体,
上图就是uart驱动注册的一个整体结构图了,上面是tty层,下面是串口核心层,再往下是具体芯片的驱动层,这里面暂时省略line disciplie层,这个图我们从下往上看整体的注册流程;
- 最下面的imx6ull驱动层:在这一层是有两个函数,一个函数是uart_register_driver(struct uart_driver *drv),另一个函数static int serial_imx_probe(struct platform_device *pdev),然后在int uart_register_driver(struct uart_driver *drv)函数里面之前看过,其实就是根据uart_driver构建了一个tty_driver,然后static int serial_imx_probe(struct platform_device *pdev)函数里面主要是调用了uart_add_one_port(struct uart_driver *drv, struct uart_port *uport),这里面注意出现了第一个ops操作函数结构体,这个结构体里面的操作函数是最底层的函数,是具体操作硬件寄存器的。
- 中间层是串口核心层:这一层注意看一个结构体uart_driver,这个结构体里面有个uart_state成员,这个uart_state成员里面有一个tty_port成员,这个tty_port成员就是和上层的tty_driver关联,这个tty_port里面也有一个ops操作函数结构体,这是第二个ops操作函数结构体,然后state里面还有个uart_port成员就和下层的imx6ull驱动层的那个操作函数结构体关联。
- 上层的tty层:这一层主要是一个tty_driver结构体,其中这里面的ops是之前串口核心层里面的,这是第三个ops结构体,另外,在tty_driver里面还有个cdevs成员,这里面又有个tty_ops结构体,这是第四个ops操作函数结构体。
好了,四个ops结构体乱七八糟的关系算是捋清楚了,当初刚开始看内核代码看到有4个ops看晕了。
那么当应用程序调用个open函数的时候,调用流程是怎么样的呢,就是上图中背影高亮的那四个函数,应用层open----tty_open----uart_open----uart_port_activate----imx_startup。
5 怎么编写串口驱动程序
像SPI,I2C这种驱动框架,一般来说他们都是分为控制器驱动和设备驱动的,然后芯片原厂负责编写控制器驱动和设备树,然后普通的驱动工程师编写设备驱动和相应的设备树文件。
但是对于UART来说他没有所谓的设备驱动,只有控制器驱动,而且控制器驱动是由芯片原厂编写好了,所以如果不是在芯片原厂工作,那么不需要编写uart驱动。如果是在芯片原厂需要编写uart驱动层,可以大体按照下面的图去做(只需要编写uart驱动层,串口核心层、tty层都是Linux内核本来就有的)。
6 串口的读和写过程是怎么一层层传递数据的
上图是我根据内核源码画的一个uart读写过程。下面分别看一下读写过程
6.1 数据发送过程
先看左边的写数据过程,从上往下,UART驱动的数据发送过程大致如下:
- 应用层调用
write
系统调用来写入数据 write
系统调用会调用到tty_write
函数,这个函数定义在driver/tty/tty_io.c
文件中。而在tty_write函数里面调用的是ld->ops->write函数,这个就是line discipline层的write函数。- line discipline层的write函数就是
n_tty_write
函数,这个函数定义在driver/tty/n_tty.c
文件中。n_tty_write
函数会进一步调用到uart_write
函数,这个函数是通过tty_operations
结构体的指针来访问的。 uart_write
函数会进一步调用到start_tx
函数,这个函数也是通过tty_operations
结构体的指针来访问的。在i.MX6ULL平台上,这个函数对应的就是imx_start_tx
函数。- 在
imx_start_tx
函数中,会通过设置UCR1寄存器的TXMPTYEN位来使能发送缓冲区空中断。这个操作是通过调用writel(temp | UCR1_TXMPTYEN, sport->port.membase + UCR1)
来完成的。 - 当UART控制器的发送缓冲区空了之后,就会产生一个中断。这个中断会被内核的中断处理机制捕获,并调用相应的中断处理程序来处理。
- 在中断处理程序中,会从环形缓冲区中取出数据,并写入到UART控制器的发送缓冲区中。然后UART控制器会自动将这些数据发送出去。
6.2 数据接收过程
在Linux内核中,UART驱动的数据接收过程大致如下:
- 当UART控制器的接收引脚接收到数据后,数据会被自动写入接收缓冲区。如果接收缓冲区中的数据达到一定数量(例如,半满或者满),或者在一定时间内没有新的数据到来,那么就会产生一个接收中断。
- 这个中断会被内核的中断处理机制捕获,并调用相应的中断处理程序来处理。在i.MX6ULL平台上,这个中断处理程序对应的就是
imx_rxint
函数,这个函数定义在drivers/tty/serial/imx.c
文件中。 - 在imx_rxint函数中,首先会得到数据,然后会通知行规层来处理,调用行规层的n_tty_receive_buf函数,
- 行规层的n_tty_receive_buf,函数里面,进一步调用n_tty_receive_buf_common__receive_buf函数来处理这些数据。这个函数会根据TTY设备的配置来处理新的数据,例如进行字符映射、回显等操作。处理完之后,数据已经被存储在了环形缓冲区,并且也已经进行了必要的处理,接下来等待应用程序的read来读取。
- 然后等待应用层的read来读取数据,应用层调用read函数时,会调用到tty层的tty_read函数,这个tty_read函数进一步调用line discipline层的read函数,这个line discipline层的read函数就可以读取前面从底层传过来的数据了,
7 怎么调试UART驱动程序
7.1 通过得到UART硬件上收发的数据来调试UART驱动
通过前面对uart收发数据过程的分析,我们可以得到调试uart驱动的第一种方法,那就是在中断服务程序中,把串口收发的数据的打印出来,如上图中红色字体所示。
可以在接收中断函数里把它打印出来,这些数据也会存入UART对应的tty_port的buffer里:
所有要发送出去的串口数据,都会通过uart_write函数发送,所有可以在uart_write中把它们打印出来:
7.2 通过proc文件调试UART驱动
7.2.1 /proc/interrupts
可以查看中断次数。
7.2.2 /proc/tty/drivers
7.2.3 /proc/tty/driver
这个跟上一个不一样,上一个是drivers,复数
7.2.4 /proc/tty/ldiscs
7.3 通过sys文件调试UART驱动程序
在`drivers\tty\serial\serial_core.c`中,有如下代码:
这些代码会在/sys目录中创建串口的对应文件,查看这些文件可以得到串口的很多参数。
怎么找到这些文件?在开发板上执行:
cd /sys
find -name uartclk // 就可以找到这些文件所在目录
8 printk执行过程
8.1 printk的使用
8.1.1 printk使用示例
调试内核、驱动的最简单方法,是使用printk函数打印信息。
printk函数与用户空间的printf函数格式完全相同,它所打印的字符串头部可以加入“\001n”样式的字符。
其中n为0~7,表示这条信息的记录级别,n数值越小级别越高。
注意:linux 2.x内核里,打印级别是用"<n>"来表示。
在驱动程序中,可以这样使用printk:
printk("This is an example\n");
printk("\0014This is an example\n");
printk("\0014""This is an example\n");
printk(KERN_WARNING"This is an example\n");
在上述例子中:
-
第一条语句没有明确表明打印级别,它被处理前内核会在前面添加默认的打印级别:"<4>"
-
KERN_WARNING是一个宏,它也表示打印级别:
#define KERN_SOH "\001" /* ASCII Start Of Header */
#define KERN_WARNING KERN_SOH "4" /* warning conditions */
内核的每条打印信息都有自己的级别,当自己的级别在数值上小于某个阈值时,内核才会打印该信息。
8.1.2 printk函数的记录级别
在内核代码include/linux/kernel.h
中,下面几个宏确定了printk函数怎么处理打印级别:
#define console_loglevel (console_printk[0])
#define default_message_loglevel (console_printk[1])
#define minimum_console_loglevel (console_printk[2])
#define default_console_loglevel (console_printk[3])
举例说明这几个宏的含义:
① 对于printk(“<n>……”),只有n小于console_loglevel时,这个信息才会被打印。
② 假设default_message_loglevel的值等于4,如果printk的参数开头没有“<n>”样式的字符,则在printk函数中进一步处理前会自动加上“<4>”;
③ minimum_console_logleve是一个预设值,平时不起作用。通过其他工具来设置console_loglevel的值时,这个值不能小于minimum_console_logleve。
④ default_console_loglevel也是一个预设值,平时不起作用。它表示设置console_loglevel时的默认值,通过其他工具来设置console_loglevel的值时,用到这个值。
上面代码中,console_printk是一个数组,它在kernel/printk.c中定义:
/* 数组里的宏在include/linux/printk.h中定义
*/
int console_printk[4] = {
CONSOLE_LOGLEVEL_DEFAULT, /* console_loglevel */
MESSAGE_LOGLEVEL_DEFAULT, /* default_message_loglevel */
CONSOLE_LOGLEVEL_MIN, /* minimum_console_loglevel */
CONSOLE_LOGLEVEL_DEFAULT, /* default_console_loglevel */
};
/* Linux 4.9.88 include/linux/printk.h */
#define CONSOLE_LOGLEVEL_DEFAULT 7 /* anything MORE serious than KERN_DEBUG */
#define MESSAGE_LOGLEVEL_DEFAULT CONFIG_MESSAGE_LOGLEVEL_DEFAULT
#define CONSOLE_LOGLEVEL_MIN 1 /* Minimum loglevel we let people use */
/* Linux 5.4 include/linux/printk.h */
#define CONSOLE_LOGLEVEL_DEFAULT CONFIG_CONSOLE_LOGLEVEL_DEFAULT
#define MESSAGE_LOGLEVEL_DEFAULT CONFIG_MESSAGE_LOGLEVEL_DEFAULT
#define CONSOLE_LOGLEVEL_MIN 1 /* Minimum loglevel we let people use */
8.1.3 在用户空间修改printk函数的记录级别
挂接proc文件系统后,读取/proc/sys/kernel/printk文件可以得知console_loglevel、default_message_loglevel、minimum_console_loglevel和default_console_loglevel这4个值。
比如执行以下命令,它的结果“7 4 1 7”表示这4个值:
也可以直接修改/proc/sys/kernel/printk文件来改变这4个值,比如:
# echo "1 4 1 7" > /proc/sys/kernel/printk
这使得console_loglevel被改为1,于是所有的printk信息都不会被打印。
8.1.4 printk函数记录级别的名称和使用
在内核代码include/linux/kernel.h中,有如下代码,它们表示0~7这8个记录级别的名称:
#define KERN_SOH "\001" /* ASCII Start Of Header */
#define KERN_SOH_ASCII '\001'
#define KERN_EMERG KERN_SOH "0" /* system is unusable */
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */
#define KERN_CRIT KERN_SOH "2" /* critical conditions */
#define KERN_ERR KERN_SOH "3" /* error conditions */
#define KERN_WARNING KERN_SOH "4" /* warning conditions */
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */
#define KERN_INFO KERN_SOH "6" /* informational */
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */
在使用printk函数时,可以这样使用记录级别;
printk(KERN_WARNING”there is a warning here!\n”)
8.2 printk执行过程
8.2.1 函数调用过程
在嵌入式Linux开发中,printk信息常常从串口输出,这时串口被称为串口控制台。从内核kernel/printk.c的printk函数开始,往下查看它的调用关系,可以知道printk函数是如何与具体设备的输出函数挂钩的。
printk函数调用的子函数的主要脉落如下:
printk
// linux 4.9: kernel/printk/internal.h
// linux 5.4: kernel/printk/printk_safe.c
vprintk_func
vprintk_default(fmt, args);
vprintk_emit
vprintk_store // 把要打印的信息保存在log_buf中
log_output
preempt_disable();
if (console_trylock_spinning())
console_unlock();
preempt_enable();
console_unlock
for (;;) {
msg = log_from_idx(console_idx);
if (suppress_message_printing(msg->level)) {
/* 如果消息的级别数值大于console_loglevel, 则不打印此信息 */
}
printk_safe_enter_irqsave(flags);
call_console_drivers(ext_text, ext_len, text, len);
printk_safe_exit_irqrestore(flags);
}
call_console_drivers函数调用驱动程序打印信息,此函数在`kernel\printk\printk.c`中,代码如下:
8.2.2 内核打印信息保存在哪里
我们执行dmesg
命令可以打印以前的内核信息,所以这些信息必定是保存在内核buffer中。
在kernel\printk\printk.c
中,定义有一个全局buffer:
执行dmesg
命令时,它就是访问虚拟文件/proc/kmsg
,把log_buf中的信息打印出来。
8.2.3 printk信息从哪些设备打印出来
在内核的启动信息中,有类似这样的命令行参数:
/* IMX6ULL */
[root@100ask:~]# cat /proc/cmdline
console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw
/* STM32MP157 */
[root@100ask:~]# cat /proc/cmdline
root=PARTUUID=491f6117-415d-4f53-88c9-6e0de54deac6 rootwait rw console=ttySTM0,115200
在命令行参数中,"console=ttymxc0"、"console=ttySTM0"就是用来选择printk设备的。
可以指定多个"console="参数,表示从多个设备打印信息。
命令行信息可以来自设备树或者环境参数
设备树
/ {
chosen {
bootargs = "console=ttymxc1,115200";
};
};
UBOOT根据环境参数修改设备树:IMX6ULL
/* 进入IMX6ULL的UBOOT */
=> print mmcargs
mmcargs=setenv bootargs console=${console},${baudrate} root=${mmcroot}
=> print console
console=ttymxc0
=> print baudrate
baudrate=115200
9 console驱动注册过程
9.1 console结构体
struct console {
char name[16]; // name为"ttyXXX",在cmdline中用"console=ttyXXX0"来匹配
// 输出函数
void (*write)(struct console *, const char *, unsigned);
int (*read)(struct console *, char *, unsigned);
// APP访问/dev/console时通过这个函数来确定是哪个(index)设备
// 举例:
// a. cmdline中"console=ttymxc1"
// b. 则注册对应的console驱动时:console->index = 1
// c. APP访问/dev/console时调用"console->device"来返回这个index
struct tty_driver *(*device)(struct console *co, int *index);
void (*unblank)(void);
// 设置函数, 可设为NULL
int (*setup)(struct console *, char *);
// 匹配函数, 可设为NULL
int (*match)(struct console *, char *name, int idx, char *options);
short flags;
// 哪个设备用作console:
// a. 可以设置为-1, 表示由cmdline确定
// b. 也可以直接指定
short index;
// 常用: CON_PRINTBUFFER
int cflag;
void *data;
struct console *next;
};
9.2 console驱动注册过程
9.2.1 处理命令行参数
在kernel\printk\printk.c
中,可以看到如下代码:
__setup("console=", console_setup);
这是用来处理u-boot通过设备树传给内核的cmdline参数,比如cmdline中有如下代码:
console=ttymxc0,115200 console=ttyVIRT0
对于这两个"console=xxx"就会调用console_setup函数两次,构造得到2个数组项:
struct console_cmdline
{
char name[16]; /* Name of the driver */
int index; /* Minor dev. to use */
char *options; /* Options for the driver */
#ifdef CONFIG_A11Y_BRAILLE_CONSOLE
char *brl_options; /* Options for braille driver */
#endif
};
static struct console_cmdline console_cmdline[MAX_CMDLINECONSOLES];
在cmdline中,最后的"console=xxx"就是"selected_console"(被选中的console,对应/dev/console):
9.2.2 register_console
console分为两类,它们通过console结构体的flags来分辨(flags中含有CON_BOOT):
-
bootconsoles:用来打印很早的信息
-
real consoles:真正的console
可以注册很多的bootconsoles,但是一旦注册real consoles
时,所有的bootconsoles都会被注销,并且以后再注册bootconsoles都不会成功。
被注册的console会放在console_drivers链表中,谁放在链表头部?
-
如果只有一个
real consoles
,它自然是放在链表头部 -
如果有多个
real consoles
,"selected_console"(被选中的console)被放在链表头部
放在链表头有什么好处?APP打开"/dev/console"时,就对应它。
uart_add_one_port
uart_configure_port
register_console(port->cons);
9.2.3 /dev/console
在drivers\tty\tty_io.c
中,代码调用过程如下:
tty_open
tty = tty_open_by_driver(device, inode, filp);
driver = tty_lookup_driver(device, filp, &index);
case MKDEV(TTYAUX_MAJOR, 1): {
struct tty_driver *console_driver = console_device(index);
/* 从console_drivers链表头开始寻找
* 如果console->device成功,就返回它对应的tty_driver
* 这就是/dev/console对应的tty_driver
*/
struct tty_driver *console_device(int *index)
{
struct console *c;
struct tty_driver *driver = NULL;
console_lock();
for_each_console(c) {
if (!c->device)
continue;
driver = c->device(c, index);
if (driver)
break;
}
console_unlock();
return driver;
}
10 early_printk和earlycon
10.1 内核信息的早期打印
前面看了printk函数的使用,我们注册了uart_driver、并调用uart_add_one_port后,它里面才注册console,在这之后才能使用printk。
如果想更早地使用printk函数,比如在安装UART驱动之前就使用printk,这时就需要自己去注册console。
更早地、单独地注册console,有两种方法:
-
early_printk:自己实现write函数,不涉及设备树,简单明了
-
earlycon:通过设备树传入硬件信息,跟内核中驱动程序匹配
earlycon是新的、推荐的方法,在内核已经有驱动的前提下,通过设备树或cmdline指定寄存器地址即可。
10.2 early_printk
源码为:arch\arm\kernel\early_printk.c
,要使用它,必须实现这几点:
-
配置内核,选择:CONFIG_EARLY_PRINTK
-
内核中实现:printch函数
-
cmdline中添加:earlyprintk
10.3 earlycon
10.3.1 提供硬件信息的两种方法
arlycon就是early console的意思,实现的功能跟earlyprintk是一样的,只是更灵活。
我们知道,对于console,最主要的是里面的write函数:它不使用中断,相对简单。
所以很多串口console的write函数,只要确定寄存器的地址就很容易实现了。
假设芯片的串口驱动程序,已经在内核里实现了,我们需要根据板子的配置给它提供寄存器地址。
怎么提供?
-
设备树
-
cmdline参数
10.3.2 设置write函数
在Linux内核中,已经有完善的earlycon驱动程序,它们使用OF_EARLYCON_DECLARE宏来定义:
问题在于,使用哪一个?
-
如果cmdline中只有"earlycon",不带更多参数:对应
early_init_dt_scan_chosen_stdout
函数-
使用"/chosen"下的"stdout-path"找到节点
-
或使用"/chosen"下的"linux,stdout-path"找到节点
-
节点里有"compatible"和"reg"属性
-
根据"compatible"找到
OF_EARLYCON_DECLARE
,里面有setup函数,它会提供write函数 -
write函数写什么寄存器?在"reg"属性里确定
-
-
-
如果cmdline中"earlycon=xxx",带有更多参数:对应
setup_earlycon
函数-
earlycon=xxx格式为:
-
<name>,io|mmio|mmio32|mmio32be,<addr>,<options>
<name>,0x<addr>,<options>
<name>,<options>
<name>
-
根据"name"找到
OF_EARLYCON_DECLARE
,里面有setup函数,它会提供write函数 -
write函数写什么寄存器?在"addr"参数里确定
10.3.3 register_console
11 费曼学习法:我录制了一个UART驱动框架讲解视频
12 UART应用程序举例gps_read.c
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <stdlib.h>
/* set_opt(fd,115200,8,'N',1) */
int set_opt(int fd,int nSpeed, int nBits, char nEvent, int nStop)
{
struct termios newtio,oldtio;
if ( tcgetattr( fd,&oldtio) != 0) {
perror("SetupSerial 1");
return -1;
}
bzero( &newtio, sizeof( newtio ) );
newtio.c_cflag |= CLOCAL | CREAD;
newtio.c_cflag &= ~CSIZE;
newtio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); /*Input*/
newtio.c_oflag &= ~OPOST; /*Output*/
switch( nBits )
{
case 7:
newtio.c_cflag |= CS7;
break;
case 8:
newtio.c_cflag |= CS8;
break;
}
switch( nEvent )
{
case 'O':
newtio.c_cflag |= PARENB;
newtio.c_cflag |= PARODD;
newtio.c_iflag |= (INPCK | ISTRIP);
break;
case 'E':
newtio.c_iflag |= (INPCK | ISTRIP);
newtio.c_cflag |= PARENB;
newtio.c_cflag &= ~PARODD;
break;
case 'N':
newtio.c_cflag &= ~PARENB;
break;
}
switch( nSpeed )
{
case 2400:
cfsetispeed(&newtio, B2400);
cfsetospeed(&newtio, B2400);
break;
case 4800:
cfsetispeed(&newtio, B4800);
cfsetospeed(&newtio, B4800);
break;
case 9600:
cfsetispeed(&newtio, B9600);
cfsetospeed(&newtio, B9600);
break;
case 115200:
cfsetispeed(&newtio, B115200);
cfsetospeed(&newtio, B115200);
break;
default:
cfsetispeed(&newtio, B9600);
cfsetospeed(&newtio, B9600);
break;
}
if( nStop == 1 )
newtio.c_cflag &= ~CSTOPB;
else if ( nStop == 2 )
newtio.c_cflag |= CSTOPB;
newtio.c_cc[VMIN] = 1; /* 读数据时的最小字节数: 没读到这些数据我就不返回! */
newtio.c_cc[VTIME] = 0; /* 等待第1个数据的时间:
* 比如VMIN设为10表示至少读到10个数据才返回,
* 但是没有数据总不能一直等吧? 可以设置VTIME(单位是10秒)
* 假设VTIME=1,表示:
* 10秒内一个数据都没有的话就返回
* 如果10秒内至少读到了1个字节,那就继续等待,完全读到VMIN个数据再返回
*/
tcflush(fd,TCIFLUSH);
if((tcsetattr(fd,TCSANOW,&newtio))!=0)
{
perror("com set error");
return -1;
}
//printf("set done!\n");
return 0;
}
int open_port(char *com)
{
int fd;
//fd = open(com, O_RDWR|O_NOCTTY|O_NDELAY);
fd = open(com, O_RDWR|O_NOCTTY);
if (-1 == fd){
return(-1);
}
if(fcntl(fd, F_SETFL, 0)<0) /* 设置串口为阻塞状态*/
{
printf("fcntl failed!\n");
return -1;
}
return fd;
}
int read_gps_raw_data(int fd, char *buf)
{
int i = 0;
int iRet;
char c;
int start = 0;
while (1)
{
iRet = read(fd, &c, 1);
if (iRet == 1)
{
if (c == '$')
start = 1;
if (start)
{
buf[i++] = c;
}
if (c == '\n' || c == '\r')
return 0;
}
else
{
return -1;
}
}
}
/* eg. $GPGGA,082559.00,4005.22599,N,11632.58234,E,1,04,3.08,14.6,M,-5.6,M,,*76"<CR><LF> */
int parse_gps_raw_data(char *buf, char *time, char *lat, char *ns, char *lng, char *ew)
{
char tmp[10];
if (buf[0] != '$')
return -1;
else if (strncmp(buf+3, "GGA", 3) != 0)
return -1;
else if (strstr(buf, ",,,,,"))
{
printf("Place the GPS to open area\n");
return -1;
}
else {
//printf("raw data: %s\n", buf);
sscanf(buf, "%[^,],%[^,],%[^,],%[^,],%[^,],%[^,]", tmp, time, lat, ns, lng, ew);
return 0;
}
}
/*
* ./serial_send_recv <dev>
*/
int main(int argc, char **argv)
{
int fd;
int iRet;
char c;
char buf[1000];
char time[100];
char Lat[100];
char ns[100];
char Lng[100];
char ew[100];
float fLat, fLng;
/* 1. open */
/* 2. setup
* 115200,8N1
* RAW mode
* return data immediately
*/
/* 3. write and read */
if (argc != 2)
{
printf("Usage: \n");
printf("%s </dev/ttySAC1 or other>\n", argv[0]);
return -1;
}
fd = open_port(argv[1]);
if (fd < 0)
{
printf("open %s err!\n", argv[1]);
return -1;
}
iRet = set_opt(fd, 9600, 8, 'N', 1);
if (iRet)
{
printf("set port err!\n");
return -1;
}
while (1)
{
/* eg. $GPGGA,082559.00,4005.22599,N,11632.58234,E,1,04,3.08,14.6,M,-5.6,M,,*76"<CR><LF>*/
/* read line */
iRet = read_gps_raw_data(fd, buf);
/* parse line */
if (iRet == 0)
{
iRet = parse_gps_raw_data(buf, time, Lat, ns, Lng, ew);
}
/* printf */
if (iRet == 0)
{
printf("Time : %s\n", time);
printf("ns : %s\n", ns);
printf("ew : %s\n", ew);
printf("Lat : %s\n", Lat);
printf("Lng : %s\n", Lng);
/* 纬度格式: ddmm.mmmm */
sscanf(Lat+2, "%f", &fLat);
fLat = fLat / 60;
fLat += (Lat[0] - '0')*10 + (Lat[1] - '0');
/* 经度格式: dddmm.mmmm */
sscanf(Lng+3, "%f", &fLng);
fLng = fLng / 60;
fLng += (Lng[0] - '0')*100 + (Lng[1] - '0')*10 + (Lng[2] - '0');
printf("Lng,Lat: %.06f,%.06f\n", fLng, fLat);
}
}
return 0;
}
13 参考文献:
tty初探 — uart驱动框架分析 - 知乎 (zhihu.com)
https://www.cnblogs.com/liqiuhao/p/9031803.html
彻底理解Linux的各种终端类型以及概念_linux 终端类型-CSDN博客
Linux终端和Line discipline图解-CSDN博客
https://www.cnblogs.com/feisky/archive/2010/05/21/1740893.html
-
Serial Programming Guide for POSIX Operating Systems
https://www.cnblogs.com/sky-heaven/p/9675253.html
韦东山老师驱动大全学习视频