Contributing to Build System for Embedded Targets

Hi all! X-posted from the Zig discord just now. I’m looking to get involved in helping make Zig an awesome experience for embedded (firmware) development. Specifically, deeply embedded MCUs that are freestanding/baremetal running either no OS, or some form of RTOS (more a code library than an “OS” IMO). I’m part of the Zig Embedded Group discord, and they’re doing some really cool work with MicroZig, however I’m more interested in making the “porting” process for existing C/C++ firmware projects as smooth and easy as possible. Ideally I want people to come for the awesome build system, and stay for the new language if that makes any sense :slightly_smiling_face:

Are there any low hanging tasks/to-do’s I could start tackling surrounding the build system and embedded targets (or even the build system in general)? I have a couple of ideas, but figured I’d start attempting to knock out some open issues first. I’m fairly experienced in low level MCU type stuff so also happy to tag team an issue someone is already working on if that’s a better way to get involved.

3 Likes

@biosbob is probably a great person to talk to about this. He’s made a bunch of posts here that you may find very interesting and he’s working on porting some pretty complex machinery for the embedded space through Zig - I imagine he has some insights he can share here.

1 Like

@haydenridd and i actually chatted earlier today – about this and other topics worthy of brainstorming :thinking:

Well, while we have our two embedded folks here, I’d say we need more arduino ported to Zig.

Arduino is the gateway drug to embedded stuff for many people and making small programs (like making an LED blink) for different boards would help a lot of people get going in Zig.

1 Like

And when you say Arduino, like a full “board support” style ecosystem tailored for their dev boards? Where LEDs/other peripherals on the board are abstracted to higher level function calls (board.led1.on();) rather than low level register writes (GPIO_CTRL_REG |= 0b00010000;)?

Agreed that could be valuable for sure.

As a tangent, when talking embedded it’s important to differentiate between hobbyist embedded developers vs. professional firmware engineers when you’re trying to figure out who to go after. Despite doing sort of the same thing, they both can have wildly different development flows. For instance, you won’t really find any firmware engineers ever booting up the Arduino IDE, since they’re writing firmware for custom boards/different processors and need a lot more low level control than the Arduino language provides. IMO both groups are valuable to target, there’s just different strategies to go after either.

Yes, I’m definitely referring to the hobbyist group (or professionals who want a toy project to try Zig for). That’s my bias, really - I tend to think about teaching the subject first when it comes to anything forum related.

I think the important thing is that people can look online and find a project that compiles for their board with sensible installation instructions - “this program makes an LED blink for an arduino uno”. Then they can read the program and see how you setup the board, how to set pin values, etc.

As an addendum, I’m into hexapods and AI systems (that’s how I got into programming, I wanted to create my own version of BMO from adventure time… long time ago :slight_smile: ) and the code is just awful when you dig into the prebuilt examples so I have hope to see that arduino examples become better for Zig users (so take what I suggest with a grain of salt, I’m out for revenge lol).

1 Like

100% agreed, good examples are key. “Official” examples that are maintained/updated with each release are even better. I’ll keep pondering on some higher level strategy and in the meantime take a peek at issues on Github I might be able to take a crack at.

1 Like

To throw another approach to bringing Zig into the embedded space, check out this project which attempts to “zigify” Zephyr.

Zephyr (which is about as old as Zig) has found relatively broad adoption in the embedded space – not only amongst users of Arduino and RaspberryPI, but also in “production firmware” used by major semiconductor companies. Said another way, both @haydenridd and @AndrewCodeDev could be served by the same solution.

At some level, I’d be curious how the zig-zephyr project compares/contrasts with microzig. Though by no means an expert in Zephyr, it does provide a uniform build system which today supports 600+ boards!!

IMHO, the way to get folks to “change” anything (programming language, build flow, silicon vendor, etc) is with a quantitative A-B comparison that somehow can be reduced to cost – whether non-recurring engineering costs and/or recurring product costs.

A bit of a leading question, but what’s the “value proposition” for Zig in the embedded space? What “problem” does it solve, and what “proof” is demanded by perspective users?

1 Like

Super interesting. I would say zig-zephyr falls a bit more on the side of “integrate Zig with existing development flows” and microZig falls more on the side of “build a new development flow in the Zig language”. At the end of the day, Zephyr’s value proposition is they use a bunch of Python scripts (their west tool) among other things to automate the most painful part of firmware: seeking out and installing/configuring your toolchains, RTOS, debug setups, etc. And it’s proven quite successful, I know of a couple firmware engineering firms that start every project with Zephyr if given a choice.

what’s the “value proposition” for Zig in the embedded space?

A really, really important question. If I were to sum it up, the value proposition for me personally is:

  • Zig allows me to incrementally try out a new (superior) language to C/C++ in my existing embedded projects, even using existing vendor support code (HALs, etc.). As a bonus it also gives me a build system that’s easier to use (albeit way less documented) than Makefiles/CMake.

Basically any other shiny new language doesn’t let me have smooth interop with my existing code, let me use existing vendor code, etc. You just have to wholesale rewrite your whole project in “X” new language.

what “proof” is demanded by perspective users?

This one is a lot harder to pin down. For me it would be a couple of things:

  • Zig must be clearly easier to use than C (subjective, but I think this is true), saving developer time
  • Zig’s build system must be easier to use than Makefiles/CMake, again saving developer time
  • Using Zig to get started with an embedded target (and Zig’s build system w/ C), must be clearly documented and have clear examples, making the time to “hello world” easier than the alternatives (definitely not there yet)
