In my previous post about me dabbling with technology I had outlined my efforts to get a grip on Raspberry Pi’s microcontroller RP2040, which is an implementation of a Cortex-M0+ processor core, including the peripherals that are essentially required, such as the clock tree, and additional devices that make up a microcontroller, such as UARTs, SPI, and a watchdog. It’s the first microcontroller I have worked with that has two processor cores.
Two Cores
First, I had thought that I would just neglect the second core to start with, and create a framework to write control programs on that basis. But – what if I later were to discover that fundamental design decisions were wrong as soon as I wanted to also use the second core? Consequently, I needed to figure out how to run a simple test program that uses both cores. Which turned out to be pretty easy, in fact. So it was clear that my nascent framework had to be conceived and implemented with both cores in mind. And actual example code to demonstrate it.
Recall that the Oberon IDE I use, Astrobe, does not yet support this system-on-a-chip, let alone two processor cores. Which means, I had to start from scratch, which comes with lots of work and experimentation, but also lots of freedoms: I don’t have to weave my framework into an existing one. And I have quite clear ideas how such a framework should be structured – in principle. There’s still sufficient uncertainty in the details. But it must include a multi-threading kernel. Ever since I had been tasked to write a fail-safe real-time program for a controller for a power station for telecom exchanges, where I had encountered and used that fundamental basis for this kind of program, I never wanted to go back.
As I wrote here:
The AGC did not make use of an operating system in today’s terms, but a well-designed real-time kernel, which provided the abstraction of a process (or job), hence allowing to split the tasks of the flight controller into separate modules, or threads of control. In general, a controller is basically reading the state of the world via sensors, and issuing commands and signals via actuators to change said state to conform with its objectives, in an endless loop. The naive approach is to actually program exactly that, one endless loop that reads and commands as needed. While OK-ish for trivial problems, this approach quickly breaks down if your controller has several realms of control, each with its own possible states. Your single endless loop becomes a mess. The Right Thing™ to do is to handle each realm of control by a separate process (thread of control).
These concepts have informed all real-time kernels and operating systems after the Apollo Guidance Computer. Which was conceived and implemented in the 1960s. Software engineering in its current form was non-existent then. In fact, Margaret Hamilton coined the term during the software development for the Apollo missions. The AGC was a real milestone achievement, even from today’s perspective.
The Oberon RTK Framework
Hence, the idea for an Oberon Real-time Kernel for the RP2040 was born (Oberon RTK). On 15 Jan 2024, I could make available a first version of the framework. I had been working quite frantically on it in the first two weeks of January. Granted, there was code I could re-use from my work for the Cortex-M0 microcontrollers from STM which I had used before the RP2040. At least basically, or conceptually, since none of STM chips has two processor cores. But having implemented real-time kernels before helped. :) In fact, it’s the first thing I do for any new microprocessor I want to use. See above.
The code is available in a public GitHub repository: https://github.com/ygrayne/oberon-rtk/.
I have also started to set up a website: https://oberon-rtk.org.
Oberon RTK is not to be mixed up with my other work, Oberon RTS, which is an operating system. Oberon RTK is a kernel, which gets compiled and linked into the control program – I have tried to explain the difference here.
I have summarised a few basic thoughts about using two cores, the concepts, and their implementation: Two Processor Cores.
The current framework provides all the basics to get started, and as basis for further extensions, of which I plan to have quite a few. I will need better peripheral device support, for example for SPI and I2C devices. Currently, I just use the UARTs, since these are needed for any kind of serial terminal interactions. Also the kernel itself will undergo development steps, or iterations, possibly resulting in variants to be used for different use cases. If you check out the code you’ll see that everything is very simple and transparent. The current kernel is about 2.5 kBytes binary code.
Extensions and Changes
I have made and published a few extensions and changes already, as listed under Change Notes. Each extensions should not only be described, but accompanied by example programs. Keeping to this scheme – documented extension and changes, with example programs – forces me to actually finish the planned features to the point where others can use them (or so I hope). Else I run the danger that as soon as something works in a prototype or proof of concept stage, I move on – so much to explore! I have been there… Now I always start with the plan and skeleton for an example program, and implement the framework features to make it work. Yes, I go back and forth, iterating until I got both right, the program and the framework. Somewhat “presentable”. You get the picture.
The Kernel
The current kernel is strictly time-driven, using cooperative scheduling. The run-queue is only managed by the scheduler. It makes many things so much easier and transparent. Lightweight. I would challenge anyone to implement a simpler multi-threading kernel with the same features. Check out the Coroutines used for the threads, and see what a context switch takes – a stack pointer change. Se sa. I simply make use of the prologue and epilogue of the corresponding procedure Coroutines.Transfer
.
I have a prototype of a kernel with a central scheduler that distributes the next ready thread dynamically to a free processor core. It works, but is pretty involved. I then realised, No! This is not an operating system, it’s the core of a control program, we know each and every thread that will ever run, each and every device that will be used, and so on. Keep it simple.
Also, when thinking about a next generation of the kernel with interrupt integration, it will be relevant on which core a thread runs, since interrupts are always handled by a specific core, no dynamics here. So right now, and for the foreseeable future, threads are defined and allocated to a specific core. For a control program, this might be even preferable. Just as different realms of control can be assigned to a process, or group of processes, they can also be allocated to specific cores. The data inputs from sensors, and output to actuators, require way less protection and synchronisation if only one core accesses them. Synchronisation between threads on one core is relatively easy, between two cores not so.
I will now work on inter-core thread communication. And then interrupt-integration, meaning an interrupt could put a thread on the ready queue, not just the scheduler. In the same vein, a thread could put another thread on the ready queue directly, bypassing the scheduler. The goal is to increase the responsiveness to changes in the controlled environment. It would be a model more and more akin to one used in the Apollo Guidance Computer.1 I will just need to also implement the so called waitlist. It’s on the to-do list. :)
-
The Apollo Guidance Computer, Architecture and Operation, by Frank O’Brien, worth a read to learn how to compute with close to no processing power, and still navigate deep space, and land two humans on a celestial body nearly 400,000 km away. ↩︎