Arduino Uno Q: The Raspberry Pi + STM32 Fusion?

The new Arduino Uno Q combines the best of Raspberry Pi-like MPUs and STM32 MCUs!

This brand new device is the Arduino Uno Q, a single board computer released just some months ago. It marks a wind of change for Arduino, as this isn't just Arduino's first device with a co-processor architecture, but also the first release under the new company acquisition by Qualcomm. This SBC aims to rival the Raspberry Pi and its relatives and even venture beyond them, as it tries to combine the best of two worlds: high level programming for machine learning and UI rendering within a powerful Dragonwing chip (MPU) running a Linux distribution and real-time capabilities and full hardware control within an ultra-low-power STM32 chip (MCU). But enough marketing talk. Will this device keep its promises? Is this the Raspberry Pi killer? Will this thing steal your job and will you pay 2.99$ a month for your blinky sketch now? Let's find out.

unoq

This guide is split into 3 parts!

Disclaimer

This is a very new product and its software components will receive updates that will cause this guide to have more and more mismatches with the actual state of the software at the time of you viewing it. Be careful and always consult the official documentation first! However, hardware will remain as it is and standard tools like that of the Linux kernel are unlikely to change drastically.

First impressions

The board is shipped in the classic minimalist Arduino packaging. Some cardboard in a saturated teal, a sticker, a small foldable pamphlet and of course the Uno Q in the exact form factor of the classic Uno. Nothing out of the ordinary for now, although my girlfriend immediately pointed out that the coloring of the board is different from the packaging which is something that never occurred to me and is indeed odd. It features the QRB2210 MPU along with memory, USB connection, JTAG pins, LED matrix and standard Arduino headers on top and the STM32U5, EMMC, media bus and miscellaneous headers on the bottom. Scanning the QR code on the box leads to the product page, which also leads to the getting started guide. From this point on, we'll mainly talk software.

Official workflow

Arduino wants you to use their "Arduino App Lab" program, which establishes a wired or network connection to your Uno Q and manages your projects. To access it, you first have to install it and then connect your Uno Q. It will be known to the program and you can configure a keyboard layout and WiFi connection. Initially, you have to connect it to the network, whether you need that or not. After that, you can set a password for your Linux system, which then leads you to a fresh Debian install on your MPU and a nice selection of examples in the App Lab.

app_lab

These examples may contain python code and C++ code, found in the folders python/ and sketch/ respectively. When running an example, the MPU compiles the C++ code and flashes it to the MCU, which executes the sketch and sends or receives data from the corresponding python program which was also started on the MPU during flashing.

This is useful for acquiring sensor data and translating it into a useful format, which can then be picked up by the MPU to perform demanding tasks such as UI rendering or machine learning. At least, that's how its supposed to work.

Initial issues

When I set this up, the network config lead to a pop-up screen with warnings about update issues. It instructed me to get the flasher tool to write a new Debian to the MPU.

So I obtained that from their website and connected a jumper on the JCTL pins (two pins furthest away from USB plug) and power-cycled the device as the official flashing guide instructed and ran the tool as command line program with

./arduino-flasher-cli flash latest

This then downloads the Debian image and flashes it to the MPU via USB. A successful flash should appear as such:

We should be back at ground zero again, but the Lab App then displayed the same error. Closing and opening the app seemed to do the trick and the updates could now be located. It seems that the App Lab may need some more time to deliver the best user experience. If you now install updates, the App Lab should display this:

Interestingly, the process revealed that Docker is involved and installed on the MPU, which I did not expect:

Only after this restart was I able to enter my chosen Linux credentials for the first time and view the example page and get started.

Project structure

The projects consist of a python program (high level stuff on the QRB2210) and/or an Arduino sketch (low level C++ on the STM32). The sketches are as you know them, although now there's an extra library called "Arduino Bridge". If you check out one of their examples that uses both the MPU and MCU, you can see that the Python program may then reference the registered functions within that bridge, essentially establishing a connection between them. At first glance, this seems like magic. If we take a closer look at the datasheet, we can see that the MPU and MCU are connected over UART and SPI. The SW D bus and reset line explain how the MCU is flashed or even debugged by the MPU. We'll come back to that. This means that this bridge will send data over the mentioned data buses, which have their own strengths and weaknesses depending on the kind of data that is sent. Here's what they are for:

  • SWD: Flashing a new program and debugging
  • UART: Low data rate messages or non-deterministic calls
  • SPI: High data rate messages


However, this would only work for a standard sketch for sending data from the MCU's perspective, similar to what the classic Serial.write() does. But these example projects are able to steer the MCU with calls from python, so what's happening? Besides some parsing logic, the MCU is equipped with an operating system as well: Zephyr OS. This RTOS introduces a task scheduler and has similar qualities to FreeRTOS. There's some framework code present that also handles accepting messages from the MPU. This essentially means that you are missing out on half the functionality of this device when you code a "classic" sketch without the bridge; the MPU may flash, start and stop your program, but doesn't do anything useful for your application if not interfaced with the bridge. The examples show a clear trend towards cloud and purely python-based applications, diminishing the importance of the MCU, which I personally find a bit ironic for a device called "Arduino", but who cares about opinions, let's keep talking software.

Basic Usage

"Basic usage" in this case refers to anything using the Arduino App Lab. It acts as a complete IDE, handling connection to the Uno Q, program building, uploading and debugging on the MPU.

Using an example

The examples page is the first page visible once you open the App Lab. In all good tradition we should start with the provided blinky example. If you launch the app, the process will show some information on the output console and after the app has started, you should see both the serial terminal and a python console pop up as tabs. In this example, the serial terminal will be empty because nothing is being sent. This example demonstrates the Arduino bridge, as the sketch no longer controls the blinking by itself, but rather the python script. It controls the timing and then sends instructions to the MCU to turn the LED on or off.

You can test various other examples, but keep in mind that some of them are very high level and require additional hardware such as a camera. You should also note that some of these examples are abstracted to the moon and back, as you can imagine with examples such as "facial recognition". Some of them don't even use the MCU and stay on the MPU entirely.

Advanced Usage with the CLI

You can use the Arduino Uno Q without the App Lab being open, by utilizing the arduino-app-cli. The App Lab is essentially just a fancy UI wrapper for calls to this interface. It handles project configuration, building and flashing firmware, starting python and monitoring the program.

You can connect to your MPU by using the Android Debug Bridge, a piece of software handling terminal communication over USB between your host machine and a Linux device. You can install it with sudo apt install adb and then, with your Arduino connected via USB, start a shell with adb shell. Your terminal will then have its session in the MPU's system. In here, we have access to the built-in arduino-app-cli. We can test it and look for existing examples with

arduino-app-cli app list

which prints all examples to the screen. This is the CLI version of the example page on the App Lab. To start an existing app, use

arduino-app-cli app start examples:blink

The docs on this CLI are quite good and I don't see any use in repeating them ad verbatim here, so check them out if you are interested!

Wicked Usage: Hacking the App Lab

So far we required one of either things to run a program: the App Lab or the arduino-app-cli program. What if we wanted to use established frameworks or custom build chains that have nothing to do with the business logic inside the Uno Q framework? We can already reach the MPU easily with either adb or ssh and are met with a derivative of Debian, so it's already as open as working with a Raspberry Pi. But what about the MCU? We already know that the STM32U5 is connected via the SWD bus to the MPU, so we should be able to flash a target-specific firmware compilation using these established buses. This leads to an issue that most makers don't usually face: In a classic workflow, we use a build and upload chain that compiles and flashes in one go; a process, that is usually hidden under some all-in-one script or fancy button, handling compilation, linking, connecting to a USB device, sending data packets in the right format for the bootloader on the MCU and finishing the transaction. In this case, there's no fancy button or even USB device, because the MPU interfaces the MCU as directly as possible over pins on the same copper.

Turning the MPU into a debugger

We now need a way to turn the MPU into a debugger and the MCU into a debugee, a term I stole from this guide called "Brushing Up on Hardware Hacking Part 3 - SWD and OpenOCD ", in which Matthew Alt hacks his toothbrush. Absolute cinema.

In his guide we can see how openOCD can be used to turn a Raspberry Pi into a debugger, so doing the same shouldn't be too complicated for the Qualcomm MPU. The crucial part are the hardware pins of the SWD bus. These can be defined in a new file QRB2210_swd.cfg, in a folder such as ~/custom_flash/:

adapter driver linuxgpiod
adapter gpio swclk -chip 1 26
adapter gpio swdio -chip 1 25
adapter gpio srst -chip 1 38
transport select swd
adapter speed 1000
reset_config srst_only srst_push_pull

The exact pins can be found in the schematic of the Arduino Uno Q. If we quickly take a look back at the App Lab's flashing process, we can see that it is already using openOCD under the hood, telling us that an openOCD binary is already present on the device.

We can now test the SWD connection with the following command:

/opt/openocd/bin/openocd -s /opt/openocd/share/openocd/scripts -s /opt/openocd -f /home/arduino/custom_flash/QRB2210_swd.cfg -f /opt/openocd/stm32u5x.cfg

