Linux设备驱动程序学习(十四)——tty设备驱动程序

本文深入解析Linux系统中的终端设备,涵盖串行端口、伪终端、控制台终端的原理与结构,阐述tty_driver、tty_operations、tty_struct的核心作用,以及设备的注册、操作和驱动流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

终端设备

  在Linux系统中,终端是一种字符型设备,它有多种类型,通常使用tty来简称各种类型的终端设备。tty是Teletype的缩写,Teletype是最早出现的一种终端设备,很像电传打字机,是由Teletype公司生产的
  Linux中包含如下几类终端设备:

  1. 串行端口终端(/dev/ttySn)
    串行端口终端(Serial Port Terminal)是使用计算机串行端口连接的终端设备。计算机把每个串行端口都看作是一个字符设备。这些串行端口所对应的设备名称是 /dev/ttyS0(或/dev/tts/0)、/dev/ttyS1(或/dev/tts/1)等,设备号分别是(4,0)、(4,1)等。
    在命令行上把标准输出重定向到端口对应的设备文件名上就可以通过该端口发送数据,例如,在命令行提示符下键入: echo test > /dev/ttyS1会把单词“test”发送到连接在ttyS1端口的设备上。
  2. 伪终端(/dev/pty/)
    伪终端(Pseudo Terminal)是成对的逻辑终端设备,并存在成对的设备文件,如/dev/ptyp3和/dev/ttyp3,它们与实际物理设备并不直接相关。如果一个程序把ttyp3看作是一个串行端口设备,则它对该端口的读/写操作会反映在该逻辑终端设备对应的ptyp3上,而ptyp3则是另一个程序用于读写操作的逻辑设备。这样,两个程序就可以通过这种逻辑设备进行互相交流,使用ttyp3的程序会认为自己正在与一个串行端口进行通信。
  3. 控制台终端(/dev/ttyn, /dev/console)
    如果当前进程有控制终端(Controlling Terminal)的话,那么/dev/tty就是当前进程的控制终端的设备特殊文件。在UNIX系统中,计算机显示器通常被称为控制台终端(Console)。它仿真了类型为Linux的一种终端(TERM=Linux),并且有一些设备特殊文件与之相关联:tty0、tty1、tty2等。当用户在控制台上登录时,使用的是tty1。使用Alt+[F1—F6]组合键时,我们就可以切换到tty2、tty3等上面去。

Linux终端设备驱动的框架结构

  Linux内核中 tty的层次结构包含tty核心、tty线路规程和tty驱动。
                   在这里插入图片描述
                       tty的层次结构图
  tty 线路规程的工作是以特殊的方式格式化从一个用户或者硬件收到的数据,这种格式化常常采用一个协议转换的形式,例如 PPP 和 Bluetooth。

  • tty设备发送数据的流程为:tty核心从一个用户获取将要发送给一个 tty设备的数据,tty核心将数据传递给tty线路规程驱动,接着数据被传递到tty驱动,tty驱动将数据转换为可以发送给硬件的格式。
  • 接收数据的流程为: 从tty硬件接收到的数据向上交给tty驱动,进入tty线路规程驱动,再进入 tty 核心,在这里它被一个用户获取。尽管大多数时候tty核心和tty之间的数据传输会经历tty线路规程的转换,但是tty驱动与tty核心之间也可以直接传输数据。

Linux终端设备的结构体

tty_driver结构体

  任何一个 tty 驱动的主要数据结构是 struct tty_driver. 它用来注册和注销一个 tty 驱动到 tty 内核, 在内核头文件 <linux/tty_driver.h> 中描述:

struct tty_driver
{
	int magic;
	struct cdev cdev;       /* 对应的字符设备cdev */
	struct module owner;    /* 这个驱动的模块拥有者 */ 
	const char * driver_name;
	const char *	devfs_name;
	const char		name;   /* 设备名 */ 
	int name_base;					/* offset of printed name */
	int 			major;							/* 主设备号 */
	int 			minor_start;					/* 开始次设备号 */
	int 			minor_num;						/* 设备数量 */
	int 			num;							/* 被分配的设备数量 */
	short			type;							/* tty驱动的类型 */
	short			subtype;						/* tty驱动的子类型 */
	struct termios init_termios;                     /* 初始线路设置 */
	int 			flags;							/* tty驱动标志 */
	int 			refcount;          // 引用计数(针对可加载的tty驱动)  
	struct proc_dir_entry proc_entry;  // proc文件系统入口 */ 
	struct tty_driver other;           // 仅对PTY驱动有意义 *//* 接口函数 */
	int(*open) (struct tty_struct * tty, struct file * filp);
	void(*close) (struct tty_struct * tty, struct file * filp);
	int(*write) (struct tty_struct * tty, const unsigned char * buf, int count);
	int(*ioctl) (struct tty_struct * tty, struct file * file, unsigned int cmd, 
		unsigned long arg);
	void(*stop) (struct tty_struct * tty);
	void(*start) (struct tty_struct * tty);
	void(*hangup) (struct tty_struct * tty);
	……

