在嵌入式开发中,显示界面是人机交互的重要窗口。相比传统的LCD屏幕,OLED以其自发光、高对比度、低功耗等特性,逐渐成为嵌入式项目的首选。而SPI(Serial Peripheral Interface)作为MCU与外围设备通信的"高速公路",其高速传输特性特别适合驱动OLED这类需要频繁刷新数据的显示设备。
我曾在一个智能家居项目中尝试过多种显示方案,最终发现SPI+OLED的组合在刷新速度、接线复杂度和功耗控制上达到了完美平衡。本文将分享如何用STM32标准外设库搭建完整的SPI-OLED显示系统,包含硬件连接技巧、软件配置要点、字库制作方法以及性能优化手段。
选择OLED模块时需要注意以下参数:
推荐型号:0.96寸128x64 SPI OLED(SSD1306驱动)
典型接线方案(以STM32F103C8T6为例):
| OLED引脚 | STM32引脚 | 功能说明 |
|---|---|---|
| GND | GND | 地线 |
| VCC | 3.3V | 电源(3.3V) |
| D0(SCK) | PA5 | SPI时钟线 |
| D1(MOSI) | PA7 | SPI数据线 |
| RES | PB0 | 复位信号(可自定义) |
| DC | PB1 | 数据/命令选择 |
| CS | PA4 | 片选信号(可接地) |
提示:若板上只有一个SPI设备,可将CS直接接地简化控制逻辑
所需工具链:
工程目录结构示例:
code复制/Drivers
/OLED
oled.c
oled.h
oledfont.h
/User
main.c
c复制void SPI_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
// 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);
// 配置SCK和MOSI引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// SPI参数配置
SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 模式0
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_Init(SPI2, &SPI_InitStructure);
SPI_Cmd(SPI2, ENABLE);
}
核心数据传输函数示例:
c复制void OLED_WR_Byte(uint8_t dat, uint8_t cmd)
{
if(cmd)
OLED_DC_Set(); // 命令模式
else
OLED_DC_Clr(); // 数据模式
while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET);
SPI_I2S_SendData(SPI2, dat);
while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET);
SPI_I2S_ReceiveData(SPI2);
}
实现文本显示的基本流程:
c复制void OLED_ShowChar(uint8_t x, uint8_t y, char chr)
{
uint8_t c = chr - ' ';
if(x > Max_Column-1){x=0; y++;}
OLED_Set_Pos(x, y);
for(uint8_t i=0; i<8; i++)
OLED_WR_Byte(font8x8[c][i], OLED_DATA);
}
中文显示需要预先制作字库,推荐使用PCtoLCD2002取模软件:
取模配置参数:
生成的字体数据存入oledfont.h:
c复制const uint8_t F16x16_CN[] = {
/*"中"*/
0x00,0x40,0x20,0x50,0x4F,0xC8,0x48,0x48,
0x48,0x48,0x48,0x4F,0xC8,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0xFF,0x00,0x00,
0x00,0x00,0x00,0x00,0xFF,0x00,0x00,0x00,
/*"文"*/
0x00,0x00,0x00,0xF8,0x00,0x00,0xFF,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x80,0x40,0x30,0x0F,0x00,0x00,0xFF,0x00,
0x02,0x04,0x08,0x10,0x20,0xC0,0x00,0x00
};
实现屏幕滚动动画:
c复制void OLED_Scroll_Horizontal(uint8_t direction)
{
OLED_WR_Byte(0x2E, OLED_CMD); // 关闭滚动
OLED_WR_Byte(direction, OLED_CMD); // 0x26右滚, 0x27左滚
OLED_WR_Byte(0x00, OLED_CMD); // 虚拟字节
OLED_WR_Byte(0x00, OLED_CMD); // 起始页
OLED_WR_Byte(0x07, OLED_CMD); // 滚动时间间隔
OLED_WR_Byte(0x07, OLED_CMD); // 结束页
OLED_WR_Byte(0x00, OLED_CMD); // 虚拟字节
OLED_WR_Byte(0xFF, OLED_CMD); // 虚拟字节
OLED_WR_Byte(0x2F, OLED_CMD); // 开启滚动
}
采用双缓冲机制可显著提高刷新效率:
c复制uint8_t oled_buffer[128][8]; // 128x64分辨率缓冲
void OLED_Refresh(void)
{
for(uint8_t page=0; page<8; page++){
OLED_Set_Pos(0, page);
for(uint8_t col=0; col<128; col++){
OLED_WR_Byte(oled_buffer[col][page], OLED_DATA);
}
}
}
通过调整预分频器提高传输速率:
| 预分频值 | 时钟频率(72MHz主频) | 适用场景 |
|---|---|---|
| SPI_BaudRatePrescaler_2 | 36 MHz | 短距离高质量布线 |
| SPI_BaudRatePrescaler_4 | 18 MHz | 一般推荐值 |
| SPI_BaudRatePrescaler_8 | 9 MHz | 长线缆或干扰环境 |
OLED省电模式配置:
c复制void OLED_PowerSave(uint8_t mode)
{
if(mode){
OLED_WR_Byte(0xAE, OLED_CMD); // 关闭显示
OLED_WR_Byte(0x8D, OLED_CMD); // 关闭电荷泵
}else{
OLED_WR_Byte(0x8D, OLED_CMD);
OLED_WR_Byte(0x14, OLED_CMD); // 开启电荷泵
OLED_WR_Byte(0xAF, OLED_CMD); // 开启显示
}
}
c复制OLED_WR_Byte(0xAF, OLED_CMD); // 开启显示
OLED_WR_Byte(0xA4, OLED_CMD); // 正常显示模式
问题1:屏幕仅显示乱码
问题2:显示内容闪烁
问题3:通信不稳定
采用状态机设计菜单系统:
c复制typedef struct {
char *text;
void (*action)(void);
struct MenuItem *children;
uint8_t child_count;
} MenuItem;
MenuItem mainMenu[] = {
{"系统设置", NULL, settingsMenu, 3},
{"数据显示", showData, NULL, 0},
{"设备信息", showInfo, NULL, 0}
};
void Menu_Handler(uint8_t key)
{
static uint8_t current = 0;
switch(key){
case KEY_UP:
current = (current-1)%itemCount;
break;
case KEY_DOWN:
current = (current+1)%itemCount;
break;
case KEY_ENTER:
if(menu[current].action)
menu[current].action();
break;
}
OLED_ShowMenu(current);
}
实现实时波形显示:
c复制void Draw_Waveform(int16_t *data, uint8_t count)
{
OLED_ClearBuffer();
// 绘制坐标轴
Draw_Line(10, 10, 10, 54, 1);
Draw_Line(10, 54, 118, 54, 1);
// 绘制波形
for(uint8_t i=0; i<count-1; i++){
int16_t y1 = 54 - (data[i]/16);
int16_t y2 = 54 - (data[i+1]/16);
Draw_Line(20+i*2, y1, 20+(i+1)*2, y2, 1);
}
OLED_Refresh();
}
构建国际化字库系统:
c复制typedef struct {
uint16_t unicode;
uint8_t width;
uint8_t height;
const uint8_t *data;
} FontChar;
const FontChar fontLib[] = {
{0x4E2D, 16, 16, F16x16_CN_1}, // "中"
{0x6587, 16, 16, F16x16_CN_2}, // "文"
{0x0041, 8, 16, F8x16_EN_A}, // "A"
// 更多字符...
};
const uint8_t* Find_Font(uint16_t unicode)
{
for(uint32_t i=0; i<sizeof(fontLib)/sizeof(FontChar); i++){
if(fontLib[i].unicode == unicode)
return fontLib[i].data;
}
return NULL;
}
利用STM32的DMA功能释放CPU资源:
c复制void OLED_Refresh_DMA(void)
{
OLED_Set_Pos(0, 0);
DMA_Cmd(DMA1_Channel5, DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel5, 1024); // 128x64/8
DMA_Cmd(DMA1_Channel5, ENABLE);
SPI_I2S_DMACmd(SPI2, SPI_I2S_DMAReq_Tx, ENABLE);
while(DMA_GetFlagStatus(DMA1_FLAG_TC5) == RESET);
SPI_I2S_DMACmd(SPI2, SPI_I2S_DMAReq_Tx, DISABLE);
DMA_ClearFlag(DMA1_FLAG_TC5);
}
通过ESP8266实现远程内容更新:
c复制void ESP8266_Process(void)
{
if(UART_Receive_JSON(&json)){
OLED_ClearBuffer();
if(json.type == TEXT){
OLED_ShowString(json.x, json.y, json.data, json.size);
}else if(json.type == GRAPH){
OLED_DrawBMP(json.x, json.y, json.w, json.h, json.data);
}
OLED_Refresh();
}
}
根据环境光自动调整亮度:
c复制void OLED_Auto_Brightness(void)
{
uint16_t light = ADC_Read(ALS_SENSOR);
uint8_t contrast = light / 16; // 0-255
OLED_WR_Byte(0x81, OLED_CMD); // 设置对比度
OLED_WR_Byte(contrast, OLED_CMD);
}
推荐采用分层架构:
code复制/Application
menu.c
chart.c
/Drivers
/OLED
oled.c
oled_font.c
/SPI
spi.c
/Hardware
board.c
/Middlewares
/GUI
gui.c
通过宏定义适配不同OLED型号:
c复制#if defined(OLED_SSD1306)
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
#define OLED_INIT_CMD ssd1306_init_cmd
#elif defined(OLED_SH1106)
#define OLED_WIDTH 132
#define OLED_HEIGHT 64
#define OLED_INIT_CMD sh1106_init_cmd
#endif
构建自动化测试框架:
c复制void OLED_Test_Suite(void)
{
TEST_CASE("Basic Display", {
OLED_Clear(0);
OLED_ShowString(0, 0, "TEST", 16);
ASSERT(Check_Screen(0, 0, "TEST"));
});
TEST_CASE("Chinese Display", {
OLED_ShowChinese(0, 2, "测试");
ASSERT(Check_Screen(0, 2, "测试"));
});
}
使用SPI Flash存储大量字库:
c复制void Load_Font_From_Flash(uint32_t addr, uint8_t *buf)
{
SPI_Flash_Read(addr, buf, 32); // 16x16汉字占32字节
OLED_ShowCustomFont(0, 0, buf);
}
通过SPI总线挂载多个OLED:
c复制void OLED_Multi_Display(void)
{
// 选择屏幕1
OLED1_CS_Clr();
OLED_ShowString(0, 0, "Screen 1");
OLED1_CS_Set();
// 选择屏幕2
OLED2_CS_Clr();
OLED_ShowString(0, 0, "Screen 2");
OLED2_CS_Set();
}
移植u8g2图形库到STM32:
c复制uint8_t u8x8_stm32_gpio_and_delay(...)
{
// GPIO控制实现
}
uint8_t u8x8_byte_stm32_spi(...)
{
// SPI传输实现
}
c复制U8G2_SSD1306_128X64_NONAME_1_4W_HW_SPI u8g2;
u8g2_Setup_ssd1306_128x64_noname_1(&u8g2, U8G2_R0, u8x8_byte_stm32_spi, u8x8_stm32_gpio_and_delay);
实现参数设置与实时监控:
c复制void TempController_UI(void)
{
static uint8_t mode = 0;
while(1){
OLED_ClearBuffer();
// 显示当前温度
OLED_ShowString(0, 0, "Temp:", 16);
OLED_ShowFloat(40, 0, read_temp(), 1, 16);
// 显示设置温度
OLED_ShowString(0, 2, "Set:", 16);
OLED_ShowFloat(40, 2, target_temp, 1, 16);
// 模式指示
if(mode) OLED_ShowString(80, 0, "AUTO", 16);
else OLED_ShowString(80, 0, "MANU", 16);
// 控制指示
if(heater_status)
OLED_ShowString(80, 2, "HEAT", 16);
OLED_Refresh();
// 按键处理
if(KEY_Read() == KEY_MODE){
mode = !mode;
}
Delay_ms(100);
}
}
显示远程传感器数据:
c复制void IoT_Dashboard(void)
{
while(1){
MQTT_Update(); // 获取最新数据
OLED_ClearBuffer();
Draw_Gauge(10, 10, "Temperature", temp_value, 0, 50);
Draw_Gauge(70, 10, "Humidity", humi_value, 0, 100);
OLED_ShowString(0, 6, "Last Update:", 12);
OLED_ShowTime(72, 6, rtc_time);
OLED_Refresh();
Delay_ms(5000);
}
}
实现经典贪吃蛇游戏:
c复制void Snake_Game(void)
{
Snake snake;
Food food;
Init_Game(&snake, &food);
while(!game_over){
// 处理输入
switch(KEY_Read()){
case KEY_UP: snake.dir = UP; break;
case KEY_DOWN: snake.dir = DOWN; break;
// 其他方向...
}
// 更新游戏状态
Move_Snake(&snake);
if(Check_Collision(&snake)){
game_over = 1;
break;
}
if(Eat_Food(&snake, &food)){
Grow_Snake(&snake);
Generate_Food(&food, &snake);
}
// 渲染画面
OLED_ClearBuffer();
Draw_Snake(&snake);
Draw_Food(&food);
OLED_ShowNumber(100, 0, snake.length-3, 2);
OLED_Refresh();
Delay_ms(200 - (snake.length/5)*20);
}
Show_Game_Over(snake.length-3);
}