TI中文支持网
TI专业的中文技术问题搜集分享网站

当通过printf()处理信息,如何知道哪一UART传输数据?

在用户开发应用程序的过程中,往往需要通过使用printf()处理信息,如果单片机支持的UART不只一组,我们可以通过定义 ”retarget.c”中的DEBUG_PORT,来决定printf()是使用哪一组UART传输数据。初始设置为UART0。
用户可以更改DEBUG_PORT的定义改变printf()的传输端口,以符合自己的系统构架。

gaoyang9992006:

printf 和scanf函数是C语言中最常用的输入出函数,从学习C语言开始,就开始使用这两个函数,然而当写用C语言写单片机程序时却不能使用这两个函数,总觉得单片机的C语言和一般的C语言差别很大,写起来不大方便;其实,单片机的C语言也是标准C语言上扩展或是改动的,都支持格式化输入输出函数(printf 和scanf);事实上,printf,scanf只负责格式化输入输出的字符,至于从哪儿输入,输出到哪儿,他们分别依靠getchar和putchar函数,只要实现单片机上的getchar函数和putchar函数,即可正常使用printf函数和scanf函数,这可以给我们单片机的信息交互带来很多方便。下面我们就来实现他们的移置。

硬件介绍:

硬件部分只需字符型输入输出设备:scanf从输入字符型设备读取字符,printf输出到字符型输出设备。在这里,我选用的字符型输入设备是超级终端,通过串口与单片机连接,输入字符;输出设备是超级终端和12864的液晶。scanf从串口读入字符,printf输出字符到串口和液晶。

有关串口的预提信息参考:MSP430程序库<二>UART异步串口。

有关液晶的具体信息参考:MSP430程序库<三>12864液晶程序库。

scanf还可以从按键读取信息,可以参考移置方法自行移置。

程序实现:

printf

单片机在调用printf时,printf是负责将数据解析成ASCII码流,通过调用putchar函数依次将字符发出。如果在putchar内编写从串口发送一字节数据,则printf的结果将从单片机串口发送出;如果putchar是向液晶写字符,让液晶显示一个字符,则printf的结果将显示在液晶上。本程序实现putchar同时向串口和液晶同时发送一个字符(液晶是显示一个字符)。

putchar函数如下:

int putchar(int ch)
{putchar2Com(ch);putchar2Lcd(ch);return (ch);
}

程序先向串口发送一个字符,然后像向晶发送字符。

其中:putchar2Com,向串口发送一个字符,代码如下:

int putchar2Com(int ch)
{if (ch == '\n')// '\n'(回车)扩展成 '\n''\r' (回车+换行) {UartWriteChar('\r') ;//0x0d 换行 }UartWriteChar(ch);//从串口发出数据 return (ch);
}

代码仅仅调用向串口写字符的函数UartWriteChar(ch)(详见Uart.c,在<二>中有介绍),当要输出换行时,需先输出’\n’将光标移至本行首位置,还需要’\r’(换行)才能将光标置于下一行起始位置,即将’\n’扩展为’\r’,’\n’两个字节依次发出。

purchar2Lcd函数比较复杂,因为我所使用的12864液晶是中文字库的液晶,每行8个地址,可以显示8个中文字符或16个英文字符,而putchar只发出一个字节,需要判断每个地址的前半字还是后半字(因为每个字可以显示中文,如果中文的两个字节在相邻的两个地址上,将不会显示,或是显示乱码)。

上代码:

int putchar2Lcd(int ch)
{char addr,dat;if (ch == '\n')// '\n'(回车),换行 {ChangeNextRow();}else {addr = LcdReadAddr();if(ch < 0x80){LcdWriteData(ch);}else {LcdWriteData(0x20);//写入一个空字符,根据地址判断是否为前半字 if(addr == LcdReadAddr())//前半字 从新写入ch字符 {LcdWriteComm(addr);LcdWriteData(ch);}else {LcdWriteComm(addr);dat = LcdReadData();if(dat < 0x80)//前一个字符是英文字符 {LcdWriteData(0x20);//空格 }LcdWriteData(ch);}}}if((addr != LcdReadAddr()) &&//写入的是行最后位的后半字则换行 (addr==0x87 || addr==0x97 || addr==0x8F || addr==0x9F)){ChangeNextRow();}return (ch);
}

