STM32 OLED屏软件IIC刷新速率优化(四)

       1.1   STM32+OLED屏初始化(一) 

       1.2  STM32+OLED屏显示字符串、汉字、图片(二)

       1.3  STM32+OLED屏多级菜单显示(三)

 1.优化刷新速率

        前三篇几乎已经完成了OLED屏显示的全部内容,当然,还缺少一些精致的图形,这方面可以自己动手做一些喜欢的图形,也可以移植一下大佬们图形库,并不是很复杂所有就不多赘述了。这一篇主要是优化屏幕的刷新速率,让OLED的刷新率尽可能的快一些,也就是经常说的高帧,优化刷新速率分为两篇,分别用软件IIC和硬件IIC,这一篇是软件IIC。

        之前的屏幕刷新主要是对屏幕的某一页的一个区间读写,如果一个屏幕要写两个或者两个以上的字符时,就要调用多次寻址操作,但是如果在我们在单片机中构建一个和SSD1306芯片的GRAM等大的SRAM内存,直接在单片机的SRAM数组上写,最后再SRAM数组的数据一次性写到SSD1306中,就只用调用一次写函数,可以大大提高芯片的读写操作。 

        构建帧缓冲区后,再优化软件IIC,使用stm32位带操作,直接操作GPIO管脚的高低电平,更快速简洁,位带是一种在单片机中使用的技术,它可以将单个位(bit)与一个特定的内存地址关联起来。 

2.帧缓冲区

        就如先前所说, 屏幕刷新主要是对屏幕的某一页的一个区间读写,但是每写一个字符就必须要寻址一次,当要写十个字符就必须要寻址十次,不仅效率不高还非常麻烦,为了解决这一麻烦提高工作效率,可以在单片机内部构建一个和OLED屏幕SSD1306芯片的GRAM等大的SRAM缓冲区(在单片机内部构建缓冲区数组),因为是为刷新帧率而存在的数组也称为帧缓冲区或显存。有了这片缓冲区就不用频繁的寻址了,如果要写十个字符就直接在帧缓冲区中操作,等这十个字符写好后再一次性把缓冲区的数据写入屏幕的GRAM中,这种操作只需要一次寻址,大大提高工作效率。按照正点原子的说法:

        单片机有无构建帧缓冲区示意图: 

  

2.1 构建帧缓冲区与画点函数

        构建一个缓冲数组OLED_GRAM[8][128](实际上是SRAM)为帧缓冲区,之后所有的显示操作都放在这个帧缓冲区中,修改完成图形数据后,再一次性将单片机内部的OLED_GRAM写入SSD1306的GRAM中。

//构建OLED帧缓冲区
uint8_t OLED_GRAM[64/8][128];

         构建帧缓冲区后,就面临两个问题,1、如何在帧缓冲区写数据?2如何将帧缓冲区的数据写入OLED屏幕上?在帧缓冲区中写数据可以使用画点函数,把帧缓冲区的数据写入SSD1306的GRAM中使用IIC通信。

        先说在帧缓冲区写数据,编写画点函数,对帧缓冲区写入数据。保存当前要写入的页面中八个点,再修改其中要修改的点,最后把修改好的八个点写入帧缓冲区,就修改了其中一个点了。画点函数对每次对帧缓冲区的一个点进行写0和1,需要那个点亮起就对该点写1,需要那个点灭掉就对该点写0,但是如果把该写1的地方写0,把该写0的地方写1,那么就会出现截然相反的效果,这就是帧缓冲区反显操作,这种操作并不需要对模块写入反显命令就可以实现。

/**
  * @brief  OLED帧缓冲画点函数
  * @param  x 行位置
  * @param  y 列位置
  * @param  mode 显示模式:1--正显  0--反显
  * @retval 无
  * @explain 在帧缓冲区任意位置正/反显示一个点
  */
