Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .github/workflows/onmcu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
filters: |
nucleo-h743zi: ['nucleo-h743zi/**']
nucleo-f401re: ['nucleo-f401re/**']
nucleo-f401re-serial: ['nucleo-f401re-serial/**']
nucleo-f439zi: ['nucleo-f439zi/**']
nucleo-wb55rg: ['nucleo-wb55rg/**']
nucleo-wl55jc: ['nucleo-wl55jc/**']
Expand All @@ -44,7 +45,7 @@ jobs:
# The shared libraries and build rules are compiled into every board,
# so a change there runs them all -- as do pushes to main and manual
# runs. Otherwise a PR runs only the boards whose folder changed.
all='["nucleo-h743zi","nucleo-f401re","nucleo-f439zi","nucleo-wb55rg","nucleo-wl55jc"]'
all='["nucleo-h743zi","nucleo-f401re","nucleo-f401re-serial","nucleo-f439zi","nucleo-wb55rg","nucleo-wl55jc"]'
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "crates=$all" >> "$GITHUB_OUTPUT"
elif [ "${{ steps.filter.outputs.shared }}" = "true" ]; then
Expand All @@ -68,21 +69,25 @@ jobs:
- name: Install Arm GNU toolchain
uses: carlosperate/arm-none-eabi-gcc-action@v1

# Build the firmware and surface the board name and linked ELF so the
# OnMCU action can flash + run it.
# Build the firmware and surface the board name, linked ELF and any extra
# OnMCU flags (e.g. --logging serial) so the action runs it the same way a
# local `make run` would.
- name: Build ${{ matrix.crate }}
id: build
working-directory: ${{ matrix.crate }}
run: |
board=$(sed -n 's/^BOARD *:= *//p' Makefile)
name=$(sed -n 's/^NAME *:= *//p' Makefile)
args=$(sed -n 's/^ONMCU_FLAGS *:= *//p' Makefile)
make
echo "board=$board" >> "$GITHUB_OUTPUT"
echo "args=$args" >> "$GITHUB_OUTPUT"
echo "elf=$PWD/build/$name.elf" >> "$GITHUB_OUTPUT"

- name: Run ${{ matrix.crate }} on OnMCU
uses: onmcu/onmcu-action@v1
with:
board: ${{ steps.build.outputs.board }}
file: ${{ steps.build.outputs.elf }}
args: ${{ steps.build.outputs.args }}
api-key: ${{ secrets.ONMCU_API_KEY }}
33 changes: 19 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

