Hacker News new | past | comments | ask | show | jobs | submit login
MicroZig: Unified abstraction layer and HAL for Zig on several microcontrollers (github.com/zigembeddedgroup)
249 points by homarp 76 days ago | hide | past | favorite | 60 comments



I started a new project with MicroZig just the other week. It’s working (with the RP2040), and that alone is a huge deal, since writing Zig is much more ergonomic than C with Arduino. It makes cross-compiling for testing in simulation locally trivial as well.

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.


I'm interested in this too, though I've had mixed experiences in the past using third party HALs for microcontrollers only to discover that they have gaps in API coverage or can only expose the most common use-cases. There was always lot of examples and documentation for the "easy" stuff like GPIOs, timers, ADCs, serial port, etc, but once you got into USB, Ethernet, or RTOS integration then things rapidly got much scarier.


I just recently switched off stable to 0.12 and I honestly didn’t run in to too much in the way of changes. It is a bit unusual for zig projects currently BUT zig changes are slowing.

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.


> Support for RP2040 is good,

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.


Arduino is C++, not C! Of course that changes nothing to what you said :) .


I used Zig (not MicroZig, just rolled my own HAL) for the bootloader and firmware on a soft RISC-V SOC + custom peripherals recently and had somewhat mixed, though positive feelings about it.

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.


> 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"


I know people who insist on putting register addresses (in hex) in their code rather than make a variable because "it's easier to debug by comparing to the user guide. If it was a variable, you would have to look up its value every time". It looks somewhat like this:

   *(int*)(0xf003) |= 39;


So, my favorite pattern in Rust for this is to use a u8, (or u16 etc)-repr Enum for register addresses and values. So, you'd do something like, assuming a direct register API vice a wrapper:

  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.


Oh deer god.

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)


Hey Jeff,

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.


Like any time you get off the beaten path, there are rough patches, but to paraphrase another comment in this thread "Nim is just C", so anything that felt a little awkward just meant using an `importc` pragma and doing whatever I needed to do in an environment I felt more comfortable (you can also have C code compiled with a {.compile: "foo.c"} pragma, so if a module required something be done in C, you didn't have to monkey about with the build system to include it, it 'just worked')

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.


I use Nim at work for embedded firmware development right now, and we evaluated Zig but it was a year and a half ago when we started the project, Zig just wasn’t as far along as it is now.

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?


Do neither of these languages have better concurrency tools than porting FreeRTOS?


OS is not the same level of abstraction as a language.

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?


FreeRTOS gives much more than just concurrency!


iirc the os needs to have processor specific task switching code in assembly, Freertos must have tons of ports already. Nim supposedly has nice FFI for C. The niceness of freertos is then mostly about it's.. system call design. If it's good, then you could probably progressively rewrite freertos in nim.

Never used nim and been a while since I did any embedded.


> 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.

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.


>- As a long time user of Nim (including on really lean embedded targets), compared to hygienic macros, comptime falls way short.

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)


> In what way? Comptime should be generally capable of anything macros are.

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.


I would generally agree that polymorphism is necessary, except maybe for embedded systems. You want more of a closed world system there, not an open world system that permits arbitrary extensions.

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.


> 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.

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.


> - As a long time user of Nim (including on really lean embedded targets)

Could you expound on how you used Nim on "really lean embedded targets"?


Nim is “just C” at the end of the day, so by leveraging the —-compileOnly flag you can use Nim anywhere you can use C

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.


> - 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.

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 fragmented state of the microcontroller market is pretty dire. Especially as there's more higher-level features being baked in to modern micros, every different manufacturer is implementing different APIs to do the same basic thing (whether that be I2C or things like MIPI-CSI). Having a unified API would really help portability (and would enable people to build workable emulators!)


fyi since you seem to be interested in this: rust has a few options including the embedded_hal crate with impls for everything from various microcontrollers to desktop linux.

https://docs.rs/embedded-hal/latest/embedded_hal/


Despite my day job involving rust, I struggled pretty hard getting Rust working adequately for my little GPU project on both pi pico and an esp32s3. Both had debugg-ability issues, missing features that would have required advanced FFI integrations to be added, and things just wouldn't even work. I spent a week on it making no progress, and with only an hour an evening I had to do anything it was frustrating.

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.


My experience wasn’t stellar but it wasn’t like yours. There’s of course no comparison between the speed of getting up and running with the official sdk and toolchain, but it wasn’t so miserable.


Sounds great in theory (writing MCU-agnostic libraries for peripherals), but I've found it unusable in practice due to the clunky API and incompatibility with DMA, interrupt-based-flow, least-common-denominator functionality etc.

The new direction that library and related is Async with Embassy, which... is also not something I want to use.


Modm.io is a a unified API for microcontrollers.


I appreciate that zig is an easily searchable name for a programming language. You’d think after Java and python that’d we’d have better options, but no! Now there is Go, Rust, etc etc


is it that much more difficult to append a `lang` after go, rust etc?


That works… sometimes. Just doing a quick search in the Rust book, 0 results for “rustlang”, 30+ results for “rust”.


ESP32 and STM32 support is very welcome!

I have been following https://github.com/ziglang/zig/issues/5467 for a while and progress seemed to have slowed significantly


Unfortunately, the upstream zig toolchain (LLVM codegen) does not support the Xtensa architecture (opaque CPU features), but it is possible to use riscv32 for esp32 from the C2/C3/C6, H2 and the new P4 release.

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...


Yeah I remember reading about it's relation to LLVM issues etc. I also think there was another (languishing) fork of the entire zig repo which had some support for Xtensa which I couldn't find at the time, but I found it.

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.


So don't wait. Tarballs already available for testing[1] and an example using esp-idf.[2]

[1]: https://github.com/kassane/zig-espressif-bootstrap/releases

[2]: https://github.com/kassane/zig-esp-idf-sample


I got this working on a RPi 2040 during my most recent microcontroller phase several months back. I know Zig can be oddly divisive here on HN but can we at least agree that it's a good thing to have alternatives to Python in this space?


In the microcontroller space, I think Zig is more of a replacement for C/C++. It's a compiled language, intended to produce optimized code that hits cost/performance targets.

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.


You're right of course, but if you pick up a 2040 and you're a newbie to microcontrollers (as I am), all of the documentation and first-party tools push you towards using Python, which I've never been too fond of and I started looking for alternatives as soon as I finished the last chapter of the manual. The alternatives are always there but they presume a level of experience (and C skills) that I just don't have. Having experimented with Zig I was glad to eventually find MicroZig. I remember that using it wasn't quite as easy as "push button, watch LED blink" as it was when I was using Python and Thonny but if a n00b like me could figure it out, anyone can.


it's true that micropython doesn't compete with zig, but zig is simple enough/no-fuss enough (imagine deployment strategies baked with one line in a build.zig) that it might compete with micropython.


BASIC, Pascal have been doing it for decades already, to the point the likes of Mikroe and Parallax don't have any issue staying in business selling compilers.


Agreed! I’ve similarly had good experiences with Rust on the rp2040, using embassy which builds on the rust HALs.


God, yes. Ruby is also at least somewhat usable in the space, FWIW; I've done RPi hacking with it in the past.


Word on the street is the badges at the next Software You Can Love conference will be running this.


You know what you doing! Move Zig for great justice.


YOU HAVE NO CHANCE TO SURVIVE MAKE YOUR TIME


I literally just posted on my blog about my adventures writing an OLED driver in MicroZig, it was a really fun little project

https://news.ycombinator.com/item?id=39540761


That's great and if you email hn@ycombinator.com in, say, a couple months*, we can send you a repost invite for it.

* i.e. enough time to flush the hivemind caches so the thread won't feel repetitive


Isn't it better just to repost with a query parameter like ?foobar ?


Oh the issue isn't a technical one. It's that once there's been a big discussion about a topic (in this case MicroZig) the community appetite for a second thread about roughly the same thing (in this case, another MicroZig thread) drops off sharply. However, if you wait long enough then it becomes fresh again and a new thread is ok. That's what I facetiously call letting the hivemind caches clear.


Great, thanks!


Meta: you have typod the name of the microcontroller as RP4020 twice, might be worth fixing. Nice post, otherwise!

Edit: typos, ironically. Mobile phone text editing ~ftw.


Thanks, I'll fix that


Are there similar efforts for other languages? Or could this be a starting point for a language-neutral, multi-board/microcontroller HAL?


There are lots of similar efforts, but they tend to be half-finished on many platforms that they supposedly support.

(Also, how is this one language-neutral?)


It's not langauge-neutral. But I suppose one could imagine zig at the bottom level on the bare metal and other languages being able to call into that zig layer.


I do hope that someone balls this up into an arduino core so it can be used with the arduino IDEs, it even supports the atmega328P already.

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.


Are there any plans to put this on the Parallax Propeller as well?




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: