Embedded Swift

Seconding what John said: most things you can do in C or C++ for these uses, you can already do in Swift today. It's just painful, sometimes. We're interested in making it less painful, but it's already possible.

There's a class of things that people incorrectly think they can do in C and C++ (e.g. generate precisely specified instruction sequences); those things are no more impossible in Swift, because you already can't do them in C or C++. This sort of stuff should be written in assembly. If you need a specific sequence of instructions, write a specific sequence of instructions. It can only be worse in a higher-level language.

There is a small but important set of stuff that you actually can't really do in Swift today that has varying levels of support in C and C++ (volatile reads and writes, setting/querying status and control registers, some atomic operations, ...). These are real problems that we want to fix. The Swift solutions for these may not look the same as the C and C++ solutions.

In as much as one is happy with using inline assembly to do all this stuff in C and C++, you can do that in Swift already, today, because you can put the inline assembly in a .h file and use it from Swift.¹ So we're not terribly interested in reinventing inline assembly for Swift; we'd much rather find better solutions where we can.

¹ More precisely, since the clang importer is part of Swift, you're not even using it "from Swift". Clang inline assembly in a header is Swift.

25 Likes

That was very helpful. Thank you.

Hiya. Long-time embedded (small microcontroller) developer here, though for some years now I've mostly been doing FPGAs (which are hardware design, of course). And we put micros in FPGAs.

As everyone probably knows, C is still the default language. All of the processor vendors provide APIs and libraries in C, and the usual RTOSes are in C. Inertia's quite a thing! I'm not sure that anyone "loves" C, but we know it.

I've been dabbling in Swift to do small macOS apps. I like the language.

There are some questions here about doing hard real time and having to drop down to assembly language to ensure that things happen in the order and with the timing desired. I guess that's really only necessary for bit-banging an interface. I try to avoid this where possible! Instead, use a peripheral that implements the interface needed (SPI, I2C, I2S, UART, whatever). Use a timer to pace operations. Use interrupts to respond to real-time events. Of course everyone knows this.

So my questions about using Swift on micros pretty much boil down to:

  1. how are interrupts handled? That is, how do we set up an interrupt vector table so that when an interrupt occurs, execution jumps to the routine set in the vector table?

  2. peripherals are accessed by writing to or reading from registers at specific locations in the processor address space. We tend to hide the memory addresses in header files, and all of the registers associated with a peripheral get put into a structure, but in the end it's just PEEK and POKE. How does Swift handle this direct memory addressing?

7 Likes

So there could be a world that is possible (not prescribing a direction here) that existing libraries could be interfaced as they are on desktop platform targets. Provided the builds of those could target the right build parameters that line up with Swift (as they do for non-embedded systems) the external calls could be made. However that being said - it also might be interesting to see how far that layer could be pushed down with the language, like how @kubamracek posed in the vision document.

It feels like to me that there are at least two sub-sections of embedded systems; ultra smol devices like ATTiny, and slightly beefier systems like RP2040. They may have different levels of needs.

Interfaces like SPI, I2C are all much higher level than say just working with registers directly - but with good inlining those could fold down compilation wise to be nearly (if not exactly) the same machine instructions. If we are looking at things in a higher level interface presuming those optimizations are achievable.

It would likely make sense that interrupts work very much like how closures work from a usage standpoint. This is how the Arduino SDK works in C; a function pointer is passed to the attach interrupt call to call-back into the user-land code when an interrupt happens. Now... that being said... there is a big rub there... closures like that should be escaping by normal swift rules. Escaping closures are currently required to be a heap allocation. This may not be ideal for embedded systems - and likely some other approach would be needed to be evaluated.

Per the register locations; in a raw-crunchy-unsafe-world (e.g. UnsafeRawPointer et al) those would be able (with perhaps some modification) to interface with that type of thing. I would guess that the raw side of things would fall more into a lower level API set that is specific per hardware and not abstracted in some sort of application layer.

