{"id":15099812,"url":"https://github.com/aleh/bluepill-zig","last_synced_at":"2026-02-27T19:46:23.286Z","repository":{"id":253794899,"uuid":"844245233","full_name":"aleh/bluepill-zig","owner":"aleh","description":"Bare Metal Zig on STM32","archived":false,"fork":false,"pushed_at":"2024-08-21T21:31:14.000Z","size":23,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-02-01T18:15:01.499Z","etag":null,"topics":["bluepill","stm32","stm32f103","zig"],"latest_commit_sha":null,"homepage":"","language":"Zig","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aleh.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-08-18T20:14:42.000Z","updated_at":"2024-08-21T21:31:16.000Z","dependencies_parsed_at":"2024-08-19T15:58:58.756Z","dependency_job_id":"a50a1e72-1550-41ac-9530-e32b6abd042c","html_url":"https://github.com/aleh/bluepill-zig","commit_stats":null,"previous_names":["aleh/bluepill-zig"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aleh%2Fbluepill-zig","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aleh%2Fbluepill-zig/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aleh%2Fbluepill-zig/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aleh%2Fbluepill-zig/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aleh","download_url":"https://codeload.github.com/aleh/bluepill-zig/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245858876,"owners_count":20684057,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bluepill","stm32","stm32f103","zig"],"created_at":"2024-09-25T17:27:50.941Z","updated_at":"2025-10-06T20:44:13.446Z","avatar_url":"https://github.com/aleh.png","language":"Zig","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Bare Metal Zig on STM32\n\nThis is about using [Zig](https://ziglang.org) alone to directly program boards based on STM32F103xx MCU, such as \"Blue\nPill\" clones of Maple Mini.\n\n## Requirements\n\n- A Blue Pill board or similar, see [here](https://stm32-base.org/boards/STM32F103C8T6-Blue-Pill.html) for general and\n  physical info.\n  \n- [Zig](https://ziglang.org/download/) to build our examples. (I used version 0.13.0 here.)\n\n- [ST-Link Tools](https://github.com/stlink-org/stlink) to flash them.\n\n## Docs\n\n- [Blue Pill Schematic](https://stm32-base.org/assets/pdf/boards/original-schematic-STM32F103C8T6-Blue_Pill.pdf).\n\n- [STM32 Cortex®-M3 Programming Manual](https://www.st.com/resource/en/programming_manual/pm0056-stm32f10xxx20xxx21xxxl1xxxx-cortexm3-programming-manual-stmicroelectronics.pdf) for general info on Cortex-M3.\n\n- [STM32F10x Reference Manual](https://www.st.com/resource/en/reference_manual/cd00171190-stm32f101xx-stm32f102xx-stm32f103xx-stm32f105xx-and-stm32f107xx-advanced-arm-based-32-bit-mcus-stmicroelectronics.pdf) to know how to program all available peripherals.\n\n- [STM32F103x8 Datasheet](https://www.st.com/resource/en/datasheet/stm32f103c8.pdf) to know what exactly is available\n  in our MCU as the above reference manual describes the whole family.\n\n- [Info on Linker Scripts](https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_chapter/ld_3.html) to be able to describe memory layout to the linker.\n\n## Target\n\nOne of the cool things about Zig is that (thanks to LLVM) it can compile for many architectures out of the box and yet\nmanages to keep its MacOS package size under 50MB without any external dependencies!\n\nZig expects its `-target` command line switch to be a dash-separated triple identifying architecture, operating system\nand ABI (application binary interface) of the target system. See the whole list by running:\n\n    zig targets | less\n\nOur architecture is ARM, we don't have any OS and we don't care about any particular ABI, so we are going to use\n`arm-freestanding-none` as our `-target`.\n\nAnother command line switch, `-mcpu`, should be used to specify the processor to generate the code for. STM32F103xx is\nbased on Cortext-M3, so consulting the list of supported targets the most logical choice seemed to be `cortex_m3`. I am\ngetting build errors when using one however (something about an instruction in `IT` block), so I decided to step back\nto `cortex_m23` that seems to be a subset of Cortext-M3 instruction-wise, missing exactly the `IT` instruction. (I\ndon't expect the compiler to use \"TrustZone\" security extensions. We could also use `cortex_m0` to be 100% sure it\nwon't generate something unsupported.)\n\nBefore we go to the remaining compiler settings let's type in some code first. Let's make the classic \"Blink\" example toggling the on-board LED (see `v0/main.zig`). It's not going to be nice, but we'll improve it later.\n\n## Version 0\n\nLooking at the Blue Pill Schematic we see that the LED is attached to PC13 of the MCU which can be controlled via port 13 of GPIO bank C. GPIO and the corresponding registers are described in Chapter 9 of the Reference Manual, while the base address of bank C, `0x4001_1000`, can be found in the STM32F103x8 Datasheet. \n\nBefore we can use the GPIO bank C however we need to enable it via one of the Reset and Clock Control (RCC) registers (base `0x4002_1000`), see chapter 7.3. GPIO bank C is controlled by bit 4 of `RCC_APB2ENR`, offset `0x18`:\n\n```zig\nreg(0x4002_1000 + 0x18).* |= 1 \u003c\u003c 4;\n```\n\nWhere `reg()` is a simple wrapper that gets us a `volatile` pointer, which we need when working with memory mapped registers for Zig's optimizer to not try removing or reorder our reads/writes.\n\n```zig\nfn reg(comptime address: u32) *volatile u32 {\n    return @ptrFromInt(address);\n}\n```\n\nBefore we can start toggling port 13 however we need to configure it as output via `GPIOx_CRH` register (offset `0x04`, see chapter 9.2.2). Each nibble in this register is responsible for configuration of ports 8-15:\n\n```zig\nreg(0x4001_1000 + 0x04).* |= 0b01_10 \u003c\u003c (4 * (13 - 8)); // Relying on the reset value being 0b01_00.\n```\n\nWe'll be using `GPIOx_BSRR` register (offset `0x10` from the base, see chapter 9.2.5) to control the output state of our port. Setting bits 0-15 here sets the output on ports 0-15 to 1, while setting bits 16-32 *resets* the output on the same ports. (This register is more convenient than more traditional `GPIOx_ODR`, because there is no need to read its current state to modify a single bit.)\n\n```zig\nconst GPIOC_BSRR = reg(0x4001_1000 + 0x10);\nwhile (true) {\n    GPIOC_BSRR.* = 1 \u003c\u003c (13 + 16);\n    delay(50);\n    GPIOC_BSRR.* = 1 \u003c\u003c 13;\n    delay(950);\n}\n```\n\nTo implement `delay()` we'll just do something in a long loop. (We'll return to better implementation in the next version of the example.)\n\n```zig\nfn delay_ticks(ticks: u32) void {\n    var i = ticks;\n    while (i \u003e 0) {\n        // Reading any location to prevent the loop from being optimized out.\n        _ = reg(0x2000_0000).*;\n        i -= 1;\n    }\n}\n```\n\nTo calculate the number of ticks we need to iterate to get a millisecond delay we need to know that after reset our CPU runs approximately at 8MHz and that every iteration in the above loop takes 6 CPU cycles (more on this below):\n\n```zig\nfn delay(comptime ms: u32) void {\n    delay_ticks(ms * 8_000 / 6);\n}\n```\n\n## Linker Script\n\nOK, now when we have a basic program (see `v0/main.zig`) we can try to compile it:\n\n    zig build-exe -target arm-freestanding-none -mcpu cortex_m23 -O ReleaseSmall -femit-asm main.zig\n\nThe extra `-femit-asm` flag makes Zig produce assembly output which is handy when examining our code. For example, this is how we can calculate how many CPU clock cycles `delay_ticks()` spends per tick (comments added by me using info on [this page](https://developer.arm.com/documentation/ddi0337/e/Instruction-Timing/Processor-instruction-timings)):\n\n```asm\nmain.delay_ticks:           ; r0 contains the number of ticks already.\n    movs    r1, #1\n    lsls    r1, r1, #29     ; (1 \u003c\u003c 29) is this 0x20000000 address we are reading from below.\n.LBB1_1:\n    cbz r0, .LBB1_3         ; 1 cycle, branch not taken. (Jump out of the loop if tick counter in r0 is zero.)\n    ldr r2, [r1]            ; 2 cycles. (Our fake read.)\n    subs    r0, r0, #1      ; 1 cycle. (Decrement the tick counter in r0.)\n    b   .LBB1_1             ; 2 cycles, branch is taken. (Repeat the loop.)\n.LBB1_3:\n    bx  lr                  ; Return from the function.\n```\n\nSpeaking of assembly, we could also disassemble the output file directly with `objdump -d main`, but it might be harder to see what's going on, here is the same `delay_ticks()` function:\n\n    20130: 2101             movs    r1, #1\n    20132: 0749             lsls    r1, r1, #29\n    20134: b110             cbz r0, 0x2013c \u003c.text+0x50\u003e @ imm = #4\n    20136: 680a             ldr r2, [r1]\n    20138: 1e40             subs    r0, r0, #1\n    2013a: e7fb             b   0x20134 \u003c.text+0x48\u003e    @ imm = #-10\n    2013c: 4770             bx  lr\n\nAnother thing that we can see with `objdump` is that our code starts at address `000200ec` which is quite wrong for our MCU where flash memory begins at `0x08000000`:\n\n    main:   file format elf32-littlearm\n\n    Disassembly of section .text:\n\n    000200ec \u003c.text\u003e:\n       200ec: 480c          ldr r0, [pc, #48]           @ 0x20120 \u003c.text+0x34\u003e\n       200ee: 6801          ldr r1, [r0]\n       200f0: 2210          movs    r2, #16\n    ...\n\nWell, this is logical because Zig does not really know much about our MCU. We need to help it by writing a \"linker script\". The official documentation on the topic mentioned above is easy to read and actual scripts are fairly self-explanatory.\n\nThe first thing we do in our script (`v0/bluepill.ld`) is describing relevant memory regions, which is quite simple in our case as we have 128K of flash memory starting at `0x08000000` and 20K of RAM starting at `0x20000000` (see chapter 4 in the Datasheet):\n\n    MEMORY {\n        flash (rx)  : o = 0x08000000, l = 128K\n        sram (rw)   : o = 0x20000000, l = 20K\n    }\n\n(The names of the regions can be arbitrary here, the linker does not know what the \"flash\" is.)\n\nThe next part of the script tells what should be placed into the flash memory:\n\n    SECTIONS {\n        .text : {\n            ...\n        } \u003eflash\n\nWe cannot tell it to begin filling with the code from the start as the first word has to be the value of the main stack pointer (MSP), as per chapter 2.1.2 of the Programming Manual:\n\n\u003e On reset, the processor loads the MSP with the value from address 0x00000000.\n\n(The address is from the start of the flash, `0x08000000` in our case.)\n\nNext go interrupt vectors (see table 63 in the Reference Manual) of which we are only interested in the first one, Reset, as we don't use interrupts just yet:\n\n    .text : {\n        /* The initial value of SP, past the end of RAM. */\n        LONG(ORIGIN(sram) + LENGTH(sram))\n        /* Reset vector. */\n        LONG(_start)\n        /* We should put a bunch of other vectors here, but since none are used yet we can use the space. */\n        /* So now goes our code. */\n        *(.text)\n        /* Then read-only data. */\n        *(.rodata.*) \n        *(.rodata) \n    } \u003eflash\n\nNext we tell that our writable data (static variables) is expected in RAM. We don't have such variables in our simple example yet, but that'll be handy later.\n\n    .bss : { \n        *(.bss) \n    } \u003esram\n\nAnd finally we exclude a few extra code segments that otherwise would increase the size of our binary:\n\n    /DISCARD/ : { \n        /* I don't want to keep sections needed only when printing stack traces. */\n        *(.ARM.*)\n    }\n\nLet's compile using our linker script now:\n\n    zig build-exe -target arm-freestanding-none -mcpu cortex_m23 -femit-asm -O ReleaseSmall --script bluepill.ld main.zig\n\nDisassembling with `objdump` shows that the addresses are correct now:\n\n    main:   file format elf32-littlearm\n\n    Disassembly of section .text:\n\n    08000000 \u003c.text\u003e:\n     8000000: 20005000      andhs   r5, r0, r0\n     8000004: 08000009      stmdaeq r0, {r0, r3}\n     8000008: 6801480c      stmdavs r1, {r2, r3, r11, lr}\n    ...\n\nThe first word appears to be the desired stack pointer just beyond the RAM followed by the Reset vector pointing to the next word. The number is even to indicate Thumb mode. Let's add `--mcpu=cortex-m23` to force Thumb mode:\n\n    08000000 \u003c.text\u003e:\n     8000000: 5000          str r0, [r0, r0]\n     8000002: 2000          movs    r0, #0\n     8000004: 0009          movs    r1, r1\n     8000006: 0800          lsrs    r0, r0, #32\n     8000008: 480c          ldr r0, [pc, #48]           @ 0x800003c \u003c.text+0x3c\u003e\n     800000a: 6801          ldr r1, [r0]\n     800000c: 2210          movs    r2, #16\n     ...\n     \nOK, now the part starting at `0x8000008` looks like the code in our `.s` file.\n\n## Flashing\n\nWe'll be using `st-flash` utility which expects either a raw binary with the starting address passes separately or an Intel hex file that already contains addresses. Let's use the latter by converting our build to `.hex` with Zig:\n\n    zig objcopy -O hex main main.hex\n\nFlashing is then as simple as:\n\n    st-flash --reset --format ihex write main.hex \n\nYou'll see something like this and your LED will hopefully start blinking every second:\n\n    st-flash 1.8.0\n    2024-08-18T22:01:38 INFO common.c: STM32F1xx_MD: 20 KiB SRAM, 128 KiB flash in at least 1 KiB pages.\n    2024-08-18T22:01:38 INFO common_flash.c: Attempting to write 90 (0x5a) bytes to stm32 address: 134217728 (0x8000000)\n    -\u003e Flash page at 0x8000000 erased (size: 0x400)\n    2024-08-18T22:01:38 INFO flash_loader.c: Starting Flash write for VL/F0/F3/F1_XL\n    2024-08-18T22:01:38 INFO flash_loader.c: Successfully loaded flash loader in sram\n    2024-08-18T22:01:38 INFO flash_loader.c: Clear DFSR\n      1/1   pages written\n    2024-08-18T22:01:38 INFO common_flash.c: Starting verification of write complete\n    2024-08-18T22:01:38 INFO common_flash.c: Flash written and verified! jolly good!\n\nAlso, as you can see our code is just 90 bytes, which is quite nice given all the required setup instructions.\n\n## V1\n\nNow let's improve the example showing some power of Zig:\n\n```zig\nexport fn _start() noreturn {\n    const bankC = GPIOBank(.C);\n    bankC.init();\n\n    const led = bankC.port(13);\n    led.setOutput(.openDrain, .max2MHz);\n\n    while (true) {\n        led.reset();\n        delay(50);\n        led.set();\n        delay(950);\n    }\n}\n```\n\nThe `GPIOBank` is an abstraction that is more readable, more reusable (we can use all banks/ports) but does not add any overhead as all the selection of the bank and port happen at compile time. Our program is 94 bytes now, which is just 4 bytes larger only because we are not relying on the reset values when writing to `GPIOC_CRH` as we want to change pin configuration at runtime:\n\n```zig\npub fn GPIOBank(comptime bank: GPIOBankIndex) type {\n    return struct {\n        /* ... */\n        pub fn port(comptime pin: u4) type {\n            return struct {\n                fn reg(comptime offset: u32) *volatile u32 {\n                    return @ptrFromInt(switch (bank) {\n                        .A =\u003e 0x4001_0800,\n                        .B =\u003e 0x4001_0C00,\n                        .C =\u003e 0x4001_1000,\n                        .D =\u003e 0x4001_1400,\n                        .E =\u003e 0x4001_1800,\n                    } + offset);\n                }\n\n                fn setModeBits(comptime bits: u32) void {\n                    const CRx = reg(if (pin \u003e= 8) 0x04 else 0x00);\n                    const shift = 4 * @as(u8, if (pin \u003e= 8) pin - 8 else pin);\n                    CRx.* = CRx.* \u0026 ~(@as(u32, 0xF) \u003c\u003c shift) | (bits \u003c\u003c shift);\n                }\n                \n                const BSRR = reg(0x10);\n\n                pub fn set() void {\n                    BSRR.* = 1 \u003c\u003c pin;\n                }\n                /* ... */\n            };\n        }\n    };\n}\n```\n\nAs you can see `reg()` depends on the bank, but since both `bank` and `pin` are `comptime`, thus both `GPIOx_CRx` (L or H) and `GPIOx_BSRR` are picked at compile time as well and we always get highly optimized code.\n\n## V2\n\nHere we are adding a `SysTick` timer for a better `delay()` along with `USART` to say the actual `Hello`. (I've moved all helpers into a module called `z41` here.)\n\nOur binary is `792` bytes now, or `314` if we completely remove the line with `usart.writer.print`, and `432` if we keep it but don't output the number. In other words, the use of `std.fmt` adds overhead only when specifiers are actually used, something that would be hard to achieve with a `printf()`-style C/C++ function.\n\n```zig\nconst std = @import(\"std\");\nconst z41 = @import(\"z41\");\n\nexport fn _start() noreturn {\n    const rcc = z41.RCC(.internalRC);\n    rcc.init();\n\n    const SysTick = z41.SysTick(rcc.SYSCLK, 50);\n    SysTick.init();\n\n    const led = z41.GPIO(rcc, .C).port(13);\n    led.Bank.init();\n    led.setOutput(.openDrain, .max2MHz);\n\n    const usart = z41.USART(rcc, .usart1);\n    usart.init(115200);\n\n    usart.writeBytes(\"\\nHello! It's V2\\n\\n\");\n\n    while (true) {\n        led.reset();\n        SysTick.delay(50);\n\n        led.set();\n        SysTick.delay(950);\n\n        try usart.writer.print(\"\\rUptime: {}s\", .{SysTick.milliseconds() / 1000});\n    }\n}\n```\n\n### RegisterSet\n\nI've added `RegisterSet` under the hood to help with definition of hardware registers. It's similar to this `reg()` helper from `v0`, but allows using structs as well. For example, this is how `STK_CTRL` is described in `SysTick` (you should appreciate Zig allowing anonymous enums like here in `CLKSOURCE`):\n\n```zig\nconst STK_CTRL = regs.at(0, packed struct(u32) {\n    /// Counter enable.\n    ENABLE: bool,\n    TICKINT: bool,\n    CLKSOURCE: enum(u1) {\n        /// AHB/8.\n        AHB_8 = 0,\n        /// Processor clock (AHB).\n        AHB = 1,\n    },\n    _r1: u13 = 0,\n    COUNTFLAG: bool = false,\n    _r2: u15 = 0,\n});\n```\n\nYou can still use raw `u32` registers where needed:\n\n```zig\nconst STK_LOAD = regs.at(4, u32);\n```\n\nThe helper checks the type you pass to make sure it's `u32` or `u32`-backed `packed struct`:\n\n```zig\npub fn at(comptime offset: u32, comptime reg_type: type) *volatile reg_type {\n    const valid = switch (@typeInfo(reg_type)) {\n        .Struct =\u003e |s| switch (s.layout) {\n            .@\"packed\" =\u003e s.backing_integer == u32,\n            else =\u003e false,\n        },\n        .Int =\u003e |i| i.bits == 32,\n        else =\u003e false,\n    };\n    if (!valid) {\n        @compileError(\"Expected `reg_type` to be u32 or a packed struct backed by u32\");\n    }\n    return @ptrFromInt(base + offset);\n}\n```\n\n### SysTick\n\nA `SysTick` timer is described in the chapter 4.5 of the Programming Manual and is something common to all processors based on Cortex®-M3. It's a simple counter that is decremented on every (or every 8ths) CPU clock cycle and generates an interrupt when it reaches zero. It can be used to implement a notion of system time (e.g. milliseconds since system start) along with better delays, where we don't have to rely on how exactly our code is compiled.\n\nWe need to be able to handle interrupts for this helper and this is where our linker script needs to be changed. The handler itself is simple:\n\n```zig\npub fn SysTick(comptime cpuFreq: u32, comptime msPerTick: u32) type {\n    return struct {\n        export fn SysTick_Vector() void {\n            total_ms +%= msPerTick;\n        }\n\n        var total_ms: u32 = undefined;\n        ...\n```\n\nIt needs to be `export`ed for our linker script to place a pointer to it into an appropriate location. (Note that the export only happens when `SysTick` is used, something that would be hard to achieve in C/C++ without macros.) \n\nOther than `export` no other attributes are needed here thanks to the clever way interrupts (\"exceptions\") are handled (see chapter 2.3.7 in the Programming Manual):\n\n- registers `r0`-`r3` are automatically pushed to the stack along with flags when an interrupt occurs, while remaining registers are already expected to be preserved by the compiler even in regular functions;\n\n- unlike other architectures no special \"return from interrupt\" instruction is needed, because the return address in `LR` register is set to a special value that any return from function (`bx lr`) will be recognized as a return from an interrupt restoring `r0`-`r3`, etc.\n\nSo we need to add a pointer to our handler into the interrupt vector table at the start of our code (see table 63 in the Reference Manual again):\n\n    .text : {\n        ...\n    \tLONG(_start);\n    \n        /* We don't use any interrupt vectors before SysTick, so let's just fill.  */\n        FILL(0); . = ADDR(.text) + 0x003C;\n    \n        LONG(DEFINED(SysTick_Vector) ? SysTick_Vector : 0xDEAD);\n    \n    \t/* Other vectors follow, but since we are not using them we can just start our code earlier. */\n        ...\n\nNote the use of `DEFINED`: it allows correct linking even when the target program does not need the `SysTick` timer. (By the way, the use of `0xDEAD` for undefined handlers is temporary here, a central \"panic\" handler halting the MCU would be a better option eventually.)\n\n### `build.zig`\n\nI've been using a simple shell script to build and flash the first 2 examples:\n\n```bash\n#!/bin/sh -e\nzig build-exe \\\n\t-target arm-freestanding-none \\\n\t-mcpu cortex_m23 \\\n\t-femit-asm \\\n\t-O ReleaseSmall \\\n\t--script bluepill.ld \\\n\tmain.zig\nzig objcopy -O hex main main.hex\nrm main main.o\nst-flash --reset --format ihex write main.hex \n```\n\nHowever in this one we want to be able to pull our helpers from a \"module\" in `./lib`. This still could be described in a shell script of course, but I also was curious about Zig's build system, so I've added `build.zig`.\n\nNow the example can be compiled with `zig build` or flashed with `zig build flash`.\n\n---\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faleh%2Fbluepill-zig","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faleh%2Fbluepill-zig","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faleh%2Fbluepill-zig/lists"}