A couple years ago, I managed a summer/afterschool program that introduced thousands of kids (age 8-18) to the Arduino. When we would set up a software abstraction to simplify multitasking (something that came up often with more advanced students), we set up something like this:
int time = 0;
int max_period = 10000;
void loop() {
if (time % 100 == 0) {
everyTenthSecond();
}
if (time % 1000 == 0) {
everySecond();
}
// ...
time = (time + 1) % max_period;
}
This `loop` assumes functions are perfectly non-blocking, which obviously is not always true, but works well for 99% of actual use cases. If keeping accurate time is required, the student would compare `time` with the elapsed millisecond time since the last loop invocation, adjust accordingly, and check for any periodic functions that weren't executed in that period. It gets tedious quite fast.
If I had to imagine a solution, it would be to simply have an easy way to enter parameters to the JS-equivalent of `setTimeout` and `setInterval`.
> The ECM hardware generates a periodic interrupt at 160 Hz. (Where have we seen that number before?) The entire ECM program operates off of this one interrupt. Except for some initialization code, there is no non-interrupt level code. In fact, the interrupt service routine never returns, it simply cleans the return address off the stack and waits for the next interrupt. There are no other interrupts in the ECM. This means that fully autonomous hardware generates all the high speed signals like the fuel injector and ignition pulses.
> At each tick of the 160 Hz, various tasks are performed. Some are done every tick. Others only on just odd or just even ticks. There are also 16 tasks, one of which is performed each tick. Every tenth of a second, all 16 tasks will have been completed. In general, this means that the ECM cannot adjust engine performance faster than 10 times a second. Perhaps this explains the engine surging at low rpm that many list members have complained about.
That is spiritually what their Scheduler library is doing, as mentioned in the article. Which works for a lot of things just fine until one task begins to overrun its time slice or just hangs. And then you're looking into preemption.
But if you're overruning an AVR micro maybe it's time to take the training wheels off and move up to a larger system.
I'll take this one step further and suggest that beginners not start with the AVR, but instead with a Blue/Black pill (STM32) or ESP32. Leave the AVR for the more advanced people who are trying to squeeze every penny out of a project (even there, in many cases STM32 will cost less!!!).
I visit the arduino.cc forums at least once every day and the things that beginners want to do these days is far more advanced than what they would have attempted even 10 years ago. It gets very difficult trying to explain how to do things on an AVR that would be much easier on a processor with far more resources. String vs char* is one of those.
Hell, I don't know why we're even telling beginners to code in C++ in 2022.
My downvoted comment that boils down to "just use an ESP32 and get FreeRTOS along for the ride" is in this vein. The reality is that beginners have a lot of trouble wrapping their heads around writing nonblocking code using timers and something like a FreeRTOS thread is a much simpler concept to explain.
I agree- I think just about everybody should start with an ESP32 instead of an Arduino. I still use the Arduino API when programming the ESP32 (due to the large amount of compatible drivers) but I've also installed FreeRTOS and ported zork. I am embarassed to say I also got C++ STL working on an AVR with 8k of ram- enough to allocate a std::vector<int> of length 2.
I actually use the ESP32 version of FreeRTOS professionally, and I can't imagine rolling your own scheduler and all the work involved when someone could just use the existing FreeRTOS codebase.
The ESP32 stuff is awesome for my hobby projects, I do see the warts when I use it professionally though, trying to get the system to do more than a couple things at a time is not easy. We've re-written a number of core ESP-IDF packages, or gone through the pain of porting the vanilla FreeRTOS stuff over. We're also using C++, I'm looking forward to Rust.
It's funny - I stepped away from electronics when I started college and in the first few years out of it just due to not having enough free time. About a 10 year period.
I come back and now AVR is old and slow and ARM is hot stuff. All it took was the tooling becoming nearly free. No more $500 ISP programmers, just USB DFU boot.
I remember the first ARM board I got back in like 2008 required some Keil IDE which was >$1k for hobbyists and only had a 30 day evaluation included in the kit. Made by a company called Luminary Micro iirc. Builtin programmer was locked to only work with that specific chip. Never seriously considered ARM for a long while after that.
I'm a casual embedded-electronics user at best, most of my projects haven't advanced much past turn a simple servo, light some LEDs, maybe read a sensor and POST to an endpoint. For these tasks, Arduino has been pretty good for me and removed a lot of the complexity. The `setup()` and `loop()` model makes a lot of intuitive sense. That being said, I've spent no small amount of time on `String` vs `char*` (as an inexperienced C/C++ developer) and found the guardrails of the Arduino ecosystem to be very frustrating.
Do you have any recommended resources or additional search terms to explore to learn more about hobbyist-level embedded electronics outside of the Arduino ecosystem? FreeRTOS looks interesting but it seems to add a lot of overhead versus something simple like Arduino. Similarly, I've looked at STM32 programming before but my searches were very generic and the STM ecosystem is massive. Specifically, I was trying to figure out if I could reprogram some old drone flight controllers (equipped an STM32F103CBT6 with a bunch of useful embedded sensors, running old versions of "betaflight") for personal projects but the entrypoint to STM programming (STM32Cube?) and the setup code was considerable.
The beauty of the Arduino is the ecosystem behind it. The original hardware is verging on obsolescence, to be polite, but the same code and programming methods can be used on STM32 hardware (that's Arduino-compatible, like the Blackpills) or ESP32. The main difference is that now you have much faster, more modern processors, far more Flash and RAM to play with, not to mention a more powerful I/O peripheral structure if you want to dig into it at that level.
FreeRTOS on an ESP32 in the Arduino IDE is effectively free: you don't have to do anything to enable it. It's pulled in by default.
AFAIK, Arduino support for STM32 is limited to the F103 & F4xx series.
I confess that since my entry point is as a professional, I really haven't kept up with what other hobbyist-level entries are still on the market. That said, a good place to start would be with an STM32 Discovery board -- if you can find one these days! Looks like Digi-Key has exactly 1 in stock rn. It's a $19.95 board with an 'F407 and some sensors and output devices. No external tools needed: you can program it through a USB port. All the tools can be downloaded from ST Thomson or its partners for free. This is more entry-level professional than hobbyist, but there's no sharp dividing line there.
I think AVR is a good architecture to learn on, because the devices are far simpler than ARM or other more modern chips. The main concept to master as an embedded developer is operating a device by its registers. The common arduino device, ATMEGA328, has a total of 84 registers across all peripherals. A simple program to blink the LED is less than 10 lines total. The datasheet is tiny compared to most ARM devices, but is still a complete source of information.
There are lots of libraries online that demonstrate how to set up registers for various peripherals. Avrgcc is an open source toolchain, and avrdude can program devices running the arduino bootloader.
That was true when one had to write assembly code. Now we don't really have to any more, thanx to Arduino mainly (which bilds upon avrgcc and avrdude). It's easier to learn on the new platforms because you can use higher level programming languages like MicroPython, especially for easy stuff. I can do a blinky by writing three lines in the uPy REPL.
If the registers are abstracted away, you're left relying on other people's code. I suppose learning microcontrollers can mean different things to different people, but as for becoming a professional embedded developer, micropython and Arduino are dead ends. There is no transition to truly understanding the hardware.
We use MicroPython commercially so my perspective is different. :) I do agree with your intent though; just because you're using a high-level language it doesn't excuse you from learning how the hardware works.
However, if you do know the hardware, if you are familiar with what's going on under the hood, then MicroPython allows you to write the majority of your system significantly faster.
This is subject to change, as embeded developers now focus on writing lower or higher level code mainly in C/C++ as the industry migrates away from 8-bit MCUs to 32-bit ARM and RISC-V. Of course you're relying on other people's code, as you can't expect everyone to write their own RTOS and standard library and also achieve an acceptable time to market. Most embedded hardware products use an existing RTOS and build upon it. Understanding how the hardware works always helps, but most of the time you don't have to dig to register level to do that.
Micropython and Arduino are not dead ends but hobby grade tools.
I hear ya, I think we're just in a different era now. Users can wrap their head around a precooked framework that's ready to go and only needs a few API calls to make something happen. Beyond that? That's homework and homework sucks.
FreeRTOS really isn't that large of a leap forward, in fact when it's done right on your target platform it's just a few API calls as well. But it means wrapping your head around a lot of detailed concepts (stack size? semaphores?) when all you want to do is light up that string of RGB LEDs.
Don't be too quick to dismiss the "training wheels" of AVR.
While ARM and RISC-V may have beefier processors, and will probably be migrated to or adopted, AVR has a lot going for it.
First, a common AVR board like the Arduino Uno has built-in peripherals like a temperature sensor. You can build something out of the box without buying external items. If your target market is cheap educational, that's key.
Second, it offers basic microcontroller features like power management, external interrupts, serial and other stuff.
It provides a terrific cheap platform to understand managing interrupts and concurrency safely in your language of choice. Which can be applied to the bigger boards.
Finally, memory constraints require careful thinking through code. I'm not going to implement a full standard library on an Arduino AVR, so what do I bring with me?
This basically how all of arduino "works". It was quite shocking to see this after working on embedded systems in grad school that used interrupt driven microcontrollers for low power situations to see a while (true) in the core arduino main.c
edit: but it makes sense from the perspective of a education and hobbyist needs to take the simplest option that could work
Really appreciate you sharing this - I used to downplay the real-world application of these kinds of methods ("if you were making a real product you'd have a priority queue and a scheduler and so on") so if I ever teach kids again I'll let them know that people out there build real things like this.
I wonder if it's possible to just interleave statements from k different (loop) functions? Then it would seem mostly equivalent to an arduino at 1/k Clock speed.
I think it's not possible to interleave instructions per se (because of registers?), but a compiler should be able to figure out the correct instructions of statement interleaving.
You can do that, but it's not very useful in practice and really breaks down when you want to do something more compute intensive that needs all the registers/cache. Multicore MCUs like the ESP32 are pretty cheap ($2 1pc pricing) so if you really care about realtime performance it makes more sense it do realtime on one core and schedule slower work to be done on the other core.
Padauk makes them (or at least did), and calls them FPPAs. These are the same folks that brought you those infamous 3-cent-at-single-quantities OTP microcontrollers, so they're probably not for the faint of heart, but what you're asking for has been done.
Some of those 3 cent microcontrollers are actually barrel processors! They have multiple sets of registers (only two on the cheapest) and rotate through them with each clock. Useful for implementing an SPI or UART thread alongside the main task, for example.
The SDCC open source C compiler targets some of those chips now, too.
I’ve made some fun projects with Espruino - where you can literally use setTimeout/setInterval and it will even sleep the uC until it hits the the timeout :)
Embedded can be a tiny little msp430 with RAM measured in bytes, or it can be the latest and greatest intel flagship - it's a description of how the chip is used more than the chip type.
As CPUs get more powerful in general, so too do the little development boards. I can spend $10 on a board with multiple cores and megabytes of ram to drive my blinky lights and do wifi - that's enough power to run a full unix.
I’m a fairly loyal customer of adafruit. I really like their stuff and I’m not ashamed to say I really like circuit python. So much of embedded work is writing drivers and circuit python has many many already written and ready to go.
The Raspberry Pi foundation recently (about a 18 months ago) released their own dual-core ARM chip - the RP2040 - and a corresponding dev board - the Pi Pico. The Pico dev board runs about $4 plus shipping from authorized resellers.
Within the past few weeks, they introduced a wireless version - the Pi Pico W. It's about $6 from authorized resellers, though stock is hard to find at the moment.
They're looking at this from a higher level for defining the API and programming model people will use to write event-based code. An implementation might run using interrupts and a timer to drive an event loop on a single core, or it might scale itself out to run on multiple cores where available like they mention.
Not really, the processor has to be designed to support it that way to make it practical. With interrupts, execution can be interrupted at any point, and you have to be able to restore the registers to the correct state before letting the process continue from that point. It's technically doable, but with a chip that has only 128 bytes of RAM, you'll be pretty limited in how many processes you can run. Even Windows, prior to Win95, used the event loop style for multitasking.
Arduino's framework divides main() into two functions:
Setup() and loop(), but they're essentially the same as whatever comes before the while loop, and the while loop itself, in a standard embedded c main function.
Then you just have interrupts setting states, and the loop responds to the change in state.
It's not quite the same as a while loop, because when you return from the loop() function, how long it takes before it gets called again isn't defined. Using arduino-pico, for example, it does some USB I/O handling (when using TinyUSB). If you write an actual while loop and don't return from the loop function often enough, you might starve the USB port implementation.
This adds enough of a delay that I moved some code to the other core on the Pico to get precise timing.
That's a fair point. I admit, I moved away from the Arduino framework pretty rapidly after embedded development became my job, well before I had to contend with everything else under the hood.
I never ran into an issue of quasi-hidden behaviors like that, because I never pushed the thing that hard. It feels a little bit like the Arduino toolset (that is pre IDE 2.0) was designed to discourage people from pushing things that hard.
Well, it's behavior specific to one board, and perhaps to TinyUSB. But it looks like Arduino has a yield function [1] that you can use to run periodic tasks if you write your own loop. And Teensy calls yield() between calls to loop(). [2]
Since switching to VSCode and PlatformIO, I read the source more. (Arduino 1.x discouraged this due to not having easy ways to navigate between callers and callees.)
Interrupts work up to a point, but the correct way to do it is with an RTOS. Looking at the GitHub discussion it seems they are looking at mbed, FreeRTOS, or some other varieties too.
In Rust there is https://embassy.dev which is an async Rust framework for embedded. It has been shipping in products for years. Can be made real-time (for some definitions of it).
There is also RTIC https://rtic.rs which is a concurrency framework for real-time systems using interrupts. IIRC the car industry is interested in it.
There are several microcontroller architectures within the Arduino family officially as well as compatible ones that are also called Arduino-something.
The classic (and oldest) are the AVR microcontrollers (ATmega), which are not supported by embassy nor rtic at the moment AFAIK.
They are supported by Rust, though. see: https://github.com/rahix/avr-hal.
However, there are several other Arduino microcontrollers that are supported. For example the Arduino Nano RP2040, Arduino Nano 33 BLE (nRF), the Arduino MKR ones as well as the STM32duino (STM32F103, the one in the hugely popular $2 blue-pill boards) are all based on the ARM cortex-m architecture, which is supported by both embassy as well as rtic.
Forths such as FlashForth https://www.flashforth.com/index.html on the Arduino support multiple tasks .. in addition to including a compiler and interactive REPL
We've done quite a bit of experimentation with adding preemptive multitasking support to non-hard real time software running on MCUs at my current company.
After a lot of head banging and dead ends, I've come to the conclusion personally that the embedded community could really use an implementation of the POSIX threading APIs or some meaningful subset thereof for various platforms. They're already standardized, well understood/used, and aren't that hard to implement on an MCU.
Of course, there would probably be some semantics that wouldn't make sense to or may not be possible to implement on MCUs, and there would be work required to support different cores, but these tradeoffs seem better to me than reinventing the wheel and probably needing to make the same tradeoffs at some point down the line with a ground up new API.
For Arduino, exposing POSIX APIs wouldn't be very user-friendly. But wrapping something more user-friendly around them seems like a maintainable and extensible path for the project and community.
RTEMS has been providing a real time, multi threaded, POSIX API on embedded systems for decades. It's got preemption, interactive shells, a libbsd port with networking, etc. It's popular in spacecraft, especially in combination with NASA's Core Flight System (cFS). I've run it on an STM32F4 and a Raspberry Pi.
BSP support is a little lacking in some cases. The raspberry pi is currently crashing on a call to fflush(). No idea why.
I really wish more people knew about it. As a matter of fact, I'll post about it here.
async/await does not result in bloated code that is hard to maintain and debug.
It's a strange thing to say - async/await is now built in to most major programming languages - strange that Arduino would dismiss it so easily.
They are correct in that async/await does not solve multicore utilisation, but it's the most important and easiest way to implement parallelism on a single core.
TFA makes no mention of async/await, only asynchronous programming. Specifically, it says:
> The traditional way to do this is to write non-blocking code so that the loop() function can run as fast as possible, updating state variables and calling the millis() function to ensure proper timing
Indeed, that style of programming is nothing at all like async/await in, say, Javascript.
I see an opening for a very good set of tutorials on how to avoid all the pitfalls.
[edit] - Actually... they have some good points, I just worry about someone thinking they can just "sprinkle threading" into their code, and failing to understand that in doing so they change the laws of physics of the code.
I find that Rust-like rules (&T across threads and &mut T within a thread, restricting cross-thread mutation to &Mutex<T> and &AtomicT, global variables are treated as shared) are the best approach I've seen so far to general multithreading (as opposed to structured subsets like message passing or structured concurrency, which I haven't explored as much). However I'm unsure if mutexes are incompatible or unnecessary with the majority of single-core Arduinos with cooperative multitasking, and treating global data as shared is a pain on embedded and binding them to a specific thread is not a problem Rust has solved yet (you'll have to find your own solution).
I wonder if async Rust would be a good fit for this use case? It doesn't solve the accidental blocking problem (although async-std did experiment with blocking detection at one point), but you can get transparent support for multiple cores and a lot of thread safety.
Nice, but pointless addition to an old processor. ESP32's are cheap and readily accessible and they come with FreeRTOS running already.
If I absolutely need to multitask on a Mega328 Arduino, I use the protothreads library. But really, for anything heavy, I'll turn to an ESP32 or an STM32. Use the right tool for the job!
Yeah, the art students with miniscule coding experience who just want to get stuff done are probably going to be thrilled to work with FreeRTOS.
The truth is, that Arduino always has put in a lot of work to do things that could already be done only to make them more accessible, easier to use and thus more widely available.
If you think this is pointless you should consider this might say more about how far you have come in your own journey than about the usefulness of that new feature.
The art students with minimal coding experience would take FreeRTOS ten times out of ten over trying to figure out what a compare match timer is, what an interrupt is, how bitwise operations work, etc. The OP was dead-on and I'd bet money that the downvotes are from people that have never had somebody's eyes glaze over when they told them that they "just need to calculate their prescaler values, mask the appropriate bits into the config register, set the number of counts in the compare register, enable the timer interrupt, and register an interrupt vector to handle it".
In 2022 Arduino has several baords, many of which are NOT using atmega 328. Some of them have multi-core Arm chips, some of them have risc-v (not sure if they have any of those as multi-core though).
There are already people running FreeRTOS on those.
Not sure why you would dismiss multitasking on those as "useless".
Even on an AVR, you can implement lightweight cooperative scheduling with low resource consumption. That gives you many of the benefits of an RTOS with less risk of creating heisenbugs. Arduino should have broken free of its main-loop architecture long ago.
If I had to imagine a solution, it would be to simply have an easy way to enter parameters to the JS-equivalent of `setTimeout` and `setInterval`.