【前言】
今天我在做Telink TL7218开发板中,使用ssd1306进行显示。在使用中,进行了一些代码的优化,现记录如下。
【SSD1306常见刷新方式】
1、按页刷新:将屏幕垂直方向划分为多个页(一般 8 页,每页 8 行像素),每次刷新一个页的数据,逐页进行操作。这种方式在处理小部分数据更新时较为灵活,适合只更新部分屏幕内容的场景。 2、整屏刷新:一次性将整个屏幕的显示数据发送给 SSD1306,这种方式适合需要快速更新整个屏幕内容的情况。 3、区域刷新:只刷新屏幕上指定的某个区域,可减少数据传输量,提高刷新效率,适用于局部内容频繁更新的场景。
在1,2种方式中,如果是整屏刷新,那么按整屏刷新的方式是要快于按页来刷新的,分析如下:
按页刷新的代码:
// 按页刷新逻辑 for (uint8_t page = 0; page < 8; page++) { // 设置页地址 OLED_WR_Byte(0xB0 + page, OLED_CMD); // 设置列地址低 4 位 OLED_WR_Byte(0x00, OLED_CMD); // 设置列地址高 4 位 OLED_WR_Byte(0x10, OLED_CMD); // 向当前页写入数据,这里假设要写入的数据全为 0x00 for (uint8_t col = 0; col < 128; col++) { OLED_WR_Byte(0x00, OLED_DATA); } }
整屏刷新的代码:
// 一次性发送整屏数据 void SSD1306_FullScreenRefresh(uint8_t *data) { // 设置起始地址 OLED_WR_Byte(0x21, 0); // 设置列地址 OLED_WR_Byte(0x00, 0); // 起始列地址 OLED_WR_Byte(SSD1306_WIDTH - 1, 0); // 结束列地址 OLED_WR_Byte(0x22, 0); // 设置页地址 OLED_WR_Byte(0x00, 0); // 起始页地址 OLED_WR_Byte(SSD1306_PAGE_COUNT - 1, 0); // 结束页地址 // 发送整屏数据 for (int i = 0; i < SSD1306_WIDTH * SSD1306_PAGE_COUNT; i++) { OLED_WR_Byte(data[i], 1); } }
两个函数中,我们可以明显的看到按页刷新,需要分8次向SSD1306写入设置地址,再发送一页即128位数据。如果是整屏的刷新,那么我们只需要设置一次起始地址。而在发送命令中,是一次写入两字字节的命令,显然占用的时间是多于整屏刷新的时间的。
【两个不同刷新方式的初始设置】
1. 内存寻址模式设置 按页初始化:通常会将内存寻址模式设置为页寻址模式,使用 0x20 命令并跟上 0x02 来选择页寻址模式。在页寻址模式下,数据是按页进行组织和传输的,每次只能操作一个页的数据。 整屏刷新:一般会将内存寻址模式设置为水平寻址模式,使用 0x20 命令并跟上 0x00 来选择水平寻址模式。在水平寻址模式下,数据可以从左到右、从上到下依次填充整个屏幕,适合一次性发送整屏数据。 2. 数据传输方式 按页初始化:在按页初始化时,数据传输是按页进行的,每次只处理一个页的数据。需要先设置页地址和列地址,然后逐列写入该页的数据,处理完一页后再处理下一页。 整屏刷新:整屏刷新则是一次性将整个屏幕的数据发送给 SSD1306。需要先设置好起始列地址、结束列地址、起始页地址和结束页地址,然后连续发送整个屏幕的数据。
【示列代码的对比】
按页初始化代码示例
// 按页初始化函数 void SSD1306_PageInit() { // 基本初始化设置 SSD1306_SendCommand(0xAE); // 关闭显示 SSD1306_SendCommand(0x20); // 设置内存寻址模式 SSD1306_SendCommand(0x02); // 页寻址模式 SSD1306_SendCommand(0xB0); // 设置页地址 SSD1306_SendCommand(0xC8); // 设置扫描方向 SSD1306_SendCommand(0x00); // 设置列地址低 4 位 SSD1306_SendCommand(0x10); // 设置列地址高 4 位 SSD1306_SendCommand(0x40); // 设置显示起始行 SSD1306_SendCommand(0x81); // 设置对比度 SSD1306_SendCommand(0xFF); // 最大对比度 SSD1306_SendCommand(0xA1); // 设置段重映射 SSD1306_SendCommand(0xA6); // 设置正常显示 SSD1306_SendCommand(0xA8); // 设置多路复用率 SSD1306_SendCommand(0x3F); // 1/64 多路复用 SSD1306_SendCommand(0xA4); // 恢复整体显示 SSD1306_SendCommand(0xD3); // 设置显示偏移 SSD1306_SendCommand(0x00); // 无偏移 SSD1306_SendCommand(0xD5); // 设置时钟分频比/振荡器频率 SSD1306_SendCommand(0xF0); // 设置分频比 SSD1306_SendCommand(0xD9); // 设置预充电周期 SSD1306_SendCommand(0x22); // 设置预充电周期 SSD1306_SendCommand(0xDA); // 设置 COM 引脚硬件配置 SSD1306_SendCommand(0x12); // 设置 COM 引脚硬件配置 SSD1306_SendCommand(0xDB); // 设置 VCOMH 取消选择级别 SSD1306_SendCommand(0x20); // 设置 VCOMH 取消选择级别 SSD1306_SendCommand(0x8D); // 设置电荷泵 SSD1306_SendCommand(0x14); // 启用电荷泵 SSD1306_SendCommand(0xAF); // 开启显示 }
整屏刷新初始化代码示例
// 整屏刷新初始化函数 void SSD1306_FullScreenInit() { // 基本初始化设置 SSD1306_SendCommand(0xAE); // 关闭显示 SSD1306_SendCommand(0x20); // 设置内存寻址模式 SSD1306_SendCommand(0x00); // 水平寻址模式 SSD1306_SendCommand(0xB0); // 设置页地址 SSD1306_SendCommand(0xC8); // 设置扫描方向 SSD1306_SendCommand(0x00); // 设置列地址低 4 位 SSD1306_SendCommand(0x10); // 设置列地址高 4 位 SSD1306_SendCommand(0x40); // 设置显示起始行 SSD1306_SendCommand(0x81); // 设置对比度 SSD1306_SendCommand(0xFF); // 最大对比度 SSD1306_SendCommand(0xA1); // 设置段重映射 SSD1306_SendCommand(0xA6); // 设置正常显示 SSD1306_SendCommand(0xA8); // 设置多路复用率 SSD1306_SendCommand(0x3F); // 1/64 多路复用 SSD1306_SendCommand(0xA4); // 恢复整体显示 SSD1306_SendCommand(0xD3); // 设置显示偏移 SSD1306_SendCommand(0x00); // 无偏移 SSD1306_SendCommand(0xD5); // 设置时钟分频比/振荡器频率 SSD1306_SendCommand(0xF0); // 设置分频比 SSD1306_SendCommand(0xD9); // 设置预充电周期 SSD1306_SendCommand(0x22); // 设置预充电周期 SSD1306_SendCommand(0xDA); // 设置 COM 引脚硬件配置 SSD1306_SendCommand(0x12); // 设置 COM 引脚硬件配置 SSD1306_SendCommand(0xDB); // 设置 VCOMH 取消选择级别 SSD1306_SendCommand(0x20); // 设置 VCOMH 取消选择级别 SSD1306_SendCommand(0x8D); // 设置电荷泵 SSD1306_SendCommand(0x14); // 启用电荷泵 SSD1306_SendCommand(0xAF); // 开启显示 }
经过上述代码的对比,应该非常之清楚了。
【整屏发送代码的优化】
以TL7218的发送为例:
// 一次性发送整屏数据 void SSD1306_FullScreenRefresh(uint8_t *data) { // 设置起始地址 OLED_WR_Byte(0x21, 0); // 设置列地址 OLED_WR_Byte(0x00, 0); // 起始列地址 OLED_WR_Byte(SSD1306_WIDTH - 1, 0); // 结束列地址 OLED_WR_Byte(0x22, 0); // 设置页地址 OLED_WR_Byte(0x00, 0); // 起始页地址 OLED_WR_Byte(SSD1306_PAGE_COUNT - 1, 0); // 结束页地址 // 发送整屏数据 for (int i = 0; i < SSD1306_WIDTH * SSD1306_PAGE_COUNT; i++) { OLED_WR_Byte(data[i], 1); } }
我们使用整屏发送修改为一次性传输:
发送数之前,发送的第一个数据为0x40,那么我们需要重新创建一个数据,这个数组为1025长度,即整屏1024字数据+1个命令。
uint8_t tx_buff[1025]={0x00}; tx_buff[0] = 0x40;//发送数据的指令码 for(int i= 0; i<1024;i++){ tx_buff[i+1] = data[i]; } i2c_master_write(SSD1306_ADDR, tx_buff, 1025);
这次的话,又相比
OLED_WR_Byte(data[i], 1);
又要快了不少。
【优化方式2】
使用for来拷贝数据,还是有提升的空间的。我这次使用memcpy来优化,由于是我们是将tx_buff的第2次开始拷贝数据,所以我将for替换为:
memcpy(&tx_buff[1], data, 1024);
这样速度是不是又快了许多,而且代码也会整洁许多。
【终级优化】
在优化方式2中,我们还需要重新申请一个1025的数组,这样对于资源紧张的MCU还是有非常大的开销的。因此我继续进行了优化。
在我们代码初始化时,我们创建了一个全局数组:
uint8_t OLED_buffer[1024] = {0};
用于做为ssd1306的显存,我们在画点画线等等,都是在这个数组里进行操作,最后我们刷新的,直接更新这个数组到ssd1306上。
而我们在整屏刷新时,再申请一个tx_buff[1025]的数组,这个两个数组唯一的不同就是在OLED_buffer最前面添加了一个0x40的写数据的命令。因此,大有改进的空间,我想办法把他们两个数组指向同一个内存空间。根据这样的方法,我做了如下的优化:
1、申请一个Flash_OLED_Buffer[1025]的数组,让我们发送整屏数据时,地址指向这个首地址,然后,我再申请一个指针,让他的地址指向Flash_OLED_Buffer地址+1,这样我们就可以不需要在发送整屏数据时再申请一个局部变量了。节约了内存空间中,又提升了速度。
终级代码:
uint8_t Flash_OLED_Buffer[1025] = {0}; //做为发送整屏数据的缓存 uint8_t * OLED_buffer = Flash_OLED_Buffer+1 ; //做为显存的空间 //发送代码 Flash_OLED_Buffer[0] = 0x40; //发送数据的指令 i2c_master_write(SSD1306_ADDR, Flash_OLED_Buffer, SSD1306_WIDTH * OLED_PAGE_SIZE +1);
【小结】
结合ssd1306的特性,以及对内存地址的理解,可以对刷新做出非常好的优化。
这只是我个人的一些建议,如有不足,还希望大佬们批评指正。