ns16550串口驱动

之前学习的STM32F103的串口时,特点是:没有FIFO、参考手册写的很棒。
这里主要介绍一下a20的串口模块,之前把printf重定向分析了个大概,没有具体到ns16550的驱动,今天还是网上找了一些资料。发现a20貌似采用的是SISDA-16550-UART的IP参考手册地址:http://www.cs.indiana.edu/classes/b649-sjoh/reading/SISDA-16550-UART.pdf,参考手册的最后还给出了怎么与arm架构、AVR架构集成的例子,再次想起来前面分析的a20串口驱动是移植的ns16550串口驱动,a20很可能集成的就是SISDA-16550-UART。
所以这里就分析一下NS16550的驱动,分析过程中会与STM32F103的串口模块对比。
从大的方面说起,ns16550模块存在FIFO、MODEM,这些是stm32f103不存在的。与stm32共同的特点是中断、DMA。
再分析一下寄存器结构,a20参考手册

ns16550串口驱动
文章图片


刚开始看的时候UART_RBR、UART_THR、UART_DLL的偏移地址竟然都是0x00,再看看代码中如何定义结构体

struct NS16550 { UART_REG(rbr); /* 0 */ UART_REG(ier); /* 1 */ UART_REG(fcr); /* 2 */ UART_REG(lcr); /* 3 */ UART_REG(mcr); /* 4 */ UART_REG(lsr); /* 5 */ UART_REG(msr); /* 6 */ UART_REG(spr); /* 7 */ #ifdef CONFIG_SOC_DA8XX UART_REG(reg8); /* 8 */ UART_REG(reg9); /* 9 */ UART_REG(revid1); /* A */ UART_REG(revid2); /* B */ UART_REG(pwr_mgmt); /* C */ UART_REG(mdr1); /* D */ #else UART_REG(mdr1); /* 8 */ UART_REG(reg9); /* 9 */ UART_REG(regA); /* A */ UART_REG(regB); /* B */ UART_REG(regC); /* C */ UART_REG(regD); /* D */ UART_REG(regE); /* E */ UART_REG(uasr); /* F */ UART_REG(scr); /* 10*/ UART_REG(ssr); /* 11*/ UART_REG(reg12); /* 12*/ UART_REG(osc_12m_sel); /* 13*/ #endif };

可以看出结构体根本没有对UART_THR、UART_DLL的定义,其实紧接着就是对偏移地址重复的寄存器定义


#define thr rbr #define iir fcr #define dll rbr #define dlm ier

寄存器定义也找到了,那么问题来了,由于收发寄存器偏移地址一样,怎么读写呢?想写的时候它到底是发送寄存器还是接受寄存器?(没办法,新手想的就是这,还停留在STM32F103串口的经验中),看看参考手册吧
ns16550串口驱动
文章图片

主要是对接收数据的说明和开启FIFO的介绍,刚开始看的时候,理解为:只有LCR的DR位被置1的时候,读RBR才有意义。所以想着直接设置LCR的DR位为1然后就能读RBR。这个想法是把UART_RBR、UART_THR、UART_DLL看做是BANK,就像内存BANK,通过高位来选择片选从而映射到不同内存BANK的地址。怎么验证呢?先看看手册吧,看了LCR寄存器的描述之后更郁闷了,压根没有DR位的定义。无语了,再来看看收发函数的代码吧

char NS16550_getc(NS16550_t com_port) { while ((serial_in(&com_port->lsr) & UART_LSR_DR) == 0) { #if !defined(CONFIG_SPL_BUILD) && defined(CONFIG_USB_TTY) extern void usbtty_poll(void); usbtty_poll(); #endif WATCHDOG_RESET(); } return serial_in(&com_port->rbr); }

我去,无语了,while ((serial_in(&com_port->lsr) & UART_LSR_DR) == 0)。怎么是LSR,再看看参考手册中LSR的描述,果然有DR位的描述,无语了,手册错误了。
【ns16550串口驱动】OK继续,while ((serial_in(&com_port->lsr) & UART_LSR_DR) == 0)的意思是UART_LSR_DR为0就一直循环,而且循环体里根本没有对UART_LSR_DR位的操作,无语了,解释为由硬件逻辑自动置1吗?原来理解的直接设置LCR的DR位为1然后就能读RBR的想法是错误的。这里再次响起STM32F103的参考手册中的约定,有直接置1、硬件逻辑自动置1、通过写a位来置1b位(虽然写a位,单a位不存在是否置的意义)、通过写入数据序列来置1某位。这就是参考手册为什么会有版本号的原因。
继续吧,同理可以看一下THR寄存器的描述,跟RBR寄存器的描述差不多一样。再看看DLL的描述
ns16550串口驱动
文章图片


这里说的LCR的DLAB位也是自动置1吗?还是看看代码吧(一般在串口的初始化中设置时钟,所以看初始化代码)

void NS16550_init(NS16550_t com_port, int baud_divisor) { #if (defined(CONFIG_SPL_BUILD) && defined(CONFIG_OMAP34XX)) /* * On some OMAP3 devices when UART3 is configured for boot mode before * SPL starts only THRE bit is set. We have to empty the transmitter * before initialization starts. */ if ((serial_in(&com_port->lsr) & (UART_LSR_TEMT | UART_LSR_THRE)) == UART_LSR_THRE) { serial_out(UART_LCR_DLAB, &com_port->lcr); serial_out(baud_divisor & 0xff, &com_port->dll); serial_out((baud_divisor >> 8) & 0xff, &com_port->dlm); serial_out(UART_LCRVAL, &com_port->lcr); serial_out(0, &com_port->mdr1); } #endif while (!(serial_in(&com_port->lsr) & UART_LSR_TEMT)) ; serial_out(CONFIG_SYS_NS16550_IER, &com_port->ier); #if defined(CONFIG_OMAP) || defined(CONFIG_AM33XX) || \ defined(CONFIG_TI81XX) || defined(CONFIG_AM43XX) serial_out(0x7, &com_port->mdr1); /* mode select reset TL16C750*/ #endif serial_out(UART_LCR_BKSE | UART_LCRVAL, &com_port->lcr); serial_out(0, &com_port->dll); serial_out(0, &com_port->dlm); serial_out(UART_LCRVAL, &com_port->lcr); serial_out(UART_MCRVAL, &com_port->mcr); serial_out(UART_FCRVAL, &com_port->fcr); serial_out(UART_LCR_BKSE | UART_LCRVAL, &com_port->lcr); serial_out(baud_divisor & 0xff, &com_port->dll); serial_out((baud_divisor >> 8) & 0xff, &com_port->dlm); serial_out(UART_LCRVAL, &com_port->lcr); #if (defined(CONFIG_OMAP) && !defined(CONFIG_OMAP3_ZOOM2)) || \ defined(CONFIG_AM33XX) || defined(CONFIG_SOC_DA8XX) || \ defined(CONFIG_TI81XX) || defined(CONFIG_AM43XX) /* /16 is proper to hit 115200 with 48MHz */ serial_out(0, &com_port->mdr1); #endif /* CONFIG_OMAP */ }

其中与LCR有关的是serial_out(UART_LCR_BKSE | UART_LCRVAL, &com_port->lcr); 其中#define UART_LCR_BKSE 0x80 /* Bank select enable */
很明白对LCR的DLAB位置1,但是为什么是UART_LCR_BKSE,而且#define UART_LCR_DLAB0x80 /* Divisor latch access bit *。想说既然UART_LCR_BKSE与UART_LCR_DLAB相等,而且此处的意思好像就是设置DLAB位(因为下面有两句serial_out(0, &com_port->dll); serial_out(0, &com_port->dlm); 很符合参考手册上说的:DLAB位置1之后才能设置DLL)。总结一下就是LCR的DLAB位要软件置1。


上面是uart寄存器结构体的理解。再来看看ns16550.c文件吧,一共5个函数。
void NS16550_init(NS16550_t com_port, int baud_divisor)

void NS16550_reinit(NS16550_t com_port, int baud_divisor)

void NS16550_putc(NS16550_t com_port, char c)

char NS16550_getc(NS16550_t com_port)

int NS16550_tstc(NS16550_t com_port)
虽然不都是叶子函数,但是其调用的子函数WATCHDOG_RESET(); 也难分析,有了刚才对结构体的分析,感觉这几个函数就不用说了。
只想说说刚才的UART_LCR_DLAB与UART_LCR_BKSE,整个项目中两处很值得注意


void uart_init(void) { /* select dll dlh */ writel(UART_LCR_DLAB, UART_LCR(UART)); /* set baudrate */ writel(0, UART_DLH(UART)); writel(BAUD_115200, UART_DLL(UART)); /* set line control */ writel(LC_8_N_1, UART_LCR(UART)); uart_initialized = 1; }

还有void NS16550_init(NS16550_t com_port, int baud_divisor)中,在这两个函数中UART_LCR_DLAB都是为了设置时钟,虽然这两处都不会被编译。
最后还要说一下不要忘了uart的时钟与GPIO配置,这两部分已经在前面分析过的s_init中完成。

    推荐阅读