Expanding on the vision document - I see perhaps two variations per architecture a "baremetal" target and an "embedded" target. The "baremetal" is the world where there are no heap allocations (no classes or things that use classes) be it from the compiler emission of runtime calls or the types from the standard library. Then there is the "embedded" world which has some of the allocations but no type metadata. From a library perspective it would mean that the "baremetal" standard library would be quite sparse (comparatively to the desktop version) and there could be an additional library to manage per board/cpu registers. The "embedded" world could then have everything that the "baremetal" version has but then add on nearly all of the accouterments one would expect from the stdlib (including types that have classes backing them) and have a higher level library for accessing pre-fabricated abstraction layers for things like SPI or UART etc.

Note: these are just my late night musings, and should not be conflated as a "plan". There is a lot to hash out here.

3 Likes

Normal functions then?

Strictly speaking in simple cases classes can do without heap allocation. Example:

let c = C() // global variable

func foo() {
    let d = C() // local variable
    d.baz()
    // d is getting out of scope upon return
}

That is an approach yea:

// fictitious attach
func attachInterrupt(to pin: Pin, mode: InterruptMode, _ action: @escaping () -> Void)

attachInterrupt(to: board.read.digital.5, mode .rising) { } // ❌

func foo() { }
attachInterrupt(to: board.read.digital.5, mode .rising, foo) // ✅

I would worry that might be a touch too subtile for folks just starting off.

That cannot be generalized (without using pre-allocated global tables or the such) - any time that reference type is escaped in any manner it will create a heap allocation; what you are observing is an optimization that promotes from what normally would be heap to stack based. That optimization can be quite fiddly; for example if the compiler can't completely prove the escape analysis it will opt for being safe and not apply that.

1 Like

You can use trailing closures with @convention(c) functions, they just can’t capture anything.

9 Likes

For the really small targets @escaping might have to infer an additional decoration like that. Maybe it gets its own name?

I’ve been wondering if non-allocating Embedded Swift should ban, not escaping closures, but captures by escaping closures.

5 Likes

Isn't this what @convention(thin) is for, assuming SR-10654 is fixed someday?

By not using @convention(thin), you can make a library that’s agnostic to whether it’s being used in an allocation-less environment. We may also want improvements for @convention(thin) (it is, as the current interim name applies, smaller than a normal escaping closure value, at least on current targets), but now that it’s been brought up I think it’s a great idea for people to not have to write that explicitly even when working with embedded code.

Interrupt service routines usually have their own conventional requirements that don’t match normal calling conventions. These conventions are not simply a variant calling convention and can vary between interrupts even on a single architecture (e.g. on ARM). IIUC, depending on architecture, there may not even be a stack the compiler can use — the ISR is expected to go find one.

