1. SPI通信基础与OLED硬件连接刚拿到0.96寸OLED模块时我第一反应是这玩意儿接线真够复杂的。不过实际用起来你会发现SPI接口的OLED其实比I2C版本更容易调试。先说说SPI的物理连接我用的是最常见的4线制SPI模式接线时要注意STM32的SPI引脚分配不是随便选的。以STM32F103C8T6为例硬件SPI1的引脚是固定的SCK(时钟线) - PA5MOSI(主机输出) - PA7MISO(主机输入) - PA6OLED不需要CS(片选) - 任意GPIO我用的PB0DC(数据/命令) - PB1RES(复位) - PB2这里有个坑要注意OLED的DC引脚决定传输的是命令还是数据必须单独控制。我第一次调试时就因为忘了配置这个引脚屏幕死活不亮。建议先用万用表确认所有连线接触良好特别是GND一定要共地。2. SPI时序配置与初始化STM32的SPI配置有几个关键参数容易出错。在CubeMX里设置时我建议这样配置hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; // 全双工 hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; // 时钟极性 hspi1.Init.CLKPhase SPI_PHASE_1EDGE; // 时钟相位 hspi1.Init.NSS SPI_NSS_SOFT; // 软件控制片选 hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_4; // 18MHz 72MHz主频 hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; // 高位先行实测发现OLED对SPI速度不敏感用软件模拟SPI也能稳定工作。下面是我常用的SPI发送函数void OLED_SPI_Send(uint8_t data) { HAL_SPI_Transmit(hspi1, data, 1, 100); } void OLED_WriteCmd(uint8_t cmd) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET); // DC0命令 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); // CS0选中 OLED_SPI_Send(cmd); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); // CS1释放 } void OLED_WriteData(uint8_t data) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET); // DC1数据 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); OLED_SPI_Send(data); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); }3. OLED初始化与基础显示SSD1306驱动的OLED初始化有固定流程必须严格按照数据手册的顺序发送命令。我整理了一份可靠的初始化代码void OLED_Init(void) { HAL_Delay(100); // 等待电源稳定 // 硬件复位 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_RESET); HAL_Delay(10); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_SET); OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0xD5); // 设置时钟分频 OLED_WriteCmd(0x80); // 建议值 OLED_WriteCmd(0xA8); // 设置多路复用率 OLED_WriteCmd(0x3F); // 64-1 OLED_WriteCmd(0xD3); // 设置显示偏移 OLED_WriteCmd(0x00); // 无偏移 OLED_WriteCmd(0x40); // 设置起始行 OLED_WriteCmd(0x8D); // 电荷泵设置 OLED_WriteCmd(0x14); // 开启电荷泵 OLED_WriteCmd(0x20); // 内存地址模式 OLED_WriteCmd(0x00); // 水平地址模式 OLED_WriteCmd(0xA1); // 段重映射 OLED_WriteCmd(0xC8); // 扫描方向 OLED_WriteCmd(0xDA); // COM引脚配置 OLED_WriteCmd(0x12); // 可选配置 OLED_WriteCmd(0x81); // 对比度控制 OLED_WriteCmd(0xCF); // 对比度值 OLED_WriteCmd(0xD9); // 预充电周期 OLED_WriteCmd(0xF1); // 建议值 OLED_WriteCmd(0xDB); // VCOMH电平 OLED_WriteCmd(0x40); // 建议值 OLED_WriteCmd(0xA4); // 全局显示开启 OLED_WriteCmd(0xA6); // 正常显示 OLED_WriteCmd(0xAF); // 开启显示 OLED_Clear(); // 清屏 }清屏函数实现如下这个操作会刷新整个GRAMvoid OLED_Clear(void) { for(uint8_t page0; page8; page) { OLED_WriteCmd(0xB0 page); // 设置页地址 OLED_WriteCmd(0x00); // 列地址低4位 OLED_WriteCmd(0x10); // 列地址高4位 for(uint8_t col0; col128; col) { OLED_WriteData(0x00); // 写入全0 } } }4. 汉字点阵显示原理显示汉字最麻烦的就是字模提取。国内常用的16x16点阵汉字每个字需要32字节数据。我推荐用PCtoLCD2002这个老牌取模软件配置如下取模方式逐列式取模走向逆向低位在前输出格式C51格式点阵大小16x16字体宋体比如显示温度两个字取模后得到的数据如下/* 温 */ 0x00,0x40,0x00,0x44,0xFE,0x44,0x92,0x44, 0x92,0x7C,0x92,0x44,0x92,0x44,0xFE,0x44, 0x92,0x44,0x92,0x44,0x92,0x7C,0x92,0x44, 0xFE,0x44,0x00,0x44,0x00,0x44,0x00,0x40, /* 度 */ 0x04,0x00,0x04,0x04,0x04,0x04,0x04,0x04, 0x04,0x04,0x04,0x04,0x04,0x04,0xFC,0x3F, 0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04, 0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x00在代码中定义字库数组时建议用结构体组织typedef struct { char word[3]; // 汉字占2字节结束符 uint8_t data[32]; } ChineseFont; const ChineseFont fontLib[] { {温, {0x00,0x40,0x00,0x44,0xFE,0x44,0x92,0x44, 0x92,0x7C,0x92,0x44,0x92,0x44,0xFE,0x44, 0x92,0x44,0x92,0x44,0x92,0x7C,0x92,0x44, 0xFE,0x44,0x00,0x44,0x00,0x44,0x00,0x40}}, {度, {0x04,0x00,0x04,0x04,0x04,0x04,0x04,0x04, 0x04,0x04,0x04,0x04,0x04,0x04,0xFC,0x3F, 0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04, 0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x00}} };5. 混合显示汉字与数字实际项目中经常需要混合显示汉字和ASCII字符。我的做法是先实现单字符显示函数再封装字符串显示接口// 显示16x16汉字 void OLED_ShowChinese(uint8_t x, uint8_t y, uint8_t index) { uint8_t page y / 8; uint8_t *pFont (uint8_t*)fontLib[index].data; for(uint8_t i0; i16; i) { OLED_WriteCmd(0xB0 page i/8); // 设置页地址 OLED_WriteCmd(((x)0x0F) | 0x00); // 列低4位 OLED_WriteCmd(((x)4) | 0x10); // 列高4位 for(uint8_t j0; j16; j) { OLED_WriteData(pFont[i*2 j/8]); } } } // 显示8x16 ASCII字符 void OLED_ShowChar(uint8_t x, uint8_t y, char chr) { uint8_t page y / 8; uint8_t *pFont ASCII_Font8x16[(chr-32)*16]; for(uint8_t i0; i16; i) { OLED_WriteCmd(0xB0 page i/8); OLED_WriteCmd(((x)0x0F) | 0x00); OLED_WriteCmd(((x)4) | 0x10); OLED_WriteData(pFont[i]); } } // 显示混合字符串 void OLED_ShowString(uint8_t x, uint8_t y, char *str) { while(*str) { if((uint8_t)*str 0x80) { // 中文字符 OLED_ShowChinese(x, y, findFontIndex(str)); x 16; str 2; } else { // ASCII字符 OLED_ShowChar(x, y, *str); x 8; str; } } }6. 实际应用案例温湿度显示结合DHT11传感器我们可以实现一个完整的温湿度监测显示。完整的主函数如下int main(void) { HAL_Init(); SystemClock_Config(); MX_SPI1_Init(); MX_GPIO_Init(); DHT11_Init(); OLED_Init(); OLED_Clear(); uint8_t temp, humi; char dispBuf[20]; while(1) { if(DHT11_Read(temp, humi) DHT11_OK) { sprintf(dispBuf, 温度:%2d℃, temp); OLED_ShowString(0, 0, dispBuf); sprintf(dispBuf, 湿度:%2d%%, humi); OLED_ShowString(0, 2, dispBuf); } HAL_Delay(2000); } }调试时遇到过字符乱码的问题最后发现是取模软件设置不对。建议新手一定要检查取模方向是否正确字节内位顺序高位在前/低位在前显示函数是否按页正确写入GRAM7. 性能优化技巧经过实际测试我发现几个提升显示效率的方法局部刷新只更新变化部分避免全屏刷新void OLED_RefreshArea(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2) { for(uint8_t pagey1/8; pagey2/8; page) { OLED_WriteCmd(0xB0 page); OLED_WriteCmd(((x1)0x0F) | 0x00); OLED_WriteCmd(((x1)4) | 0x10); for(uint8_t colx1; colx2; col) { OLED_WriteData(getGRAMValue(col, page)); } } }双缓冲机制在内存中维护显示缓存减少SPI通信次数使用DMA传输在支持DMA的STM32型号上可以显著提升大数据量传输效率字库优化对于固定内容可以预先渲染成位图存储减少实时计算最后提醒一点OLED长期显示静态内容容易烧屏。建议设计UI时加入定期像素位移或亮度调节功能。