1. 项目概述
最近在做一个基于STM32的嵌入式项目,需要用到OLED显示屏来显示一些实时数据。经过一番折腾,终于搞定了STM32 CubeIDE环境下通过I2C接口驱动OLED显示屏的全过程。这里把完整的配置流程和代码实现分享给大家,希望能帮到有同样需求的开发者。
OLED显示屏因其高对比度、低功耗、宽视角等优点,在嵌入式领域应用非常广泛。我这次使用的是128x64分辨率的SSD1306驱动芯片的OLED屏,通过I2C接口与STM32F103C8T6最小系统板通信。整个开发环境基于STM32CubeIDE,利用HAL库进行开发,大大简化了底层驱动编写的工作量。
2. 硬件准备与连接
2.1 所需硬件清单
- STM32开发板(我使用的是STM32F103C8T6最小系统板)
- 0.96寸OLED显示屏(SSD1306驱动,I2C接口)
- 杜邦线若干
- USB转TTL模块(用于串口调试)
2.2 硬件连接方式
OLED显示屏通常有4个引脚需要连接:
- VCC:接3.3V电源
- GND:接地
- SCL:I2C时钟线,接STM32的PB6(默认I2C1_SCL)
- SDA:I2C数据线,接STM32的PB7(默认I2C1_SDA)
注意:不同型号的STM32芯片,I2C引脚可能不同,需要查阅具体芯片的数据手册确认。有些OLED模块需要外接上拉电阻(通常4.7KΩ),但大多数模块已经内置了上拉电阻。
3. STM32CubeMX配置
3.1 创建新工程
- 打开STM32CubeIDE,新建工程
- 选择对应的STM32系列和具体型号(我选的是STM32F103C8Tx)
- 进入图形化配置界面
3.2 I2C外设配置
- 在Pinout & Configuration标签页中,找到I2C1
- 将I2C1的模式设置为"I2C"
- 参数配置:
- I2C Speed Mode:Fast Mode(400kHz)
- Clock No Stretch Mode:Disabled
- Primary Slave Address:0x00(默认)
- Primary Address Length:7-bit
提示:OLED的I2C地址通常是0x78或0x7A(取决于模块),这是7位地址形式。在HAL库中调用时需要左移一位(即0x3C或0x3D)。
3.3 GPIO配置
- 确认I2C1_SCL和I2C1_SDA引脚已自动分配(通常是PB6和PB7)
- 检查引脚模式是否自动设置为"Alternate Function Open Drain"
- 如果需要使用硬件I2C,确保这两个引脚没有被其他功能占用
3.4 时钟配置
- 切换到Clock Configuration标签页
- 根据芯片最高主频配置时钟树
- 确保I2C时钟源正确(通常使用APB1总线时钟)
- 我的配置:
- HCLK:72MHz
- APB1 Prescaler:2(APB1时钟36MHz)
- I2C时钟:36MHz
3.5 生成代码
- 点击Project > Generate Code
- 选择使用HAL库
- 设置工程名称和存储路径
- 生成代码后打开工程
4. OLED驱动实现
4.1 添加OLED驱动文件
在工程中新建两个文件:
- oled.h:OLED驱动头文件
- oled.c:OLED驱动源文件
oled.h内容示例:
c复制#ifndef __OLED_H
#define __OLED_H
#include "stm32f1xx_hal.h"
#define OLED_ADDRESS 0x78 // I2C地址
// OLED控制命令定义
#define OLED_CMD 0x00
#define OLED_DATA 0x40
// 函数声明
void OLED_Init(I2C_HandleTypeDef *hi2c);
void OLED_Clear(void);
void OLED_DisplayOn(void);
void OLED_DisplayOff(void);
void OLED_SetPixel(uint8_t x, uint8_t y, uint8_t color);
void OLED_UpdateScreen(void);
void OLED_DrawChar(uint8_t x, uint8_t y, char ch, uint8_t size);
void OLED_DrawString(uint8_t x, uint8_t y, char *str, uint8_t size);
// 其他图形绘制函数声明...
#endif
4.2 OLED初始化函数
在oled.c中实现初始化函数:
c复制#include "oled.h"
#include "font.h" // 字模数据
static I2C_HandleTypeDef *hi2c_oled;
static uint8_t OLED_Buffer[1024]; // 128x64/8=1024
void OLED_Init(I2C_HandleTypeDef *hi2c) {
hi2c_oled = hi2c;
uint8_t init_cmds[] = {
0xAE, // 关闭显示
0xD5, 0x80, // 设置时钟分频
0xA8, 0x3F, // 设置多路复用率
0xD3, 0x00, // 设置显示偏移
0x40, // 设置显示开始行
0x8D, 0x14, // 电荷泵设置
0x20, 0x00, // 内存地址模式
0xA1, // 段重映射
0xC8, // 扫描方向
0xDA, 0x12, // COM引脚配置
0x81, 0xCF, // 对比度设置
0xD9, 0xF1, // 预充电周期
0xDB, 0x30, // VCOMH设置
0xA4, // 全亮显示
0xA6, // 正常显示
0xAF // 开启显示
};
for(uint8_t i=0; i<sizeof(init_cmds); i++) {
uint8_t buf[2] = {OLED_CMD, init_cmds[i]};
HAL_I2C_Master_Transmit(hi2c_oled, OLED_ADDRESS, buf, 2, 100);
}
OLED_Clear();
OLED_UpdateScreen();
}
4.3 基本显示函数实现
c复制void OLED_Clear(void) {
memset(OLED_Buffer, 0, sizeof(OLED_Buffer));
}
void OLED_DisplayOn(void) {
uint8_t buf[2] = {OLED_CMD, 0xAF};
HAL_I2C_Master_Transmit(hi2c_oled, OLED_ADDRESS, buf, 2, 100);
}
void OLED_DisplayOff(void) {
uint8_t buf[2] = {OLED_CMD, 0xAE};
HAL_I2C_Master_Transmit(hi2c_oled, OLED_ADDRESS, buf, 2, 100);
}
void OLED_UpdateScreen(void) {
for(uint8_t i=0; i<8; i++) {
uint8_t cmd[] = {
OLED_CMD, 0xB0 + i, // 设置页地址
OLED_CMD, 0x00, // 设置列地址低4位
OLED_CMD, 0x10 // 设置列地址高4位
};
HAL_I2C_Master_Transmit(hi2c_oled, OLED_ADDRESS, cmd, sizeof(cmd), 100);
uint8_t data[129];
data[0] = OLED_DATA;
memcpy(&data[1], &OLED_Buffer[i*128], 128);
HAL_I2C_Master_Transmit(hi2c_oled, OLED_ADDRESS, data, sizeof(data), 100);
}
}
void OLED_SetPixel(uint8_t x, uint8_t y, uint8_t color) {
if(x >= 128 || y >= 64) return;
uint16_t index = x + (y/8)*128;
if(color) {
OLED_Buffer[index] |= (1 << (y%8));
} else {
OLED_Buffer[index] &= ~(1 << (y%8));
}
}
5. 高级图形功能实现
5.1 绘制直线算法
c复制void OLED_DrawLine(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2, uint8_t color) {
int16_t dx = abs(x2 - x1);
int16_t dy = abs(y2 - y1);
int16_t sx = (x1 < x2) ? 1 : -1;
int16_t sy = (y1 < y2) ? 1 : -1;
int16_t err = dx - dy;
while(1) {
OLED_SetPixel(x1, y1, color);
if(x1 == x2 && y1 == y2) break;
int16_t e2 = 2 * err;
if(e2 > -dy) {
err -= dy;
x1 += sx;
}
if(e2 < dx) {
err += dx;
y1 += sy;
}
}
}
5.2 绘制圆形算法
c复制void OLED_DrawCircle(uint8_t x0, uint8_t y0, uint8_t r, uint8_t color) {
int16_t f = 1 - r;
int16_t ddF_x = 1;
int16_t ddF_y = -2 * r;
int16_t x = 0;
int16_t y = r;
OLED_SetPixel(x0, y0 + r, color);
OLED_SetPixel(x0, y0 - r, color);
OLED_SetPixel(x0 + r, y0, color);
OLED_SetPixel(x0 - r, y0, color);
while(x < y) {
if(f >= 0) {
y--;
ddF_y += 2;
f += ddF_y;
}
x++;
ddF_x += 2;
f += ddF_x;
OLED_SetPixel(x0 + x, y0 + y, color);
OLED_SetPixel(x0 - x, y0 + y, color);
OLED_SetPixel(x0 + x, y0 - y, color);
OLED_SetPixel(x0 - x, y0 - y, color);
OLED_SetPixel(x0 + y, y0 + x, color);
OLED_SetPixel(x0 - y, y0 + x, color);
OLED_SetPixel(x0 + y, y0 - x, color);
OLED_SetPixel(x0 - y, y0 - x, color);
}
}
5.3 显示字符和字符串
c复制void OLED_DrawChar(uint8_t x, uint8_t y, char ch, uint8_t size) {
if(x > 128-8 || y > 64-16) return;
uint8_t i, j, temp;
for(i=0; i<size; i++) {
if(size == 8) temp = Font8[ch-32][i];
else if(size == 16) temp = Font16[ch-32][i];
for(j=0; j<8; j++) {
if(temp & (1<<j)) {
OLED_SetPixel(x+i, y+j, 1);
} else {
OLED_SetPixel(x+i, y+j, 0);
}
}
}
}
void OLED_DrawString(uint8_t x, uint8_t y, char *str, uint8_t size) {
uint8_t x0 = x;
while(*str) {
if(*str == '\n') {
y += size;
x = x0;
} else {
OLED_DrawChar(x, y, *str, size);
x += size/2;
}
str++;
}
}
6. 字模提取与使用
6.1 使用PCtoLCD2005提取字模
- 下载并打开PCtoLCD2005软件
- 设置参数:
- 点阵格式:阴码
- 取模方式:逐列式
- 取模走向:顺向
- 输出数制:十六进制
- 输入需要转换的文字或导入图片
- 生成字模数据并保存为C数组格式
6.2 字模数据示例
c复制// 8x16 ASCII字体
const uint8_t Font16[][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}, // !
// 其他字符...
};
// 8x8 ASCII字体
const uint8_t Font8[][8] = {
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, // 空格
{0x18,0x3C,0x3C,0x18,0x18,0x00,0x18,0x00}, // !
// 其他字符...
};
7. 主程序实现
7.1 main.c示例
c复制#include "main.h"
#include "i2c.h"
#include "oled.h"
I2C_HandleTypeDef hi2c1;
int main(void) {
HAL_Init();
SystemClock_Config();
MX_I2C1_Init();
OLED_Init(&hi2c1);
OLED_Clear();
// 显示字符串
OLED_DrawString(0, 0, "Hello STM32!", 16);
OLED_DrawString(0, 20, "OLED Display", 16);
// 绘制图形
OLED_DrawLine(0, 40, 127, 40, 1);
OLED_DrawRectangle(10, 45, 30, 15, 1);
OLED_DrawCircle(80, 52, 10, 1);
OLED_UpdateScreen();
while(1) {
HAL_Delay(1000);
}
}
7.2 I2C初始化
c复制void MX_I2C1_Init(void) {
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 400000;
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
if(HAL_I2C_Init(&hi2c1) != HAL_OK) {
Error_Handler();
}
}
8. 常见问题与解决方案
8.1 OLED不显示任何内容
-
检查硬件连接是否正确
- 确认VCC和GND连接正确
- 确认SCL和SDA线没有接反
- 检查I2C地址是否正确(尝试0x78和0x7A)
-
检查I2C配置
- 确认I2C时钟配置正确(Fast Mode 400kHz)
- 确认GPIO模式设置为Alternate Function Open Drain
- 检查是否启用了I2C外设时钟
-
使用逻辑分析仪或示波器检查I2C信号
- 确认SCL和SDA线上有信号
- 检查信号质量(是否有干扰或信号幅度不足)
8.2 显示内容错乱或部分显示
-
检查初始化序列是否正确
- 确保发送了完整的初始化命令序列
- 检查命令值是否正确(特别是0xAE/0xAF显示开关命令)
-
检查显存操作
- 确保显存缓冲区大小足够(128x64/8=1024字节)
- 检查像素坐标计算是否正确(特别是Y坐标的分页处理)
-
检查字模数据
- 确认字模数据格式与显示函数匹配
- 检查字模提取参数设置是否正确
8.3 显示刷新慢或闪烁
-
优化刷新策略
- 只在内容变化时刷新对应区域
- 使用双缓冲机制(准备下一帧内容,然后一次性刷新)
-
提高I2C通信速度
- 确认I2C时钟配置为最高支持速度(通常400kHz)
- 检查是否有其他设备占用I2C总线导致延迟
-
减少传输数据量
- 只刷新变化的部分区域
- 使用更高效的传输方式(如DMA)
9. 性能优化技巧
9.1 使用DMA加速数据传输
c复制// 在I2C初始化中添加DMA配置
void MX_I2C1_Init(void) {
// ...其他初始化代码...
__HAL_RCC_DMA1_CLK_ENABLE();
hdma_i2c1_tx.Instance = DMA1_Channel6;
hdma_i2c1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_i2c1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_i2c1_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_i2c1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_i2c1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_i2c1_tx.Init.Mode = DMA_NORMAL;
hdma_i2c1_tx.Init.Priority = DMA_PRIORITY_HIGH;
if(HAL_DMA_Init(&hdma_i2c1_tx) != HAL_OK) {
Error_Handler();
}
__HAL_LINKDMA(&hi2c1, hdmatx, hdma_i2c1_tx);
}
// 修改OLED刷新函数使用DMA
void OLED_UpdateScreen_DMA(void) {
for(uint8_t i=0; i<8; i++) {
uint8_t cmd[] = {
OLED_CMD, 0xB0 + i,
OLED_CMD, 0x00,
OLED_CMD, 0x10
};
HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDRESS, cmd, sizeof(cmd), 100);
uint8_t data[129];
data[0] = OLED_DATA;
memcpy(&data[1], &OLED_Buffer[i*128], 128);
HAL_I2C_Master_Transmit_DMA(&hi2c1, OLED_ADDRESS, data, sizeof(data));
while(HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);
}
}
9.2 部分刷新优化
c复制// 只刷新指定区域
void OLED_UpdateArea(uint8_t page, uint8_t col_start, uint8_t col_end) {
uint8_t cmd[] = {
OLED_CMD, 0xB0 + page,
OLED_CMD, (col_start & 0x0F),
OLED_CMD, 0x10 | ((col_start >> 4) & 0x0F),
OLED_CMD, 0x21,
OLED_CMD, col_start,
OLED_CMD, col_end
};
HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDRESS, cmd, sizeof(cmd), 100);
uint8_t data[col_end-col_start+2];
data[0] = OLED_DATA;
memcpy(&data[1], &OLED_Buffer[page*128+col_start], col_end-col_start+1);
HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDRESS, data, sizeof(data), 100);
}
9.3 使用硬件加速
对于支持硬件I2C的STM32芯片,确保:
- 正确配置I2C时钟源
- 启用I2C硬件功能
- 使用中断或DMA方式传输数据
- 合理设置I2C时序参数
10. 扩展功能实现
10.1 多级菜单系统
c复制typedef struct {
char *text;
void (*action)(void);
MenuItem *children;
uint8_t child_count;
} MenuItem;
MenuItem main_menu[] = {
{"Display Test", test_screen, NULL, 0},
{"Settings", NULL, settings_menu, 3},
{"Info", show_info, NULL, 0}
};
MenuItem settings_menu[] = {
{"Brightness", set_brightness, NULL, 0},
{"Contrast", set_contrast, NULL, 0},
{"Reset", reset_settings, NULL, 0}
};
void show_menu(MenuItem *menu, uint8_t count, uint8_t selected) {
OLED_Clear();
for(uint8_t i=0; i<count; i++) {
OLED_DrawString(10, i*10, menu[i].text, 8);
if(i == selected) {
OLED_DrawChar(0, i*10, '>', 8);
}
}
OLED_UpdateScreen();
}
10.2 动画效果实现
c复制void OLED_ScrollHorizontal(uint8_t start, uint8_t end, uint8_t dir, uint8_t speed) {
uint8_t cmd[] = {
OLED_CMD, 0x2E, // 停止滚动
OLED_CMD, dir ? 0x26 : 0x27, // 滚动方向
OLED_CMD, 0x00, // 虚拟字节
OLED_CMD, start, // 起始页
OLED_CMD, speed, // 滚动速度
OLED_CMD, end, // 结束页
OLED_CMD, 0x00, // 虚拟字节
OLED_CMD, 0xFF, // 虚拟字节
OLED_CMD, 0x2F // 开始滚动
};
HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDRESS, cmd, sizeof(cmd), 100);
}
void animate_logo() {
// 加载logo到显存
// ...
// 执行动画
for(int i=0; i<128; i++) {
OLED_ScrollHorizontal(0, 7, 1, 3);
HAL_Delay(50);
}
OLED_ScrollHorizontal(0, 7, 0, 0); // 停止滚动
}
10.3 触摸屏交互
如果OLED模块带有触摸功能,可以结合触摸检测实现交互:
c复制void check_touch() {
if(HAL_GPIO_ReadPin(TOUCH_GPIO_Port, TOUCH_Pin) == GPIO_PIN_RESET) {
// 获取触摸坐标
uint16_t x = read_touch_x();
uint16_t y = read_touch_y();
// 转换为OLED坐标
uint8_t oled_x = map(x, 0, 4095, 0, 127);
uint8_t oled_y = map(y, 0, 4095, 0, 63);
// 处理触摸事件
handle_touch(oled_x, oled_y);
}
}
11. 项目总结与心得
在实际开发过程中,我发现以下几点特别值得注意:
-
I2C地址问题:不同厂家的OLED模块I2C地址可能不同,常见的有0x3C和0x3D(7位地址形式)。如果OLED不响应,可以尝试扫描I2C总线上的设备地址。
-
初始化时序:SSD1306对初始化序列的顺序有一定要求,必须严格按照数据手册中的顺序发送命令。有些模块对某些命令特别敏感,比如电荷泵设置命令(0x8D)必须正确发送。
-
刷新优化:全屏刷新速度较慢,在实际应用中应该尽量减少刷新次数,可以只刷新变化的部分区域。使用DMA可以显著提高刷新速度。
-
显存管理:OLED的显存是按页组织的(每页8行),在编写图形算法时需要特别注意这一点。错误的坐标计算会导致显示错位。
-
电源稳定性:OLED对电源噪声比较敏感,建议在VCC和GND之间加一个0.1μF的滤波电容,特别是在长线连接的情况下。
这个项目让我对STM32的I2C接口和OLED驱动有了更深入的理解。通过优化显示算法和刷新策略,最终实现了流畅的图形界面效果。这套驱动代码已经应用在多个实际项目中,稳定性和性能都得到了验证。