In existing systems, ISRs are often not implemented directly in C and instead use an assembly stub which sets the basic environment up for a call into C. That technique would work directly with Swift without any new features, although it would be easier if we offered a “cdecl” feature that caused the compiler to export a C-conventions function with a particular name. But if Clang already supports writing ISRs directly for a platform in C (e.g. with __attribute__((interrupt(kind))), we could certainly adopt that support in Swift and create a @convention(interrupt(kind)) for ISRs that enforces the appropriate semantic constraints (often they cannot take parameters or return results). That would not be difficult.

Setting the ISR table is something that is usually not done directly in C today, IIUC.

4 Likes

So here is an example set of SPI from Arduino's Arduino Core for Atmel's SAMD21 processor which uses __attribute__()

My impression at the moment is that this is not the type of code we will be able to write (at the moment) in embedded swift but the type of code we will be calling. TBD if it will be possible?

Sorry - this one might have been better: https://github.com/arduino/ArduinoCore-samd/blob/84c09b3265e2a8b548a29b141f0c9281b1baf154/cores/arduino/WInterrupts.c#L55

(equiv to Rauhul's example below in the ARM CMSIS that Arduino pulls in, provides both c and assembly. https://github.com/ARM-software/CMSIS_4/blob/8c0e1a91341cde86532b05625f2ad584ce856118/Device/ARM/ARMCM0plus/Source/GCC/startup_ARMCM0plus.c)

(more info on the platform specification: Platform specification - Arduino CLI)

1 Like

FWIW this "just works" on armv7em (as seen in the above demos) by abusing @_cdecl

@_cdecl("SPI2_IRQHandler")
public func swift_SPI2_IRQHandler() {
  //  Please ignore the body of the handler :sweat_smile:
  print("SPI2_IRQ")
  while true { }
}

The (LTO) demo above setups up the vector table in assembly in a custom section:

// MARK: - Vector table
  .section __VECTORS,__text
  .balign 4
  .global _Vectors
_Vectors:
  .word STACK_START_ADDR                // Start the stack at the requested addr
  .word _Reset_Handler                  // Jump to _Reset_Handler to boot
  .word _NMI_Handler
  .word _HardFault_Handler
  .word _MemManage_Handler
  .word _BusFault_Handler
  .word _UsageFault_Handler

The vector table is linked to the correct address using linker flags.

All the handles are weak linked to a default handler which must be implemented by the application code:

   .weak_definition _NMI_Handler
   .set             _NMI_Handler,_Default_Handler

The weak links can be overridden using the @_cdecl handler seen above.

6 Likes

This is exactly how we implement interrupt handlers in Swift for Arduino at the moment. Currently we have small C shims that implement the interrupt handlers and call back into global static function pointers that are set with @convention(c) callbacks that call into Swift (and some linker tricks to make sure we don't add things unless a user uses this particular interrupt handler. It works but has a small overhead per handler (2 bytes of global memory and a few bytes of program memory). We have interrupt handlers working fast and well. Like @jrose says, we can't capture anything. We can update global variables in some cases in our programs and it works for us. There were some issues with optimisations but we pretty much fixed them.

p.s. I'm going to add a simple Swift patch to the compiler to add an attribute for interrupt handlers so we can ditch the C stubs in the near future. The main thing will be it emitting the correct llvm function attribute for interrupt handlers so it lowers correctly (this makes the AVR back end saves all registers on the stack, not just the gcc ABI standards, and does a RETI instead of RET). It's on my todo list for some time before Christmas, but it's not a blocker for us. People can already use interrupts on our platform and do.

2 Likes

@aspdigital ... AVR solves it by having special symbol names for the interrupt handlers, which link via the linker script to the right place in the final binary. There are also weak symbols there that act as "defaults" for handlers that are not specified (there are about 25 handlers in the interrupt table for atmega328p for example) so that those vectors redirect to the symbol bad_interrupt (from memory), which just redirects to a reboot, so that code that triggers unexpected interrupts on AVR (and on our platform) causes a reboot, which is probably reasonable. We might change that to something more like a NOOP (perhaps a single RETI instruction)... I'm still considering that approach. Enabling an interrupt without writing an ISR feels a bit like a programmer mistake that should be flagged obviously (hence the reason a reboot seems reasonable).

On Swift I considered using @_silgen_name("__vector_3") to do this but like @John_McCall points out, I'd still need to emit the interrupt llvm function attribute (it might actually be signal on our platform, I forget...)

So I'll probably do all of it with a small patch to the compiler on our tree to add something simple. Then possibly swift macros to try and reduce boilerplate.

2 Likes

Wowww !! Congratulations to all for these advancements !!!

In our organization, in addition to Swift work on macOS, Linux and Windows we have our HW products using 8-bit microcontrollers, up to Altera (Intel) SOC FPGAs with embedded ARM CPU on which I dream of programming in Swift rather than C /C++. For embedded OS, we are using from bare-metal to FreeRTOS and LinuxRT.

Lets go Swift everywhere!

8 Likes

@rauhul -- thanks for the overview. This is pretty neat!

1 Like

As Andy knows, I’m working on a Linux HAL for SwiftIO, to allow for portability between bare metal and Linux embedded Swift applications.

5 Likes

That's so awesome @lukeh ! Welcome to the embedded / microcontroller / bare metal part of the Swift Universe! I hope it will be very big one day! :slight_smile: