├── .gitignore ├── LICENSE ├── README.md ├── build.zig └── src ├── linker.ld ├── main.zig ├── registers.zig ├── startup.zig └── vector.zig /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache 2 | zig-out 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Riccardo Binetti 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zig STM32 Blink 2 | 3 | Make LEDs blink on an STM32F4 Discovery board using only Zig (and a linker script). 4 | 5 | See [my blogpost](https://rbino.com/posts/zig-stm32-blink/) for a more thorough explanation of 6 | what's going on. 7 | 8 | ## Build 9 | 10 | The code was tested with Zig `0.9.0`. 11 | 12 | To build the ELF file just run: 13 | 14 | ``` 15 | zig build 16 | ``` 17 | 18 | ## Flashing 19 | 20 | The easiest way to flash the board is to install [`stlink` 21 | tools](https://github.com/stlink-org/stlink). Most Linux distributions should have them in their 22 | repos, the build system will try to use the `st-flash` program. 23 | 24 | The command to flash the board is: 25 | 26 | ``` 27 | zig build flash 28 | ``` 29 | 30 | After flashing the board you should see the 4 LEDs blinking in an alternating pattern. 31 | 32 | ## Debugging 33 | 34 | It's possible to use [`openocd`](http://openocd.org/) and `gdb-multiarch` to debug the firmware 35 | directly on the board. 36 | 37 | In a terminal run: 38 | ``` 39 | openocd -f board/stm32f4discovery.cfg 40 | ``` 41 | 42 | Then from another terminal navigate to the directory containing the ELF output (i.e. 43 | `zig-out/bin`) and run: 44 | 45 | ``` 46 | gdb-multiarch zig-stm32-blink.elf -ex "target remote :3333" 47 | ``` 48 | 49 | You can also manually flash the firmware inside `gdb` with: 50 | 51 | ``` 52 | load 53 | ``` 54 | 55 | ## Emulation using `qemu` 56 | 57 | If you don't have an STM32F4 Discovery board or you just want to test the code locally, you can use 58 | [xPack QEMU Arm](https://xpack.github.io/qemu-arm/install/#manual-install). 59 | 60 | *Note*: you have to comment out [this 61 | line](https://github.com/rbino/zig-stm32-blink/blob/master/src/main.zig#L44) to make the code work 62 | in QEMU since it doesn't support the FPU coprocessor yet. 63 | 64 | After that, you can emulate the board with: 65 | 66 | ``` 67 | qemu-system-gnuarmeclipse -machine STM32F4-Discovery -mcu STM32F407VG \ 68 | -kernel zig-out/bin/zig-stm32-blink.elf -gdb tcp::3333 69 | ``` 70 | 71 | You should see the blinking LEDs on the board image, and you can connect a `gdb` instance to it (see 72 | the previous section). 73 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const Builder = @import("std").build.Builder; 2 | const builtin = @import("builtin"); 3 | const std = @import("std"); 4 | 5 | pub fn build(b: *Builder) void { 6 | // Target STM32F407VG 7 | const target = .{ 8 | .cpu_arch = .thumb, 9 | .cpu_model = .{ .explicit = &std.Target.arm.cpu.cortex_m4 }, 10 | .os_tag = .freestanding, 11 | .abi = .eabihf, 12 | }; 13 | 14 | // Standard release options allow the person running `zig build` to select 15 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. 16 | const mode = b.standardReleaseOptions(); 17 | 18 | const elf = b.addExecutable("zig-stm32-blink.elf", "src/startup.zig"); 19 | elf.setTarget(target); 20 | elf.setBuildMode(mode); 21 | 22 | const vector_obj = b.addObject("vector", "src/vector.zig"); 23 | vector_obj.setTarget(target); 24 | vector_obj.setBuildMode(mode); 25 | 26 | elf.addObject(vector_obj); 27 | elf.setLinkerScriptPath(.{ .path = "src/linker.ld" }); 28 | 29 | const bin = b.addInstallRaw(elf, "zig-stm32-blink.bin", .{}); 30 | const bin_step = b.step("bin", "Generate binary file to be flashed"); 31 | bin_step.dependOn(&bin.step); 32 | 33 | const flash_cmd = b.addSystemCommand(&[_][]const u8{ 34 | "st-flash", 35 | "write", 36 | b.getInstallPath(bin.dest_dir, bin.dest_filename), 37 | "0x8000000", 38 | }); 39 | flash_cmd.step.dependOn(&bin.step); 40 | const flash_step = b.step("flash", "Flash and run the app on your STM32F4Discovery"); 41 | flash_step.dependOn(&flash_cmd.step); 42 | 43 | b.default_step.dependOn(&elf.step); 44 | b.installArtifact(elf); 45 | } 46 | -------------------------------------------------------------------------------- /src/linker.ld: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the libopencm3 project. 3 | * 4 | * Copyright (C) 2009 Uwe Hermann 5 | * Copyright (C) 2011 Stephen Caudle 6 | * 7 | * This library is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Lesser General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This library is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU Lesser General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Lesser General Public License 18 | * along with this library. If not, see . 19 | */ 20 | 21 | /* Linker script for ST STM32F4DISCOVERY (STM32F407VG, 1024K flash, 128K RAM, 22 | * 64K Core Coupled Memory RAM). 23 | */ 24 | 25 | /* Define memory regions. */ 26 | MEMORY 27 | { 28 | rom (rx) : ORIGIN = 0x08000000, LENGTH = 1024K 29 | ram (rwx) : ORIGIN = 0x20000000, LENGTH = 128K 30 | ccm (rwx) : ORIGIN = 0x10000000, LENGTH = 64K 31 | } 32 | 33 | /* Enforce emmition of the vector table. */ 34 | EXTERN (vector_table) 35 | 36 | /* Define the entry point of the output file. */ 37 | ENTRY(resetHandler) 38 | 39 | /* This provide weak linking to empty handlers, which can be overridden */ 40 | PROVIDE(nmiHandler = nullHandler); 41 | PROVIDE(hardFaultHandler = blockingHandler); 42 | PROVIDE(memoryManagementFaultHandler = blockingHandler); 43 | PROVIDE(busFaultHandler = blockingHandler); 44 | PROVIDE(usageFaultHandler = blockingHandler); 45 | PROVIDE(svCallHandler = nullHandler); 46 | PROVIDE(debugMonitorHandler = nullHandler); 47 | PROVIDE(pendSVHandler = nullHandler); 48 | PROVIDE(sysTickHandler = nullHandler); 49 | 50 | /* Define sections. */ 51 | SECTIONS 52 | { 53 | .text : { 54 | *(.vectors) /* Vector table */ 55 | *(.text*) /* Program code */ 56 | . = ALIGN(4); 57 | *(.rodata*) /* Read-only data */ 58 | . = ALIGN(4); 59 | } >rom 60 | 61 | /* C++ Static constructors/destructors, also used for __attribute__ 62 | * ((constructor)) and the likes */ 63 | .preinit_array : { 64 | . = ALIGN(4); 65 | __preinit_array_start = .; 66 | KEEP (*(.preinit_array)) 67 | __preinit_array_end = .; 68 | } >rom 69 | .init_array : { 70 | . = ALIGN(4); 71 | __init_array_start = .; 72 | KEEP (*(SORT(.init_array.*))) 73 | KEEP (*(.init_array)) 74 | __init_array_end = .; 75 | } >rom 76 | .fini_array : { 77 | . = ALIGN(4); 78 | __fini_array_start = .; 79 | KEEP (*(.fini_array)) 80 | KEEP (*(SORT(.fini_array.*))) 81 | __fini_array_end = .; 82 | } >rom 83 | 84 | /* 85 | * Another section used by C++ stuff, appears when using newlib with 86 | * 64bit (long long) printf support 87 | */ 88 | .ARM.extab : { 89 | *(.ARM.extab*) 90 | } >rom 91 | .ARM.exidx : { 92 | __exidx_start = .; 93 | *(.ARM.exidx*) 94 | __exidx_end = .; 95 | } >rom 96 | 97 | . = ALIGN(4); 98 | _etext = .; 99 | 100 | /* ram, but not cleared on reset, eg boot/app comms */ 101 | .noinit (NOLOAD) : { 102 | *(.noinit*) 103 | } >ram 104 | . = ALIGN(4); 105 | 106 | .data : { 107 | _data = .; 108 | *(.data*) /* Read-write initialized data */ 109 | *(.ramtext*) /* "text" functions to run in ram */ 110 | . = ALIGN(4); 111 | _edata = .; 112 | } >ram AT >rom 113 | _data_loadaddr = LOADADDR(.data); 114 | 115 | .bss : { 116 | _bss = .; 117 | *(.bss*) /* Read-write zero initialized data */ 118 | *(COMMON) 119 | . = ALIGN(4); 120 | _ebss = .; 121 | } >ram 122 | 123 | /* 124 | * The .eh_frame section appears to be used for C++ exception handling. 125 | * You may need to fix this if you're using C++. 126 | */ 127 | /DISCARD/ : { *(.eh_frame) } 128 | 129 | . = ALIGN(4); 130 | end = .; 131 | } 132 | 133 | PROVIDE(_stack = ORIGIN(ram) + LENGTH(ram)); 134 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const regs = @import("registers.zig"); 2 | 3 | pub fn main() void { 4 | systemInit(); 5 | 6 | // Enable GPIOD port 7 | regs.RCC.AHB1ENR.modify(.{ .GPIODEN = 1 }); 8 | 9 | // Set pin 12/13/14/15 mode to general purpose output 10 | regs.GPIOD.MODER.modify(.{ .MODER12 = 0b01, .MODER13 = 0b01, .MODER14 = 0b01, .MODER15 = 0b01 }); 11 | 12 | // Set pin 12 and 14 13 | regs.GPIOD.BSRR.modify(.{ .BS12 = 1, .BS14 = 1 }); 14 | 15 | while (true) { 16 | // Read the LED state 17 | var leds_state = regs.GPIOD.ODR.read(); 18 | // Set the LED output to the negation of the currrent output 19 | regs.GPIOD.ODR.modify(.{ 20 | .ODR12 = ~leds_state.ODR12, 21 | .ODR13 = ~leds_state.ODR13, 22 | .ODR14 = ~leds_state.ODR14, 23 | .ODR15 = ~leds_state.ODR15, 24 | }); 25 | 26 | // Sleep for some time 27 | var i: u32 = 0; 28 | while (i < 600000) { 29 | asm volatile ("nop"); 30 | i += 1; 31 | } 32 | } 33 | } 34 | 35 | fn systemInit() void { 36 | // This init does these things: 37 | // - Enables the FPU coprocessor 38 | // - Sets the external oscillator to achieve a clock frequency of 168MHz 39 | // - Sets the correct PLL prescalers for that clock frequency 40 | // - Enables the flash data and instruction cache and sets the correct latency for 168MHz 41 | 42 | // Enable FPU coprocessor 43 | // WARN: currently not supported in qemu, comment if testing it there 44 | regs.FPU_CPACR.CPACR.modify(.{ .CP = 0b11 }); 45 | 46 | // Enable HSI 47 | regs.RCC.CR.modify(.{ .HSION = 1 }); 48 | 49 | // Wait for HSI ready 50 | while (regs.RCC.CR.read().HSIRDY != 1) {} 51 | 52 | // Select HSI as clock source 53 | regs.RCC.CFGR.modify(.{ .SW0 = 0, .SW1 = 0 }); 54 | 55 | // Enable external high-speed oscillator (HSE) 56 | regs.RCC.CR.modify(.{ .HSEON = 1 }); 57 | 58 | // Wait for HSE ready 59 | while (regs.RCC.CR.read().HSERDY != 1) {} 60 | 61 | // Set prescalers for 168 MHz: HPRE = 0, PPRE1 = DIV_2, PPRE2 = DIV_4 62 | regs.RCC.CFGR.modify(.{ .HPRE = 0, .PPRE1 = 0b101, .PPRE2 = 0b100 }); 63 | 64 | // Disable PLL before changing its configuration 65 | regs.RCC.CR.modify(.{ .PLLON = 0 }); 66 | 67 | // Set PLL prescalers and HSE clock source 68 | // TODO: change the svd to expose prescalers as packed numbers instead of single bits 69 | regs.RCC.PLLCFGR.modify(.{ 70 | .PLLSRC = 1, 71 | // PLLM = 8 = 0b001000 72 | .PLLM0 = 0, 73 | .PLLM1 = 0, 74 | .PLLM2 = 0, 75 | .PLLM3 = 1, 76 | .PLLM4 = 0, 77 | .PLLM5 = 0, 78 | // PLLN = 336 = 0b101010000 79 | .PLLN0 = 0, 80 | .PLLN1 = 0, 81 | .PLLN2 = 0, 82 | .PLLN3 = 0, 83 | .PLLN4 = 1, 84 | .PLLN5 = 0, 85 | .PLLN6 = 1, 86 | .PLLN7 = 0, 87 | .PLLN8 = 1, 88 | // PLLP = 2 = 0b10 89 | .PLLP0 = 0, 90 | .PLLP1 = 1, 91 | // PLLQ = 7 = 0b111 92 | .PLLQ0 = 1, 93 | .PLLQ1 = 1, 94 | .PLLQ2 = 1, 95 | }); 96 | 97 | // Enable PLL 98 | regs.RCC.CR.modify(.{ .PLLON = 1 }); 99 | 100 | // Wait for PLL ready 101 | while (regs.RCC.CR.read().PLLRDY != 1) {} 102 | 103 | // Enable flash data and instruction cache and set flash latency to 5 wait states 104 | regs.FLASH.ACR.modify(.{ .DCEN = 1, .ICEN = 1, .LATENCY = 5 }); 105 | 106 | // Select PLL as clock source 107 | regs.RCC.CFGR.modify(.{ .SW1 = 1, .SW0 = 0 }); 108 | 109 | // Wait for PLL selected as clock source 110 | var cfgr = regs.RCC.CFGR.read(); 111 | while (cfgr.SWS1 != 1 and cfgr.SWS0 != 0) : (cfgr = regs.RCC.CFGR.read()) {} 112 | 113 | // Disable HSI 114 | regs.RCC.CR.modify(.{ .HSION = 0 }); 115 | } 116 | -------------------------------------------------------------------------------- /src/startup.zig: -------------------------------------------------------------------------------- 1 | const main = @import("main.zig"); 2 | 3 | // These symbols come from the linker script 4 | extern const _data_loadaddr: u32; 5 | extern var _data: u32; 6 | extern const _edata: u32; 7 | extern var _bss: u32; 8 | extern const _ebss: u32; 9 | 10 | export fn resetHandler() void { 11 | // Copy data from flash to RAM 12 | const data_loadaddr = @ptrCast([*]const u8, &_data_loadaddr); 13 | const data = @ptrCast([*]u8, &_data); 14 | const data_size = @ptrToInt(&_edata) - @ptrToInt(&_data); 15 | for (data_loadaddr[0..data_size]) |d, i| data[i] = d; 16 | 17 | // Clear the bss 18 | const bss = @ptrCast([*]u8, &_bss); 19 | const bss_size = @ptrToInt(&_ebss) - @ptrToInt(&_bss); 20 | for (bss[0..bss_size]) |*b| b.* = 0; 21 | 22 | // Call contained in main.zig 23 | main.main(); 24 | 25 | unreachable; 26 | } 27 | -------------------------------------------------------------------------------- /src/vector.zig: -------------------------------------------------------------------------------- 1 | // These two are the default empty implementations for exception handlers 2 | export fn blockingHandler() void { 3 | while (true) {} 4 | } 5 | 6 | export fn nullHandler() void {} 7 | 8 | // This comes from the linker script and represents the initial stack pointer address. 9 | // Not a function, but pretend it is to suppress type error 10 | extern fn _stack() void; 11 | 12 | // These are the exception handlers, which are weakly linked to the default handlers 13 | // in the linker script 14 | extern fn resetHandler() void; 15 | extern fn nmiHandler() void; 16 | extern fn hardFaultHandler() void; 17 | extern fn memoryManagementFaultHandler() void; 18 | extern fn busFaultHandler() void; 19 | extern fn usageFaultHandler() void; 20 | extern fn svCallHandler() void; 21 | extern fn debugMonitorHandler() void; 22 | extern fn pendSVHandler() void; 23 | extern fn sysTickHandler() void; 24 | 25 | // The vector table 26 | export const vector_table linksection(".vectors") = [_]?fn () callconv(.C) void{ 27 | _stack, 28 | resetHandler, // Reset 29 | nmiHandler, // NMI 30 | hardFaultHandler, // Hard fault 31 | memoryManagementFaultHandler, // Memory management fault 32 | busFaultHandler, // Bus fault 33 | usageFaultHandler, // Usage fault 34 | null, // Reserved 1 35 | null, // Reserved 2 36 | null, // Reserved 3 37 | null, // Reserved 4 38 | svCallHandler, // SVCall 39 | debugMonitorHandler, // Debug monitor 40 | null, // Reserved 5 41 | pendSVHandler, // PendSV 42 | sysTickHandler, // SysTick 43 | }; 44 | --------------------------------------------------------------------------------