Featured image of post Zig 驱动 STM32:基于 libopencm3 与 FreeRTOS 的裸机开发体验

Zig 驱动 STM32:基于 libopencm3 与 FreeRTOS 的裸机开发体验

在 STM32 上使用 Zig 编写代码,基于 libopencm3 与 FreeRTOS 来闪烁 LED

为了制作一些玩具,我开始尝试使用 Zig 在 STM32 上编程。我选择了常见且便宜的 STM32F103C8T6,买好了开发板和 ST-Link/V2 调试器。本文将分享在 STM32 上使用 Zig 编写代码,基于 libopencm3 与 FreeRTOS 来闪烁 LED 的过程。制作这个项目也是我在 Zig 和 “嵌入式领域” 的一次探索,这也是我第一次使用 STM32。

一些链接:

为什么选择 Zig?

嵌入式语言的选择并不多,除了 C/C++ 之外,还有 Rust, TinyGo 等等。我接触 Zig 已经大半年了,我很喜欢 Zig,虽然对比我最熟悉的 Go 语言来说,手动管理内存确实繁琐,但可以非常简单地调用 C 库,这在某些领域的开发上为我带来了极大的便利。Zig 还有非常灵活的构建系统,构建过程都可以塞进 build.zig 文件中,非常方便。

在开始之前,我假设你对 Zig 有一定了解,在本文顶部我放置了本文提到的项目的链接,你可以从那里找到项目的源码。

感谢 rbino 的文章为我指点迷津,你可以在顶部找到他的博客链接。

基本配置

创建一个基本的 Zig 项目,创建一个匹配目标芯片的 elf 编辑 build.zig 文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const target = b.resolveTargetQuery(.{
    .abi = .eabi,
    .cpu_arch = .thumb,
    .os_tag = .freestanding,
    .cpu_model = .{ .explicit = &std.Target.arm.cpu.cortex_m3 },
});

const elf = b.addExecutable(.{
    .name = name,
    .root_module = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = .ReleaseSafe,
        .strip = !debug,
    }),
});
elf.link_data_sections = true;
elf.link_function_sections = true;
elf.link_gc_sections = true;

参考 rbion 的博客,这是我使用的芯片的配置,如果你使用其他芯片,请按照你的芯片的实际配置来修改。

导入 libopencm3

如果你也跟我一样没有接触过 libopencm3,可以看看他的文档,克隆他的仓库,在仓库中执行 make 命令就能生成我们需要的库文件。现在我们使用 Zig 完成这一切。

编辑 build.zig.zon

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.{
    ...
    .dependencies = .{
        .libopencm3 = .{
            .url = "git+https://github.com/libopencm3/libopencm3#5e7dc5d092e52bbfbb8b5929e2097732e1b7f81c",
            .hash = "N-V-__8AAPMXXgAQa5KZISzTTuLJyAhrAPA88Gm8xyInun4J",
        },
        ...
    },
    ...
}

现在 libopencm3 在 build.zig 中可用了,我们可以开始编写代码了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
pub fn getStaticLibrary(b: *std.Build, comptime target: []const u8) std.Build.LazyPath {
    const libopencm3 = b.dependency("libopencm3", .{});

    const slash_pos = std.mem.indexOf(u8, target, "/").?;
    const part1 = target[0..slash_pos];
    const part2 = target[slash_pos + 1 ..];
    const lib_name = b.fmt("libopencm3_{s}{s}.a", .{ part1, part2 });
    const lib_path = libopencm3.path(b.pathJoin(&.{ "lib", lib_name })).getPath(b);

    {
        const f = std.fs.openFileAbsolute(lib_path, .{}) catch null;
        if (f) |file| {
            file.close();
            return .{ .cwd_relative = lib_path };
        }
    }

    const make_lib_cmd = b.addSystemCommand(&.{ "make", "-C" });
    make_lib_cmd.addDirectoryArg(libopencm3.path("."));
    make_lib_cmd.addArg(b.fmt("TARGETS={s}", .{target}));

    const output_file = b.allocator.create(std.Build.GeneratedFile) catch @panic("OOM");
    output_file.* = .{
        .step = &make_lib_cmd.step,
        .path = lib_path,
    };
    return .{ .generated = .{ .file = output_file } };
}

我编写了这个函数,他可以返回构建出来的 .a 文件的路径。现在我们将其链接到 elf 中并添加其他导入路径

1
2
3
4
const libopencm3 = b.dependency("libopencm3", .{});
elf.addObjectFile(getStaticLibrary(b, "stm32/f1"));
elf.addLibraryPath(libopencm3.path("lib"));
elf.addIncludePath(libopencm3.path("include"));

我们还需要导入 libopencm3 的链接器脚本,他在 lib/cortex-m-generic.ld 中,根据其中的注释,我们还需要声明 romram 的内存区域。我写了下面的函数生成链接脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
pub const MemoryRegion = struct {
    origin: u32,
    length: u32,
};

pub fn getLinkScript(b: *std.Build, rom: MemoryRegion, ram: MemoryRegion) std.Build.LazyPath {
    const libopencm3 = b.dependency("libopencm3", .{});
    const generic_ld_path = libopencm3.path("lib/cortex-m-generic.ld").getPath(b);
    var file = std.fs.openFileAbsolute(generic_ld_path, .{}) catch unreachable;

    var result: std.Io.Writer.Allocating = .init(b.allocator);
    result.writer.writeAll(b.fmt(
        \\MEMORY
        \\{{
        \\    rom (rx) : ORIGIN = 0x{x}, LENGTH = {d}
        \\    ram (rwx)  : ORIGIN = 0x{x}, LENGTH = {d}
        \\}}
    , .{
        rom.origin,
        rom.length,
        ram.origin,
        ram.length,
    })) catch unreachable;

    result.writer.writeAll(file.readToEndAlloc(b.allocator, std.math.maxInt(usize)) catch unreachable) catch unreachable;

    const wf = b.addWriteFiles();
    return wf.add("linker.ld", result.toOwnedSlice() catch unreachable);
}

然后让 elf 使用这个脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
elf.setLinkerScript(getLinkScript(
    b,
    .{
        .origin = 0x08000000,
        .length = 64 * 1024,
    },
    .{
        .origin = 0x20000000,
        .length = 20 * 1024,
    },
));

现在你可以该可以在 src/main.zig 中导入和使用 libopencm3 的函数了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const hal = @cImport({
    @cDefine("STM32F1", "1");
    @cInclude("libopencm3/stm32/rcc.h");
    @cInclude("libopencm3/stm32/gpio.h");
});

// 重写启动入口
export fn _start() callconv(.c) void {
    main();
    unreachable;
}

// 主程序
export fn main() callconv(.c) void {
    // 设置系统时钟为 72MHz
    hal.rcc_clock_setup_in_hse_8mhz_out_72mhz();
    // 打开 GPIOC 时钟
    hal.rcc_periph_clock_enable(hal.RCC_GPIOC);

    // 设置 PC13 推挽输出
    hal.gpio_set_mode(
        hal.GPIOC,
        hal.GPIO_MODE_OUTPUT_2_MHZ,
        hal.GPIO_CNF_OUTPUT_PUSHPULL,
        hal.GPIO13,
    );
    // 熄灭 LED
    hal.gpio_set(hal.GPIOC, hal.GPIO13);
    while (true) {};
}

构建和刷入

还需要将 elf 转为能够直接刷写进 STM32 的 bin 文件,使用以下代码:

1
2
3
const bin = elf.addObjCopy(.{ .format = .bin });
const bin_output = b.addInstallBinFile(bin.getOutput(), "bin");
b.getInstallStep().dependOn(&bin_output.step);

执行 zig build 编译项目,应该能在 zig-cache/bin 目录下找到 bin 文件。可以使用 st-flash 工具将其刷入 STM32。

导入 FreeRTOS

类似的,先让 Zig 获取 FreeRTOS 的源码,在 build.zig.zon 中添加依赖:

1
2
3
4
.freertos = .{
    .url = "https://github.com/FreeRTOS/FreeRTOS-Kernel/releases/download/V11.2.0/FreeRTOS-KernelV11.2.0.zip",
    .hash = "N-V-__8AABBQEQEqOzP-h5Nz5cgduMAUVp4TMn1BadkEWiam",
}

首先,需要准备 FreeRTOSConfig.h, 可以从 这里 获取模板,然后根据实际情况进行修改。

特别的,将 configCHECK_HANDLER_INSTALLATION 设为 0,否则 FreeRTOS 会无法正常运行。

1
#define configCHECK_HANDLER_INSTALLATION 0

最后在 FreeRTOSConfig.h 的底部 #endif 之前加上:

1
2
3
4
#define vPortSVCHandler sv_call_handler
#define xPortPendSVHandler pend_sv_handler
#define xPortSysTickHandler sys_tick_handler
#endif /* FREERTOS_CONFIG_H */

FreeRTOSConfig.h 放到你喜欢的目录下,并在 build.zig 中添加:

1
elf.addIncludePath(b.path("<dir to FreeRTOSConfig.h>"));

FreeRTOS 还需要导入 Libc 的头文件,这里我使用 gcc-arm-embedded 包中提供的 arm-none-eabi 的头文件,因为正好 libopencm3 也依赖 gcc-arm-embedded。在 build.zig 中添加:

1
elf.addIncludePath(.{ .cwd_relative = "<path to arm-none-eabi/include" });

还差最后一步就万事大吉了,导入 FreeRTOS 的头文件和 C 文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const freertos = b.dependency("freertos", .{});

elf.addIncludePath(freertos.path("portable/GCC/ARM_CM3"));
elf.addIncludePath(freertos.path("include"));
elf.addCSourceFile(.{ .file = freertos.path("portable/MemMang/heap_4.c") });
elf.addCSourceFile(.{ .file = freertos.path("portable/GCC/ARM_CM3/port.c") });
elf.addCSourceFile(.{ .file = freertos.path("tasks.c") });
elf.addCSourceFile(.{ .file = freertos.path("list.c") });
elf.addCSourceFile(.{ .file = freertos.path("queue.c") });
elf.addCSourceFile(.{ .file = freertos.path("timers.c") });
elf.addCSourceFile(.{ .file = freertos.path("event_groups.c") });
elf.addCSourceFile(.{ .file = freertos.path("stream_buffer.c") });
elf.addCSourceFile(.{ .file = freertos.path("croutine.c") });

portable/GCC/ARM_CM3portable/GCC/ARM_CM3/port.c 需要根据实际的芯片架构进行修改。

最后,在 main.zig 中使用 FreeRTOS 的函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const os = @cImport({
    @cInclude("FreeRTOS.h");
    @cInclude("task.h");
});

_ = os.xTaskCreate(
    led_task,
    "LED",
    128,
    null,
    1,
    null,
);

os.vTaskStartScheduler();

一切都完成了,开始享受吧!