	struct list_head tty_drivers;
};
  • magic表示给这个结构体的“幻数”,设为 TTY_DRIVER_MAGIC,在 alloc_tty_driver()函数中被初始化。
  • name与driver_name的不同在于后者表示驱动的名字,用在 /proc/tty 和 sysfs中,而前者表示驱动的设备节点名。
  • type 与subtype描述tty驱动的类型和子类型,subtype的值依赖于type,type成员的可能值为 TTY_DRIVER_TYPE_SYSTEM。
  • init_termios 为初始线路设置,为一个termios结构体,这个成员被用来提供一个线路设置集合。
  • termios 用于保存当前的线路设置,这些线路设置控制当前波特率、数据大小、数据流控设置等,这个结构体包含tcflag_t c_iflag(输入模式标志)、tcflag_t c_oflag(输出模式标志)、tcflag_t c_cflag(控制模式标志)、tcflag_t c_lflag(本地模式标志)、cc_t c_line(线路规程类型)、cc_t c_cc[NCCS](一个控制字符数组)等成员。驱动会使用一个标准的数值集初始化这个成员,它拷贝自tty_std_termios变量,tty_std_termos结构体表示为:
struct termios tty_std_termios =
{
	.c_iflag = ICRNL | IXON,						/* 输入模式 */
	.c_oflag = OPOST | ONLCR,						/* 输出模式 */
	.c_cflag = B38400 | CS8 | CREAD | HUPCL,		/* 控制模式 */
	.c_lflag = ISIG | ICANON | ECHO | ECHOE | ECHOK | ECHOCTL | ECHOKE | IEXTEN,
 /* 本地模式 */
	.c_cc = INIT_C_CC				/* 控制字符,用来修改终端的特殊字符映射 */
};
  • major、minor_start、minor_num表示主设备号、次设备号及可能的次设备数,name表示设备名(如ttyS)。
  • 后面的函数指针实际和tty_operations结构体等同,它们通常需在特定设备tty驱动模块初始化函数中被赋值。

tty_operations结构体

  tty_operations中的成员函数与 tty_driver中的同名成员函数意义完全一致,其定义为:

struct tty_operations
{
	int(open) (struct tty_struct tty, struct file * filp);     //打开函数
	void(close) (struct tty_struct tty, struct file * filp);    //关闭函数
	int(write) (struct tty_struct tty,                   //写函数
		        const unsigned char * buf, int count);
	void(*put_char) (struct tty_struct * tty, unsigned char ch);
/*put_char()为单字节写函数,当单个字节被写入设备时这个函数被 tty 核心调用,如果一个 tty 驱动没有定义这个函数,将使用count参数为1的write()函数。*/
	void(*flush_chars) (struct tty_struct * tty);
	int(*write_room) (struct tty_struct * tty);
/*flush_chars()与wait_until_sent()函数都用于刷新数据到硬件。write_room()指示有多少缓冲区空闲。*/
	int(*chars_in_buffer) (struct tty_struct * tty);
/*chars_in_buffer()指示缓冲区中包含的数据数。*/
	int(ioctl) (struct tty_struct * tty, struct file file, 
		        unsigned int cmd, unsigned long arg);
/*控制函数,类似其它设备的ioctl函数*/
	void(set_termios) (struct tty_struct * tty, struct termios old);
/*当设备的 termios 设置被改变时,set_termios()函数将被tty核心调用。*/
	void(throttle) (struct tty_struct tty);
	void(unthrottle) (struct tty_struct tty);
	void(*stop) (struct tty_struct * tty);
	void(*start) (struct tty_struct * tty);
/*throttle ()、unthrottle()、stop()和start()为数据抑制函数,这些函数用来帮助控制 tty 核心的输入缓存。当 tty 核心的输入缓冲满时,throttle()函数将被调用,tty驱动试图通知设备不应当发送字符给它。当 tty 核心的输入缓冲已被清空时,unthrottle()函数将被调用暗示设备可以接收数据。stop()和start()函数非常像throttle()和 unthrottle()函数,但它们表示 tty 驱动应当停止发送数据给设备以及恢复发送数据。 */
	void(*hangup) (struct tty_struct * tty);
/*当 tty驱动挂起 tty设备时,hangup()函数被调用,在此函数中进行相关的硬件操作*/
	void(*flush_buffer) (struct tty_struct * tty);
/*flush_buffer()函数用于刷新缓冲区并丢弃任何剩下的数据。*/
	void(*set_ldisc) (struct tty_struct * tty);
/*set_ldisc()函数用于设置线路规程,当 tty 核心改变tty驱动的线路规程时这个函数被调用,这个函数通常不需要被驱动定义。*/
	int(*read_proc) (char * page, char * *start, off_t off, 
		             int count, int * eof, void * data);
	int(*write_proc) (struct file * file, const char __user *buffer, 
		              unsigned long count, void * data);
/*read_proc()和write_proc()为/proc 读和写函数。*/
	int(*tiocmget) (struct tty_struct * tty, struct file * file);
	int(*tiocmset) (struct tty_struct * tty, struct file * file, 
		            unsigned int set, unsigned int clear);
/*tiocmget()函数用于获得tty 设备的线路设置,对应的tiocmset()用于设置tty设备的线路设置,参数set和clear包含了要设置或者清除的线路设置。 */
};

