nimbme – Nim bare-metal environment

2025-06-2718:458122github.com

Nim bare-metal environment. Contribute to mikra01/nimbme development by creating an account on GitHub.

Nim bare-metal environment for embedded targets. headless mode

Actual implemented target: raspberry pi1 / pi zero (bcm2835)

General target requirements:

  • at least 4KiB ram
  • at least 20KiB flash
  • 1 UART for terminal
  • 1 hardware timer
  • [cycle counter]
  • software interrupt mechanism
  • cooperative scheduler (actual simple round-robin scheme / deadline-scheduler planned)
  • code runs under system-mode (armv6)
  • async programming model (requirement: do not block the event loop)
  • easy portable / most of the stuff is in nim - only a 'few' lines of asm
  • bare-metal playground and research
  • no vendor specific API's - just Nim and direct hardware access
  • having fun
  • GNU-ARM
  • a terminal for uploading files (CoolTerm, yat(supports 3000000baud) , realterm or something else)
  • usb to serial adapter [!!! 3.3Vmax on the line !!!] (ftdi, CH340..)
  • tested with Nim compiler devel / GNU-ARM toolchain 13.3Rel1 with windows11 host
  • older toolchains are also fine but not 14.2Rel1 (actually not investigated)
  • current baudrate is set to 3000000 / highest one for 250mHz core clock - changeable in envconfig.nim [config_uartBaudRate]
  • checkout project and compile the demo with "nim build_rp1 project.nims"
  • configure host terminal (EOL is linefeed)
  • copy kernel.img to raspberry´s sd-card (consecutive builds are uploadable via terminal (Motorola SRecord format used))
  • wire gpio14 (TxD0) to adapter's rx-line
  • wire gpio15 (RxD0) to adapter's tx-line
  • Rx/Tx wires should be short as possible (10cm jumper wired will work) / the ftdi runs fine with 3000000baud (other vendors not tested)
  • wire ground (I always use pin39) to adapter ground connect tx/rx uart lines
  • power target on
  • follow the instructions on terminal :-)
  • stdio is retargeted to UART
  • the demo spawns up to 10 'processes' (at the moment no posix api)
  • the complete runtime in cycles (per process) is collected (irq cycles are also collected but at the moment not related to the actual active process)
  • total ram is limited to 64kiB / shared-heap around 24kiB but you can change that in the main linker file subdir 'hal/boardname/boardname.ld'
  • process stacksize is 1kiB (adjustable in envconfig.nim)
  • race conditions are trapped and the cause is printed out to UART (same for uncaught exceptions)
  • you can do snapshots of the entire register set (cpsr/spsr not implemented)
  • in memory image uploadable (the loader is invoked if a race condition occurs or the demo is finished)
  • the ARM is configured to prevent unaligned access

the build size is heavily influenced by libraries and functions you are using. For instance printf and friends occupies whooping >20kiB program space. I faced some runtime problems (spurious exceptions) with newlib-nano so it is not used. The default newlib build delivered with the toolchain has reent-support and that stuff needs also much program-space. If you experiment keep in mind that the stack needs to be 8byte aligned in most cases. When you face race conditions this could be the culprit (or your stack is corrupted due to overflows). Experimenting with SBC´s is great because your hardware is not brickable. Unfortunately the provided bcm2835 datasheet is everything but not a datasheet (seriously). If you look at the PI4/PI5 nothing changed. There are other vendors with superior documentation if you like to start with that topic. I simply choosed this target because I recently found one onto my desk ( I remembered Linux was awful slow on this target (10yrs ago) )... and now there is something better :-) --- and yes I was not aware how 'detailed' the BCM-datasheet is.

I did some overclocking experiments (core-clock 500mHz / arm-clock 1gHz) and I found no issues if you do not utilize the VC. This setup is working fine and the chip temp levels out at around 40 degrees celsius (around 50 degrees celsius for the pi zero). I refer so some community efforts to find the correct settings in config.txt and/or consult config.txt properties