1 Like

Hi, I’ve not posted on here before, and only really started playing with Zig this weekend. But thought I might have something useful to add as I think I am exactly your target user!

I’m an embedded software engineer, and been interested in Zig for a while. I have been investigating converting a basic bare metal stm32 project of mine to use build.zig rather than a makefile, to allow incrementally trying out zig in my c project (exactly as you describe!).

I had a fair bit of trouble converting my makefile into build.zig, mostly due to being confused how pass flags to the linker. After scouring forum posts and some example projects (and eventually just reading through the build system source code) I managed to convert/work around most of the linker flags in my makefile in the build.zig script. I still haven’t figured out how I can get the linker to output a map file from build.zig.

I found that looking at example projects and source code was useful, especially this one GitHub - haydenridd/stm32-zig-porting-guide: Compiling an STM32CubeMX with Zig's compiler instead of arm-none-eabi-gcc., which pointed out that if you want to link libc for arm-none-eabi targets you have to point the zig cc compiler to the builtin arm-none-eabi libraries and startup files (essentially the equivalent of providing the -nostartfiles -nostdlib --specs nano.spec -lc -lgcc flags to the linker). For a simple bare metal project, its achievable to investigate the pain points and resolve them. But if I were to have to convert our codebase at work, which is a monstrous make build system with generated makefiles for different targets, all with different linker scripts etc. I think I would have a pretty rough time.

I think if you can create resources so that people can more easily convert embedded build systems (Makefiles, CMake, Linker Scripts etc.) to build.zig, then naturally people will begin to create wrappers for vendor hal’s, RTOS’s etc. which will lead to better experiences for both professionals trying to incrementally introduce zig as a build tool, and hobbyist’s looking for Arduino type experience?

1 Like

Hey @rej696 welcome to Zig! Appreciate you taking the time to take it for a spin for embedded :slight_smile:

I had a fair bit of trouble converting my makefile into build.zig, mostly due to being confused how pass flags to the linker. After scouring forum posts and some example projects (and eventually just reading through the build system source code) I managed to convert/work around most of the linker flags in my makefile in the build.zig script. I still haven’t figured out how I can get the linker to output a map file from build.zig.

Yep, I had a similar experience porting something to the Zig build system for the first time, and fortunately it looks like you found the example porting guide I wrote which is great!

Sadly Zig’s linker doesn’t yet have the ability to produce .map files:

But if I were to have to convert our codebase at work, which is a monstrous make build system with generated makefiles for different targets, all with different linker scripts etc. I think I would have a pretty rough time.

I’m about to embark on this exact task before too long to try to “pipe clean” out all the pain points in Zig’s build system for embedded targets :slight_smile:

I think if you can create resources so that people can more easily convert embedded build systems (Makefiles, CMake, Linker Scripts etc.) to build.zig, then naturally people will begin to create wrappers for vendor hal’s, RTOS’s etc. which will lead to better experiences for both professionals trying to incrementally introduce zig as a build tool, and hobbyist’s looking for Arduino type experience?

Couldn’t agree more, that’s the goal for sure! If I were to summarize my current top pain points with using Zig as a build system for embedded it would be:

  • Lack of clarity in linker/compile arg translation from their raw form to Zig’s build system commands
  • Need to manually link in a precompiled libc from arm-none-eabi-gcc due to no libc implementation being “included” with Zig that works for freestanding ARM targets
3 Likes

aha! I just realised after I posted that was your guide :man_facepalming:
It was very helpful!

at the moment I’ve got my C project compiling with build.zig, but trying to add in a zig file that calls some c functions and exports a function, that gets called in the c main function. I have to add an extra section in my linker script to discard the .init_array and .fini_array sections. Compiling my zig file seems to add these and they make my binary way too large.

After a decent amount of hacking, It seems to work! dissertation/blackpill at main · rej696/dissertation · GitHub

Nice! I’ll take a look more in depth at your repo later this week but just a quick thing I noticed, change your floating point feature to:
std.Target.arm.Feature.vfp4d16sp

From the current value of:
std.Target.arm.Feature.fp_armv8d16sp

Cortex M4 only supports up to v4 of the arm spec, and this matches the settings of the static library you’re linking against. A good article on what these flags actually do generally:

1 Like

Okay messed around with your repo a bit! Couple of notes:

  • No need to generate an intermediate object for the zig code, you can intermingle zig + C sources in the build, can delete:
const zig_obj = b.addObject(.{
    .name = "zig_main.o",
    .target = target,
    .optimize = optimize,
    .link_libc = false,
    .single_threaded = true,
    .pic = true,
    .root_source_file = b.path("src/zig/main.zig"),
});
zig_obj.addIncludePath(b.path("inc"));

And just add:

const exe = b.addExecutable(.{
    .name = exe_name ++ ".elf",
    .target = target,
    .optimize = optimize,
    .link_libc = false,
    .linkage = .static,
    .single_threaded = true,
    .root_source_file = b.path("src/zig/main.zig"),
});
  • It looks like you know what you’re doing with the linker script, but noticed there were a couple of the “usual” things I have in some of my ST linker scripts missing (various ARM debug symbols/etc). Can’t hurt to fire up STM32CubeMX and generate a base project for the stm32f411xx chip and compare their generated linker script to yours and add in (after confirming you actually need it) anything that might be missing.
1 Like