Close
0%
0%

Bare-Metal Operating System for STM32F7

Operating system from scratch for STM32F7 (ARM Cortex-M7)

Similar projects worth following
80 views
0 followers
Mk is a bare-metal operating system built from scratch for the STM32F746G-Eval2 and STM32F746G-DISCO REV.C boards. It targets the STM32F74xxx and STM32F75xxx MCU families and provides a complete software ecosystem: a preemptive kernel, a dynamic ELF loader, a FAT file system, a multitasking USB stack, a graphical engine with Unicode support, and an interactive shell — all written in C and ARM assembly, with no external dependencies.

welcome

Kernel

  • Preemptive, priority-based multitasking scheduler (fixed-priority, O(1) selection via CLZ)
  • Trusted Execution Environment (TEE) using the Cortex-M7 MPU:
    • Handler mode (privileged): full access to protected memory and system resources
    • Thread mode (privileged): used by kernel and system tasks — full memory access, unrestricted use of protected instructions (MSR/MRS on BASEPRI, etc.)
    • Thread mode (unprivileged): used by user applications — restricted memory access enforced by the MPU; 
  • Synchronization primitives: mutex (with priority inheritance), semaphore, event flags, mailbox
  • Fixed-size memory pools, no variable-size dynamic allocation, eliminating heap fragmentation entirely
  • Synchronous and asynchronous callback execution system
  • Structured fault handling: HardFault, MemFault, BusFault, UsageFault, stack overflow detection

Dynamic ELF Loader

Mk can load and execute external ".elf" files at runtime, relocated into 64 KB pages of external
SDRAM. Programs reference Mk's own API symbols directly. The full kernel symbol table
is embedded in the firmware at a fixed address, so external applications require no copy of the
kernel API in their own binary. Shared libraries can be added to overcome the 64 KB page limit.

See the sym2srec tool for details on the symbol embedding mechanism.

File System

  • FAT32 with multi-partition support
  • Concurrent access from multiple tasks (per-volume mutex)
  • Full API: open, close, read, write, seek, tell, eof, stat, rename, unlink, chmod, expand, truncate, directory browsing
  • Supports SD/MMC cards and USB Mass Storage Class (MSC) devices

USB Stack

  • Multitasking USB host stack built on the STM32F7 OTG peripheral
  • Supported device classes: HUB, HID (keyboard, mouse, joystick, gamepad), MSC
  • Designed for extensibility, new device classes can be added without modifying the core stack

Graphical Engine

  • Hardware-accelerated 2D rendering via the Cortex-M7 ChromART (DMA2D) unit
  • Drawing primitives: rectangles, circles, lines, ...
  • Image rendering: BMP 24-bit and 32-bit
  • Full Unicode text rendering: ASCII, UTF-8, UTF-16, UTF-32
  • Font manager: native fonts stored in FLASH or QSPI; additional fonts can be loaded at runtime into RAM
  • UI object library: buttons, text fields, edit fields, progress bars, 2D graphs, cursors, layers
  • Event-driven application model: painting callbacks and input-listening callbacks
  • Screenshot capability

Shell

The system includes a built-in interactive shell supporting both native and dynamically loaded commands, allowing users to navigate directories (ls, cd, pwd), list mounted disks (lsdsk), and fully manage external applications (launch, install, uninstall, terminate, getapps).

Architecture

Mk follows a three-layer architecture:

  • Foundation: It manages hardware peripherals (STM32F7), handles kernel execution (scheduler, mutexes, etc.), and provides low-level BSP drivers.
  • Subsystems: It delivers system-wide services including a custom hardware-accelerated graphics engine, a dynamic ELF loader, a FAT file system, and a USB stack.
  • System Platform: It runs high-level applications like the Supervisor (fault management), the Home screen, and the Shell.

Video Demonstration

For a complete overview of the system booting, the shell interaction, and dynamic application loading, watch the full video