tty_struct结构体

  tty_struct结构体被 tty核心用来保存当前tty端口的状态,它的大多数成员只被 tty核心使用。
   tty_struct中的几个重要成员如下:

  • flags标示tty 设备的当前状态,包括TTY_THROTTLED、TTY_IO_ERROR、TTY_OTHER_CLOSED、TTY_EXCLUSIVE、 TTY_DEBUG、TTY_DO_WRITE_WAKEUP、TTY_PUSH、TTY_CLOSING、TTY_DONT_FLIP、 TTY_HW_COOK_OUT、TTY_HW_COOK_IN、TTY_PTY_LOCK、TTY_NO_WRITE_SPLIT等。
  • ldisc为给 tty 设备的线路规程。
  • write_wait、read_wait为给tty写/读函数的等待队列,tty驱动应当在合适的时机唤醒对应的等待队列。
  • termios为指向 tty 设备的当前 termios 设置的指针。
  • stopped:1指示是否停止tty设备,tty 驱动可以设置这个值;hw_stopped:1指示是否tty设备已经被停止,tty 驱动可以设置这个值;flow_stopped:1指示是否 tty 设备数据流停止。
  • driver_data、disc_data为数据指针,用于存储tty驱动和线路规程的“私有”数据。

Linux终端设备的装载和卸载

  Linux内核提供了一组函数用于操作tty_driver结构体及tty设备,包括:

tty设备的注册和注销

注册tty设备

void tty_register_device(struct tty_driver *driver, unsigned index, struct device *device);
  仅有tty_driver是不够的,驱动必须依附于设备,tty_register_device()函数用于注册关联于tty_driver的设备,index为设备的索引(范围是0~driver->num),如:

for (i = 0; i < XXX_TTY_MINORS; ++i) 
tty_register_device(xxx_tty_driver, i, NULL); 
注销tty设备

void tty_unregister_device(struct tty_driver *driver, unsigned index);
  上述函数与tty_register_device()对应,用于注销tty设备,其使用方法如:

for (i = 0; i < XXX_TTY_MINORS; ++i) 
tty_unregister_device(xxx_tty_driver, i); 

tty驱动的操作

分配tty驱动

struct tty_driver *alloc_tty_driver(int lines);
  这个函数返回tty_driver指针,其参数为要分配的设备数量,line会被赋值给tty_driver的num成员,例如:

xxx_tty_driver = alloc_tty_driver(XXX_TTY_MINORS); 
if (!xxx_tty_driver) //分配失败 
return -ENOMEM; 
注册tty驱动

int tty_register_driver(struct tty_driver *driver);
  注册tty驱动成功时返回0,参数为由alloc_tty_driver ()分配的tty_driver结构体指针,例如:

retval = tty_register_driver(xxx_tty_driver); 
if (retval) //注册失败 
{ 
printk(KERN_ERR “failed to register tiny tty driver”); 
put_tty_driver(xxx_tty_driver); 
return retval; 
} 
注销tty驱动

int tty_unregister_driver(struct tty_driver *driver);
  这个函数与tty_register_driver ()对应,tty驱动最终会调用上述函数注销tty_driver。

设置tty驱动操作

void tty_set_operations(struct tty_driver *driver, struct tty_operations *op);
上述函数会将tty_operations结构体中的函数指针拷贝给tty_driver对应的函数指针,在具体的tty驱动中,通常会定义1个设备特定的 tty_operations。

tty驱动的模块加载和卸载

  终端设备驱动都围绕tty_driver结构体而展开,一般而言,终端设备驱动应包含如下组成:

  • 终端设备驱动模块加载函数和卸载函数,完成注册和注销tty_driver,初始化和释放终端设备对应的tty_driver结构体成员及硬件资源。
  • 实现tty_operations结构体中的一系列成员函数,主要是实现open()、close()、write()、tiocmget()、tiocmset()等函数。

  tty驱动的模块加载函数中通常需要分配、初始化tty_driver结构体并申请必要的硬件资源,对应的模块加载函数模板为:

static int __init xxx_init(void)
{/* 分配tty_driver结构体 */
	xxx_tty_driver	= alloc_tty_driver(XXX_PORTS);
	/* 初始化tty_driver结构体 */
	xxx_tty_driver->owner = THIS_MODULE;
	xxx_tty_driver->devfs_name = “tts /;
	xxx_tty_driver->name = “ttyS”;
	xxx_tty_driver->major = TTY_MAJOR;
	xxx_tty_driver->minor_start = 64;
	xxx_tty_driver->type = TTY_DRIVER_TYPE_SERIAL;
	xxx_tty_driver->subtype = SERIAL_TYPE_NORMAL;
	xxx_tty_driver->init_termios = tty_std_termios;
	xxx_tty_driver->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;
	xxx_tty_driver->flags = TTY_DRIVER_REAL_RAW;
	tty_set_operations(xxx_tty_driver, &xxx_ops);
	ret 	= tty_register_driver(xxx_tty_driver);    //注册驱动
	if (ret)
		{
		printk(KERN_ERR “Couldn’t register xxx serial driver \ n”);
		put_tty_driver(xxx_tty_driver);
		return ret;
		}
ret = request_irq();                   /* 硬件资源申请 */ 
}

  卸载函数的话就是把已经注册的设备和驱动调用相关的注销函数。

Linux终端设备的函数操作

打开和关闭函数

  当用户对tty驱动所分配的设备节点进行open()系统调用时,tty_driver中的open()成员函数将被tty核心调用。tty 驱动必须设置open()成员,否则,-ENODEV将被返回给调用open()的用户。open()成员函数的第1个参数为一个指向分配给这个设备的 tty_struct 结构体的指针,第2个参数为文件指针:

static int xxx_open(struct tty_struct * tty, struct file * file)
{
	struct xxx_tty * xxx;
	/* 分配xxx_tty内存 */
	xxx 	= kmalloc(sizeof(*xxx), GFP_KERNEL);
	if (!xxx)
		return - ENOMEM;
	/* 初始化xxx_tty中的成员 */
	init_MUTEX(&xxx->sem);
	xxx->open_count 	= 0;/* 让tty_struct中的driver_data指向xxx_tty */
	tty->driver_data	= xxx;
	xxx->tty			= tty;return 0;
}

  在用户对前面使用 open()系统调用而创建的文件句柄进行close()系统调用时,tty_driver中的close()成员函数将被tty核心调用。

数据发送和接收函数

   用户在有数据发送给终端设备时,通过“write()系统调用――tty核心――线路规程”的层层调用,最终调用tty_driver结构体中的write()函数完成发送。
  因为速度和tty硬件缓冲区容量的原因,不是所有的写程序要求的字符都可以在调用写函数时被发送,因此写函数应当返回能够发送给硬件的字节数以便用户程序检查是否所有的数据被真正写入。如果在 wirte()调用期间发生任何错误,一个负的错误码应当被返回。
  tty_driver 的write()函数接受3个参数tty_struct、发送数据指针及要发送的字节数,一般首先会通过tty_struct的driver_data成员得到设备私有信息结构体,然后依次进行必要的硬件操作开始发送:

static int xxx_write(struct tty_struct * tty, const unsigned char * buf, int count)
{
	/* 获得tty设备私有数据 */
	struct xxx_tty xxx = (struct xxx_tty)
	tty->driver_data;while (1)        /* 开始发送 */
		{
		local_irq_save(flags);
		c= min_t(int, count, min(SERIAL_XMIT_SIZE - xxx->xmit_cnt - 1, 
			SERIAL_XMIT_SIZE - xxx->xmit_head));
		if (c <= 0)
			{
			local_irq_restore(flags);
			break;
			}
		//拷贝到发送缓冲区 
		memcpy(xxx->xmit_buf + xxx->xmit_head, buf, c);
		xxx->xmit_head	= (xxx->xmit_head + c) & (SERIAL_XMIT_SIZE - 1);
		xxx->xmit_cnt		+= c;
		local_irq_restore(flags);
		buf += c;
		count -= c;
		total	+= c;
		}
	if (xxx->xmit_cnt && !tty->stopped && !tty->hw_stopped)
		{
		start_xmit(xxx);							//开始发送 
		}
	return total;    //返回发送的字节数 
}

  当tty子系统自己需要发送数据到 tty 设备时,如果没有实现 put_char()函数,write()函数将被调用,此时传入的count参数为1。
  tty_driver结构体中没有提供 read()函数。因为发送是用户主动的,而接收即用户调read()则是读一片缓冲区中已放好的数据。tty 核心在一个称为 struct tty_flip_buffer 的结构体中缓冲数据直到它被用户请求。因为tty核心提供了缓冲逻辑,因此每个 tty 驱动并非一定要实现它自身的缓冲逻辑。
  tty驱动不必过于关心tty_flip_buffer 结构体的细节,如果其count字段大于或等于TTY_FLIPBUF_SIZE,这个flip缓冲区就需要被刷新到用户,刷新通过对 tty_flip_buffer_push()函数的调用来完成:

for (i = 0; i < data_size; ++i)
{
	if (tty->flip.count >= TTY_FLIPBUF_SIZE)
		tty_flip_buffer_push(tty);                 //数据填满向上层“推” 
	tty_insert_flip_char(tty, data[i], TTY_NORMAL);   //把数据插入缓冲区 
}
tty_flip_buffer_push(tty);

  从tty 驱动接收到字符通过tty_insert_flip_char()函数被插入到flip缓冲区。
  该函数的第1个参数是数据应当保存入的 tty_struct结构体,第 2 个参数是要保存的字符,第3个参数是应当为这个字符设置的标志,如果字符是一个接收到的常规字符,则设为TTY_NORMAL,如果是一个特殊类型的指示错误的字符,依据具体的错误类型,应当设为TTY_BREAK、 TTY_PARITY或TTY_OVERRUN。

### 编写ARM架构下串口驱动程序 #### 一、理解Linux内核中的UART接口 在Linux环境中,编写针对ARM架构的串口(UART)驱动程序需要深入了解Linux内核的工作机制及其提供的API。对于串口通信而言,主要涉及到的是`TTY子系统`和具体的硬件抽象层(HAL),这些都由内核提供支持[^1]。 #### 二、创建平台设备驱动结构体 为了使驱动能够被正确识别并初始化,在代码实现之前应该先定义好相应的platform_device数据结构,并将其注册到系统的总线之上。这一步骤通常是在板级配置文件中完成的,比如arch/arm/mach-*/board-*.c这样的路径下面找到对应的入口函数来添加新的device声明。 ```c static struct platform_device my_uart_device = { .name = "my_serial", .id = -1, }; ``` #### 三、实现基本的操作方法集 接下来要做的就是构建实际处理收发消息等功能的具体逻辑了。这部分工作主要是通过填充tty_operations这个结构体成员变量的方式来达成目的。这里包含了诸如open/close/read/write等标准I/O操作以及其他一些辅助性的功能接口。 ```c static const struct tty_operations serial_ops = { /* ... */ .write = uart_write, // 发送字符流给终端 .read = uart_read, // 接受来自终端的数据 .ioctl = uart_ioctl, // 执行控制命令 /* ... */ }; ``` #### 四、设置波特率和其他参数 除了上述基础之外,还需要考虑如何让用户空间的应用可以灵活调整连接属性,像波特率之类的物理特性往往都是可变的因素之一。为此可以在driver内部维护一套私有的line discipline对象用于保存当前会话期间所使用的各项设定值。 ```c struct ktermios termios; // 设置默认波特率为9600bps termios.c_cflag &= ~CBAUD; termios.c_cflag |= B9600; ``` #### 五、编译与部署 当所有的源码都已经准备完毕之后,按照常规流程执行Makefile来进行编译即可得到目标模块(.ko文件)[^2]。需要注意的是由于最终是要放到ARM平台上运行的缘故,所以在交叉编译阶段应当指定正确的工具链前缀(如arm-linux-gnueabihf-)以确保生成的结果适用于目标环境。 ```bash make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- insmod your_module_name.ko ``` #### 六、验证成果 最后一步自然是检验整个过程是否顺利结束——即能否正常使用新加入的功能单元。可以通过简单的应用程序去读取/发送字符串至对应端口从而观察预期效果;当然也可以借助专门的日志记录设施(dmesg)查看是否有异常报错信息输出。 ```python with open("/dev/ttyS0", 'r+') as f: print(f.read()) f.write("test message\n") ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值