用CMake 实现U8g2 的 SDL2 模拟环境
就是在电脑上运行U8G2 代码,模拟出OLED 屏幕上的显示效果。参考了别人写的教程:https://github.com/snqx-lqh/u8g2-windows-sdl-simulate。U8G2 本身带有SDL2 相关的支持,只是关于具体要怎么跑起来,找到的教程都比较土法炼钢,说要到处复制粘贴文件,然后写makefile。毕竟已经是21 世纪了,还是应该从那种恐龙时代的风格上前进一步,所以我决定试试用CMake。
准备项目文件
可以去用我配置好的项目:https://gitee.com/etberzin/u8g2_sdl2,注意里面有submodule,clone 的时候用:
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:
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_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_BUILDCOMMAND ${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,那就自己想别的办法。
scoop install mingw-winlibs cmake
装好这些东西以后,用VScode 打开项目文件夹,cmake 扩展会自动启动,提示让选择编译工具,在这里选择mingw 提供的gcc。
然后会自动生成build
文件夹,输出一些类似下面这样的东西,就表示配置没有问题。
[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
编译失败,提示
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
#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
#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: keyloop();}
}
}
为了兼容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
#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); // Blacku8g2.drawXBMP(frame_size * 0.5, 24, cross_width, cross_height, cross_bits);u8g2.setDrawColor(1); // Whiteu8g2.drawXBMP(frame_size * 2, 24, cross_width, cross_height, cross_bits);u8g2.setDrawColor(2); // XORu8g2.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 loopu8g2.clearBuffer();draw();u8g2.sendBuffer();// increase the statedraw_state++;if (draw_state >= 12 * 8) {draw_state = 0;}// delay between each pagedelay(100);
}
这里面主要就是U8g2 的GraphicsTest 示例代码,显示效果参考之前运行的截图。值得说的只有开头这部分:
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 模式的话可以自己实现。
总结
想用这个方法辅助调试代码的话,最好是把显示和绘图相关的代码都封装成独立的函数,完全不与程序中其他部分耦合,这样就能把这些函数拿出来单独调试。比如类似下面这样:
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,但是上模拟器还是可能要修改代码,好处大概只有不用等编译。