就是在电脑上运行U8G2 代码,模拟出OLED 屏幕上的显示效果。参考了别人写的教程:https://github.com/snqx-lqh/u8g2-windows-sdl-simulate。U8G2 本身带有SDL2 相关的支持,只是关于具体要怎么跑起来,找到的教程都比较土法炼钢,说要到处复制粘贴文件,然后写makefile。毕竟已经是21 世纪了,还是应该从那种恐龙时代的风格上前进一步,所以我决定试试用CMake。
准备项目文件
可以去用我配置好的项目:https://gitee.com/etberzin/u8g2_sdl2,注意里面有submodule,clone 的时候用:
shell
git clone https://gitee.com/etberzin/u8g2_sdl2.git --recurse-submodule
也可以参照我的结构自己搞。项目结构:
u8g2_sdl2
├─lib
│ ├─SDL2
│ └─u8g2
├─src
│ ├─ Arduino.h
│ ├─ entry.cpp
│ └─ main.cpp
└─CMakeLists.txt
lib/SDL2
是SDL2 的库文件,版本2.23.6-mingw,自己下载的话去找下面这个压缩包:

lib/u8g2
是U8G2 的库文件,这里只放了个submodule,指向U8G2 的repo https://github.com/olikraus/u8g2。要自己添加的话,运行下面的指令添加submodule:
shell
git submodule add https://github.com/olikraus/u8g2.git lib/u8g2
github 下载太慢的话,也可以去gitee 上找别人复制来的u8g2 repo。这两个库的文件都可以直接原样放进项目里,也可以自己裁剪掉不需要的部分。
src
里面是项目自己的源代码,详细的内容后面再说。
src/entry.cpp
:里面是main
函数,也负责处理SDL 的事件队列;src/man.cpp
:里面是Arduino 风格的setup
和loop
函数,主要的代码都放在这里;src/Arduino.h
:提供了兼容Arduino 的接口,方便直接使用Arduino 代码,目前里面只有delay
函数;
CMakeLists.txt
是项目的编译脚本,内容如下。这里面写的什么意思,可以去问AI 逐行解释,反正都是很无聊的东西。
cmake
cmake_minimum_required(VERSION 3.21.0)
# Get folder name and set as project name
get_filename_component(ProjectName ${CMAKE_CURRENT_SOURCE_DIR} NAME)
project(${ProjectName} VERSION 1.0 LANGUAGES C CXX)
message("Project name: ${PROJECT_NAME}")
# Set C++ standards
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_C_STANDARD_REQUIRED ON)
# Compile definitions
add_compile_definitions(U8G2_USE_LARGE_FONTS)
# Source files
file(GLOB MainFiles CONFIGURE_DEPENDS "src/*.cpp")
file(GLOB U8g2SDL CONFIGURE_DEPENDS "lib/u8g2/sys/sdl/common/*.c")
# Create executable
add_executable(${PROJECT_NAME}
${MainFiles}
${U8g2SDL}
)
target_include_directories(${PROJECT_NAME} PRIVATE src)
# U8G2 Library
add_subdirectory(lib/u8g2)
target_link_libraries(${PROJECT_NAME} PRIVATE u8g2)
# SDL2 Configuration
set(SDL2_DIR "lib/SDL2/cmake")
find_package(SDL2 REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE ${SDL2_LIBRARIES})
# copy DLL to build directory(Windows)
if(WIN32)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_SOURCE_DIR}/lib/SDL2/x86_64-w64-mingw32/bin/SDL2.dll
$<TARGET_FILE_DIR:${PROJECT_NAME}>
)
endif()
编译环境
推荐用VScode 搭配CMake 插件,因为我是这么用的。直接命令行编译应该也行,我没试。

然后要装cmake 和mingw,用scoop 可以直接安装,并且自动配置好环境变量,如果不用scoop,那就自己想别的办法。
shell
scoop install mingw-winlibs cmake
装好这些东西以后,用VScode 打开项目文件夹,cmake 扩展会自动启动,提示让选择编译工具,在这里选择mingw 提供的gcc。

然后会自动生成build
文件夹,输出一些类似下面这样的东西,就表示配置没有问题。
shell
[main] Configuring project: u8g2_sdl2
[proc] Executing command: C:\Users\shell\scoop\apps\mingw-winlibs\current\bin\cmake.EXE -DCMAKE_BUILD_TYPE:STRING=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -DCMAKE_C_COMPILER:FILEPATH=C:\Users\shell\scoop\apps\mingw-winlibs\current\bin\gcc.exe -DCMAKE_CXX_COMPILER:FILEPATH=C:\Users\shell\scoop\apps\mingw-winlibs\current\bin\g++.exe --no-warn-unused-cli -S C:/Users/shell/source/CXX/u8g2_sdl2 -B c:/Users/shell/source/CXX/u8g2_sdl2/build -G "MinGW Makefiles"
[cmake] Not searching for unused variables given on the command line.
[cmake] Project name: u8g2_sdl2
[cmake] -- Configuring done (2.3s)
[cmake] -- Generating done (0.4s)
[cmake] -- Build files have been written to: C:/Users/shell/source/CXX/u8g2_sdl2/build
编译和报错处理
在cmake 扩展的侧边栏里点击build 项目,开始编译。

目前应该是编译不过的,因为U8G2 里有一点小问题。如果报错u8x8_d_sdl_128x64.c
编译失败,提示
shell
u8g2_sdl2\lib\u8g2\sys\sdl\common\u8x8_d_sdl_128x64.c:103:5: error: implicit declaration of function 'printf' [-Wimplicit-function-declaration]
这个错误是因为代码里调用了printf
,但是并没有包含头文件stdio.h
,也没有给出显式的声明。这种代码在C99 标准以前好像是可以编译的,因为在C99 以后就不允许这样写了。经典的老人写的老版本老代码的问题。解决方法是手动修改这个文件,加上头文件,文件路径在报错提示里有。当然也可以在编译时指定用C99 以前的标准,不推荐。

上面图里右边是修改后的结果。然后再点编译,可能会遇到另一个报错,提示找不到头文件U8g2lib.h
。这是因为U8G2 把C 语言的头文件和C++ 的分开到两个文件夹里,但是在CMakeLists.txt
里没把C++ 的文件夹加入头文件路径。解决方法是去编辑u8g2 根目录 下的CMakeLists.txt
文件。注意,是u8g2 的,不是这个项目下的文件。然后做如下修改:

这样改了以后应该就能编译成功了,之后点击运行。

默认仿真的对象是128x64 尺寸的OLED 屏幕,画面背景里每一个小方块代表8x8 像素。main.cpp
有U8G2 官方提供的图形测试示例代码,运行的效果大概就是下面这样,用过U8G2 的人肯定很熟悉了。

现在疑似还有个BUG,就是示例代码显示ASCII 字符表的时候,最后一行字母显示不全,超出范围了,不确定是哪里有问题。如果真的是个BUG,那就是u8x8_d_sdl_128x64.c
这个文件里有什么逻辑问题,需要先去研究一下SDL2 的用法,再看看要怎么修。
源文件
下面大致说一下src
里面文件的内容。
Arduino.h
cpp
#pragma once
/**
* @brief 提供兼容Arduino 的常用接口
*
*/
#include <stdint.h>
#include <SDL_timer.h>
void delay(uint32_t ms) {
SDL_Delay(ms);
}
U8g2 的示例代码里用了delay
函数,所以有必要提供与之兼容的接口。不能直接把delay
删掉,否则界面绘图的代码跑的太快,显示的东西一闪而过,啥都看不见。
调用SDL_Delay
而不是别的Sleep
之类的延迟,因为这是个基于SDL2 的程序。但这样也有个问题,因为经典Arduino 程序里,delay 期间其实还在不停调用yield
函数,所以可以把某些在delay 期间还要不停轮询的代码放在yield
里。用SDL_Delay
实现延迟的话,就不能去调用yield
了。以后再说吧,要是有必要模拟yield
逻辑,可以考虑开一个后台线程用来执行yield
。只不过这样又会遇到多线程同步的问题,不方便用全局变量了。
entry.c
cpp
#include "SDL.h"
extern void setup();
extern void loop();
extern "C" {
int SDL_main(int argc, char **argv) {
setup();
SDL_Event event;
while (1) {
SDL_PollEvent(&event);
if (event.type == SDL_QUIT) {
return 0;
}
// TODO: key
loop();
}
}
}
为了兼容Arduino 代码的结构,需要在main
函数里调用setup
和loop
,Arduino 官方的代码也是这么写的。只不过因为用了SDL2,需要按照SDL2 的要求改写main
函数,把名字改成SDL_main
,不然编译会报错找不到符号。大概是SDL2 内部需要做一些处理才能显示出图形界面。
在SDL_main
函数内部,除了要调用setup
和loop
,还要处理SDL2 的消息循环。写过win32 图形界面或者Qt 的人应该知道这是什么意思。我在每次loop
迭代的间隙调用SDL_PollEvent(&event)
处理消息,所以loop
里面的代码不能卡住太长时间,否则窗口会白屏,弹出个窗口无响应。
此外,需要处理SDL_QUIT
事件,在这个事件发生后退出SDL_main
函数,或者调用exit
终止程序,否则就没办法点击X 按钮关闭窗口。之后可以加上处理键盘事件的代码,比如把按键映射成单片机引脚,然后可以调用digitalRead
读取电平,要是按键A 按下了,digitalRead('A')
就返回低电平。这样就可以模拟一些界面交互逻辑。
main.cpp
cpp
#include <U8g2lib.h>
#include "Arduino.h"
class U8G2_SDL_128X64_F : public U8G2 {
public:
U8G2_SDL_128X64_F(const u8g2_cb_t *rotation) : U8G2() {
// 全缓冲模式
u8g2_SetupBuffer_SDL_128x64(&this->u8g2, rotation);
}
};
U8G2_SDL_128X64_F u8g2{U8G2_R0};
void u8g2_prepare(void) {
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.setFontRefHeightExtendedText();
u8g2.setDrawColor(1);
u8g2.setFontPosTop();
u8g2.setFontDirection(0);
}
void u8g2_box_frame(uint8_t a) {
u8g2.drawStr(0, 0, "drawBox");
u8g2.drawBox(5, 10, 20, 10);
u8g2.drawBox(10 + a, 15, 30, 7);
u8g2.drawStr(0, 30, "drawFrame");
u8g2.drawFrame(5, 10 + 30, 20, 10);
u8g2.drawFrame(10 + a, 15 + 30, 30, 7);
}
void u8g2_disc_circle(uint8_t a) {
u8g2.drawStr(0, 0, "drawDisc");
u8g2.drawDisc(10, 18, 9);
u8g2.drawDisc(24 + a, 16, 7);
u8g2.drawStr(0, 30, "drawCircle");
u8g2.drawCircle(10, 18 + 30, 9);
u8g2.drawCircle(24 + a, 16 + 30, 7);
}
void u8g2_r_frame(uint8_t a) {
u8g2.drawStr(0, 0, "drawRFrame/Box");
u8g2.drawRFrame(5, 10, 40, 30, a + 1);
u8g2.drawRBox(50, 10, 25, 40, a + 1);
}
void u8g2_string(uint8_t a) {
u8g2.setFontDirection(0);
u8g2.drawStr(30 + a, 31, " 0");
u8g2.setFontDirection(1);
u8g2.drawStr(30, 31 + a, " 90");
u8g2.setFontDirection(2);
u8g2.drawStr(30 - a, 31, " 180");
u8g2.setFontDirection(3);
u8g2.drawStr(30, 31 - a, " 270");
}
void u8g2_line(uint8_t a) {
u8g2.drawStr(0, 0, "drawLine");
u8g2.drawLine(7 + a, 10, 40, 55);
u8g2.drawLine(7 + a * 2, 10, 60, 55);
u8g2.drawLine(7 + a * 3, 10, 80, 55);
u8g2.drawLine(7 + a * 4, 10, 100, 55);
}
void u8g2_triangle(uint8_t a) {
uint16_t offset = a;
u8g2.drawStr(0, 0, "drawTriangle");
u8g2.drawTriangle(14, 7, 45, 30, 10, 40);
u8g2.drawTriangle(14 + offset, 7 - offset, 45 + offset, 30 - offset, 57 + offset, 10 - offset);
u8g2.drawTriangle(57 + offset * 2, 10, 45 + offset * 2, 30, 86 + offset * 2, 53);
u8g2.drawTriangle(10 + offset, 40 + offset, 45 + offset, 30 + offset, 86 + offset, 53 + offset);
}
void u8g2_ascii_1() {
char s[2] = " ";
uint8_t x, y;
u8g2.drawStr(0, 0, "ASCII page 1");
for (y = 0; y < 6; y++) {
for (x = 0; x < 16; x++) {
s[0] = y * 16 + x + 32;
u8g2.drawStr(x * 7, y * 10 + 10, s);
}
}
}
void u8g2_ascii_2() {
char s[2] = " ";
uint8_t x, y;
u8g2.drawStr(0, 0, "ASCII page 2");
for (y = 0; y < 6; y++) {
for (x = 0; x < 16; x++) {
s[0] = y * 16 + x + 160;
u8g2.drawStr(x * 7, y * 10 + 10, s);
}
}
}
void u8g2_extra_page(uint8_t a) {
u8g2.drawStr(0, 0, "Unicode");
u8g2.setFont(u8g2_font_unifont_t_symbols);
u8g2.setFontPosTop();
u8g2.drawUTF8(0, 24, "☀ ☁");
switch (a) {
case 0:
case 1:
case 2:
case 3:
u8g2.drawUTF8(a * 3, 36, "☂");
break;
case 4:
case 5:
case 6:
case 7:
u8g2.drawUTF8(a * 3, 36, "☔");
break;
}
}
#define cross_width 24
#define cross_height 24
static const unsigned char cross_bits[] U8X8_PROGMEM = {
0x00, 0x18, 0x00, 0x00, 0x24, 0x00, 0x00, 0x24, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00,
0x00, 0x81, 0x00, 0x00, 0x81, 0x00, 0xC0, 0x00, 0x03, 0x38, 0x3C, 0x1C, 0x06, 0x42, 0x60, 0x01, 0x42, 0x80,
0x01, 0x42, 0x80, 0x06, 0x42, 0x60, 0x38, 0x3C, 0x1C, 0xC0, 0x00, 0x03, 0x00, 0x81, 0x00, 0x00, 0x81, 0x00,
0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x42, 0x00, 0x00, 0x24, 0x00, 0x00, 0x24, 0x00, 0x00, 0x18, 0x00,
};
#define cross_fill_width 24
#define cross_fill_height 24
static const unsigned char cross_fill_bits[] U8X8_PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x18, 0x64, 0x00, 0x26, 0x84, 0x00, 0x21, 0x08, 0x81, 0x10,
0x08, 0x42, 0x10, 0x10, 0x3C, 0x08, 0x20, 0x00, 0x04, 0x40, 0x00, 0x02, 0x80, 0x00, 0x01, 0x80, 0x18, 0x01,
0x80, 0x18, 0x01, 0x80, 0x00, 0x01, 0x40, 0x00, 0x02, 0x20, 0x00, 0x04, 0x10, 0x3C, 0x08, 0x08, 0x42, 0x10,
0x08, 0x81, 0x10, 0x84, 0x00, 0x21, 0x64, 0x00, 0x26, 0x18, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
#define cross_block_width 14
#define cross_block_height 14
static const unsigned char cross_block_bits[] U8X8_PROGMEM = {
0xFF, 0x3F, 0x01, 0x20, 0x01, 0x20, 0x01, 0x20, 0x01, 0x20, 0x01, 0x20, 0xC1, 0x20,
0xC1, 0x20, 0x01, 0x20, 0x01, 0x20, 0x01, 0x20, 0x01, 0x20, 0x01, 0x20, 0xFF, 0x3F,
};
void u8g2_bitmap_overlay(uint8_t a) {
uint8_t frame_size = 28;
u8g2.drawStr(0, 0, "Bitmap overlay");
u8g2.drawStr(0, frame_size + 12, "Solid / transparent");
u8g2.setBitmapMode(false /* solid */);
u8g2.drawFrame(0, 10, frame_size, frame_size);
u8g2.drawXBMP(2, 12, cross_width, cross_height, cross_bits);
if (a & 4) u8g2.drawXBMP(7, 17, cross_block_width, cross_block_height, cross_block_bits);
u8g2.setBitmapMode(true /* transparent*/);
u8g2.drawFrame(frame_size + 5, 10, frame_size, frame_size);
u8g2.drawXBMP(frame_size + 7, 12, cross_width, cross_height, cross_bits);
if (a & 4) u8g2.drawXBMP(frame_size + 12, 17, cross_block_width, cross_block_height, cross_block_bits);
}
void u8g2_bitmap_modes(uint8_t transparent) {
const uint8_t frame_size = 24;
u8g2.drawBox(0, frame_size * 0.5, frame_size * 5, frame_size);
u8g2.drawStr(frame_size * 0.5, 50, "Black");
u8g2.drawStr(frame_size * 2, 50, "White");
u8g2.drawStr(frame_size * 3.5, 50, "XOR");
if (!transparent) {
u8g2.setBitmapMode(false /* solid */);
u8g2.drawStr(0, 0, "Solid bitmap");
}
else {
u8g2.setBitmapMode(true /* transparent*/);
u8g2.drawStr(0, 0, "Transparent bitmap");
}
u8g2.setDrawColor(0); // Black
u8g2.drawXBMP(frame_size * 0.5, 24, cross_width, cross_height, cross_bits);
u8g2.setDrawColor(1); // White
u8g2.drawXBMP(frame_size * 2, 24, cross_width, cross_height, cross_bits);
u8g2.setDrawColor(2); // XOR
u8g2.drawXBMP(frame_size * 3.5, 24, cross_width, cross_height, cross_bits);
}
uint8_t draw_state = 0;
void draw(void) {
u8g2_prepare();
switch (draw_state >> 3) {
case 0:
u8g2_box_frame(draw_state & 7);
break;
case 1:
u8g2_disc_circle(draw_state & 7);
break;
case 2:
u8g2_r_frame(draw_state & 7);
break;
case 3:
u8g2_string(draw_state & 7);
break;
case 4:
u8g2_line(draw_state & 7);
break;
case 5:
u8g2_triangle(draw_state & 7);
break;
case 6:
u8g2_ascii_1();
break;
case 7:
u8g2_ascii_2();
break;
case 8:
u8g2_extra_page(draw_state & 7);
break;
case 9:
u8g2_bitmap_modes(0);
break;
case 10:
u8g2_bitmap_modes(1);
break;
case 11:
u8g2_bitmap_overlay(draw_state & 7);
break;
}
}
void setup() { u8g2.begin(); }
void loop() {
// picture loop
u8g2.clearBuffer();
draw();
u8g2.sendBuffer();
// increase the state
draw_state++;
if (draw_state >= 12 * 8) {
draw_state = 0;
}
// delay between each page
delay(100);
}
这里面主要就是U8g2 的GraphicsTest 示例代码,显示效果参考之前运行的截图。值得说的只有开头这部分:
cpp
class U8G2_SDL_128X64_F : public U8G2 {
public:
U8G2_SDL_128X64_F(const u8g2_cb_t *rotation) : U8G2() {
// 全缓冲模式
u8g2_SetupBuffer_SDL_128x64(&this->u8g2, rotation);
}
};
U8G2_SDL_128X64_F u8g2{U8G2_R0};
U8G2 没有内置SDL 显示环境相关的类,默认是只能使用C 语言API,但是我的代码都是用C++ API 的,用C 我咳嗽,所以需要自己添加这么个C++ 类。这个是全缓冲full buffer 模式的,所以加了_F
后缀,需要page buffer 模式的话可以自己实现。
总结
想用这个方法辅助调试代码的话,最好是把显示和绘图相关的代码都封装成独立的函数,完全不与程序中其他部分耦合,这样就能把这些函数拿出来单独调试。比如类似下面这样:
cpp
void clear_box(u8g2_int_t x, u8g2_int_t y, u8g2_int_t w, u8g2_int_t h) {
u8g2.setDrawColor(0); // 反色
u8g2.drawBox(x, y, w, h);
u8g2.setDrawColor(1);
}
void show_logo() {
u8g2.clearBuffer();
// u8g2.drawBox(12, 16, 104, 40);
// clear_box(17, 17, 98, 35);
u8g2.setFont(CHS_FONT);
u8g2.drawUTF8X2(35, 25, "刻");
u8g2.drawUTF8X2(17, 55, "BITTER");
u8g2.sendBuffer();
}
void begin_msg() {
u8g2.setCursor(20, 63);
clear_box(20, 57, 45, 8);
u8g2.setFont(SMALL_TEXT_FONT); // 设置小字体
}
void end_msg() {
u8g2.setDrawColor(1); // 恢复正常颜色
}
这样子调试U8G2 的图形界面大概稍微能比实机调试快一丢丢,改坐标,调大小什么的。还是要等编译,不用等烧录,像ESP8266 那种串口小水管烧录贼慢的,用这个正合适。优点理论上是完全支持U8G2 的所有显示功能,不会像模拟器一样可能遇到没实现的差异,而且后续比较容易扩展支持128x64 以外的屏幕尺寸。缺点是能实现的仅限U8G2 和一些硬件无关的东西,其他和单片机硬件相关的东西都模拟不了,所以可能还得为了模拟而修改代码。想更快、更方便的话,可能还是得找那些能模拟Arduino 硬件的模拟器,比如wowki,但是上模拟器还是可能要修改代码,好处大概只有不用等编译。