扩展串口驱动程序移植

嵌入式Linux设备驱动开发之扩展串口驱动程序移植

《嵌入式Linux应用完全开发手册》第4篇第21章总结归纳

本章目标

  1. 了解串口终端设备驱动程序的层次结构
  2. 掌握移植标准串口驱动程序的方法

串口驱动程序框架概述

串口驱动程序术语介绍

在Linux中经常碰到“控制台”、“终端”、“console”、“tty”、“terminal”等术语,也经常使用到这些设备文件:/dev/ttySAC0/dev/tty0/dev/console等。要理解这些术语,需要从以前的计算机说起。
最初的计算机价格昂贵,一台计算机通常连接多套键盘和显示器供人使用。在以前专门有这种可以连上一台电脑的设备,他只有显示器和键盘,外加简单的处理电路,本身不具备处理信息的能力。用户通过它连接到计算机上(通常是通过串口),然后登录系统,对计算机进行操作。这样一台只有输入、显示部件,并能够连接到计算机的设备就叫做终端。tty是Teletype的缩写,Teletype是最早出现的一种终端设备,很像电传打字机。在Linux中,就用“tty”来表示终端设备,比如内核文件“tty_io.c”、“tty_ioctl.c”等都是与“终端相关的驱动程序”。设备文件/dev/ttySAC0/dev/tty0等也表示某类终端设备。
“console”的意思即为控制台,顾名思义,控制台就是用户与系统进行交互的设备,这和终端的作用类似。实际上控制台和终端相比,也只是多了一项功能:它可以显示系统信息,比如内核消息、后台服务消息。从硬件上看,控制台和终端都是具备输入和显示功能的设备。
控制台和终端的区别体现在软件上,Linux内核从很早之前发展而来,代码中仍保留了“控制台”、“终端”的概念。启动Linux内核前传入的命令行参数“console=…”就是用来指定“控制台”的。控制台在tty驱动初始化之前就可以使用了,它最开始的时候被用来显示内核消息(比如printk函数输出的消息)。
当tty驱动初始化完毕之后。用户程序就可以通过tty驱动的接口来操作各类终端设备,包括控制台。从这个意义上来说,控制台也是一种终端,只不过它还能显示内核信息。
从命令行参数“console=ttySAC0”、“console=tty0”可以了解到:系统中有很多终端设备,可以选取其中一个或多个来作为控制台。设备文件/dev/console对应的设备就是命令行参数“console=...”指定的、用作控制台的终端设备

串口驱动程序的4种结构

终端设备有很多种类,比如串行终端、键盘和显示器、通过网络实现的终端等。串口也属于一种终端设备,它的驱动程序不仅仅是初始化硬件、发送/接收数据。在基本硬件操作的基础上,还增加了很多软件的功能,这是一个多层次的驱动程序。
串口驱动程序从上到下分为4层:终端设备层、行规程、串口抽象层、串口芯片层。这种分法不是绝对的,只是为了更方便理解程序,如下图所示:
img not found
终端设备层和行规程的下面还有其他类型的层次和串口的层次并列,比如键盘、显示器等,接下来只关注串口。
终端设备层向上提供统一的访问接口,使得用户不必关注具体终端的类型。
行规程的作用是指定数据交互的“规矩”,比如流量控制、对输入的数据进行变换处理等。常见的用途有:将TAB字符转换为8个空格,当接收到删除键时删除前面输入的字符,当接收到“CTRL + C”时,发送SIGINT信号。
串口抽象层和串口芯片层都属于底层的驱动程序,它们用来操作硬件。串口抽象层将各类串口的共性概括出来,他也是底层串口驱动的核心部分,比如根据串口芯片层提供的地址识别串口类型,设置波特率等。
串口芯片层与芯片无关,主要是向串口抽象层提供串口芯片所用的资源(访问地址、中断号等),还进行一些与芯片相关的设置。对于标准串口,移植的工作主要在这一层。

串口接收到 “Ctrl + C”时

