STM32和7针OLED显示屏

STM32和7针OLED显示屏

硬件:

  • STM32F103C8T6
  • SSD1306 OLED 7 PIN

Alt text

这里采用 4‑wire SPI mode 在此模式下:

  • The interface is write-only (no data read). 只写模式
  • Data is sent MSB first. 数据以最高有效位(MSB)优先的方式发送
  • Every 8 clock pulses transfer one byte. 每8个时钟脉冲传输一个字节

SSD1306 → STM32F103C8T6 Wiring(连线)

STM32F103选择 SPI1 全双工(Full-Duplex Master)

SSD1306 Pin Function Connect to STM32 Pin
GND Ground GND
VDD Power Supply (3.3V logic) 3.3V
SCK SPI Clock PA5 (SPI1_SCK)
SDA Serial Data (MOSI) PA7 (SPI1_MOSI)
RES Reset Any GPIO, e.g., PA0
DC Data/Command Select Any GPIO, e.g., PA1
CS Chip Select Any GPIO, e.g., PA4 (SPI1_NSS or manual control)

不要使用5V电压(不确定,需要问硬件客服确认)

STM32CubeMX配置(configure)

Let's go step‑by‑step through the exact configuration process to make it work reliably with your STM32F103C8T6 (Blue Pill).让我们一步步详细了解配置过程,以确保它与你的 STM32F103C8T6(蓝药丸)稳定工作。

Alt text

Even though SSD1306 is write-only, Full Duplex is more stable on STM32 HAL

PA0, PA1, PA4 都设置为GPIO_Output模式,推挽输出(Output Push‑Pull),No Pull,Low

时钟配置 Keep system clock at 72 MHz (HSE + PLL). 然后点击 STM32CubeMX 中生成代码

SSD1306 原理

The SSD1306 has GDDRAM (Graphic Display Data RAM) — an internal 128 × 64 bit memory. SSD1306 拥有 GDDRAM(图形显示数据 RAM)——一个内部 128 × 64 位的内存

每一位代码一个像素,1表示开,0表示关

Display (128x64)
↑
|  Page 7 (y = 56 to 63)
|  Page 6
|  Page 5
|  Page 4
|  Page 3
|  Page 2
|  Page 1
|  Page 0 (y = 0 to 7)
|
+---------------------------------> X (0–127)

水平方向 128 位表示 128 像素,垂直方向8个分页,每页有8个像素,8×8=64,8bit=1Byte,也就是每个字节控制8个垂直像素。

要显示内容需要按照以下步骤:

  • Set the page address(0-7)
  • Set the column address(0-127)
  • Send data bytes

GDDRAM 访问命令

Command Function
0xB0 + page Select page (0–7)
0x00 + (column & 0x0F) Lower nibble of column address(低位)
0x10 + ((column >> 4) & 0x0F) Higher nibble of column address(高位)
OLED_WriteData(byte) Write pixel data to screen

功能规格

Alt text

这是 Power up 和 Power down 的顺序图

Power up
  1. Vdd 上电
  2. 发送 Display off 命令
  3. 初始化
  4. Clear Screen
  5. Power up Vcc(OLED内部实现)
  6. Delay 100ms
  7. Send Display on command
Power down
  1. 发送 Display off 命令
  2. Vcc 下电
  3. Delay 100ms
  4. Vdd 下电

根据手册和时序图来实现功能 Alt text

代码部分

先声明几个函数

void OLED_WriteCommand(uint8_t cmd);
void OLED_WriteData(uint8_t data);
void OLED_Reset(void);
void OLED_Init(void);
void OLED_Clear(void);
void OLED_ClearArea(uint8_t x, uint8_t page, uint8_t width, uint8_t page_count);
void OLED_SetCursor(uint8_t x, uint8_t page);
void OLED_WriteCommand(uint8_t cmd) {
    HAL_GPIO_WritePin(OLED_DC_PORT, OLED_DC_PIN, GPIO_PIN_RESET); // Command
    HAL_GPIO_WritePin(OLED_CS_PORT, OLED_CS_PIN, GPIO_PIN_RESET);
    HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY);
    HAL_GPIO_WritePin(OLED_CS_PORT, OLED_CS_PIN, GPIO_PIN_SET);
}
void OLED_WriteData(uint8_t data) {
    HAL_GPIO_WritePin(OLED_DC_PORT, OLED_DC_PIN, GPIO_PIN_SET); // Data
    HAL_GPIO_WritePin(OLED_CS_PORT, OLED_CS_PIN, GPIO_PIN_RESET);
    HAL_SPI_Transmit(&hspi1, &data, 1, HAL_MAX_DELAY);
    HAL_GPIO_WritePin(OLED_CS_PORT, OLED_CS_PIN, GPIO_PIN_SET);
}
void OLED_Reset(void) {
    HAL_GPIO_WritePin(OLED_RES_PORT, OLED_RES_PIN, GPIO_PIN_RESET);
    HAL_Delay(10);
    HAL_GPIO_WritePin(OLED_RES_PORT, OLED_RES_PIN, GPIO_PIN_SET);
}

重点是初始化函数,根据上面时序图设计初始化代码

void OLED_Init(void) {
    HAL_Delay(100); // Wait for more than 100ms
    OLED_Reset(); // Hardware reset

    OLED_WriteCommand(0xAE); // Display off
    OLED_WriteCommand(0xD5); OLED_WriteCommand(0x80); // Set display clock divide ratio/oscillator frequency
    OLED_WriteCommand(0xA8); OLED_WriteCommand(0x3F); // Set multiplex ratio (1 to 64)
    OLED_WriteCommand(0xD3); OLED_WriteCommand(0x00); // Set display offset, No offset
    OLED_WriteCommand(0x40); // Set display start line to 0
    OLED_WriteCommand(0xA1); // Set segment re-map 0xA0->0xA1 (mirror horizontally)    
    // OLED_WriteCommand(0x20); OLED_WriteCommand(0x00); // Memory addressing mode: horizontal    
    OLED_WriteCommand(0xC8); // Set COM scan direction (remapped mode, vertical flip)
    OLED_WriteCommand(0xDA); OLED_WriteCommand(0x12); // Set COM pins hardware configuration
    OLED_WriteCommand(0x81); OLED_WriteCommand(0x7F); // Contrast control, RESET = 7Fh
    OLED_WriteCommand(0xD9); OLED_WriteCommand(0xF1); // Set pre-charge period
    OLED_WriteCommand(0xDB); OLED_WriteCommand(0x20); // Set Vcom detect, RESET = 20h
    OLED_WriteCommand(0xA4); // Entire display ON (resume RAM content display)
    OLED_WriteCommand(0xA6); // Set Normal display (not inverted)
    OLED_WriteCommand(0x8D); OLED_WriteCommand(0x14); // Enable charge pump, Enable charge pump regulator
    HAL_Delay(100); // Power stabilized delay (recommended ~100ms)
    OLED_WriteCommand(0xAF); // Display ON
}
void OLED_Clear(void)
{
    uint8_t i, j;

    for (i = 0; i < 8; i++)  // 8 pages total (for 64 rows)
    {
        OLED_SetCursor(0, i);
        for (j = 0; j < 128; j++)
        {
            OLED_WriteData(0x00);  // All pixels OFF
        }
    }
}
void OLED_SetCursor(uint8_t x, uint8_t page)
{
    OLED_WriteCommand(0xB0 + page);              // page (0..7)
    OLED_WriteCommand(0x00 | (x & 0x0F));        // low column
    OLED_WriteCommand(0x10 | (x >> 4));          // high column
}

至此,前期准备结束。下面开始在屏幕上显示内容。


要显示字符或者字符串需要三样东西:

  1. 一个字体表【A font table (bitmaps for each character)】
  2. 一个在(x, page)处绘制单个字符的函数【A function to draw one character at (x, page)
  3. 一个通过循环调用字符函数来绘制字符串的函数

创建一个 6×8 的 ASCII 小字体表

font6x8.h

#ifndef FONT6X8_H
#define FONT6X8_H

#include <stdint.h>

// 6x8 font, each character = 6 bytes (6 columns x 8 rows)
// Characters from ASCII 0x20 (' ') to 0x7E ('~')
extern const uint8_t Font6x8[][6];

#endif

font6x8.c

#include "font6x8.h"

// Source: classic 5x7 font extended to 6x8 (1 column spacing)
// Each entry: {col0, col1, col2, col3, col4, col5}
const uint8_t Font6x8[][6] =
{
    // ASCII 0x20 ' '
    {0x00,0x00,0x00,0x00,0x00,0x00}, // ' '
    {0x00,0x00,0x5F,0x00,0x00,0x00}, // '!'
    {0x00,0x07,0x00,0x07,0x00,0x00}, // '"'
    {0x14,0x7F,0x14,0x7F,0x14,0x00}, // '#'
    {0x24,0x2A,0x7F,0x2A,0x12,0x00}, // '$'
    {0x23,0x13,0x08,0x64,0x62,0x00}, // '%'
    {0x36,0x49,0x55,0x22,0x50,0x00}, // '&'
    {0x00,0x05,0x03,0x00,0x00,0x00}, // '''
    {0x00,0x1C,0x22,0x41,0x00,0x00}, // '('
    {0x00,0x41,0x22,0x1C,0x00,0x00}, // ')'
    {0x14,0x08,0x3E,0x08,0x14,0x00}, // '*'
    {0x08,0x08,0x3E,0x08,0x08,0x00}, // '+'
    {0x00,0x50,0x30,0x00,0x00,0x00}, // ','
    {0x08,0x08,0x08,0x08,0x08,0x00}, // '-'
    {0x00,0x60,0x60,0x00,0x00,0x00}, // '.'
    {0x20,0x10,0x08,0x04,0x02,0x00}, // '/'

    {0x3E,0x51,0x49,0x45,0x3E,0x00}, // '0'
    {0x00,0x42,0x7F,0x40,0x00,0x00}, // '1'
    {0x42,0x61,0x51,0x49,0x46,0x00}, // '2'
    {0x21,0x41,0x45,0x4B,0x31,0x00}, // '3'
    {0x18,0x14,0x12,0x7F,0x10,0x00}, // '4'
    {0x27,0x45,0x45,0x45,0x39,0x00}, // '5'
    {0x3C,0x4A,0x49,0x49,0x30,0x00}, // '6'
    {0x01,0x71,0x09,0x05,0x03,0x00}, // '7'
    {0x36,0x49,0x49,0x49,0x36,0x00}, // '8'
    {0x06,0x49,0x49,0x29,0x1E,0x00}, // '9'

    {0x00,0x36,0x36,0x00,0x00,0x00}, // ':'
    {0x00,0x56,0x36,0x00,0x00,0x00}, // ';'
    {0x08,0x14,0x22,0x41,0x00,0x00}, // '<'
    {0x14,0x14,0x14,0x14,0x14,0x00}, // '='
    {0x00,0x41,0x22,0x14,0x08,0x00}, // '>'
    {0x02,0x01,0x51,0x09,0x06,0x00}, // '?'
    {0x32,0x49,0x79,0x41,0x3E,0x00}, // '@'

    {0x7E,0x11,0x11,0x11,0x7E,0x00}, // 'A'
    {0x7F,0x49,0x49,0x49,0x36,0x00}, // 'B'
    {0x3E,0x41,0x41,0x41,0x22,0x00}, // 'C'
    {0x7F,0x41,0x41,0x22,0x1C,0x00}, // 'D'
    {0x7F,0x49,0x49,0x49,0x41,0x00}, // 'E'
    {0x7F,0x09,0x09,0x09,0x01,0x00}, // 'F'
    {0x3E,0x41,0x49,0x49,0x7A,0x00}, // 'G'
    {0x7F,0x08,0x08,0x08,0x7F,0x00}, // 'H'
    {0x00,0x41,0x7F,0x41,0x00,0x00}, // 'I'
    {0x20,0x40,0x41,0x3F,0x01,0x00}, // 'J'
    {0x7F,0x08,0x14,0x22,0x41,0x00}, // 'K'
    {0x7F,0x40,0x40,0x40,0x40,0x00}, // 'L'
    {0x7F,0x02,0x0C,0x02,0x7F,0x00}, // 'M'
    {0x7F,0x04,0x08,0x10,0x7F,0x00}, // 'N'
    {0x3E,0x41,0x41,0x41,0x3E,0x00}, // 'O'
    {0x7F,0x09,0x09,0x09,0x06,0x00}, // 'P'
    {0x3E,0x41,0x51,0x21,0x5E,0x00}, // 'Q'
    {0x7F,0x09,0x19,0x29,0x46,0x00}, // 'R'
    {0x46,0x49,0x49,0x49,0x31,0x00}, // 'S'
    {0x01,0x01,0x7F,0x01,0x01,0x00}, // 'T'
    {0x3F,0x40,0x40,0x40,0x3F,0x00}, // 'U'
    {0x1F,0x20,0x40,0x20,0x1F,0x00}, // 'V'
    {0x3F,0x40,0x38,0x40,0x3F,0x00}, // 'W'
    {0x63,0x14,0x08,0x14,0x63,0x00}, // 'X'
    {0x07,0x08,0x70,0x08,0x07,0x00}, // 'Y'
    {0x61,0x51,0x49,0x45,0x43,0x00}, // 'Z'

    {0x00,0x7F,0x41,0x41,0x00,0x00}, // '['
    {0x02,0x04,0x08,0x10,0x20,0x00}, // '\'
    {0x00,0x41,0x41,0x7F,0x00,0x00}, // ']'
    {0x04,0x02,0x01,0x02,0x04,0x00}, // '^'
    {0x40,0x40,0x40,0x40,0x40,0x00}, // '_'
    {0x00,0x01,0x02,0x04,0x00,0x00}, // '`'

    {0x20,0x54,0x54,0x54,0x78,0x00}, // 'a'
    {0x7F,0x48,0x44,0x44,0x38,0x00}, // 'b'
    {0x38,0x44,0x44,0x44,0x20,0x00}, // 'c'
    {0x38,0x44,0x44,0x48,0x7F,0x00}, // 'd'
    {0x38,0x54,0x54,0x54,0x18,0x00}, // 'e'
    {0x08,0x7E,0x09,0x01,0x02,0x00}, // 'f'
    {0x0C,0x52,0x52,0x52,0x3E,0x00}, // 'g'
    {0x7F,0x08,0x04,0x04,0x78,0x00}, // 'h'
    {0x00,0x44,0x7D,0x40,0x00,0x00}, // 'i'
    {0x20,0x40,0x44,0x3D,0x00,0x00}, // 'j'
    {0x7F,0x10,0x28,0x44,0x00,0x00}, // 'k'
    {0x00,0x41,0x7F,0x40,0x00,0x00}, // 'l'
    {0x7C,0x04,0x18,0x04,0x78,0x00}, // 'm'
    {0x7C,0x08,0x04,0x04,0x78,0x00}, // 'n'
    {0x38,0x44,0x44,0x44,0x38,0x00}, // 'o'
    {0x7C,0x14,0x14,0x14,0x08,0x00}, // 'p'
    {0x08,0x14,0x14,0x18,0x7C,0x00}, // 'q'
    {0x7C,0x08,0x04,0x04,0x08,0x00}, // 'r'
    {0x48,0x54,0x54,0x54,0x20,0x00}, // 's'
    {0x04,0x3F,0x44,0x40,0x20,0x00}, // 't'
    {0x3C,0x40,0x40,0x20,0x7C,0x00}, // 'u'
    {0x1C,0x20,0x40,0x20,0x1C,0x00}, // 'v'
    {0x3C,0x40,0x30,0x40,0x3C,0x00}, // 'w'
    {0x44,0x28,0x10,0x28,0x44,0x00}, // 'x'
    {0x0C,0x50,0x50,0x50,0x3C,0x00}, // 'y'
    {0x44,0x64,0x54,0x4C,0x44,0x00}, // 'z'

    {0x00,0x08,0x36,0x41,0x00,0x00}, // '{'
    {0x00,0x00,0x7F,0x00,0x00,0x00}, // '|'
    {0x00,0x41,0x36,0x08,0x00,0x00}, // '}'
    {0x08,0x04,0x08,0x10,0x08,0x00}, // '~'
};

