printf 函数的原理以及在单片机上重定向至LCD12832的实现

时间:2021-03-07 19:49:16

首先,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的处理就是一个指向这个内存空间起始地址的指针。每调用一次,这个指针就向后移动一次,所以重点是,你可以用到一半不用了,但是必须从头开始用。