在串口控制台的前台运行一个程序时,如果要手动结束它,可以输入“Ctrl + C”,处理流程如下:

  1. 串口接受到字符“Ctrl + C”(ASCII码为0x03)后触发中断。假设中断处理函数是drivers/serial/8250.c中的serial8250_interrupt,它属于最底层的函数。
  2. 中断处理函数会将这个字符放入tty层的缓冲区中,每个终端设备都有一个接收缓冲区,里面保存的是原始数据。这一步的函数调用顺序如下:
    1
    2
    3
    4
    5
    serial8250_interrupt(串口芯片层)--->
    serial8250_handle_port(串口芯片层)--->
    receive_chars(串口芯片层)--->
    usrt_insert_char(串口抽象层)--->
    tty_insert_flip_char(终端设备层)--->
  3. 中断处理函数还要调用其他函数进一步处理原始数据,他最终会向当前进程发送SIGINT信号,让它退出。函数调用顺序如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    serial8250_interrupt(串口芯片层)--->
    serial8250_handle_port(串口芯片层)--->
    receive_chars(串口芯片层)--->
    usrt_insert_char(串口抽象层)--->
    tty_insert_flip_char(终端设备层)//保存接收到的数据及它的标志(是否有错误)
    tty_flip_buffer_push(终端设备层)--->
    flush_to_ldisc(终端设备层)--->
    disc->receive_buf,即n_tty_receive_buf(行规程)--->
    n_tty_receive_char(行规程)--->
    n_tty_receice_char(终端设备层)--->//根据字符进行不同的处理
    发送SIGINT信号,isig(行规程)//对于“Ctrl + C”,发信号

串口接收普通数据时

串口的接口简单,它的驱动程序相对于USB、IDE等接口的驱动程序而言比较容易掌握。但是串口驱动程序中的分层思想、通过中断处理函数或者定时器处理函数来完成硬件的操作以释放CPU资源的技巧等,这些技术在内核中比较普遍。
以串口接收到字符为例,在控制台上输入“ls”并按回车键时,发生如下事情:

  1. shell程序一直在休眠,等待接收到“足够”或者“合适”的字符。
  2. 串口接受到字符“l”,将它保存起来。
  3. 串口输出字符“l”,这样控制台就能看到“l”的字样了。
  4. 同理,字符“s”也是如此。
  5. 串口接收到回车符,唤醒shell进程。
  6. shell进程就会读取这些字符做什么事情,本例中,它会打印出当前目录下的内容。

这些过程涉及的函数与上面对“Ctrl + C”的处理过程类似,只是在n_tty_sereive_char函数中,对于普通字符将调用echo_char函数将它回显;对于回车符,回显之后还要调用waitqueue_active唤醒等待数据的进程。

串口发送数据时

在往串口上发送数据时,在U-Boot中是发送一个字符之后,循环查询串口状态,当串口再次就绪时,发送下一个字符。如此循环,知道发送完所有字符。在查询状态的过程中,耗费了CPU资源,效率低下。
在Linux中,串口字符的发送也是通过中断来驱动的。比如在串口控制台上运行一个程序,里面有printf("hello,world\n")字样的语句,它的函数调用关系如下:

1
2
3
4
5
6
7
8
9
10
11
12
tty_write(终端设备层)--->
do_tty_write(终端设备层)--->
write_chan(行规程)--->
add_wait_queue(&tty_write_wait, &wait); //加入等待队列
tty->driver->write, 即uart_write(串口抽象层)--->
//数据先被保存在串口端口(port)的缓冲区,然后启动发送
uart_start(串口抽象层)--->
__uart_start(串口抽象层)--->
port->ops->start_tx,即serial8250_start_tx(串口芯片层)--->
up->ier |= UART_IER_THRI; //这两行使能串口发送中断
serial_out(up, UART_IER,up->ier); //字符的发送在中断函数中进行
schedule() //假如uart_write没立刻发送完数据,进程休眠

可见,即使是发送数据,也没有使用循环查询的方法,他只是把数据保存起来,然后开启发送中断。当串口芯片内部的发送缓冲区可以再次存入数据时,这个中断被触发;在中断处理函数中将数据一点点地发送给串口芯片。
仍以serial8250_interrupt函数为例,发送数据时的调用关系如下:

1
2
3
4
5
6
7
8
9
10
11
serial8250_interrupt(串口芯片层)--->
serial8250_handle_port(串口芯片层)--->
transmit_chars(串口芯片层)--->
serial_out(串口芯片层)---> //将数据写入串口芯片
//如果已经发送完毕,唤醒进程
uart_write_wakeup,将调用uart_tasklet_action(串口抽象层)--->
tty_wakeup(终端设备层)--->
//与上面的write_chan中的“add_wait_queue(&tty->write_wait,&wait)”对应
wake_up_interruptible(&tty->write_wait); //唤醒“等待发送完毕”的进程
//如果已经发送完毕,则禁止发送中断
__stop_tx(串口芯片层)

扩展串口驱动移植

串口驱动程序底层代码分析

