在嵌入式开发领域,LCD显示屏驱动是基础但极其关键的环节。这次要啃的硬骨头是HS96L01B4S03这款96x64像素的OLED屏,搭配SSD1335驱动芯片。这种组合在智能穿戴设备、工业HMI界面中很常见,但官方资料往往只有干巴巴的datasheet,真正调试时各种坑都得自己趟。
选择STM32XX系列单片机是因为其性价比和生态支持——72MHz主频足够流畅刷屏,标准库和HAL库都成熟稳定。但要注意,不同STM32型号的GPIO速度等级、SPI时钟配置会直接影响显示效果,这个后面会重点说明。
SSD1335支持4线SPI和8位并行接口,考虑到STM32的引脚资源,我们选择硬件SPI模式。具体接线:
关键细节:RES引脚必须接GPIO!有些开发板默认接VCC,会导致初始化序列无法正确执行。实测发现保持至少10ms的低电平复位脉冲才能可靠初始化。
SSD1335的初始化比普通LCD复杂得多,需要严格按照时序发送26条命令。核心命令包括:
c复制0xAE, // 关闭显示
0xA0, 0x72, // 设置重映射格式 (RGB排列方式)
0xA1, 0x00, // 设置显示起始行
0xA2, 0x00, // 设置显示偏移
0xA4, // 正常显示模式
0xB3, 0xF1, // 设置时钟分频
0x8A, 0x3F, // 预充电电流
0x8B, 0x3F, // 预充电周期
0xBB, 0x3F, // VCOMH电压
0xBE, 0x3E, // VCOMH电平
0x87, 0x06, // 主电流控制
特别注意0xA0参数:0x72表示采用RGB像素排列(而非BGR),这个值不对会导致颜色完全错乱。不同批次的屏幕可能配置不同,建议先用0x72和0x76都试一下。
SSD1335没有内置显存,需要开发者自行维护帧缓冲区。推荐两种方案:
c复制uint8_t framebuffer[96][8]; // 每列8像素对应1字节
性能实测:在STM32F103上,全缓冲刷新需2.3ms(SPI时钟18MHz),人眼无明显闪烁。如果出现撕裂现象,建议开启垂直消隐中断同步刷新。
c复制void DrawPixel(uint8_t x, uint8_t y, uint16_t color) {
if(x >=96 || y >=64) return;
// RGB565转SSD1335的16位格式
uint8_t color_hi = ((color >> 8) & 0xF8) | (color >> 13);
uint8_t color_lo = ((color << 3) & 0xE0) | ((color >> 3) & 0x1F);
// 计算显存位置
uint8_t page = y / 8;
uint8_t bit_mask = 1 << (y % 8);
// 更新显存
if(color != 0x0000) {
framebuffer[x][page] |= bit_mask;
} else {
framebuffer[x][page] &= ~bit_mask;
}
}
直接逐像素绘制效率太低,推荐使用SSD1335的连续写入模式:
c复制void FillRect(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2, uint16_t color) {
SetCommand(0x15); // 设置列地址
SendData(x1); SendData(x2);
SetCommand(0x75); // 设置行地址
SendData(y1); SendData(y2);
SetCommand(0x5C); // 进入数据连续写入模式
for(uint16_t i=0; i<(x2-x1+1)*(y2-y1+1); i++) {
SendData(color >> 8); SendData(color & 0xFF);
}
}
实测比像素级操作快15倍以上,特别适合全屏清屏操作。
STM32的SPI时钟不是越高越好!当超过20MHz时:
c复制hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; // 18MHz @72MHz主频
hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; // 必须设为第二边沿采样
hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH;
要实现60FPS流畅动画,必须上DMA:
c复制uint8_t dma_buffer[2][768]; // 双缓冲
volatile uint8_t active_buffer = 0;
void RefreshScreen() {
DMA_Cmd(DMA1_Channel3, DISABLE);
memcpy(dma_buffer[active_buffer], framebuffer, 768);
DMA1_Channel3->CMAR = (uint32_t)dma_buffer[active_buffer];
DMA1_Channel3->CNDTR = 768;
DMA_Cmd(DMA1_Channel3, ENABLE);
active_buffer ^= 1; // 切换缓冲
}
配合DMA传输完成中断,可实现无撕裂的流畅显示。
SSD1335特有的电荷积累问题:
c复制// 在初始化时加入这些命令
0x82, 0x01, // 预充电电压
0x83, 0x3F, // VSL电压等级
0x84, 0x3F, // VSL激活周期
动态调整刷新率:
c复制void SetRefreshRate(uint8_t fps) {
uint8_t div = 100/fps - 1;
SendCommand(0xB3); // 设置时钟分频
SendData(div);
}
在待机时可降至5fps,实测电流从12mA降至3.2mA。
利用STM32的硬件SPI+DMA+GPIO快速切换:
c复制void FastSendData(uint8_t* data, uint16_t len) {
GPIO_ResetBits(GPIOC, GPIO_Pin_4); // CS拉低
GPIO_SetBits(GPIOB, GPIO_Pin_0); // DC=1(数据)
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)data;
DMA_InitStructure.DMA_BufferSize = len;
DMA_Init(DMA1_Channel3, &DMA_InitStructure);
SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE);
while(DMA_GetFlagStatus(DMA1_FLAG_TC3) == RESET);
}
比标准库快3倍,特别适合全屏刷新。
针对小尺寸OLED的特性:
c复制void DrawCharAA(uint8_t x, uint8_t y, char c) {
for(uint8_t i=0; i<4; i++) {
uint8_t col = font_4x6[c][i];
for(uint8_t j=0; j<6; j++) {
if(col & (1<<j)) {
DrawPixel(x+i, y+j, WHITE);
} else if(GetPixel(x+i, y+j-1) || GetPixel(x+i-1, y+j)) {
DrawPixel(x+i, y+j, GRAY); // 边缘灰度过渡
}
}
}
}
调试这个驱动时最深的体会是:OLED对时序的敏感度远超普通LCD。有一次因为SPI相位设置偏差0.5个时钟周期,导致显示出现规律性噪点,折腾了两天才定位到问题。后来养成了习惯——每次修改时序参数后,都要用逻辑分析仪抓取SPI波形对照手册检查。