thanks to Nim you literally do not need a jtag debugger (this project was completely done without one). If you like to invest some money go for a scope and utilize gpio for tackling rt problems. If you like to monitor specific code parts use intro and outro procs with exported symbols (to find the codepart of interest) and consult the .lss output. Join state information (print out register- and/or memory snapshots) if needed. For latency measurements take snapshots of the 64bit free running counter (clocked at 1mHz fixed). Unfortunately the free running arm counter option is not implemented (bcm2835) and the prescaler only works with 256 so there is now way to get a better resolution than around 512ns (500mHz core clock divided by 256). Fine granuled measurement is only possible with the arm cycle counter (roundabout 4 seconds max. without prescaler)

  • GPIO handling helper (RP1)
  • more targets (Cortex-M0 / Sitara AM3358 / risc-v /.. planned)
  • in memory-app mode for flash targets with ram > 32kiB (compile and run your prototype in ram without flashing)
  • generic driver layer
  • get ethernet running / usb gadget mode for raspberry pi zero
  • sdcard I/O
  • signal handling
  • spi and i2c examples
  • ...

David Welch´s experiments years ago saved me some time in figuring out 'bcm2835-details' in cases the datasheet lacks some information or was simply wrong (no official erratasheet out there...). If you like to look into the datasheet consult this before: bcm2835 errata


Read the original article