这个函数首先判断换行;然后处理其他一般字符,如果是英文字符,不用考虑前后半字,只需正常写入液晶即可;如果是中文字符,在判断是否是前半字,前半字则直接写入,后半字则判断之前写入的前半字是否是中文,是则直接写入,不是则把英文字符移入后半字,然后写入;最后判断是否到行尾,是则换行。

程序更新为:更新日期:20110821 18:51
目的是修复原来,行尾前半字为英文,再输入中文会显示乱码。
int putchar2Lcd(int ch)
{char addr,dat;char changeRowFlag = 0;if (ch == '\n')// '\n'(回车),换行 {ChangeNextRow();changeRowFlag = 1;}else if (ch == '\b')// '\b' (退格) {BackSpace();}else {addr = LcdReadAddr();if(ch < 0x80){LcdWriteData(ch);}else {LcdWriteData(0x20);//写入一个空字符,根据地址判断是否为前半字 if(addr == LcdReadAddr())//前半字 从新写入ch字符 {LcdWriteComm(addr);LcdWriteData(ch);}else {LcdWriteComm(addr);dat = LcdReadData();if(dat < 0x80)//前一个字符是英文字符 {LcdWriteData(0x20);//空格 }if((addr != LcdReadAddr()) &&//写入的是行最后位的后半字则换行 (addr==0x87 || addr==0x97 || addr==0x8F || addr==0x9F)){ChangeNextRow();changeRowFlag = 1;}LcdWriteData(ch);}}}if((addr != LcdReadAddr()) &&//写入的是行最后位的后半字则换行,且未换过行 (changeRowFlag == 0) &&(addr==0x87 || addr==0x97 || addr==0x8F || addr==0x9F)){ChangeNextRow();}return (ch);
}

前后半字判断方法如下:读液晶地址,向液晶写入一个空格,再读地址,两地址相同则是前半字,不同则是后半字。读地址函数在Lcd12864.c中,新加入函数,代码如下:

char LcdReadAddr()
{char ch;WaitForEnable();CLR_RS;SET_RW;DATA_DIR_IN;SET_EN;_NOP();ch = DATA_IN;//读数据 CLR_EN;DATA_DIR_OUT;return (ch|0x80);
}

这个是读地址,ch|0x80是因为写入液晶地址首位应为1.。

液晶中新加入两个函数,一个是上边的读地址,另外一个是读数据;作用是读取液晶当前地址处的数据,从而判断之前半字是否是中文。代码如下:

char LcdReadData()
{char ch;WaitForEnable();SET_RS;SET_RW;DATA_DIR_IN;SET_EN;_NOP();ch = DATA_IN;//读数据 CLR_EN;DATA_DIR_OUT;return ch;
}

另外 putchar还调用了换行——ChangeNextRow函数,完成液晶输出换至下一行。

代码如下:

void ChangeNextRow()
{char addr;addr = LcdReadAddr();//当前地址 if(addr <= 0x88){LcdWriteComm(0x90);}else if(addr <= 0x90){LcdWriteComm(0x98);}else if(addr <= 0x98){LcdWriteComm(0x88);}else {AddNewline();//添加行,同时向上滚动 LcdWriteComm(0x98);}
}

读取当前地址,判断在哪一行,然后写入下一行首地址;如果是最后一行,则所有安徽那个向上移,写入最后一行首地址。

AddNewLine函数完成所有行向上滚动一行,然后地址定位至最后一行。

代码如下:

void AddNewline()
{char str[17];str[16] = 0;//第二行 移至第一行 LcdWriteComm(0x90);LcdReadData();//空读取 for(int i = 0;i<16;i++){str[i] = LcdReadData();}LcdWriteString(0x80,str);//第三行 移至第二行 LcdWriteComm(0x88);LcdReadData();for(int i = 0;i<16;i++){str[i] = LcdReadData();}LcdWriteString(0x90,str);//第四行 移至第三行 LcdWriteComm(0x98);LcdReadData();for(int i = 0;i<16;i++){str[i] = LcdReadData();}LcdWriteString(0x88,str);//第四行 空白 LcdWriteString(0x98," ");//十六个空格 }

读出下一行数据,写入上一行,最后一行写入空格即可。

到此putchar函数全部完成,printf移植的程序部分完成,使用方法详见使用示例。

scanf

scanf和printf类似,其只负责格式化输入的字符,字符来源是从getchar函数获取;同样,在使用scanf函数之前,要针对字符输入源自行编写getchar函数

最简getchar:

int getchar()
{return (putchar(UartReadChar()));
}

这是最简单的getchar函数,直接调用读取字符函数,输出并返回。

但是人的输入过程会偶尔犯错误的,为了支持退格键等,需要开辟一个缓存区。

详细代码如下:

#define LINE_LENGTH 80//行缓冲区大小,决定每行最多输入的字符数 /*标准终端设备中,特殊ASCII码定义,请勿修改*/ #define InBACKSP 0x08//ASCII <-- (退格键) #define InDELETE 0x7F//ASCII <DEL> (DEL 键) #define InEOL '\r' //ASCII <CR> (回车键) #define InSKIP '\3' //ASCII control-C #define InEOF '\x1A' //ASCII control-Z #define OutDELETE "\x8 \x8" //VT100 backspace and clear #define OutSKIP "^C\n" //^C and new line #define OutEOF "^Z" //^Z and return EOF 
int getchar()
{static char inBuffer[LINE_LENGTH + 2];//Where to put chars static char ptr;//Pointer in buffer char c;while(1){if(inBuffer[ptr])//如果缓冲区有字符 return (inBuffer[ptr++]);//则逐个返回字符 ptr = 0;//直到发送完毕,缓冲区指针归零 while(1)//缓冲区没有字符,则等待字符输入 {c = UartReadChar();//等待接收一个字符 if(c == InEOF && !ptr)//==EOF== Ctrl+Z {//只有在未入其他字符时才有效 printf(OutEOF);//终端显示EOF符 return EOF;//返回 EOF(-1) }if(c==InDELETE || c==InBACKSP)//==退格或删除键== {if(ptr)//缓冲区有值 {ptr--;//从缓冲区移除一个字符 printf(OutDELETE);//同时显示也删掉一个字符 }}else if(c == InSKIP)//==取消键 Ctrl+C == {printf(OutSKIP);//终端显示跳至下一行 ptr = LINE_LENGTH + 1;//==0 结束符== break;}else if(c == InEOL)//== '\r' 回车== {putchar(inBuffer[ptr++] = '\n');//终端换行 inBuffer[ptr] = 0;//末尾添加结束符(NULL) ptr = 0;//指针清空 break;}else if(ptr < LINE_LENGTH)//== 正常字符 == {if(c >= ' ')//删除 0x20以下字符 {//存入缓冲区 putchar(inBuffer[ptr++] = c);}}else //缓冲区已满 {putchar('\7');//== 0x07 蜂鸣符,PC回响一声 }}}
}

注释已经很详细了,这里不再详细解释。

scanf的移植程序部分已经完成,如果需要从键盘读入字符,可以仿照上述函数写getchar函数。具体使用和设置见使用示例。

另外,iar的安装文件夹下,430文件夹下有一个src文件夹,lib/clib文件夹下(我的具体文件夹是:D:\Program Files\IAR Systems\Embedded Workbench 6.0 Evaluation\430\src\lib\clib\getchar.c),有一个getchar.c文件,这是getchar的函数,内容如下:

#include "stdio.h" extern char _low_level_get(void);/* Read one char from I/O */ /* Should be supplied by user */ static void put_message(char *s)
{while (*s)putchar(*s++);
}#define LINE_LENGTH 80/* Change if you need */ #define In_DELETE 0x7F/* ASCII <DEL> */ #define In_EOL '\r' /* ASCII <CR> */ #define In_SKIP '\3' /* ASCII control-C */ #define In_EOF '\x1A' /* ASCII control-Z */ #define Out_DELETE "\x8 \x8" /* VT100 backspace and clear */ #define Out_SKIP "^C\n" /* ^C and new line */ #define Out_EOF "^Z" /* ^Z and return EOF */ int getchar(void)
{static char io_buffer[LINE_LENGTH + 2];/* Where to put chars */ static int ptr;/* Pointer in buffer */ char c;for (;;){if (io_buffer[ptr])return (io_buffer[ptr++]);ptr = 0;for (;;){if ((c = _low_level_get()) == In_EOF && !ptr){put_message(Out_EOF);return EOF;}if (c == In_DELETE){if (ptr){ptr--;put_message(Out_DELETE);}}else if (c == In_SKIP){put_message(Out_SKIP);ptr = LINE_LENGTH + 1;/* Where there always is a zero... */ break;}else if (c == In_EOL){putchar(io_buffer[ptr++] = '\n');io_buffer[ptr] = 0;ptr = 0;break;}else if (ptr < LINE_LENGTH){if (c >= ' '){putchar(io_buffer[ptr++] = c);}}else {putchar('\7');}}}
}

_low_level_get(void); 这个函数需用户定义,不过这个getchar函数不支持退格键,可以更改以支持;_low_level_get(void);这个函数可以直接调用UartReadChar();这个函数,使用时,把getchar.c加入项目,同时在项目中添加_low_level_get(void);函数,函数体只有一句:return UartReadChar();即可。

程序调用示例:

程序使用方式,项目中添加printf.c文件和scanf.c文件(用printf函数则加printf.c文件,用scanf函数就添加scanf.c文件),在要使用函数的地方包含stdio.h(编译器自带库——标准输入输出库)

image_thumb1

还要设置使用库和printf的大小:

image_thumb4

如果不进行这项设置,使用scanf时将报错:

Error[e27]: Entry "getchar" in module Scanf ( G:\work\程序库\Printf\Debug\Obj\Scanf.r43 ) redefined in module ?getchar ( D:\Program Files\IAR Systems\Embedded;用的是C语言,这里选择CLIB。

然后设置库选项:

image_thumb7

这里选择大尺寸,目的是支持所有的格式,因为所用单片机有64kb的程序存储空间,足够使用,如果程序存储空间不够大,推荐选择中尺寸或小尺寸。大尺寸printf占用空间 4.8kb、scanf :2.3kb,中尺寸 printf:2.5kb、scanf:1.6kb,小尺寸 printf:1.6kb。实际使用时根据需要进行选择。

同时要加入Lcd12864的使用(c文件,h文件(要调用lcd12864的初始化函数))Uart和液晶一样要调用初始化函数。

#include <msp430x16x.h> #include <stdio.h> #include "Uart.h" #include "Lcd12864.h" 

头文件包含。

void main( void )
{// Stop watchdog timer to prevent time out reset WDTCTL = WDTPW + WDTHOLD;ClkInit();LcdInit();UartInit(38400,'n',8,1);//串口初始化,设置成38400bps,无校验,8位数据,1位停止 //int a; _EINT();//scanf("%d",&a); //printf("刘中原%d\n",a); printf("刘中原%f\n",23.6);printf("刘中原%1.2f\n",23.6);
}

使用时,先调用液晶和串口的初始化函数,然后开中断;就可以正常的调用scanf和printf函数了。

至此,printf和scanf的移植全部完成,使用这两个函数将给单片机的输入输出带来极大方便。另外,Lcd12864的液晶使用是4行显示,空间较小,可能需要定位至具体位置,以使界面看起来更合理,为此,在Printf中再添加一个定位函数(GotoXY):

void GotoXY(char x,char y)
{char addr;if(y==0){addr = 0x80 + x / 2;}else if(y==1){addr = 0x90 + x / 2;}else if(y==2){addr = 0x88 + x / 2;}else {addr = 0x98 + x / 2;}LcdWriteComm(addr);if(x % 2)//是奇数,后移一位(写入空格) {LcdWriteData(0x20);}
}

这样就方便了液晶程序的编写。

又加入一个函数,在printf.c里,目的是支持退格键,内容如下:

void BackSpace()
{char addr,dat;addr = LcdReadAddr();//当前地址 LcdWriteData(0x20);//写入一个空字符,根据地址判断是否为前半字 if(addr == LcdReadAddr())//前半字 {if(addr == 0x80)return;else if(addr == 0x90)addr = 0x87;else if(addr == 0x88)addr = 0x97;else if(addr == 0x98)addr = 0x8F;else addr = addr - 1;LcdWriteComm(addr);LcdReadData();//空读取 dat = LcdReadData();LcdWriteComm(addr);if(dat < 0x80)LcdWriteData(dat);}else {LcdWriteComm(addr);}
}

退格完成功能:仅仅地址向前退一格,详细见源程序。

printf和scanf移植全部完成,欢迎大家使用;有什么不足之处,欢迎提出意见或是建议;多谢大家支持啦。

附件:程序库

囧:

你的输出界面是什么?

gaoyang9992006:

回复 囧:

串口助手。

赞(0)
未经允许不得转载:TI中文支持网 » 当通过printf()处理信息,如何知道哪一UART传输数据?
分享到: 更多 (0)