Debian Linux Operating System for Qualcomm QRB2210 MPU and Zephyr RTOS for STM32U585 MCU of the Arduino UNO Q Development Board

Currently, I am investigating why the Debian Linux OS is needed to enable the QRB2210 MPU to turn on LED1R. Similarly, I am exploring why the Zephyr RTOS is needed on the STM32U585 MCU to execute the statement digitalWrite(LED_BUILTIN, HIGH); to turn on the onboard LED (LED3R), when the same task can be performed on the Arduino UNO R3 without using any operating system.

Ultimately, the fundamental question is: What is an operating system, and what does it do? ChatGPT and Gemini provide answers, but those answers may not fully satisfy students who want to visualize the actual purpose of an operating system in the UNO Q. Such concepts can be explained with greater clarity and simplicity by experienced forum members who can relate them to practical examples.

1. Do I need or not an operating system for UNO R3?

In UNO R3, I have the following application code and the single core 328P MCU to turn on LED_BUILTIN connected at DPin-13:

digitalWrie(13, HIGH);
==>
0x004C - 9a2d

(1) The above code is loaded into flash.
(2) PC of the MCU is loaded with 0x004C.
(3) The MCU itself is collecting the instruction (opcode+operand) from flash.
(4) Sequence generator of the MCU generates the requied timing functions that directly asserts HIGH on Bit-5 of PORTB; as a result, the LED_BUILTIN is turned on.

In this case, I do not see, nor do I think I need, the presence of any supervisory software to oversee the activities of the MCU. If we define such supervisory software as an operating system (OS) in the simplest sense, then I would say that the Arduino UNO R3 does not require an OS to execute its application program and control its hardware.

Now, if I assign the MCU of the UNO R3 two simultaneous tasks of blinking two LEDS s at different rates, then there is a need of a supervisory software to allocate service time for the two tasks and also to command the MCU to switch from one tasks to another task. The said multi-tasking can be caried out by including the Arduino_FreeRTOS.h Librray in the skstec. Is this Library software be called a Scheduler or a Rudimentary Operating System?

Then what are the constituent parts of an Operating System? This is the answer from AI:

Operating System
β”‚
β”œβ”€β”€ Scheduler
β”œβ”€β”€ Memory Manager
β”œβ”€β”€ Device Drivers
β”œβ”€β”€ Interrupt Management
β”œβ”€β”€ Timer Services
β”œβ”€β”€ Synchronization (Mutexes, Semaphores)
β”œβ”€β”€ Communication Services
└── Other System Services

The scheduler is only one part of the OS.

he OS handles multi-tasks and multi-users; it also resolves conflicting situation when two or more tasks compete to access a common resource like Serial Monitor at the same time.

FreeRTOS is a complete operating system that is running in dual-core ESP32 Development Kit. Also, Zephyr RTOS is also a complete operating system that is running in the MCU of UNO Q Development Board.

So, what is an Operating System?
An operating system (OS) is a complex software system that is stored on persistent storage (such as a hard disk or eMMC) and loaded into RAM during the boot process. Once loaded, it continues running as long as the computer is powered on. Working together, the OS and the MPU manage the efficient execution of one or more user application programs, while also handling various background tasks and system resources.

Simple answers: They don't have to have, these Operating systems... You could in theory you could program these boards to run on bare metal.

First lets take the MPU - You have 4 cores to deal with, plus a whole bunch of other stuff. You learning curve and programming task would be rather large... Similar to why does an RPI run using an OS, which could be RaspberryPI Os or Ubuntu, ...

Plus as it is setup as an OS, it allows you to run multiple programs, command prompts, etc.

Now as for the MCU, the STM32, one could write their own core code base, like the UNO R3. In fact there are more powerful boards out there that do, Example PJRC Teensy 4.x board, which use IMXRT1060 processors, that typically run at 600mhz and can be pushed higher, which does not typically use an OS (or MicroKernel). That is the code base that you install when you install on Arduino, through the board manager), does not have things like, threads, or semaphores, or ... You want to touch the registers, go for it...
If your curious, the core software for these boards is located at:
PaulStoffregen/cores: Teensy Core Libraries for Arduino

Why did Arduino decide to use Zephyr? We should back up that question, to why is Arduino layering their stuff on top of something else? Before Zephyr, Arduino developed several of their boards on top of MBED by ARM ( Mbed OS | Mbed )
As you can see in my boards list:

And the Uno R4 and some others were built on top of some Renesas code base.

Why the change now to Zephyr: The ARM group is discontinuing MBED and so Arduino is moving over to Zephyr...

Why are they building using Zephyr? There is a lot of features in it, which are nice, things like threads, semaphores, events, common drivers for things like GPIO, USARTS, SPI, Wire, Cameras, ... Plus there is a lot of people developing code for Zephyr that one can leverage, like drivers for lots of different things.

There are also lots of different boards that have at least some support on Zephyr
Supported Boards and Shields β€” Zephyr Project Documentation

So Arduino is in the process of converting the boards from MBED to Zephyr and some new boards are being developed for it. Current list I have


And I know there are others that are being worked on, but not currently released into the beta...

Note: For most of these boards you can now also develop directly using Zephyr without any Arduino code. I am not saying that none of the code is by the Arduino Developers, as they are making lots of Pull Requests into the Zephyr code base.

I program some of the time directly using Zephyr. Why? I have contributed some code changes and I currently have one active Pull Request. You should probably try learning some more about how Zephyr works, things like Device Trees, and configuration files, etc.

One of the main ideas is you can write your app, to run on your board, with all of the hardware pre-configured at program load time. That is you can define how each pin is defined, how each SPI object that you use is defined, etc...)

But again there is nothing keeping you from actually touching the registers of the STM32 board.

For example in some sketches I have code like:


void set_pin_moder(GPIO_TypeDef *port, uint8_t pin, uint8_t pin_mode) {
  // Set the MODER = could be done in fewer steps
  uint32_t moder = port->MODER;
  uint32_t mask = ~(0x3 << (pin * 2));
  moder = (moder & mask) | (pin_mode << (pin * 2));
  port->MODER = moder;
}

void set_gpio_pin_mode(GPIO_TypeDef *port, uint8_t pin, uint8_t af) {
  // Set the MODER = could be done in fewer steps
  uint32_t moder = port->MODER;
  uint32_t mask = ~(0x3 << (pin * 2));
  moder = (moder & mask) | (0x2 << (pin * 2));
  port->MODER = moder;

  if (pin < 8) {
    port->AFR[0] = port->AFR[0] & ~(0xf << (pin * 4)) | (af << (pin * 4));
  } else {
    pin -= 8;
    port->AFR[1] = port->AFR[1] & ~(0xf << (pin * 4)) | (af << (pin * 4));
  }
}

void print_gpio_regs(const char *name, GPIO_TypeDef *port) {
  //printk("GPIO%s(%p) %08X %08X %08x\n", name, port, port->MODER, port->AFR[0], port->AFR[1]);
  Serial.print("GPIO");
  Serial.print(name);
  Serial.print(" ");
  uint32_t moder = port->MODER;
  Serial.print(moder, HEX);
  Serial.print(" : ");
  for (uint8_t i = 0; i < 16; i++) {
    switch (moder & 0xC0000000) {
      case 0x00000000ul: Serial.print("I"); break;
      case 0x40000000ul: Serial.print("O"); break;
      case 0x80000000ul: Serial.print("F"); break;
      default: Serial.print("A"); break;
    }
    moder <<= 2;
  }
  Serial.print(" ");
  Serial.print(port->AFR[0], HEX);
  Serial.print(" ");
  Serial.print(port->AFR[1], HEX);
  Serial.print(" ");
  Serial.print(port->IDR, HEX);
  Serial.print(" ");
  Serial.print(port->ODR, HEX);
  Serial.print(" ");
  uint32_t pupdr = port->PUPDR;
  Serial.print(pupdr, HEX);
  Serial.print(" : ");
  for (uint8_t i = 0; i < 16; i++) {
    switch (pupdr & 0xC0000000) {
      case 0x00000000ul: Serial.print("-"); break;
      case 0x40000000ul: Serial.print("U"); break;
      case 0x80000000ul: Serial.print("D"); break;
      default: Serial.print("?"); break;
    }
    pupdr <<= 2;
  }
  Serial.println();
}