扩展串口在开发板上的连线如下图所示,中间的缓冲器用来提高电路的驱动能力。
img not found
扩展串口芯片16C2550属于标准串口,内核的串口驱动程序对它支持良好。可以大胆假设,移植的工作只有一点:告诉这些驱动程序这个扩展芯片所使用的资源,即访问地址和中断号。
与具体芯片相关的驱动代码在“串口芯片层”。对于16C2550,它就是drivers/serial/8250.c。入口函数为serial8250_init,它被用来向上层驱动程序注册串口的物理信息。只要弄清楚了这个函数就知道怎么增加对扩展串口的支持了。
serial8250_init函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static int __init serial8250_init(void)
{
...
ret = uart_register_driver(&serial8250_reg); //注册串口终端设备,未和具体串口挂钩
...
serial8250_isa_devs = platform_device_alloc("serial8250",
PLAT8250_DEV_LEGACY);//分配platform_device 结构
...
ret = platform_device_add(serial8250_isa_devs);//加入内核设备层
...
serial8250_register_ports(&serial8250_reg, &serial8250_isa_devs->dev);//枚举old_serial_port中定义的串口

ret = platform_driver_register(&serial8250_isa_driver);//枚举内核设备层中的接口
...
}

上述5个函数是关键,其中platform_driver_register是重点。
uart_register_driver函数向“终端设备层”注册驱动serial8250_reg,它指定了终端设备的名称、主次设备号等。serial8250_reg内容如下:

1
2
3
4
5
6
7
8
9
static struct uart_driver serial8250_reg = {
.owner = THIS_MODULE,
.driver_name = "serial", //驱动名称,可以使用“cat /proc/tty/driver/serial”来查看
.dev_name = "ttyS",//设备名称,可以使用“cat /proc/devices”来查看
.major = TTY_MAJOR,//主设备号为4
.minor = 64,//次设备号
.nr = UART_NR,//支持的最大串口数,默认为8
.cons = SERIAL8250_CONSOLE,//控制台,如果非空,可以用作控制台
};

uart_register_driver只是注册了主次设备号分别为4和64的终端设备,它还没和具体的硬件挂钩。
platform_device_allocplatform_device_addserial8250_register_ports三个函数被用来枚举“老方法定义的”串口设备。所谓“老方法定义的”串口设备就是使用old_serial_port结构指定物理信息的串口,这是为了与以前的串口驱动兼容而遗留下的数据结构。在drivers/serial/8250.c中有如下几行,其中的SERIAL_PORT_DFNS宏在本书所用的内核中被定义为NULL:

1
2
3
static const struct old_serial_port old_serial_port[] = {
SERIAL_PORT_DFNS /* defined in asm/serial.h */
};

platform_driver_register函数向内核注册了一个平台驱动serial8250_isa_driver,它用来枚举名称为“serial8250”的平台设备。
内核根据其他发生确定了很多设备的信息,这些设备被称为平台设备;加载平台驱动程序时将驱动程序与平台设备逐个比较,如果两者匹配,就是用这个驱动来进一步处理。是否匹配的判断方法是:设备名称和驱动名称是否一样。serial8250_isa_driver结构定义如下:

1
2
3
4
5
6
7
8
9
10
static struct platform_driver serial8250_isa_driver = {
.probe = serial8250_probe,
.remove = __devexit_p(serial8250_remove),
.suspend = serial8250_suspend,
.resume = serial8250_resume,
.driver = {
.name = "serial8250",
.owner = THIS_MODULE,
},
};

可见,serial8250_isa_driver中驱动名称为“serial8250”,只要内核中有相同名称的平台设备,platform_driver_register函数最终会调用serial8250_probe函数来枚举它。
只要内核中名为“serial8250”的平台设备定义了正确的串口物理信息,serial8250_probe函数就能自动的检测串口,并将它和前面注册的终端设备联系起来。
总而言之,移植扩展串口的主要工作是构建一个平台设备的数据结构,在里面指定串口的物理信息。

修改代码以支持扩展串口

串口的物理信息主要有两类,访问地址、中断号。只要指明了这两点,并使它们可用,就可以驱动串口了。“使它们可用”的意思是:设置相关的存储控制器以适当的位宽访问这些地址,注册中断时指明合适的触发方式。

构建串口平台设备的数据结构

在内核代码中查找字符“serial8250”,可以在arch/arm/mach-s3c2410/mach-bast.c中看到如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static struct plat_serial8250_port bast_sio_data[] = {
...
};

