Linux内核UART串口子系统驱动框架详解

news2024/11/18 4:26:33

目录

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  

韦东山老师驱动大全学习视频 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1113055.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

信息安全产品汇总

主要安全机制 防火墙&#xff08;进入系统的安全门&#xff09; 网络入侵检测&#xff08;监控和报警&#xff09; 漏洞扫描&#xff08;巡逻保安&#xff09; 中央控制的安全和策略管理&#xff08;闭路电视监控器&#xff09; 身份识别、4A认证、访问控制服务器、证书验证&am…

html+css布局,DIV区域的宽度和高度随页面宽度变化时等比变化

htmlcss布局,DIV区域的宽度和高度随页面宽度变化时等比变化(即:DIV保持纵横比随页面宽度变化),利用img图片标签能撑起高度的特点实现。 如下图所示&#xff1a; 代码如下&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset&q…

浅谈安科瑞无线测温产品在埃及某房建配电项目中的应用

1.电气接点测温的必要性 电力系统的一次系统一般由供电线路&#xff08;包括架空线路和电缆&#xff09;、变压器、母线、开关柜等电气设备组成。其相互之间存在大量的电气连接点&#xff0c;由于电流流过产生热量&#xff0c;所以几乎所有的电气故障都会导致故障点温度的变化…

药物滥用第五篇介绍

THC&#xff1a; 四氢大麻酚&#xff08;Tetrahydrocannabinol,THC&#xff09;&#xff0c;是一种有机化合物&#xff0c;化学式为C21H30O2&#xff0c;是大麻植物中的一种化学物质&#xff0c;也是大麻中最主要的活性成分之一。THC可以产生放松和幻觉的效果&#xff0c;有助于…

16.1 Socket 端口扫描技术

端口扫描是一种网络安全测试技术&#xff0c;该技术可用于确定对端主机中开放的服务&#xff0c;从而在渗透中实现信息搜集&#xff0c;其主要原理是通过发送一系列的网络请求来探测特定主机上开放的TCP/IP端口。具体来说&#xff0c;端口扫描程序将从指定的起始端口开始&#…

Java —— 程序逻辑控制

目录 1. 顺序结构 2. 分支结构 2.1 if 语句 2.1.1 语法格式1 2.1.2 语法格式2 2.1.3 语法格式3 2.2 switch 语句 3. 循环结构 3.1 while循环 3.2 break与continue 3.3 for循环 4. 输入输出 4.1 输出到控制台 格式化字符串 4.2 从键盘输入 5. 练习 和C语言类似地, Java的程序逻辑…

通讯协议学习之路:USB协议协议理论

通讯协议之路主要分为两部分&#xff0c;第一部分从理论上面讲解各类协议的通讯原理以及通讯格式&#xff0c;第二部分从具体运用上讲解各类通讯协议的具体应用方法。 后续文章会同时发表在个人博客(jason1016.club)、CSDN&#xff1b;视频会发布在bilibili(UID:399951374) 1、…

服务CPU异常飙高问题分析和解决

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是「奇点」&#xff0c;江湖人称 singularity。刚工作几年&#xff0c;想和大家一同进步&#x1f91d;&#x1f91d; 一位上进心十足的【Java ToB端大厂…

R语言提取文字(字符串)中的内容--正则式(1)

科学研究中有时候咱们收集到的数据很乱&#xff0c;不能马上进行分析&#xff0c;如SEER数据&#xff0c;用过都知道&#xff0c;咱们需要对数据进行清洗&#xff0c;从数据中提取咱们需要的东西&#xff0c;才能进行分析&#xff0c;这时候有个有用的东西叫正则式&#xff0c;…

中文编程工具开发语言编程案例:会员VIP管理系统软件实例

