1. 项目概述
HS13L01WZ01是一款基于SH1106驱动芯片的单色OLED显示屏,采用被动矩阵技术,分辨率为128×64像素。这款显示屏在嵌入式系统中应用广泛,尤其适合需要低功耗、高对比度显示的场合。我在最近的一个智能家居控制器项目中使用了这款显示屏,发现它的显示效果相当不错,但驱动开发过程中也遇到了一些值得分享的问题。
作为嵌入式开发者,我们经常需要为各种外设编写驱动程序。LCD驱动看似简单,但实际上涉及到硬件接口、时序控制、显存管理等多个技术要点。本文将基于STM32平台,详细介绍如何为HS13L01WZ01显示屏开发完整的驱动方案。
2. 硬件接口设计
2.1 显示屏引脚定义
HS13L01WZ01采用标准的16引脚排针接口,各引脚定义如下:
| 引脚编号 | 符号 | 功能描述 |
|---|---|---|
| 1 | VSS | 地(GND) |
| 2 | VDD | 电源(3.3V) |
| 3 | VCC | 面板驱动电压(需升压) |
| 4 | D/C# | 数据/命令选择 |
| 5 | R/W# | 读写选择(SPI模式下接地) |
| 6 | E | 使能信号(SPI模式下接CS) |
| 7-14 | DB0-DB7 | 数据线(SPI模式下可简化) |
| 15 | CS# | 片选(低电平有效) |
| 16 | RES# | 复位(低电平有效) |
2.2 接口模式选择
SH1106支持并行8位、串行SPI和I2C三种接口模式。考虑到STM32的GPIO资源有限,我推荐使用SPI模式,原因如下:
- 接线简单,只需4根线(SCK, MOSI, CS, DC)
- 占用MCU资源少
- 传输速度足够满足刷新需求
- STM32硬件SPI性能稳定
在SPI模式下,我们需要将R/W#引脚接地,E引脚作为片选(CS)使用。
3. 软件驱动实现
3.1 底层硬件抽象层
首先定义硬件接口相关的宏和函数:
c复制// 引脚定义(根据实际连接修改)
#define OLED_DC_PIN GPIO_PIN_0
#define OLED_DC_PORT GPIOA
#define OLED_CS_PIN GPIO_PIN_1
#define OLED_CS_PORT GPIOA
#define OLED_RES_PIN GPIO_PIN_2
#define OLED_RES_PORT GPIOA
// SPI实例(根据实际使用修改)
extern SPI_HandleTypeDef hspi1;
// 基本操作宏
#define OLED_DC_LOW() HAL_GPIO_WritePin(OLED_DC_PORT, OLED_DC_PIN, GPIO_PIN_RESET)
#define OLED_DC_HIGH() HAL_GPIO_WritePin(OLED_DC_PORT, OLED_DC_PIN, GPIO_PIN_SET)
#define OLED_CS_LOW() HAL_GPIO_WritePin(OLED_CS_PORT, OLED_CS_PIN, GPIO_PIN_RESET)
#define OLED_CS_HIGH() HAL_GPIO_WritePin(OLED_CS_PORT, OLED_CS_PIN, GPIO_PIN_SET)
#define OLED_RES_LOW() HAL_GPIO_WritePin(OLED_RES_PORT, OLED_RES_PIN, GPIO_PIN_RESET)
#define OLED_RES_HIGH() HAL_GPIO_WritePin(OLED_RES_PORT, OLED_RES_PIN, GPIO_PIN_SET)
// SPI发送函数
static void SPI_Send(uint8_t *data, uint16_t size) {
HAL_SPI_Transmit(&hspi1, data, size, HAL_MAX_DELAY);
}
3.2 初始化序列
SH1106需要特定的初始化序列才能正常工作。以下是经过验证的初始化代码:
c复制void SH1106_Init(void) {
// 硬件复位
OLED_RES_LOW();
HAL_Delay(10);
OLED_RES_HIGH();
HAL_Delay(10);
// 发送初始化命令
SH1106_WriteCommand(0xAE); // 关闭显示
SH1106_WriteCommand(0xD5); // 设置显示时钟分频
SH1106_WriteCommand(0x80);
SH1106_WriteCommand(0xA8); // 设置多路复用比例
SH1106_WriteCommand(0x3F);
SH1106_WriteCommand(0xD3); // 设置显示偏移
SH1106_WriteCommand(0x00);
SH1106_WriteCommand(0x40); // 设置起始行
SH1106_WriteCommand(0x8D); // 电荷泵设置
SH1106_WriteCommand(0x14); // 启用内部电荷泵
SH1106_WriteCommand(0x20); // 内存地址模式
SH1106_WriteCommand(0x00); // 水平地址模式
SH1106_WriteCommand(0xA1); // 段重映射设置
SH1106_WriteCommand(0xC8); // COM输出扫描方向
SH1106_WriteCommand(0xDA); // COM引脚硬件配置
SH1106_WriteCommand(0x12);
SH1106_WriteCommand(0x81); // 对比度控制
SH1106_WriteCommand(0xCF);
SH1106_WriteCommand(0xD9); // 预充电周期
SH1106_WriteCommand(0xF1);
SH1106_WriteCommand(0xDB); // VCOMH取消选择级别
SH1106_WriteCommand(0x40);
SH1106_WriteCommand(0xA4); // 显示全部打开
SH1106_WriteCommand(0xA6); // 正常显示(非反转)
SH1106_WriteCommand(0xAF); // 开启显示
// 清屏
SH1106_Clear();
SH1106_Update();
}
注意:不同批次的SH1106芯片可能需要微调初始化参数,特别是对比度(0x81)和预充电周期(0xD9)的值。如果显示效果不理想,可以尝试调整这些参数。
3.3 显存管理
SH1106内部没有集成显存,需要外部MCU提供显示缓冲区。我们定义一个128x64的缓冲区,对应屏幕的每个像素:
c复制#define OLED_WIDTH 128
#define OLED_HEIGHT 64
#define OLED_PAGES (OLED_HEIGHT/8)
static uint8_t oled_buffer[OLED_PAGES][OLED_WIDTH];
void SH1106_Clear(void) {
memset(oled_buffer, 0, sizeof(oled_buffer));
}
void SH1106_Update(void) {
for(uint8_t page = 0; page < OLED_PAGES; page++) {
SH1106_WriteCommand(0xB0 + page); // 设置页地址
SH1106_WriteCommand(0x02); // 设置列地址低4位
SH1106_WriteCommand(0x10); // 设置列地址高4位
OLED_DC_HIGH(); // 数据模式
OLED_CS_LOW();
for(uint8_t col = 0; col < OLED_WIDTH; col++) {
uint8_t data = oled_buffer[page][col];
SPI_Send(&data, 1);
}
OLED_CS_HIGH();
}
}
4. 高级功能实现
4.1 基本绘图函数
有了显存管理,我们可以实现各种绘图函数。以下是几个基本函数的实现:
c复制void SH1106_DrawPixel(uint8_t x, uint8_t y, uint8_t color) {
if(x >= OLED_WIDTH || y >= OLED_HEIGHT) return;
if(color) {
oled_buffer[y/8][x] |= (1 << (y%8));
} else {
oled_buffer[y/8][x] &= ~(1 << (y%8));
}
}
void SH1106_DrawLine(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t color) {
int16_t dx = abs(x1 - x0);
int16_t dy = abs(y1 - y0);
int16_t sx = (x0 < x1) ? 1 : -1;
int16_t sy = (y0 < y1) ? 1 : -1;
int16_t err = dx - dy;
while(1) {
SH1106_DrawPixel(x0, y0, color);
if(x0 == x1 && y0 == y1) break;
int16_t e2 = 2 * err;
if(e2 > -dy) {
err -= dy;
x0 += sx;
}
if(e2 < dx) {
err += dx;
y0 += sy;
}
}
}
void SH1106_DrawRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t color) {
SH1106_DrawLine(x, y, x+w-1, y, color);
SH1106_DrawLine(x, y+h-1, x+w-1, y+h-1, color);
SH1106_DrawLine(x, y, x, y+h-1, color);
SH1106_DrawLine(x+w-1, y, x+w-1, y+h-1, color);
}
void SH1106_FillRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t color) {
for(uint8_t i = 0; i < h; i++) {
SH1106_DrawLine(x, y+i, x+w-1, y+i, color);
}
}
4.2 字符和字符串显示
要在屏幕上显示文字,我们需要一个字体库。以下是使用8x16点阵字体的实现:
c复制// 8x16 ASCII字模(只显示部分)
static const uint8_t Font8x16[][16] = {
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, // 空格
{0x00,0x00,0x18,0x3C,0x3C,0x3C,0x18,0x18,0x18,0x00,0x18,0x18,0x00,0x00,0x00,0x00}, // !
// 其他字符定义...
};
void SH1106_DrawChar(uint8_t x, uint8_t y, char c, uint8_t color) {
if(c < 32 || c > 127) return; // 只支持可打印ASCII字符
const uint8_t *font = Font8x16[c-32];
for(uint8_t row = 0; row < 16; row++) {
uint8_t line = font[row];
for(uint8_t col = 0; col < 8; col++) {
if(line & (1 << col)) {
SH1106_DrawPixel(x + col, y + row, color);
}
}
}
}
void SH1106_DrawString(uint8_t x, uint8_t y, const char *str, uint8_t color) {
while(*str) {
SH1106_DrawChar(x, y, *str++, color);
x += 8;
if(x > OLED_WIDTH - 8) {
x = 0;
y += 16;
}
}
}
提示:实际项目中,建议将完整字库存放在外部Flash或SD卡中,以节省MCU的RAM空间。也可以使用更小的字体(如6x8)来显示更多内容。
5. 性能优化技巧
5.1 局部刷新优化
全屏刷新(128x64)需要传输1024字节数据,在SPI时钟为8MHz时大约需要1ms。对于动态显示应用,我们可以实现局部刷新来提升性能:
c复制void SH1106_UpdateArea(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1) {
uint8_t page_start = y0 / 8;
uint8_t page_end = y1 / 8;
for(uint8_t page = page_start; page <= page_end; page++) {
SH1106_WriteCommand(0xB0 + page); // 设置页地址
SH1106_WriteCommand(0x02 + (x0 & 0x0F)); // 设置列地址低4位
SH1106_WriteCommand(0x10 + ((x0 >> 4) & 0x0F)); // 设置列地址高4位
OLED_DC_HIGH(); // 数据模式
OLED_CS_LOW();
for(uint8_t col = x0; col <= x1; col++) {
uint8_t data = oled_buffer[page][col];
SPI_Send(&data, 1);
}
OLED_CS_HIGH();
}
}
5.2 双缓冲技术
对于动画或快速变化的界面,可以考虑使用双缓冲技术:
c复制static uint8_t oled_buffer_front[OLED_PAGES][OLED_WIDTH];
static uint8_t oled_buffer_back[OLED_PAGES][OLED_WIDTH];
void SH1106_SwapBuffers(void) {
memcpy(oled_buffer_front, oled_buffer_back, sizeof(oled_buffer_front));
SH1106_Update();
}
// 所有绘图操作在back buffer上进行
void SH1106_DrawPixel(uint8_t x, uint8_t y, uint8_t color) {
if(x >= OLED_WIDTH || y >= OLED_HEIGHT) return;
if(color) {
oled_buffer_back[y/8][x] |= (1 << (y%8));
} else {
oled_buffer_back[y/8][x] &= ~(1 << (y%8));
}
}
6. 常见问题与解决方案
6.1 显示模糊或对比度不佳
现象:屏幕显示模糊,对比度不足,或者有重影。
可能原因及解决方案:
- 电源问题:确保VDD(3.3V)和VCC(升压电压)稳定。可以在VCC引脚加一个10μF电容。
- 初始化参数不当:调整0x81(对比度)和0xD9(预充电周期)命令的参数值。
- 复位时序问题:确保复位脉冲宽度足够(至少10μs),复位后等待足够时间(>100ms)再初始化。
6.2 显示内容错位
现象:显示内容左右或上下偏移,部分内容显示在屏幕外。
解决方案:
- 检查0xD3(显示偏移)命令的设置,通常设为0x00。
- 确认0xA1(段重映射)和0xC8(COM扫描方向)的设置是否符合硬件连接。
- 检查物理连接,确保所有信号线接触良好。
6.3 SPI通信失败
现象:屏幕无任何显示,或者显示乱码。
排查步骤:
- 用逻辑分析仪或示波器检查SPI信号,确认时钟、数据线波形正常。
- 检查CS和DC信号时序是否符合SH1106要求。
- 确认SPI模式设置正确(CPOL=0, CPHA=0)。
- 降低SPI时钟频率测试(如从8MHz降到1MHz),排除信号完整性问题。
7. 实际应用案例
在我的智能家居控制器项目中,HS13L01WZ01作为人机交互界面,需要显示以下内容:
- 当前温湿度数据
- 设备状态图标
- 简易菜单系统
为了实现流畅的界面效果,我采用了以下优化措施:
- 将屏幕分为静态区域和动态区域,静态区域(如边框、标题)只在初始化时刷新一次
- 动态数据(如温湿度值)使用局部刷新,每2秒更新一次
- 菜单切换时使用整页刷新,确保显示一致性
- 使用图标缓存技术,避免重复渲染固定图标
经过优化后,系统在STM32F103C8T6(72MHz)上运行流畅,界面刷新无明显闪烁,整体功耗控制在合理范围内。