void OLED_Framebuffer_Drawpoint(uint8_t x, uint8_t y, uint8_t mode)
{
	uint8_t page, line, temp = 0;
	
	if(x>128 || y>64) return;//超出屏幕范围保护
	page = y/8; // y方向8个字节 8Byte*8Bit = 64Bit  y坐标除以8得要操作的Byte位
	line = y%8; // y方向8个字节 8Byte*8Bit = 64Bit  y坐标取余8得要操作的Bit位 
	temp = OLED_GRAM[page][x]; 
	if(mode) temp |= 1<<line;
	else temp &= ~(1<<line);
	OLED_GRAM[page][x] = temp;
}

2.2 在帧缓冲区绘制一个字符

        写入字符和之前一样,画点函数和写数据函数(OLED_Write_Data()) 有部分差异,写数据函数(OLED_Write_Data()) 是一次写入8 bit,画点函数一次只写入一个点,所以在我们写入字符时要在之前的基础上加上八次循环,再相应的偏移八位即可,mode表示正/反显。

/**
  * @brief  OLED在帧缓冲绘制一个字符
  * @param  x 行位置
  * @param  y 列位置
  * @param  Fontsize 字体大小
  * @param  Char 要显示的一个字符
  * @param  mode 显示模式:1--正显  0--反显
  * @retval 无
  */
void OLED_Framebuffer_DrawChar(uint8_t x, uint8_t y, uint16_t Fontsize, const char Char, uint8_t mode)
{
	uint8_t i, j, k;
	uint8_t temp;
	
	for(k=0; k<Fontsize/8; k++) {
		switch(Fontsize) {
			case 8:{
				for(i=0; i<6; i++){//字宽为6
					temp = Ascii_6x8[Char - ' '][i+k*6];
					for (j=0; j<8; j++) {//画8个点
						if(mode) OLED_Framebuffer_Drawpoint(x+i, y+j+k*8, (temp>>j & 0x01) ? 1:0);
						else     OLED_Framebuffer_Drawpoint(x+i, y+j+k*8, (temp>>j & 0x01) ? 0:1); 
					}
				}
				break;
			}
			case 16: {
				for(i=0; i<8; i++) {//字宽为8
					temp = Ascii_8x16[Char - ' '][i+k*8];
					for (j=0; j<8; j++) {//画8个点
						if(mode) OLED_Framebuffer_Drawpoint(x+i, y+j+k*8, (temp>>j & 0x01) ? 1:0);
						else     OLED_Framebuffer_Drawpoint(x+i, y+j+k*8, (temp>>j & 0x01) ? 0:1); 
					}
				}
				break;
			}
			case 24: {
				for(i=0; i<12; i++) {//字宽为12
					temp = Ascii_12x24[Char - ' '][i+k*12];
					for (j=0; j<8; j++) {//画8个点
						if(mode) OLED_Framebuffer_Drawpoint(x+i, y+j+k*8, (temp>>j & 0x01) ? 1:0);
						else     OLED_Framebuffer_Drawpoint(x+i, y+j+k*8, (temp>>j & 0x01) ? 0:1); 
					}
				}
				break;
			}
		}
	}
}

2.3 在帧缓冲区绘制一串字符

        写入字符串可以照搬,和没有显存之前相差无异,只是在后面加上一个mode表示正/反显。

/**
  * @brief  OLED在帧缓冲绘制字符串
  * @param  x 行位置
  * @param  y 列位置
  * @param  Fontsize 字体大小
  * @param  String 显示字符串,
  * @param  mode 显示模式:1--正显  0--反显
  * @retval 无
  */
#include <string.h>
void OLED_Framebuffer_DrawString(uint8_t x, uint8_t y, uint16_t Fontsize, const char* String, uint8_t mode)
{
	uint8_t i, len;
	len = strlen(String);
	
	for(i=0; i<len; i++) {
		switch(Fontsize) {
			case 8:OLED_Framebuffer_DrawChar(x+i*6, y, Fontsize, String[i], mode);
			break;
			case 16:OLED_Framebuffer_DrawChar(x+i*8, y, Fontsize, String[i], mode);
			break;
			case 24:OLED_Framebuffer_DrawChar(x+i*12, y, Fontsize, String[i], mode);
			break;
		}
	}
}

 2.4 在帧缓冲区绘制一个汉字

        在原有的基础上,和写入字符一样,偏移八位即可。

