在嵌入式软件开发过程中,往往都会用到串口进行打印信息以跟踪调试代码的运行。只要在代码的关键位置加入打印函数,即可分析代码在这一位置的关键参数是否正确,运行状态是否无误以及相关的出错信息。通过串口线连接PC端COM口与开发板的UART即可实现开发板与PC机的通信。在代码调试阶段,开始板的状态信息通过串口打印显示在PC端屏幕,可以一目了然,是一种非常重要的调试手段。

笔者此处就s3c2416的串口打印使用作一个简单的介绍。

1. UART模块实现

1.1. UART初始化

在使用任何外设前,一般都是需要对外设初始化。UART也不例外,使用前需设置波特率、通信控制、通信的处理方式(中断还是查询)等。设置波特率为115200,8位数据,1位停止位,没有奇偶检验(uboot默认设置),对于慢速外设,高速cpu一般不应通过查询的方式去确定外设发送完数据或接收到数据,这会让cpu处于空等的状态,cpu效率很低,对于外设需发送或接收大量数据的情况更是如此。因此笔者此处主要讲解s3c2416串口中断方式去发送和接收数据的情况。由于s3c2416的UART发送和接收均有64字节的FIFO,通过中断的方式可以连续装载发送的数据到FIFO中或从FIFO中连续读出接收的数据,应用只需通过Uart0中断请求告知需发送或接收的数据长度及数据保存位置,即可进行等待挂起(如ucos中等待信号量标志等函数OSSemPend),cpu可转而处理其它的事情,当Uart0中断发送或接收完应用请求的所有数据,即可发送相关的信号量或标志唤醒之前等待挂起的应用(如ucos中发送信号量标志等函数OSSemPost),cpu再转而继续处理之前的应用。

Uart0_Init()函数如下:

void Uart0_Init()
{
    // 设置GPH0,GPH1为TX和RX
    rGPHCON &= ~((3<<0)|(3<<2));
    rGPHCON |= (2<<0)|(2<<2);  
    // 8位数据,一个停止位,没有奇偶检验
    rULCON0 = 0x3;
    // UART0 FIFO使能,Tx发送空时中断,Rx 16bytes中断
    rUFCON0 = (0<<6) | (2<<4) | 0x7;
    // 流控制禁止
    rUMCON0 = 0;
    // 发送接收中断使能,使用PCLK时钟66M
    rUCON0 = 0x5 | (1<<7) | (2<<10);
    // 设置波特率
    rUBRDIV0 = 66000000/(16*Baudrate)-1;
    rUDIVSLOT0 = 0x0888;
    // 注册UART0 IRQ中断
    IRQ_Register(INT_UART0, Uart0_IRQ);
    // UART0 IRQ
    rINTMOD1 &= ~(1 << INT_UART0);
    // 开启RX子中断
    // 在写入FIFO之前,TX必须关闭中断,不然FIFO空引发中断
    rINTSUBMSK |= 0x7;
    rINTSUBMSK &= ~(1 <<0);   
    // UART0开启中断
    rINTMSK1 &= ~(1 <<INT_UART0);
}

1.2. 格式化打印函数

C语言的一些库函数功能很强大,没有必要再自己去实现。软件调试时,可能用的最多的函数就是printf。由于printf格式化输出是面向控制台的,在arm目标中是通过一种半主机的方式,把printf函数输出请求传送至运行调试器的主机。如果不对printf进行重定向是不能在目标板中使用printf等函数的(fputc和 fgetc重定向到串口或目标板屏幕)。笔者此处为了通用,不重定向改写printf,而是使用vsnprintf函数进行格式化输出到字符串中,再把字符串发送到串口,实现与printf类似的串口打印输出。串口格式化打印函数Uart0_Printf()如下:

// 串口打印函数,替换库函数printf函数功能
// printf是向控制台输出信息,通过vsnprintf格式化数据输出
// 到字符串,并通过串口发送字符串函数进行串口打印
voidUart0_Printf(char *fmt, ...)
{
    va_list ap;
    char String[1024];

    va_start(ap, fmt);
    vsnprintf(String, sizeof(String), fmt, ap);
    va_end(ap);

    Uart0_SendString(String);
}

1.3. 串口字符串输出

Uart0_Printf()通过vsnprintf()格式化输出到字符串后,即可用Uart0_SendString()进行字符串发送,Uart0_SendString()会确定出字符串的长度,即确定串口发送的数据长度,再调用Uart0_SendData()进行发送一定长度的字节数据到串口。

// 通过串口发送字符串
voidUart0_SendString(char *String)
{  
    unsigned int Len;
    char *Temp = String;
    if (String == 0) {
        return;
    }
    Len = 0;
    // 获得字符串的长度
    while (*Temp++) {
        Len++;
    }
    Uart0_SendData((unsigned char *)String,Len);
}

1.4. 任意长度字节输出

Uart0_SendData()会最终向uart0请求发送一定长度的数据,以及数据所在的位置,之后会进入等待,直到uart0中断发送完所有请求的数据。由于Uart对外发送数据是很慢的,如果有操作系统或状态机实现中,在while等待中,可改成类似信号量等待,把Uart0_SendData()函数挂起(如OSSemPend(ucos)),直到uart0中断完成请求后,发送信号量或标志再唤醒执行(如OSSemPost(ucos))。Uart0_SendData()函数实现如下:

// 通过串口发送任意长度的数据
voidUart0_SendData(unsigned char *pBuffer, unsigned int Len)
{
    if (pBuffer == 0 || Len == 0) {
        return;
    }

    TxLen = Len; // 向中断请求发送的数据字节长度
    pTxData = pBuffer; // 发送数据的位置
    TxLen--; // 发送了一字节,发送数据长度减1   
    // 发送第一个字节完后会产生中断,之后数据在中断函数中连续发送
    rUTXH0 = *pTxData++;   
    rINTSUBMSK &= ~(1 <<1); // 数据写入FIFO后开启TX发送完中断
    while(TxLen != -1) {
    // 等待UART0数据发送完         
    // 可改成操作系统信号量等待函数,提高cpu效率,如OSSemPend(ucos)
    }
}

1.5. 发送字节函数

Uart0_SendByte()用来向串口发送一字节的数据(字符),其向uart0请求一字节的数据发送。

// 通过串口发送一字节数据
voidUart0_SendByte(unsigned char Byte)
{
    TxLen = 0; // 发送1字节后,发送数据长度为0  
    rUTXH0 = Byte; // 1字节数据装载进FIFO中
    rINTSUBMSK &= ~(1 <<1); // 数据写入FIFO后开启TX中断   
    while (TxLen != -1) { // 等待中断中发送完标志
    // 等待UART0数据发送完         
    // 可改成操作系统信号量等待函数,提高cpu效率,如OSSemPend(ucos)              
    }
}

1.6. 字符串接收函数

Uart0_ReceiveString()用来请求通过串口接收字符串,其会调用Uart0_ReceiveByte()来向uart0请求接收一个字节的数据,根据接收到的字符数据判断是否字符串结束或者回车结束。

// 通过串口接收字符串,指定缓存长度为Len
voidUart0_ReceiveString(char *pBuffer, unsigned int Len)
{
    char *Temp;
    unsigned int i;
    if (pBuffer == 0 || Len == 0) {
        return;
    }
    Temp = pBuffer;
    for (i=0; i<Len; i++) { // 不能超过缓存长度Len
        *Temp = Uart0_ReceiveByte();
        if (*Temp == 0 || *Temp == '\r') {
            break; // 字符串结束或回车则结束输入
        }
        Temp++;
    }
    if (i < Len) {
        pBuffer[i] = 0; // 字符串末尾加入结束字符0 
    } else {
        pBuffer[Len-1] = 0;
    }
}

1.7. 字节接收函数

Uart0_ReceiveByte()会通过uart0请求接收一个字节的数据,阻塞直到接收到数据返回。

// 通过串口接收一字节数据
charUart0_ReceiveByte(void)
{
    unsigned char Value;
    pRxData = &Value; // 中断中接收的1字节数据放在Value中
    RxLen = 1; // 接收长度为1字节  
    while(RxLen != 0) {
    // 等待UART0数据接收       
    // 可改成操作系统信号量等待函数,提高cpu效率,如OSSemPend(ucos)
    }      
    return ((char)Value);
}

1.8. 中断请求处理

Uart0_IRQ()中断处理函数用来处理应用的发送请求以及接收请求。如果请求的发送数据长度或请求的接收数据长度过大,将会分多个中断请求来分包发送数据或接收数据,直到所有的数据请求均完成后,通过发送信号量(如OSSemPost(ucos))或完成标志告知请求的应用。

// 请求通过串口发送的字节数
static volatile int TxLen = 0;
// 请求通过串口接收的字节数
static volatile int RxLen = 0;
// 通过串口发送数据的缓存位置
static volatile unsigned char *pTxData = 0;
// 通过串口接收数据的缓存位置
static volatile unsigned char *pRxData = 0;

