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 = LcdReadData();
}
LcdWriteString(0x80,str);
//第三行 移至第二行
LcdWriteComm(0x88);
LcdReadData();
for(int i = 0;i<16;i++)
{
str = LcdReadData();
}
LcdWriteString(0x90,str);
//第四行 移至第三行
LcdWriteComm(0x98);
LcdReadData();
for(int i = 0;i<16;i++)
{
str = 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();即可。