/**
  * @brief  OLED在帧缓冲区绘制汉字
  * @param  x 行位置
  * @param  y 列位置
  * @param  Chinese 汉字,
  * @param  mode 显示模式:1--正显  0--反显
  * @retval 无
  */
void OLED_Framebuffer_DrawChinese(uint8_t x, uint8_t y, uint8_t Chinese, uint8_t mode)
{
	uint8_t i, j, k;
	uint8_t temp;
	
	for(k=0; k<2; k++) {
		for (i=0; i<16; i++) {//字宽为24
			temp = Chinese_16x16[Chinese][i+k*16];
			for (j=0; j<8; j++) {//画8个点
				if(mode) OLED_Framebuffer_Drawpoint(x+i, y+j+k*8, (temp>>j & 0x01) ? 1:0);
				else     OLED_Framebuffer_Drawpoint(x+i, y+j+k*8, (temp>>j & 0x01) ? 0:1); 
			}
		}
	}
}

2.5 在帧缓冲区绘制一张图片

        图片也一样,照猫画虎,直接写入。

/**
  * @brief  OLED显示图片
  * @param  mode 显示模式:1--正显  0--反显
  * @retval 无
  * @explain 无
  */
void OLED_Framebuffer_DrawImageBMG(uint8_t mode)
{
	uint8_t i, j, k;
	uint8_t temp;
	
	for(k=0; k<8; k++) {
		for (i=0; i<128; i++) {
			temp = ImageBMG64x128[i+k*128];
			for (j=0; j<8; j++) {//画8个点
				if(mode) OLED_Framebuffer_Drawpoint(i, j+k*8, (temp>>j & 0x01) ? 1:0);
				else     OLED_Framebuffer_Drawpoint(i, j+k*8, (temp>>j & 0x01) ? 0:1); 
			}
		}
	}
}

 2.6 在帧缓冲区绘制一条直线

        再加一个画直线函数,此函数做了简化处理,只绘制水平线和垂直线,没有正/反显。

/**
  * @brief  OLED在帧缓冲区绘制直线
  * @param  x1,y1 起点坐标
  * @param  x2,y2 终点坐标
  * @retval 此函数做了简化处理,只绘制水平线和垂直线
  */
void OLED_Framebuffer_DrawLine(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2)
{	
	uint16_t delta_x, delta_y, direction, a;
	
	//1、计算坐标增量
	delta_x = x2 - x1;
	delta_y = y2 - y1;
	
	//2、判断是否满足绘制条件(水平线、垂直线)
	if(delta_x && delta_y) return;//表示两个自增都不为0
	else if(!(delta_x || delta_y)) return;//表示两个自增都为0
	//当delta_y=0时,表示水平线;当delta_x=0时,表示垂直线
	if(delta_y == 0) { 
		direction = delta_x;
		a = x1;
	}
	else if(delta_x == 0) { 
		direction = delta_y;
		a = y1;
	}
	
	//3、开始绘制
	for(int i=a; i<direction+a; i++) {
		if(delta_x == 0) OLED_Framebuffer_DrawPoint(x1, i, 1);	
		else OLED_Framebuffer_DrawPoint(i, y1, 1);
	}
} 

2.7 在帧缓冲区绘制一个矩形 

        利用直线函数,画矩形

/**
  * @brief  OLED在帧缓冲区绘制矩形
  * @param  x1、x2 行位置
  * @param  y1、y2 列位置
  * @retval 无
  */
void OLED_Framebuffer_DrawRectangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2)
{
	OLED_Framebuffer_DrawLine(x1, y1, x2, y1);
	OLED_Framebuffer_DrawLine(x1, y1, x1, y2);
	OLED_Framebuffer_DrawLine(x1, y2, x2, y2);
	OLED_Framebuffer_DrawLine(x2, y1, x2, y2);
}

 2.8 在帧缓冲区绘制一个圆形

        最后再画一个圆形。

/**
  * @brief  OLED在帧缓冲区绘制圆
  * @param  x0、x0 圆心
  * @param  radius 列位置
  * @param  mode 显示模式:1--正显  0--反显
  * @retval 无
  */