中文编程工具开发语言编程案例&#xff1a;会员VIP管理系统软件实例 中文编程工具开发语言编程案例&#xff1a;会员VIP管理系统软件实例。 软件功能&#xff1a; 1、系统设置&#xff1a;参数设定&#xff0c;账号及权限设置&#xff0c;系统初始化&#xff0c;卡类型设置&a…

通讯协议学习之路:IrDA协议协议理论

通讯协议之路主要分为两部分&#xff0c;第一部分从理论上面讲解各类协议的通讯原理以及通讯格式&#xff0c;第二部分从具体运用上讲解各类通讯协议的具体应用方法。 后续文章会同时发表在个人博客(jason1016.club)、CSDN&#xff1b;视频会发布在bilibili(UID:399951374) 序、…

Flutter的Invalid use of a private type in a public API警告

文章目录 问题描述有问题的源码 问题原因解决方法 问题描述 自己在写Flutter 应用时发现了一个Invalid use of a private type in a public API警告。 发现很多官方的例子也有这个问题。 有问题的源码 有问题的源码如下&#xff1a; class MyTabPage extends StatefulWid…

window系统如何管理多版本node

何时需要切换node版本 如果你正在维护一个旧项目&#xff0c;同时也在进行新项目&#xff0c;两个项目所依赖的node版本害不同&#xff0c;那么你可以就需要经常切换node版本。项目中可能依赖于某些npm包&#xff0c;而这些包对特定版本的Node有要求。需要满足这些要求以确保依…

UE4 UltrDynamicSky与场景物体进行交互

找到材质 找到其最父类的材质 把这个拖过去连上即可

Nvm管理NodeJs版本

文章目录 Nvm管理NodeJs版本一、前言1.简介2.环境 二、正文1.卸载NodeJs2.安装Nvm3.配置国内镜像4.Nvm使用5.其它1&#xff09;报错12&#xff09;报错2 Nvm管理NodeJs版本 一、前言 1.简介 Node Version Manager&#xff08;nvm&#xff09;可通过命令行快速安装和使用不同…

【数据库】拼接字段 使用别名

拼接字段 使用别名 e . g . e.g. e.g. Vendors 表包含供应商名和电话信息&#xff0c;name 和 mobile&#xff1b;需要输出这两个属性的值的组合作为供应商的基本信息组合。 SELECT concat(name, _, mobile) FROM Vendors; -- 语句通过 MySQL 环境下测试&#xff0c;其他 DBMS…

【CMN】Components组件汇总

CMN 700由各种类型的设备组成&#xff0c;包括路由器模块、CHI节点和网桥。所需要的组件取决于系统的需求&#xff0c;有些组件是可选的&#xff0c;或者只有在满足某些需求时才会使用。CMN 700可以集成到一个完整的SoC系统中&#xff0c;该系统还包括其他这里未描述到的设备。…

4.6 IPv6

思维导图&#xff1a; 4.6 IPv6 **IPv6简介** - IP 是互联网的核心协议。 - IPv4 是20世纪70年代末期设计的&#xff0c;到2011年2月其地址耗尽。 - 2014-2015年间&#xff0c;我国逐步停止向新用户分配IPv4地址&#xff0c;开始全面部署IPv6。 - IPv6 目的&#xff1a;解决I…

JDK安装后Path和java_home环境变量

1.Path环境变量 1.1Path环境变量用于记住程序路径&#xff0c;方便在命令行窗口的任意目录启动程序 举例&#xff1a;在命令行窗口的任意目录下启动QQ Path环境部变量的位置在&#xff1a;我的电脑->属性->高级系统设置->高级->环境变量 2.2Path环境变量的原理 …

(1)(1.7) GY-US42声纳

文章目录 前言 1 连接方式 2 参数说明 前言 GY-US042v2 声纳是一款非常便宜的短程&#xff08;最多 4m&#xff09;测距仪&#xff0c;主要设计用于室内&#xff0c;但也成功用于室外的 Copter。与许多气压计相比&#xff0c;它能更稳定地控制 4m 以下的高度。 1 连接方式…