MicroZig: Unified abstraction layer and HAL for Zig on several microcontrollers
github.com
github.com
However, the project is still not very mature. They just refactored into the repo you see since I started using it. Support for RP2040 is good, but the HAL for other microcontrollers seems less supported. (And it seems to be written in such a way that code isn’t shared between HALs for different microcontrollers.) It’s targeting stable Zig (instead of HEAD), which is an unusual choice for Zig projects, since Zig is also in an immature state and updates rapidly. There’s zero documentation except for the source, but that’s par for Zig projects.
Zig has the potential to be huge for embedded, once these APIs are fleshed out and stabilize. It’s already more enjoyable than writing C.
Additionally, Andrew himself has stated he wants 0.12 to be a version where zig devs should feel comfortable pinning to stable. I know stable now is 0.11, but the changes are not egregious at the moment. Not sure if that’s still the deal.
I've done a fair bit of stuff with embedded Rust and the main reason for this is manufacturer documentation. The RP2040 is really in its own tier of documentation quality, nothing comes close. Other chipsets are so bad that the Rust SVD codegen has a way for maintainers to fix errata.
I've concluded that I'm only going to use RP2040s going forward.
On the positive side:
- As a 'safer c', getting things up and running was a breeze, writing code largely felt intuitive.
- The additions to C (slices/iterators, enhanced structs, arbitrarily sized integers) are excellent
- It produces fairly small firmware images (useful when stuffing a boot rom in logic/EBRAM)
- Easier (than C IMO) to get up and running with formatted IO vs retargeting libc
- Comptime is neat, and you can build some decent low-cost abstractions with it (ex: I built a comptime heavy write-through cache for key-value storage that required very little overhead and largely self-generated based on a simple struct)
- I really enjoy the use of structs for function+data organization. It maps well to hardware instances, giving you an 'object' like feeling without OOP ick.
On the negative side:
- The compiler is still a seriously moving target. Upgrading sometimes meant rather large refactors.
- Documentation is somewhat poor IMO.
- As a long time user of Nim (including on really lean embedded targets), compared to hygienic macros, comptime falls way short.
- The lack of first class interfaces/traits/typeclasses is not my favorite. The currently suggested alternatives are so un-ergonomic I'd almost call them hostile.
All-in-all, I'm excited to see where Zig ends up. After nearly 20 years writing embedded code I'm really (really really really) tired of C. The embedded systems community really needs to embrace better tools.
Hear hear, brother!
I get particularly frustrated about the second point, and I've made it my career goal to get my teams to update their tools and processes. For example, when I joined my current team, they didn't compile debug symbols and didn't know how to use a debugger on our system! Hell, in 2024 I still have colleagues who prefer to use a .dis and .map file than leverage the debug symbols... "What is this "mixed code and disassembly" display you speak of"
*(int*)(0xf003) |= 39;We've never been that bad, but we are still using macros for registers instead of block structure pointers, which we have access to...
(although I'll grant that structures can be risky due to undefined packing rules)
write_register(Reg::Config as u8, value)
Where `value` may be constructed from variables or, wait for it... Might be a binary literal because it's easier to compare to the datasheet if it's a one-off vice a general API. If it's a general API, it is probably handled with a config struct etc, where each field is a u8-repr enum.Code like this should IMO always have a reference to the relevant DS table in comments, and probably an explanation of why you're setting the bits that way.
The use of anytype as a sort of universal interface is my least favourite part of Zig. I’ve seen enough griping about it that I’m hopeful something happens here.
In what way? Comptime should be generally capable of anything macros are.
>- The lack of first class interfaces/traits/typeclasses is not my favorite. The currently suggested alternatives are so un-ergonomic I'd almost call them hostile.
Terribly difficult to implement without breaking a major language tenant of “no hidden control flow”.
But I found that once I left behind OO style of thinking, I haven’t missed this all that much. For the rare time I do generalize like this, you can literally just check at comptime that the passed in type provides the necessary decls. It’s not terribly complex or hostile (although tooling could use some work around it)
I started to reply to this with 'Comptime is generally capable of doing anything that Nim's templates can accomplish (but not it's macros)', but I stopped myself because even though Zig's comptime is more akin to Nim's templates than its macros (in my opinion), Nim templates are more powerful as they allow you to embed arbitrary blocks of code to implement constructs similar to python's context managers (which I'm fairly certain you can't do with comptime).
W/R/T Macros vs Comptime, you can't create arbitrarily complex DSLs with comptime the way you can with a Nim's macros as you don't have full control over AST generation.
All that said, the power you'd get from a Macro or Template system like Nim's don't really jive with Zig's whole "no hidden control flow" thing.
>Terribly difficult to implement without breaking a major language tenant of “no hidden control flow”.
I dunno if I agree with that, even very simple rust-like traits that simply enforce that a struct implemented a given interface at compile time (static dispatch only) would go a long way without compromising obvious control flow IMO.
>But I found that once I left behind OO style of thinking, I haven’t missed this all that much. For the rare time I do generalize like this, you can literally just check at comptime that the passed in type provides the necessary decls. It’s not terribly complex or hostile (although tooling could use some work around it)
Respectfully, I don't view it as OO thinking (I think typeclasses come from SML...). Making polymorphism reasonably ergonomic goes a long way towards code reuse and (again, only my humble opinion here) would help with what some of the folks in this comment section are talking about w/r/t code re-use and generalizing a HAL layer in a consistent way without forcing users (or library authors) to write a bunch of ad-hoc code to check that functions exist on a given struct, or manually implementing dispatch tables.
By open world, I'm thinking of abstractions like closures and interfaces which permit arbitrary extension and require dynamic dispatch. Given any fixed set of such abstractions, you can simulate in a closed world system via something like defunctionalization during whole program compilation, which is what you do on embedded systems. You can probably do a defunctionalization transformation with comptime, and that gets you better visibility on the state of the system in a way that's not possible with a truly open world system.
Only if you do it wrong.. It's quite simple to mandate a special character which indicates that "something's happening here", for example you could have y = a #+ b with + being a function operating on matrixes, the # indicating that this is a function called not the regular operator.
Could you expound on how you used Nim on "really lean embedded targets"?
Now that arc/orc are the default memory management strategy, it’s even possible to keep some of the niceties from the standard library when doing so — but that will depend on your target of course.
Even going the “no heap allocation” route is totally feasible, you sort of end up using Nim as a nicer C syntax with extra features. All of the libraries we write for work have two interfaces, one that returns (possible heap allocated) results, and one that takes a buffer pointer (as a var openArray[T] param) in instead.
Can you share what your experience has been like with Nim on embedded targets? Both Nim and Zig are on my wishlist to try out for embedded but I'm doing C and RTOSes for the next few projects.
Ada with Ravenscar seems like another "seems like it solves a lot of common problems intelligently" but I haven't had much time to try it being a simple proof of concept.
I’m currently in the process of writing a nice HAL/dev framework agnostic FreeRTOS binding in Nim, which maybe you’ll find useful once we can post it?
Never used nim and been a while since I did any embedded.
This is an active problem in embedded. Cheap microcontrollers can have multiple cores that are not even the same architecture.
This means that just to get "blinky" running, you need to choose a (language, OS) tuple. And, given that "language" is generally "C", that means that your abstraction choices for OS are lousy.
Side question: last I checked, FreeRTOS didn't do a great job when multiple microcontrollers were involved--especially if the communication channels or synchronization were hardware-based. Has this changed?
The biggest negative I ever hit was that early on, Nim didn't have support for `volatile`, which meant it was a non-starter for doing anything with MMIO (I ended up being the one who added volatileLoad/volatileStore to Nim's stdlib so I could use it on a Cortex-M without having to drop into C so much).
For the most part though, if you're reasonably comfortable with embedded toolchains (i.e. you understand how to write linker scripts, understand what happens between a reset and actually getting into `main()`, etc), it's not much of a hurdle to set up a simple build system to compile your Nim code to C, link appropriately, and then sort of forget about it.
It's been a while, but IIRC I also got step-through debugging working with OpenOCD by having the nim compiler generate `#line` pragmas and including debug symbols, which was pretty neat.
This was all pre ARC/ORC, so I did have to make sure to be careful not to use ref objects, but ultimately it felt pretty seamless. I still tend towards fully manually managed memory on embedded projects, but I'd be curious to give it a go.
Personally, I don't see this as a problem. I like implementing an interface in Zig the same way I implement one in C -- I use a function pointer that takes a packet that defines the operation to execute with that packet data. Zig's exhaustive switch statements make these even better to work with.
Here is some pseudocode to illustrate what I mean:
fn doThing(ThingPacket p) switch(p.Opcode) MyInterfaceFunc1_Opcode => return MyInterfaceFunc1(p.Func1Params); MyInterfaceFunc2_Opcode => return MyInterfaceFunc2(p.Func2Param); ... default => error.InvalidOpcode;
The new direction that library and related is Async with Embassy, which... is also not something I want to use.
I then spent 2 days brushing up on C and got things up and running with ESP-IDF painlessly. I've been iterating really fast on C just fine, and the few times I wish I had Rust's features have been eclipsed by the advanced ESP-IDF api's I've needed to use that didn't have support in the Rust HALs.
I have been following https://github.com/ziglang/zig/issues/5467 for a while and progress seemed to have slowed significantly
I'm currently working on a fork of the zig toolchain with espressif-LLVM to support Xtensa.[1] It is now possible to test with esp-idf instead of microzig for baremetal (Blink) yet.
[1] https://gist.github.com/kassane/7bdb782a1984d0c6581ae7b44e1f...
https://github.com/ominitay/zig/tree/xtensa
Edit: I see you are the same kassane from the GitHub thread. Thank you for your efforts on Xtensa support! Would be great to have it out-of-the-box at some point.
[1]: https://github.com/kassane/zig-espressif-bootstrap/releases
Micropython and friends are well-suited for education/hobby use, protoptyping, and maybe some commercial applications that aren't sensitive to cost or realtime characteristics. But since they don't really compete with C/C++, they also don't really compete with Zig.
* i.e. enough time to flush the hivemind caches so the thread won't feel repetitive
Edit: typos, ironically. Mobile phone text editing ~ftw.
(Also, how is this one language-neutral?)
Purely for making it easy to try out without too much setup. e.g. click a button in the boards manager, and everything should just work.
This saved my ass when I was able to switch to a third-party AVR core which allowed changing the clockspeed so I could have working serial on a chip running slower than 16mhz and get a demo out of an otherwise busted board.