工作中需要调试linux系统下的串口驱动,因特殊需求需要创建一些虚拟串口,工作中需要将串口驱动的整个流程全部梳理,中途中遇到了不少问题,现在问题已经基本解决,现将整个过程做个总结记录。

整体框图

首先给出整体框图,后面结合代码详细介绍各个流程

UART驱动注册

uart_driver数据结构

向kernel中注册一个串口驱动之前,需要先准备一个uart_driver结构该,uart_driver就是串口的驱动

1
2
3
4
5
6
7
/* 虚拟uart驱动 结构体*/
static struct uart_driver virtual_uart_driver = {
.owner = THIS_MODULE,
.driver_name = "virtual_serial",
.dev_name = "COM",
.nr = VIRTUAL_UART_PORT_NR,
};

driver_name:串口驱动的名字,可以自定义
dev_name:使用该驱动注册串口设备的设备名前缀,也可以自定义
nr:该驱动允许注册的设备数量,尽可能按照实际需求来写,注册串口驱动时会根据nr来申请资源

uart_register_driver注册驱动

准备好uart_driver数据结构后,调用uart_register_driver即可将其注册进kernel.

接下来看看uart_register_driver里做了哪些事情(请注意nr的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//为uart_driver->state, 每个端口需要一个state
drv->state = kzalloc(sizeof(struct uart_state) * drv->nr, GFP_KERNEL);
if (!drv->state)
goto out;
//创建一个tty_driver数据结构,并为各个端口分配资源
normal = alloc_tty_driver(drv->nr);
if (!normal)
goto out_kfree;
//uart_driver中的tty_driver指向新创建的tty_driver,方便其他代码根据uart_driver查找tty_driver
drv->tty_driver = normal;
//一系统的参数初始化
normal->driver_name = drv->driver_name;
normal->name = drv->dev_name;
normal->major = drv->major;
normal->minor_start = drv->minor;
normal->type = TTY_DRIVER_TYPE_SERIAL;
normal->subtype = SERIAL_TYPE_NORMAL;
normal->init_termios = tty_std_termios;
normal->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;
normal->init_termios.c_ispeed = normal->init_termios.c_ospeed = 9600;
normal->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV;
normal->driver_state = drv;
//设置tty_driver的ops,uart_ops由kernel核心代码提供
tty_set_operations(normal, &uart_ops);

/*
* Initialise the UART state(s).
*/
for (i = 0; i < drv->nr; i++) {
struct uart_state *state = drv->state + i;
struct tty_port *port = &state->port;
//初始化每个tty端口,核心部分是tty_buffer_init
tty_port_init(port);
port->ops = &uart_port_ops;
port->close_delay = HZ / 2; /* .5 seconds */
port->closing_wait = 30 * HZ;/* 30 seconds */
}
//注册tty_driver
retval = tty_register_driver(normal);
if (retval >= 0)
return retval;

uart_port数据结构

向kernel中添加串口之前,需要准备好uart_port数据结构

uart_port与底层的硬件密切相关,对于驱动框架而言,最重要的是uart_ops,定义了操作硬件的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static struct uart_ops virtual_uart_pops = {
.tx_empty = virtual_tx_empty,
.set_mctrl = virtual_set_mctrl,
.get_mctrl = virtual_get_mctrl,
.stop_tx = virtual_nothing,
.start_tx = virtual_start_tx,
.stop_rx = virtual_nothing,
.break_ctl = virtual_break_ctl,
.startup = virtual_startup,
.shutdown = virtual_nothing,
.set_termios = virtual_set_termios,
.type = virtual_type,
.release_port = virtual_nothing,
.request_port = virtual_nothing1,
.config_port = virtual_config_port,
};

另外1个重要成员是line,指示该uart_port属于驱动中的第几个端口,对于到应用层的串口编号

uart_add_one_port 添加串口

uart_driver注册好后,需要将uart_port添加进去

uart_driver只是一框架,不具备与硬件打交道的能力,与硬件打交道是uart_port,因此一般调试驱动时更多的是在修改uart_port,但查找问题的过程一般要贯穿整个流程

1
2
3
4
5
6
7
8
9
10
for(i = 0; i < VIRTUAL_UART_PORT_NR; i++) {
virtual_uart_port[i].ops = &virtual_uart_pops;
virtual_uart_port[i].attr_group = &virtual_attr_group;
virtual_uart_port[i].type = PORT_VIRTUAL;
virtual_uart_port[i].line = i;
//将准备好的uart_port添加到uart_driver中
ret = uart_add_one_port(&virtual_uart_driver, virtual_uart_port+i);
if (unlikely(ret))
goto remove_port;
}

现在来看看uart_add_one_port干了些什么事(以下只将重要部分展现出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int uart_add_one_port(struct uart_driver *drv, struct uart_port *uport)
{
struct uart_state *state;
struct tty_port *port;
struct device *tty_dev;
state = drv->state + uport->line;
port = &state->port;
state->uart_port = uport;
uport->state = state;
//使用serial_core提供的代码对端口进行配置,会调用uart_port的ops里的接口对硬件做配置
uart_configure_port(drv, state, uport);
/*
* Register the port whether it's detected or not. This allows
* setserial to be used to alter this port's parameters.
*/
tty_dev = tty_port_register_device_attr(port, drv->tty_driver,
uport->line, uport->dev, port, uport->tty_groups);
}

再来跟踪一下tty_port_register_device_attr

1
2
3
4
5
6
7
8
9
10
11
struct device *tty_port_register_device_attr(struct tty_port *port,
struct tty_driver *driver, unsigned index,
struct device *device, void *drvdata,
const struct attribute_group **attr_grp)
{
//tty_port_link_devie非常简单,将tty_driver的uart_port指向当前的port
// driver->ports[index] = port;
tty_port_link_device(port, driver, index);
return tty_register_device_attr(driver, index, device, drvdata,
attr_grp);
}

继续跟踪tty_register_device_attr(以下只将重要部分展现出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct device *tty_register_device_attr(struct tty_driver *driver,
unsigned index, struct device *device,
void *drvdata,
const struct attribute_group **attr_grp)
{
char name[64];
dev_t devt = MKDEV(driver->major, driver->minor_start) + index;
struct device *dev = NULL;

if (driver->type == TTY_DRIVER_TYPE_PTY)
pty_line_name(driver, index, name);
else
tty_line_name(driver, index, name);

if (!(driver->flags & TTY_DRIVER_DYNAMIC_ALLOC)) {
//初始化字符设备tty_driver->cdev,并添加进kernel,文件操作使用tty_fops
retval = tty_cdev_add(driver, devt, index, 1);
}

dev = kzalloc(sizeof(*dev), GFP_KERNEL);
dev->devt = devt;
dev_set_name(dev, "%s", name);

retval = device_register(dev);
}

各数据结构的关系

经过一系列的注册及初始化过程后,可以得到以下关系图

open uart,打开串口

应用层打开串口通常是通过打开/dev/下的节点来实现,应用层打开串口后就可以像操作普通文件一样来操作串口了,下面详细讲述一下open串口的具体流程

## tty_open

首先根据VFS可以查找到tty_fops,因此打开/dev/COMx时会调用tty_open,tty_open会做许多初始化

  • 申请一个tty_struct,每个串口只有1个tty_struct,如果多个应用打开同一个串口时,不会申请新的,直接使用已经申请好的;
  • 将tty_struct与当前的文件描述符进行绑定(关闭串口时会用到,如果还有其它文件描述符占用tty_struct,关闭串口时则不会释放tty_struct,否则会释放);
  • 调用tty_struct->ops->open打开tty,做进一步的打开操作(向下一层进攻,开始研究tty_struct的申请过程
  • 其实对串口本身不相关的初始化

申请tty_struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static int tty_open(struct inode *inode, struct file *filp)
{
struct tty_struct *tty;
struct tty_driver *driver = NULL;
int index;
dev_t device = inode->i_rdev;

//根据device(有设备号)可以查找刚才注册的tty_driver,同时也可以得到index(第几个串口)
driver = tty_lookup_driver(device, filp, &noctty, &index);
//在tty_driver中取出第index个tty_struct指针,没有别的应用打开该串口时,则返回的是一个空指针
/* check whether we're reopening an existing tty */
tty = tty_driver_lookup_tty(driver, inode, index);
if (IS_ERR(tty)) {
retval = PTR_ERR(tty);
goto err_unlock;
}
}

if (tty) {
//如果有别的应用打开了这个串口,则只需要将计数加1
tty_lock(tty);
retval = tty_reopen(tty);
if (retval < 0) {
tty_unlock(tty);
tty = ERR_PTR(retval);
}
} else /* Returns with the tty_lock held for now */
tty = tty_init_dev(driver, index); //如果没有别的应用打开这个串口,需要自己创建tty_struct

进一步来看tty_init_dev(以下只将重要部分展现出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct tty_struct *tty_init_dev(struct tty_driver *driver, int idx)
{
struct tty_struct *tty;
int retval;
//重点是通过alloc_tty_strcut来申请1个tty_struct
tty = alloc_tty_struct(driver, idx);

//将tty_driver中的tty_struct指向新申请的tty_struct,便于其他应用直接使用
//初始化tty_struct的termios
retval = tty_driver_install_tty(driver, tty);

//初始化tty_sturct->tty_port
if (!tty->port)
tty->port = driver->ports[idx];
tty->port->itty = tty;

//这也是一个重要的东西
retval = tty_ldisc_setup(tty, tty->link);
return tty;
}

来看看alloc_tty_struct,就是一系统的初始化(以下只将重要部分展现出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct tty_struct *alloc_tty_struct(struct tty_driver *driver, int idx)
{
struct tty_struct *tty;
tty = kzalloc(sizeof(*tty), GFP_KERNEL);
tty->magic = TTY_MAGIC;
//这是一个重要的地方,使用N_TYY作为tty的ldisc,简化成tty->ldisc->ops = tty_ldisc_N_TTY
tty_ldisc_init(tty);
tty->driver = driver;
//tty_struct的ops直接使用tty_driver的ops,对于串口就是uart_ops
tty->ops = driver->ops;
tty->index = idx;
tty_line_name(driver, idx, tty->name);

return tty;
}

n_tty

注意:tty的数据读取主要由tty_ldisc_N_TTY来完成

n_tty负责tty的读写控制,各种数据缓存、阻塞、调度处理等
另外也要处理tty的字符转换,比如回显、换行转回车等

在申请tty_struct时调用了tty_ldisc_setup,现在来看看tty_ldisc_setup(以下只将重要部分展现出来

1
2
3
4
5
6
7
8
9
10
int tty_ldisc_setup(struct tty_struct *tty, struct tty_struct *o_tty)
{
struct tty_ldisc *ld = tty->ldisc;
int retval;
//使用tty_ldisc_N_TTY->open打开,即执行n_tty_open
retval = tty_ldisc_open(tty, ld);
if (retval)
return retval;
return 0;
}

n_tty_open会初始化处理数据需要的各类数据结构,本次工作没有对n_tty做深入研究,因此不作进一步展开

uart_open

打开串口的最后一重要部分就是执行uart_open,uart_open由serial_core.c提供,完成串口的初始操作

来看下uart_open干了哪些事(同样只将重要部分展现出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int uart_open(struct tty_struct *tty, struct file *filp)
{
struct uart_driver *drv = (struct uart_driver *)tty->driver->driver_state;
int retval, line = tty->index;
struct uart_state *state = drv->state + line;
struct tty_port *port = &state->port;

//打开计算,关闭时会用到
port->count++;

tty->driver_data = state;
state->uart_port->state = state;
tty_port_tty_set(port, tty);//port->tty = tty

//做进一步处理
retval = uart_startup(tty, state, 0);

return retval;
}

uart_open没干什么事情,都丢给了uart_startup来处理

1
2
3
4
5
6
7
8
9
10
11
12
13
static int uart_startup(struct tty_struct *tty, struct uart_state *state,
int init_hw)
{
struct tty_port *port = &state->port;

//如果已经初始过,则直接退出
if (port->flags & ASYNC_INITIALIZED)
return 0;
uart_port_startup(tty, state, init_hw);
//标记已经成功初始化
set_bit(ASYNCB_INITIALIZED, &port->flags);
return retval;
}

不过uart_startup也没干什么事,又丢给了uart_port_startup处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static int uart_port_startup(struct tty_struct *tty, struct uart_state *state,
int init_hw)
{
struct uart_port *uport = state->uart_port;
unsigned long page;
int retval = 0;

if (uport->type == PORT_UNKNOWN)
return 1;

//为发送申请4Kbytes的FIFO
if (!state->xmit.buf) {
/* This is protected by the per port mutex */
page = get_zeroed_page(GFP_KERNEL);
if (!page)
return -ENOMEM;

state->xmit.buf = (unsigned char *) page;
uart_circ_clear(&state->xmit);
}
//调用最底层的硬件startup来初始化硬件
retval = uport->ops->startup(uport);
if (retval == 0) {
if (uart_console(uport) && uport->cons->cflag) {
tty->termios.c_cflag = uport->cons->cflag;
uport->cons->cflag = 0;
}
//打开串口时会配置默认波特率,需要特别注意
uart_change_speed(tty, state, NULL);
}

return retval;
}

分析到这里,就已经从应用层打开串口到底层硬件初始的所有流程,此时底层硬件的startup需要根据实际硬件来编写,此次工作中是一种特殊的虚拟串口驱动,因此startup会相对比较简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int virtual_startup(struct uart_port *port)
{
struct uart_port *phy_port;
//获取虚拟串口绑定的硬件串口,将硬件串口的波特率等信息复制到虚拟串口
phy_port = get_phy_uart_by_config(port->line);
if(phy_port) {
struct tty_struct *vir_tty = port->state->port.tty;
struct tty_struct *phy_tty = phy_port->state->port.tty;

vir_tty->termios.c_cflag = phy_tty->termios.c_cflag;
}

return 0;
}

各数据结构的关系

经过一系统初始后,可以得到以下关系图

write uart,串口写流程

向串口中写数据分成几个层次,每一层都分工明确

tty_write
​ do_tty_write
​ n_tty_write
​ uart_write/uart_put_char
​ 底层硬件驱动_start_tx
​ handle_tx

tty_write

功能相对比较简,tty_write调用do_tty_write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
static inline ssize_t do_tty_write(
ssize_t (*write)(struct tty_struct *, struct file *, const unsigned char *, size_t),
struct tty_struct *tty,
struct file *file,
const char __user *buf,
size_t count)
{
ssize_t ret, written = 0;
unsigned int chunk;

chunk = 2048;
if (test_bit(TTY_NO_WRITE_SPLIT, &tty->flags))
chunk = 65536;
if (count < chunk)
chunk = count;
//为tty_struct->write_buf准备一段空间,有一点小小算法在效率和内存占用上取一个平衡
/* write_buf/write_cnt is protected by the atomic_write_lock mutex */
if (tty->write_cnt < chunk) {
unsigned char *buf_chunk;

if (chunk < 1024)
chunk = 1024;

buf_chunk = kmalloc(chunk, GFP_KERNEL);
if (!buf_chunk) {
ret = -ENOMEM;
goto out;
}
kfree(tty->write_buf);
tty->write_cnt = chunk;
tty->write_buf = buf_chunk;
}
//循环将数据从用户态复制到write_buf中,然后调用n_tty_write发送
/* Do the write .. */
for (;;) {
size_t size = count;
if (size > chunk)
size = chunk;
ret = -EFAULT;
if (copy_from_user(tty->write_buf, buf, size))
break;
ret = write(tty, file, tty->write_buf, size);
if (ret <= 0)
break;
written += ret;
buf += ret;
count -= ret;
if (!count)
break;
//每写完一次调度一次,因为n_tty_write只是将数据写入缓存
//底层硬件发送一般比较慢,调度一次可以更加充分的利用CPU
ret = -ERESTARTSYS;
if (signal_pending(current))
break;
cond_resched();
}
return ret;
}

do_tty_write,将数据分次发给n_tty_write,n_tty_write会做更加复杂的处理,因为linux将tty数据收发的主要工作交给了n_tty

##n_tty_write_

n_tty_write的代码比较多,这里就不贴出来了,描述一下它的几种重要流程

  • 处理tty的回显
  • 通过tty_write_room(用调用uart_write_room)获取底层xmit.buf可用空间
  • 调用uart_write将数据写入串口的xmit.buf中
  • 如果底层xmit.buf的空间不足导致数据没有发送完,则将当前进程挂起,等待唤醒,由tty->write_wait控制

uart_write

uart_write负责将数据写入xmit.buf,然后调用uart_start启动底层发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

static int uart_write(struct tty_struct *tty,
const unsigned char *buf, int count)
{
struct uart_state *state = tty->driver_data;
struct uart_port *port;
struct circ_buf *circ;
unsigned long flags;
int c, ret = 0;

port = state->uart_port;
circ = &state->xmit;

spin_lock_irqsave(&port->lock, flags);
while (1) {
c = CIRC_SPACE_TO_END(circ->head, circ->tail, UART_XMIT_SIZE);
if (count < c)
c = count;
//如果circ中没有可用空间了则退出,底层中断会不停的中取走数据
if (c <= 0)
break;
memcpy(circ->buf + circ->head, buf, c);
circ->head = (circ->head + c) & (UART_XMIT_SIZE - 1);
buf += c;
count -= c;
ret += c;
}
spin_unlock_irqrestore(&port->lock, flags);
//通知底层,可以执行发送动作
uart_start(tty);
return ret;
}

uart_start

uart_start简单的调用底层驱动的start_tx

底层驱动的start_tx一般只是打开发送中断(芯片tx fifo中没有数据则产生中断,数据发完后再关闭中断

handle_tx

底层驱动的发送中断函数,负责从circ_buf中取数据,然写入寄存器将数据发送到硬件端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void handle_tx(struct uart_port *port)
{
struct circ_buf *xmit = &port->state->xmit;
int tx_count;
unsigned int tf_pointer = 0;

tx_count = uart_circ_chars_pending(xmit);

while (tf_pointer < tx_count) {
//循环从xmit.buf中取数据并发送出去
}
//数据发完后,circ_buf中没有写入新的数据,则xmit会为空,此时将硬件中断关闭
if (uart_circ_empty(xmit))
msm_hsl_stop_tx(port);

if (uart_circ_chars_pending(xmit) < WAKEUP_CHARS) {
//如果circ_buf中为的数据少于临界值了,则唤醒正在等待的进程,告诉它可以继续写数据了
uart_write_wakeup(port);
}
}

发送流程

到此为止,整个发送流程已经梳理完毕,附上流程图

read uart,串口读流程

包含2部分

  • open uart时,底层驱动会打开串口的接收功能,硬件接收到数据后会调用rx中断服务函数,数据的入口既是中断服务函数:

    handle_rx
    ​ tty_insert_flip_xx
    ​ flush_to_ldisc
    ​ n_tty_receive_buf2

  • 应用层读取串口数据

    tty_read
    ​ n_tty_read
    ​ copy_from_read_buf

handle_rx

底层驱动的接收中断函数,负责从硬件FIFO中读取数据然后写入到驱动层的buf中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static void handle_rx(struct uart_port *port, unsigned int misr)
{
struct tty_struct *tty = port->state->port.tty;
int count = 0;

//从硬件读取当前FIFO中有多少数据
if (misr & UARTDM_ISR_RXSTALE_BMSK) {
count = msm_hsl_read(port,
regmap[vid][UARTDM_RX_TOTAL_SNAP]) -
msm_hsl_port->old_snap_state;
msm_hsl_port->old_snap_state = 0;
} else {
count = 4 * (msm_hsl_read(port, regmap[vid][UARTDM_RFWR]));
msm_hsl_port->old_snap_state += count;
}

/* and now the main RX loop */
while (count > 0) {
unsigned int c;
//循环将FIFO中的数据读取出来,放到驱动层的buf中
/* TODO: handle sysrq */
/* if (!uart_handle_sysrq_char(port, c)) */
tty_insert_flip_string(tty->port, (char *) &c,
(count > 4) ? 4 : count);
count -= 4;
}
//唤醒处理接收buf的工作队列
tty_flip_buffer_push(tty->port);
}

##flush_to_ldisc

flush_to_ldisc会调用 n_tty_receive_buf2处于接收到的数据,代码比较复杂,只做简单的功能众介绍

  • 检查n_tty_data的可用空间,如果应用层没有及时将数据读走,则不会从驱动层读取数据,同时做流控制操作
  • 有可用空间的情况下,会将驱动层的数据读取到n_tty_data中
  • 根据当前tty的模式,会将接收到的数据做转换处理,要完成终端协议,这里的处理会比较多
  • 唤醒应用层的读取挂起进程

##tty_read

功能相对比较简,tty_read调用n_tty_read(同样只将重要部分展现出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
static ssize_t n_tty_read(struct tty_struct *tty, struct file *file,
unsigned char __user *buf, size_t nr)
{
struct n_tty_data *ldata = tty->disc_data;
unsigned char __user *b = buf;
DECLARE_WAITQUEUE(wait, current);
int c;
int minimum, time;
ssize_t retval = 0;
long timeout;
unsigned long flags;
int packet;


//根据termios.c_cc[VTIME]和termios.c_cc[VMIN]计算出单次读取操作的超时时间
minimum = time = 0;
timeout = MAX_SCHEDULE_TIMEOUT;
if (!ldata->icanon) {
minimum = MIN_CHAR(tty);
if (minimum) {
time = (HZ / 10) * TIME_CHAR(tty);
if (time)
ldata->minimum_to_wake = 1;
else if (!waitqueue_active(&tty->read_wait) ||
(ldata->minimum_to_wake > minimum))
ldata->minimum_to_wake = minimum;
} else {
timeout = (HZ / 10) * TIME_CHAR(tty);
ldata->minimum_to_wake = minimum = 1;
}
}

packet = tty->packet;
//如果没有数据可读取,当前进程挂起,通过read_wait来唤醒
add_wait_queue(&tty->read_wait, &wait);
while (nr) {
if (!input_available_p(tty, 0)) {
//如果没有数据,则挂起进程
timeout = schedule_timeout(timeout);
continue;
}
//copy_from_read_buf:从n_tty_data数据结构中读取数据
uncopied = copy_from_read_buf(tty, &b, &nr);
}
return retval;
}

读取流程

到此为止,整个接收流程已经梳理完毕,附上流程图