目录
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=1152009 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
韦东山老师驱动大全学习视频



