static struct platform_device bast_sio = {
.name = "serial8250",
.id = PLAT8250_DEV_PLATFORM,
.dev = {
.platform_data = &bast_sio_data,
},
};
...
static struct platform_device *bast_devices[] _initdata = {
...
&bast_sio,
};

arch/arm/plat-s3c24xx/common-smdk.c中仿照mach-bast.c文件增加如下3段代码。增加的代码如下,它们都使用宏CONFIG_SERIAL_EXTEND_S3C24XX包含起来:

  1. 增加要包含的头文件

    1
    2
    3
    #ifndef CONFIG_SERIAL_EXTEND_S3C24XX
    #include <linux/serial_8250.h>
    #endif
  2. 增加平台设备数据结构

    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
    //for extend serial chip
    #ifdef CONFIG_SERIAL_EXTEND_S3C24XX
    static struct plat_serial8250_port s3c_device_8250_data[] = {
    [0] = {
    .mapbase = 0x28000000,
    .irq = IRQ_EINT18,
    .flag = (UPF_BOOT_AUTOCONF | UPF_IOREMAP | UPF_SHARE_IRQ),
    .iotype = UPIO_MEM,
    .regshift = 0,
    .uartclk = 115200 * 16,
    },
    [1] = {
    .mapbase = 0x29000000,
    .irq = IRQ_EINT17,
    .flag = (UPF_BOOT_AUTOCONF | UPF_IOREMAP | UPF_SHARE_IRQ),
    .iotype = UPIO_MEM,
    .regshift = 0,
    .uartclk = 115200 * 16,
    },
    {}
    };

    static struct platform_device s3c_device_8250 = {
    .name = "serial8250",
    .id = 0,
    .dev = {
    .platform_data = &s3c_device_8250_data,
    },
    };

    #endif

    s3c_device_8250_data结构定义了两个数组项,表示16C2550芯片中的两个串口。数组项0表示扩展串口A,数组项1表示扩展串口B。
    .mapbase = 0x28000000表示串口A的访问基址,这是物理地址。
    .irq = IRQ_EINT18指定串口A使用的中断号为IRQ_EINT18,由电路连接图可知道。
    UPF_BOOT_AUTOCONF表示自动配置串口,即自动检测它的类型、FIFO大小等;UPF_IOREMAP表示需要将前面使用的.mapbase = 0x28000000指定的物理地址映射为虚拟地址,然后才能使用这个虚拟地址来访问串口A;UPF_SHARE_IRQ表示IRQ_EINT18是个共享中断。
    .iotype = UPIO_MEM表示使用“内存地址”(就是mapbase映射后的地址)来访问串口A,与之对应的有UPIO_HUB6、UPIO_RM9000等,它们读写串口芯片的方式有所不同。
    .regshift = 0用来计算串口的寄存器地址。串口的寄存器都有特定的序号,比如发送/接收寄存器(TX/RX)序号为0,中断使能寄存器(IER)序号为1。假设mapbase映射后的地址为membase,寄存器序号为index,则它的访问地址为:membase + (index << regshift)。由电路连接图可知,S3C2410/S3C2440与16C2550的连接的总线宽度为8,所以regshift为0;如果总线宽度为16,则regshift为1;如果总线宽度为32,则regshift为2。
    .uartclk = 115200 * 16表示串口A的时钟。此值计算方法为:假设为了设置串口波特率为baud,需要往串口的商数寄存器写入数值quot=uart/(baudx16)。从16C2550的芯片手册可知quot的计算公式为

    1
    divisor(decimal) = (XTAL1 clock frequency) / (serial data rate x 16)

    由电路连接图可知,晶振频率为1.8432MHz,所以:

    1
    uartclk = (XTAL1 clock frequency) = 1.8432M = 115200 * 16

    串口B与串口A类似,不再赘述。
    s3c_device_8250(platform_device类型的数据结构),它的名字为“serial8250”,这与drivers/serial/8250.c中的平台驱动程序serial8250_isa_driver相对应。

  3. 加入内核设备列表
    把平台设备s3c_device_8250加入smdk_devs数组后,系统启动时会把这个数组中的设备注册进内核中。增加的代码如下:

    1
    2
    3
    4
    5
    6
    7
    static struct platform_device __initdata *smdk_devs[] = {
    &s3c_device_nand,
    ...
    #ifdef CONFIG_SERIAL_EXTEND_S3C24XX
    &s3c_device_8250,
    #endif
    };

    现在,平台设备的数据结构已经设置好。

增加开发板相关的代码使得串口可用

这一步需要实现两点:设置相关的存储控制器以适当的位宽访问串口芯片,注册中断时指明合适的触发方式。这需要在drivers/serial/8250.c中增加代码。

  1. 增加头文件
    设置存储控制器的BANK5时需要用到这个头文件,代码如下:
    1
    2
    3
    4
    //for extend serial chip,
    #ifdef CONFIG_SERIAL_EXTEND_S3C24xx
    #include <asm/arch-s3c2410/regs-mem.h>
    #endif
  2. 设置存储控制器的BANK5的位宽
    由电路原理图可知,16C2550扩展串口芯片需要以8位的总线宽度进行访问,我们在drivers/serial/8250.c的初始化函数前面进行设置,如下所示:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    static int __init serial8250_init(void)
    {
    int ret, i;

    if (nr_uarts > UART_NR)
    nr_uarts = UART_NR;

    printk(KERN_INFO "Serial: 8250/16550 driver $Revision: 1.90 $ "
    "%d ports, IRQ sharing %sabled\n", nr_uarts,
    share_irqs ? "en" : "dis");

    #ifdef CONFIG_SERIAL_EXTEND_S3C24XX
    //设置BANK5的位宽为8
    *((volatile unsigned int *)S3C2410_BWSCON) = \
    ((*((volatile unsigned int *)S3C2410_BWSCON)) & ~(3 << 20)) | S3C2410_BWSCON_DW5_8;
    #endif
    }
  3. 注册中断处理程序时,指定触发方式
    由电路原理图可知,16C2550扩展串口芯片的INTA、INTB中断信号为高电平有效。低电平有效的信号在电路原理图中一般都在前面加上字母“n”,或者加上上划线,比如图中的nIOR、nIOW等信号表示低电平有效。
    所以需要将INTA、INTB指定为上升沿触发(指定为高电平触发也可以),在drivers/serial/8250.c文件中调用request_irq函数之前增加如下代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    static int serial_link_irq_chain(struct uart_8250_port *up)
    {
    ...
    #ifdef CONFIG_SERIAL_EXTEND_S3C24XX
    irq_flags |= IRQF_TRIGGER_RISING; //中断触发方式为上升沿触发
    #endif
    ret = request_irq(up->port.irq, serial8250_interrupt,
    irq_flags, "serial", i);
    ...
    }

增加内核配置项 CONFIG_SERIAL_EXTEND_S3C24XX

在内核文件drivers/serial/Kconfig中增加如下几行:

1
2
3
4
5
config CONFIG_SERIAL_EXTEND_S3C24XX
bool "Extend UART for S3C24XX DEMO Board"
depends on SERIAL_8250=y
--help--
Say Y here to use the extend UART

测试扩展串口

准备工作

首先配置内核,选中配置项CONFIG_SERIAL_EXTEND_S3C24XX。执行“make menuconfig”后,如下选择:

1
2
3
4
5
6
Device Drivers --->
Character devices --->
Serial drivers --->
<*> 8250/16550 and compatible serial support
...
[*] Extend UART for S3C24XX DEMO board

然后执行“make uImage”编译内核,这将在内核arch/arm/boot目录下生成内核映像文件uImage。
最后修改开发板根文件系统,步骤如下。

  1. 如果不使用mdev,如下增加ttyS0,ttyS1设备文件;如果使用mdev,这步可以省略。
    1
    2
    mknod  /dev/ttyS0 c 4 64
    mknod /dev/ttyS1 c 4 64
  2. 修改/etc/inittab文件,增加如下代码。
    1
    ttyS0::askfirst:-/bin/sh

测试扩展串口

使用新内核、新的根文件系统启动系统,然后原来的控制台下执行如下命令,可以看到检测到了两个串口。

1
2
3
4
cat /proc/tty/driver/serial
serinfo:1.0 driver revision:
0: uart:16550A mmio:0x28000000 irq:62 mmbase 0xC486A000 tx:0 rx:0
1: uart:16550A mmio:0x29000000 irq:61 mmbase 0xC486C000 tx:0 rx:0

将第一个扩展串口连接到主机上、将主机的串口设为(9600,8N1)后,就可以通过这个扩展串口来控制系统了。
如果想设置串口的默认波特率为115200,可以参加如下修改内核文件drivers/serial/serial_core.c

1
2
3
4
5
6
7
int uart_register_driver(struct uart_driver *drv)
{
...
normal->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;
改为:
normal->init_termios.c_cflag = B115200 | CS8 | CREAD | HUPCL | CLOCAL;
}