## 一、OLED驱动接口:SPI vs I2C的选型博弈 OLED屏常见接口包括SPI(4线/3线)和I2C,两者在引脚占用、传输速率和复杂度上差异显著。 - **SPI接口**: - 引脚:SCLK、MOSI、DC(数据/命令)、CS(片选)、RST(复位),共5-6根。 - 速率:可达10Mbps以上,适合高刷新率或动画显示。 - 优势:传输快,可配合DMA实现无CPU干预的刷屏。 - 劣势:占用引脚多,且需额外控制DC线。 - **I2C接口**: - 引脚:SCL、SDA,仅2根,支持多设备挂载。 - 速率:标准模式100kbps,快速模式400kbps,远低于SPI。 - 优势:节省引脚,接线简单,适合资源紧张的板卡。 - 劣势:速率低,刷屏时CPU占用高,且需处理地址和ACK。 **选型建议**:若项目对显示流畅度要求高(如波形刷新),优先SPI+DMA;若仅显示静态信息且引脚紧张,I2C足够。此外,部分OLED模块同时支持两种接口,可通过电阻配置切换,设计时需预留兼容性。 ## 二、底层驱动原理:从时序到初始化 无论SPI还是I2C,OLED驱动核心是向SSD1306(常见控制器)发送命令和数据。命令用于配置显示参数,数据则填充显存。 ### 2.1 SPI时序要点 - 模式:CPOL=0,CPHA=0(模式0),即空闲时SCLK为低,数据在上升沿采样。 - DC线:高电平表示数据,低电平表示命令。 - 传输流程:拉低CS→设置DC→发送字节→拉高CS。 ### 2.2 I2C时序要点 - 设备地址:通常为0x3C(写)或0x3D(读),由SA0引脚决定。 - 控制字节:紧跟地址后,0x00表示命令,0x40表示数据。 - 需注意:每次传输需等待ACK,且发送命令时需先发送控制字节。 ### 2.3 初始化序列(以SSD1306为例) 初始化需配置显示关闭、时钟分频、复用比、偏移、起始行、电荷泵等。以下为精简序列: ```c void OLED_Init(void) { // 复位(若使用硬件复位引脚) OLED_RST_Clr(); delay_ms(100); OLED_RST_Set(); delay_ms(100); OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0xD5); OLED_WriteCmd(0x80); // 时钟分频 OLED_WriteCmd(0xA8); OLED_WriteCmd(0x3F); // 复用比(1/64) OLED_WriteCmd(0xD3); OLED_WriteCmd(0x00); // 显示偏移 OLED_WriteCmd(0x40); // 起始行0 OLED_WriteCmd(0x8D); OLED_WriteCmd(0x14); // 电荷泵使能 OLED_WriteCmd(0x20); OLED_WriteCmd(0x00); // 内存寻址模式(水平) OLED_WriteCmd(0xA1); // 段重映射 OLED_WriteCmd(0xC8); // COM扫描方向 OLED_WriteCmd(0xDA); OLED_WriteCmd(0x12); // COM引脚配置 OLED_WriteCmd(0x81); OLED_WriteCmd(0xCF); // 对比度 OLED_WriteCmd(0xD9); OLED_WriteCmd(0xF1); // 预充电周期 OLED_WriteCmd(0xDB); OLED_WriteCmd(0x40); // VCOMH OLED_WriteCmd(0xA4); // 全局显示开启 OLED_WriteCmd(0xA6); // 正常显示 OLED_WriteCmd(0xAF); // 开启显示 OLED_Clear(); } ``` ## 三、中文字库的存储与渲染 OLED显存为1KB(128*64/8),直接存储全字库不现实。常用方案是提取所需汉字模,存储于外部Flash或MCU内部Flash。 ### 3.1 字模生成与格式 使用取模软件(如PCtoLCD2002)生成16x16点阵字模,格式选择“阴码、逐行式、列行式”等。通常每个汉字占32字节(16行*2字节)。 ### 3.2 字库存储策略 - **全字库存储**:将GB2312常用汉字(约6763个)存于外部SPI Flash,占用约216KB,适合大容量场景。 - **动态加载**:仅存储常用汉字(如100个),按需从Flash读取,节省空间。 - **内部Flash存储**:若MCU Flash充足,可直接定义为常量数组,访问最快。 ### 3.3 渲染函数实现 以下代码演示如何根据GB2312编码获取字模并显示: ```c // 假设字模存储在外部Flash,通过SPI读取 #define FONT_WIDTH 16 #define FONT_HEIGHT 16 #define FONT_BYTES (FONT_WIDTH * FONT_HEIGHT / 8) // 从字库文件读取字模(简化版) void GetFontData(uint16_t code, uint8_t *buf) { uint32_t offset = (code - 0xB0A1) * FONT_BYTES; // GB2312区位码偏移 // 从Flash读取offset处FONT_BYTES字节到buf SPI_Flash_Read(buf, offset, FONT_BYTES); } // 显示一个汉字(x,y为左上角坐标) void OLED_ShowChinese(uint8_t x, uint8_t y, uint16_t code) { uint8_t buf[FONT_BYTES]; GetFontData(code, buf); for (uint8_t row = 0; row < FONT_HEIGHT; row++) { for (uint8_t col = 0; col < FONT_WIDTH / 8; col++) { OLED_SetPos(x + col*8, y + row); OLED_WriteData(buf[row * (FONT_WIDTH/8) + col]); } } } ``` ### 3.4 内存优化技巧 - 使用分页显示:仅更新变化区域,避免全屏刷新。 - 双缓冲:在RAM中维护显存副本,批量修改后一次性写入,减少I/O操作。 - 压缩字模:对稀疏字模进行RLE压缩,解压时动态生成。 ## 四、完整代码示例:SPI驱动+中文字库 以下为基于STM32 HAL库的SPI驱动示例,包含初始化、显示字符和汉字。 ```c // spi_oled.h #ifndef __SPI_OLED_H #define __SPI_OLED_H #include "stm32f1xx_hal.h" // 引脚定义(根据实际连接修改) #define OLED_CS_PIN GPIO_PIN_4 #define OLED_CS_PORT GPIOA #define OLED_DC_PIN GPIO_PIN_2 #define OLED_DC_PORT GPIOA #define OLED_RST_PIN GPIO_PIN_3 #define OLED_RST_PORT GPIOA void OLED_Init(void); void OLED_Clear(void); void OLED_ShowString(uint8_t x, uint8_t y, char *str); void OLED_ShowChinese(uint8_t x, uint8_t y, uint16_t code); #endif ``` ```c // spi_oled.c #include "spi_oled.h" #include extern SPI_HandleTypeDef hspi1; static void OLED_WriteCmd(uint8_t cmd) { HAL_GPIO_WritePin(OLED_DC_PORT, OLED_DC_PIN, GPIO_PIN_RESET); // DC=0 HAL_GPIO_WritePin(OLED_CS_PORT, OLED_CS_PIN, GPIO_PIN_RESET); // CS=0 HAL_SPI_Transmit(&hspi1, &cmd, 1, 100); HAL_GPIO_WritePin(OLED_CS_PORT, OLED_CS_PIN, GPIO_PIN_SET); } static void OLED_WriteData(uint8_t data) { HAL_GPIO_WritePin(OLED_DC_PORT, OLED_DC_PIN, GPIO_PIN_SET); // DC=1 HAL_GPIO_WritePin(OLED_CS_PORT, OLED_CS_PIN, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, &data, 1, 100); HAL_GPIO_WritePin(OLED_CS_PORT, OLED_CS_PIN, GPIO_PIN_SET); } static void OLED_SetPos(uint8_t x, uint8_t y) { OLED_WriteCmd(0xB0 + y / 8); // 页地址 OLED_WriteCmd(0x00 + (x % 16)); // 列低4位 OLED_WriteCmd(0x10 + (x / 16)); // 列高4位 } void OLED_Init(void) { // 复位 HAL_GPIO_WritePin(OLED_RST_PORT, OLED_RST_PIN, GPIO_PIN_RESET); HAL_Delay(100); HAL_GPIO_WritePin(OLED_RST_PORT, OLED_RST_PIN, GPIO_PIN_SET); HAL_Delay(100); // 初始化序列(同前) // ... 省略,参考上文 } void OLED_Clear(void) { for (uint8_t page = 0; page < 8; page++) { OLED_SetPos(0, page * 8); for (uint16_t i = 0; i < 128; i++) { OLED_WriteData(0x00); } } } void OLED_ShowChar(uint8_t x, uint8_t y, char ch) { // 从ASCII字模表获取8x16点阵,此处省略字模数组 // 实现类似ShowChinese,但字模为8x16 } void OLED_ShowString(uint8_t x, uint8_t y, char *str) { while (*str) { if (*str < 0x80) { // ASCII字符 OLED_ShowChar(x, y, *str); x += 8; } else { // 汉字(GB2312双字节) uint16_t code = ((uint8_t)*str << 8) | (uint8_t)*(str+1); OLED_ShowChinese(x, y, code); str++; // 跳过第二字节 x += 16; } str++; if (x > 112) { x = 0; y += 16; } // 换行 } } // 字模获取函数需根据实际存储介质实现 void OLED_ShowChinese(uint8_t x, uint8_t y, uint16_t code) { // 调用GetFontData并显示,参考上文 } ``` ## 五、注意事项与调试技巧 - **时序验证**:使用逻辑分析仪抓取SPI/I2C波形,确认时钟极性和相位。 - **电源稳定性**:OLED电荷泵需稳定3.3V供电,避免纹波导致花屏。 - **字模取模方向**:务必与显示方向一致,否则汉字会镜像或颠倒。 - **DMA优化**:若使用SPI+DMA,需注意缓冲区生命周期,避免数据被覆盖。 - **I2C速率**:若总线有其他设备,适当降低速率至100kbps以保证稳定。 - **调试建议**:先点亮单像素,再显示字符,最后测试汉字,逐步排查。 通过合理选型接口和优化字库存储,OLED显示驱动可兼顾性能与资源。希望本文能助你在嵌入式显示领域更进一步。