首先,printf 函数的的原型是这样的:
int printf(char const * format, ...);
*format 就是要格式化的字符串的起始地址。注意这个必须是字符串以'\0' 为结尾,否则格式化的的时候会以指针为起点一直向后格式化,直到在后面连续的内存中遇到一个'\0'
后面的 ”...“ 是变参列表。可变参数列表是通过宏来实现的,这些宏定义在 stdarg.h里。这个头文件定义了一个类型 va_list 和三个宏 va_start、va_arg、va_end。我们一边写代码一边讲吧。
首先我们要明白一件事儿,printf 本身是将输出定向到了标准输出流。OK,但是在单片机编程时我们想要将格式化的字符串打印到屏幕上呢?比如串口,或者是LCD。这就需要我们自己实现这个函数。单片机是AVR的ATmega128 编译器是AVR-GCC,目的是将格式化的字符串打印在LCD12832上。代码如下,当然重点在最后,你可以从后往前看。。。
#include<avr/io.h><span style="white-space:pre"> </span>//AVR单品机外设寄存器定义
#include"Delay.h"<span style="white-space:pre"> </span>//延迟函数
#include"stdio.h"
#include"string.h"
#define FIRST 0x80//0xC0<span style="white-space:pre"> </span>//打印在LCD1602第一行命令码<span style="white-space:pre"> </span>
#define SECOND 0x90<span style="white-space:pre"> </span>//<span style="font-family: Arial, Helvetica, sans-serif;">打印在LCD1602第一行命令码</span><span style="white-space:pre">
</span>
#define FIRST_ROW 0<span style="white-space:pre"> </span>
#define SECOND_ROW 1
#define ON 0
#define OFF 1
#define H 0
#define L 1
#define SYNC(x) (0xf8|(x<<1))
#define DATA_H(x) (x&0xf0)<span style="white-space:pre"> </span>//取高位
#define DATA_L(x) ((x&0xf)<<4) //取低位
#define Spi_disable() (SPCR&=~(1<<SPE))
#define Spi_enable() (SPCR|=(1<<SPE))
//SPI总线通信IO口动作命令
//宏函数实现#define LCD_CS_BIT (1<<5)#define LCD_CS(x) x?(PORTB&=~LCD_CS_BIT):(PORTB|=LCD_CS_BIT)<span style="white-space:pre"> </span>//片选信号#define LCD_SCLK_BIT (1<<6)#define LCD_SCLK(x) x?(PORTE&=~LCD_SCLK_BIT):(PORTE|=LCD_SCLK_BIT)<span style="white-space:pre"> </span>#define LCD_SID_BIT (1<<5)#define LCD_SID(x) x?(PORTE&=~LCD_SID_BIT):(PORTE|=LCD_SID_BIT)#define LCD_RES_BIT (1<<0)#define LCD_RES(x) x?(PORTF&=~LCD_RES_BIT):(PORTF|=LCD_RES_BIT)#define RD 0x3 //11b 读数据#define RC 0x2 //10b 读指令#define WD 0x1 //01b 写数据#define WC 0x0 //00b 写指令void Data_tranfer(unsigned long int data_to_tran){ char i = 24; while(i--){ //LCD_SCLK(L); if(data_to_tran&0x800000){ LCD_SID(H); }else{ LCD_SID(L); } LCD_SCLK(H); data_to_tran<<=1; LCD_SCLK(L); }}//写命令void LCD_write_command(unsigned char cmd){ unsigned long int sync; unsigned long int data_h; unsigned long int data_l; unsigned long int data_to_tran; Spi_disable(); //数据格式化 sync=SYNC(WC); data_h=DATA_H(cmd); data_l=DATA_L(cmd); data_to_tran=0x0|(sync<<16)|(data_h<<8)|(data_l); //开始发数据 LCD_CS(ON); SPDR = sync; Data_tranfer(data_to_tran); LCD_CS(OFF); Delay_us(5); Spi_enable();}//写数据void LCD_write_data(unsigned char data){ unsigned long int sync; unsigned long int data_h; unsigned long int data_l; unsigned long int data_to_tran; Spi_disable(); //数据格式化 sync=SYNC(WD); data_h=DATA_H(data); data_l=DATA_L(data); data_to_tran=0x00|(sync<<16)|(data_h<<8)|(data_l); //开始发数据 LCD_CS(ON); Data_tranfer(data_to_tran); LCD_CS(OFF); Delay_us(5); Spi_enable();}void LCD_Config(){ //引脚配置 DDRB|=LCD_CS_BIT; PORTB&=~LCD_CS_BIT; DDRF|=LCD_RES_BIT; DDRE|=(LCD_SID_BIT|LCD_SCLK_BIT); PORTE|=(LCD_SID_BIT|LCD_SCLK_BIT); LCD_RES(L); Delay_ms(10); LCD_RES(H); //SPI_Config LCD_write_command(0x30); Delay_us(800); LCD_write_command(0x01); Delay_ms(50); LCD_write_command(0x06); Delay_us(800); LCD_write_command(0x0c);}
//字符串格式化缓冲char outbuf[32];//返回值是打印数据数量int printf_LCD(char row,const char *fmt,...){
//首先要定义一个变参列表 va_list args;
//定义返回值 int i;//显示在第几行 LCD_write_command(row);//va_start也是个宏函数,第一个数据是变参列表,第二个数据是变参的数据类型,一般是变参列表前的最后一个数据。这么说可能比较绕,其实就是变参列表到底应该被当作什么数据类型处理?和它的前一个数据类型一样,这里就是把变参列表的数据类型强制转换成字符串。 va_start(args,fmt);
//亮点来了,这个就是对字符串的格式化。他会将字符串格式化好放入缓冲数组中。 vsprintf((char *)outbuf, fmt, args);
//标记变参列表使用结束 va_end(args);
//开始把字符串中的元素逐个取出然后打印 for(i=0;i<strlen((char *)outbuf);i++){
//实际上,标准C中有个putc() 函数。也可以通过重写putc函数来达到重定向的目的。 LCD_write_data(outbuf[i]); }
//返回实际的打印字符数量 return i;}
<span style="white-space:pre"> </span>对于变参我啰嗦两句,我早年间看过在ARM上的变参函数编译成汇编后的结果。实际上,就是分配了一个连续的内存空间,然后va_list的处理就是一个指向这个内存空间起始地址的指针。每调用一次,这个指针就向后移动一次,所以重点是,你可以用到一半不用了,但是必须从头开始用。