void OLED_Framebuffer_DrawCircle(uint16_t x0, uint16_t y0, uint16_t radius, uint8_t mode)
{
	int a, b, di;
	a = 0;
	b = radius;	  
	di = 3-(radius<<1);             //判断下个点位置的标志
	while(a <= b) {
		OLED_Framebuffer_DrawPoint(x0+a, y0-b, mode);             //5
 		OLED_Framebuffer_DrawPoint(x0+b, y0-a, mode);             //0           
		OLED_Framebuffer_DrawPoint(x0+b, y0+a, mode);             //4               
		OLED_Framebuffer_DrawPoint(x0+a, y0+b, mode);             //6 
		OLED_Framebuffer_DrawPoint(x0-a, y0+b, mode);             //1       
 		OLED_Framebuffer_DrawPoint(x0-b, y0+a, mode);             
		OLED_Framebuffer_DrawPoint(x0-a, y0-b, mode);             //2             
		OLED_Framebuffer_DrawPoint(x0-b, y0-a, mode);             //7     	         
		a++;  
		if(di<0) di += 4*a+6;	//Bresenham画圆算法   
		else {
			di += 10+4*(a-b);   
			b--;
		}
	}
}

 3.数据写入

        虽然在帧缓冲区中绘制好了图像,但是并没有写到屏幕上,所有接下来的一步就是将帧缓冲区的数据写入屏幕上,可以直接使用之前写好的IIC通信协议将数据写入屏幕,不过之前的IIC通信协议高低电平的转变需要找到对应的位使用位运算或逻辑运算来实现,大大增加了单片机的负担,为了解决这样麻烦引入了位带操作。

3.1 位带操作 

        位带(Bit Band)是一种用于嵌入式系统中内存优化的技术。位带访问使得单个位的访问操作变得更加高效,有助于提高嵌入式系统的性能和资源利用率。STM32微控制器上的位带区是一种特殊的内存区域,用于对单个位进行读写操作。位带区在8位和16位的外设寄存器中使用,让STM32也可以像51单片机一样IO口置高置低,可以提供更高的速度和更灵活的位操作能力。在Cortex-M3权威指南中5.5节,有详细介绍:

        位带区的主要优势是可以通过使用C语言的位带操作符(例如*(bitband_base + bit_number))来直接对单个位进行操作,而无需进行读取-修改-写入的操作。这种直接访问位的方式可以提高代码的执行速度和效率,特别是在对多个位进行操作时。使用位带操作的公式:

//把“位带地址+位序号”转换成别名地址的宏
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr & 0xFFFFF)<<5)+(bitnum<<2))

//把该地址转换成一个指针
#define MEM_ADDR(addr) *((volatile unsigned long *) (addr))
    
//位带的实现---GPIOA/B/C IDR/ODR
#define PAin(bitnum)  MEM_ADDR(BITBAND(GPIOA_BASE + 0x08, bitnum))//GPIOA的IDR的第bitnum位的地址
#define PAout(bitnum) MEM_ADDR(BITBAND(GPIOA_BASE + 0x0C, bitnum))//GPIOA的ODR的第bitnum位的地址
#define PBin(bitnum)  MEM_ADDR(BITBAND(GPIOB_BASE + 0x08, bitnum))//GPIOB的IDR的第bitnum位的地址
#define PBout(bitnum) MEM_ADDR(BITBAND(GPIOB_BASE + 0x0C, bitnum))//GPIOB的ODR的第bitnum位的地址
#define PCin(bitnum)  MEM_ADDR(BITBAND(GPIOC_BASE + 0x08, bitnum))//GPIOB的IDR的第bitnum位的地址
#define PCout(bitnum) MEM_ADDR(BITBAND(GPIOC_BASE + 0x0C, bitnum))//GPIOB的ODR的第bitnum位的地址

在软件IIC中使用位带操作有以下好处:

  1. 简化代码:通过使用位带,可以直接对单个位进行操作,而不需要使用位运算或逻辑运算来实现。这样可以简化代码,提高代码的可读性和可维护性。

  2. 提高性能:由于位带操作是直接对单个位进行操作,而不需要对整个字节进行读取和写入,因此可以提高代码的执行效率和速度。

  3. 减少内存占用:使用位带可以减少对内存的占用,因为每个位都可以单独进行操作,而不需要为每个位都分配一个字节的内存空间。

  4. 方便调试:使用位带可以方便地对单个位进行调试和监测,可以更容易地定位和解决问题。