现在你拥有了一个适用于所有可打印 ASCII 字符的单色字体。 在oled.h中添加函数原型

// New functions: 6x8
void OLED_DrawChar(uint8_t x, uint8_t page, char c);
void OLED_DrawString(uint8_t x, uint8_t page, const char *str);

oled.c添加字体和函数实现

#include "oled.h"
#include "font6x8.h"

// ...

void OLED_DrawChar(uint8_t x, uint8_t page, char c)
{
    // We only have printable characters 0x20..0x7E
    if ((uint8_t)c < 0x20 || (uint8_t)c > 0x7E) {
        c = '?'; // fallback
    }

    uint8_t index = (uint8_t)c - 0x20; // map ASCII to font index

    OLED_SetCursor(x, page);

    for (uint8_t i = 0; i < 6; i++) { // 6 columns per char
        OLED_WriteData(Font6x8[index][i]);
    }
}

void OLED_DrawString(uint8_t x, uint8_t page, const char *str)
{
    while (*str) {
        // If we reach right edge, go to next page (simple wrap)
        if (x > OLED_WIDTH - 6) {
            x = 0;
            page++;
            if (page >= 8) { // no more space
                break;
            }
        }

        OLED_DrawChar(x, page, *str);
        x += 6;    // move to next character position
        str++;     // next character in C string
    }
}

main.c中使用

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_SPI1_Init();

    OLED_Init();
    OLED_Clear();

    // One character "A" at x=0, page=0 (top-left)
    OLED_DrawChar(0, 0, 'A');

    // "Hello world!" starting at x=0, page=1 (second text line)
    OLED_DrawString(0, 1, "Hello world!");

    while (1) {
        // your loop
    }
}

完成三样东西后,屏幕能够显示字符和字符串了,但是看起来字体太小了。因为 6×8 的字体就是这么小,如果要放大字体,那么需要使用更大的位图字体(bitmap font),例如 8×16,16×16

定义 8×16 字体

font8x16.h

#ifndef FONT8X16_H
#define FONT8X16_H

#include <stdint.h>

// 6x8 font, each character = 6 bytes (6 columns x 8 rows)
// Characters from ASCII 0x20 (' ') to 0x7E ('~')
extern const uint8_t Font8x16[][16];

#endif

font8x16.c

#include "font8x16.h"
// From https://github.com/datacute/Tiny4kOLED/blob/master/src/font8x16.h
const uint8_t Font8x16[][16] =
{
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, //   0
  0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x33,0x30,0x00,0x00,0x00, // ! 1
  0x00,0x10,0x0C,0x06,0x10,0x0C,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // " 2
  0x40,0xC0,0x78,0x40,0xC0,0x78,0x40,0x00,0x04,0x3F,0x04,0x04,0x3F,0x04,0x04,0x00, // # 3
  0x00,0x70,0x88,0xFC,0x08,0x30,0x00,0x00,0x00,0x18,0x20,0xFF,0x21,0x1E,0x00,0x00, // $ 4
  0xF0,0x08,0xF0,0x00,0xE0,0x18,0x00,0x00,0x00,0x21,0x1C,0x03,0x1E,0x21,0x1E,0x00, // % 5
  0x00,0xF0,0x08,0x88,0x70,0x00,0x00,0x00,0x1E,0x21,0x23,0x24,0x19,0x27,0x21,0x10, // & 6
  0x10,0x16,0x0E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // ' 7
  0x00,0x00,0x00,0xE0,0x18,0x04,0x02,0x00,0x00,0x00,0x00,0x07,0x18,0x20,0x40,0x00, // ( 8
  0x00,0x02,0x04,0x18,0xE0,0x00,0x00,0x00,0x00,0x40,0x20,0x18,0x07,0x00,0x00,0x00, // ) 9
  0x40,0x40,0x80,0xF0,0x80,0x40,0x40,0x00,0x02,0x02,0x01,0x0F,0x01,0x02,0x02,0x00, // * 10
  0x00,0x00,0x00,0xF0,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x1F,0x01,0x01,0x01,0x00, // + 11
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0xB0,0x70,0x00,0x00,0x00,0x00,0x00, // , 12
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01, // - 13
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x30,0x00,0x00,0x00,0x00,0x00, // . 14
  0x00,0x00,0x00,0x00,0x80,0x60,0x18,0x04,0x00,0x60,0x18,0x06,0x01,0x00,0x00,0x00, // / 15
  0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00,0x00,0x0F,0x10,0x20,0x20,0x10,0x0F,0x00, // 0 16
  0x00,0x10,0x10,0xF8,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00, // 1 17
  0x00,0x70,0x08,0x08,0x08,0x88,0x70,0x00,0x00,0x30,0x28,0x24,0x22,0x21,0x30,0x00, // 2 18
  0x00,0x30,0x08,0x88,0x88,0x48,0x30,0x00,0x00,0x18,0x20,0x20,0x20,0x11,0x0E,0x00, // 3 19
  0x00,0x00,0xC0,0x20,0x10,0xF8,0x00,0x00,0x00,0x07,0x04,0x24,0x24,0x3F,0x24,0x00, // 4 20
  0x00,0xF8,0x08,0x88,0x88,0x08,0x08,0x00,0x00,0x19,0x21,0x20,0x20,0x11,0x0E,0x00, // 5 21
  0x00,0xE0,0x10,0x88,0x88,0x18,0x00,0x00,0x00,0x0F,0x11,0x20,0x20,0x11,0x0E,0x00, // 6 22
  0x00,0x38,0x08,0x08,0xC8,0x38,0x08,0x00,0x00,0x00,0x00,0x3F,0x00,0x00,0x00,0x00, // 7 23
  0x00,0x70,0x88,0x08,0x08,0x88,0x70,0x00,0x00,0x1C,0x22,0x21,0x21,0x22,0x1C,0x00, // 8 24
  0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00,0x00,0x00,0x31,0x22,0x22,0x11,0x0F,0x00, // 9 25
  0x00,0x00,0x00,0xC0,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x30,0x00,0x00,0x00, // : 26
  0x00,0x00,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x60,0x00,0x00,0x00,0x00, // ; 27
  0x00,0x00,0x80,0x40,0x20,0x10,0x08,0x00,0x00,0x01,0x02,0x04,0x08,0x10,0x20,0x00, // < 28
  0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x00,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x00, // = 29
  0x00,0x08,0x10,0x20,0x40,0x80,0x00,0x00,0x00,0x20,0x10,0x08,0x04,0x02,0x01,0x00, // > 30
  0x00,0x70,0x48,0x08,0x08,0x08,0xF0,0x00,0x00,0x00,0x00,0x30,0x36,0x01,0x00,0x00, // ? 31
  0xC0,0x30,0xC8,0x28,0xE8,0x10,0xE0,0x00,0x07,0x18,0x27,0x24,0x23,0x14,0x0B,0x00, // @ 32
  0x00,0x00,0xC0,0x38,0xE0,0x00,0x00,0x00,0x20,0x3C,0x23,0x02,0x02,0x27,0x38,0x20, // A 33
  0x08,0xF8,0x88,0x88,0x88,0x70,0x00,0x00,0x20,0x3F,0x20,0x20,0x20,0x11,0x0E,0x00, // B 34
  0xC0,0x30,0x08,0x08,0x08,0x08,0x38,0x00,0x07,0x18,0x20,0x20,0x20,0x10,0x08,0x00, // C 35
  0x08,0xF8,0x08,0x08,0x08,0x10,0xE0,0x00,0x20,0x3F,0x20,0x20,0x20,0x10,0x0F,0x00, // D 36
  0x08,0xF8,0x88,0x88,0xE8,0x08,0x10,0x00,0x20,0x3F,0x20,0x20,0x23,0x20,0x18,0x00, // E 37
  0x08,0xF8,0x88,0x88,0xE8,0x08,0x10,0x00,0x20,0x3F,0x20,0x00,0x03,0x00,0x00,0x00, // F 38
  0xC0,0x30,0x08,0x08,0x08,0x38,0x00,0x00,0x07,0x18,0x20,0x20,0x22,0x1E,0x02,0x00, // G 39
  0x08,0xF8,0x08,0x00,0x00,0x08,0xF8,0x08,0x20,0x3F,0x21,0x01,0x01,0x21,0x3F,0x20, // H 40
  0x00,0x08,0x08,0xF8,0x08,0x08,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00, // I 41
  0x00,0x00,0x08,0x08,0xF8,0x08,0x08,0x00,0xC0,0x80,0x80,0x80,0x7F,0x00,0x00,0x00, // J 42
  0x08,0xF8,0x88,0xC0,0x28,0x18,0x08,0x00,0x20,0x3F,0x20,0x01,0x26,0x38,0x20,0x00, // K 43
  0x08,0xF8,0x08,0x00,0x00,0x00,0x00,0x00,0x20,0x3F,0x20,0x20,0x20,0x20,0x30,0x00, // L 44
  0x08,0xF8,0xF8,0x00,0xF8,0xF8,0x08,0x00,0x20,0x3F,0x00,0x3F,0x00,0x3F,0x20,0x00, // M 45
  0x08,0xF8,0x30,0xC0,0x00,0x08,0xF8,0x08,0x20,0x3F,0x20,0x00,0x07,0x18,0x3F,0x00, // N 46
  0xE0,0x10,0x08,0x08,0x08,0x10,0xE0,0x00,0x0F,0x10,0x20,0x20,0x20,0x10,0x0F,0x00, // O 47
  0x08,0xF8,0x08,0x08,0x08,0x08,0xF0,0x00,0x20,0x3F,0x21,0x01,0x01,0x01,0x00,0x00, // P 48
  0xE0,0x10,0x08,0x08,0x08,0x10,0xE0,0x00,0x0F,0x18,0x24,0x24,0x38,0x50,0x4F,0x00, // Q 49
  0x08,0xF8,0x88,0x88,0x88,0x88,0x70,0x00,0x20,0x3F,0x20,0x00,0x03,0x0C,0x30,0x20, // R 50
  0x00,0x70,0x88,0x08,0x08,0x08,0x38,0x00,0x00,0x38,0x20,0x21,0x21,0x22,0x1C,0x00, // S 51
  0x18,0x08,0x08,0xF8,0x08,0x08,0x18,0x00,0x00,0x00,0x20,0x3F,0x20,0x00,0x00,0x00, // T 52
  0x08,0xF8,0x08,0x00,0x00,0x08,0xF8,0x08,0x00,0x1F,0x20,0x20,0x20,0x20,0x1F,0x00, // U 53
  0x08,0x78,0x88,0x00,0x00,0xC8,0x38,0x08,0x00,0x00,0x07,0x38,0x0E,0x01,0x00,0x00, // V 54
  0xF8,0x08,0x00,0xF8,0x00,0x08,0xF8,0x00,0x03,0x3C,0x07,0x00,0x07,0x3C,0x03,0x00, // W 55
  0x08,0x18,0x68,0x80,0x80,0x68,0x18,0x08,0x20,0x30,0x2C,0x03,0x03,0x2C,0x30,0x20, // X 56
  0x08,0x38,0xC8,0x00,0xC8,0x38,0x08,0x00,0x00,0x00,0x20,0x3F,0x20,0x00,0x00,0x00, // Y 57
  0x10,0x08,0x08,0x08,0xC8,0x38,0x08,0x00,0x20,0x38,0x26,0x21,0x20,0x20,0x18,0x00, // Z 58
  0x00,0x00,0x00,0xFE,0x02,0x02,0x02,0x00,0x00,0x00,0x00,0x7F,0x40,0x40,0x40,0x00, // [ 59
  0x00,0x0C,0x30,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x06,0x38,0xC0,0x00, // \ 60
  0x00,0x02,0x02,0x02,0xFE,0x00,0x00,0x00,0x00,0x40,0x40,0x40,0x7F,0x00,0x00,0x00, // ] 61
  0x00,0x00,0x04,0x02,0x02,0x02,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // ^ 62
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, // _ 63
  0x00,0x02,0x02,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // ` 64
  0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x00,0x19,0x24,0x22,0x22,0x22,0x3F,0x20, // a 65
  0x08,0xF8,0x00,0x80,0x80,0x00,0x00,0x00,0x00,0x3F,0x11,0x20,0x20,0x11,0x0E,0x00, // b 66
  0x00,0x00,0x00,0x80,0x80,0x80,0x00,0x00,0x00,0x0E,0x11,0x20,0x20,0x20,0x11,0x00, // c 67
  0x00,0x00,0x00,0x80,0x80,0x88,0xF8,0x00,0x00,0x0E,0x11,0x20,0x20,0x10,0x3F,0x20, // d 68
  0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x00,0x1F,0x22,0x22,0x22,0x22,0x13,0x00, // e 69
  0x00,0x80,0x80,0xF0,0x88,0x88,0x88,0x18,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00, // f 70
  0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x00,0x00,0x6B,0x94,0x94,0x94,0x93,0x60,0x00, // g 71
  0x08,0xF8,0x00,0x80,0x80,0x80,0x00,0x00,0x20,0x3F,0x21,0x00,0x00,0x20,0x3F,0x20, // h 72
  0x00,0x80,0x98,0x98,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00, // i 73
  0x00,0x00,0x00,0x80,0x98,0x98,0x00,0x00,0x00,0xC0,0x80,0x80,0x80,0x7F,0x00,0x00, // j 74
  0x08,0xF8,0x00,0x00,0x80,0x80,0x80,0x00,0x20,0x3F,0x24,0x02,0x2D,0x30,0x20,0x00, // k 75
  0x00,0x08,0x08,0xF8,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00, // l 76
  0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x00,0x20,0x3F,0x20,0x00,0x3F,0x20,0x00,0x3F, // m 77
  0x80,0x80,0x00,0x80,0x80,0x80,0x00,0x00,0x20,0x3F,0x21,0x00,0x00,0x20,0x3F,0x20, // n 78
  0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x00,0x1F,0x20,0x20,0x20,0x20,0x1F,0x00, // o 79
  0x80,0x80,0x00,0x80,0x80,0x00,0x00,0x00,0x80,0xFF,0xA1,0x20,0x20,0x11,0x0E,0x00, // p 80
  0x00,0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x0E,0x11,0x20,0x20,0xA0,0xFF,0x80, // q 81
  0x80,0x80,0x80,0x00,0x80,0x80,0x80,0x00,0x20,0x20,0x3F,0x21,0x20,0x00,0x01,0x00, // r 82
  0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x00,0x00,0x33,0x24,0x24,0x24,0x24,0x19,0x00, // s 83
  0x00,0x80,0x80,0xE0,0x80,0x80,0x00,0x00,0x00,0x00,0x00,0x1F,0x20,0x20,0x00,0x00, // t 84
  0x80,0x80,0x00,0x00,0x00,0x80,0x80,0x00,0x00,0x1F,0x20,0x20,0x20,0x10,0x3F,0x20, // u 85
  0x80,0x80,0x80,0x00,0x00,0x80,0x80,0x80,0x00,0x01,0x0E,0x30,0x08,0x06,0x01,0x00, // v 86
  0x80,0x80,0x00,0x80,0x00,0x80,0x80,0x80,0x0F,0x30,0x0C,0x03,0x0C,0x30,0x0F,0x00, // w 87
  0x00,0x80,0x80,0x00,0x80,0x80,0x80,0x00,0x00,0x20,0x31,0x2E,0x0E,0x31,0x20,0x00, // x 88
  0x80,0x80,0x80,0x00,0x00,0x80,0x80,0x80,0x80,0x81,0x8E,0x70,0x18,0x06,0x01,0x00, // y 89
  0x00,0x80,0x80,0x80,0x80,0x80,0x80,0x00,0x00,0x21,0x30,0x2C,0x22,0x21,0x30,0x00, // z 90
  0x00,0x00,0x00,0x00,0x80,0x7C,0x02,0x02,0x00,0x00,0x00,0x00,0x00,0x3F,0x40,0x40, // { 91
  0x00,0x00,0x00,0x00,0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x00,0x00,0x00, // | 92
  0x00,0x02,0x02,0x7C,0x80,0x00,0x00,0x00,0x00,0x40,0x40,0x3F,0x00,0x00,0x00,0x00, // } 93
  0x00,0x06,0x01,0x01,0x02,0x02,0x04,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // ~ 94
};

添加绘制 8×16 的函数 oled.h

// New: 8x16 font 
void OLED_DrawChar16(uint8_t x, uint8_t page, char c); 
void OLED_DrawString16(uint8_t x, uint8_t page, const char *str);

oled.c

void OLED_DrawChar16(uint8_t x, uint8_t page, char c)
{
    // A 16-pixel tall char needs two pages: page and page+1.
    if (page >= 7) {
        // No room for bottom half if page is 7
        return;
    }

    if ((uint8_t)c < 0x20 || (uint8_t)c > 0x7E) {
        c = '?';  // fallback character
    }

    uint8_t index = (uint8_t)c - 0x20;

    // Top 8 rows (page)
    OLED_SetCursor(x, page);
    for (uint8_t col = 0; col < 8; col++) {
        OLED_WriteData(Font8x16[index][col]);     // bytes 0..7
    }

    // Bottom 8 rows (page + 1)
    OLED_SetCursor(x, page + 1);
    for (uint8_t col = 0; col < 8; col++) {
        OLED_WriteData(Font8x16[index][col + 8]); // bytes 8..15
    }

    // Optional: add 1-pixel spacing column
    OLED_SetCursor(x + 8, page);
    OLED_WriteData(0x00);
    OLED_SetCursor(x + 8, page + 1);
    OLED_WriteData(0x00);
}

void OLED_DrawString16(uint8_t x, uint8_t page, const char *str)
{
    while (*str) {
        // Each character uses 9 px in width (8 + 1 spacing)
        if (x > OLED_WIDTH - 9) {
            x = 0;
            page += 2;     // Move down by 2 pages (16 pixels)
            if (page >= 7) // No more vertical space
                break;
        }

        OLED_DrawChar16(x, page, *str);
        x += 9;   // move to next character position
        str++;
    }
}

然后在 main.c中添加

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_SPI1_Init();

    OLED_Init();
    OLED_Clear();

    // Small text (6x8)
    OLED_DrawString(0, 0, "Small text");

    // Big text (8x16) starting at top pages 2 & 3
    OLED_DrawString16(0, 2, "Hello");

    while (1) { }
}

英文字符串已经完成。接下来需要显示中文。

首先,使用PCtoLCD2002 获取所要显示的汉字的C数组,比如"你好世界"这四个字

{0x00,0x80,0x60,0xF8,0x07,0x40,0x20,0x18,0x0F,0x08,0xC8,0x08,0x08,0x28,0x18,0x00},
{0x01,0x00,0x00,0xFF,0x00,0x10,0x0C,0x03,0x40,0x80,0x7F,0x00,0x01,0x06,0x18,0x00},/*"你",0*/
{0x10,0x10,0xF0,0x1F,0x10,0xF0,0x00,0x80,0x82,0x82,0xE2,0x92,0x8A,0x86,0x80,0x00},
{0x40,0x22,0x15,0x08,0x16,0x61,0x00,0x00,0x40,0x80,0x7F,0x00,0x00,0x00,0x00,0x00},/*"好",1*/
{0x20,0x20,0x20,0xFE,0x20,0x20,0xFF,0x20,0x20,0x20,0xFF,0x20,0x20,0x20,0x20,0x00},
{0x00,0x00,0x00,0x7F,0x40,0x40,0x47,0x44,0x44,0x44,0x47,0x40,0x40,0x40,0x00,0x00},/*"世",2*/
{0x00,0x00,0x00,0xFE,0x92,0x92,0x92,0xFE,0x92,0x92,0x92,0xFE,0x00,0x00,0x00,0x00},
{0x08,0x08,0x04,0x84,0x62,0x1E,0x01,0x00,0x01,0xFE,0x02,0x04,0x04,0x08,0x08,0x00},/*"界",3*/

PCtoLCD2002 生成的每个汉字提供 32 字节:

  • 第一组 16 字节 → 顶部 8 行(页面 P
  • 第二组 16 字节 → 底部 8 行(页面 P+1
  • 16 列宽

16×16 字体

font16x16.h

#ifndef FONT16X16_H
#define FONT16X16_H

#include <stdint.h>

// Each entry = 16 bytes; two entries per Chinese character (top + bottom)
extern const uint8_t Font16x16[][16];

// Optional: readable indices for each character
#define CHN_NI     0  // "你"
#define CHN_HAO    1  // "好"
#define CHN_SHI    2  // "世"
#define CHN_JIE    3  // "界"

#endif

font16x16.c

#include "font16x16.h"

const uint8_t Font16x16[][16] =
{
    {0x00,0x80,0x60,0xF8,0x07,0x40,0x20,0x18,0x0F,0x08,0xC8,0x08,0x08,0x28,0x18,0x00},
    {0x01,0x00,0x00,0xFF,0x00,0x10,0x0C,0x03,0x40,0x80,0x7F,0x00,0x01,0x06,0x18,0x00},/*"你",0*/

    {0x10,0x10,0xF0,0x1F,0x10,0xF0,0x00,0x80,0x82,0x82,0xE2,0x92,0x8A,0x86,0x80,0x00},
    {0x40,0x22,0x15,0x08,0x16,0x61,0x00,0x00,0x40,0x80,0x7F,0x00,0x00,0x00,0x00,0x00},/*"好",1*/

    {0x20,0x20,0x20,0xFE,0x20,0x20,0xFF,0x20,0x20,0x20,0xFF,0x20,0x20,0x20,0x20,0x00},
    {0x00,0x00,0x00,0x7F,0x40,0x40,0x47,0x44,0x44,0x44,0x47,0x40,0x40,0x40,0x00,0x00},/*"世",2*/

    {0x00,0x00,0x00,0xFE,0x92,0x92,0x92,0xFE,0x92,0x92,0x92,0xFE,0x00,0x00,0x00,0x00},
    {0x08,0x08,0x04,0x84,0x62,0x1E,0x01,0x00,0x01,0xFE,0x02,0x04,0x04,0x08,0x08,0x00},/*"界",3*/
};

定义绘制汉字的函数 oled.h

void OLED_DrawChinese(uint8_t x, uint8_t page, uint8_t index);
void OLED_DrawChineseString(uint8_t x, uint8_t page, const uint8_t *idx, uint8_t len);

oled.c

void OLED_DrawChinese(uint8_t x, uint8_t page, uint8_t index)
{
    // A 16-pixel-high character uses 2 pages: page and page+1
    if (page >= 7) {
        // No room for bottom half
        return;
    }

    // Two 16-byte blocks per character
    uint8_t topIdx    = index * 2;
    uint8_t bottomIdx = index * 2 + 1;

    // Optional bounds check (if you know how many characters you have):
    // uint8_t totalBlocks = sizeof(Font16x16) / 16;
    // if (bottomIdx >= totalBlocks) return;

    // Top 8 rows
    OLED_SetCursor(x, page);
    for (uint8_t col = 0; col < 16; col++) {
        OLED_WriteData(Font16x16[topIdx][col]);
    }

    // Bottom 8 rows
    OLED_SetCursor(x, page + 1);
    for (uint8_t col = 0; col < 16; col++) {
        OLED_WriteData(Font16x16[bottomIdx][col]);
    }
}

void OLED_DrawChineseString(uint8_t x, uint8_t page, const uint8_t *idx, uint8_t len)
{
    for (uint8_t i = 0; i < len; i++) {
        if (x > OLED_WIDTH - 16) {
            x = 0;
            page += 2;  // 16 pixels down (2 pages)
            if (page >= 7) {
                return;  // no more space
            }
        }

        OLED_DrawChinese(x, page, idx[i]);
        x += 16;   // move right by 16 pixels (no extra gap)
    }
}

main.c中显示汉字

#include "oled.h"
#include "font16x16.h"

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_SPI1_Init();

    OLED_Init();
    OLED_Clear();

    // Optional: show some English text
    OLED_DrawString(0, 0, "Hello");

    // Chinese "你好世界"
    uint8_t hello_cn[] = { CHN_NI, CHN_HAO, CHN_SHI, CHN_JIE };

    // Start at x=0, page=2 (so characters are not at very top)
    OLED_DrawChineseString(0, 2, hello_cn, 4);

    while (1) {
    }
}

最后,显示图片和动画。其实动画就是一帧一帧的图片播放。SSD1306只能使用单色位图(monochrome bitmap)

首先需要128×64的图片,然后通过Img2Lcd将图片转换成单色的BMP格式,也就是1bit bmp图片,然后通过下面的C代码转换成C数组,这就是一帧的图片

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#define OLED_WIDTH   128
#define OLED_HEIGHT  64
#define OLED_BYTES   (OLED_WIDTH * OLED_HEIGHT / 8)

#pragma pack(push, 1)
typedef struct {
    uint16_t bfType;      // 'BM'
    uint32_t bfSize;
    uint16_t bfReserved1;
    uint16_t bfReserved2;
    uint32_t bfOffBits;   // offset to pixel data
} BITMAPFILEHEADER;

typedef struct {
    uint32_t biSize;      // header size (40)
    int32_t  biWidth;
    int32_t  biHeight;
    uint16_t biPlanes;
    uint16_t biBitCount;
    uint32_t biCompression;
    uint32_t biSizeImage;
    int32_t  biXPelsPerMeter;
    int32_t  biYPelsPerMeter;
    uint32_t biClrUsed;
    uint32_t biClrImportant;
} BITMAPINFOHEADER;
#pragma pack(pop)

int main(int argc, char *argv[])
{
    if (argc != 3) {
        printf("Usage: %s <source.bmp> <target.h>\n", argv[0]);
        return 1;
    }

    const char *inName  = argv[1];
    const char *outName = argv[2];

    FILE *in = fopen(inName, "rb");
    if (!in) {
        perror("fopen input");
        return 1;
    }

    BITMAPFILEHEADER bfh;
    BITMAPINFOHEADER bih;

    fread(&bfh, sizeof(bfh), 1, in);
    fread(&bih, sizeof(bih), 1, in);

    if (bfh.bfType != 0x4D42) {    // 'BM'
        fprintf(stderr, "Not a BMP file\n");
        return 1;
    }
    if (bih.biBitCount != 1) {
        fprintf(stderr, "Only 1-bit BMP supported (got %d bpp)\n", bih.biBitCount);
        return 1;
    }
    if (bih.biWidth != OLED_WIDTH || abs(bih.biHeight) != OLED_HEIGHT) {
        fprintf(stderr, "BMP must be %dx%d (got %dx%d)\n",
                OLED_WIDTH, OLED_HEIGHT, bih.biWidth, bih.biHeight);
        return 1;
    }

    // BMP rows: each row padded to 4-byte boundary
    int rowBytes = ((bih.biWidth + 31) / 32) * 4;   // for 1 bpp: ceil(width/32)*4
    int height   = abs(bih.biHeight);
    int topDown  = (bih.biHeight < 0);              // rarely used, but handle

    // Read full pixel data
    uint8_t *bmp = (uint8_t *)malloc(rowBytes * height);
    if (!bmp) {
        fprintf(stderr, "Out of memory\n");
        return 1;
    }

    fseek(in, bfh.bfOffBits, SEEK_SET);
    fread(bmp, 1, rowBytes * height, in);
    fclose(in);

    // Convert to SSD1306 format: pages (8 rows) × columns
    uint8_t oled[OLED_BYTES];

    for (int page = 0; page < OLED_HEIGHT / 8; page++) {
        for (int x = 0; x < OLED_WIDTH; x++) {
            uint8_t byte = 0;
            for (int bit = 0; bit < 8; bit++) {
                int y = page * 8 + bit;

                int bmpY = topDown ? y : (height - 1 - y); // BMP is usually bottom-up
                int bmpX = x;

                int rowOffset  = bmpY * rowBytes;
                int byteIndex  = rowOffset + (bmpX / 8);
                uint8_t mask   = 0x80 >> (bmpX & 7);  // leftmost pixel is MSB

                int pixelOn = (bmp[byteIndex] & mask) ? 1 : 0;

                if (pixelOn) {
                    byte |= (1 << bit);  // bit0=top, bit7=bottom
                }
            }
            oled[page * OLED_WIDTH + x] = byte;
        }
    }

    free(bmp);

    // Write out as C array
    FILE *out = fopen(outName, "w");
    if (!out) {
        perror("fopen output");
        return 1;
    }

    fprintf(out, "const unsigned char img_data[%d] = {\n", OLED_BYTES);

    for (int i = 0; i < OLED_BYTES; i++) {
        fprintf(out, "0x%02X,", oled[i]);
        if ((i % 16) == 15) fprintf(out, "\n");
    }

    fprintf(out, "};\n");
    fclose(out);

    printf("Converted %s -> %s OK\n", inName, outName);
    return 0;
}

然后编译gcc converter.c -o converter 运行converter.exe input.bmp output.h output.h 就是一帧图片,然后显示出来

#include "output.h"  // contains const unsigned char img_data[1024];

void ShowImage(void)
{
    uint8_t page, x;
    const uint8_t *p = img_data;

    for (page = 0; page < 8; page++) {
        OLED_SetCursor(0, page);
        for (x = 0; x < 128; x++) {
            OLED_WriteData(*p++);
        }
    }
}
显示动画

创建frames.h

/* frames.h  –  ALL animation frame data in one place */
#ifndef FRAMES_H
#define FRAMES_H

#define FRAME_WIDTH        128
#define FRAME_HEIGHT       64
#define FRAME_SIZE         (FRAME_WIDTH * FRAME_HEIGHT / 8)   // 1024
#define ANIM_FRAME_COUNT   12    // change if you have more/less frames

/* ---- Frame data (paste converter output here) ---- */

/* Example: frame 0 */
const unsigned char frame0[FRAME_SIZE] = {
    /* paste converter output for picture 0 here, rename to frame0 */
};

/* frame 1 */
const unsigned char frame1[FRAME_SIZE] = {
    /* frame 1 data */
};

/* ... repeat for all frames ... */

const unsigned char frame11[FRAME_SIZE] = {
    /* frame 11 data */
};

/* ---- Pointer table to frames ---- */
const unsigned char* const AnimFrames[ANIM_FRAME_COUNT] =
{
    frame0,
    frame1,
    frame2,
    frame3,
    frame4,
    frame5,
    frame6,
    frame7,
    frame8,
    frame9,
    frame10,
    frame11
};

#endif /* FRAMES_H */

oled.h添加绘图 API

/* New: draw a bitmap already in SSD1306 page format */
void OLED_DrawBitmap(uint8_t x, uint8_t page, uint8_t width, uint8_t height, const unsigned char *bmp);
void PlayAnimation(void);

oled.c实现OLED_DrawBitmap

void OLED_DrawBitmap(uint8_t x,
                     uint8_t page,
                     uint8_t width,
                     uint8_t height,
                     const unsigned char *bmp)
{
    uint8_t pages = height / 8;

    for (uint8_t p = 0; p < pages; ++p)
    {
        OLED_SetCursor(x, page + p);

        for (uint8_t col = 0; col < width; ++col)
        {
            OLED_WriteData(*bmp++);
        }
    }
}

void PlayAnimation(void)
{
    const uint16_t frameDelayMs = 200;  // adjust speed

    while (1)
    {
        for (uint8_t i = 0; i < ANIM_FRAME_COUNT; ++i)
        {
            OLED_DrawBitmap(
                0,                 // x
                0,                 // page
                FRAME_WIDTH,       // 128
                FRAME_HEIGHT,      // 64
                AnimFrames[i]);    // const unsigned char*

            HAL_Delay(frameDelayMs);
        }
    }
}

main.c中测试

#include "main.h"
#include "oled.h"
#include "frames.h"

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_SPI1_Init();

    OLED_Init();
    OLED_Clear();

    PlayAnimation();   // loop forever

    while (1)
    {
    }
}

总结:

Here's a clean recap of the whole journey, ignoring all the side mistakes and focusing only on:

From: show characters
To: show full‑screen animation


1. How text/characters are usually drawn on the OLED

1.1. Display basics

Your 128×64 SSD1306‑type OLED is organized in pages:

  • Width: 128 pixels → x = 0…127
  • Height: 64 pixels, grouped in 8‑pixel‑high pages → page = 0…7
  • Each byte = vertical column of 8 pixels.

So for a full screen:

  • 128 (columns) × 8 (pages) = 1024 bytes

1.2. Text drawing flow

Typical “show character” code does something like:

void OLED_SetCursor(uint8_t x, uint8_t page);  // set column and page
void OLED_WriteData(uint8_t data);             // send one byte to display

For a font (say 8×16) you have:

const uint8_t Font8x16[][16] = { /* font data */ };

To draw a character:

  1. Look up its bytes in the font table.
  2. Position the cursor (OLED_SetCursor(x, page)).
  3. Write out that character’s bytes with OLED_WriteData(...).
  4. Maybe two pages if height is 16: page p and p+1.

So the “show character” pipeline is:

Font data → bytes in RAM/flash → OLED_WriteData(...) → pixels on screen


2. Extending that idea to images / bitmaps

Animation frames are just big bitmaps instead of small font glyphs:

  • One frame covers all 128×64 pixels.
  • Size = 128 × 64 / 8 = 1024 bytes.
  • Layout: same page format as the OLED expects.

So we build a generic helper:

void OLED_DrawBitmap(uint8_t x,
                     uint8_t page,
                     uint8_t width,
                     uint8_t height,
                     const unsigned char *bmp)
{
    uint8_t pages = height / 8;

    for (uint8_t p = 0; p < pages; ++p)
    {
        OLED_SetCursor(x, page + p);         // select current page

        for (uint8_t col = 0; col < width; ++col)
        {
            OLED_WriteData(*bmp++);          // send next byte
        }
    }
}

Conceptually it’s the same as drawing a big font character:

  • You start at a position.
  • You stream bytes one by one.

3. Preparing frame data with the converter

Your new converter takes a BMP (128×64) and outputs data already in page order:

const unsigned char img_data[1024] = {
    0xFF, 0xFF, 0xFF, 0xFF, 0x00, ...
    /* 1024 bytes total */
};

This matches exactly what OLED_DrawBitmap expects. So there is no extra rearranging needed.

For animation, you simply generate multiple such arrays, one per frame.


4. Putting all frames into one header: frames.h

You decided to keep everything in one file, which is fine.

  1. For each frame’s output from the converter:

    • Change the name from img_data to frame0, frame1, …
  2. Put them all in frames.h:
/* frames.h */
#ifndef FRAMES_H
#define FRAMES_H

#define FRAME_WIDTH        128
#define FRAME_HEIGHT       64
#define FRAME_SIZE         1024       // 128*64/8
#define ANIM_FRAME_COUNT   N          // number of frames

/* Frame 0 */
const unsigned char frame0[FRAME_SIZE] = {
    /* 1024 bytes from converter */
};

/* Frame 1 */
const unsigned char frame1[FRAME_SIZE] = {
    /* another 1024 bytes */
};

/* ... more frames ... */

/* Pointer table */
const unsigned char* const AnimFrames[ANIM_FRAME_COUNT] = {
    frame0,
    frame1,
    /* frame2, frame3, ... */
};

#endif

Key rule: Only animation.c includes frames.h to avoid multiple-definition link errors.


5. The animation driver: PlayAnimation(void)

PlayAnimation just:

  1. Loops over each frame pointer in AnimFrames[].
  2. Calls OLED_DrawBitmap(...) to draw that frame.
  3. Waits a short delay (HAL_Delay) for animation speed.
  4. Repeats forever (or once, depending on your design).
/* animation.c */
#include "stm32f1xx_hal.h"
#include "oled.h"
#include "frames.h"
#include "animation.h"

void PlayAnimation(void)
{
    const uint16_t frameDelayMs = 75;  // animation speed

    while (1)
    {
        for (uint8_t i = 0; i < ANIM_FRAME_COUNT; ++i)
        {
            OLED_DrawBitmap(
                0,                 // x start
                0,                 // top page
                FRAME_WIDTH,       // 128
                FRAME_HEIGHT,      // 64
                AnimFrames[i]);    // frame pointer

            HAL_Delay(frameDelayMs);
        }
    }
}

So the “show animation” pipeline is:

Frame arrays (flash) → AnimFrames[]OLED_DrawBitmapOLED_WriteData → OLED

Same idea as text, just more bytes and multiple frames.


6. Integration in main.c

Once OLED and HAL are initialized, you simply hand control to PlayAnimation():

#include "oled.h"
#include "animation.h"

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_SPI1_Init();

    OLED_Init();
    OLED_Clear();

    PlayAnimation();     // never returns; displays animation

    while (1)
    {
        // not reached in this design
    }
}

7. Conceptual summary

  • Show characters = draw small bitmaps (font glyphs) at different x/page positions.
  • Show one static image = draw one big bitmap (128×64) with OLED_DrawBitmap.
  • Show animation = draw a sequence of big bitmaps stored as frameN[] arrays, with a delay between each.

Nothing fundamentally changes in how the OLED is driven; you just changed:

  • Source of bytes: font → frame arrays.
  • Amount of data: a few bytes per character → 1024 bytes per frame.
  • Timing: added a delay loop to create motion.

Create this tutorial with GPT-5.1