串口驱动程序设计详解---串口初始化(上)

Posted coding__madman

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了串口驱动程序设计详解---串口初始化(上)相关的知识,希望对你有一定的参考价值。

TTY驱动程序架构:

1. TTY概念解析

    1.1 /dev/ttySCA0

    1.2 /dev/tty1-n

    1.3 /dev/console

      在linux系统中,终端是一类字符型设备,它包括多种类型,通常使用tty来简称各种类型的终端设备。

      . 串口终端(/dev/ttyS*)

      串口终端是使用计算机串口连接的终端设备。Linux把每个串行端口都看做是一个字符设备。这些串行端口所对应的设备名称是/dev/ttySAC*;

       .控制台终端(/dev/console)

       在Linux系统中,计算机的输出设备通常被称为控制台终端,这里特指printk信息输出到设备。/dev/console是一个虚拟的设备,它需要映射到真正的tty上,比如通过内核启动参数“console=ttySCA0”就把console映射到了串口0

      . 虚拟终端(、dev/tty*)

      当用户登录时,使用的是虚拟终端。使用Ctcl+Alt[F1 - F6]组合键时,我们就可以切换到tty1、tty2、tty3等上面去。tty*就称为虚拟终端,而tty0则是当前所使用虚拟终端的一个别名。

2. TTY架构分析



下面来通过一个回溯功能从内核代码来印证上面一张图的tty架构各函数调用关系!

第一红色箭头对应的函数就是上图中的driver_write()


上图中第二个红色箭头部分:在该函数处加上一个dump_stack函数保存然后重新编译内核

通过tftp下载到开发板启动内核会看到串口终端打印出这个函数调用的回溯信息!

今天电脑难产了!编译内核用了好久,之前几分钟就搞定了!


开发板启动内核然后dmesg命令查看回溯打印的信息(这里为什么没直接打印到串口要用dmesg和printk打印级别有关)


这里打印的比较多,仔细看其实都是重复的这里我里面的一段复制粘贴下来:

[<c049c7c0>] (dump_stack+0x18/0x1c) from [<c0237670>] (s3c24xx_serial_start_tx+0x14/0xb4)
[<c0237670>] (s3c24xx_serial_start_tx+0x14/0xb4) from [<c023374c>] (uart_start+0x64/0x68)
[<c023374c>] (uart_start+0x64/0x68) from [<c0234cb4>] (uart_write+0xc0/0xe4)
[<c0234cb4>] (uart_write+0xc0/0xe4) from [<c021dd84>] (do_output_char+0x16c/0x1d8)
[<c021dd84>] (do_output_char+0x16c/0x1d8) from [<c021de28>] (process_output+0x38/0x54)
[<c021de28>] (process_output+0x38/0x54) from [<c021e978>] (n_tty_write+0x204/0x444)
[<c021e978>] (n_tty_write+0x204/0x444) from [<c021b808>] (tty_write+0x14c/0x244)
[<c021b808>] (tty_write+0x14c/0x244) from [<c021b958>] (redirected_tty_write+0x58/0x68)
[<c021b958>] (redirected_tty_write+0x58/0x68) from [<c00e5ca4>] (vfs_write+0xbc/0x150)
[<c00e5ca4>] (vfs_write+0xbc/0x150) from [<c00e5e14>] (sys_write+0x44/0x74)
[<c00e5e14>] (sys_write+0x44/0x74) from [<c002fb40>] (ret_fast_syscall+0x0/0x30)

上面的图显示了每个函数一层一层的调用关系,对照着上面的架构分析图来看是不是?(这里再把架构图贴上来)

这函数dump_stack()功能好强大!(要是能做成图形化界面直接显示下面的图形结构真是逆天了)

上面的有个函数n_tty_write函数其实是线路规程里面的write!


下面先来分析串口初始化:先上图


内核中2440、6410的串口部分用的都是s3c24xx.. 这个函数,内部实现都差不多,肯定是内核驱动人员偷懒了!

串口驱动分析之初始化

总结来说最重要的有四项工作:串口初始化、串口打开、串口读取操作、串口写操作

内核代码里面最重要和串口相关文件一个是samsung.c,一个是s3c6400.c


1. 串口驱动程序结构

看下面的图(下面2. 串口中的重要数据结构那张分析流程图):首先用户空间的write串口写函数找到内核中对应的数据结构


这里可以看到用户空间的write函数对应的file_operations中的 tty_write,这里还有一些其他的常用函数

继续对照着下面的图看tty_write对应的应该是线路规程中的tty_ldisc_ops中的函数:


同样可以从内核代码中找到线路规程的ops数据结构:


这里总结一下用户空间的write函数会调用系统调用接口的tty-write, 然后tty-write又会调用线路规程里面n_tty_write。

还是对照着下面的图继续分析,可以看到n_tty_write会调用tty_operations里面的函数,内核代码中n_tty_write函数有点长,这里截了两张图:



下面来瞅瞅tty_operations结构定义:


由这个可以看出n_tty_write又对应着这里的uart_write函数!跳来跳去的好麻烦啊!感觉都是套路!

下面还是继续分析uart_write庐山真面目,同样可以从下面的图中可以看到uart_write又对应着驱动里面写函数:


这里面有几个重要的数据结构:第二第三个箭头指向的部分:这里可以看到从struct uart_state结构中拿到了port端口号uart_port, 下面再跳到uart_start(tty)函数中瞅瞅:


继续跳:


从上图可以看出这个函数又通过port结构跳到ops里面的函数!其实port就是uart_ops数据结构类型!上面的图中可以函数这个数据结构里面有很多的函数指针!然后就可以利用这些函数指针在驱动层来操作硬件了!

上面要特别注意struct uart_state数据结构,把上面截图中的一行代码扣出来分析:

struct uart_state *state = tty->driver_data;

可以看到这个数据结构是从tty的driver_data里面来的,那么这个数据结构又是怎么放到driver_data里面去的呢?这个是uart_open函数里面做的:


从上面的函数中可以看出从uart_open函数中拿到state 然后将state放入tty的driver_data(void*型)结构中。

下面继续分析state是如何获得的:


从sourceInsight中可以看到state是从driver里面uart_state的数据结构中拿到的:



上面分析了那么多其实总结最重要的几种数据结构的包含关系可以总结如下:

uart_driver  ------>  uart_state  ------>  uart_port  ------>  uart_ops



2. 串口驱动中的重要数据结构


第一步串口调用过程都是以下面这张图来分析的,上一步中所说的下图都是这下面这张图




3. 初始化分析

老规矩,从原有的内核源码中开始分析!重复造轮子!

在SI中打开samsung.c

module_init(s3c24xx_serial_modinit);
module_exit(s3c24xx_serial_modexit);

跳转到模块初始化函数代码部分:


这里可以看到在模块初始化部分使用了uart_register_driver函数注册了一个串口驱动,下面来看看函数参数的类型:


结合上面的第一步的分析和第二步的流程图看看是不是清晰多了!(这里分析的其实也是对照着上面的流程图来的)

下面再来分析s3c6400.c文件中的模块初始化:


继续跳:


可以看到这个函数又调用了平台驱动注册函数(上一篇博文有详细讲解),注册平台驱动的时候,平台总线会将平台驱动和我们系统中的平台设备进行一一的匹配,如果有匹配的上的,他就会调用平台驱动的probe函数

跳来跳去跳到这了:


下面来细致的分析上面的函数:

首先看第一个箭头所指的地方,看看这个结构体:


这个数组存放的全部是uart_port的串口信息。

然后会调用串口初始化函数:

<span style="color:#ff0000;">ret = s3c24xx_serial_init_port(ourport, info, dev);</span>


函数比较长这里做为两张图截了,可以看到这个函数主要是做一些初始化工作!其中比较重要的是上图中红色箭头所指部分,第一个箭头上一行首先是基地址初始化,这里使用的是静态映射(linux在启动的时候就把物理地址和虚拟地址之间的关系映射好了)!拿虚拟地址,拿中断号, 复位fifo



跳回去继续分析probe函数:


看看第一个箭头所指向的函数,这个函数主要建立uart_driver和uart_port之间的联系的:

/**
 *	uart_add_one_port - attach a driver-defined port structure
 *	@drv: pointer to the uart low level driver structure for this port
 *	@uport: uart port structure to use for this port.
 *
 *	This allows the driver to register its own uart_port structure
 *	with the core driver.  The main purpose is to allow the low
 *	level uart drivers to expand uart_port, rather than having yet
 *	more levels of structures.
 */
int uart_add_one_port(struct uart_driver *drv, struct uart_port *uport)
{
	struct uart_state *state;
	struct tty_port *port;
	int ret = 0;
	struct device *tty_dev;

	BUG_ON(in_interrupt());

	if (uport->line >= drv->nr)
		return -EINVAL;

	state = drv->state + uport->line;
	port = &state->port;

	mutex_lock(&port_mutex);
	mutex_lock(&port->mutex);
	if (state->uart_port) {
		ret = -EINVAL;
		goto out;
	}

	state->uart_port = uport;
	state->pm_state = -1;

	uport->cons = drv->cons;
	uport->state = state;

	/*
	 * If this port is a console, then the spinlock is already
	 * initialised.
	 */
	if (!(uart_console(uport) && (uport->cons->flags & CON_ENABLED))) {
		spin_lock_init(&uport->lock);
		lockdep_set_class(&uport->lock, &port_lock_key);
	}

	uart_configure_port(drv, state, uport);

	/*
	 * Register the port whether it's detected or not.  This allows
	 * setserial to be used to alter this ports parameters.
	 */
	tty_dev = tty_register_device(drv->tty_driver, uport->line, uport->dev);
	if (likely(!IS_ERR(tty_dev))) {
		device_init_wakeup(tty_dev, 1);
		device_set_wakeup_enable(tty_dev, 0);
	} else
		printk(KERN_ERR "Cannot register tty device on line %d\\n",
		       uport->line);

	/*
	 * Ensure UPF_DEAD is not set.
	 */
	uport->flags &= ~UPF_DEAD;

 out:
	mutex_unlock(&port->mutex);
	mutex_unlock(&port_mutex);

	return ret;
}

ret = device_create_file(&dev->dev, &dev_attr_clock_source);//创建属性文件 这个驱动中会用到

安装驱动我们在/sys/目录下看到的串口相关信息就是这个函数实现的:




又是尼玛的跳来跳去,好几把套路卧槽!


这里这个函数暂时就跳到这吧!心累!没完没了的!先顾大局!

ret = s3c24xx_serial_cpufreq_register(ourport);//动态频率调节函数


又是套路,这里暂时只要知道和cpu有关就行了,暂时放过!

下面再来一张串口初始化分析流程图:


至此为止整个串口初始化流程就分析完了!内核代码还真不是那么简单!

先注册平台总线驱动,然后平台总线驱动和平台设备相匹配,匹配上了就会调用probe函数,然后probe函数就会执行一些初始化工作!

下一篇开始着手边分析边撸代码!四步走战略:串口初始化 、打开串口、串口写操作、串口读操作

以上是关于串口驱动程序设计详解---串口初始化(上)的主要内容,如果未能解决你的问题,请参考以下文章

嵌入式串口数据接收程序

12.2440串口驱动程序设计

Python Qt GUI设计:做一款串口调试助手(实战篇—1)

uboot中串口(控制台)初始化详解

Python Qt GUI设计:做一款串口调试助手(实战篇—1)

USB转串口电路设计详解