1 概述
在Linux中,常碰到“控制台”、“终端”、“console”、“tty”等术语,也会经常使用一些设备文件:/dev/console、/dev/ttyS0、/dev/ttyUSB0等。tty是Teletype的缩写,Teletype是最早出现的一种终端设备,Linux通常使用tty来表示“终端”,终端设备的种类有很多,比如串行终端、键盘和显示器、通过网络实现的终端等。
UART与USART都是单片机上的串口通信, UART(universal asynchronous receiver and transmitter)通用异步收/发器,USART(universal synchronous asynchronous receiver and transmitter)通用同步/异步收/发器。从名字上可以看出,USART在UART基础上增加了同步功能,即USART是UART的增强型。当我们使用USART在异步通信的时候,它与UART没有什么区别,但是用在同步通信的时候,区别就很明显了:同步通信需要时钟来触发数据传输,也就是说USART相对UART的区别之一就是能提供主动时钟。
串口属于终端设备,它的驱动程序并不仅仅是简单的初始化硬件、接收/发送数据,在基本硬件操作的基础上,还增加了很多软件功能,是一个多层次的驱动程序。
2 串口驱动程序层次结构
串口驱动程序层次结构如图2.1所示。简单来说,串口驱动程序层次结构可以分为两层,下层为串口驱动层,它直接与硬件相接触,需要填充一个 struct uart_ops 的结构体。上层为tty层,包括tty核心层及线路规程,它们各自都有一个 ops 结构体,用户空间可以通过tty注册的字符设备节点来访问串口设备。如图2.1所示,涉及到了4个 ops 结构体,层层进行跳转。
下面以Microchip SAMA5D4处理器的UART控制器来进行代码分析。
2.1 串口驱动注册-uart_register_driver
路径: \linux-at91\drivers\tty\serial\atmel_serial.c。对于Atmel平台,是这样来注册串口驱动的,首先分配一个struct uart_driver 简单填充,并调用uart_register_driver 注册到内核中去。
struct uart_driver 中,只是填充了一些名字、设备号等信息,这些都是不涉及底层硬件访问的。
下面看一下完整的 uart_driver 结构。
在struct uart_driver atmel_uart结构体中,有两个成员未被赋值,分别是tty_driver和uart_state。对于tty_driver,代表的是上层,它会在 uart_ register_driver 的过程中赋值。而uart_state ,则代表下层,uart_state也会在uart_ register_driver 的过程中分配空间,但是它里面真正设置硬件相关的东西是 uart_state->uart_port ,这个uart_port 是需要从其它地方调用 uart_add_one_port 来添加的。
下层(串口驱动层)
首先,认识几个结构体。
1) 结构体:uart_state。
路径:\linux-at91\linux-at91\linux-at91\include\linux\serial_core.h
/*
* This is the state information which is persistent across opens.
* 打开时下的状态信息
*/
struct uart_state {
struct tty_port port;
enum uart_pm_state pm_state;
struct circ_buf xmit;
atomic_t refcount;
wait_queue_head_t remove_wait;
struct uart_port *uart_port; // 对应于一个串口设备
};
在注册 driver 时,会根据 uart_driver->nr 来申请 nr 个 uart_state 空间,用来存放驱动所支持的串口(端口)物理信息。
2) 结构体:uart_port
路径:\linux-at91\linux-at91\linux-at91\include\linux\serial_core.h
struct uart_port {
spinlock_t lock; /* port lock */
unsigned long iobase; /* in/out[bwl] io端口基地址(物理) */
unsigned char __iomem *membase; /* read/write[bwl] */
unsigned int (*serial_in)(struct uart_port *, int);
void (*serial_out)(struct uart_port *, int, int);
void (*set_termios)(struct uart_port *,
struct ktermios *new,
struct ktermios *old);
unsigned int (*get_mctrl)(struct uart_port *);
void (*set_mctrl)(struct uart_port *, unsigned int);
int (*startup)(struct uart_port *port);
void (*shutdown)(struct uart_port *port);
void (*throttle)(struct uart_port *port);
void (*unthrottle)(struct uart_port *port);
int (*handle_irq)(struct uart_port *);
void (*pm)(struct uart_port *, unsigned int state,
unsigned int old);
void (*handle_break)(struct uart_port *);
int (*rs485_config)(struct uart_port *,
struct serial_rs485 *rs485);
unsigned int irq; /* irq number */
unsigned long irqflags; /* irq flags */
unsigned int uartclk; /* base uart clock */
unsigned int fifosize; /* tx fifo size */
unsigned char x_char; /* xon/xoff char */
unsigned char regshift; /* reg offset shift */
unsigned char iotype; /* io access style */
unsigned char unused1;
#define UPIO_PORT (SERIAL_IO_PORT) /* 8b I/O port access */
#define UPIO_HUB6 (SERIAL_IO_HUB6) /* Hub6 ISA card */
#define UPIO_MEM (SERIAL_IO_MEM) /* driver-specific */
#define UPIO_MEM32 (SERIAL_IO_MEM32) /* 32b little endian */
#define UPIO_AU (SERIAL_IO_AU) /* Au1x00 and RT288x type IO */
#define UPIO_TSI (SERIAL_IO_TSI) /* Tsi108/109 type IO */
#define UPIO_MEM32BE (SERIAL_IO_MEM32BE) /* 32b big endian */
#define UPIO_MEM16 (SERIAL_IO_MEM16) /* 16b little endian */
unsigned int read_status_mask; /* driver specific */
unsigned int ignore_status_mask; /* driver specific */
struct uart_state *state; /* pointer to parent state */
struct uart_icount icount; /* statistics */
struct console *cons; /* struct console, if any */
#if defined(CONFIG_SERIAL_CORE_CONSOLE) || defined(SUPPORT_SYSRQ)
unsigned long sysrq; /* sysrq timeout */
#endif
。。。
。。。
int hw_stopped; /* sw-assisted CTS flow state */
unsigned int mctrl; /* current modem ctrl settings */
unsigned int timeout; /* character-based timeout */
unsigned int type; /* port type */
const struct uart_ops *ops;
unsigned int custom_divisor;
unsigned int line; /* port index */
unsigned int minor;
resource_size_t mapbase; /* for ioremap */
resource_size_t mapsize;
struct device *dev; /* parent device */
unsigned char hub6; /* this should be in the 8250 driver */
unsigned char suspended;
unsigned char irq_wake;
unsigned char unused[2];
struct attribute_group *attr_group; /* port specific attributes */
const struct attribute_group **tty_groups; /* all attributes (serial core use only) */
struct serial_rs485 rs485;
void *private_data; /* generic platform data pointer */
};
struct uart_port,这个结构体对应于一个串口设备,如果平台有3个串口那么就需要填充3个uart_port ,并且通过 uart_add_one_port 添加到 uart_driver->uart_state->uart_port 中去。当然 uart_driver 有多个 uart_state ,每个 uart_state 有一个 uart_port。在 uart_port 里还有一个非常重要的成员 struct uart_ops *ops ,一般芯片厂家都写好了,只需要稍作修改。
下面是struct uart_ops 结构体。
上层(tty核心层)
tty 核心层要从 uart_register_ driver 来看起,因为 tty_driver 是在注册过程中构建的,顺便了解注册过程。
/**
* uart_register_driver - register a driver with the uart core layer
* @drv: low level driver structure
*
* Register a uart driver with the core driver. We in turn register
* with the tty layer, and initialise the core driver per-port state.
*
* We have a proc file in /proc/tty/driver which is named after the
* normal driver.
*
* drv->port should be NULL, and the per-port structures should be
* registered using uart_add_one_port after this call has succeeded.
*/
int uart_register_driver(struct uart_driver *drv)
{
struct tty_driver *normal;
int i, retval;
BUG_ON(drv->state);
/*
* Maybe we should be using a slab cache for this, especially if
* we have a large number of ports to handle.
*/
/* 根据driver支持的最大设备数,申请n个 uart_state 空间,每一个 uart_state 都有一个uart_port */
drv->state = kzalloc(sizeof(struct uart_state) * drv->nr, GFP_KERNEL);
if (!drv->state)
goto out;
/* tty层:分配一个 tty_driver ,并将drv->tty_driver 指向它 */
normal = alloc_tty_driver(drv->nr);
if (!normal)
goto out_kfree;
drv->tty_driver = normal;
/* 对 tty_driver 进行设置 */
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_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_port_init(port);
port->ops = &uart_port_ops;
//port->close_delay = HZ / 2; /* .5 seconds */
//port->closing_wait = 30 * HZ;/* 30 seconds */
}
/* tty层:注册 driver->tty_driver */
retval = tty_register_driver(normal);
if (retval >= 0)
return retval;
for (i = 0; i < drv->nr; i++)
tty_port_destroy(&drv->state[i].port);
put_tty_driver(normal);
out_kfree:
kfree(drv->state);
out:
return -ENOMEM;
}
uart_register_ driver 注册过程干了哪些事:
1)根据driver支持的最大设备数,申请n个 uart_state 空间,每一个 uart_state 都有一个 uart_port ;
2)分配一个 tty_driver ,并将drv->tty_driver 指向它;
3)对 tty_driver 进行设置,其中包括默认波特率、校验方式等,还有一个重要的ops ,uart_ops ,它是tty核心与串口驱动通信的接口;
4)初始化每一个 uart_state ;
5)注册 tty_driver 。
注册 uart_driver 实际上是注册 tty_driver,因此与用户空间打交道的工作完全交给了 tty_driver ,而且这一部分都是内核实现好的,基本不需要修改,了解一下工作原理即可。
这里介绍下uart_ops结构体。
static const struct tty_operations uart_ops = {
.open = uart_open,
.close = uart_close,
.write = uart_write,
.put_char = uart_put_char,
.flush_chars = uart_flush_chars,
.write_room = uart_write_room,
.chars_in_buffer= uart_chars_in_buffer,
.flush_buffer = uart_flush_buffer,
.ioctl = uart_ioctl,
.throttle = uart_throttle,
.unthrottle = uart_unthrottle,
.send_xchar = uart_send_xchar,
.set_termios = uart_set_termios,
.set_ldisc = uart_set_ldisc,
.stop = uart_stop,
.start = uart_start,
.hangup = uart_hangup,
.break_ctl = uart_break_ctl,
.wait_until_sent= uart_wait_until_sent,
#ifdef CONFIG_PROC_FS
.proc_fops = &uart_proc_fops,
#endif
.tiocmget = uart_tiocmget,
.tiocmset = uart_tiocmset,
.get_icount = uart_get_icount,
#ifdef CONFIG_CONSOLE_POLL
.poll_init = uart_poll_init,
.poll_get_char = uart_poll_get_char,
.poll_put_char = uart_poll_put_char,
#endif
};
这个是 tty 核心层的 ops ,简单一看,后面分析调用关系时,我们再来看具体的里边的函数。
下面来看 tty_driver 的注册,tty_register_driver。
/*
* Called by a tty driver to register itself.
*/
int tty_register_driver(struct tty_driver *driver)
{
int error;
int i;
dev_t dev;
struct device *d;
/* 如果没有主设备号则申请 */
if (!driver->major) {
error = alloc_chrdev_region(&dev, driver->minor_start,
driver->num, driver->name);
if (!error) {
driver->major = MAJOR(dev);
driver->minor_start = MINOR(dev);
}
} else {
dev = MKDEV(driver->major, driver->minor_start);
error = register_chrdev_region(dev, driver->num, driver->name);
}
if (error < 0)
goto err;
/* 创建字符设备 ,使用 tty_fops */
if (driver->flags & TTY_DRIVER_DYNAMIC_ALLOC) {
error = tty_cdev_add(driver, dev, 0, driver->num);
if (error)
goto err_unreg_char;
}
mutex_lock(&tty_mutex);
/* 将该 driver->tty_drivers 添加到全局链表 tty_drivers */
list_add(&driver->tty_drivers, &tty_drivers);
mutex_unlock(&tty_mutex);
if (!(driver->flags & TTY_DRIVER_DYNAMIC_DEV)) {
for (i = 0; i < driver->num; i++) {
d = tty_register_device(driver, i, NULL);
if (IS_ERR(d)) {
error = PTR_ERR(d);
goto err_unreg_devs;
}
}
}
/* proc 文件系统注册driver */
proc_tty_register_driver(driver);
driver->flags |= TTY_DRIVER_INSTALLED;
return 0;
err_unreg_devs:
for (i--; i >= 0; i--)
tty_unregister_device(driver, i);
mutex_lock(&tty_mutex);
list_del(&driver->tty_drivers);
mutex_unlock(&tty_mutex);
err_unreg_char:
unregister_chrdev_region(dev, driver->num);
err:
return error;
}
tty_register_driver注册过程干了哪些事:
1)为线路规程和termios分配空间,并使 tty_driver 相应的成员指向它们;
2)注册字符设备,名字是 uart_driver->name 我们这里是“ttyS”,文件操作函数集是 tty_fops。可查看tty_cdev_add 函数的定义;
3)将该 uart_driver->tty_drivers 添加到全局链表 tty_drivers ;
4)向 proc 文件系统添加 driver ,这个暂时不了解。
下面是结构体 tty_fops。
至此,文章起初的结构图中的4个ops已经出现了3个,另一个关于线路规程的在哪?下面来看一下调用关系。
调用关系
tty_driver注册了一个字符设备(/dev/ttyS0),从它的 tty_fops 入手,可以得知用户空间是如何访问到最底层的硬件操作函数。以 open、read、write 为例,用户空间 open 时将调用到 uart_port.ops.startup ,在用户空间 write 则调用 uart_port.ops.start_tx。这些内核都已经实现好,在驱动开发过程中几乎不涉及这些代码的修改移植工作。图2.2为串口读写函数调用关系。
2.2 平台驱动注册-platform _driver_register
目前Linux2.6版本以后的ARM 结构使用设备树(DTS,Device Tree Source)来分管设备,DeviceTree是一种描述硬件的数据结构,为什么要引入DTS?这是因为在Linux2.6中,ARM架构的板极硬件细节过多地被硬编码在arch/arm/plat-xxx和arch/arm/mach-xxx,比如板上的platform设备、resource、i2c_board_info、spi_board_info以及各种硬件的platform_data,这些板级细节代码对内核来讲只不过是垃圾代码。而采用DeviceTree后,许多硬件的细节可以直接透过它传递给Linux,而不再需要在kernel中进行大量的冗余编码。
platform_driver的入口函数,仍采用platform_driver_register注册。platform_driver_register用来枚举名称为 “atmel_usart” 的平台设备,只要内核中有相同名称的平台设备,platform_driver_register函数就会调用atmel_serial_driver 中的atmel_serial_probe函数来枚举它。
函数:platform_driver_register
结构体:atmel_serial_driver
看结构体参数 atmel_serial_driver 的定义,该结构体一个很重要的成员是 .of_match_table = of_match_ptr(atmel_serial_dt_ids), of_match_ptr函数用于将atmel_serial_dt_ids 中.compatible 属性与.dts 文件中描述的设备节点.compatible 属性进行匹配,且不区分大小写,当完成配对之后,就调用atmel_serial_probe函数完成驱动注册的最后工作。
结构体:atmel_serial_dt_ids
设备树
Dtsi和dts同为设备树文件,dtsi为被包含文件,可被dts或者dtsi文件包含。
设备树文件sama5d4.dtsi中对串口设备节点的描述。路径:\linux-at91\arch\arm\boot\dts\sama5d4.dtsi。
sama5d4.dtsi中描述了7个串口设备节点,2个uart,5个usart。
(别名)
(节点描述)描述了节点的一些属性。
(引脚定义)
设备树文件at91-sama5d4_xplained.dts中对串口设备节点的描述。路径:\linux-at91\arch\arm\boot\dts\at91-sama5d4_xplained.dts。
3 串口移植
3.1 查看板卡的串口引脚
阅读A5处理器资料,查看UART章节和USART章节对串口的相关描述,根据硬件原理图确定串口的引脚定义。
3.2 修改情况
1) 设备树文件,到sama5d4.dtsi 文件下去搜索 USART ,对应找到相应的串口节点以及串口的引脚定义部分。sama5d4.dtsi 文件不需要进行修改。
节点描述
引脚定义
2) 设备树文件,查看 at91-sama5d4_xplained.dts 文件中的串口USART,在此文件下对节点进行相应参数修改。
—原来的串口节点信息如下:原始状态使能了uart0但我们没有接其引脚使用,使能了usart3的收发功能,使能了usart4但不具备收发功能,没有使能usart2。
—修改后的串口节点信息如下:
3) 驱动文件,在\linux-at91\drivers\tty\serial\atmel_serial.c文件中,查找static int __init atmel_serial_init(void)函数,找到uart_register_driver()函数,转到该函数定义,函数定义在 serial_core.c 文件中,文件中修改波特率,。原始波特率9600 ,可按自己的需求修改波特率,我们这里将其改为115200。
到此,串口驱动移植完成,重新编译驱动文件,包括设备树文件和驱动文件,烧录至开发板,测试串口是否可以正常使用。
4 串口收发功能测试
将编译成功的内核镜像和设备树文件编译到开发板中,然后使用超级终端接开发板查看打印的日志。
串口收发功能测试步骤如下:
1) 查看tty设备。
(1)编译运行内核,如果UART驱动加载成功会在/dev目录下产生相应UART设备节点; 名称为:ttySx。这里会产生 ttyS0, ttyS1, ttyS2, ttyS5(没有使用)。
ttyS0对应USART3,为DEGB口;
ttyS1对应 USART4;
ttyS2对应 USART2;
ttyS5 对应UART0;
(2)运行命令: $ cat /proc/tty/driver/atmel_serial ,可以查看显示设备节点详细信息,其中通过地址可以对照设备节点;
root@sama5d4-xplained:/proc/tty/driver# cat atmel_serial
serinfo:1.0 driver revision:
0: uart:ATMEL_SERIAL mmio:0xFC00C000 irq:34 tx:2176533 rx:2142522 RTS|CTS|DTR|DSR|CD|RI
1: uart:ATMEL_SERIAL mmio:0xFC010000 irq:35 tx:1253 rx:68 brk:68 CTS|DSR|CD|RI
2: uart:ATMEL_SERIAL mmio:0xFC008000 irq:33 tx:6 rx:0 DSR|CD|RI
如果UART设备节点未产生,可在其相应驱动程序xx_probe函数中添加打印,查看xx_probe函数是否被调用,进一步查找原因。
2) 通过跳冒选择串口。
下图中,两个黑色串口共用一个芯片。左边USART3和USART4共用一个串口,通过跳冒来区分,右边USART2单独使用一个串口。
跳线冒J3用于设置选择DEBG- USART3和串口4,电路图如下图4.2。
具体的跳线设置如下表4.1和表4.2所示。
表4.1 DEGU模式跳线状态
3) 如果成功产生了UART设备节点,下面进行串口收发测试;
方法1:
可通过软件回环测试确认UART驱动程序功能是否正常。编写测试程序,将串口2、3引脚短接。读写数据。如果管脚信号测试通过,则串口功能基本调试成功。此方法的优点是无需上位机串口助手的配合,在串口模块到位之前提前完成接口调试工作。
方法2:
Echo 命令回显测试;
在超级终端下进入 /dev 目录下,以测试USART2为例,输入命令: echo test > ttyS2.在串口助手中查看收到的信息,这一步测试串口的发。在超级终端下输入命令: cat ttyS2, 在串口助手中发送clear,在超级终端下查看收到信息,这一步查看串口的收。由于USART2不是调试口,无法打印系统信息,需要使用SSH连接开发板。具体的测试如下图所示。
方法3: 使用系统集成的串口调试工具 busybox microcom
1) busybox microcom工具的使用;
命令(busybox microcom)使用方法很简单:
Usage: microcom [-d DELAY] [-t TIMEOUT] [-s SPEED] [-X] TTY
参数如下:
-d 表示延时时间,一般我都不设置。
-t 表示超时时间,超多少时间就自动退出。单位为ms
-s 表示传输速度,波特率的意思,这个根据自己的情况而定。
-X 不加
最后指定你的串口设备。如 /dev/ttyO0 , 这是TI的串口设备节点
2)测试方式如下:
将要测试串口与pc端连接,在pc端开启串口调试工具,波特率设定跟等下microcom设定一样。
在产品端运行如下命令:
microcom -s 115200 /dev/ttyS2
在超级终端命令行输出信息,在PC是否有正确显示。反之也测试一下。