W83697/W83977 super I/O串口驱动开发
内容简介: 介绍了Linux下的串口驱动的设计层次及接口, 并指出串口与TTY终端之间的关联层次(串口可作TTY终端使用), 以及Linux下的中断处理机制/中断共享机制, 还有串口缓冲机制当中涉及的软中断机制; 其中有关w83697/w83977 IC方面的知识, 具体参考相关手册, 对串口的配置寄存器有详细介绍, 本文不再进行说明.
目录索引:
一. Linux的串口接口及层次.
二. Linux的中断机制及中断共享机制.
三. Linux的软中断机制.
四. TTY与串口的具体关联.
一. Linux的串口接口及层次.
串口是使用已经非常广的设备了, 因此在linux下面的支持已经很完善了, 具有统一的编程接口, 驱动开发者所要完整的工作就是针对不同的串口IC来做完成相应的配置宏, 这此配置宏包括读与写, 中断打开与关闭(如传送与接收中断), 接收状态处理, 有FIFO时还要处理FIFO的状态. 如下我们就首先切入这一部分, 具体了解一下与硬件串口IC相关的部分在驱动中的处理, 这一部分可以说是串口驱动中的最基础部分, 直接与硬件打交道, 完成最底层具体的串口数据传输.
1. 串口硬件资源的处理.
W83697及W83977在ep93xx板子上的映射的硬件物理空间如下:
W83697: 0x20000000起1K空间.
W83977: 0x30000000起1K空间.
因为串口设备的特殊性, 可以当作终端使用, 但是终端的使用在内核还未完全初始化之前(关于串口与终端的关联及层次在第四节中详细), 此时还没有通过mem_init()建立内核的虚存管理机制, 所以不能通过ioreamp来进行物理内存到虚存的映射(物理内存必须由内核映射成系统管理的虚拟内存后才能进行读写访问), 这与先前所讲的frAMEbuffer的物理内存映射是不同的, 具体原因如下:
√终端在注册并使用的调用路径如下:
STart_kernel→cONsole_init→uart_console_init→ep93xxuart_console_init→register_console→csambuart_console_write.
√FrameBuffer显卡驱动中的物理内存映射调用路径如下:
start_kernel→ rest_init→init(内核初始线程)→ do_basic_setup→ do_initcalls→fbmem_init→lanrryfb_init
(Linux下用__setup启动初期初始机制与__initcall系统初始化完成后的调用机制, 这两个机制本质没有什么差别,主要是执行时所处的系统时段)
√串口物理内存映射到虚存的时机:
依据上面所介绍的两条执行路径,再看内核的内存初始化的调用时期,只有完成这个初始化后才能进行物理内存到虚存的映射,内存的初始化主要是在start_kernel中调用的mem_init,这个调用明显在uart_console_init之后,在fbmem_init之后,到此就全部说明了为何不能在对串口使用ioremap进行物理内存的映射了。那么究竟要在什么时机用什么方法进行串口物理内存的映射呢?
√串口物理内存的映射方式:
参考ep93xx的板载I/O的映射处理,它的处理方式是一次性将所有的物理I/O所在的内存空间映射到虚存空间,映射的基址是IO_BASE_VIRT,大小是IO_SIZE.
/* Where in virtual memory the IO devices (TImers, system controllers
* and so on). This gets used in arch/ARM/mach-ep93xx/mm.c.*/
#define IO_BASE_VIRT 0xFF000000 // Virtual addrESS of IO
#define IO_BASE_PHYS 0x80000000 // Physical address of IO
#define IO_SIZE 0x00A00000 // How much?
完成映射的函数是ep93xx_map_io, 所有要进行映射内存都在ep93xx_io_desc结构当中描述,我们的串口映射也加在这个地方,基址分别如下:
文件: linux-2.4.21/include/asm-arm/arch-ep93xx/regmap.h
#define IO_W83697_UART_BASE 0x20000000
#define IO_W83697_UART_SIZE 0x1000
#define IO_W83977_UART_BASE 0x30000000
#define IO_W83977_UART_SIZE 0x1000
#define IO_SIZE_2 (IO_SIZE+0x100000)
#define IO_BASE83697_VIRT IO_BASE_VIRT+IO_SIZE
#define IO_BASE83977_VIRT IO_BASE_VIRT+IO_SIZE_2
ep93xx_map_io完成是在arch初始化中赋值给struct machine_desc mdesc这个机器描述结构体,主要由位于mach-ep93xxarch.c文件中如下宏完成此结构的初始化:
MACHINE_START(EDB9302, "edb9302")
…..
MAPIO(ep93xx_map_io) //初始化. map_io= ep93xx_map_io….
MACHINE_END
最终这个函数在调用路径如下:
start_kernel→setup_arch→paging_init→(mdesc->map_io())
至此完成串口物理内存的映射,这个过程在console_init调用之前,因此不会有问题, 此种方法建立映射是直接创建物理内存页与虚存页的对应,大小为4k一页,最终调用的是create_mapping(), 建立页表映射是与具体的平台相关的,位于mach_ep93xx/mm/ proc-arm920.S文件中提供了与具体平台相关的页表建立函数,其中包括TLB表操作/Cache操作/页表操作等:
在上层的start_kernel→setup_arch→ setup_processor调用下,会在proc-arm920.S文件中查找".proc.info"节的__arm920_proc_info,并从中找到配置的process相关的操作函数,具体的arm页表建立的详情须要参看ARM内存管理的相关手册.
.section ".proc.info", #alloc, #execinstr
.type __arm920_proc_info,#object
__arm920_proc_info:
.long 0x41009200
……
.long arm920_processor_functions
.size __arm920_proc_info, . - __arm920_proc_info
在arm920_processor_functions中包含的页表操作如下:
/* pgtable */
.word CPU_arm920_set_pgd
.word cpu_arm920_set_pmd
.word cpu_arm920_set_pte
2. 与串口硬件相关的宏主.
如下, 下面将详术如下, 并指出其具体被使用的环境上下文:
<1>. 读写数据.
#define UART_GET_CHAR(p) ((readb((p)->membase + W83697_UARTDR)) & 0xff)
#define UART_PUT_CHAR(p, c) writeb((c), (p)->membase + W83697_UARTDR)
<2>. 接收发送状态.
#define UART_GET_RSR(p) ((readb((p)->membase + W83697_UARTRSR)) & 0xff)
#define UART_PUT_RSR(p, c) writeb((c), (p)->membase + W83697_UARTRSR)
<3>. 发送及接收中断状态.
#define UART_GET_CR(p) ((readb((p)->membase + W83697_UARTCR)) & 0xff)
#define UART_PUT_CR(p,c) writeb((c), (p)->membase + W83697_UARTCR)
#define UART_GET_INT_STATUS(p) ((readb((p)->membase + W83697_UARTIIR)) & 0xff)
<4>. 以及其它的中断使能设置等, 在传送时打开传送中断即会产生传送中断.
#define UART_PUT_ICR(p, c) writeb((c), (p)->membase + W83697_UARTICR)
<5>. FIFO的状态, 是否读空/是否写满; 每次读时必须读至FIFO空, 写时必须等到FIFO不满时才能写(要等硬件传送完) .
接收中断读空FIFO的判断:
status = UART_GET_FR(port);
while (UART_RX_DATA(status) && max_count--) {
……
}
发送中断写FIFO: 当发送缓冲区中有数据要传送时, 置发送中断使能, 随后即产生传送中断, 此时FIFO为空, 传送半个FIFO大小的字节, 如果发送缓冲区数据传完,则关闭发送中断.
<6>. 传送时可直接写串口数据口, 而不使用中断, 但必须等待检测FIFO的状态
do {
status = UART_GET_FR(port);
} while (!UART_TX_READY(status)); //wait for tx buffer not full...
3. 串口驱动的参数配置
串口的参数主要包括如下几个参数,全部都记录在uart_port结构上,为静态的赋值,本串口驱动支持6个设备,所以驱动中就包括了6个port,一个串口对应一个port口,他们之间除了对应的中断号/寄存器起始基址/次设备号不同之外,其它的参数基本相同.
√串口对应中断, 这里六个串口,其中有3个串口使用的系统外部中断0/1/2, 其中另外几个中断用提GPIO中断,具体有关GPIO中断的内容可参见EP93XX芯片手册, GPIO中断共享一个系统中断向量,涉及中断共享的问题,后面将详述LINUX中的中断共享支持.
√串口时钟, 串口时钟用来转换计算须要设置到配置寄存器当中的波特率比值,其计算方法为:quot = (port->uartclk / (16 * baud)); baud为当前设置的波特率,可为115200等值, 取决于所选的串口时钟源, quot即为要设置到寄存器当中的比值.
√串口基址, 即串口所有配置寄存器基础址.
√串口次设备号(由驱动中的最低次设备号依次累加)
前面已经讲过了六个串口中断,这里详细列出对应情况如下,方便查找:
w83697的三个串口对应中断如下:
uart 1: 读写数据寄存器偏移为00x3F8, 对应系统外部中断INT_EXT[0].
uart 2: 读写数据寄存器偏移为00x2F8, 对应系统外部中断INT_EXT[1].
uart 3: 读写数据寄存器偏移为00x3e8, 对应系统外部中断INT_EXT[2].
uart 4: 读写数据寄存器偏移为00x3e8, 对应EGPIO[8].
w83977的两个串口对应中断如下:
uart 1: 读写数据寄存器偏移为00x3F8, 对应EGPIO[1].
uart 2: 读写数据寄存器偏移为00x2F8, 对应EGPIO[2].
下面列出其中一个具体的串口port的定义如下:
{
.port = {
.membase = (void *)W83697_UART4_BASE,
.mapbase = W83697_UART4_BASE,
.iotype = SERIAL_IO_MEM,
.irq = W83697_IRQ_UART4, //串口中断号
.uartclk = 1846100, //uart时钟,默认.
.fifosize = 8, //硬件fifo大小.
.ops = &amba_pops, //底层驱动的硬件操作集,如开关中断等.
.flags = ASYNC_BOOT_AUTOCONF,
.line = 3, //串口在次设备数组中的索引号,须注意从0计起…
},
.dtr_mask = 0,
.rts_mask = 0,
}
4. 串口驱动的底层接口函数
驱动文件:linux-2.4.21/drivers/serial/Ep93xx_w83697.c
相关文件: linux-2.4.21/drivers/serial/core.c 下面详述.
函数: w83697uart_rx_chars(struct uart_port *port, struct pt_regs *regs)
描述: 串口接收数据中断, 此函数中应当注意的要点如下:
接收数据时,要注意判断FIFO是否读空(参见上述2点中说明).
接收数据放入flip缓冲区,此缓冲区专供缓存中断中接收到的数据,是最原始的串口数据,为更上一层中各种终端处理模式的原始数据,可以进行各种加工处理。
接收数据到flip缓冲区中时,须根据硬件接收状态,置每一个接收到的字符的接收标志,放在flag_buf_ptr当中, 标志类型有TTY_NORMAL/ TTY_PARITY/ TTY_FRAME等,分别表示正常/校验出错/帧出错(无停止位)等.
每接收数据之后,会通过在退出中断前调用tty_flip_buffer_push()来往tq_timer任务列表中加一个队列任务,串口的队列任务主要是负责将中断接收到flip缓冲区中的数据往上传输至终端终冲区, 队列任务的机制将在后面介绍,它是一种异步执行机制,在软中断中触发执行.
函数: static void w83697uart_tx_chars(struct uart_port *port)
描述: 串口发送数据中断, 发送中断中要做的事比较少,比起接收中断简单了好多,注意事项如下:
当上层要发送数据时,就会打开串口发送中断,此时FIFO为空,传送半个FIFO大小数据即退出, 通常打开中断是通过更上一层的uart_flush_chars()调用,最终调用的是w83697uart_start_tx().
检测当没有数据要传输的时候,关闭传送中断,在传送之前与传送完之后都有检测.
最重要的一点是如果传送缓冲区当中的字符数已经小于WAKEUP_CHARS, 则可以唤醒当前正在使用串口进行传送的进程,这里是通过tasklet机制来完成,这也是一异步执行机制.
顺带介绍开关中断接口:
static void w83697uart_start_tx(struct uart_port *port, unsigned int tty_start)
static void w83697uart_stop_tx(struct uart_port *port, unsigned int tty_stop)
函数: static void w83697uart_int(int irq, void *dev_id, struct pt_regs *regs)
描述: 中断处理函数,为3个使用系统外部中断的的串口的中断入口,其中必须处理的中断状态分为如下几种, 注意必须在处理中断时根据手册中的说明来清除中断,通常是读或写某些寄存器即可。
接收中断.
传送中断.
FIFO超时中断.
其它不具体处理的中断,必须读相应寄存器清中断.
函数: static void w83697uart_int2(int irq, void *dev_id, struct pt_regs *regs)
描述: 中断处理函数,为另外几个使用串口使用的GPIO中断入口,GPIO中断共享同一个系统中断向量, 必须根据GPIO的中断状态寄存器的相应位来判断对应的中断是属哪一个串口的,从而进行相应的处理,其实这个判断也是无所谓的,因为中断产生时传进来的参数已经含有了相应串口的参数, 在判断完中断产生的GPIO口后立即调用w83697uart_int2 完成具体的中断处理.
函数: static int w83697uart_startup(struct uart_port *port)
描述: 串口开启后的初始化函数,主要完成初始化配置,以及安装中断处理了函数,初始化配置包括打开中断使能标志。
函数: static void w83697uart_shutdown(struct uart_port *port)
描述: 串口关闭函数,清除配置,半闭中断.
函数: static void w83697uart_change_speed(struct uart_port *port, unsigned int cflag, unsigned int iflag, unsigned int quot)
描述: 配置函数,经由上次调用下来,主要配制串口的波特率比,以及各种容错处理,在串口打开初始化时会被调用,在必变串口波特率/校验方式/停止位/传送位数等参数时会被调用.
5. 串口驱动与上层的接口关联
文件: linux-2.4.21/drivers/serial/core.c
这一层接口是串口驱动中的共用部分代码, 核心结构为struct uart_driver. 这一层上承TTY终端,下启串口底层,串口底层主要处理了与串口硬件相关的部分,并向上提供uart中间层向下的接口. Uart coar向下与底层驱动的接口,通过一个static struct uart_ops amba_pops结构完成? 这个结构直接赋值给串口struct uart_amba_port amba_ports 的.ops成员,最后将串口的port加入到uart_driver当中完成关联, 通过uart_add_one_port加入.
static int __init w83697uart_init(void)
{
int ret, i;
ret = uart_register_driver(&amba_reg);
if (ret == 0) {
for (i = 0; i < UART_NR; i++)
uart_add_one_port(&amba_reg, &amba_ports[i].port);
}
return ret;
}
二. Linux的中断机制及中断共享机制.
前面讲到了有6个串口,除了w83697中的前三个串使用的是独立的系统外部中断之外,其它的在个串口是共享一个系统中断向量的,现在我们来看看多个中断是如何挂在一个系统中断向量表当中的,共享中断到底是什么样的一种机制?
进行分析代码可知,linux下的中断采用的是中断向量的方式,每一个中断对应一个中断描述数组当中的一项, 结构为struct irqdesc,其当中对应一成员结构为struct irqactionr 的成员action, 这个即表示此中断向量对应的中断处理动作,这里引用从网上下载的一幅图讲明中断向量表与中断动作之间的关系:
struct irqaction {
void (*handler)(int, void *, struct pt_regs *);
unsigned long flags;
unsigned long mask;
const char *name;
void *dev_id;
struct irqaction *next;
};
从上面的结构体与图当中,我们就可以很清楚的看到,一个中断向量表可以对应一个irqaction,也可能对应多个由链表链在一起的一个链表irqaction, 这当中主要在安装中断的时候通过中断的标志位来决定:
安装中断处理,不可共享:
retval = request_irq(port->irq, w83697uart_int, 0, "w83697_uart3", port);
安装中断处理,可共享:
retval = request_irq(port->irq, w83697uart_int2, SA_SHIRQ, "w83977_uart5", port);
由上即可知,安装共享中断时,只须指定安装的中断标志位flag为SA_SHIRQ, 进入分析安装中断的处理可知,在安装时,会检测已经安装的中断是否支持共享中断,如果不支持,则新的中断安装动作失败;如果已经安装的中断支持共享中断,则还必须检测将要安装的新中断是否支持中断共享,如果不支持则安装还是会失败,如果支持则将此新的中断处理链接到此中断向量对应的中断动作处理链表当中.
在产生中断时,共享中断向量中对应的中断处理程序链表中的每一个都会被调用,依据链表的次序来,这样处理虽然会有影响到效率,但是一般情况下中断传到用户的中断处理服务程序中时,由用户根据硬件的状态来决定是否处理中断,所以能常情况下都是立即就返回了,效率的影响不会是大的问题.
三. Linux的软中断机制.
前面已经简单讲过了LINUX下的硬中断处理机制,其实硬中断的处理都由LINUX底层代码具体完成了,使用者一般在处理硬中断时是相当简单的,只须要用request_irq()简单的挂上中断即可,这里我们进一步介绍一下LINUX下的软中断机制,软中断机制相比起硬中断机制稍微复杂一些,而且在LINUX内核本身应用非常的广, 它作为一种软性的异步执行机制,只有深入理解了它才能灵活的运用.
之所以提到内核的softirq机制,主要是因为在串口中断也使用了这些机制,理解了这些机制就能更加明白串口驱动一些问题, 现在先提出几个问题如下:
前面提供到中断接收后数据,先放到flip缓冲区当中,这样让人很容易进一步想知道,中断处理的缓冲区的数据,用户进程读取串口时如何读到的?很明显中断处于内核空间,用户读取串口输入进程是在用户空间,中断缓冲区中的数据如何被处理到终端缓冲区中,供用户读取的?