This should cause a successful connection to our "debugger" now waiting for commands. We don't necessarily want to debug yet, we want to flash firmware, so let's get some of that. There are many ways to achieve this, we just need the right libraries and compiler. For example, you can compile firmware on a local machine with arm-gcc-none-eabi, the STM32CubeIDE or PlatformIO and then transfer the files to your MPU with adb or scp (SSH), see the repo for details. So let's configure that, whip out the classic blinky sketch for testing and build the firmware. Of course you will need the exact MCU type, in this case the STM32U585 family.

Flashing custom firmware with openOCD

Many ways lead to Rome and a compiled firmware, but now, with your .elf file in one hand and openOCD in the other, let's flash:

/opt/openocd/bin/openocd -s /opt/openocd/share/openocd/scripts -s /opt/openocd -f /home/arduino/custom_flash/QRB2210_swd.cfg -f /opt/openocd/stm32u5x.cfg -c "program /home/arduino/custom_flash/uno_q_stm.elf verify reset exit"

This command tells openOCD to not only start and use our configurations, but to also flash the firmware. After this, we should see the ongoing flashing process. You should now see the LED blink, which is basically the same as before, but now without the App Lab and any other middle-ware. We returned to bare metal and fully control the entire firmware of the MCU. This also means that we completely wiped Zephyr OS and any Arduino Bridge handling code.

Using PlatformIO remote for easier deployment

If you don't fancy calling a file transfer with adb or scp every time you build your project, you can use PlatformIO remote to build locally and automatically flash to the target MPU over PlatformIO's cloud service. For this, you need a PlatformIO account. It's free and you get multiple hours of remote runtime per day. Install PlatformIO locally, for example as VSCode extension and install the PlatformIO core CLI on the MPU. You can then login on both devices with

pio account login

After that, you can launch a remote agent on the MPU:

pio remote agent start

To check if that worked, you can poll for running agents on your local machine:

pio remote agent list

This should then list your Arduino Uno Q as running remote agent. You can now navigate to my example repo and from there call

pio remote run -t upload

This will build the demo program of the repo located in src/main.cpp and send it to the MPU, where it will be flashed to the MCU. This command may install dependencies on the MPU that PlatformIO needs for the flashing process.

Custom data connection from MPU to MCU

Now let's use the connected LPUART (low power UART) bus to send and receive serial messages. According to the datasheet, the connected UART is LPUART1, which in turn is connected to the MCU pins PG7 (TX) / PG8 (RX) and the MPU pins GPIO_71 (TX) / GPIO_80 (RX). This peripheral sadly isn't well integrated into the Arduino Framework for STM32 MCUs, so we have to fall back to the STM32 HAL to activate it in the firmware:

#include <Arduino.h>
#include "stm32u5xx_hal.h"

UART_HandleTypeDef hlpuart1;

void error_handler(uint32_t speed);

void SystemClock_Config(void) {
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
    if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE4) != HAL_OK){
        error_handler(1);
    }
    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_MSI;
    RCC_OscInitStruct.MSIState = RCC_MSI_ON;
    RCC_OscInitStruct.MSICalibrationValue = RCC_MSICALIBRATION_DEFAULT;
    RCC_OscInitStruct.MSIClockRange = RCC_MSIRANGE_4;
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK){
        error_handler(1);
    }
    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                                |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2
                                |RCC_CLOCKTYPE_PCLK3;
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_MSI;
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
    RCC_ClkInitStruct.APB3CLKDivider = RCC_HCLK_DIV1;
    if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK){
        error_handler(1);
    }
}

static void MX_LPUART1_UART_Init(void){
    hlpuart1.Instance = LPUART1;
    hlpuart1.Init.BaudRate = 9600;
    hlpuart1.Init.WordLength = UART_WORDLENGTH_8B;
    hlpuart1.Init.StopBits = UART_STOPBITS_1;
    hlpuart1.Init.Parity = UART_PARITY_NONE;
    hlpuart1.Init.Mode = UART_MODE_TX_RX;
    hlpuart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    hlpuart1.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
    hlpuart1.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
    hlpuart1.FifoMode = UART_FIFOMODE_DISABLE;
    if (HAL_UART_Init(&hlpuart1) != HAL_OK){
        error_handler(1);
    }
    if (HAL_UARTEx_SetTxFifoThreshold(&hlpuart1, UART_TXFIFO_THRESHOLD_1_8) != HAL_OK){
        error_handler(1);
    }
    if (HAL_UARTEx_SetRxFifoThreshold(&hlpuart1, UART_RXFIFO_THRESHOLD_1_8) != HAL_OK){
        error_handler(1);
    }
    if (HAL_UARTEx_DisableFifoMode(&hlpuart1) != HAL_OK){
        error_handler(1);
    }
}

