UART(Universal Asynchronous Receiver/Transmitter),通用异步收发器,是常用于开发调试的RS-232C串口的核心器件。在百度百科(http://baike.baidu.com/view/245027.htm)上可以找到对它的详细介绍。
虽然这段时间实验定时器、键盘、LED操作时都使用了UART串口输出功能,但那是用开发板提供的示例程序中已有的代码的,想要自己写代码正确实现串口读写操作,还不是那么容易的。昨天花了整整一天时间才稍微弄得明白了点,主要是涉及的寄存器和可选操作方式多,包括收发方式、边沿触发中断与电平触发中断、是否使用自动流控制、是否使用FIFO以及FIFO触发级别、换行符转换、退格键的处理等。特别是边沿触发中断和电平触发中断的处理方式,这个问题困扰了我很久,经过很多次失败的尝试,最后才稍微明白点。关于这两种中断触发方式的论述,是本文的重点。
1 TTL与RS-232C
mini2440开发板有3个串口,即TTL0、TTL1和TTL2,其中TTL0还有一个RS-232C形式的物理插座,和PC机上常见的COM口外形相同,用于程序调试。看到TTL,很多人可能会立刻想到Time To Live,即IP分组的存活时间,但这里它显然不是这个意思。用百度搜索了下,就是找不到对TTL串口的解释,不知道这里的TTL是不是Transistor-Transistor Logic(逻辑门电路)的意思,但感觉这个跟串口没有关系。
RS-232C即通常所说的串口的通信标准,是1969年美国电子工业协会(Electronic Industries Association,EIA)建立的串口接口电气信号与电缆连接特性标准,命名为建议标准第232号版本C,即Recommend Standard 232C,简写为EIA-RS-232C,或者更常见的写法是RS-232C。当然,更常见的情况是,少了最后的字母C,写作RS-232,或者RS232。
TTL与RS-232C是有所差别的,最大的差别是:TTL以正电平表示逻辑1,负电平表示逻辑0;而RS-232C是相反的,用-3~-15V的电平表示逻辑1,用+3~+15V的电平表示逻辑0。这样,RS-232C插座就不能直接连接到TTL串口上,因为需要进行电平转换,开发板是用MAX3232芯片进行这种转换的。在http://focus.ti.com.cn/cn/docs/prod/folders/print/max3232.html?DCMP=CN_HPA_BSE&HQS=Interf-494&247SEM可以下载到它的说明书,芯片的名字是多通道RS-232线路驱动器/接收器。
2 相关寄存器
S3C2440A的UART单元对每个串口使用10多个寄存器,3个串口共使用了30多个寄存器。我稍微总结了下(下面的n表示串口编号,取值为0,1,2;寄存器名称中第一个字母U应该是表示UART):
ULCONn:线路控制寄存器,用于设定线路的字长度、停止位个数、奇偶校验方式、是否使用红外模式。(看的书中翻译成“线性控制寄存器”,我感觉是不正确的)
UCONn: 控制寄存器,用于设定操作模式(中断或轮询/DMA)、环回模式、中断方式、时钟选择。
UFCONn:FIFO控制寄存器,用于控制FIFO操作方式,如是否使用FIFO以及触发级别。
UMCONn:Modem控制寄存器,用于设置是否使用AFC(自动流控制)和RTS。TTL2是不支持流控制的,所以没有UMCON2寄存器。
UTRSTATn:收发状态寄存器,可从中读取收发保持寄存器的状态,即是否有数据,仅在非FIFO模式下使用。
UFSTATn:FIFO状态寄存器,可从中读取FIFO状态信息,用于FIFO模式。
UMSTATn:Modem状态寄存器,可从中读取Modem状态,即CTS信号状态。TTL2不支持流控制,所以没有UMSTAT2寄存器。
UERSTATn:错误状态寄存器,可从中读取接收错误状态。
UTXHn和URXHn:收发保持(对非FIFO模式)和缓冲(对FIFO模式)寄存器,用于收发数据。
UBRDIV:波特率除数寄存器,用于设定串口通信波特率。
关于Modem:
串口通信跟Modem是怎样的关系?搜索到一篇好文章:http://blog.chinaunix.net/u/20526/showart.php?id=355252。个人理解应该是两台计算机以“计算机A--UART-A(串口A)--Modem-A-----Modem-B--UART-B(串口B)--计算机B”的方式进行连接,这里的Modem可以是电话线上使用的56K Modem或者xDSL Modem,Cable Modem,GPRS装置等。这也可以看出为什么UART名字中有U(通用)的了:它可以与各种Modem相连接(或者不使用Modem,两个串口直接相连也是可以的吧),实现并行数据的串行收发,让内部以并行方式传输数据的系统可以以串行方式进行远距离通信。因为并行通信的速度虽然快,但传输距离有限,一般只用于系统内部,如系统内的各种总线(IDE、ISA、PCI、PCIExpress等);而串行通信速度慢,但可以传输较远的距离,一般用于系统间的通信,如USB、蓝牙、网络、串口等。
3 中断方式操作
开发板提供的示例程序是使用轮询方式进行串口读写操作的,即程序采用同步或者阻塞方式进行串口操作。对于简单的示例程序,这当然没有问题,因为程序即使不等待串口操作完成,也没有其他事情可做。但在实际应用中,很可能是要求以非阻塞方式进行读写操作的,这时可能就应该使用中断方式进行串口读写操作了。
通过UCONn可以设定UART以中断方式操作,还可以设定中断触发方式:边沿触发和电平触发。这个问题困扰了我好久才稍微弄明白点。下面是我咬文嚼字的理解,不知道对不对。引用一段S3C2440A用户手册的原文如下:
边沿(脉冲?)触发:一旦(as soon as)Tx缓冲区变为(becomes)空(非FIFO模式)或者达到(reaches)Tx FIFO触发级别(FIFO模式),则请求中断。注意,这里的“一旦”不仅限定前半句(非FIFO模式),也限定后半句(FIFO模式)。“一旦”与“变为”和“达到”连接起来,表示的是一个时间点,一个变化过程,是变为空或者达到触发级别的时间点或者说变化过程中请求中断。这个时间点过后,或者变化完成后,就不再请求中断了。只要处理中断后,清除源未决寄存器(SRCPND)和中断寄存器(INTPND)的相关位,就不会再有中断发生了。
电平触发:当(while)Tx缓冲区为空(非FIFO模式)或者达到Tx FIFO触发级别,则请求中断。这里的“当”限定前半句和后半句。“当”表示的是一种状态,是处于缓冲区空或者达到Tx FIFO触发级别的状态的时候,就请求中断。处理中断后,即使清除了源未决寄存器(SRCPND)和中断寄存器(INTPND)的相关位,只要这种状态存在,就一直请求中断,即会再次发起中断请求。只有不存在这种状态了,才不会再发起中断请求。
对两种中断触发方式的理解很重要,它直接影响到程序的编写方法。先看看边沿触发:
串口初始化代码为:
void UART0_Init(void) { // 8-N-1,50MHz,115200bps rULCON0 = 0x03; rUBRDIV0 = (int)(50 * 1000 * 1000 / (16.0 * 115200) + 0.5) - 1; rUMCON0 = 0x01; rUFCON0 = 0x00; rUCON0 = 0x05; pISR_UART0 = (unsigned int)UART0_ISR; rINTSUBMSK &= (~(BIT_SUB_RXD0 | BIT_SUB_TXD0 | BIT_SUB_ERR0)); rINTMSK &= (~BIT_UART0); } |
这里设定串口以边沿中断方式工作,不使用FIFO。中断处理代码为:
static void __irq UART0_ISR(void) { UART0_do_transmit(); rSUBSRCPND = BIT_SUB_RXD0; rSUBSRCPND = BIT_SUB_TXD0; rSUBSRCPND = BIT_SUB_ERR0; rSRCPND = BIT_UART0; rINTPND = BIT_UART0; } void UART0_do_transmit() { int cur,fifo_size; if (!(rSUBSRCPND & BIT_SUB_TXD0)) return; if (data_to_send_len > 0) { // 使用FIFO if (rUFCON0 & 0x01) { fifo_size = UART_TX_FIFO_SIZE - ((rUFSTAT0 & 0x3F00) >> 8); if (rUFSTAT0 & 0x4000) fifo_size = 0; } // 不使用 FIFO else if (rUTRSTAT0 & 0x02) { fifo_size = 1; } else { fifo_size = 0; } if (fifo_size > 0) { if (fifo_size > data_to_send_len) fifo_size = data_to_send_len; for(cur = 0; cur < fifo_size; cur++) { rUTXH0 = uart_send_buff[cur]; } data_to_send_len -= fifo_size; if (data_to_send_len > 0) { memcpy(&uart_send_buff[0], &uart_send_buff[fifo_size], data_to_send_len); } } } } |
编译了,烧录到开发板中,在串口调试工具中看不到开发板发送的字符串。为什么呢?
在设定UCON0之后,因为缓冲区变为空,所以会发起Tx中断,但在中断处理代码中,发现没有等待发送的数据,所以不进行发送操作。中断处理完成后,清除相关寄存器的相应位。因为是边沿触发,所以随后虽然缓冲区一直为空,但因为没有“变为空”这个边沿条件,所以不会再发生中断。是这样的吗?验证一下,在打开中断屏蔽前,在发送缓冲区里面放一些等待发送的数据试试:
void UART0_Init(void) { // 8-N-1,50MHz,115200bps rULCON0 = 0x03; rUBRDIV0 = (int)(50 * 1000 * 1000 / (16.0 * 115200) + 0.5) - 1; rUMCON0 = 0x01; rUFCON0 = 0x00; rUCON0 = 0x05; UART0_send_string("Some data before open interrupt maskn"); pISR_UART0 = (unsigned int)UART0_ISR; rINTSUBMSK &= (~(BIT_SUB_RXD0 | BIT_SUB_TXD0 | BIT_SUB_ERR0)); rINTMSK &= (~BIT_UART0); } |
这样修改程序后却发现,只输出了第一个字符“S”。原因何在?
a 由于使用的是非FIFO模式,所以一次只能发送一个字节
b 虽然第一个字节发送后,会存在缓冲区“变为空”的条件,会再次引发中断,但发送操作后的语句清除了相关标志位,导致中断丢失,具体过程为:
(1)缓冲区变为空,发起Tx中断
(2)进行中断处理,发送一个字节的数据
(3)一个字节数据发送完成,缓冲区变为空,再次发起Tx中断
(4)清除SUBSRCPND、SRCPND和INTPND中相关标志位,造成中断丢失
(5)因为没有再向发送缓冲区写入数据,所以不会再发生缓冲区“变为空”的条件,不会再发生中断
这里要注意中断的发起与处理方式:UART芯片进行数据收发和发起中断的操作,是与中央处理器同时进行的(不是软件模拟的并行,是硬件上的真正并行)。这个分析对不对呢?还是可以验证下:把清除SUBSRCPND的语句放换个位置,放在UART0_do_transmit()函数最开始看看:
static void __irq UART0_ISR(void) { UART0_do_transmit(); rSUBSRCPND = BIT_SUB_RXD0; //rSUBSRCPND = BIT_SUB_TXD0; rSUBSRCPND = BIT_SUB_ERR0; rSRCPND = BIT_UART0; rINTPND = BIT_UART0; } void UART0_do_transmit() { int cur,fifo_size; if (!(rSUBSRCPND & BIT_SUB_TXD0)) return; rSUBSRCPND = BIT_SUB_TXD0; } |
这样修改后可以正确输出第一次要求输出的字符串,但随后在看门狗中断中要求输出的字符串却不能输出。因为只要某次调用中断处理函数时,发现没有等待发送的数据,则最后一次缓冲区变为空引发的中断请求被处理完成后,由于再没有缓冲区变为空的条件,就不会再发生中断。随后即使有等待发送的数据了,那也与UART无关,不会再引发中断,无法再进行数据发送操作了。
这种情况下,如果采用电平触发方式,则由于一直处于缓冲区空的状态,所以会再次发起中断请求,可以继续进行数据发送操作。只要在UART0_Init()函数中设置UCON0的值为0x0205就是采用电平触发方式了,测试结果与预期一致。
在边沿触发方式下,为保证连续发送,必须不停地进行发送操作,以保证总有发送缓冲区“变为空”的条件发生;而电平触发方式则不需要这种不停的发送操作。但是,在所有数据发送完成,没有新的数据等待发送时,缓冲区一直处于空的状态,这时会不断地发生不必要的中断,让系统负载过重,严重影响效率,甚至让由定时器中断控制的LED累加器操作都不能进行了。为克服这种影响,可以采取的方式是:在中断处理中,当发现没有等待发送的数据时,屏蔽Tx中断,避免不必要的中断响应;但Tx中断未决请求仍然保留。这样,在主程序产生需要发送的数据后,只要打开对Tx中断的屏蔽,就可以继续进行发送了。
static void UART0_do_transmit() { int cur,fifo_size; if (!(rSUBSRCPND & BIT_SUB_TXD0)) return; if (0 == data_to_send_len) { rINTSUBMSK |= BIT_SUB_TXD0; } } static int UART0_send_string(const char* buff) { if ((0 == old_len) && (send_len > 0)) { rINTSUBMSK &= (~BIT_SUB_TXD0); } return send_len; } |
4 FIFO
S3C2440A的UART内部对于接收和发送各有64字节的缓冲区,当使用FIFO模式时,UART将使用这个缓冲区进行数据暂存操作,这样可以增加数据吞吐量,提高传输速率。其实,非FIFO模式也可以看作是特殊的FIFO模式,即只有一个字节缓冲区的FIFO模式。二者的主要不同在于读取缓冲区状态的方式:非FIFO模式下,通过UTRSTAT寄存器得知收发缓冲区状态;FIFO模式下,则从UFSTAT寄存器获得缓冲区状态。要注意的是,在FIFO模式下,只有达到触发级别后才会发起Rx或Tx中断。比如说,如果设置接收触发级别为16字节,则只有在接收缓冲区中有16个字节以上数据时,才会发起Rx中断请求。如果需要进行输入回显,则可能导致不能立即回显用户在串口工具中输入的字符。
5 自动流控制(AFC)
自动流控制涉及到RTS和CTS,我查了下相关缩写的含义:
DTE:Data Terminal Equipment,数据终端设备,一般指计算机。
DCE:Data Communication Equipment,数据通信设备,一般是调制解调器(Modem)。
DTR:Data Terminal Ready,数据终端就绪,DTE向DCE发送这个信号表示已经准备就绪。
DSR:Data Set Ready,数据设备就绪,DCE向DTE发送这个信号表示已经准备就绪。
RTS:Request To Send,请求发送,DTE向DCE请求发送数据。
CTS:Clear To Send,清除发送,DCE向DTE表示准备就绪,可以接收数据了。
在百度百科上发现,RTS和CTS原来是用于半双工通信中,DTE从接收模式转换成发送模式,在全双工模式中两个信号线一直有效就可以了。后来,SmartModem的出现,使得DTR,DSR,RTS和CTS的作用都改变了,变成用于“硬件流控制”了。总之,太复杂了,上文提到的http://blog.chinaunix.net/u/20526/showart.php?id=355252处的文章说得很详细了,就不深入研究了。日常工作使用中,也很少见到使用硬件流控制的。
6 其他问题
DMA操作方式:等学习了DMA后再进行尝试。
换行符问题:C语言中习惯用n表示换行,但在Windows世界里,却需要用rn表示换行,有的串口工具又进行了相互转换,所以为保证n真正起到回车换行的作用,需要仔细研究下串口工具,可能还需要在程序中进行一些转换。
退格符问题:有时候b是不能起到删除前一个字符的作用的,这时可以用bx20b试试。
中文显示问题:串口工具可能剥离了字节数据的最高位,导致不能正确显示中文。SecureCRT可以设置是否剥离最高位,在不剥离最高位的情况下,是可以正确显示中文的。