diff --git a/.github/workflows/onmcu.yml b/.github/workflows/onmcu.yml index 837566c..c71c52f 100644 --- a/.github/workflows/onmcu.yml +++ b/.github/workflows/onmcu.yml @@ -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/**'] @@ -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 @@ -68,16 +69,19 @@ 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 @@ -85,4 +89,5 @@ jobs: with: board: ${{ steps.build.outputs.board }} file: ${{ steps.build.outputs.elf }} + args: ${{ steps.build.outputs.args }} api-key: ${{ secrets.ONMCU_API_KEY }} diff --git a/README.md b/README.md index 16d0002..95621d6 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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 | diff --git a/common.mk b/common.mk index ff04df5..5780a63 100644 --- a/common.mk +++ b/common.mk @@ -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 @@ -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 \ @@ -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 diff --git a/nucleo-f401re-serial/Makefile b/nucleo-f401re-serial/Makefile new file mode 100644 index 0000000..8a27eda --- /dev/null +++ b/nucleo-f401re-serial/Makefile @@ -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 diff --git a/nucleo-f401re-serial/main.c b/nucleo-f401re-serial/main.c new file mode 100644 index 0000000..602bf4b --- /dev/null +++ b/nucleo-f401re-serial/main.c @@ -0,0 +1,95 @@ +#include + +#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); +} diff --git a/nucleo-f401re-serial/startup_stm32f401.c b/nucleo-f401re-serial/startup_stm32f401.c new file mode 100644 index 0000000..86a5a1b --- /dev/null +++ b/nucleo-f401re-serial/startup_stm32f401.c @@ -0,0 +1,75 @@ +#include + +// 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 (;;) { + } +} diff --git a/nucleo-f401re-serial/stm32f401.ld b/nucleo-f401re-serial/stm32f401.ld new file mode 100644 index 0000000..4f2325d --- /dev/null +++ b/nucleo-f401re-serial/stm32f401.ld @@ -0,0 +1,65 @@ +/* STM32F401RET6: 512 KB FLASH, 96 KB SRAM. The RTT control block lives in SRAM + at 0x20000000, the region the host scans for the signature. */ + +ENTRY(Reset_Handler) + +MEMORY +{ + FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K + RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 96K +} + +_estack = ORIGIN(RAM) + LENGTH(RAM); + +SECTIONS +{ + .isr_vector : + { + KEEP(*(.isr_vector)) + } > FLASH + + .text : + { + *(.text) + *(.text*) + *(.rodata) + *(.rodata*) + KEEP(*(.init)) + KEEP(*(.fini)) + . = ALIGN(4); + } > FLASH + + .ARM.extab : { *(.ARM.extab* .gnu.linkonce.armextab.*) } > FLASH + .ARM : + { + __exidx_start = .; + *(.ARM.exidx*) + __exidx_end = .; + } > FLASH + + . = ALIGN(4); + _sidata = LOADADDR(.data); + + .data : + { + . = ALIGN(4); + _sdata = .; + *(.data) + *(.data*) + . = ALIGN(4); + _edata = .; + } > RAM AT> FLASH + + .bss : + { + . = ALIGN(4); + _sbss = .; + *(.bss) + *(.bss*) + *(COMMON) + . = ALIGN(4); + _ebss = .; + } > RAM + + /DISCARD/ : { *(.ARM.attributes) } +}