Linux串口驱动解析之s3c2440

一、Linux TTY子系统软件架构

参考文档

1、前言

    在Linux kernel中,TTY就是各类终端(Terminal)的简称。为了简点击打开链接化终端的使用,以及终端驱动程序的编写,Linux kernel抽象出了TTY framework:对上,向应用程序提供使用终端的统一接口;对下,提供编写终端驱动程序(如serial driver)的统一框架。

2、软件架构

Linux kernel TTY framework位于“drivers/tty”目录中,其软件框架如下面图片1所示:

tty_arch

和Linux其它的framework类似,TTY framework通过TTY core屏蔽TTY有关的技术细节,对上以字符设备的形式向应用程序提供统一接口,对下以TTY device/TTY driver的形式提供驱动程序的编写框架。

2.1、TTY 核心

TTY core是TTY framework的核心逻辑,功能包括:

1)以字符设备的形式,向用户空间提供访问TTY设备的接口,例如:

设备号(主, 次)        字符设备                                   备注 
(5, 0)             /dev/tty                                控制终端(Controlling Terminal) 
(5, 1)             /dev/console                            控制台终端(Console Terminal) 
(4, 0)             /dev/vc/0 or /dev/tty0                  虚拟终端(Virtual Terminal) 
(4, 1)             /dev/vc/1 or /dev/tty1                  同上 
…                        …                                             … 
(x, x)             /dev/ttyS0                              串口终端(名称和设备号由驱动自行决定) 
…                        …                                             … 
(x, x)             /dev/ttyUSB0                            USB转串口终端 
…                        …                                             … 
2)通过设备模型中的struct device结构抽象TTY设备,并通过struct tty_driver抽象该设备的驱动,并提供相应的register接口。TTY驱动程序的编写,简化为填充并注册相应的struct tty_driver结构。

注:TTY framework弱化了TTY设备(上图中使用虚线框标注)的概念,通常情况下,可以在注册TTY驱动的时候,自动分配并注册TTY设备。

3)使用struct tty_struct、struct tty_port等数据结构,从逻辑上抽象TTY设备及其“组件”,以实现硬件无关的逻辑。
4)抽象出名称为线路规程(Line Disciplines)的模块,在向TTY硬件发送数据之前,以及从TTY设备接收数据之后,进行相应的处理(如特殊字符的转换等)。

2.2 System Console Core

Linux kernel的system console主要有两个功能:
1)向系统提供控制台终端(Console Terminal) ,以便让用户登录进行交互操作。
2)提供printk功能,以便kernel代码进行日志输出。
System console core模块使用struct console结构抽象system console功能,具体的driver不需要关心console的内部逻辑,填充该接口并注册给kernel即可。

2.3 TTY Line Disciplines

线路规程(Line Disciplines)在TTY framework中是一个非常优雅的设计,我们可以把它看成设备驱动和应用接口之间的一个适配层。从字面意思理解,就是辅助TTY driver,将我们通过TTY设备键入的字符转换成一行一行的数据[3],当然,实际情况远比这复杂,例如在蜗窝x project所使用的kernel版本中,存在如下的Line Disciplines(以n_为前缀):
pengo@DESKTOP-CH8SB7C:~/work/xprj/linux$ ls drivers/tty/n_* 
drivers/tty/n_gsm.c   drivers/tty/n_r3964.c        drivers/tty/n_tracesink.c  drivers/tty/n_tty.c 
drivers/tty/n_hdlc.c  drivers/tty/n_tracerouter.c  drivers/tty/n_tracesink.h

2.4 TTY Drivers以及System Console Drivers

最后,对内核以及驱动工程师来说,更关注的还是具体的TTY设备驱动。在kernel为我们搭建的如此beauty的框架下面,编写相应的driver就成为一件比较简单的事情了。当然的kernel中,主要的TTY driver有两类:
1)虚拟终端(Virtual Terminal,VT)驱动,位于drivers/tty/vt中,负责实现VT(后续文章会详细介绍)有关的功能。
2)串口终端驱动,也即我们所熟知的serial subsystem(话说终于到重点了,哈哈),位于drivers/tty/serial中。

二、串口驱动解析-2440

参考文档

1、串口移植

S3C2440共有3个串口,在SMDK2440平台上串口0和串口1都作为普通串口使用,串口2工作在红外收发模式。TQ2440开发板将它们都作为普通串口,目前所需要的只有串口0,作为控制终端,所以此处不作修改。
在文件 linux/arch/arm/plat-s3c24xx/devs.c中定义了三个串口的硬件资源。

static struct resource s3c2410_uart0_resource[] = {
    ………………………………
};
static struct resource s3c2410_uart1_resource[] = {
    ………………………………
};
static struct resource s3c2410_uart2_resource[] = {
    ………………………………
};
在文件linux/arch/arm/plat-samsung/dev-uart.c中定义了每个串口对应的平台设备。
static struct platform_device s3c24xx_uart_device0 = {
    .id = 0,
};
static struct platform_device s3c24xx_uart_device1 = {
    .id = 1,
};
static struct platform_device s3c24xx_uart_device2 = {
    .id = 2,
};
在文件linux/arch/arm/mach-s3c2440/mach-smdk2440.c中有串口一些寄存器的初始化配置。
static struct s3c2410_uartcfg smdk2440_uartcfgs[] __initdata = {
    [0] = {
        …………………………
    },
    [1] = {
        …………………………
    },
    /* IR port */
    [2] = {
        …………………………
    }
};
在文件 linux/arch/arm/mach-s3c2440/mach-smdk2440.c中将调用函数s3c24xx_init_uarts()最终将上面的硬件资源,初始化配置,平台设备整合到一起。

在文件 linux/arch/arm/plat-s3c/init.c中有

static int __init s3c_arch_init(void)
{
    ………………………………
    ret = platform_add_devices(s3c24xx_uart_devs, nr_uarts);
    return ret;
}
这个函数将串口所对应的平台设备添加到了内核。

2、串口设备驱动原理浅析

我认为任何设备在linux中的实现就“两条线”。一是设备模型的建立,二是读写数据流。串口驱动也是这样。
串口设备模型建立:
串口设备驱动的核心结构体在文件linux/drivers/serial/samsuing.c中如下

static struct uart_driver s3c24xx_uart_drv = {
    .owner = THIS_MODULE,
    .dev_name = "s3c2410_serial",  
    .nr = CONFIG_SERIAL_SAMSUNG_UARTS,
    .cons = S3C24XX_SERIAL_CONSOLE,
    .driver_name = S3C24XX_SERIAL_NAME,
    .major = S3C24XX_SERIAL_MAJOR,
    .minor = S3C24XX_SERIAL_MINOR,
};
串口驱动的注册


static int __init s3c24xx_serial_modinit(void)
{
    ………………………………
    ret = uart_register_driver(&s3c24xx_uart_drv);
    ………………………………
}
串口驱动其实是一个典型的tty驱动
int uart_register_driver(struct uart_driver *drv)
{
	………………………………
	//每一个端口对应一个state
	drv->state = kzalloc(sizeof(struct uart_state) * drv->nr, GFP_KERNEL);
	………………………………
	normal = alloc_tty_driver(drv->nr); 	//分配该串口驱动对应的tty_driver 
	………………………………
	drv->tty_driver = normal; //让drv->tty_driver字段指向这个tty_driver
	………………………………
	normal->driver_name = drv->driver_name;
	normal->name = drv->dev_name;
	normal->major = drv->major;
	normal->minor_start = drv->minor;
	………………………………
	//设置该tty驱动对应的操作函数集tty_operations (linux/drivers/char/core.c)
	tty_set_operations(normal, &uart_ops); 
	………………………………
	retval = tty_register_driver(normal); 	//将tty驱动注册到内核
	………………………………
}
其实tty驱动的本质是一个字符设备,在文件 linux/drivers/char/tty_io.c中
int tty_register_driver(struct tty_driver *driver)
{
	………………………………
	cdev_init(&driver->cdev, &tty_fops);
	driver->cdev.owner = driver->owner;
	error = cdev_add(&driver->cdev, dev, driver->num);
	………………………………
}
它所关联的操作函数集 tty_fops在文件 linux/drivers/char/tty_io.c中实现
static const struct file_operations tty_fops = {
	.llseek = no_llseek,
	.read = tty_read,
	.write = tty_write,
	………………………………
	.open = tty_open,
	………………………………
};
到此串口的驱动作为tty_driver被注册到了内核。前面提到串口的每一个端口都是作为平台设备被添加到内核的。那么这些平台设备就对应着有它们的平台设备驱动。在文件 linux/drivers/serial/s3c2440.c中有:
static struct platform_driver s3c2440_serial_driver = {
	.probe = s3c2440_serial_probe,
	.remove = __devexit_p(s3c24xx_serial_remove),
	.driver = {
		.name = "s3c2440-uart",
		.owner = THIS_MODULE,
	},
};
当其驱动与设备匹配时就会调用他的探测函数
static int s3c2440_serial_probe(struct platform_device *dev)
{
	return s3c24xx_serial_probe(dev, &s3c2440_uart_inf);
}
每一个端口都有一个描述它的结构体s3c24xx_uart_port 在 文件 linux/drivers/serial/samsuing.c
static struct s3c24xx_uart_port s3c24xx_serial_ports[CONFIG_SERIAL_SAMSUNG_UARTS] = {
	[0] = {
		.port = {
			.lock = __SPIN_LOCK_UNLOCKED(s3c24xx_serial_ports[0].port.lock),
			.iotype = UPIO_MEM,
			.irq = IRQ_S3CUART_RX0,  //该端口的中断号
			.uartclk = 0,
			.fifosize = 16,
			.ops = &s3c24xx_serial_ops, //该端口的操作函数集
			.flags = UPF_BOOT_AUTOCONF,
			.line = 0, //端口编号
		}
	},
	………………………………
}
上面探测函数的具体工作是函数 s3c24xx_serial_probe()来完成的
int s3c24xx_serial_probe(struct platform_device *dev, struct s3c24xx_uart_info *info)
{
	………………………………
	//根据平台设备提供的硬件资源等信息初始化端口描述结构体中的一些字段
	ret = s3c24xx_serial_init_port(ourport, info, dev);
	//前面注册了串口驱动,这里便要注册串口设备
	uart_add_one_port(&s3c24xx_uart_drv, &ourport->port);
	………………………………
}
int uart_add_one_port(struct uart_driver *drv, struct uart_port *uport)
{
	………………………………
	//前面说串口驱动是tty_driver,这里可以看到串口设备其实是tty_dev
	tty_dev = tty_register_device(drv->tty_driver, uport->line, uport->dev);
	………………………………
}
串口数据流分析:
    在串口设备模型建立中提到了三个操作函数集, uart_ops tty_fopss3c24xx_serial_ops数据的流动便是这些操作函数间的调用,这些调用关系如下:

在对一个设备进行其他操作之前必须先打开它,linux/drivers/char/tty_io.c

