{"article":{"slug":"blinking-a-blue-pill-led-with-rust-from-scratch","title":"Blinking a Blue Pill LED with Rust, from scratch","subtitle":null,"summary":"A from-scratch Blue Pill walkthrough in Rust without a HAL: wiring and flashing, the reset vector, volatile GPIO writes, delay loops, and the assumptions a working blink can hide.","content_type":"tutorial","language":"en","canonical_url":"https://unmb.pw/blog/misc/2026/09/24/blue-pill-rust-from-scratch.html","author":{"name":"unmanbearpig","url":"https://unmb.pw/","person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"unmanbearpig","url":"https://unmb.pw/","listing_slug":null,"listing":null},"topics":[{"name":"Rust","slug":"rust","url":"https://listedarticles.com/topics/rust"},{"name":"Hardware","slug":"hardware","url":"https://listedarticles.com/topics/hardware"},{"name":"Embedded","slug":"embedded","url":"https://listedarticles.com/topics/embedded"},{"name":"Tutorial","slug":"tutorial","url":"https://listedarticles.com/topics/tutorial"},{"name":"Systems Programming","slug":"systems-programming","url":"https://listedarticles.com/topics/systems-programming"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":5155,"reading_minutes":22,"published_at":"2026-09-24T04:30:10.000Z","added_at":"2026-09-26T06:11:45.709Z","updated_at":"2026-09-26T06:11:45.709Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/blinking-a-blue-pill-led-with-rust-from-scratch","markdown_url":"https://listedarticles.com/articles/blinking-a-blue-pill-led-with-rust-from-scratch.md","example":false,"citation":"unmanbearpig, unmanbearpig. \"Blinking a Blue Pill LED with Rust, from scratch.\" 24 Sept 2026. https://unmb.pw/blog/misc/2026/09/24/blue-pill-rust-from-scratch.html (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://unmb.pw/blog/misc/2026/09/24/blue-pill-rust-from-scratch.html"},"body_markdown":"# Blinking a Blue Pill LED with Rust, from scratch\n\nI wanted to figure out how microcontrollers work, so I wrote a small\n[LED-blinking program in Rust, available on GitHub](https://github.com/unmanbearpig/blinky-from-scratch/tree/d518188bb144e7dab077e27b4d3de7388461177a).\n\nMost well-documented ways to program a microcontroller use libraries and generated code to make the job easier. If you’re trying to understand the hardware, you end up working backwards through those layers. Beginner-friendly material that starts from scratch is harder to find.\n\nI know how compilers work well enough that using Rust wouldn’t get in the way. It also gives me a chance to learn more about Rust: raw pointers, volatile access, and what it takes to run without the usual startup code.\n\nLeaving out the helper libraries is a good way to test vague knowledge. “The CPU starts executing after reset” sounds simple until you have to put its starting address in the right place.\n\nThe project has no external Rust dependencies. We’ll write to registers\ndirectly and look at the startup code and linker script. I’ll assume you\nalready have a Blue Pill and know how to program. First we’ll\n[get code onto the board](https://unmb.pw#get-the-program-onto-the-board), then\n[follow it from reset](https://unmb.pw#who-calls-main) to the LED.\n\n## The CPU, the microcontroller, and the board\n\nThe CPU in an STM32F103 is an Arm Cortex-M3. It executes instructions, uses a stack, and handles exceptions. ST combines that core with flash, RAM, timers, GPIO, and communication peripherals such as UART, SPI, I²C, and USB. That whole chip is the microcontroller.\n\nTwo chips can use the same CPU and have completely different peripherals.\nFor example, QEMU’s [Stellaris LM3S6965 board](https://www.qemu.org/docs/master/system/arm/stellaris.html) also has a\nCortex-M3. It can execute the same relevant instructions, but its GPIO registers\naren’t the STM32’s GPIO registers. Selecting a Cortex-M3 in an emulator doesn’t\nmake it a Blue Pill.\n\nThe Blue Pill is the circuit board around the chip. It adds the regulator, crystals, USB connector, reset button, boot jumpers, LEDs, and accessible pins. This project targets an STM32F103C8T6 board with its user LED on PC13. That chip has 64 KiB of flash and 20 KiB of RAM.\n\nCheck the marking on yours; Blue Pills don’t all come with the same chip.\n\nThe names we’ll use on the board are:\n\n| Label or part | What it does here | \n|---|---|\n| PC13 | The GPIO pin connected to the built-in user LED and its resistor. | \n| Power LED | Indicates board power. It is separate from the programmable LED. | \n| SWDIO / DIO / PA13 | Debug data connection. | \n| SWCLK / CLK / PA14 | Debug clock connection. | \n| GND and 3.3 V | Ground and the board’s 3.3 V supply rail. | \n| BOOT0 | Selects whether the chip starts from our flash program or another boot source. | \n| NRST / RST | Reset signal; the reset button also restarts the chip. | \n\nPC13 means pin 13 of GPIO port C, not pin 13 of the chip’s package. Similarly, PA13 and PA14 are the GPIO names of the pins used for debugging. Leave those two assigned to debugging for this example.\n\n## Where to find the information\n\nYou’ll want a few documents open. The board schematic, chip datasheet, and reference manual answer different questions. Searching for the right thing in the wrong PDF can waste a lot of time.\n\n| Document | What to look up | \n|---|---|\n| [Blue Pill board reference](https://stm32-base.org/boards/STM32F103C8T6-Blue-Pill.html) and[schematic](https://stm32-base.org/assets/pdf/boards/original-schematic-STM32F103C8T6-Blue_Pill.pdf) | Header pin labels, which pin the LED connects to, how the boot jumpers are wired, and where power comes from. | \n| [STM32F103x8/xB datasheet](https://www.st.com/resource/en/datasheet/stm32f103c8.pdf) | The chip’s pinout, package, memory sizes, supply and I/O limits, and restrictions on particular pins. Check the PC13 through PC15 footnote. | \n| [STM32F1 reference manual, RM0008](https://www.st.com/resource/en/reference_manual/cd00171190-stm32f101xx-stm32f102xx-stm32f103xx-stm32f105xx-and-stm32f107xx-advanced-arm-based-32-bit-mcus-stmicroelectronics.pdf) | Memory map, boot modes, clocks, and peripheral registers. For this program, start with the memory map, Reset and clock control, and General-purpose and alternate-function I/Os. | \n| [Cortex-M3 Devices Generic User Guide, DUI0552](https://developer.arm.com/documentation/dui0552/a/) | The CPU’s reset sequence, stack, vector table, exceptions, core registers, and instructions. ST’s [Cortex-M3 programming manual, PM0056](https://www.st.com/resource/en/programming_manual/cd00228163-stm32f10xxx20xxx21xxxl1xxxx-cortexm3-programming-manual-stmicroelectronics.pdf) is another useful reference for this part. | \n\nThe Blue Pill reference is maintained by the STM32-base community. Use your seller’s schematic if it matches the board you bought; compare the components and pin labels before treating a generic drawing as an exact match.\n\nThe STM32 datasheet gives the details of a particular chip. RM0008 covers peripherals shared across a larger family, including parts with peripherals and pins your chip may not have. Keep both open. The datasheet tells you what you have; the reference manual tells you how to program it.\n\nFor the Cortex-M3, the programming guide is what we need here. The core’s reset and exception behavior comes from Arm. The electrical limits of the STM32 pins come from ST’s datasheet.\n\n### Finding a register without guessing\n\nSuppose we want to enable GPIOC’s clock. In RM0008’s memory map, RCC starts\nat `0x40021000`. In the RCC chapter, the APB2 peripheral clock enable register,\n`RCC_APB2ENR`, has offset `0x18`. Its address is therefore:\n\n```\n0x40021000 + 0x18 = 0x40021018\n```\nThe register’s bit diagram labels bit 4 `IOPCEN`, the I/O port C clock enable.\nThat’s where the address and `1 << 4` in our code come from. Check the access\ntype, reset value, and reserved-bit notes too. Don’t assume every register can\nbe read and written like RAM.\n\nFor the LED, start with the board schematic to find PC13 and see how it’s connected. Then check the datasheet for the pin’s restrictions, and RM0008 for the GPIO configuration and set/reset registers. You can jump straight to the relevant chapters; there’s no need to read the whole manual first.\n\n### Other docs\n\nThe chip’s [product documentation page](https://www.st.com/en/microcontrollers-microprocessors/stm32f103c8.html#documentation) also lists its errata.\nThose describe known hardware bugs and workarounds. Match them to your part\nand silicon revision if a peripheral isn’t behaving as documented.\n\nUse the documentation for your exact debug probe for connector pinouts and power wiring. Black Magic’s firmware variant matters too; we’ll get to that in the setup instructions.\n\nFor the Rust side, the [Embedonomicon](https://docs.rust-embedded.org/embedonomicon/) walks through building\nan embedded program from scratch. The [libopencm3 GPIO definitions](https://libopencm3.org/docs/latest/stm32f1/html/group__gpio__defines.html)\nand [cortex-m-rt source](https://github.com/rust-embedded/cortex-m-rt) are also useful to read when you’re stuck.\nYou can study how a library does something without adding it as a dependency.\nThe Rust reference explains what [`#\\[used\\]`](https://doc.rust-lang.org/reference/abi.html#the-used-attribute) retains, and the\nstandard-library docs explain what [`write_volatile`](https://doc.rust-lang.org/core/ptr/fn.write_volatile.html) guarantees.\n\nFor `link.x`, use the [GNU ld linker-script manual](https://sourceware.org/binutils/docs/ld/Scripts.html) to look up\n`MEMORY`, `SECTIONS`, and `KEEP`. Rust’s bundled linker is LLD, which implements\nthis script syntax; its [implementation notes](https://lld.llvm.org/ELF/linker_script.html) describe differences\nfrom GNU ld. You don’t need to install a second linker to read its manual.\n\n## Get the program onto the board\n\nA debug probe connects the computer to the target microcontroller. The computer talks to the probe over USB. The probe talks to the target over SWD, Arm’s Serial Wire Debug interface. That lets a debugger halt execution, inspect memory, and arrange for a program to be written to flash.\n\nWe’ll use either an ST-Link or another Blue Pill running Black Magic firmware:\n\n```\nComputer → USB → ST-Link → SWD → target Blue Pill\n                   ↑\n           controlled by OpenOCD\nComputer → USB → Black Magic probe → SWD → target Blue Pill\n                         ↑\n                GDB connects directly\n```\nOpenOCD is the host program that controls the ST-Link. Black Magic includes a GDB server in the probe’s firmware, so GDB connects to it directly.\n\nI verified the Blue Pill running Black Magic route on my hardware: connecting to the target, flashing, and checking the written firmware. The ST-Link/OpenOCD instructions follow the tools’ documentation and should work, but I haven’t verified them on hardware. My Black Magic probe already had its firmware installed; preparing a blank probe is a separate prerequisite.\n\nThe target’s USB connector can supply power, but our firmware doesn’t implement USB. A stock STM32F103’s factory bootloader doesn’t provide USB flashing either. You can install an additional USB bootloader, but this guide uses SWD.\n\n### Build once, then choose your probe\n\nThe commands below assume Linux. The Rust project also builds on macOS and Windows; probe permissions and device paths differ.\n\nInstall [rustup](https://rustup.rs/) if necessary, then run:\n\n```\ngit clone https://github.com/unmanbearpig/blinky-from-scratch.git\ncd blinky-from-scratch\ngit checkout --detach d518188bb144e7dab077e27b4d3de7388461177a\nrustup toolchain install 1.96.1 --profile minimal \\\n  --component rustfmt --component clippy --target thumbv7m-none-eabi\ncargo build --release --locked\n```\nThe checkout selects [firmware revision `d518188`](https://github.com/unmanbearpig/blinky-from-scratch/tree/d518188bb144e7dab077e27b4d3de7388461177a), used for the code,\ndisassembly, and sizes in this article.\n\n`rust-toolchain.toml` pins that compiler version. The target\n`thumbv7m-none-eabi` selects the Cortex-M3-compatible instruction set and\nbare-metal environment. `.cargo/config.toml` selects that target and passes\n`link.x` to Rust’s bundled linker. Building doesn’t require Arm GCC.\n\nThe output is:\n\n```\ntarget/thumbv7m-none-eabi/release/blinky-from-scratch\n```\nThis is an ELF file, containing the program and information about where its parts belong in memory. Both flashing tools below understand ELF. They can use those addresses directly, so you don’t need a raw binary or a separate flash address.\n\nUse the release build. The delay is a busy loop, and compiler optimization affects its timing. We’ll inspect that loop later.\n\n### Wire the target\n\nDisconnect power while attaching wires. Set the target’s BOOT0 jumper to 0, so it boots our program from flash after reset. BOOT1 doesn’t affect this mode.\n\nBoth probes need these connections:\n\n| Probe signal | Target Blue Pill | \n|---|---|\n| SWDIO | SWDIO / DIO / PA13 | \n| SWCLK | SWCLK / CLK / PA14 | \n| GND | GND | \n| NRST, if available | NRST / RST, optional for normal programming | \n\nRead the labels on your probe. Connector layouts vary, especially on ST-Link clones. Connecting NRST can help recover a target whose existing firmware interferes with normal debug access; my Black Magic setup worked without it.\n\nPower wiring depends on the probe, so follow the relevant section below.\n\n### Option A: a Blue Pill running Black Magic\n\nMy probe runs Black Magic’s SWLINK firmware, `v1.6.1-409-g7a595ea`. On that\nbuild, PA13 and PA14 are the probe’s connections to the target:\n\n| Probe Blue Pill | Target Blue Pill | \n|---|---|\n| PA13 / SWDIO | PA13 / SWDIO | \n| PA14 / SWCLK | PA14 / SWCLK | \n| GND | GND | \n| 3.3 V supply pin | 3.3 V supply pin | \n\nThe probe received USB power from the computer and supplied the target through that 3.3 V connection. The target had no separate USB supply. This describes my two-board setup; check the power-output capability before using another probe to supply a target.\n\nThe firmware variant matters. Other Black Magic builds for a Blue Pill can\nuse different output pins. Check the [supported hardware notes](https://black-magic.org/docs/intro/hardware/)\nand the [SWLINK notes for this revision](https://github.com/blackmagic-debug/blackmagic/blob/7a595ea/src/platforms/swlink/README.md).\n\nIf your second Blue Pill is blank, install Black Magic before using it as a probe. You’ll need another programming method, such as an existing ST-Link, or a suitable 3.3 V USB-to-UART adapter using the STM32’s factory serial bootloader. That bootloader requires a different boot-jumper setting from the target’s normal flash boot.\n\nFollow the [Black Magic build and installation documentation](https://black-magic.org/docs/hacking/hacking/) for\nthe chosen hardware variant. Its pins, bootloader arrangement, and firmware\naddresses matter. I haven’t verified a fresh probe installation for this guide.\n\nOn the computer, install an Arm-capable GDB, commonly named `arm-none-eabi-gdb`\nor `gdb-multiarch`. An ordinary host GDB may only support your computer’s CPU.\nCheck the one you plan to use:\n\n```\narm-none-eabi-gdb -q -batch -ex 'set architecture arm'\n```\nIf it rejects `arm`, use a debugger built with Arm support. The Rust toolchain\nbuilds the firmware but doesn’t supply this debugger. Substitute your debugger’s\nname in the commands if you’re using `gdb-multiarch`.\n\nBlack Magic exposes two USB serial interfaces on Linux. One speaks GDB’s remote protocol; the other is an optional UART bridge. Find the stable names:\n\n```\nls -l /dev/serial/by-id/*Black_Magic*\n```\nThe GDB interface ends in `-if00`. Use it rather than a remembered name such as\n`/dev/ttyACM0`, since the tty numbering can change after reconnecting the probe.\n\nYour user needs permission to open the serial device. If it belongs to the\n`dialout` group, add yourself to that group using your system’s administrator\ncommand, then log out and back in. For example:\n\n```\nsudo usermod -aG dialout \"$USER\"\n```\nSome distributions use another group or a udev rule. Check the device’s ownership. You shouldn’t need to run GDB as root.\n\nWith one Black Magic probe connected, find its GDB interface:\n\n```\nBMP_GDB_PORT=$(find /dev/serial/by-id -maxdepth 1 -type l \\\n  -name '*Black_Magic*if00' -print -quit)\nprintf '%s\\n' \"$BMP_GDB_PORT\"\n```\nCheck that this prints the expected path, then open the connection with the ELF loaded in GDB:\n\n```\narm-none-eabi-gdb -q \\\n  -ex \"target extended-remote $BMP_GDB_PORT\" \\\n  target/thumbv7m-none-eabi/release/blinky-from-scratch\n```\nScan for a target:\n\n```\n(gdb) monitor swdp_scan\n```\nMy scan found target 1 as `STM32F1 medium density M3/M4`. Check that yours\nfinds the intended target before attaching:\n\n```\n(gdb) attach 1\n```\nIf you want to preserve the existing firmware, save the project’s 64 KiB flash range before overwriting it:\n\n```\n(gdb) dump binary memory blue-pill-before-blinky.bin 0x08000000 0x08010000\n```\nThis requires the target to permit flash reads. It saves that address range, not the option bytes or necessarily all the flash on an unknown chip.\n\nNow flash the program, read it back for comparison, and reset the target:\n\n```\n(gdb) load\n(gdb) compare-sections\n(gdb) kill\n(gdb) quit\n```\n`load` replaces firmware in the target’s flash. Inspect the `compare-sections`\noutput: a mismatch can produce a warning without giving a batch GDB command a\nfailing exit status. For the current build, my comparison reported:\n\n```\nSection .vector_table, range 0x8000000 -- 0x80000ec: matched.\nSection .text, range 0x80000ec -- 0x8000284: matched.\n```\nOn Black Magic, `kill` detaches and resets the target to start the program.\nThe [probe’s GDB documentation](https://black-magic.org/docs/usage/gdb-commands/) describes this behavior.\n\n### Option B: ST-Link and OpenOCD\n\nYou can power the target from its USB connector and connect the ST-Link separately to the computer. Wire SWDIO, SWCLK, and GND as shown above.\n\nIf the probe has a target voltage reference input, often labelled VTref or VAPP, connect it to the target’s 3.3 V rail. It tells the probe what voltage the target uses; it does not supply power.\n\nSome probes instead provide a 3.3 V power output. A documented, suitable output can power the target’s 3.3 V pin, in which case leave the target’s USB power disconnected. Check your specific probe. Use one target power source, and don’t connect 5 V to the target’s 3.3 V rail or debug signals.\n\nInstall OpenOCD and its supplied Linux udev rules using your distribution’s instructions. The rules let your user access the probe. Reload them and reconnect the probe as directed by the package.\n\nFrom the project directory, run:\n\n```\nopenocd \\\n  -f interface/stlink.cfg \\\n  -f target/stm32f1x.cfg \\\n  -c 'program target/thumbv7m-none-eabi/release/blinky-from-scratch verify reset exit'\n```\nThe first configuration describes the probe; the second describes the target\nfamily. `program` writes the ELF, `verify` checks it, `reset` restarts the chip,\nand `exit` closes OpenOCD. This replaces the firmware currently in flash.\n\nThe command follows [OpenOCD’s documentation](https://openocd.org/doc/html/Flash-Programming.html). Let the supplied\nconfiguration choose its transport, since OpenOCD versions differ in which\nST-Link driver they use. This is the route I haven’t tested on hardware.\n\n### Check the result\n\nThe current program should repeat one flash, a gap, two flashes, a gap, three flashes, and a longer pause. Watch the PC13 user LED, not the steady power LED. On my board, the LED blinked as expected.\n\nIf the computer can’t open the probe, check host access first: USB permissions for ST-Link, or the serial interface and permissions for Black Magic. If the probe opens but can’t find the target, check target power, shared ground, SWDIO, SWCLK, and the pin assignment of your particular probe firmware.\n\nIf programming and verification succeed but the LED doesn’t blink, check BOOT0, reset the board, and confirm its user LED is connected to PC13. A flash comparison establishes that the bytes arrived, not that the LED wiring matches.\n\n## Who calls main?\n\nOn a desktop, the loader and runtime set things up before `main` starts.\nHere we’re running without an OS, so we have to do the setup ourselves.\nThese attributes turn off Rust’s usual standard library and entry setup:\n\n```\n#![no_std]\n#![no_main]\n```\n`no_std` keeps Rust’s `core` library but leaves out the usual standard library.\n`no_main` opts out of the normal entry machinery. We can still call a function\n`main`; we just have to arrange for execution to reach it.\n\nA reset handler that just calls `main` looks plausible:\n\n```\npub unsafe extern \"C\" fn Reset() -> ! {\n    main()\n}\n```\nBut the CPU still needs a way to find `Reset`, and `main` may expect its\nglobal variables to have been initialized. Let’s set those up.\n\n### Give the CPU a starting point\n\nWhen the STM32 boots from flash, it maps the start of flash into the boot address space. The Cortex-M3 reads two 32-bit words from there. The first is the initial stack pointer. The second gives the reset-handler address.\n\nThat means the image can’t just begin with arbitrary machine instructions. Its first words must have the structure the CPU expects. They begin the vector table, whose later entries give handler addresses for exceptions and interrupts.\n\nOur linker script, `link.x`, describes where the memory is:\n\n```\nMEMORY\n{\n  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 64K\n  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 20K\n}\n__stack_top = ORIGIN(RAM) + LENGTH(RAM);\n```\nThe initial stack pointer is `0x20005000`, the address just above RAM. The\nstack grows downward into it. Inside the script’s `SECTIONS` block, we put\nthe vector table at the start of flash:\n\n```\n.vector_table ORIGIN(FLASH) :\n{\n  __vector_table = .;\n  LONG(__stack_top);\n  KEEP(*(.vector_table.reset_vector));\n  KEEP(*(.vector_table.exceptions));\n  KEEP(*(.vector_table.interrupts));\n} > FLASH\n```\nThe dot is the linker’s current address. `LONG` emits the initial stack pointer\nas a 32-bit value. The next entry comes from this Rust static:\n\n```\n#[used]\n#[unsafe(no_mangle)]\n#[unsafe(link_section = \".vector_table.reset_vector\")]\nstatic RESET_VECTOR: unsafe extern \"C\" fn() -> ! = Reset;\n```\nIt holds the reset function’s address. `link_section` puts it in the named\nsection; the linker script places that section after the stack pointer.\n`no_mangle` preserves the symbol name, `extern \"C\"` specifies the calling\nconvention, and `!` means the handler never returns. The toolchain encodes the\nhandler address for Arm’s Thumb instruction state.\n\nRust’s [`#\\[used\\]`](https://doc.rust-lang.org/reference/abi.html#the-used-attribute) keeps the static in its object file, but the\nlinker can still discard it. `KEEP` prevents that second step. No Rust code\nhas to call through `RESET_VECTOR`; the CPU reads it on reset. The toolchain\nneeds to keep it even though ordinary code doesn’t refer to it.\n\nThe script also contains `ENTRY(Reset)`, which records the entry point in the\nELF. The chip doesn’t read an ELF header after reset. It reads the table we\nplaced in flash.\n\n### Give global variables their initial values\n\nImagine adding a writable global whose initial value is `123`. Its working\nstorage must be in RAM so the program can change it. But RAM doesn’t remember\n`123` across power cycles. A copy of that initial value must live in flash,\nand startup must copy it into RAM on each reset.\n\nA zero-initialized global has the same requirement to start with the right value. We can save flash space by recording the RAM range and clearing it, instead of storing a copy of all those zeroes.\n\nThese jobs correspond to the usual sections:\n\n| Section | Contents | Startup’s job | \n|---|---|---|\n| `.text` | Executable code in flash | Execute it in place. | \n| `.rodata` | Read-only constants in flash | Leave them in flash. | \n| `.data` | Variables in RAM with initial values stored in flash | Copy the initial bytes into RAM. | \n| `.bss` | Variables in RAM that must start at zero | Clear the range. | \n\nThat one-line `Reset` handler skips all of this. A blink can still work if\nthere’s no global storage to initialize. Constants can become immediate values\nin instructions, and local values can live in CPU registers or on the stack.\n\nAdd a global that really occupies RAM, and you need more startup code even if the LED loop stays exactly the same. The blink didn’t test that part.\n\nOur handler copies `.data` and clears `.bss` before calling `main`.\nThe linker provides the range boundaries and the source address of the initial\ndata through symbols such as `__sdata`, `__edata`, and `__sidata`. The handler\nalso sets the vector-table address register so later exceptions use our table\ndirectly in flash.\n\nOne part still needs care before extending this example: the memory-init\nloops are written in Rust. The [Embedonomicon recommends assembly for this\nstage](https://docs.rust-embedded.org/embedonomicon/sections-in-rust.html) because of Rust’s memory-model assumptions before\nglobal memory is initialized. This blink has empty `.data` and `.bss`, so it\ndoesn’t test those loops with actual variables. Review that code before adding\nglobals, or use a maintained startup implementation such as\n[cortex-m-rt](https://github.com/rust-embedded/cortex-m-rt).\n\n### Leave somewhere to go when things break\n\nAn interrupt lets a peripheral request CPU attention. Exceptions also include faults detected by the processor. When one occurs, the CPU looks up the appropriate handler in the vector table.\n\nYou could supply just the stack pointer and reset-handler address and get a program started. But if a fault occurs, the CPU will still look for its handler at the defined offset in the table. It doesn’t know you stopped writing the table after two entries.\n\nOur table includes the core exception entries and 43 peripheral interrupt entries for this STM32F103 target. The default handler loops forever, giving you a known place to inspect with GDB. We don’t enable peripheral interrupts for the blink.\n\nRust panics have a separate handler that also loops forever. There’s no terminal to print to or OS to return an exit status to.\n\n## When a pointer refers to hardware\n\nOnce execution reaches `main`, it has to configure GPIOC and change PC13.\nThese operations use memory-mapped registers: addresses where loads and stores\ninteract with peripheral hardware.\n\nThe [STM32F1 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) gives us these addresses and bit meanings:\n\n| Register | Address | What it controls | \n|---|---|---|\n| `RCC_APB2ENR` | `0x40021018` | Peripheral clocks, including the GPIOC clock. | \n| `GPIOC_CRH` | `0x40011004` | Configuration of GPIOC pins 8 through 15. | \n| `GPIOC_BSRR` | `0x40011010` | Commands to set or reset GPIOC output bits. | \n\nIn Rust, we describe an address as a raw pointer:\n\n```\nconst GPIOC_BASE: usize = 0x4001_1000;\npub const GPIOC_BSRR: *mut u32 = (GPIOC_BASE + 0x10) as *mut u32;\n```\nThis doesn’t allocate anything. It points at an address the hardware has\nalready assigned. The `u32` selects a 32-bit access. `usize` would happen to\nhave the same width on this target, but the register’s width comes from the\nchip specification. Use the type that matches it.\n\n### Unsafe doesn’t mean volatile\n\nYou might try enabling GPIOC with an ordinary pointer operation:\n\n```\n*RCC_APB2ENR |= RCC_APB2ENR_IOPCEN;\n```\nAn `unsafe` block permits that raw-pointer access. It doesn’t tell the compiler\nthat the address is a peripheral or that the access must reach it.\n\nConsider two assignments through an ordinary `&mut u32`:\n\n```\n*cell = 1;\n*cell = 2;\n```\nIf nothing can observe the first value, the compiler can remove the first\nassignment. The final value is still `2`, and the ordinary program’s observable\nbehavior is unchanged.\n\nA peripheral can observe something different. Each write might start a timer, acknowledge an interrupt, or change an output. Switching an LED on and then off is different from only switching it off, even if the program never reads anything back. A write whose value looks redundant can still be an action we need the hardware to perform.\n\nRust’s [`read_volatile` and `write_volatile`](https://doc.rust-lang.org/core/ptr/fn.write_volatile.html) express that the\naccesses themselves are observable. For the small example, the volatile\nversion is:\n\n```\nunsafe {\n    core::ptr::write_volatile(cell, 1);\n    core::ptr::write_volatile(cell, 2);\n}\n```\nCompiling these small examples with the project’s compiler and Arm target at optimization level 3 produced one store for the ordinary version and two for the volatile version.\n\nWe use volatile accesses for the hardware registers. These still\nrequire `unsafe`: the address, access width, alignment, and hardware setup\nmust be correct. Volatile access supplies an observable operation, not a check\nthat we chose the right peripheral.\n\nIt also doesn’t make a sequence of accesses atomic. Preserving a read and a write is separate from preventing an interrupt from doing something between them. We’ll run into that when changing an output.\n\n### Powering a chip doesn’t enable every peripheral\n\nPeripherals have separate clock gates. The CPU can be running while GPIOC’s\nclock is disabled, so first we set its enable bit in `RCC_APB2ENR`:\n\n```\nRCC_APB2ENR.write_volatile(RCC_APB2ENR.read_volatile() | RCC_APB2ENR_IOPCEN);\nlet _ = RCC_APB2ENR.read_volatile();\n```\nThese accesses run inside `main`’s `unsafe` block. `RCC_APB2ENR_IOPCEN` is\n`1 << 4`. The read-modify-write preserves other enable bits. The following\nread provides a delay for the enable write to reach the peripheral before\nwe access GPIOC.\n\nWe leave the CPU clock at its reset configuration, using the internal 8 MHz oscillator. The STM32F103’s advertised maximum speed requires configuring the clock tree. Its external crystal isn’t needed for this program.\n\n## Configuring one pin can change other pins\n\nGPIO pins can be inputs, outputs, or connections to other peripherals. We want PC13 to be a general-purpose push-pull output, so the program can drive it high or low.\n\nThe configuration lives in `GPIOC_CRH`. This is one 32-bit register containing\neight four-bit fields, for pins 8 through 15:\n\n```\npin:      PC15  PC14  PC13  PC12  PC11  PC10  PC9   PC8\nbits:     31:28 27:24 23:20 19:16 15:12 11:8  7:4   3:0\n```\nWithin each field, the two low bits select the mode and the two high bits\nselect the configuration. For our output, `MODE = 10` and `CNF = 00`, giving\n`0b0010`. That selects a general-purpose push-pull output in the 2 MHz mode.\n\nThe 2 MHz value describes the output-driver mode. It doesn’t set the CPU clock or make the LED blink two million times per second. PC13 has stricter drive limits than most pins on this chip, and the slow output mode is appropriate for the onboard LED.\n\nSo we could write this value to the register:\n\n```\n0b0010 << 20\n```\nPC13’s field starts at bit 20, so that sets it correctly. It also writes zeroes into every other field. Those zeroes select analog-input mode. They don’t mean “leave this field alone” or “drive this pin low”.\n\nYou might not notice while the LED is the only thing you’re using. Add something else to the port, and configuring the LED could change its settings.\n\nWe can preserve the other fields by reading the register, clearing only PC13’s four bits, and inserting our setting:\n\n```\nlet shift = (LED_PIN - 8) * 4;\nlet config = GPIOC_CRH.read_volatile();\nGPIOC_CRH.write_volatile(\n    (config & !(0b1111 << shift)) | (GPIO_OUTPUT_PUSHPULL_2_MHZ << shift),\n);\n```\nHere `LED_PIN` is 13 and `GPIO_OUTPUT_PUSHPULL_2_MHZ` is `0b0010`. The formula\nfor `shift` accounts for this register beginning at pin 8 and allocating four\nbits per pin.\n\nCheck the meaning of the zeroes you write, too.\n\n## Some registers hold state; others accept commands\n\nOnce a pin is an output, its output latch selects high or low. GPIO’s `ODR`,\nthe output data register, holds those latch states as bits. A natural approach\nwould be to read `ODR`, change the desired bit, and write the result back.\n\nThat approach has a catch when another part of the program can update an output between your read and write. Imagine this sequence:\n\n1. The main code reads the output register.\n2. An interrupt handler changes another output bit.\n3. The main code changes its bit in the old value and writes that value back.\n\nThe last write can undo the interrupt handler’s change. Volatile reads and writes would preserve all those operations, including the one that overwrites the newer state.\n\nThe GPIO hardware offers a different operation through `BSRR`, the bit\nset/reset register. Its writes tell the peripheral which bits to change:\n\n| Bits written in BSRR | Command | \n|---|---|\n| A one in bits 0 through 15 | Set the corresponding output high. | \n| A one in bits 16 through 31 | Reset the corresponding output low. | \n| Zeroes in both command bits for a pin | Leave that output unchanged. | \n\nChanging one output takes a single write, and the other outputs keep their states. Our blink doesn’t enable interrupts, but this is why the hardware offers the operation.\n\nThere’s also `BRR`, a bit reset register. We don’t need it here because the\nupper half of `BSRR` already lets us reset a pin. `BSRR` handles both directions.\n\n### Low turns this LED on\n\nThe board connects the LED and its resistor between the 3.3 V supply and PC13. Driving PC13 low lets current flow through the LED. Driving it high turns the LED off. This is what “active low” means here.\n\nOur two commands are therefore:\n\n```\n// Reset PC13 low: LED on.\nGPIOC_BSRR.write_volatile(1 << (LED_PIN + 16));\n// Set PC13 high: LED off.\nGPIOC_BSRR.write_volatile(1 << LED_PIN);\n```\nThe first writes bit 29, which resets output 13. The second writes bit 13, which sets output 13. The register accepts those commands; it isn’t a variable whose final stored value we care about.\n\nThe program also sets the output latch high before switching PC13 from input to output mode. That way, it begins driving the pin in the LED-off state.\n\n## A loop iteration isn’t a CPU cycle\n\nSwitching the output immediately back and forth would be too fast to see. The delay in this project is a small loop around a no-operation instruction:\n\n```\nfn wait(iterations: u32) {\n    for _ in 0..iterations {\n        asm::nop();\n    }\n}\n```\nEach iteration also has to count and decide whether to repeat. In the release build, one delay loop looks like this, with a label substituted for its address:\n\n```\ndelay:\n    subs r1, #1\n    nop\n    bne delay\n```\n`subs` decrements the counter and updates the condition flags. `bne` repeats\nwhile the result is nonzero. The counter selects how many iterations run,\nnot how many CPU cycles pass. Even counting instructions wouldn’t completely\nsettle the timing, because instructions and branches need not all take one\ncycle.\n\nThe firmware uses `200_000` iterations as one timing unit. It turns the LED\non for one unit and off for one unit, grouping flashes into counts of one,\ntwo, and three. The gaps between groups total three units, and the gap before\nrepeating totals seven. The code accounts for the off-time already spent\nafter the last flash when adding those longer gaps.\n\nThose units aren’t microseconds. A hardware timer would be the next step for stable timing, or for letting the CPU do other work while waiting.\n\n### Check what the compiler actually produced\n\nUsing Rust still leaves the generated instructions available to inspect.\nWith LLVM’s `llvm-objdump` installed, run:\n\n```\nllvm-objdump --disassemble target/thumbv7m-none-eabi/release/blinky-from-scratch\n```\nYou can find the delay loop and the stores that change the output. You can\nalso compare a source expression with its implementation. In this build, the\nGPIO configuration mask became a `bfi` instruction, which inserts a bit field,\nonce the operands were in registers. Several operations in Rust source don’t\nnecessarily become several separate operations on the CPU.\n\nThe ELF also separates the program’s loaded sections from debug information. This build loads 236 bytes of vector table and 408 bytes of code, 644 bytes in total. The ELF file itself is larger because it includes debug symbols and other metadata. Keeping those symbols for GDB doesn’t put them all in microcontroller flash.\n\n## Try changing it\n\nChange `GROUP_FLASH_COUNTS` in `src/main.rs` to `[3, 2, 1]`, rebuild, and flash\nagain. The groups should now count down. Change `UNIT` to adjust their timing,\nthen compare the source with the generated instructions.\n\nFor a bigger change, replace the busy loop with a timer. RM0008’s clock tree and general-purpose timer chapters are the places to start. You’ll need to work out which clock feeds the timer and how its prescaler and counter turn that into the interval you want.\n","body_html":"<h1 id=\"blinking-a-blue-pill-led-with-rust-from-scratch\">Blinking a Blue Pill LED with Rust, from scratch</h1>\n<p>I wanted to figure out how microcontrollers work, so I wrote a small\n<a href=\"https://github.com/unmanbearpig/blinky-from-scratch/tree/d518188bb144e7dab077e27b4d3de7388461177a\" rel=\"nofollow ugc noopener\">LED-blinking program in Rust, available on GitHub</a>.</p>\n<p>Most well-documented ways to program a microcontroller use libraries and generated code to make the job easier. If you’re trying to understand the hardware, you end up working backwards through those layers. Beginner-friendly material that starts from scratch is harder to find.</p>\n<p>I know how compilers work well enough that using Rust wouldn’t get in the way. It also gives me a chance to learn more about Rust: raw pointers, volatile access, and what it takes to run without the usual startup code.</p>\n<p>Leaving out the helper libraries is a good way to test vague knowledge. “The CPU starts executing after reset” sounds simple until you have to put its starting address in the right place.</p>\n<p>The project has no external Rust dependencies. We’ll write to registers\ndirectly and look at the startup code and linker script. I’ll assume you\nalready have a Blue Pill and know how to program. First we’ll\n<a href=\"https://unmb.pw#get-the-program-onto-the-board\" rel=\"nofollow ugc noopener\">get code onto the board</a>, then\n<a href=\"https://unmb.pw#who-calls-main\" rel=\"nofollow ugc noopener\">follow it from reset</a> to the LED.</p>\n<h2 id=\"the-cpu-the-microcontroller-and-the-board\">The CPU, the microcontroller, and the board</h2>\n<p>The CPU in an STM32F103 is an Arm Cortex-M3. It executes instructions, uses a stack, and handles exceptions. ST combines that core with flash, RAM, timers, GPIO, and communication peripherals such as UART, SPI, I²C, and USB. That whole chip is the microcontroller.</p>\n<p>Two chips can use the same CPU and have completely different peripherals.\nFor example, QEMU’s <a href=\"https://www.qemu.org/docs/master/system/arm/stellaris.html\" rel=\"nofollow ugc noopener\">Stellaris LM3S6965 board</a> also has a\nCortex-M3. It can execute the same relevant instructions, but its GPIO registers\naren’t the STM32’s GPIO registers. Selecting a Cortex-M3 in an emulator doesn’t\nmake it a Blue Pill.</p>\n<p>The Blue Pill is the circuit board around the chip. It adds the regulator, crystals, USB connector, reset button, boot jumpers, LEDs, and accessible pins. This project targets an STM32F103C8T6 board with its user LED on PC13. That chip has 64 KiB of flash and 20 KiB of RAM.</p>\n<p>Check the marking on yours; Blue Pills don’t all come with the same chip.</p>\n<p>The names we’ll use on the board are:</p>\n<div class=\"table-wrap\"><table><thead><tr><th>Label or part</th><th>What it does here</th></tr></thead><tbody><tr><td>PC13</td><td>The GPIO pin connected to the built-in user LED and its resistor.</td></tr><tr><td>Power LED</td><td>Indicates board power. It is separate from the programmable LED.</td></tr><tr><td>SWDIO / DIO / PA13</td><td>Debug data connection.</td></tr><tr><td>SWCLK / CLK / PA14</td><td>Debug clock connection.</td></tr><tr><td>GND and 3.3 V</td><td>Ground and the board’s 3.3 V supply rail.</td></tr><tr><td>BOOT0</td><td>Selects whether the chip starts from our flash program or another boot source.</td></tr><tr><td>NRST / RST</td><td>Reset signal; the reset button also restarts the chip.</td></tr></tbody></table></div>\n<p>PC13 means pin 13 of GPIO port C, not pin 13 of the chip’s package. Similarly, PA13 and PA14 are the GPIO names of the pins used for debugging. Leave those two assigned to debugging for this example.</p>\n<h2 id=\"where-to-find-the-information\">Where to find the information</h2>\n<p>You’ll want a few documents open. The board schematic, chip datasheet, and reference manual answer different questions. Searching for the right thing in the wrong PDF can waste a lot of time.</p>\n<div class=\"table-wrap\"><table><thead><tr><th>Document</th><th>What to look up</th></tr></thead><tbody><tr><td><a href=\"https://stm32-base.org/boards/STM32F103C8T6-Blue-Pill.html\" rel=\"nofollow ugc noopener\">Blue Pill board reference</a> and<a href=\"https://stm32-base.org/assets/pdf/boards/original-schematic-STM32F103C8T6-Blue_Pill.pdf\" rel=\"nofollow ugc noopener\">schematic</a></td><td>Header pin labels, which pin the LED connects to, how the boot jumpers are wired, and where power comes from.</td></tr><tr><td><a href=\"https://www.st.com/resource/en/datasheet/stm32f103c8.pdf\" rel=\"nofollow ugc noopener\">STM32F103x8/xB datasheet</a></td><td>The chip’s pinout, package, memory sizes, supply and I/O limits, and restrictions on particular pins. Check the PC13 through PC15 footnote.</td></tr><tr><td><a href=\"https://www.st.com/resource/en/reference_manual/cd00171190-stm32f101xx-stm32f102xx-stm32f103xx-stm32f105xx-and-stm32f107xx-advanced-arm-based-32-bit-mcus-stmicroelectronics.pdf\" rel=\"nofollow ugc noopener\">STM32F1 reference manual, RM0008</a></td><td>Memory map, boot modes, clocks, and peripheral registers. For this program, start with the memory map, Reset and clock control, and General-purpose and alternate-function I/Os.</td></tr><tr><td><a href=\"https://developer.arm.com/documentation/dui0552/a/\" rel=\"nofollow ugc noopener\">Cortex-M3 Devices Generic User Guide, DUI0552</a></td><td>The CPU’s reset sequence, stack, vector table, exceptions, core registers, and instructions. ST’s <a href=\"https://www.st.com/resource/en/programming_manual/cd00228163-stm32f10xxx20xxx21xxxl1xxxx-cortexm3-programming-manual-stmicroelectronics.pdf\" rel=\"nofollow ugc noopener\">Cortex-M3 programming manual, PM0056</a> is another useful reference for this part.</td></tr></tbody></table></div>\n<p>The Blue Pill reference is maintained by the STM32-base community. Use your seller’s schematic if it matches the board you bought; compare the components and pin labels before treating a generic drawing as an exact match.</p>\n<p>The STM32 datasheet gives the details of a particular chip. RM0008 covers peripherals shared across a larger family, including parts with peripherals and pins your chip may not have. Keep both open. The datasheet tells you what you have; the reference manual tells you how to program it.</p>\n<p>For the Cortex-M3, the programming guide is what we need here. The core’s reset and exception behavior comes from Arm. The electrical limits of the STM32 pins come from ST’s datasheet.</p>\n<h3 id=\"finding-a-register-without-guessing\">Finding a register without guessing</h3>\n<p>Suppose we want to enable GPIOC’s clock. In RM0008’s memory map, RCC starts\nat <code>0x40021000</code>. In the RCC chapter, the APB2 peripheral clock enable register,\n<code>RCC_APB2ENR</code>, has offset <code>0x18</code>. Its address is therefore:</p>\n<pre><code>0x40021000 + 0x18 = 0x40021018</code></pre>\n<p>The register’s bit diagram labels bit 4 <code>IOPCEN</code>, the I/O port C clock enable.\nThat’s where the address and <code>1 &lt;&lt; 4</code> in our code come from. Check the access\ntype, reset value, and reserved-bit notes too. Don’t assume every register can\nbe read and written like RAM.</p>\n<p>For the LED, start with the board schematic to find PC13 and see how it’s connected. Then check the datasheet for the pin’s restrictions, and RM0008 for the GPIO configuration and set/reset registers. You can jump straight to the relevant chapters; there’s no need to read the whole manual first.</p>\n<h3 id=\"other-docs\">Other docs</h3>\n<p>The chip’s <a href=\"https://www.st.com/en/microcontrollers-microprocessors/stm32f103c8.html#documentation\" rel=\"nofollow ugc noopener\">product documentation page</a> also lists its errata.\nThose describe known hardware bugs and workarounds. Match them to your part\nand silicon revision if a peripheral isn’t behaving as documented.</p>\n<p>Use the documentation for your exact debug probe for connector pinouts and power wiring. Black Magic’s firmware variant matters too; we’ll get to that in the setup instructions.</p>\n<p>For the Rust side, the <a href=\"https://docs.rust-embedded.org/embedonomicon/\" rel=\"nofollow ugc noopener\">Embedonomicon</a> walks through building\nan embedded program from scratch. The <a href=\"https://libopencm3.org/docs/latest/stm32f1/html/group__gpio__defines.html\" rel=\"nofollow ugc noopener\">libopencm3 GPIO definitions</a>\nand <a href=\"https://github.com/rust-embedded/cortex-m-rt\" rel=\"nofollow ugc noopener\">cortex-m-rt source</a> are also useful to read when you’re stuck.\nYou can study how a library does something without adding it as a dependency.\nThe Rust reference explains what <a href=\"https://doc.rust-lang.org/reference/abi.html#the-used-attribute\" rel=\"nofollow ugc noopener\"><code>#\\[used\\]</code></a> retains, and the\nstandard-library docs explain what <a href=\"https://doc.rust-lang.org/core/ptr/fn.write_volatile.html\" rel=\"nofollow ugc noopener\"><code>write_volatile</code></a> guarantees.</p>\n<p>For <code>link.x</code>, use the <a href=\"https://sourceware.org/binutils/docs/ld/Scripts.html\" rel=\"nofollow ugc noopener\">GNU ld linker-script manual</a> to look up\n<code>MEMORY</code>, <code>SECTIONS</code>, and <code>KEEP</code>. Rust’s bundled linker is LLD, which implements\nthis script syntax; its <a href=\"https://lld.llvm.org/ELF/linker_script.html\" rel=\"nofollow ugc noopener\">implementation notes</a> describe differences\nfrom GNU ld. You don’t need to install a second linker to read its manual.</p>\n<h2 id=\"get-the-program-onto-the-board\">Get the program onto the board</h2>\n<p>A debug probe connects the computer to the target microcontroller. The computer talks to the probe over USB. The probe talks to the target over SWD, Arm’s Serial Wire Debug interface. That lets a debugger halt execution, inspect memory, and arrange for a program to be written to flash.</p>\n<p>We’ll use either an ST-Link or another Blue Pill running Black Magic firmware:</p>\n<pre><code>Computer → USB → ST-Link → SWD → target Blue Pill\n                   ↑\n           controlled by OpenOCD\nComputer → USB → Black Magic probe → SWD → target Blue Pill\n                         ↑\n                GDB connects directly</code></pre>\n<p>OpenOCD is the host program that controls the ST-Link. Black Magic includes a GDB server in the probe’s firmware, so GDB connects to it directly.</p>\n<p>I verified the Blue Pill running Black Magic route on my hardware: connecting to the target, flashing, and checking the written firmware. The ST-Link/OpenOCD instructions follow the tools’ documentation and should work, but I haven’t verified them on hardware. My Black Magic probe already had its firmware installed; preparing a blank probe is a separate prerequisite.</p>\n<p>The target’s USB connector can supply power, but our firmware doesn’t implement USB. A stock STM32F103’s factory bootloader doesn’t provide USB flashing either. You can install an additional USB bootloader, but this guide uses SWD.</p>\n<h3 id=\"build-once-then-choose-your-probe\">Build once, then choose your probe</h3>\n<p>The commands below assume Linux. The Rust project also builds on macOS and Windows; probe permissions and device paths differ.</p>\n<p>Install <a href=\"https://rustup.rs/\" rel=\"nofollow ugc noopener\">rustup</a> if necessary, then run:</p>\n<pre><code>git clone https://github.com/unmanbearpig/blinky-from-scratch.git\ncd blinky-from-scratch\ngit checkout --detach d518188bb144e7dab077e27b4d3de7388461177a\nrustup toolchain install 1.96.1 --profile minimal \\\n  --component rustfmt --component clippy --target thumbv7m-none-eabi\ncargo build --release --locked</code></pre>\n<p>The checkout selects <a href=\"https://github.com/unmanbearpig/blinky-from-scratch/tree/d518188bb144e7dab077e27b4d3de7388461177a\" rel=\"nofollow ugc noopener\">firmware revision <code>d518188</code></a>, used for the code,\ndisassembly, and sizes in this article.</p>\n<p><code>rust-toolchain.toml</code> pins that compiler version. The target\n<code>thumbv7m-none-eabi</code> selects the Cortex-M3-compatible instruction set and\nbare-metal environment. <code>.cargo/config.toml</code> selects that target and passes\n<code>link.x</code> to Rust’s bundled linker. Building doesn’t require Arm GCC.</p>\n<p>The output is:</p>\n<pre><code>target/thumbv7m-none-eabi/release/blinky-from-scratch</code></pre>\n<p>This is an ELF file, containing the program and information about where its parts belong in memory. Both flashing tools below understand ELF. They can use those addresses directly, so you don’t need a raw binary or a separate flash address.</p>\n<p>Use the release build. The delay is a busy loop, and compiler optimization affects its timing. We’ll inspect that loop later.</p>\n<h3 id=\"wire-the-target\">Wire the target</h3>\n<p>Disconnect power while attaching wires. Set the target’s BOOT0 jumper to 0, so it boots our program from flash after reset. BOOT1 doesn’t affect this mode.</p>\n<p>Both probes need these connections:</p>\n<div class=\"table-wrap\"><table><thead><tr><th>Probe signal</th><th>Target Blue Pill</th></tr></thead><tbody><tr><td>SWDIO</td><td>SWDIO / DIO / PA13</td></tr><tr><td>SWCLK</td><td>SWCLK / CLK / PA14</td></tr><tr><td>GND</td><td>GND</td></tr><tr><td>NRST, if available</td><td>NRST / RST, optional for normal programming</td></tr></tbody></table></div>\n<p>Read the labels on your probe. Connector layouts vary, especially on ST-Link clones. Connecting NRST can help recover a target whose existing firmware interferes with normal debug access; my Black Magic setup worked without it.</p>\n<p>Power wiring depends on the probe, so follow the relevant section below.</p>\n<h3 id=\"option-a-a-blue-pill-running-black-magic\">Option A: a Blue Pill running Black Magic</h3>\n<p>My probe runs Black Magic’s SWLINK firmware, <code>v1.6.1-409-g7a595ea</code>. On that\nbuild, PA13 and PA14 are the probe’s connections to the target:</p>\n<div class=\"table-wrap\"><table><thead><tr><th>Probe Blue Pill</th><th>Target Blue Pill</th></tr></thead><tbody><tr><td>PA13 / SWDIO</td><td>PA13 / SWDIO</td></tr><tr><td>PA14 / SWCLK</td><td>PA14 / SWCLK</td></tr><tr><td>GND</td><td>GND</td></tr><tr><td>3.3 V supply pin</td><td>3.3 V supply pin</td></tr></tbody></table></div>\n<p>The probe received USB power from the computer and supplied the target through that 3.3 V connection. The target had no separate USB supply. This describes my two-board setup; check the power-output capability before using another probe to supply a target.</p>\n<p>The firmware variant matters. Other Black Magic builds for a Blue Pill can\nuse different output pins. Check the <a href=\"https://black-magic.org/docs/intro/hardware/\" rel=\"nofollow ugc noopener\">supported hardware notes</a>\nand the <a href=\"https://github.com/blackmagic-debug/blackmagic/blob/7a595ea/src/platforms/swlink/README.md\" rel=\"nofollow ugc noopener\">SWLINK notes for this revision</a>.</p>\n<p>If your second Blue Pill is blank, install Black Magic before using it as a probe. You’ll need another programming method, such as an existing ST-Link, or a suitable 3.3 V USB-to-UART adapter using the STM32’s factory serial bootloader. That bootloader requires a different boot-jumper setting from the target’s normal flash boot.</p>\n<p>Follow the <a href=\"https://black-magic.org/docs/hacking/hacking/\" rel=\"nofollow ugc noopener\">Black Magic build and installation documentation</a> for\nthe chosen hardware variant. Its pins, bootloader arrangement, and firmware\naddresses matter. I haven’t verified a fresh probe installation for this guide.</p>\n<p>On the computer, install an Arm-capable GDB, commonly named <code>arm-none-eabi-gdb</code>\nor <code>gdb-multiarch</code>. An ordinary host GDB may only support your computer’s CPU.\nCheck the one you plan to use:</p>\n<pre><code>arm-none-eabi-gdb -q -batch -ex &#39;set architecture arm&#39;</code></pre>\n<p>If it rejects <code>arm</code>, use a debugger built with Arm support. The Rust toolchain\nbuilds the firmware but doesn’t supply this debugger. Substitute your debugger’s\nname in the commands if you’re using <code>gdb-multiarch</code>.</p>\n<p>Black Magic exposes two USB serial interfaces on Linux. One speaks GDB’s remote protocol; the other is an optional UART bridge. Find the stable names:</p>\n<pre><code>ls -l /dev/serial/by-id/*Black_Magic*</code></pre>\n<p>The GDB interface ends in <code>-if00</code>. Use it rather than a remembered name such as\n<code>/dev/ttyACM0</code>, since the tty numbering can change after reconnecting the probe.</p>\n<p>Your user needs permission to open the serial device. If it belongs to the\n<code>dialout</code> group, add yourself to that group using your system’s administrator\ncommand, then log out and back in. For example:</p>\n<pre><code>sudo usermod -aG dialout &quot;$USER&quot;</code></pre>\n<p>Some distributions use another group or a udev rule. Check the device’s ownership. You shouldn’t need to run GDB as root.</p>\n<p>With one Black Magic probe connected, find its GDB interface:</p>\n<pre><code>BMP_GDB_PORT=$(find /dev/serial/by-id -maxdepth 1 -type l \\\n  -name &#39;*Black_Magic*if00&#39; -print -quit)\nprintf &#39;%s\\n&#39; &quot;$BMP_GDB_PORT&quot;</code></pre>\n<p>Check that this prints the expected path, then open the connection with the ELF loaded in GDB:</p>\n<pre><code>arm-none-eabi-gdb -q \\\n  -ex &quot;target extended-remote $BMP_GDB_PORT&quot; \\\n  target/thumbv7m-none-eabi/release/blinky-from-scratch</code></pre>\n<p>Scan for a target:</p>\n<pre><code>(gdb) monitor swdp_scan</code></pre>\n<p>My scan found target 1 as <code>STM32F1 medium density M3/M4</code>. Check that yours\nfinds the intended target before attaching:</p>\n<pre><code>(gdb) attach 1</code></pre>\n<p>If you want to preserve the existing firmware, save the project’s 64 KiB flash range before overwriting it:</p>\n<pre><code>(gdb) dump binary memory blue-pill-before-blinky.bin 0x08000000 0x08010000</code></pre>\n<p>This requires the target to permit flash reads. It saves that address range, not the option bytes or necessarily all the flash on an unknown chip.</p>\n<p>Now flash the program, read it back for comparison, and reset the target:</p>\n<pre><code>(gdb) load\n(gdb) compare-sections\n(gdb) kill\n(gdb) quit</code></pre>\n<p><code>load</code> replaces firmware in the target’s flash. Inspect the <code>compare-sections</code>\noutput: a mismatch can produce a warning without giving a batch GDB command a\nfailing exit status. For the current build, my comparison reported:</p>\n<pre><code>Section .vector_table, range 0x8000000 -- 0x80000ec: matched.\nSection .text, range 0x80000ec -- 0x8000284: matched.</code></pre>\n<p>On Black Magic, <code>kill</code> detaches and resets the target to start the program.\nThe <a href=\"https://black-magic.org/docs/usage/gdb-commands/\" rel=\"nofollow ugc noopener\">probe’s GDB documentation</a> describes this behavior.</p>\n<h3 id=\"option-b-st-link-and-openocd\">Option B: ST-Link and OpenOCD</h3>\n<p>You can power the target from its USB connector and connect the ST-Link separately to the computer. Wire SWDIO, SWCLK, and GND as shown above.</p>\n<p>If the probe has a target voltage reference input, often labelled VTref or VAPP, connect it to the target’s 3.3 V rail. It tells the probe what voltage the target uses; it does not supply power.</p>\n<p>Some probes instead provide a 3.3 V power output. A documented, suitable output can power the target’s 3.3 V pin, in which case leave the target’s USB power disconnected. Check your specific probe. Use one target power source, and don’t connect 5 V to the target’s 3.3 V rail or debug signals.</p>\n<p>Install OpenOCD and its supplied Linux udev rules using your distribution’s instructions. The rules let your user access the probe. Reload them and reconnect the probe as directed by the package.</p>\n<p>From the project directory, run:</p>\n<pre><code>openocd \\\n  -f interface/stlink.cfg \\\n  -f target/stm32f1x.cfg \\\n  -c &#39;program target/thumbv7m-none-eabi/release/blinky-from-scratch verify reset exit&#39;</code></pre>\n<p>The first configuration describes the probe; the second describes the target\nfamily. <code>program</code> writes the ELF, <code>verify</code> checks it, <code>reset</code> restarts the chip,\nand <code>exit</code> closes OpenOCD. This replaces the firmware currently in flash.</p>\n<p>The command follows <a href=\"https://openocd.org/doc/html/Flash-Programming.html\" rel=\"nofollow ugc noopener\">OpenOCD’s documentation</a>. Let the supplied\nconfiguration choose its transport, since OpenOCD versions differ in which\nST-Link driver they use. This is the route I haven’t tested on hardware.</p>\n<h3 id=\"check-the-result\">Check the result</h3>\n<p>The current program should repeat one flash, a gap, two flashes, a gap, three flashes, and a longer pause. Watch the PC13 user LED, not the steady power LED. On my board, the LED blinked as expected.</p>\n<p>If the computer can’t open the probe, check host access first: USB permissions for ST-Link, or the serial interface and permissions for Black Magic. If the probe opens but can’t find the target, check target power, shared ground, SWDIO, SWCLK, and the pin assignment of your particular probe firmware.</p>\n<p>If programming and verification succeed but the LED doesn’t blink, check BOOT0, reset the board, and confirm its user LED is connected to PC13. A flash comparison establishes that the bytes arrived, not that the LED wiring matches.</p>\n<h2 id=\"who-calls-main\">Who calls main?</h2>\n<p>On a desktop, the loader and runtime set things up before <code>main</code> starts.\nHere we’re running without an OS, so we have to do the setup ourselves.\nThese attributes turn off Rust’s usual standard library and entry setup:</p>\n<pre><code>#![no_std]\n#![no_main]</code></pre>\n<p><code>no_std</code> keeps Rust’s <code>core</code> library but leaves out the usual standard library.\n<code>no_main</code> opts out of the normal entry machinery. We can still call a function\n<code>main</code>; we just have to arrange for execution to reach it.</p>\n<p>A reset handler that just calls <code>main</code> looks plausible:</p>\n<pre><code>pub unsafe extern &quot;C&quot; fn Reset() -&gt; ! {\n    main()\n}</code></pre>\n<p>But the CPU still needs a way to find <code>Reset</code>, and <code>main</code> may expect its\nglobal variables to have been initialized. Let’s set those up.</p>\n<h3 id=\"give-the-cpu-a-starting-point\">Give the CPU a starting point</h3>\n<p>When the STM32 boots from flash, it maps the start of flash into the boot address space. The Cortex-M3 reads two 32-bit words from there. The first is the initial stack pointer. The second gives the reset-handler address.</p>\n<p>That means the image can’t just begin with arbitrary machine instructions. Its first words must have the structure the CPU expects. They begin the vector table, whose later entries give handler addresses for exceptions and interrupts.</p>\n<p>Our linker script, <code>link.x</code>, describes where the memory is:</p>\n<pre><code>MEMORY\n{\n  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 64K\n  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 20K\n}\n__stack_top = ORIGIN(RAM) + LENGTH(RAM);</code></pre>\n<p>The initial stack pointer is <code>0x20005000</code>, the address just above RAM. The\nstack grows downward into it. Inside the script’s <code>SECTIONS</code> block, we put\nthe vector table at the start of flash:</p>\n<pre><code>.vector_table ORIGIN(FLASH) :\n{\n  __vector_table = .;\n  LONG(__stack_top);\n  KEEP(*(.vector_table.reset_vector));\n  KEEP(*(.vector_table.exceptions));\n  KEEP(*(.vector_table.interrupts));\n} &gt; FLASH</code></pre>\n<p>The dot is the linker’s current address. <code>LONG</code> emits the initial stack pointer\nas a 32-bit value. The next entry comes from this Rust static:</p>\n<pre><code>#[used]\n#[unsafe(no_mangle)]\n#[unsafe(link_section = &quot;.vector_table.reset_vector&quot;)]\nstatic RESET_VECTOR: unsafe extern &quot;C&quot; fn() -&gt; ! = Reset;</code></pre>\n<p>It holds the reset function’s address. <code>link_section</code> puts it in the named\nsection; the linker script places that section after the stack pointer.\n<code>no_mangle</code> preserves the symbol name, <code>extern &quot;C&quot;</code> specifies the calling\nconvention, and <code>!</code> means the handler never returns. The toolchain encodes the\nhandler address for Arm’s Thumb instruction state.</p>\n<p>Rust’s <a href=\"https://doc.rust-lang.org/reference/abi.html#the-used-attribute\" rel=\"nofollow ugc noopener\"><code>#\\[used\\]</code></a> keeps the static in its object file, but the\nlinker can still discard it. <code>KEEP</code> prevents that second step. No Rust code\nhas to call through <code>RESET_VECTOR</code>; the CPU reads it on reset. The toolchain\nneeds to keep it even though ordinary code doesn’t refer to it.</p>\n<p>The script also contains <code>ENTRY(Reset)</code>, which records the entry point in the\nELF. The chip doesn’t read an ELF header after reset. It reads the table we\nplaced in flash.</p>\n<h3 id=\"give-global-variables-their-initial-values\">Give global variables their initial values</h3>\n<p>Imagine adding a writable global whose initial value is <code>123</code>. Its working\nstorage must be in RAM so the program can change it. But RAM doesn’t remember\n<code>123</code> across power cycles. A copy of that initial value must live in flash,\nand startup must copy it into RAM on each reset.</p>\n<p>A zero-initialized global has the same requirement to start with the right value. We can save flash space by recording the RAM range and clearing it, instead of storing a copy of all those zeroes.</p>\n<p>These jobs correspond to the usual sections:</p>\n<div class=\"table-wrap\"><table><thead><tr><th>Section</th><th>Contents</th><th>Startup’s job</th></tr></thead><tbody><tr><td><code>.text</code></td><td>Executable code in flash</td><td>Execute it in place.</td></tr><tr><td><code>.rodata</code></td><td>Read-only constants in flash</td><td>Leave them in flash.</td></tr><tr><td><code>.data</code></td><td>Variables in RAM with initial values stored in flash</td><td>Copy the initial bytes into RAM.</td></tr><tr><td><code>.bss</code></td><td>Variables in RAM that must start at zero</td><td>Clear the range.</td></tr></tbody></table></div>\n<p>That one-line <code>Reset</code> handler skips all of this. A blink can still work if\nthere’s no global storage to initialize. Constants can become immediate values\nin instructions, and local values can live in CPU registers or on the stack.</p>\n<p>Add a global that really occupies RAM, and you need more startup code even if the LED loop stays exactly the same. The blink didn’t test that part.</p>\n<p>Our handler copies <code>.data</code> and clears <code>.bss</code> before calling <code>main</code>.\nThe linker provides the range boundaries and the source address of the initial\ndata through symbols such as <code>__sdata</code>, <code>__edata</code>, and <code>__sidata</code>. The handler\nalso sets the vector-table address register so later exceptions use our table\ndirectly in flash.</p>\n<p>One part still needs care before extending this example: the memory-init\nloops are written in Rust. The <a href=\"https://docs.rust-embedded.org/embedonomicon/sections-in-rust.html\" rel=\"nofollow ugc noopener\">Embedonomicon recommends assembly for this\nstage</a> because of Rust’s memory-model assumptions before\nglobal memory is initialized. This blink has empty <code>.data</code> and <code>.bss</code>, so it\ndoesn’t test those loops with actual variables. Review that code before adding\nglobals, or use a maintained startup implementation such as\n<a href=\"https://github.com/rust-embedded/cortex-m-rt\" rel=\"nofollow ugc noopener\">cortex-m-rt</a>.</p>\n<h3 id=\"leave-somewhere-to-go-when-things-break\">Leave somewhere to go when things break</h3>\n<p>An interrupt lets a peripheral request CPU attention. Exceptions also include faults detected by the processor. When one occurs, the CPU looks up the appropriate handler in the vector table.</p>\n<p>You could supply just the stack pointer and reset-handler address and get a program started. But if a fault occurs, the CPU will still look for its handler at the defined offset in the table. It doesn’t know you stopped writing the table after two entries.</p>\n<p>Our table includes the core exception entries and 43 peripheral interrupt entries for this STM32F103 target. The default handler loops forever, giving you a known place to inspect with GDB. We don’t enable peripheral interrupts for the blink.</p>\n<p>Rust panics have a separate handler that also loops forever. There’s no terminal to print to or OS to return an exit status to.</p>\n<h2 id=\"when-a-pointer-refers-to-hardware\">When a pointer refers to hardware</h2>\n<p>Once execution reaches <code>main</code>, it has to configure GPIOC and change PC13.\nThese operations use memory-mapped registers: addresses where loads and stores\ninteract with peripheral hardware.</p>\n<p>The <a href=\"https://www.st.com/resource/en/reference_manual/cd00171190-stm32f101xx-stm32f102xx-stm32f103xx-stm32f105xx-and-stm32f107xx-advanced-arm-based-32-bit-mcus-stmicroelectronics.pdf\" rel=\"nofollow ugc noopener\">STM32F1 reference manual</a> gives us these addresses and bit meanings:</p>\n<div class=\"table-wrap\"><table><thead><tr><th>Register</th><th>Address</th><th>What it controls</th></tr></thead><tbody><tr><td><code>RCC_APB2ENR</code></td><td><code>0x40021018</code></td><td>Peripheral clocks, including the GPIOC clock.</td></tr><tr><td><code>GPIOC_CRH</code></td><td><code>0x40011004</code></td><td>Configuration of GPIOC pins 8 through 15.</td></tr><tr><td><code>GPIOC_BSRR</code></td><td><code>0x40011010</code></td><td>Commands to set or reset GPIOC output bits.</td></tr></tbody></table></div>\n<p>In Rust, we describe an address as a raw pointer:</p>\n<pre><code>const GPIOC_BASE: usize = 0x4001_1000;\npub const GPIOC_BSRR: *mut u32 = (GPIOC_BASE + 0x10) as *mut u32;</code></pre>\n<p>This doesn’t allocate anything. It points at an address the hardware has\nalready assigned. The <code>u32</code> selects a 32-bit access. <code>usize</code> would happen to\nhave the same width on this target, but the register’s width comes from the\nchip specification. Use the type that matches it.</p>\n<h3 id=\"unsafe-doesn-t-mean-volatile\">Unsafe doesn’t mean volatile</h3>\n<p>You might try enabling GPIOC with an ordinary pointer operation:</p>\n<pre><code>*RCC_APB2ENR |= RCC_APB2ENR_IOPCEN;</code></pre>\n<p>An <code>unsafe</code> block permits that raw-pointer access. It doesn’t tell the compiler\nthat the address is a peripheral or that the access must reach it.</p>\n<p>Consider two assignments through an ordinary <code>&amp;mut u32</code>:</p>\n<pre><code>*cell = 1;\n*cell = 2;</code></pre>\n<p>If nothing can observe the first value, the compiler can remove the first\nassignment. The final value is still <code>2</code>, and the ordinary program’s observable\nbehavior is unchanged.</p>\n<p>A peripheral can observe something different. Each write might start a timer, acknowledge an interrupt, or change an output. Switching an LED on and then off is different from only switching it off, even if the program never reads anything back. A write whose value looks redundant can still be an action we need the hardware to perform.</p>\n<p>Rust’s <a href=\"https://doc.rust-lang.org/core/ptr/fn.write_volatile.html\" rel=\"nofollow ugc noopener\"><code>read_volatile</code> and <code>write_volatile</code></a> express that the\naccesses themselves are observable. For the small example, the volatile\nversion is:</p>\n<pre><code>unsafe {\n    core::ptr::write_volatile(cell, 1);\n    core::ptr::write_volatile(cell, 2);\n}</code></pre>\n<p>Compiling these small examples with the project’s compiler and Arm target at optimization level 3 produced one store for the ordinary version and two for the volatile version.</p>\n<p>We use volatile accesses for the hardware registers. These still\nrequire <code>unsafe</code>: the address, access width, alignment, and hardware setup\nmust be correct. Volatile access supplies an observable operation, not a check\nthat we chose the right peripheral.</p>\n<p>It also doesn’t make a sequence of accesses atomic. Preserving a read and a write is separate from preventing an interrupt from doing something between them. We’ll run into that when changing an output.</p>\n<h3 id=\"powering-a-chip-doesn-t-enable-every-peripheral\">Powering a chip doesn’t enable every peripheral</h3>\n<p>Peripherals have separate clock gates. The CPU can be running while GPIOC’s\nclock is disabled, so first we set its enable bit in <code>RCC_APB2ENR</code>:</p>\n<pre><code>RCC_APB2ENR.write_volatile(RCC_APB2ENR.read_volatile() | RCC_APB2ENR_IOPCEN);\nlet _ = RCC_APB2ENR.read_volatile();</code></pre>\n<p>These accesses run inside <code>main</code>’s <code>unsafe</code> block. <code>RCC_APB2ENR_IOPCEN</code> is\n<code>1 &lt;&lt; 4</code>. The read-modify-write preserves other enable bits. The following\nread provides a delay for the enable write to reach the peripheral before\nwe access GPIOC.</p>\n<p>We leave the CPU clock at its reset configuration, using the internal 8 MHz oscillator. The STM32F103’s advertised maximum speed requires configuring the clock tree. Its external crystal isn’t needed for this program.</p>\n<h2 id=\"configuring-one-pin-can-change-other-pins\">Configuring one pin can change other pins</h2>\n<p>GPIO pins can be inputs, outputs, or connections to other peripherals. We want PC13 to be a general-purpose push-pull output, so the program can drive it high or low.</p>\n<p>The configuration lives in <code>GPIOC_CRH</code>. This is one 32-bit register containing\neight four-bit fields, for pins 8 through 15:</p>\n<pre><code>pin:      PC15  PC14  PC13  PC12  PC11  PC10  PC9   PC8\nbits:     31:28 27:24 23:20 19:16 15:12 11:8  7:4   3:0</code></pre>\n<p>Within each field, the two low bits select the mode and the two high bits\nselect the configuration. For our output, <code>MODE = 10</code> and <code>CNF = 00</code>, giving\n<code>0b0010</code>. That selects a general-purpose push-pull output in the 2 MHz mode.</p>\n<p>The 2 MHz value describes the output-driver mode. It doesn’t set the CPU clock or make the LED blink two million times per second. PC13 has stricter drive limits than most pins on this chip, and the slow output mode is appropriate for the onboard LED.</p>\n<p>So we could write this value to the register:</p>\n<pre><code>0b0010 &lt;&lt; 20</code></pre>\n<p>PC13’s field starts at bit 20, so that sets it correctly. It also writes zeroes into every other field. Those zeroes select analog-input mode. They don’t mean “leave this field alone” or “drive this pin low”.</p>\n<p>You might not notice while the LED is the only thing you’re using. Add something else to the port, and configuring the LED could change its settings.</p>\n<p>We can preserve the other fields by reading the register, clearing only PC13’s four bits, and inserting our setting:</p>\n<pre><code>let shift = (LED_PIN - 8) * 4;\nlet config = GPIOC_CRH.read_volatile();\nGPIOC_CRH.write_volatile(\n    (config &amp; !(0b1111 &lt;&lt; shift)) | (GPIO_OUTPUT_PUSHPULL_2_MHZ &lt;&lt; shift),\n);</code></pre>\n<p>Here <code>LED_PIN</code> is 13 and <code>GPIO_OUTPUT_PUSHPULL_2_MHZ</code> is <code>0b0010</code>. The formula\nfor <code>shift</code> accounts for this register beginning at pin 8 and allocating four\nbits per pin.</p>\n<p>Check the meaning of the zeroes you write, too.</p>\n<h2 id=\"some-registers-hold-state-others-accept-commands\">Some registers hold state; others accept commands</h2>\n<p>Once a pin is an output, its output latch selects high or low. GPIO’s <code>ODR</code>,\nthe output data register, holds those latch states as bits. A natural approach\nwould be to read <code>ODR</code>, change the desired bit, and write the result back.</p>\n<p>That approach has a catch when another part of the program can update an output between your read and write. Imagine this sequence:</p>\n<ol><li>The main code reads the output register.</li><li>An interrupt handler changes another output bit.</li><li>The main code changes its bit in the old value and writes that value back.</li></ol>\n<p>The last write can undo the interrupt handler’s change. Volatile reads and writes would preserve all those operations, including the one that overwrites the newer state.</p>\n<p>The GPIO hardware offers a different operation through <code>BSRR</code>, the bit\nset/reset register. Its writes tell the peripheral which bits to change:</p>\n<div class=\"table-wrap\"><table><thead><tr><th>Bits written in BSRR</th><th>Command</th></tr></thead><tbody><tr><td>A one in bits 0 through 15</td><td>Set the corresponding output high.</td></tr><tr><td>A one in bits 16 through 31</td><td>Reset the corresponding output low.</td></tr><tr><td>Zeroes in both command bits for a pin</td><td>Leave that output unchanged.</td></tr></tbody></table></div>\n<p>Changing one output takes a single write, and the other outputs keep their states. Our blink doesn’t enable interrupts, but this is why the hardware offers the operation.</p>\n<p>There’s also <code>BRR</code>, a bit reset register. We don’t need it here because the\nupper half of <code>BSRR</code> already lets us reset a pin. <code>BSRR</code> handles both directions.</p>\n<h3 id=\"low-turns-this-led-on\">Low turns this LED on</h3>\n<p>The board connects the LED and its resistor between the 3.3 V supply and PC13. Driving PC13 low lets current flow through the LED. Driving it high turns the LED off. This is what “active low” means here.</p>\n<p>Our two commands are therefore:</p>\n<pre><code>// Reset PC13 low: LED on.\nGPIOC_BSRR.write_volatile(1 &lt;&lt; (LED_PIN + 16));\n// Set PC13 high: LED off.\nGPIOC_BSRR.write_volatile(1 &lt;&lt; LED_PIN);</code></pre>\n<p>The first writes bit 29, which resets output 13. The second writes bit 13, which sets output 13. The register accepts those commands; it isn’t a variable whose final stored value we care about.</p>\n<p>The program also sets the output latch high before switching PC13 from input to output mode. That way, it begins driving the pin in the LED-off state.</p>\n<h2 id=\"a-loop-iteration-isn-t-a-cpu-cycle\">A loop iteration isn’t a CPU cycle</h2>\n<p>Switching the output immediately back and forth would be too fast to see. The delay in this project is a small loop around a no-operation instruction:</p>\n<pre><code>fn wait(iterations: u32) {\n    for _ in 0..iterations {\n        asm::nop();\n    }\n}</code></pre>\n<p>Each iteration also has to count and decide whether to repeat. In the release build, one delay loop looks like this, with a label substituted for its address:</p>\n<pre><code>delay:\n    subs r1, #1\n    nop\n    bne delay</code></pre>\n<p><code>subs</code> decrements the counter and updates the condition flags. <code>bne</code> repeats\nwhile the result is nonzero. The counter selects how many iterations run,\nnot how many CPU cycles pass. Even counting instructions wouldn’t completely\nsettle the timing, because instructions and branches need not all take one\ncycle.</p>\n<p>The firmware uses <code>200_000</code> iterations as one timing unit. It turns the LED\non for one unit and off for one unit, grouping flashes into counts of one,\ntwo, and three. The gaps between groups total three units, and the gap before\nrepeating totals seven. The code accounts for the off-time already spent\nafter the last flash when adding those longer gaps.</p>\n<p>Those units aren’t microseconds. A hardware timer would be the next step for stable timing, or for letting the CPU do other work while waiting.</p>\n<h3 id=\"check-what-the-compiler-actually-produced\">Check what the compiler actually produced</h3>\n<p>Using Rust still leaves the generated instructions available to inspect.\nWith LLVM’s <code>llvm-objdump</code> installed, run:</p>\n<pre><code>llvm-objdump --disassemble target/thumbv7m-none-eabi/release/blinky-from-scratch</code></pre>\n<p>You can find the delay loop and the stores that change the output. You can\nalso compare a source expression with its implementation. In this build, the\nGPIO configuration mask became a <code>bfi</code> instruction, which inserts a bit field,\nonce the operands were in registers. Several operations in Rust source don’t\nnecessarily become several separate operations on the CPU.</p>\n<p>The ELF also separates the program’s loaded sections from debug information. This build loads 236 bytes of vector table and 408 bytes of code, 644 bytes in total. The ELF file itself is larger because it includes debug symbols and other metadata. Keeping those symbols for GDB doesn’t put them all in microcontroller flash.</p>\n<h2 id=\"try-changing-it\">Try changing it</h2>\n<p>Change <code>GROUP_FLASH_COUNTS</code> in <code>src/main.rs</code> to <code>[3, 2, 1]</code>, rebuild, and flash\nagain. The groups should now count down. Change <code>UNIT</code> to adjust their timing,\nthen compare the source with the generated instructions.</p>\n<p>For a bigger change, replace the busy loop with a timer. RM0008’s clock tree and general-purpose timer chapters are the places to start. You’ll need to work out which clock feeds the timer and how its prescaler and counter turn that into the interval you want.</p>","headings":[{"level":1,"text":"Blinking a Blue Pill LED with Rust, from scratch","id":"blinking-a-blue-pill-led-with-rust-from-scratch"},{"level":2,"text":"The CPU, the microcontroller, and the board","id":"the-cpu-the-microcontroller-and-the-board"},{"level":2,"text":"Where to find the information","id":"where-to-find-the-information"},{"level":3,"text":"Finding a register without guessing","id":"finding-a-register-without-guessing"},{"level":3,"text":"Other docs","id":"other-docs"},{"level":2,"text":"Get the program onto the board","id":"get-the-program-onto-the-board"},{"level":3,"text":"Build once, then choose your probe","id":"build-once-then-choose-your-probe"},{"level":3,"text":"Wire the target","id":"wire-the-target"},{"level":3,"text":"Option A: a Blue Pill running Black Magic","id":"option-a-a-blue-pill-running-black-magic"},{"level":3,"text":"Option B: ST-Link and OpenOCD","id":"option-b-st-link-and-openocd"},{"level":3,"text":"Check the result","id":"check-the-result"},{"level":2,"text":"Who calls main?","id":"who-calls-main"},{"level":3,"text":"Give the CPU a starting point","id":"give-the-cpu-a-starting-point"},{"level":3,"text":"Give global variables their initial values","id":"give-global-variables-their-initial-values"},{"level":3,"text":"Leave somewhere to go when things break","id":"leave-somewhere-to-go-when-things-break"},{"level":2,"text":"When a pointer refers to hardware","id":"when-a-pointer-refers-to-hardware"},{"level":3,"text":"Unsafe doesn’t mean volatile","id":"unsafe-doesn-t-mean-volatile"},{"level":3,"text":"Powering a chip doesn’t enable every peripheral","id":"powering-a-chip-doesn-t-enable-every-peripheral"},{"level":2,"text":"Configuring one pin can change other pins","id":"configuring-one-pin-can-change-other-pins"},{"level":2,"text":"Some registers hold state; others accept commands","id":"some-registers-hold-state-others-accept-commands"},{"level":3,"text":"Low turns this LED on","id":"low-turns-this-led-on"},{"level":2,"text":"A loop iteration isn’t a CPU cycle","id":"a-loop-iteration-isn-t-a-cpu-cycle"},{"level":3,"text":"Check what the compiler actually produced","id":"check-what-the-compiler-actually-produced"},{"level":2,"text":"Try changing it","id":"try-changing-it"}]}}