3.2 软件IIC使用位带操作

        从下面的宏定义可以看出来,使用位带操作和不使用位带操作有明显的区别。

//使用位带区域直接读写
#define IIC_W_SCL PBout(6)
#define IIC_W_SDA PBout(7)
#define IIC_R_SDA PBin(7)
//读写
read = IIC_R_SDA;
IIC_W_SDA = 1;

//不使用位带区域直接读写
#define IIC_W_SCL(x) GPIO_WriteBit(GPIOB, GPIO_Pin_6, (BitAction)(x))
#define IIC_W_SDA(x) GPIO_WriteBit(GPIOB, GPIO_Pin_7, (BitAction)(x))
#define IIC_R_SDA()  GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_7)
//读写
read = IIC_R_SDA();
IIC_W_SDA(1);

        软件IIC运行代码,IIC的起始信号、停止信号和应答信号和以前一样,读写也同样,不过为了简化代码,把应答信号放到写入函数内部去了。在之前的测试中,产生电平信号时不加延时也可以,但是使用位带操作后好像不行了,例如:在产生起始信号和停止信号时,直接操作GPIO管脚的高低电平,但是因为跳变太快必须得加2us的延时,不然无法产生可识别的时序,从而无法产生起始信号和停止信号。

/**
  * @brief  软件IIC起始信号
  * @param  无
  * @retval 无
  * @explain IIC开启需要在SCL(1)为高的时候拉低SDA(0)
  */
void MyIIC_Start(void)
{	
	IIC_W_SDA = 1;
	IIC_W_SCL = 1;
	delay_us(2);
	IIC_W_SDA = 0;
	delay_us(2);
	IIC_W_SCL = 0;//钳住总线
}

/**
  * @brief  软件IIC停止信号
  * @param  无
  * @retval 无
  * @explain IIC结束需要在SCL(1)为高的时候拉高SDA(1)
  */
void MyIIC_Stop(void)
{
	IIC_W_SDA = 0;
	IIC_W_SCL = 1;
	delay_us(2);
	IIC_W_SDA = 1;
	delay_us(2);
}

/**
  * @brief  软件IIC发送一个字节
  * @param  Byte 要发送的字节
  * @retval 无
  * @explain 主机在SCL(0)低的时候只会发送一位,从机在SCL(1)为高的时候一次也只接收一位
  */
void MyIIC_SendByte(uint8_t Byte)
{
	uint8_t i;
	
	for (i=0; i<8; i++) {
		IIC_W_SCL = 0;//主机开始写
		delay_us(2);
		IIC_W_SDA = (Byte<<i & 0x80) ? 1 : 0;
		delay_us(2);
		IIC_W_SCL = 1;//从机开始读
		delay_us(2);
	}
	IIC_W_SCL = 0;//钳住总线
	delay_us(2);
	//额外的时钟,不处理应答
	IIC_W_SDA = 0;
	delay_us(2);
	IIC_W_SCL = 1;
	delay_us(2);
	IIC_W_SCL = 0;//钳住总线
}

3.3 OLED写入命令/数据函数

        直接搬入。

/**
  * @brief  OLED 写命令
  * @param  Command 写入命令
  * @retval 无
  * @explain 无
  */
void OLED_Write_Command(uint8_t Command)
{
	MyIIC_Start();
	MyIIC_SendByte(0x78);  	//写入从机地址
	MyIIC_SendByte(0x00);  	//写入命令
	MyIIC_SendByte(Command); //写命令
	MyIIC_Stop();      		//发送停止信号
}
 
/**
  * @brief  OLED 写数据
  * @param  Data 写入数据
  * @retval 无
  * @explain 无
  */
void OLED_Write_Data(uint8_t Data)
{
	MyIIC_Start();
	MyIIC_SendByte(0x78);  	//写入从机地址
	MyIIC_SendByte(0x40);  	//写入命令
	MyIIC_SendByte(Data);   //写命令
	MyIIC_Stop();      		//发送停止信号
}

 3.4 OLED的各种命令

        这里介绍一下OLED的三种寻址方式,页寻址方式水平寻址方式垂直寻址方式,通过写入命令选择相应的寻址方式,因为页寻址方式和水平寻址方式两者编码相通,所有比较常用的,垂直寻址方式用于一些特殊的场所。

        在页地址模式(页寻址方式)下,读写SSD1306的GRAM后,列地址指针自动增加1。如果列地址指针到达列结束地址,则列地址指针将被重置为列起始地址,而页面地址指针将不会被更改。用户必须设置新的页面和列地址,才能访问下一页RAM内容(只用设置一个光标就可以写一整页,换页要另外设置起始光标)。页面寻址模式的Page和Col的移动顺序如下所示:

        在列地址模式(水平寻址方式)下,读写SSD1306的GRAM后,列地址指针自动增加1。如果列地址指针到达列结束地址,则将列地址指针重置为列起始地址,页地址指针增加1。水平寻址模式下的页面和列地址点的移动顺序如下所示(设置一个初始位置从头写到尾,适用于写整个屏幕)。当列和页面地址指针都到达结束地址时,指针将重置为列起始地址和页面起始地址。请看下图:

        在行地址模式(垂直寻址方式)下,读写SSD1306的GRAM后,页面地址指针自动增加1。如果页面地址指针到达页面结束地址,则页面地址指针重置为页面起始地址,列地址指针增加1。垂直寻址模式的页面和列地址点的移动顺序如下所示。当列和页面地址指针都到达结束地址时,指针将重置为列起始地址和页面起始地址(同样从头写到尾,适用于写整个屏幕)。

设置寻址模式命令与写入位置命令:

        在25个命令后加上这8个命令,或者直接修改前面的设置内存寻址模式,就只用添加后面六个命令了。 

static uint8_t OLED_Command[31] = 
{
	0xAE, //关闭显示
	0xD5, //设置时钟分频因子,震荡频率
	0x80, //[3:0],分频因子;[7:4],震荡频率
	0xA8, //设置驱动路数
	0X3F, //默认 0X3F(1/64) 
	0xD3, //设置显示偏移
	0X00, //默认为 0 
	0x40, //设置显示开始行 [5:0],行数. 
	0x8D, //电荷泵设置
	0x14, //bit2,开启/关闭
	0x20, //设置内存地址模式
	0x00, //[1:0],00列地址模式; 01行地址模式; 10页地址模式; 默认10;
	0xA1, //段重定义设置,bit0:0,0->0; 1,0->127;
	0xC8, //设置 COM 扫描方向;bit3:0,普通模式;1,重定义模式 COM[N-1]->COM0;N:驱动路数
	0xDA, //设置 COM 硬件引脚配置
	0x12, //[5:4]配置
	0x81, //对比度设置
	0xEF, //1~255;默认 0X7F (亮度设置,越大越亮)
	0xD9, //设置预充电周期
	0xf1, //[3:0],PHASE 1;[7:4],PHASE 2;
	0xDB, //设置 VCOMH 电压倍率
	0x30, //[6:4] 000,0.65*vcc;001,0.77*vcc;011,0.83*vcc;
	0xA4, //全局显示开启;bit0:1,开启;0,关闭;(白屏/黑屏)
	0xA6, //设置显示方式;bit0:1,反相显示;0,正常显示
	0xAF, //开启显示
	
	0x21, //设置列地址起始位置与终点位置
	0x00,
	0x7F,//0x00~0x7F = 0~127
	0x22,//设置页地址起始位置与终点位置
	0x00,
	0x07,//0x00~0x07 = 0~7
};

3.5 OLED初始化

        OLED初始化需要写入命令和写入数据两个函数,两个函数和之前一样,虽然这两个函数已经可以完成了现在所需要的操作,但是还是不够快,重复的发送地址和指令太浪费时间了,需要进行一次优化,直接将所有数据一次性全部写入。