void show_all_gpio_regs() {
  print_gpio_regs("A", (GPIO_TypeDef *)GPIOA_BASE);
  print_gpio_regs("B", (GPIO_TypeDef *)GPIOB_BASE);
  print_gpio_regs("C", (GPIO_TypeDef *)GPIOC_BASE);
  print_gpio_regs("D", (GPIO_TypeDef *)GPIOD_BASE);
  print_gpio_regs("E", (GPIO_TypeDef *)GPIOE_BASE);
  print_gpio_regs("F", (GPIO_TypeDef *)GPIOF_BASE);
  print_gpio_regs("G", (GPIO_TypeDef *)GPIOG_BASE);
  print_gpio_regs("H", (GPIO_TypeDef *)GPIOH_BASE);
  print_gpio_regs("I", (GPIO_TypeDef *)GPIOI_BASE);
}

Where I set the MODE of specific pins like:

void loop() {
  // put your main code here, to run repeatedly:
  if (Serial.available()) {
    while (Serial.available()) {
      Serial.read();
      delay(200);
    }
    // force Analog mode PC0-1
    set_pin_moder(GPIOC, 0, 3);
    set_pin_moder(GPIOC, 1, 3);
    print_gpio_regs("C", (GPIO_TypeDef *)GPIOC_BASE);
  }
  Serial.print(analogRead(A4));
  Serial.print(" ");
  Serial.println(analogRead(A5));
  delay(1000);
}

I am not sure if this is the information you are wanting or not...

I am grateful to have received guidance from an experienced human being. You have shared a wealth of information. So far, I have only had time to skim through it, but I plan to study it carefully and come back with meaningful questions.

Thank you very much for taking the time to educate me. You have done your part exceptionally well; now it is my responsibility to study the material and make the best use of it by extracting what I am looking for.

You have everything currently ready to use due to manually installing the complete platform from the repository, but for those only using Boards Manager installations of the platform, it is worth mentioning that additional boards are accessed by installing the "Zephyr Community Boards (BETA)" platform in addition to the "Arduino UNO Q Board" and "Arduino Zephyr Boards (BETA)" platforms.

These are all implemented in the codebase hosted in the arduino/ArduinoCore-zephyr repository, but we split it into distinct families when publishing the release platforms to Boards Manager in order to avoid bloat for users who only need to add support for specific boards. I'm guessing the "Arduino Zephyr Boards (BETA)" platform will end up being split up further into separate platforms for each board family once it is promoted out of the beta development state. This is what was done for the Mbed OS-based platforms.

@KurtE

Referring to your post #3, if I have understood properly . Seeking advance apology for placing queries that do not make sense.

1. I ack that the MCU like STM32 can be programmed without the need of Zephyr RTOS. In UNO Q, does this OS come pre-installed in the flash of the MCU or is it fused in the flash during boot process? Can it be disabled?

2. When I execute digitalWrite(LED_BUILLTIN, HIGH) in UNO Q, does the execution of this code require the assistance of the Zephyr RTOS? If yes, how? This is my curiosity to know.

3. You may post a simple, short, and complete arduino style sketch using pure Zephyr programming constructs to blink LED_BUILTIN at 1 sec interval. This example will show me the route of learning Zephyr based programming for the MCU.

Thank you for the kind attention.

This is an answer for 2 done with Google Gemini:

  • cores/arduino/wiring_digital.cpp:
    • user functions: digitalWrite, ...
  • variants/arduino_uno_q_stm32u585xx/llext-edk/include/zephyr/include/zephyr/drivers/gpio.h :
    • user functions: gpio_ * (not ends with "_raw" or not starts with __syscall)
    • kernel functions: __syscall gpio_*_raw, z_impl_gpio_*_raw
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                          [ USER SPACE ]                                β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  β–  1. Application Layer (User Sketch & Libraries)                      β”‚
β”‚     └─ Sketch (setup / loop) ──> Arduino_RouterBridge                  β”‚
β”‚                                                                        β”‚
β”‚  β–  2. Hardware Abstraction Layer (HAL / Arduino API Layer)             β”‚
β”‚     └─ digitalWrite(pin, status) [Loaded into SRAM]                    β”‚
β”‚           └─ gpio_pin_set_dt()   [static inline]                       β”‚
β”‚                 └─ gpio_pin_set()    [static inline / Inversion Logics]β”‚
β”‚                       └─ gpio_pin_set_raw() [static inline / Bitmask]  β”‚
β”‚                                                                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                    β”‚
          [ Triggered via __syscall : System Call Gate (Stub) ]
          - Context Switch: CPU Mode (User Mode ──> Supervisor Mode)
          - Kernel Validation: Verifies parameters & access rights (MPU)
                                    β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         [ KERNEL SPACE ]                               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  β–  3. OS Core & Subsystem Layer                                        β”‚
