本次分享下板子上OLED显示屏移植U8G2库。U8G2是一个专为嵌入式设备设计的开源单色图形库。用于驱动单色OLED和LCD显示屏,因其功能强大、支持广泛而被众多开发者采用。
核心特点:
广泛的兼容性:支持超过128种显示控制器,几乎涵盖了所有常见的小型单色屏幕。包括但不限于:OLED驱动:SSD1306, SH1106, SSD1309等。LCD驱动:ST7565, ST7920, PCD8544(Nokia 5110)等。
多接口支持:兼容I²C、SPI和并行接口,可灵活适配Arduino、ESP32、STM32等多种平台。
丰富的图形与字体:提供点、线、圆、矩形、三角形等基本图形的绘制API。内置超过700种字体,并支持UTF-8编码,方便显示中文等多语言字符。
灵活的内存管理模式:针对资源受限的嵌入式环境,提供了三种缓冲模式。全缓冲模式:在内存中维护完整的显示缓冲区,适合流畅动画,但内存消耗大。页面缓冲模式:将屏幕分成多个“页”进行渲染,内存需求低,是官方推荐的模式。直接模式:无缓冲区,直接将数据写入屏幕,内存占用极低。
板上OLED使用I2C2接口SSD1306驱动。I2C2引脚为PA11和PA12。

在STM32CubeMX中选择I2C初始化参数。然后生成KEIL代码工程。

接着下载U8G2库代码,添加到工程中。

添加接口代码,发送数据使用HAL_I2C_Master_Transmit(&hi2c2, 0x3C<<1, buffer, buf_idx, HAL_MAX_DELAY);
#define U8G2_SSD1306_I2C_NUM 0
#define U8G2_SSD1306_I2C_ADDR (0x3C)
uint8_t u8x8_byte_hc32_hw_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
static uint8_t buffer[32]; /* u8g2/u8x8 will never send more than 32 bytes between START_TRANSFER and END_TRANSFER */
static uint8_t buf_idx;
uint8_t *data;
switch(msg)
{
case U8X8_MSG_BYTE_SEND:
data = (uint8_t *)arg_ptr;
while( arg_int > 0 )
{
buffer[buf_idx++] = *data;
data++;
arg_int--;
}
break;
case U8X8_MSG_BYTE_INIT:
/* add your custom code to init i2c subsystem */
break;
case U8X8_MSG_BYTE_SET_DC:
/* ignored for i2c */
break;
case U8X8_MSG_BYTE_START_TRANSFER:
buf_idx = 0;
break;
case U8X8_MSG_BYTE_END_TRANSFER:
HAL_I2C_Master_Transmit(&hi2c2, 0x3C<<1, buffer, buf_idx, HAL_MAX_DELAY);
//I2C_WriteOnly(U8G2_SSD1306_I2C_NUM, U8G2_SSD1306_I2C_ADDR, buffer, buf_idx);
break;
default:
return 0;
}
return 1;
}
uint8_t u8x8_gpio_and_delay(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
switch(msg)
{
case U8X8_MSG_GPIO_AND_DELAY_INIT:
/* Insert codes for initialization */
break;
case U8X8_MSG_DELAY_MILLI:
/* ms Delay */
delay_ms(arg_int);
break;
case U8X8_MSG_GPIO_MENU_SELECT:
u8x8_SetGPIOResult(u8x8, /* get menu select pin state */ 0);
break;
case U8X8_MSG_GPIO_MENU_NEXT:
u8x8_SetGPIOResult(u8x8, /* get menu next pin state */ 0);
break;
case U8X8_MSG_GPIO_MENU_PREV:
u8x8_SetGPIOResult(u8x8, /* get menu prev pin state */ 0);
break;
case U8X8_MSG_GPIO_MENU_HOME:
u8x8_SetGPIOResult(u8x8, /* get menu home pin state */ 0);
break;
default:
u8x8_SetGPIOResult(u8x8, 1); // default return value
break;
}
return 1;
}
然后就是U8G2 gui初始化代码和绘图字符显示测试代码:
void u8g2Init(u8g2_t *u8g2)
{
u8g2_Setup_ssd1306_i2c_128x64_noname_f(u8g2, U8G2_R0, u8x8_byte_hc32_hw_i2c, u8x8_gpio_and_delay);
u8g2_InitDisplay(u8g2);
u8g2_SetPowerSave(u8g2, 0);
u8g2_ClearBuffer(u8g2);
u8g2_SendBuffer(u8g2);
}
void draw(u8g2_t *u8g2)
{
u8g2_ClearDisplay(u8g2);
u8g2_SetFontMode(u8g2, 1); /*字体模式选择*/
u8g2_SetFontDirection(u8g2, 0); /*字体方向选择*/
u8g2_SetFont(u8g2, u8g2_font_inb24_mf); /*字库选择*/
u8g2_DrawStr(u8g2, 0, 20, "U");
u8g2_SetFontDirection(u8g2, 1);
u8g2_SetFont(u8g2, u8g2_font_inb30_mn);
u8g2_DrawStr(u8g2, 21,8,"8");
u8g2_SetFontDirection(u8g2, 0);
u8g2_SetFont(u8g2, u8g2_font_inb24_mf);
u8g2_DrawStr(u8g2, 51,30,"g");
u8g2_DrawStr(u8g2, 67,30,"\xb2");
u8g2_DrawHLine(u8g2, 2, 35, 47);
u8g2_DrawHLine(u8g2, 3, 36, 47);
u8g2_DrawVLine(u8g2, 45, 32, 12);
u8g2_DrawVLine(u8g2, 46, 33, 12);
u8g2_SetFont(u8g2, u8g2_font_5x8_tr);
u8g2_DrawStr(u8g2, 1,54,"github.com/olikraus/u8g2");
u8g2_SendBuffer(u8g2);
}
再在MAIN中调用初始化和测试代码,定义变量u8g2_t u8g2;

编译下载之后可以看到OLED屏上显示字符串。

下面利用OLED和按键基于U8G2实现一个简单的打方块游戏玩玩。游戏控制逻辑和显示代码使用AI生成的,我简单调整下就可以用了。
代码:
#include <stdlib.h> // 用于 rand() 和 srand()
#include <string.h> // 用于 sprintf()
#include <stdio.h> // 用于 sprintf()
#include "main.h" // 用于 sprintf()
#include "u8g2.h" // U8G2库核心头文件
// --- 硬件配置 (需要根据你的平台修改) ---
// 这些函数需要底层实现: 读取按键状态, 初始化硬件等
extern void delay_ms(uint32_t ms); // 毫秒级延时函数
u8g2_t u8g2;
// --- 游戏常量 ---
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define PADDLE_WIDTH 20
#define PADDLE_HEIGHT 4
#define PADDLE_Y (SCREEN_HEIGHT - 10)
#define BALL_RADIUS 2
#define BALL_SPEED 1.5f
#define BRICK_ROWS 5 // 砖块行数
#define BRICK_COLS 16 // 砖块列数
#define BRICK_WIDTH 8
#define BRICK_HEIGHT 6
#define BRICK_TOP 10 // 砖块区域顶部Y坐标
// --- 游戏状态变量 ---
typedef struct {
float ballX, ballY; // 球心坐标
float ballSpeedX, ballSpeedY;
int paddleX; // 挡板左上角X坐标
bool brickMap[BRICK_COLS][BRICK_ROWS]; // 砖块地图: true表示存在
int score;
bool gameOver;
bool levelComplete;
} GameState;
GameState game;
// --- 函数声明 ---
static void game_update(void);
static void game_draw(void);
static bool check_collision(float ax, float ay, float aw, float ah,
float bx, float by, float bw, float bh);
// ============================================================
// 初始化游戏
// ============================================================
void game_init(void)
{
// 初始化随机数种子 (如需随机生成砖块)
// srand(42); // 或者使用硬件RNG
game.paddleX = (SCREEN_WIDTH - PADDLE_WIDTH) / 2;
game.ballX = SCREEN_WIDTH / 2;
game.ballY = SCREEN_HEIGHT / 2;
game.ballSpeedX = BALL_SPEED;
game.ballSpeedY = -BALL_SPEED;
game.score = 0;
game.gameOver = false;
game.levelComplete = false;
// 初始化砖块地图: 全部置为true (表示有砖块)
for (int x = 0; x < BRICK_COLS; x++) {
for (int y = 0; y < BRICK_ROWS; y++) {
// 可选: 隔列留空, 增加趣味性
game.brickMap[x][y] = ( (x % 2 == 0) || (y % 2 == 0) );
}
}
}
// ============================================================
// 矩形碰撞检测工具函数
// ============================================================
static bool check_collision(float ax, float ay, float aw, float ah,
float bx, float by, float bw, float bh) {
return (ax < bx + bw && ax + aw > bx &&
ay < by + bh && ay + ah > by);
}
// ============================================================
// 更新游戏逻辑 (物理、碰撞等)
// ============================================================
static void game_update(void)
{
if (game.gameOver || game.levelComplete) {
return;
}
// 1. 读取按键并更新挡板位置
if (GetKey_KEY2()) {
game.paddleX -= 4;
if (game.paddleX < 0) game.paddleX = 0;
}
if (GetKey_KEY3()) {
game.paddleX += 4;
if (game.paddleX + PADDLE_WIDTH > SCREEN_WIDTH) {
game.paddleX = SCREEN_WIDTH - PADDLE_WIDTH;
}
}
// 2. 更新小球位置
game.ballX += game.ballSpeedX;
game.ballY += game.ballSpeedY;
// 3. 边界碰撞 (左右墙)
if (game.ballX - BALL_RADIUS < 0 || game.ballX + BALL_RADIUS > SCREEN_WIDTH) {
game.ballSpeedX = -game.ballSpeedX;
}
// 边界碰撞 (上墙)
if (game.ballY - BALL_RADIUS < 0) {
game.ballSpeedY = -game.ballSpeedY;
}
// 边界碰撞 (下墙 -> 游戏结束)
if (game.ballY + BALL_RADIUS > SCREEN_HEIGHT) {
game.gameOver = true;
return;
}
// 4. 挡板碰撞检测
if (check_collision(
game.ballX - BALL_RADIUS, game.ballY - BALL_RADIUS, BALL_RADIUS*2, BALL_RADIUS*2,
game.paddleX, PADDLE_Y, PADDLE_WIDTH, PADDLE_HEIGHT))
{
// 根据撞击位置改变球的水平速度, 增加可玩性
float hitPos = (game.ballX - game.paddleX) / PADDLE_WIDTH; // 0~1
game.ballSpeedX = (hitPos - 0.5f) * 3.0f; // -1.5 ~ 1.5
game.ballSpeedY = -BALL_SPEED; // 确保向上反弹
// 将球移到挡板上方, 防止卡碰撞
game.ballY = PADDLE_Y - BALL_RADIUS;
}
// 5. 砖块碰撞检测
for (int x = 0; x < BRICK_COLS; x++) {
for (int y = 0; y < BRICK_ROWS; y++) {
if (game.brickMap[x][y]) {
int brickX = x * BRICK_WIDTH;
int brickY = y * BRICK_HEIGHT + BRICK_TOP;
if (check_collision(
game.ballX - BALL_RADIUS, game.ballY - BALL_RADIUS, BALL_RADIUS*2, BALL_RADIUS*2,
brickX, brickY, BRICK_WIDTH, BRICK_HEIGHT)) {
// 消除砖块
game.brickMap[x][y] = false;
game.score++;
// 反转Y轴速度
game.ballSpeedY = -game.ballSpeedY;
// 检查是否所有砖块都被消除
bool allCleared = true;
for (int x2 = 0; x2 < BRICK_COLS; x2++) {
for (int y2 = 0; y2 < BRICK_ROWS; y2++) {
if (game.brickMap[x2][y2]) {
allCleared = false;
break;
}
}
if (!allCleared) break;
}
if (allCleared) {
game.levelComplete = true;
}
// 跳出双重循环 (一个球一次只撞一块砖)
goto collision_done;
}
}
}
}
collision_done:
// 空语句, 用于跳出嵌套循环
;
}
// ============================================================
// 绘制游戏画面 (使用U8G2 API)
// ============================================================
static void game_draw(void) {
// --- 开始绘制 (U8G2双缓冲标准流程) ---
u8g2_FirstPage(&u8g2);
do {
// 1. 绘制砖块
for (int x = 0; x < BRICK_COLS; x++) {
for (int y = 0; y < BRICK_ROWS; y++) {
if (game.brickMap[x][y]) {
int brickX = x * BRICK_WIDTH;
int brickY = y * BRICK_HEIGHT + BRICK_TOP;
// 绘制空心砖块 (更美观)
u8g2_DrawFrame(&u8g2, brickX, brickY, BRICK_WIDTH, BRICK_HEIGHT);
// 若需要实心砖块, 可使用: u8g2_DrawBox(&u8g2, brickX, brickY, BRICK_WIDTH, BRICK_HEIGHT);
}
}
}
// 2. 绘制挡板
u8g2_DrawBox(&u8g2, game.paddleX, PADDLE_Y, PADDLE_WIDTH, PADDLE_HEIGHT);
// 3. 绘制小球
u8g2_DrawDisc(&u8g2, (uint16_t)game.ballX, (uint16_t)game.ballY, BALL_RADIUS,U8G2_DRAW_ALL);
// 4. 绘制分数
char scoreStr[16];
sprintf(scoreStr, "Score:%d", game.score);
u8g2_SetFont(&u8g2, u8g2_font_6x10_tf);
u8g2_DrawStr(&u8g2, 0, 8, scoreStr);
// 5. 绘制游戏结束/通关信息
if (game.gameOver) {
u8g2_SetFont(&u8g2, u8g2_font_6x10_tf);
u8g2_DrawStr(&u8g2, 30, 32, "GAME OVER");
u8g2_DrawStr(&u8g2, 20, 45, "Press RESET");
} else if (game.levelComplete) {
u8g2_SetFont(&u8g2, u8g2_font_6x10_tf);
u8g2_DrawStr(&u8g2, 20, 32, "LEVEL COMPLETE!");
u8g2_DrawStr(&u8g2, 20, 45, "Press RESET");
}
} while (u8g2_NextPage(&u8g2));
}
// ============================================================
// 主循环 (在你的 main() 中调用)
// ============================================================
void game_main_loop(void)
{
// 检测复位按键
if (GetKey_KEY1()) {
game_init();
// 等待按键释放, 防止连续复位
while (GetKey_KEY1()) {
delay_ms(1);
}
}
// 更新游戏逻辑
game_update();
// 绘制画面
game_draw();
}
4个按键读取:
uint32_t GetKey_KEY1(void)
{
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) == GPIO_PIN_RESET) return 1;
else return 0;
}
uint32_t GetKey_KEY2(void)
{
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin) == GPIO_PIN_RESET) return 1;
else return 0;
}
uint32_t GetKey_KEY3(void)
{
if(HAL_GPIO_ReadPin(KEY3_GPIO_Port, KEY3_Pin) == GPIO_PIN_RESET) return 1;
else return 0;
}
uint32_t GetKey_KEY4(void)
{
if(HAL_GPIO_ReadPin(KEY4_GPIO_Port, KEY4_Pin) == GPIO_PIN_RESET) return 1;
else return 0;
}
main函数中调用游戏初始化和运行循环:

编译下载之后就可以看到游戏正常运行了。S1按键重新开始游戏,S2和S3按键左右移动挡板弹球。