//写命令
void OLED_Write_Command(uint8_t Command)
//写数据
void OLED_Write_Data(uint8_t Data);

         第一篇介绍IIC通信协议时说道,放在SDA行上的每个字节必须是8位长。每次传输可以传输的字节数不受限制。每个字节后面必须有一个确认位。首先使用最重要位(MSB)传输数据。如果从属服务器不能接收或传输另一个完整的数据字节,直到它执行了一些其他功能,例如服务一个内部中断,它可以保持时钟线SCL低,以迫使主服务器进入等待状态。当从服务器准备处理另一个字节并释放时钟线SCL时,继续数据传输。

        所以得到一个新的写数据函数,可以将帧缓冲区是数据一次性写入SSD1306中。 

/**
  * @brief  OLED 将帧缓冲区的数据一次性写入
  * @param  无
  * @retval 无
  * @explain 无
  */
void OLED_Write_Datarray(void)
{
	MyIIC_Start();
	MyIIC_SendByte(0x78);  	//写入从机地址
	MyIIC_SendByte(0x40);  	//写入命令
	for(int i=0; i<8; i++)
	    for(int j=0; j<128; j++)
	        MyIIC_SendByte(OLED_GRAM[i][j]);
	MyIIC_Stop();      		//发送停止信号
}

        还是标准的OLED屏幕初始化函数。 

/**
  * @brief  OLED初始化函数
  * @param  无
  * @retval 无
  */
void OLED_Init(void)
{
	//1、上电延时
	delay_ms(200);
	
	//2、软件IIC初始化
	MyIIC_Init();
	
	//3、写入OLED初始化命令
	for(int i=0; i<31; i++) 
		OLED_Write_Command(OLED_Command[i]);

	//4、清屏
	OLED_Clear();
}

/**
  * @brief  OLED清屏
  * @param  无
  * @retval 无
  */
void OLED_Clear(void)
{  
	OLED_Write_Datarray();
}

        更新屏幕和帧缓冲区清屏。

/**
  * @brief  OLED屏幕显示更新
  * @param  无
  * @retval 无
  * @explain 把帧缓冲区OLED_GRAM的内容更新到显示屏上
  */
void OLED_Update_FramebufferShow(void)
{
	OLED_Write_Datarray();
}

/**
  * @brief  OLED帧缓冲清屏
  * @param  无
  * @retval 无
  */
void OLED_Framebuffer_Clear(void)
{  
	uint8_t i, j;
	for (i = 0; i < 8; i++) {
		for(j = 0; j < 128; j++) {
			OLED_GRAM[i][j] = 0x00;
		}
	}
}

4.多级目录

        多级目录与之前的代码差别不大,可以直接照搬。

4.1 设计菜单界面

        菜单界面的设计。

/**
  * @brief  菜单界面函数
  * @param  无
  * @retval 无
  */
void Menu_Interface(void)
{
	char buff[50];
	
	sprintf(buff, "Date:%04d/%02d/%02d", 
	RTC_timedate.tm_year, RTC_timedate.tm_mon, RTC_timedate.tm_mday);
	OLED_Framebuffer_DrawString(10, 36,  8, buff, 1);
	sprintf(buff, "time:%02d:%02d:%02d", 
	RTC_timedate.tm_hour, RTC_timedate.tm_min, RTC_timedate.tm_sec);
	OLED_Framebuffer_DrawString(6,  8,  16, buff, 1);
}

4.2 设计功能选项界面

        因为在编写画点函数时用了正/反显的功能,所以这里可以省去">>",直接用正/反显更加美观。

/**
  * @brief  功能界面函数
  * @param  无
  * @retval 无
  */
