之前学习的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参考手册
文章图片
刚开始看的时候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串口的经验中),看看参考手册吧
文章图片
主要是对接收数据的说明和开启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的描述
文章图片
这里说的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中完成。