# Bare-Metal Hexagon with Picolibc This guide covers compiling and running programs on bare-metal Hexagon targets using picolibc — meaning no H2 hypervisor and no operating system. I/O is provided either by QEMU semihosting (for development and testing) or by user-supplied stub functions (for production hardware without a host debugger). * * * ## Overview The bare-metal target triple is: --target=hexagon-unknown-none-elf --cstdlib=picolibc Copy to clipboard With this target the toolchain: - Uses picolibc headers and libraries from the `hexagon-unknown-none-elf` sysroot - Links `libsemihost.a` (QEMU TRAP0-based I/O) when semihosting is enabled - Does not pull in H2 or OS-specific libraries * * * ## Minimal compile command hexagon-clang \ --target=hexagon-unknown-none-elf \ --cstdlib=picolibc \ -mv68 \ -G0 \ -T picolibc.ld \ -o myprogram myprogram.c Copy to clipboard | Flag | Why | | --- | --- | | `--target=hexagon-unknown-none-elf` | Bare-metal target, no OS | | `--cstdlib=picolibc` | Use picolibc (not Dinkumware) | | `-mv68` | Target Hexagon v68 architecture | | `-G0` | Disable small-data GP-relative section when required by the application configuration | | `-T picolibc.ld` | Supply the default Picolibc memory layout and runtime symbols | * * * ## Memory layout and linker scripts Picolibc ships a default linker script (`picolibc.ld`) that defines heap and stack symbols the C runtime needs. The default memory map used by the configure script is: Flash (rx) : ORIGIN = 0x00100000, LENGTH = 4M RAM (rwx) : ORIGIN = 0x00500000, LENGTH = 8M Copy to clipboard This matches the QEMU `virt` machine defaults and works out of the box for simulator testing. ### Using the default picolibc linker script Pass the default `picolibc.ld` explicitly with `-T picolibc.ld` for the none-elf target. Use a custom script only when the default memory map does not match the target. ### Custom memory map Create a wrapper that overrides only the MEMORY regions and delegates everything else to picolibc’s sections layout: /* custom-memory.ld */ MEMORY { FLASH (rx) : ORIGIN = 0x00100000, LENGTH = 4M RAM (rwx) : ORIGIN = 0x00500000, LENGTH = 16M /* extend RAM to 16M */ } INCLUDE picolibc.ld Copy to clipboard Pass it at link time: hexagon-clang \ --target=hexagon-unknown-none-elf \ --cstdlib=picolibc \ -mv68 -G0 \ -T custom-memory.ld \ -o myprogram myprogram.c Copy to clipboard * * * ## Semihosting (QEMU-hosted I/O) Semihosting lets bare-metal programs use stdio and file I/O via QEMU without any real hardware peripherals. It is the standard way to test bare-metal picolibc programs. Picolibc links `libsemihost.a` automatically when the semihosting I/O backend is selected (the default for `none-elf` with `--cstdlib=picolibc`). ### Compile and run with semihosting /* hello.c */ #include int main(void) { printf("Hello from bare-metal picolibc!\n"); return 0; } Copy to clipboard # Compile hexagon-clang \ --target=hexagon-unknown-none-elf \ --cstdlib=picolibc \ -mv68 -G0 \ -T picolibc.ld \ -o hello hello.c # Run under QEMU with semihosting qemu-system-hexagon \ -cpu v68 \ -machine virt \ -nographic \ -kernel hello Copy to clipboard The TRAP0 mechanism used by semihosting is documented in [Semihosting](https://docs.qualcomm.com/doc/80-N3040-23/topic/semihosting.html). ### File I/O via semihosting Semihosting maps `open`/`read`/`write`/`close` onto the QEMU host filesystem: #include #include #include int main(void) { int fd = open("/tmp/out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0) { perror("open"); return 1; } const char *msg = "written from Hexagon bare-metal\n"; write(fd, msg, 32); close(fd); return 0; } Copy to clipboard Relative paths are resolved against QEMU’s working directory on the host; absolute paths such as `/tmp/out.txt` name an absolute host path. * * * ## No-semihosting: stub I/O for production When running on hardware without a host debugger, you must supply your own low-level I/O. Picolibc’s `retarget` model lets you replace just the `__io_putchar` / `__io_getchar` layer (or the full `read`/`write` syscall stubs) without rebuilding picolibc itself. ### Minimal stub: redirect stdout to a UART Implement the POSIX `write` and `read` syscalls that picolibc uses for I/O: /* uart_io.c */ #include #include /* Replace with your UART register address */ #define UART_TX_REG ((volatile unsigned int *)0xFE000000) static void uart_putchar(char c) { /* Simplified: assumes UART is always ready */ *UART_TX_REG = (unsigned int)c; } ssize_t write(int fd, const void *buf, size_t count) { (void)fd; /* treat all FDs as stdout */ const char *p = (const char *)buf; for (size_t i = 0; i < count; i++) { uart_putchar(p[i]); } return (ssize_t)count; } ssize_t read(int fd, void *buf, size_t count) { (void)fd; (void)buf; (void)count; errno = ENOSYS; return -1; } Copy to clipboard Compile together with your program: hexagon-clang \ --target=hexagon-unknown-none-elf \ --cstdlib=picolibc \ -mv68 -G0 \ -T picolibc.ld \ -o myprogram myprogram.c uart_io.c Copy to clipboard * * * ## Heap configuration Picolibc’s `malloc` uses the region between `__heap_start` and `__heap_end` as defined by the linker script. The default `picolibc.ld` sets the heap to whatever RAM remains between BSS and the stack. To give the heap a fixed size, pass a minimum size at link time: hexagon-clang \ --target=hexagon-unknown-none-elf \ --cstdlib=picolibc \ -mv68 -G0 \ -T picolibc.ld \ -Wl,--defsym=__heap_size_min=0x80000 \ -o myprogram myprogram.c Copy to clipboard * * * ## Stack configuration The default stack lives at the top of the RAM region. To set a fixed stack size: -Wl,--defsym=__stack_size=0x8000 # 32 KB stack Copy to clipboard * * * ## Building without the C standard library (fully standalone) If you want only the compiler runtime (no libc, no picolibc): hexagon-clang \ --target=hexagon-unknown-none-elf \ -mv68 -G0 \ -nostdlib \ -ffreestanding \ -o myprogram myprogram.c my_crt0.S \ -lclang_rt.builtins Copy to clipboard This skips picolibc entirely. You are responsible for startup, BSS init, and all I/O. * * * ## Worked example: semihosting file I/O /* filetest.c */ #include #include #include int main(void) { /* Write a file on the QEMU host */ FILE *fp = fopen("/tmp/hexagon_test.txt", "w"); if (!fp) { perror("fopen"); return 1; } fprintf(fp, "Bare-metal picolibc file I/O works.\n"); fclose(fp); /* Read it back */ fp = fopen("/tmp/hexagon_test.txt", "r"); if (!fp) { perror("fopen"); return 1; } char buf[128]; if (fgets(buf, sizeof(buf), fp)) { printf("Read back: %s", buf); } fclose(fp); return 0; } Copy to clipboard hexagon-clang \ --target=hexagon-unknown-none-elf \ --cstdlib=picolibc \ -mv68 -G0 \ -T picolibc.ld \ -o filetest filetest.c qemu-system-hexagon \ -cpu v68 -machine virt -nographic \ -kernel filetest Copy to clipboard Expected output: Read back: Bare-metal picolibc file I/O works. Copy to clipboard * * * ## Troubleshooting ### `undefined reference to __heap_start` / `__heap_end` The linker script is missing. Pass `-T picolibc.ld` explicitly. ### Program exits immediately with no output (QEMU) Semihosting may not be initialised in QEMU. Ensure you are using `-machine virt` and not a machine type that doesn’t implement TRAP0 semihosting. ### `write` multiply defined You supplied a `write` implementation while also linking a provider that exports `write`. Select only one I/O provider; for a user-supplied UART stub, do not link the semihosting provider. hexagon-clang ... uart_io.o myprogram.o -lc Copy to clipboard Object files before archives satisfy unresolved references before the archive is searched. * * * ## POSIX and Syscall Coverage on Bare-Metal When running bare-metal with `libsemihost.a`, system calls are handled as follows: | Category | Supported via `libsemihost.a` | Not supported (bare-metal) | | --- | --- | --- | | **File I/O** | `open`, `close`, `read`, `write`, `lseek`, `unlink`, `rename`, `stat`, `fstat` | Directory operations (`opendir`, `fdopendir`), permissions (`chmod`, `fchmod`), links (`link`, `symlink`) | | **Memory** | Heap growth via `sbrk` | Page-level VM primitives (`mmap`, `munmap`, `mprotect`) | | **Process** | `exit`, `_exit` | Process creation / control (`fork`, `execve`, `waitpid`, `pipe`) | | **Time** | `gettimeofday` (seconds resolution via `SYS_TIME`) | High-resolution clocks (`clock_settime`, `clock_getres`) | Pre-built bare-metal sysroots (`hexagon-unknown-none-elf`) in the Hexagon toolchain ship with `libsemihost.a` pre-configured. When building picolibc from source, enable semihosting using `-Dsemihost=true`. * * * ## Related documentation - [Getting Started](https://docs.qualcomm.com/doc/80-N3040-23/topic/getting-started.html) — Minimal end-to-end flows - [Semihosting](https://docs.qualcomm.com/doc/80-N3040-23/topic/semihosting.html) — TRAP0 interface, syscall opcodes, QEMU - [H2 + Picolibc](https://docs.qualcomm.com/doc/80-N3040-23/topic/picolibc-h2.html) — H2 hypervisor target Last Published: Oct 06, 2026 [Previous Topic Documentation](https://docs.qualcomm.com/bundle/publicresource/80-N3040-23/topics/picolibc-h2.md) [Next Topic Semihosting Support for Hexagon](https://docs.qualcomm.com/bundle/publicresource/80-N3040-23/topics/semihosting.md)