Embedded Swift Example Projects for ARM and RISC-V Microcontrollers

Hello!

@rauhul, @Philippe_Hausler and myself have assembled a repository with several simple example projects that show how to use Embedded Swift to target a range of MCUs (STM32, Raspberry Pi Pico, nRF, and even a RISC-V ESP32), how to integrate with existing SDKs, and how to control some interesting peripherals. The repository is available here:

Note that the repository contains code that is not supposed to be continuously developed, but instead it should serve as an educational and demonstration tool. Adding more examples (e.g. to cover more MCUs, or more peripherals) is absolutely welcome, but we should refrain from making extensive changes to the existing examples.

We've also published a blog post highlighting this new repository and encouraging anyone interested in embedded development to try out the examples:

Please feel free to discuss both the blog post and the examples themselves here, any feedback and ideas are welcome.

35 Likes

Thanks! Very nice to see the advancements in this area! It looks like the examples only work on MacOS. Is that correct?

I have only tried them on macOS, but I don't think it should be hard to make them work on other operating systems. The Swift toolchain is certainly able to produce binaries for these embedded targets (including ELF binaries) from other OS's too.

(I haven’t worked with embedded systems before, so the following questions may sound dumb.)

The build process for each example looks a little complicated. For example, here is what the stm32-uart-echo example looks like:

  1. The package manifest exposes a static library product called Application.
  2. The Makefile does a few things:
    a. Build the libApplication.a library.
    b. Use the linker to create an Application executable from the libApplication.a library.
    c. Demangle the linker map.
    d. Disassemble the Application executable.
    e. Extract an Application.bin binary from the Application executable.

Questions:

  1. Why not specify an executable product in the package manifest and build the executable directly?
  2. What are steps 2c and 2d for? They don’t seem to be used in step 2e.
  3. What is the difference between the Application executable and the Application.bin binary?
  4. Is there a way to encapsulate some of these details inside the Swift build tools to reduce the barrier to entry for developers who are new to embedded systems?

These are excellent questions. The summary answer for why are the build processes in the examples complicated would be that it's because embedded hardware does very often need bespoke setup in terms of compilation, linking and packaging and it differs for different vendors. It would certainly be possible to hide the complexities but I think that would be an anti-goal for these Embedded Swift examples -- it's useful to see all the complexity that's necessary to assemble a fully working firmware for a particular board.

Concrete answers:

  1. Why not specify an executable product in the package manifest and build the executable directly?

Linking an embedded firmware binary is a very different step from linking a userspace dynamic executable -- the author of this example (@rauhul) probably split it out of SwiftPM for two reasons (1) it's such an important step that needs to get a lot of configuration exactly right that it's better to highlight what exact command is used for that, (2) in any more real-world situation, you are likely to also have some C/C++ code as part of your build, and vendor-provided libraries, etc. and you will probably want to still control your link step separately.

  1. What are steps 2c and 2d for? They don’t seem to be used in step 2e.

Correct, they're unused. These are side outputs that are useful for manual or automatic analysis. E.g. the linker map can be used to check if there's any undesired library code being linked in, or if the codesize isn't too large for the device. And the disassembly listing can be used to check if there's any instructions outside of the ISA that the device supports.

  1. What is the difference between the Application executable and the Application.bin binary?

The bits that need to be flashed onto the device need to be raw bytes written to a particular memory address. This means that executable file formats like Mach-O or ELF are not suitable -- there is no support for Mach-O or ELF in the hardware. So instead this step extracts the raw code and data segments/sections, with specific well-known addresses where we expect to load them. Application.bin is the raw representation of the firmware suitable for direct flashing onto the device. There is quite a bit more complexity in this, and sadly it is again device/vendor/chip specific and doesn't generalize easily.

  1. Is there a way to encapsulate some of these details inside the Swift build tools to reduce the barrier to entry for developers who are new to embedded systems?

This is a fair request, but at this point the example code is trying to avoid building abstractions, it's instead trying to "show all that's involved" because that serves the educational/demonstration goals of these examples projects. That said, there are other examples in the repo which integrate into existing vendor SDKs, and those will be using the abstractions already built by these SDKs.

7 Likes

Thank you for providing this example repository. It's incredibly helpful for understanding the details of these build tools!

I'm looking forward to exploring how we might integrate our SDK with it, even though you do not recommend integrating with the Zephyr SDK :joy:. Truly outstanding work :heart:

2 Likes

What is the binary size of the simplest example?

I‘m extremely delighted to see the ESP32 family in there. Thanks! Is Xtensa completely out of the picture or just harder than RISC-V?

