3855|2

5

帖子

0

TA的资源

一粒金砂(中级)

楼主
 

MSP430程序库<四>printf和scanf函数移植 [复制链接]



  1. [indent]
  2. 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还可以从按键读取信息,可以参考移置方法自行移置。

    2、程序实现:

  • 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 键)
    #define InEOL '\r'              //ASCII   (回车键)
    #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 */
    #define In_EOL '\r'             /* ASCII */
    #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(编译器自带库——标准输入输出库)


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


        如果不进行这项设置,使用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。

        然后设置库选项:


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

        同时要加入Lcd12864的使用(c文件,h文件(要调用lcd12864的初始化函数))Uart和液晶一样要调用初始化函数。
        #include
        #include
        #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移植全部完成,欢迎大家使用;有什么不足之处,欢迎提出意见或是建议;多谢大家支持啦。



[ 本帖最后由 Engin 于 2013-7-30 10:53 编辑 ]

201107311700481054.png (36.62 KB, 下载次数: 0)

201107311700481054.png

2011073117005017.png (27.09 KB, 下载次数: 0)

2011073117005017.png

201107311701011402.png (24.11 KB, 下载次数: 0)

201107311701011402.png

201107311700481054.png (36.62 KB, 下载次数: 0)

201107311700481054.png

2011073117005017.png (27.09 KB, 下载次数: 0)

2011073117005017.png

201107311701011402.png (24.11 KB, 下载次数: 0)

201107311701011402.png

最新回复

这套教程不错  详情 回复 发表于 2013-7-30 14:03
 
点赞 关注

回复
举报

5

帖子

0

TA的资源

一粒金砂(中级)

沙发
 

MSP430各模块程序库

MSP430程序库<一>综述

MSP430程序库<二>UART异步串口

MSP430程序库<三>12864液晶程序库

MSP430程序库<四>printf和scanf函数移植

MSP430程序库<五>SPI同步串行通信

MSP430程序库<六>通过SPI操作AD7708

MSP430程序库<七>按键

MSP430程序库<八>DAC12的使用

MSP430程序库<九>数码管显示

MSP430程序库<十>ADC12模块

MSP430程序库<十一>定时器TA的PWM输出

MSP430程序库<十二>SVS(电源电压监控器)模块

MSP430程序库<十三>硬件乘法器使用

MSP430程序库<十四>DMA程序库

MSP430程序库<十五>Flash控制器

MSP430程序库<十六>总结
 
 

回复

2453

帖子

19

TA的资源

五彩晶圆(中级)

板凳
 
这套教程不错
 
 
 

回复
您需要登录后才可以回帖 登录 | 注册

随便看看
查找数据手册?

EEWorld Datasheet 技术支持

相关文章 更多>>
关闭
站长推荐上一条 1/6 下一条

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 国产芯 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2025 EEWORLD.com.cn, Inc. All rights reserved
快速回复 返回顶部 返回列表