β”‚     └─ Syscall Dispatcher (Receives the System Call)                   β”‚
β”‚           └─ z_impl_gpio_port_set_bits_raw()  [static inline]          β”‚
β”‚           └─ z_impl_gpio_port_clear_bits_raw() [static inline]         β”‚
β”‚                 β”‚                                                      β”‚
β”‚                 β–Ό (TRACING: Logs Driver Entry/Exit Timeline)           β”‚
β”‚                                                                        β”‚
β”‚  β–  4. Device / Driver Layer                                            β”‚
β”‚     └─ Zephyr GPIO Driver API (api->port_set_bits_raw)                 β”‚
β”‚           └─ SoC Specific Driver                                       β”‚
β”‚                                                                        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                     [ HARDWARE (Physical Chip) ]                       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  β–  5. MCU Physical Registers                                           β”‚
β”‚     └─ PODR (Port Output Data Register) Direct Manipulation ──> Output β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

What would be the textual description of the above as to how the high level digitalWrite(LED_BUILTIN, HIGH)calls upon te device driver of Zephyr which actvates the MCU register to turn on the LED_BUILTIN?

Digital pin number will be converted into the port and bit position number and the value HIGH will be called with gpio_port_set_bits(...). If LOW gpio_port_clear_bits(...) function.

ChatGPT has given the following description/answer. @KurtE may moderate/criticize it for our correct/better understanding:

This is a very good question. The answer depends on how the Arduino core for UNO Q is implemented.

On the UNO R3, the execution path is approximately:


Your Sketch
     |
digitalWrite()
     |
Arduino Core
     |
Write to GPIO Register
     |
LED

There is no operating system involved.


On the UNO Q (STM32U585 MCU), the path is more like:


Your Sketch
     |
digitalWrite(LED_BUILTIN, HIGH)
     |
Arduino Core for Zephyr
     |
Zephyr GPIO API
     |
STM32 GPIO Driver
     |
STM32 GPIO Hardware
     |
LED_BUILTIN

Here, Zephyr RTOS provides the GPIO driver that the Arduino core uses to access the hardware.

What role does Zephyr play?

When your sketch executes


digitalWrite(LED_BUILTIN, HIGH);

your sketch is not directly writing to the STM32U585 GPIO register.

Instead:

  1. The Arduino core translates LED_BUILTIN into the correct GPIO port and pin.
  2. It calls the Zephyr GPIO API (for example, gpio_pin_set_dt() or a similar internal function).
  3. Zephyr checks that the GPIO device has been initialized.
  4. Zephyr calls the STM32 GPIO driver.
  5. The driver writes to the STM32 GPIO registers.
  6. The LED turns on.

Does Zephyr's scheduler participate?

For a simple digitalWrite(), usually not.

The scheduler's job is to switch between threads. A GPIO write is normally just a function call that executes in the context of the current thread.

So, during


digitalWrite(LED_BUILTIN, HIGH);

the scheduler is not actively involved, unless:

  • another thread preempts the current one,
  • an interrupt occurs, or
  • the code blocks and yields the CPU.

Why use Zephyr at all?

The advantage is that the same Arduino code can work on many different boards.

Your sketch calls:


digitalWrite(LED_BUILTIN, HIGH);

without needing to know:

  • which STM32 GPIO register to modify,
  • which clock to enable,
  • or which alternate-function settings are required.

The Arduino core relies on Zephyr's hardware abstraction and device drivers to handle those hardware-specific details.


A useful analogy for your students

Think of the system as follows:


Your Sketch        ← Gives the command
      β”‚
Arduino Core       ← Translates Arduino functions
      β”‚
Zephyr RTOS        ← Provides hardware services and drivers
      β”‚
STM32 Driver       ← Knows the STM32 registers
      β”‚
GPIO Hardware      ← Changes the output voltage
      β”‚
LED

An interesting point to investigate

From your earlier questions, you are asking whether Zephyr is truly "necessary" for digitalWrite().