1 Like

Depends on the degree of LLVM support for that architecture. At least it looks like we don't handle the corresponding triple component while LLVM can do that. Fixed in swift-driver#1576.

2 Likes

See https://github.com/apple/swift-embedded-examples/tree/main/stm32-blink which includes a size breakdown in the README. It's a blinking LED program for STM32F746G, which ends up being 142 bytes of executable code total. With a mandatory vector table, and segment alignment, the final firmware is slightly over a kilobyte.

I'm sure this could be squeezed down more, maybe the vector table can be incomplete (we're not using most of the entries), maybe the alignment could be reduced, LTO would probably shave off a few more bytes of executable code... but that all would really be code golfing rather than anything practical.

3 Likes

With nightly-jammy I can get some things working, but I get stuck at linking. After some investigation, I concluded that for the stm32-blink example, I should theoretically use ld64.ldd, but that doesn't work. I guess it's possible to use a different linker, but I'm not sure how to configure it, so I'll have to wait until I have some more time to look into it, or someone else figures it out.

I'm surprised that stm32-blink example uses macho instead of elf as the object format component in its target triple, I wonder if switching to the latter is possible? Then you would be able to use ld.lld on any host platform consistently.

Right, the biggest blocker for a while was that LLVM did not support Xtensa as a backend at all. Now it does have it, but it's still experimental and doesn't seem to be complete yet, although there's active progress on it, see https://github.com/espressif/llvm-project/issues/4. The LLVM backend support alone is not enough, Clang will also need to gain support for Xtensa. Once that happens, there shouldn't be anything hard about supporting it in Swift.

3 Likes

I’m not sure I understand. Doesn’t Xtensa apply to esp32, instead of stm32? Or are you saying that in general some choices were made, due to the lack of Xtensa support, that we need to use macho for stm32 as well?

The note about Xtensa support in LLVM and Clang was a reply to a previous post (Embedded Swift Example Projects for ARM and RISC-V Microcontrollers - #8 by mickeyl).

Using Mach-O files for firmware really is unusual. Other examples in the repo use ELF. It would be a great idea to also provide ELF versions of these Mach-O only examples, yes.

But we wanted to show what it would look like to use Mach-O too, it's much simpler to link, and until very recently we didn't have an ELF linker in the macOS Swift toolchains (as of the latest 'main' toolchain, it now has lld). Also, at the end of the day, a linked binary still needs post-processing and conversion into a raw firmware, so both Mach-O or ELF headers and structure ends up being removed anyway.

2 Likes

Ah, that makes sense!

Clear. Perhaps I’ll have a look at it when I find some time. Thanks!

Thanks for the detailed answers!

Absolutely, I agree with that! I was just thinking about whether we had any longer-term plans to make all this easier for n00bs like me. :smile:

I see! Could a linker script be used to encapsulate the device/vendor/chip-specific details of the firmware layout? Then, perhaps the firmware extraction step could be eliminated because the linker could output the firmware directly:

@echo "linking..."
clang ... \
  -fuse-ld=lld \
  -Wl,--oformat,binary \
  -Wl,-T,"$PATH_TO_LINKER_SCRIPT"

Imagine if manufacturers published linker scripts for their devices and developers could just pass the linker script to the linker without having to worry about the details of the firmware layout!

Hmm I guess what I found strange was that a library product had a main function. If the above linker script approach works, I wonder if we could introduce a new product type (perhaps named .firmware) that accepted a linker script as one of its parameters and outputted the firmware directly:

products: [
  .firmware(
    name: "Application",
    linkerScriptPath: "STM32F746G-DISCO.ld",
    targets: [
      "Application"
    ]
  )
]

Similar to a .executable product, swift build would output the Application.bin firmware directly, eliminating the linker step for applications that use only SwiftPM.

This is not how I'd personally recommend people blink their first LED (YET!), but for the committed it might be easier to look at how things go from Assembly to C first on small chips (better documented path). Here are some Assembly and C using the GNU tool chain on STM32 chips:

Something to keep in mind, the Arm assembly for many of the smaller chips is the 16 bit "thumb" width. It's a different instruction set.

And just for fun - one of my favorite SAMD21 resources (Core M0) It's comments on a manufacturer provided linker script - so that is a thing. For the SAMD chips search github for "brief Linker script for running in internal FLASH on the $YOURPARTNUMBER" finds lots of things.

3 Likes

Just wanted to chime in to say, I like the way you are thinking.

David

1 Like

Thanks for the resources! Here’s another one that I just finished reading: Everything You Never Wanted To Know About Linker Script · mcyoung.

1 Like