Mk

  • 1 × 32F746GDISCOVERY or STM32746G-EVAL Discovery/Eval kit with STM32F746NG MCU
  • 1 × J-Link Pro Debug probe with USB and Ethernet interfaces

  • How Mk handles dynamic loading

    EmbSoft307/08/2026 at 15:18 0 comments

    To allow external programs to be loaded dynamically at runtime without being statically linked against the kernel, the Mk ELF loader needs to resolve symbols on the fly. Instead of duplicating or hardcoding the entire kernel API inside every external application, the system relies on Sym2srec.

    Sym2srec extracts the .symtab and .strtab sections from an ELF32 executable, generates an optimized GNU hash table, and embeds everything as loadable segments into a new S-Record file ready to flash.

    Here is the complete layout and the lookup protocol used inside the Mk ecosystem.

    Concepts

    Symbol — a unique identifier representing a function, variable, or object in a program.

    Symbol resolution — the process of finding the exact memory address of a symbol:

    • Static resolution: performed at compile time, for symbols known within a single compilation unit.
    • Dynamic resolution: performed at runtime, for symbols shared across compilation units or loaded dynamically.

    Relocation — adjusting a symbol's address so it can be correctly referenced after being loaded at a different address than originally linked.

    Dynamic loading — copying a program from storage into RAM and resolving all symbol references so it can execute correctly.

    Output format — S-Record layout

    Sym2srec parses the input ELF file and performs the following steps:

    1. Builds a GNU hash table (.gnuhash) from the .symtab and .strtab sections.
    2. Builds a SymbolsAreaHeader_t header pointing to all three tables.
    3. Copies all existing loadable segments from the ELF file to the S-Record.
    4. Appends .symtab, .strtab, and .gnuhash as new loadable segments.

    The resulting S-Record layout is:

    S-Record file
    │
    ├── Existing loadable segments (from input ELF)
    │
    └── Symbol area  (at <base_address>)    
        ├── SymbolsAreaHeader_t        ← loaded at <base_address>   
        ├── .symtab                    ← at symtabBaseAddr    
        ├── .strtab                    ← at strtabBaseAddr    
        └── .gnuhash                   ← at gnuHashBaseAddr
    

    S-Record layout

    Symbol resolution protocol

    This section describes the complete protocol a dynamic loader must implement to resolve a symbol by name using the data produced by sym2srec.

    SymbolsAreaHeader_t

    The symbol area always begins with this header at <base_address>:

    typedef struct
    {    
        uint32_t  magicNumber;       /* Magic number: 0x53594D42 ('SYMB') */    
        uint32_t  headerSize;        /* Size of this header in bytes (40) */    
        uint32_t  padding;           /* Reserved: 0xFFFFFFFF */    
        uint32_t  version;           /* Header version: 0x00000001 */    
        uint32_t* symtabBaseAddr;    /* Pointer to the symbol table */    
        uint32_t  symtabSize;        /* Size of the symbol table in bytes */    
        uint32_t* strtabBaseAddr;    /* Pointer to the string table */    
        uint32_t  strtabSize;        /* Size of the string table in bytes */    
        uint32_t* gnuhashBaseAddr;   /* Pointer to the GNU hash table */    
        uint32_t  gnuhashSize;       /* Size of the GNU hash table in bytes */
    } SymbolsAreaHeader_t;
    

    To access the header in your loader:

    SymbolsAreaHeader_t* l_header = (SymbolsAreaHeader_t*) 0x002C0000;

    GNU hash table

    The GNU hash table is stored contiguously in memory. Initialize the following structure from the header to navigate it:

    typedef struct
    {    
        uint32_t  nbuckets;          /* Number of buckets */    
        uint32_t  symbolsOffset;     /* Index of first non-local symbol */    
        uint32_t  bloomFilterSize;   /* Bloom filter size in 32-bit words */    
        uint32_t  bloomFilterShift;  /* Bloom filter shift value */    
        uint32_t* bloomAddr;         /* Pointer to the bloom filter array */    
        uint32_t* bucketsAddr;       /* Pointer to the bucket array */    
        uint32_t* hashValuesAddr;    /* Pointer to the hash value array */
    } ELF32GNUHashTable_t;
    

    Initialize it from the header:

    ELF32GNUHashTable_t l_hashTable;
    
    l_hashTable.nbuckets        =  l_header->gnuhashBaseAddr[0];
    l_hashTable.symbolsOffset   =  l_header->gnuhashBaseAddr[1];
    l_hashTable.bloomFilterSize =  l_header->gnuhashBaseAddr[2];
    l_hashTable.bloomFilterShift=  l_header->gnuhashBaseAddr[3];
    l_hashTable.bloomAddr       = &l_header->gnuhashBaseAddr[4];
    l_hashTable.bucketsAddr = &l_header->gnuhashBaseAddr[4...
    Read more »

  • Initial release

    EmbSoft306/26/2026 at 18:58 0 comments

    Mk is available on GitHub after 6 years of solo development. Future logs will dive into specific parts of the implementation: the kernel, the dynamic ELF loader, the USB stack, and more.

View all 2 project logs

  • 1
    Build, Flash, and Debug Instructions

    Detailed step-by-step instructions for setting up the toolchain, configuring the build options, flashing the generated firmware, and initiating a debug session can be found in my github

View all instructions

Enjoy this project?

Share

Discussions

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates