用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 的时候用:

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 风格的setuploop 函数,主要的代码都放在这里;
  • 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 函数里调用setuploop,Arduino 官方的代码也是这么写的。只不过因为用了SDL2,需要按照SDL2 的要求改写main 函数,把名字改成SDL_main,不然编译会报错找不到符号。大概是SDL2 内部需要做一些处理才能显示出图形界面。

SDL_main 函数内部,除了要调用setuploop,还要处理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,但是上模拟器还是可能要修改代码,好处大概只有不用等编译。

相关推荐
三贝勒文子4 小时前
C++ 多线程实战 14|如何系统性避免死锁
c++
Yupureki4 小时前
从零开始的C++学习生活 9:stack_queue的入门使用和模板进阶
c语言·数据结构·c++·学习·visual studio
远远远远子4 小时前
C++-- 内存管理
c++·算法
小年糕是糕手4 小时前
【数据结构】单链表“0”基础知识讲解 + 实战演练
c语言·开发语言·数据结构·c++·学习·算法·链表
Dobby_055 小时前
【Go】C++ 转 Go 第(一)天:环境搭建 Windows + VSCode 远程连接 Linux
linux·运维·c++·vscode·golang
咸鱼爱学习5 小时前
【题解】B2613【深基1.习5】打字速度
数据结构·c++·算法
一匹电信狗5 小时前
【C++】C++风格的类型转换
服务器·开发语言·c++·leetcode·小程序·stl·visual studio
syt_biancheng5 小时前
C++ 多态(1)
jvm·c++·学习
std78796 小时前
用C++ 实现屏幕保护程序
开发语言·c++