[![OnMCU](https://github.com/onmcu/c-examples/actions/workflows/onmcu.yml/badge.svg)](https://github.com/onmcu/c-examples/actions/workflows/onmcu.yml)

Minimal bare-metal **C** examples for [OnMCU](https://onmcu.com). Each one counts
upwards, streams the count over **RTT** on real hardware in the cloud, then
signals success to the host via ARM **semihosting** so the run exits `0`. This
mirrors the [`rust-examples`](../rust-examples) repository, in C.
Minimal bare-metal **C** examples for [OnMCU](https://onmcu.com). Most count
upwards and stream the count over **RTT** on real hardware in the cloud; one
logs over a **serial** port instead. Each signals success to the host via ARM
**semihosting** so the run exits `0`. This mirrors the
[`rust-examples`](../rust-examples) repository, in C.

```sh
cd nucleo-h743zi
Expand Down Expand Up @@ -44,9 +45,9 @@ nucleo-h743zi/ one example crate per board
main.c the counter
```

The two libraries under `lib/` are compiled into every example, so adding a new
board only means adding a board directory with its `Makefile`, linker script,
startup and `main.c`.
Semihosting is compiled into every example, and RTT into all but the serial-only
one (which sets `USE_RTT = 0`), so adding a new board only means adding a board
directory with its `Makefile`, linker script, startup and `main.c`.

## How it works

Expand All @@ -57,13 +58,17 @@ startup and `main.c`.
- **Semihosting** (`lib/semihosting`): `sys_exit(code)` issues `SYS_EXIT_EXTENDED`
via `bkpt 0xAB`. The debugger services the trap and ends the session with that
code (`0` = success, non-zero = failure).
- **Serial** (`nucleo-f401re-serial`): logs over **USART2** (PA2/PA3, the ST-LINK
virtual COM port) at 115200 8N1 instead of RTT. It sets `USE_RTT = 0` and runs
with `--logging serial`, so OnMCU captures the board's serial output.

## Boards

| Directory | Board | MCU | Core | Approach |
|-----------|-------|-----|------|----------|
| `nucleo-h743zi` | NUCLEO-H743ZI | STM32H743ZIT6 | Cortex-M7 | bare-metal |
| `nucleo-f401re` | NUCLEO-F401RE | STM32F401RET6 | Cortex-M4F | bare-metal |
| `nucleo-f439zi` | NUCLEO-F439ZI | STM32F439ZIT6 | Cortex-M4F | bare-metal |
| `nucleo-wb55rg` | NUCLEO-WB55RG | STM32WB55RGV6 | Cortex-M4F | bare-metal |
| `nucleo-wl55jc` | NUCLEO-WL55JC | STM32WL55JC | Cortex-M4 | bare-metal |
| Directory | Board | MCU | Core | Logging |
|-----------|-------|-----|------|---------|
| `nucleo-h743zi` | NUCLEO-H743ZI | STM32H743ZIT6 | Cortex-M7 | RTT |
| `nucleo-f401re` | NUCLEO-F401RE | STM32F401RET6 | Cortex-M4F | RTT |
| `nucleo-f401re-serial` | NUCLEO-F401RE | STM32F401RET6 | Cortex-M4F | serial (USART2) |
| `nucleo-f439zi` | NUCLEO-F439ZI | STM32F439ZIT6 | Cortex-M4F | RTT |
| `nucleo-wb55rg` | NUCLEO-WB55RG | STM32WB55RGV6 | Cortex-M4F | RTT |
| `nucleo-wl55jc` | NUCLEO-WL55JC | STM32WL55JC | Cortex-M4 | RTT |
25 changes: 16 additions & 9 deletions common.mk
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Shared build rules for the OnMCU C examples.
#
# A board Makefile sets NAME, BOARD, LDSCRIPT, MCU_FLAGS and SRCS, then includes
# this file. The RTT and semihosting libraries are compiled in for every board,
# so new examples only describe what is board specific.
# this file. Semihosting is compiled in for every board; RTT is too unless the
# board opts out with USE_RTT = 0 (e.g. a serial-only example), so new examples
# only describe what is board specific.

TOOLCHAIN ?= arm-none-eabi-
CC := $(TOOLCHAIN)gcc
Expand All @@ -13,12 +14,17 @@ SIZE := $(TOOLCHAIN)size
ROOT := $(dir $(lastword $(MAKEFILE_LIST)))
LIBDIR := $(ROOT)lib

# Reusable libraries shared by every example.
LIB_SRCS := \
$(LIBDIR)/rtt/rtt.c \
$(LIBDIR)/semihosting/semihosting.c
# Reusable libraries shared by every example. RTT is included by default; a
# serial-only example sets USE_RTT = 0 to leave it (and its RAM block) out.
USE_RTT ?= 1

INCLUDES += -I$(LIBDIR)/rtt -I$(LIBDIR)/semihosting
LIB_SRCS := $(LIBDIR)/semihosting/semihosting.c
INCLUDES += -I$(LIBDIR)/semihosting

ifeq ($(USE_RTT),1)
LIB_SRCS += $(LIBDIR)/rtt/rtt.c
INCLUDES += -I$(LIBDIR)/rtt
endif

CFLAGS += $(MCU_FLAGS) $(INCLUDES) -std=c11 -Os -g3 \
-ffreestanding -fno-common -ffunction-sections -fdata-sections \
Expand Down Expand Up @@ -50,9 +56,10 @@ $(ELF): $(OBJS)
$(BIN): $(ELF)
$(OBJCOPY) -O binary $< $@

# Build, flash and run on real hardware via OnMCU; exits 0 on success.
# Build, flash and run on real hardware via OnMCU; exits 0 on success. A board
# may pass extra flags via ONMCU_FLAGS (e.g. --logging serial).
run: $(ELF)
onmcu run --board $(BOARD) --file $(ELF)
onmcu run --board $(BOARD) $(ONMCU_FLAGS) --file $(ELF)

flash: run

Expand Down
15 changes: 15 additions & 0 deletions nucleo-f401re-serial/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
NAME := serial-hello
BOARD := NUCLEO-F401RE
LDSCRIPT := stm32f401.ld

# STM32F401RE: Cortex-M4 with single-precision FPU, hard-float ABI.
MCU_FLAGS := -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard

# Serial-only example: no RTT. Log over USART2 and have OnMCU capture the
# board's serial output instead of scanning RAM for an RTT block.
USE_RTT := 0
ONMCU_FLAGS := --logging serial

SRCS := main.c startup_stm32f401.c

include ../common.mk
95 changes: 95 additions & 0 deletions nucleo-f401re-serial/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#include <stdint.h>

#include "semihosting.h"

// Serial-only example for the NUCLEO-F401RE: log a greeting over USART2, then
// signal success to the host via ARM semihosting so the run exits 0. No RTT is
// used; OnMCU captures the board's serial output with `--logging serial`.
//
// USART2 is wired to the ST-LINK virtual COM port: PA2 (TX), PA3 (RX).

// HSI is the default system clock out of reset: 16 MHz. With no prescalers the
// APB1 peripheral clock feeding USART2 is also 16 MHz.
#define CPU_HZ 16000000u
#define BAUD 115200u

// RCC.
#define RCC_BASE 0x40023800u
#define RCC_AHB1ENR (*(volatile uint32_t *)(RCC_BASE + 0x30u))
#define RCC_APB1ENR (*(volatile uint32_t *)(RCC_BASE + 0x40u))
#define RCC_AHB1ENR_GPIOAEN (1u << 0)
#define RCC_APB1ENR_USART2EN (1u << 17)

// GPIOA.
#define GPIOA_BASE 0x40020000u
#define GPIOA_MODER (*(volatile uint32_t *)(GPIOA_BASE + 0x00u))
#define GPIOA_AFRL (*(volatile uint32_t *)(GPIOA_BASE + 0x20u))

// USART2.
#define USART2_BASE 0x40004400u
#define USART2_SR (*(volatile uint32_t *)(USART2_BASE + 0x00u))
#define USART2_DR (*(volatile uint32_t *)(USART2_BASE + 0x04u))
#define USART2_BRR (*(volatile uint32_t *)(USART2_BASE + 0x08u))
#define USART2_CR1 (*(volatile uint32_t *)(USART2_BASE + 0x0Cu))
#define USART_SR_TXE (1u << 7)
#define USART_SR_TC (1u << 6)
#define USART_CR1_UE (1u << 13)
#define USART_CR1_TE (1u << 3)

// SysTick registers (Cortex-M4 core peripheral).
#define SYST_CSR (*(volatile uint32_t *)0xE000E010u)
#define SYST_RVR (*(volatile uint32_t *)0xE000E014u)
#define SYST_CVR (*(volatile uint32_t *)0xE000E018u)
#define SYST_CSR_COUNTFLAG (1u << 16)

static const char GREETING[] = "hello from NUCLEO-F401RE over USART2\r\n";

static void delay_ms(uint32_t ms)
{
SYST_RVR = (CPU_HZ / 1000u) - 1u;
SYST_CVR = 0u;
SYST_CSR = 0x5u; // enable, processor clock, no exception
for (uint32_t i = 0; i < ms; i++) {
while ((SYST_CSR & SYST_CSR_COUNTFLAG) == 0u) {
}
}
SYST_CSR = 0u;
}

static void usart2_init(void)
{
RCC_AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
RCC_APB1ENR |= RCC_APB1ENR_USART2EN;

// PA2, PA3 -> alternate function mode (0b10), AF7 (USART2).
GPIOA_MODER &= ~((3u << (2 * 2)) | (3u << (3 * 2)));
GPIOA_MODER |= ((2u << (2 * 2)) | (2u << (3 * 2)));
GPIOA_AFRL &= ~((0xFu << (2 * 4)) | (0xFu << (3 * 4)));
GPIOA_AFRL |= ((7u << (2 * 4)) | (7u << (3 * 4)));

// 115200 8N1, oversampling by 16: BRR is PCLK1/baud in 12.4 fixed point,
// which equals round(PCLK1 / baud).
USART2_BRR = (CPU_HZ + BAUD / 2u) / BAUD;
USART2_CR1 = USART_CR1_UE | USART_CR1_TE;
}

static void usart2_write(const char *s, uint32_t len)
{
for (uint32_t i = 0; i < len; i++) {
while ((USART2_SR & USART_SR_TXE) == 0u) {
}
USART2_DR = (uint32_t)(uint8_t)s[i];
}
while ((USART2_SR & USART_SR_TC) == 0u) {
}
}

int main(void)
{
usart2_init();

usart2_write(GREETING, sizeof(GREETING) - 1u);
delay_ms(100); // let the ST-LINK USB-serial bridge drain before exit

sys_exit(0);
}
75 changes: 75 additions & 0 deletions nucleo-f401re-serial/startup_stm32f401.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#include <stdint.h>

// Minimal startup for the STM32F401RE Cortex-M4 core: vector table, reset
// handler (.data/.bss init + FPU enable), and a catch-all default handler.

extern uint32_t _sidata; // .data init image in FLASH
extern uint32_t _sdata; // .data start in RAM
extern uint32_t _edata;
extern uint32_t _sbss;
extern uint32_t _ebss;
extern uint32_t _estack; // top of stack, from the linker script

extern int main(void);

void Reset_Handler(void);
void Default_Handler(void);

// Core system exceptions; everything else falls through to Default_Handler.
void NMI_Handler(void) __attribute__((weak, alias("Default_Handler")));
void HardFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void MemManage_Handler(void) __attribute__((weak, alias("Default_Handler")));
void BusFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void UsageFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void SVC_Handler(void) __attribute__((weak, alias("Default_Handler")));
void DebugMon_Handler(void) __attribute__((weak, alias("Default_Handler")));
void PendSV_Handler(void) __attribute__((weak, alias("Default_Handler")));
void SysTick_Handler(void) __attribute__((weak, alias("Default_Handler")));

__attribute__((section(".isr_vector"), used))
void (*const g_vectors[])(void) = {
(void (*)(void))(&_estack),
Reset_Handler,
NMI_Handler,
HardFault_Handler,
MemManage_Handler,
BusFault_Handler,
UsageFault_Handler,
0,
0,
0,
0,
SVC_Handler,
DebugMon_Handler,
0,
PendSV_Handler,
SysTick_Handler,
};

void Reset_Handler(void)
{
uint32_t *src = &_sidata;
for (uint32_t *dst = &_sdata; dst < &_edata;) {
*dst++ = *src++;
}
for (uint32_t *dst = &_sbss; dst < &_ebss;) {
*dst++ = 0;
}

// Enable the FPU (CP10/CP11 full access) before any hard-float code runs.
volatile uint32_t *cpacr = (volatile uint32_t *)0xE000ED88u;
*cpacr |= (0xFu << 20);
__asm volatile("dsb");
__asm volatile("isb");

main();

for (;;) {
}
}

void Default_Handler(void)
{
for (;;) {
}
}
Loading
Loading