static const struct file_operations tty_fops = {
	………………………………
	.open = tty_open,
	………………………………
};
//看一下tty_open函数原型
static int tty_open(struct inode *inode, struct file *filp)
{
	………………………………
	dev_t device = inode->i_rdev;
	………………………………
	driver = get_tty_driver(device, &index); //根据端口设备号获取它的索引号
	………………………………
	if (tty) {
		………………………………
	} 
	else
		tty = tty_init_dev(driver, index, 0); 	//创建一个tty_struct 并初始化
	………………………………
}
struct tty_struct *tty_init_dev(struct tty_driver *driver, int idx,int first_ok)
{
	………………………………
	tty = alloc_tty_struct(); 			//分配一个tty_struct结构
	//一些字段的初始化,
	initialize_tty_struct(tty, driver, idx);
	//完成的主要工作是driver->ttys[idx] = tty;
	retval = tty_driver_install_tty(driver, tty);
	………………………………
	/*
	下面函数主要做的就是调用线路规程的打开函数ld->ops->open(tty)。
	在这个打开函数中分配了一个重要的数据缓存
	tty->read_buf = kzalloc(N_TTY_BUF_SIZE, GFP_KERNEL);
	*/
	retval = tty_ldisc_setup(tty, tty->link);
}
void initialize_tty_struct(struct tty_struct *tty,struct tty_driver *driver, int idx)
{
	………………………………
	//获取线路规程操作函数集tty_ldisc_N_TTY,并做这样的工作tty->ldisc = ld;
	tty_ldisc_init(tty);
	………………………………
	/*
	下面函数的主要工作是INIT_DELAYED_WORK(&tty->buf.work, flush_to_ldisc);
	初始化一个延时tty->buf.work 并关联一个处理函数flush_to_ldisc(),这个函数将在
	数据读取的时候用到。
	*/
	tty_buffer_init(tty);
	………………………………
	tty->driver = driver; 
	tty->ops = driver->ops; //这里的ops就是struct tty_operations uart_ops
	tty->index = idx; 		//idx就是该tty_struct对应端口的索引号
	tty_line_name(driver, idx, tty->name);
}
端口设备打 开之后就可以进行读写操作了,这里只讨论数据的读取,在文件 linux/drivers/char/tty_io.c中,
static const struct file_operations tty_fops = {
	………………………………
	.read = tty_read,
	………………………………
};
static ssize_t tty_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
	………………………………
	ld = tty_ldisc_ref_wait(tty); //获取线路规程结构体
	if (ld->ops->read) //调用线路规程操作函数集中的n_tty_read()函数
		i = (ld->ops->read)(tty, file, buf, count);
	else
		………………………………
}
linux/drivers/char/N_tty.c中:
struct tty_ldisc_ops tty_ldisc_N_TTY = {
	………………………………
	.open            = n_tty_open,
	………………………………
	.read            = n_tty_read,
	………………………………
};
static ssize_t n_tty_read(struct tty_struct *tty, struct file *file,
 unsigned char __user *buf, size_t nr)
{
	………………………………
	while (nr) {
		………………………………
		if (tty->icanon && !L_EXTPROC(tty)) {
			//如果设置了tty->icanon 就从缓存tty->read_buf[]中逐个数据读取,并判断读出的每一个数//据的正确性或是其他数据类型等。
			eol = test_and_clear_bit(tty->read_tail,tty->read_flags);
			c = tty->read_buf[tty->read_tail];
			………………………………
		} 
		else {
			………………………………
			//如果没有设置tty->icanon就从缓存tty->read_buf[]中批量读取数据,之所以要进行两次读
			//取是因为缓存tty->read_buf[]是个环形缓存
			uncopied = copy_from_read_buf(tty, &b, &nr);
			uncopied += copy_from_read_buf(tty, &b, &nr);
			………………………………
		}
	}
	………………………………
}
用户空间是从缓存tty->read_buf[]中读取数据读的,那么缓存tty->read_buf[]中的数据有是从那里来的呢?分析如下:
回到文件 linux/drivers/serial/samsuing.c中,串口数据接收中断处理函数实现如下:
这是串口最原始的数据流入的地方
static irqreturn_t  s3c24xx_serial_rx_chars(int irq, void *dev_id)
{
	………………………………
	while (max_count-- > 0) {
		………………………………
		ch = rd_regb(port, S3C2410_URXH); //从数据接收缓存中读取一个数据
		………………………………
		flag = TTY_NORMAL; //普通数据,还可能是其他数据类型在此不做讨论
		………………………………
		/*
		下面函数做的最主要工作是这样
		struct tty_buffer *tb = tty->buf.tail;
		tb->flag_buf_ptr[tb->used] = flag;
		tb->char_buf_ptr[tb->used++] = ch;
		将读取的数据和该数据对应标志插入 tty->buf。
		*/
		uart_insert_char(port, uerstat, S3C2410_UERSTAT_OVERRUN, ch, flag);
	}
	tty_flip_buffer_push(tty); //将读取到的max_count个数据向上层传递。
out:
	return IRQ_HANDLED;
}
void tty_flip_buffer_push(struct tty_struct *tty)
{
	………………………………
	if (tty->low_latency)
		flush_to_ldisc(&tty->buf.work.work);
	else
		schedule_delayed_work(&tty->buf.work, 1); 
	//这里这个延时work在上面串口设备打开中提到过,该work的处理函数也是flush_to_ldisc。
}
static void flush_to_ldisc(struct work_struct *work)
{
	………………………………
	while ((head = tty->buf.head) != NULL) {
		………………………………
		char_buf = head->char_buf_ptr + head->read;
		flag_buf = head->flag_buf_ptr + head->read;
		………………………………
		//刚才在串口接收中断处理函数中,将接收到的数据和数据标志存到tty->buf中,现在将
		//这些数据和标志用char_buf 和flag_buf指向进一步向上传递。
		disc->ops->receive_buf(tty, char_buf,flag_buf, count);
		spin_lock_irqsave(&tty->buf.lock, flags);
	}
}
上面调用的函数disc->ops->receive_buf在文件 linux/drivers/char/N_tty.c中实现
struct tty_ldisc_ops tty_ldisc_N_TTY = {
	………………………………
	.receive_buf     = n_tty_receive_buf,
	………………………………
};
static void n_tty_receive_buf(struct tty_struct *tty, const unsigned char *cp, char *fp, int count)
{
	………………………………
	//现在可以看到缓冲区tty->read_buf 中数据的由来了。
	if (tty->real_raw) {
		//如果设置了tty->real_raw将上面讲到的些传入数据批量拷贝到tty->read_head中。
		//对环形缓存区的数据拷贝需要进行两次,第一次拷贝从当前位置考到缓存的末尾,如果还//有没考完的数据而且缓存区开始出处还有剩余空间,就把没考完的数据考到开始的剩余空
		//间中。
		spin_lock_irqsave(&tty->read_lock, cpuflags);
		i = min(N_TTY_BUF_SIZE - tty->read_cnt,N_TTY_BUF_SIZE - tty->read_head);
		i = min(count, i);
		memcpy(tty->read_buf + tty->read_head, cp, i);
		tty->read_head = (tty->read_head + i) & (N_TTY_BUF_SIZE-1);
		tty->read_cnt += i;
		cp += i;
		count -= i;
		i = min(N_TTY_BUF_SIZE - tty->read_cnt,
		N_TTY_BUF_SIZE - tty->read_head);
		i = min(count, i);
		memcpy(tty->read_buf + tty->read_head, cp, i);
		tty->read_head = (tty->read_head + i) & (N_TTY_BUF_SIZE-1);
		tty->read_cnt += i;
		spin_unlock_irqrestore(&tty->read_lock, cpuflags);
	} 
	else {
		for (i = count, p = cp, f = fp; i; i--, p++) {
			//如果没有设置tty->real_raw,就根据传入数据标志分类获取数据。
			………………………………
		}
		………………………………
	}
	………………………………
}
到此,数据读取的整个过程就结束了。可以看出数据读取可以分为两个阶段,一个阶段是上层函数从环形缓存区tty->read_buf 读取数据,第二阶段是底层函数将接收的数据考到环形缓存区tty->read_buf 中。


  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值