// Uart0中断处理
static void Uart0_IRQ()
{
    volatile char TempChar;
    unsigned char Count;
    unsigned int i;

    if (rSUBSRCPND & 0x1) {
        // Receive Interrupt
        Count = rUFSTAT0 & 0x3f;// 接收的字节数
        if (RxLen > Count) { // 应用需接收的剩余长度大于接收的长度
            RxLen -= Count; // 剩余长度减少这次接收的数据长度
            for (i=0; i<Count; i++){ // FIFO数据读入到数据保存区
                *pRxData++ = (unsignedchar)rURXH0;
            }
        } else {
            for (i=0; i<RxLen; i++){
                // 保存应用剩余需接收的数据
                *pRxData++ = (unsigned char)rURXH0;
            }
            for (i=0;i<(Count-RxLen); i++) {
                // 多余的接收数据从FIFO口读出直接丢弃
                TempChar= (unsigned char)rURXH0;
            }
            //串口接收完应用需求长度的所有数据,若有操作系统
            //应用在数据等待发送完时通过信号量或标志等待而挂起
            //OSSemPend(ucos),这样不会让cpu查询等待,极大提高效率
            //发送信号量或标志,唤醒等待的应用OSSemPost(ucos)
            RxLen= 0; // 所有数据接收完毕         
        }          
        rSUBSRCPND|= 0x1; // 清除Uart0 RX中断源等待
    }
    if(rSUBSRCPND & (0x1<<1)) {
        //Transmit Interrupt
        Count= (rUFSTAT0>>8) & 0x3f; // FIFO中待发送的长度
        if(TxLen > (64-Count)) { // FIFO最大64个字节
    // 若应用请求发送的数据较大,每次写滿64字节的FIFO,发送完FIFO空后
    // 再装载数据到FIFO中,直到所有应用所有的数据发送完
            TxLen-= 64 - Count; // 剩余长度减去此次发送的数据长度
            for(i=0; i<64-Count; i++) { // 装载发送数据到FIFO中
                rUTXH0= *pTxData++;
            }
        } elseif (TxLen > 0) { // 剩余数据能一次性装载入FIFO中
            for(i=0; i<TxLen; i++) { // 写入剩余数据到FIFO中          
                rUTXH0= *pTxData++;
            }
            TxLen= 0; // 剩余发送数据长度
        } else{
            //有操作系统加入信号量或标志,提高cpu效率
            TxLen= -1; // 所有数据发送完毕
        // 数据发送完,FIFO空,必须关中断,不然会重复引起发送空中断   
            rINTSUBMSK|= (1 << 1);
        }      
        rSUBSRCPND|= (0x1<<1); // 清除Uart0 TX中断源等待
    }
    if(rSUBSRCPND & (0x1<<2)) { // 串口状态错误处理
        rSUBSRCPND|= (0x1<<2);        
    }

    rSRCPND1|= (0x1 << INT_UART0); // 清除Uart0中断源等待
    rINTPND1|= (0x1 << INT_UART0); // 清除Uart0中断请求
}

2. 串口打印调试应用实例

加入上一章启动代码工程System目录下所有源码,以及串口打印调试模块源码文件,即可调用串口格式化打印函数Uart0_Printf(),串口获取输入函数Uart0_ReceiveByte()等。在mani.c中的应用实例如下:

#include"s3c2416.h"
#include"UART0.h"

__asm voidDelay_ms(unsigned int nCount)
{
//延时1ms,共延时nCount(R0) ms
Delay1_ms  
        LDR R1, =100000  // Arm clock为400M       
Delay2_ms
        SUBS R1, R1, #1  // 一个Arm clock
        BNE Delay2_ms      // 跳转会清流水线,3个Arm clock
        SUBS R0, R0, #1 // 调用者确保nCount不为0
        BNE Delay1_ms
        BX  LR
}

int main()
{
    char Buffer[1024];
    unsigned int i;
    float pi = 3.1415926535898f;
    Uart0_Init();
    // 打印浮点数pi的值
    Uart0_Printf("The value pi =%f\n\r", pi);
    Uart0_SendString("Count down(sec):");
    for (i=5; i!=0; i--) { // 5s倒计时
        Uart0_Printf("%d", i);     
        Delay_ms(1000);
        Uart0_SendByte('\b'); // 回退  
    }
    Uart0_SendString("0\n\r");
    while(1) {
        Uart0_SendString("Input a string,press Enter to end\n\r");
        // 获取字符串输入
        Uart0_ReceiveString(Buffer,sizeof(Buffer));
        // 打印输入的字符串
        Uart0_Printf("Yourpress is %s\n\r", Buffer);
    }
}

3. 附录

UART打印调试.rar,包含MDK工程源码,其中System目录下为启动代码实现部分,支持sd/mmc或nand启动等功能,需理解后更改,任何bug或疑问欢迎联系笔者。UART0.c/UART0.h为本章串口打印调试模块源码文件。
源码下载: http://pan.baidu.com/s/1jG3HqXC