void Function_Interface1(void)
{
	OLED_Framebuffer_DrawString(0, 0,  16, "String",   0);
	OLED_Framebuffer_DrawString(0, 16, 16, "Chinese",  1);
	OLED_Framebuffer_DrawString(0, 32, 16, "ImageBMG", 1);
	OLED_Framebuffer_DrawString(0, 48, 16, "Graphics", 1);
}
void Function_Interface2(void)
{
	OLED_Framebuffer_DrawString(0, 0,  16, "String",   1);
	OLED_Framebuffer_DrawString(0, 16, 16, "Chinese",  0);
	OLED_Framebuffer_DrawString(0, 32, 16, "ImageBMG", 1);
	OLED_Framebuffer_DrawString(0, 48, 16, "Graphics", 1);
}
void Function_Interface3(void)
{
	OLED_Framebuffer_DrawString(0, 0,  16, "String",   1);
	OLED_Framebuffer_DrawString(0, 16, 16, "Chinese",  1);
	OLED_Framebuffer_DrawString(0, 32, 16, "ImageBMG", 0);
	OLED_Framebuffer_DrawString(0, 48, 16, "Graphics", 1);
}
void Function_Interface4(void)
{
	OLED_Framebuffer_DrawString(0, 0,  16, "String",   1);
	OLED_Framebuffer_DrawString(0, 16, 16, "Chinese",  1);
	OLED_Framebuffer_DrawString(0, 32, 16, "ImageBMG", 1);
	OLED_Framebuffer_DrawString(0, 48, 16, "Graphics", 0);
}

 4.3 设计执行功能界面

        执行功能,同样无区别。

/**
  * @brief  功能设置界面函数
  * @param  设置有三种状态
  * @retval 无
  */
void Function_Interface5(void)
{
	OLED_Framebuffer_DrawString(0, 0,  8,  "ABCD", 1);
	OLED_Framebuffer_DrawString(0, 8,  16, "ABCD", 1);
	OLED_Framebuffer_DrawString(0, 24, 24, "ABCD", 1);
}
void Function_Interface6(void)
{   
	for(int i=0; i<4; i++) {
		OLED_Framebuffer_DrawChinese(32+i*16, 24, i, 1);// 点
	}

}
void Function_Interface7(void)
{
	OLED_Framebuffer_DrawImageBMG(0);
}
void Function_Interface8(void)
{
	OLED_Framebuffer_DrawRectangle(16,16,112,48);
	OLED_Framebuffer_DrawCircle(64,32,16);
	OLED_Framebuffer_DrawCircle(64,32,15);
}

 4.4 设计界面任务调度表

        任务调度表,同样无区别。

uint8_t taskIndex = 0;	//初始任务
//任务调度表
Menu_table_t taskTable[] =
{
    //菜单界面函数 -- 一级界面
    {0, 1, 0, 1, Menu_Interface}, 
    //功能界面函数 -- 二级界面
    {1, 5, 2, 0, Function_Interface1},
    {2, 6, 3, 0, Function_Interface2},
    {3, 7, 4, 0, Function_Interface3},
	{4, 8, 1, 0, Function_Interface4},
	//功能设置界面函数 -- 三级界面
	{5, 5, 5, 1, Function_Interface5},
	{6, 6, 6, 2, Function_Interface6},
	{7, 7, 7, 3, Function_Interface7},
	{8, 8, 8, 4, Function_Interface8},
};

5.主函数

        最后,就是主函数,在执行函数下面加一个显存刷新函数,就可以完成显示。

int main(void)
{
	delay_Init();//延时函数初始化
	USART1_Init();//串口函数初始化
	TIME2_Init();//定时器辅助串口接收初始化
	RTC_Init();//实时时钟初始化
	TIME4_Init();//定时器辅助按键实现长短按初始化
	
	LED_Init();
	KEY_EXTI_Init();
	PWM_Init();
	OLED_Init();
	
	printf("标准库 -- 位带+帧缓冲区优化刷新速率\r\n");
	while(1) {
		if(g_APPTOIN_MUTEX) {
			taskTable[taskIndex].Current_Operation();//执行函数
			OLED_Update_FramebufferShow();//更新帧缓冲区数据到OLED显示
		}
	}
}

        经过测试发现屏幕的刷新是12帧,好像更慢了。。。哈哈

效果演示:

OLED优化刷新速率

源码分享:

链接:https://pan.baidu.com/s/1CXnW-ZW7TqTC_2xbZyo9Ew?pwd=jgtz 
提取码:jgtz

物联沃分享整理
物联沃-IOTWORD物联网 » STM32 OLED屏软件IIC刷新速率优化(四)

发表评论