Architecturally, no. The STM32U585 can control its GPIO pins without an RTOS; you could write bare-metal code that accesses the GPIO registers directly, just as on the UNO R3. In the UNO Q software stack, however, the Arduino core is built on top of Zephyr, so digitalWrite() uses Zephyr's GPIO framework rather than accessing the registers directly. This design provides portability, consistent driver interfaces, and access to Zephyr's other services when needed.

Sometimes you might be overthinking somethings...

With all things like this, sometimes easier to look at the sources.
The main Arduino sources are up at:
arduino/ArduinoCore-zephyr: Arduino Core based on Zephyr+llext

They are also already on your machine, under where Arduino15 is on your machine
On mine right now:
C:\Users\kurte\AppData\Local\Arduino15\packages\arduino\hardware\zephyr\0.56.0
In the cores\arduino\wiring_digital.cpp you see:

void digitalWrite(pin_size_t pinNumber, PinStatus status) {
	RETURN_ON_INVALID_PIN(pinNumber);

	gpio_pin_set_dt(&arduino_pins[pinNumber], status);
}

So it is taking the pin number passed in and looking it up in a table to find the
PORT and PIN of it...

This table is created by Zehpyr magic from the device tree, in wiring_private.h

constexpr struct gpio_dt_spec arduino_pins[] = {
	DT_FOREACH_PROP_ELEM_SEP(
	DT_PATH(zephyr_user), digital_pin_gpios, GPIO_DT_SPEC_GET_BY_IDX, (, ))};

If you wish to know how the gpio_pin_set_dt works, you can see reference to it in the documentation:
General-Purpose Input/Output (GPIO) β€” Zephyr Project Documentation

Which if you go to the actual API:
Zephyr API Documentation: GPIO
It shows that the call:
gpio_pin_set_dt(&arduino_pins[pinNumber], status);

is the same as:


gpio_pin_set(&arduino_pins[pinNumber].port, &arduino_pins[pinNumber].pin, status);

The main header file for Zephyr GPIO is included in the Arduino15 code base under the variant for each board: like:
...Arduino15\packages\arduino\hardware\zephyr\0.56.0\variants\arduino_uno_q_stm32u585xx\llext-edk\include\zephyr\include\zephyr\drivers\gpio.h

Where you will find that it does a few checks and then calls off to the gpio_pin_set_raw
...
Where it finally calls off to the STM32 HAL (Hardware Abstraction Layer) which sets or clears the appropriate bit in the correct register for the GPIO port...

Note: I have/had code that I did it directly on UNOQ, but I may not have had it stored on computer when I fried the one Q...

Note: post 3 has some code for allowing you to directly set which mode each pin is used by using Port/Pin number...

But I have a header file I was using with Arduino GIGA and Portenta H7 on MBED, that I could easily adapt:
Arduino_GIGA-stuff/libraries/GIGA_digitalWriteFast at main Β· KurtE/Arduino_GIGA-stuff

For example has:

#ifndef _GIGA_DIGITALWRITEFAST_H_
#define _GIGA_DIGITALWRITEFAST_H_
#include <Arduino.h>
#include "pinDefinitions.h"
#if !defined(ARDUINO_GIGA) && !defined(ARDUINO_PORTENTA_H7_M7)

#error "Only works on Arduino GIGA or Portenta H7 boards"
#endif


static  GPIO_TypeDef * const port_table[] = { GPIOA, GPIOB, GPIOC, GPIOD, GPIOE, GPIOF, GPIOG, GPIOH, GPIOI, GPIOJ, GPIOK };
static const uint16_t mask_table[] = { 1 << 0, 1 << 1, 1 << 2, 1 << 3, 1 << 4, 1 << 5, 1 << 6, 1 << 7,
                                       1 << 8, 1 << 9, 1 << 10, 1 << 11, 1 << 12, 1 << 13, 1 << 14, 1 << 15 };

// quick and dirty digitalWriteFast

// Sets the state of an IO pin
// Two versions - this version you takes in a pin number as pin_size_t type - uint8_t
static inline void digitalWriteFast(pin_size_t pin, PinStatus val) __attribute__((always_inline, unused));
static inline void digitalWriteFast(pin_size_t pin, PinStatus val) {
  PinName pin_name = g_APinDescription[pin].name;
  uint16_t mask = mask_table[pin_name & 0xf];
  GPIO_TypeDef  * const port = port_table[pin_name >> 4];
  if (val) port->BSRR = mask;
  else port->BSRR = (uint32_t)(mask << 16);
}

// This version you takes in a pin name (PinName) like LED_RED
static inline void digitalWriteFast(PinName pin_name, PinStatus val) __attribute__((always_inline, unused));
static inline void digitalWriteFast(PinName pin_name, PinStatus val) {
  uint16_t mask = mask_table[pin_name & 0xf];
  GPIO_TypeDef  * const port = port_table[pin_name >> 4];
  if (val) port->BSRR = mask;
  else port->BSRR = (uint32_t)(mask << 16);
}
...

The first one would require work, to actually pass in the pin number and map to PORT/Pin. I had code that simply duplicated that zephyr magic to create the same table...
or simply use it... Before it was marked private so duplicated.

For names, I duplicated a table like that was used in MBED, like:

typedef enum {
    PA_0=0x00, PA_1, PA_2, PA_3, PA_4, PA_5, PA_6, PA_7, PA_8, PA_9, PA_10, PA_11, PA_12, PA_13, PA_14, PA_15,
    PB_0=0x10, PB_1, PB_2, PB_3, PB_4, PB_5, PB_6, PB_7, PB_8, PB_9, PB_10, PB_11, PB_12, PB_13, PB_14, PB_15,
    PC_0=0x20, PC_1, PC_2, PC_3, PC_4, PC_5, PC_6, PC_7, PC_8, PC_9, PC_10, PC_11, PC_12, PC_13, PC_14, PC_15,
    PD_0=0x30, PD_1, PD_2, PD_3, PD_4, PD_5, PD_6, PD_7, PD_8, PD_9, PD_10, PD_11, PD_12, PD_13, PD_14, PD_15,
    PE_0=0x40, PE_1, PE_2, PE_3, PE_4, PE_5, PE_6, PE_7, PE_8, PE_9, PE_10, PE_11, PE_12, PE_13, PE_14, PE_15,
    PF_0=0x50, PF_1, PF_2, PF_3, PF_4, PF_5, PF_6, PF_7, PF_8, PF_9, PF_10, PF_11, PF_12, PF_13, PF_14, PF_15,
    PG_0=0x60, PG_1, PG_2, PG_3, PG_4, PG_5, PG_6, PG_7, PG_8, PG_9, PG_10, PG_11, PG_12, PG_13, PG_14, PG_15,
    PH_0=0x70, PH_1, PH_2, PH_3, PH_4, PH_5, PH_6, PH_7, PH_8, PH_9, PH_10, PH_11, PH_12, PH_13, PH_14, PH_15,
    PI_0=0x80, PI_1, PI_2, PI_3, PI_4, PI_5, PI_6, PI_7, PI_8, PI_9, PI_10, PI_11, PI_12, PI_13, PI_14, PI_15,
    PJ_0=0x90, PJ_1, PJ_2, PJ_3, PJ_4, PJ_5, PJ_6, PJ_7, PJ_8, PJ_9, PJ_10, PJ_11, PJ_12, PJ_13, PJ_14, PJ_15,
    PK_0=0xA0, PK_1, PK_2, PK_3, PK_4, PK_5, PK_6, PK_7
} PinNames;

I don't remember if PinStatus is defined or not, but not hard to define or simply
use defines and the like.

Was also pointed out you don't need the mask table, it should reduce to about the same code like:

static inline void digitalWriteFast(PinName pin_name, PinStatus val) {
  uint16_t mask = 1 << (pin_name & 0xf);
  GPIO_TypeDef  * const port = port_table[pin_name >> 4];
  if (val) port->BSRR = mask;
  else port->BSRR = (uint32_t)(mask << 16);
}

As you probably can see in the PinNames, the port/Pin are encoded into the number...

As for how to do delay()...
Several options depending on things like accuracy...
Like a for loop that simply does nops...

On some boards there is a cycle counter register often part of debug... But I don't think this one has one...

You could setup some timer, that interrupts every millisecond and keeps a counter...
and then delay simply waits until the counter gets to where you need it...

Probably enough for one message.

This is the stuff I was expecting to have exposure. I hope that I will be able to figure out how the digitalWrite() code, the Zephyr API, and the STM32 IO driver are interrelated.

Thank you,