Recently, I’ve been exploring using Crystal on embedded devices i.e. microcontrollers. Since most microcontrollers use 32bit arm, there’s no compiler changes required. However, the standard library is designed around use with an operating system, so it must be rewritten. I’ve started working on a toolchain, including a new stdlib, targeted at this.
My preferred way to develop tools and libraries is alongside a real project, since it guides the design with real goals and checkpoints. In this case, I’m designing a indoor lighting board, with two channels of 6500K and 2600K LEDs. The LED current is controlled by opamps built in to the MCU I’m using, a MSPM0L from TI. The current setpoint is set via filtered PWM output from the microcontroller. Schematics here for the interested.
The application code I have so far is available here. Also in that repository are a set of high-level bindings for the peripherals I have used so far. The nicest of which being the opamp, and the most complex the timer. I hope to package these high-level abstractions up alongside some linker scripts and makefile snippets into a library soon. I also have some work on a more well-known microcontroller platform: the RP2040 from the Raspberry Pi Pico.
These two Crystal on microcontroller ports share a standard library, kecil.cr (pronounced /kəˈt͡ʃɪl/, “small” in Indonesian and Malay). My goal with kecil was not to replicate a subset of the interface of the Crystal stdlib, but to explore different design options given the opportunity. It remains a very small library so far, since I have been implementing only methods I actually use. For example, division is not yet implemented.
Also fundamental to this effort is svd.cr. Memory-mapped registers are used to configure the hardware of microcontrollers. ARM microcontrollers share information on the layout and description of these registers using svd files. svd.cr is a code generator tool which takes those descriptions and generates type-safe bindings which make using those registers easier.
I’m going to keep working on trying to create an ecosystem around crystal on microcontrollers, because so far I’ve enjoyed this experiment and I think it has legs. And I’m looking forward to not having to touch C when working on more of my future projects. Let me know what you think!
With -Oz the application code I posted above is ~1kb, but 192 bytes of that is from the interrupt vector table, and there’s some unnecessary duplication in the way I wrote some of the high-level abstractions. The actual assembly looks nice—after optimization—and it seems like the SVD.cr raw access is zero-cost. -O0 is a disaster, and needs huge stack sizes, but for most mcu programs, optimized builds take less than a second. I don’t currently see any indication Crystal programs will be larger than C++ ones built with clang and the same optimization passes.
I don’t have an equivalent C program to compare the sizes with directly, but it shouldn’t be too hard to measure if someone wants.
I’m aiming for there to be abstractions (with common interfaces) for each platform supported for GPIO/Timers/UART/I2C/SPI, as well as support for fibers (using LLVM’s segmented stacks), eventually. People can write libraries on top of these to support various peripherals etc.
I have no idea about how far off these goals are, but at least the common abstractions should be easily achievable. The fiber stuff maybe the next year if I keep having time for the project.
I’ve tried to reproduce and got a blinking led on STM32F303! Can’t say how fun it is to use Crystal for MCU pogramming instead of C.
Troubles (minor) that i’ve met:
svd.cr displays error on “0X00…” numbers
patched svd.cr to do downcase
svd.cr generate errors due to registers and enums named “1”, “2” etc
patched svd.cr to replace with “N1” (for types) and “_1” for methods\args. Can share a code but it is well, crunchy and perhaps there is a better way.
Unsupported dim array usage (Exception) from src\model\dim.cr:21 in 'instance_name'
i don’t understand what is it so replaced instance_name in templates
svd defines methods only for GPIOA, not for GPIOB\C\D\E
dropped crystal-svd in makefile, instead generate bindings once and fix them manually
trying on Windows shows “LLVM was built without ARM support”
used Linux in virtualbox.
svd file lacks an RCC bit for GPIOE, so pointer.store and pointer.load manually.
And finally led is blinking!
I understand there is still a way to go
irq handlers
linking with C code if it is possible (to reuse things like LWIP or FreeRTOS)
fibers and synchronization primitives if FreeRTOS not going to be possible.
String support that don’t use GC
but what you already done is great!
These errors make sense to me, I wrote svd.cr firstly for the specific SVD files in front of me, and secondly for future expansion. Since SVD files from vendors often barely follow the spec let alone make sense, I figured it best to write features to solve problems in specific SVD files instead of guessing. I’d love to see patches, or issues for these!
Unfortunately, looks like you’ll have to build LLVM yourself
Yes, vendor SVD files are often wrong/incomplete, so much so that the rust embedded project wrote svdtools to create yml files which mutate the svd files to fix them. I had to use such patches for my MSPM0L bringup.
Indeed, all of the above is on my goals list (including a GC.) The interesting part will be making the result scalable from 1-2K of SRAM up, presumably with reduced features.
In all, please contribute in whatever way you can, publish your code, etc. I’ll try my best to make it an easy experience for you.