Building an STM32 Keil MDK Project in Pure Assembly and Blinking an LED
Target hardware and tools
- MCU/board: STM32F103 series (e.g., STM32-F103-MINI)
- Toolchain: Keil MDK-ARM (uVision5 or newer)
- Optional: Serial terminal for observation
1. A minimal Keil project written entirely in assembly
1.1 Create the project
- Open Keil uVision, create a new project (example name: asm_demo).
- Choose your STM32F103 device.
- In the Pack Installer dialog, enable at least:
- CMSIS → Core
- Device → Startup (provides system startup and vector table if you choose to keep it; for a pure-assembly demo you can also provide your own minimal vector table)
1.2 Add an assembly source file
- Right‑click Source Group 1 → Add New Item → Asm Module.
- Name it demo.s.
1.3 Example assemb code
The following snippet demonstrates calls, link register usage, and a simple infinite loop. It is written for ARMASM/Thumb on Cortex‑M (little‑endian) and exports __main as entry.
AREA |.text|, CODE, READONLY
THUMB
REQUIRE8
PRESERVE8
EXPORT __main
ENTRY
__main
; initialize some scratch registers
MOVS r4, #3
MOVS r5, #7
BL add_pair ; r6 = r4 + r5
BL mix_registers ; r7 = r6 ^ r5
BL clobber_example ; demonstrate prologue/epilogue with PUSH/POP
stay_here
B stay_here
; r6 = r4 + r5
add_pair
ADDS r6, r4, r5
BX lr
; r7 = r6 XOR r5
mix_registers
EORS r7, r6, r5
BX lr
; shows function frame with saved registers
clobber_example
PUSH {r0, r1, lr}
MOVS r0, #0x12
ADDS r1, r0, #0x34
; do something trivial to keep the pipeline busy
EORS r0, r0, r1
POP {r0, r1, pc}
Save the file and build.
1.4 Run-time debugging (stepping and watch windows)
- Select a debugger (Options for Target → Debug → your debug adapter, e.g., ST-LINK).
- Start a debug session.
- Set breakpoints on labels like __main or mix_registers.
- Open the Registers and Watch windows and add r4–r7 to observe changes as you single‑step.
1.5 Produce and inspect the HEX file
-
Enable Output → Create HEX File in target options, then rebuild.
-
The Intel HEX format encodes binary data as ASCII records:
- Each line: ":" [byte count] [address] [record type] [data…] [checksum] CRLF
- Example first non‑data record often seen for STM32:
:020000040800F2- 02 → byte count (2 data bytes)
- 0000 → address field (0x0000)
- 04 → record type (Extended Linear Address)
- 0800 → data (upper 16 bits of the 32‑bit linear address = 0x0800 → base 0x0800_0000)
- F2 → checksum
-
The first eight data bytes at the image’s base address (0x0800_0000 for STM32F1) are the vector table’s first two entries:
- Word 0: initial Main Stack Pointer value (MSP)
- Word 1: Reset_Handler address (LSB set becuase Thumb state) Because Cortex‑M is little‑endian, each 32‑bit word is stored least‑significant byte first in the HEX data records that map to 0x0800_0000.
1.6 Section and image size
- After a build, Keil prints a size summary (or check the .map file):
- Code and RO-data (Flash)
- RW-data (initial values stored in Flash; copied to RAM at startup)
- ZI-data (zero‑initialized data in RAM; not stored in HEX, only size reserved)
- These correspond to segments the linker emits and what eventually appears in the HEX (only what must reside in non‑volatile memory is emitted as data records).
2. LED blink (1 Hz) in pure assembly
Below is a standalone assembly program with a minimal vector table and a software delay. It enables the GPIOC clock and configures PC2 as push‑pull output (change the pin if your board’s LED is on a different GPIO, e.g., PC13). It then toggles the pin roughly once per second.
Notes:
- RCC base: 0x4002_1000; RCC_APB2ENR at 0x4002_1018
- GPIOC base: 0x4001_1000
- GPIOC_CRL (pins 0–7) at 0x4001_1000
- GPIOC_CRH (pins 8–15) at 0x4001_1004
- GPIOC_BSRR at 0x4001_1010
- For PC2, use CRL (bits for pin 2 are [11:8]); MODE=10 (2 MHz), CNF=00 → nibble value 0x2
- BSRR lower half sets a pin; upper half (bit+16) resets a pin
;============================================================
; Minimal STM32F103 LED blink on PC2 (active-high)
;============================================================
LED_PIN_BIT EQU 2
RCC_APB2ENR EQU 0x40021018
GPIOC_CRL EQU 0x40011000
GPIOC_BSRR EQU 0x40011010
IOPCEN_BIT EQU (1<<4) ; enable GPIOC clock
; Stack
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
; Vector table (minimal)
AREA RESET, DATA, READONLY
__Vectors
DCD __initial_sp ; Initial MSP
DCD Reset_Handler ; Reset vector
; Code
AREA |.text|, CODE, READONLY
THUMB
REQUIRE8
PRESERVE8
ENTRY
Reset_Handler
; enable GPIOC clock
LDR r0, =RCC_APB2ENR
LDR r1, [r0]
ORR r1, r1, #IOPCEN_BIT
STR r1, [r0]
; configure PC2: MODE=10 (2 MHz), CNF=00 → nibble = 0b0010
LDR r0, =GPIOC_CRL
LDR r2, [r0]
; clear bits [11:8] for pin 2
LDR r3, =0xFFFFF0FF ; mask to clear the nibble for pin 2
AND r2, r2, r3
; set MODE/CNF for pin 2
ORR r2, r2, #(0x2 << (LED_PIN_BIT*4))
STR r2, [r0]
main_loop
; set PC2 high
LDR r0, =GPIOC_BSRR
MOVS r1, #(1<<LED_PIN_BIT)
STR r1, [r0]
BL delay_long
; set PC2 low
MOVS r1, #(1<<(LED_PIN_BIT+16))
STR r1, [r0]
BL delay_long
B main_loop
; crude delay loop (~1 s depending on clock; adjust constants as needed)
; Uses r4–r6 as counters, preserves lr
delay_long
PUSH {r4, r5, r6, lr}
MOVS r4, #0
MOVS r5, #0
MOVS r6, #0
.dl0
ADDS r4, r4, #1
CMP r4, #900
BCC .dl0
MOVS r4, #0
ADDS r5, r5, #1
CMP r5, #900
BCC .dl0
MOVS r4, #0
MOVS r5, #0
ADDS r6, r6, #25
CMP r6, #25
BCC .dl0
POP {r4, r5, r6, pc}
END
Build the target with "Create HEX File" enabled and program the device using your preferred tool. If your LED is connected to a different pin, update LED_PIN_BIT and the GPIO register addresses/nibbles accordingly.