Comments

  • By ronsor 2025-06-2722:112 reply

    Since Nim compiles to C, porting it to new platforms is surprisingly easy. I did it a few years ago for 16-bit DOS (OpenWatcom): https://github.com/Ronsor/Nim/tree/i086-and-watcom.

    • By snac 2025-06-2816:10

      Pleasantly surprised about the ease of porting to new platforms. Being able to inline assembly, nevermind call regular C functions (eg from an OEM SDK) is incredibly handy. I recently wrote a pure Nim implementation for the CH32V003 microcontroller https://github.com/snacsnoc/nim-on-ch32v003

    • By cb321 2025-06-288:271 reply

      While there are several "OS in Nim" projects (https://github.com/khaledh/fusion is probably the most interesting), this same ability to run bare-metal and generate to C should, in theory, make it possible to write kernel modules in Nim for Linux/SomeBSD without any special permission slip / drama over integration (the way Rust suffers, for example). I haven't heard of anyone doing such modules, but it might be a fun project for someone to try.

      • By PMunch 2025-06-288:371 reply

        Well, the C that Nim outputs isn't exactly human readable. Sure if you know what you're doing you might be able to follow along, but no sane maintainer would admit it into a C project.

        It could help the build process a you can pre-compile to C and only use the C build system. And it allows you to run anywhere C can run (we use it for everything from servers to microcontrollers). But apart from that it's mostly an implementation detail.

        • By cb321 2025-06-288:59

          My idea is not to submit the generated C any more than you submit C-compiler generated assembly, but to write directly in Nim. The niceness would mostly just be if you wanted to write some complex thing like a filesystem or driver in Nim with its templates & macros & user-defined operators and DSL building and all that goodness.

          Being a module means it can be separately distributed - kernel maintainers don't need to admit them or even be aware of them. ZFS is not distributed in mainline Linux out of Oracle fears (though that module is popular enough some distros help out). This is more or less the key to any extensible system. The C backend is helpful here in that internal kernel APIs can rely heavily on things like the C preprocessor, not just object file outputs.

          I think the main challenges would be "bindings for all the internal kernel APIs" and the ultimately limited ability &| extra work to make them Nimonic and adapting the "all set up for your C module" build process to ease a Nim build (as well as the generic in-kernel no-stdlib hassles). So, I imagine a 2- or 3-level/stage approach would be best - the background to make the Nim modules easy and then actual modules. { Linux itself, while the most popular free Unix, is also kind of a faster moving target. So, it would probably present an additional challenge of "tracking big shifts in internal APIs". }

  • By pmarreck 2025-06-285:132 reply

    Nim seems underrated. A nice balance of utility, longevity, speed, and coding niceness.

    • By ryukoposting 2025-06-2811:10

      I was active in the community for several years before getting too busy with other things. Good folks.

    • By corv 2025-06-286:26

      Happy to see it get more attention here - it really is versatile

  • By hugs 2025-06-2722:372 reply

    Happy to see more Nim projects on HN!

    I don't know if AI code gen helped with this particular project, so please forgive my small tangent; Claude Code is surprisingly good at writing Nim. I just created a QuickJS + MicroPython wrapper in Nim with it last week, and it worked great!

    Don't let "but the Rust/Go/Python/JavaScript/TypeScript community is bigger!" be the default argument. I see the same logic applied to LLM training data: more code means more training data, so you should only use popular languages. That reasoning suggests less mainstream languages are doomed in the AI era.

    But the reality is, if a non-mainstream language is well-documented and mature (Nim's been around for nearly 20 years!), go for it. Modern AI code gen can help fill in the gaps.

    tl;dr: If you want to use Nim, use Nim! It's fun, and now with AI, easier than before.

    • By ternaryoperator 2025-06-2722:523 reply

      Is there a reasonably good IDE for Nim that provides debugging, specifically the full debugging experience (Nim code rather than C, breakpoints, inspect/modify values, etc.)? That's been the gating factor for me trying it. What's the present situation?

      • By hugs 2025-06-2723:011 reply

        I'm using Claude Code... And then for manual review and editing, using Zed with Nim extensions: https://zed.dev/docs/languages/nim

        Sorry, I don't really do debuggers... I mostly step through code interactively using a REPL (INim).

      • By pmarreck 2025-06-280:541 reply

        I see comments like this fairly often and in my entire career (I'm 53) I've never had to use a debugger. For inspection of values, I write small functions and unit tests or just output to stderr in debug modes set with env vars. I've always thought that the need to use a debugger was a code smell- too much cyclomatic complexity of single functions.

      • By HexDecOctBin 2025-06-280:571 reply

        Does Nim not output the #line directives when compiling to C? That alone should help with the debugging experience.

        • By cb321 2025-06-286:371 reply

          Sure! Just compile with nim c --lineDir=on or drop `lineDir=on` in your $HOME/.config/nim/nim.cfg (or per project NimScript .nims or per file foo.nim.cfg or ...) and source-level debugging with gdb on Linux works..

          mostly at the level of Nim source, although various things are likely to "leak through" like mangled names (though in fairness, lower-level assembly notions leak through in "C source level" debugging...).

          Beware, though, that since around Spring 2024 this can make the tinycc/tcc backend infinite loop (but otherwise tcc can be a nice way to get fast development iteration).

          • By elcritch 2025-06-2814:08

            Also recently Nim’s generated C code started using Itanium name mangling, which oddly has become the defacto name mangling method for C++ on Linux and other platforms.

            Meaning you get pretty function names with generic types and all included with debuggers that support it. Works better with ldb as gdb seems to refuse to recognize it.

    • By aryonoco 2025-06-2723:111 reply

      My experience has been the same. I have found it much easier to write good Nim and F# code with Claide Code, than say modern Python with type hints everywhere.

      Both Nim and F# have strong types and strict compilers (arguably more strict in case of F#). These factors matter a lot more than how much code there is for the LLM to train on. And there probably is less ostensibly bad Nim and F# code out there than old Python code written by people who were not really developers, so the training data set is higher quality.

      • By elcritch 2025-06-2814:20

        I agree, the code output for Nim with Claude and Gemini as well is pretty good. It misses some Nim idioms but generally does really well. Simple syntax with strong types helps both Nim and F#.

        I used Claude 4 to generate SIMD versions of signed distance functions for both NEON and SSE2 and it all worked and compiled fine the first time aside from some minor import changes [1]. Gave me an almost 4x increase for many SDFs.

        I also use Atlas, an alternative package manager for Nim [2], to clone deps into a local folder which lets Cursor grep and search the dependencies to add it to the context as needed.

        1: https://github.com/elcritch/sdfy 2: https://github.com/nim-lang/atlas

HackerNews