static void MX_GPIO_Init(void){
    __HAL_RCC_GPIOG_CLK_ENABLE();
}

void HAL_MspInit(void){
    __HAL_RCC_PWR_CLK_ENABLE();
    HAL_PWREx_EnableVddIO2();
}

void HAL_UART_MspInit(UART_HandleTypeDef* huart){
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
    if(huart->Instance==LPUART1){
        PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_LPUART1;
        PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_PCLK3;
        if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK){
            error_handler(1);
        }
        __HAL_RCC_LPUART1_CLK_ENABLE();
        __HAL_RCC_GPIOG_CLK_ENABLE();
        GPIO_InitStruct.Pin = GPIO_PIN_7|GPIO_PIN_8;
        GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
        GPIO_InitStruct.Pull = GPIO_NOPULL;
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
        GPIO_InitStruct.Alternate = GPIO_AF8_LPUART1;
        HAL_GPIO_Init(GPIOG, &GPIO_InitStruct);
    }
}

void HAL_UART_MspDeInit(UART_HandleTypeDef* huart){
    if(huart->Instance==LPUART1){
        __HAL_RCC_LPUART1_CLK_DISABLE();
        HAL_GPIO_DeInit(GPIOG, GPIO_PIN_7|GPIO_PIN_8);
    }
}

void setup() {
    pinMode(PH10, OUTPUT);
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_LPUART1_UART_Init();
    char msg[] = "Change blink speed from 1 to 9!\n\r";
    HAL_UART_Transmit(&hlpuart1, (uint8_t*)msg, sizeof(msg), HAL_MAX_DELAY);
}

void loop() {
    uint8_t rxByte;
    static uint32_t blinkSpeed = 1000;
    if (HAL_UART_Receive(&hlpuart1, &rxByte, 1, 100) == HAL_OK) {
        char buf[] = {rxByte, '\n', '\r'};
        HAL_UART_Transmit(&hlpuart1, (uint8_t*)buf, sizeof(buf), HAL_MAX_DELAY);
        if (rxByte >= '0' && rxByte <= '9') {
        blinkSpeed = (rxByte - '0') * 1000;
        }
    }
    digitalWrite(PH10, HIGH);
    delay(blinkSpeed / 2);
    digitalWrite(PH10, LOW);
    delay(blinkSpeed / 2);
}

void error_handler(uint32_t speed) {
    while (1) {
        digitalWrite(PH10, HIGH);
        delay(speed*1000);
        digitalWrite(PH10, LOW);
        delay(speed*1000);
    }
}

We set up the LPUART1 for bidirectional data transfer with a baudrate of 9600, but we could use a higher rate as well. The demo program awaits a given number from 1 to 9 which it will then use as interval delay for blinking, nothing fancy.

Monitoring

To also observe the return prints of the MCU, we need to open the enumerated device representing it on the MPU's side. This is a bit complicated to find out, which is why I'll skip any explanations of that, as it does not have much in common with the actual task at hand. We can open /dev/ttyHS1 with a serial terminal emulator such as picocom:

sudo picocom -b 9600 /dev/ttyHS1

The peripheral may require privileges, which is why sudo is used. But this sadly is not good enough. If you were to write a program that prints a statement every second, you would only see output sporadically.

We are in unofficial territory here, meaning that we are trying to interact with a device that is usually not meant for direct access. It is therefore very plausible to assume that the device is being used somewhere else, in an official monitoring task implemented by the Arduino developers. We can probe that assumption by checking what processes are using the device with:

sudo lsof /dev/ttyHS1
# yields
arduino-r 562 root 7u   CHR  239,1      0t0  148 /dev/ttyHS1

So there is a process, supposedly called "arduino-r". We can check for its status with systemctl:

sudo systemctl list-units --all | grep arduino-r
# yields
arduino-router-serial.service
arduino-router.service

So there are actually two processes that manage some sort of serial connection, but only one of them, arduino-router.service takes control of the dev/ttyHS1 device, which can be confirmed by observing the output of sudo systemctl status arduino-router.service. That process must be terminated! Otherwise, observing the MCU's output does not reflect the true messages arriving at the device. Since we are using custom firmware anyway, this process no longer serves a purpose and can be killed without much worry.

sudo systemctl stop arduino-router.service

Keep in mind that you have to do this every time the Uno Q is power-cycled, as long as you don't disable that service entirely.

If you flashed the demo program, you can now interact with the program after opening a terminal session with sudo picocom -b 9600 /dev/ttyHS1.

You can now use your Arduino Uno Q like everybody else, an advanced user or a wicked Arduino wizard. Consider a donation on Ko-Fi if you found this useful, as that is the only way this content receives funding. Happy Hacking!