1. Mengapa Bare Metal?
Bare metal programming berarti memprogram mikrokontroler tanpa library abstraksi seperti HAL atau Arduino framework. Kita berinteraksi langsung dengan hardware registers melalui pointer ke address yang spesifik. Ini memberikan kontrol penuh, binary yang sangat kecil, dan pemahaman mendalam tentang hardware.
| Aspek | Arduino/HAL | Bare Metal |
|---|---|---|
| Binary size | 10-50 KB | 0.5-2 KB |
| Startup time | ~100 ms | ~1 ยตs |
| Predictability | Abstraksi bisa mengejutkan | 100% prediktable |
| Pemahaman HW | Tingkat tinggi | Tingkat register |
| Kurva belajar | Mudah | Sulit |
| Debugging | Serial.print | GDB + register view |
- โ Binary sangat kecil
- โ Prediktable real-time
- โ Tidak ada dependency
- โ Sulit untuk pemula
- โ Tidak portabel antar chip
- โ Mudah dipelajari
- โ Portabel antar chip
- โ Library ecosystem besar
- โ Binary lebih besar
- โ Overhead abstraksi
2. Setup Toolchain ARM GCC
# 1. Install ARM GCC cross-compiler
# Linux/Mac:
sudo apt install gcc-arm-none-eabi libnewlib-arm-none-eabi
# Atau download dari: https://developer.arm.com/downloads/-/gnu-rm
# 2. Install OpenOCD untuk flashing/debugging
sudo apt install openocd
# Atau: https://github.com/openocd-org/openocd
# 3. Install GDB untuk ARM
arm-none-eabi-gdb --version
# 4. Verifikasi
arm-none-eabi-gcc --version
# arm-none-eabi-gcc (GNU Arm Embedded Toolchain 13.2)
# 5. Install ST-Link tools (untuk ST-Link debugger)
sudo apt install stlink-tools
# Atau: https://github.com/stlink-org/stlink
# 6. Install Make
sudo apt install makeMakefile untuk Bare Metal
# Toolchain
CC = arm-none-eabi-gcc
AS = arm-none-eabi-as
LD = arm-none-eabi-ld
OBJCOPY = arm-none-eabi-objcopy
OBJDUMP = arm-none-eabi-objdump
SIZE = arm-none-eabi-size
GDB = arm-none-eabi-gdb
# Target MCU: STM32F103C8T6 (Blue Pill)
TARGET = stm32f103
CPU = cortex-m3
FPU = none
FLOAT = soft
# Files
C_SRC = src/main.c src/startup.c src/system.c
ASM_SRC = src/startup_stm32f103.s
LD_SCRIPT = linker/stm32f103c8.ld
TARGET_ELF = build/$(TARGET).elf
TARGET_BIN = build/$(TARGET).bin
TARGET_HEX = build/$(TARGET).hex
# Flags
CFLAGS = -mcpu=$(CPU) -mthumb -mfloat-abi=$(FLOAT)
CFLAGS += -Wall -Werror -std=c11
CFLAGS += -ffunction-sections -fdata-sections
CFLAGS += -Os -g
CFLAGS += -Iinclude
CFLAGS += -DSTM32F103xB
LDFLAGS = -T$(LD_SCRIPT) -Wl,--gc-sections
LDFLAGS += --specs=nano.specs --specs=nosys.specs
LDFLAGS += -mcpu=$(CPU) -mthumb -mfloat-abi=$(FLOAT)
LDFLAGS += -Wl,-Map=build/$(TARGET).map
.PHONY: all clean flash debug
all: $(TARGET_ELF) $(TARGET_BIN) $(TARGET_HEX)
$(SIZE) $(TARGET_ELF)
build/%.o: src/%.c
@mkdir -p build
$(CC) $(CFLAGS) -c $< -o $@
$(TARGET_ELF): $(C_SRC:.c=.o)
$(CC) $(LDFLAGS) $^ -o $@
$(TARGET_BIN): $(TARGET_ELF)
$(OBJCOPY) -O binary $< $@
$(TARGET_HEX): $(TARGET_ELF)
$(OBJCOPY) -O ihex $< $@
flash: $(TARGET_BIN)
st-flash write $< 0x08000000
debug: $(TARGET_ELF)
$(GDB) $< -ex "target remote :3333" -ex "monitor reset halt"
clean:
rm -rf build/3. Memory Map STM32
STM32 menggunakan arsitektur ARM Cortex-M yang memiliki memory map yang konsisten. Memahami memory map sangat penting untuk bare metal programming.
| Region | Address | Size | Deskripsi |
|---|---|---|---|
| Flash | 0x0800_0000 | 64-128 KB | Program code |
| SRAM | 0x2000_0000 | 20 KB | RAM untuk data |
| APB1 | 0x4000_0000 | - | Low-speed peripherals |
| APB2 | 0x4001_0000 | - | High-speed peripherals |
| AHB | 0x4002_0000 | - | DMA, GPIO, RCC |
| Cortex-M Core | 0xE000_E000 | - | NVIC, SysTick, SCB |
4. Register-Level GPIO Programming
#ifndef STM32F103_H
#define STM32F103_H
#include <stdint.h>
// ============ Peripheral Base Addresses ============
#define PERIPH_BASE 0x40000000UL
#define APB1_BASE (PERIPH_BASE)
#define APB2_BASE (PERIPH_BASE + 0x10000UL)
#define AHB_BASE (PERIPH_BASE + 0x20000UL)
// ============ RCC (Reset & Clock Control) ============
#define RCC_BASE (AHB_BASE + 0x1000UL)
typedef struct {
volatile uint32_t CR; // 0x00: Clock control
volatile uint32_t CFGR; // 0x04: Clock configuration
volatile uint32_t CIR; // 0x08: Clock interrupt
volatile uint32_t APB2RSTR; // 0x0C: APB2 reset
volatile uint32_t APB1RSTR; // 0x10: APB1 reset
volatile uint32_t AHBENR; // 0x14: AHB enable
volatile uint32_t APB2ENR; // 0x18: APB2 enable โ PENTING
volatile uint32_t APB1ENR; // 0x1C: APB1 enable
} RCC_TypeDef;
#define RCC ((RCC_TypeDef *)RCC_BASE)
// RCC APB2ENR bits
#define RCC_APB2ENR_IOPAEN (1 << 2) // GPIOA clock enable
#define RCC_APB2ENR_IOPBEN (1 << 3) // GPIOB clock enable
#define RCC_APB2ENR_IOPCEN (1 << 4) // GPIOC clock enable
#define RCC_APB2ENR_ADC1EN (1 << 9) // ADC1 clock enable
#define RCC_APB2ENR_SPI1EN (1 << 12) // SPI1 clock enable
#define RCC_APB2ENR_USART1EN (1 << 14) // USART1 clock enable
// ============ GPIO ============
#define GPIOA_BASE (APB2_BASE + 0x0800UL)
#define GPIOB_BASE (APB2_BASE + 0x0C00UL)
#define GPIOC_BASE (APB2_BASE + 0x1000UL)
typedef struct {
volatile uint32_t CRL; // 0x00: Configuration low (pin 0-7)
volatile uint32_t CRH; // 0x04: Configuration high (pin 8-15)
volatile uint32_t IDR; // 0x08: Input data register
volatile uint32_t ODR; // 0x0C: Output data register
volatile uint32_t BSRR; // 0x10: Bit set/reset register
volatile uint32_t BRR; // 0x14: Bit reset register
volatile uint32_t LCKR; // 0x18: Lock register
} GPIO_TypeDef;
#define GPIOA ((GPIO_TypeDef *)GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *)GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *)GPIOC_BASE)
// ============ ADC ============
#define ADC1_BASE (APB2_BASE + 0x2400UL)
typedef struct {
volatile uint32_t SR; // 0x00: Status
volatile uint32_t CR1; // 0x04: Control 1
volatile uint32_t CR2; // 0x08: Control 2
volatile uint32_t SMPR1; // 0x0C: Sample time 1
volatile uint32_t SMPR2; // 0x10: Sample time 2
volatile uint32_t JOFR1; // 0x14-0x20
volatile uint32_t JOFR2;
volatile uint32_t JOFR3;
volatile uint32_t JOFR4;
volatile uint32_t HTR; // 0x24
volatile uint32_t LTR; // 0x28
volatile uint32_t SQR1; // 0x2C
volatile uint32_t SQR2; // 0x30
volatile uint32_t SQR3; // 0x34
volatile uint32_t JSQR; // 0x38
volatile uint32_t JDR1; // 0x3C-0x48
volatile uint32_t JDR2;
volatile uint32_t JDR3;
volatile uint32_t JDR4;
volatile uint32_t DR; // 0x4C: Data register
} ADC_TypeDef;
#define ADC1 ((ADC_TypeDef *)ADC1_BASE)
// ============ NVIC ============
#define NVIC_BASE (0xE000E100UL)
typedef struct {
volatile uint32_t ISER[8]; // Interrupt Set Enable
uint32_t RESERVED0[24];
volatile uint32_t ICER[8]; // Interrupt Clear Enable
uint32_t RESERVED1[24];
volatile uint32_t ISPR[8]; // Interrupt Set Pending
uint32_t RESERVED2[24];
volatile uint32_t ICPR[8]; // Interrupt Clear Pending
} NVIC_TypeDef;
#define NVIC ((NVIC_TypeDef *)NVIC_BASE)
#endif // STM32F103_HBlink LED Tanpa HAL
#include "stm32f103.h"
// System clock: 8 MHz internal oscillator (HSI)
#define HSI_FREQ 8000000UL
static void delay(volatile uint32_t count) {
while (count--) {
__asm volatile("nop");
}
}
void system_init(void) {
// GPIOC clock enable (untuk LED PC13)
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
}
void gpio_init(void) {
// PC13 = Output Push-Pull, 2 MHz
// CRL register: pin 13 ada di CRH (pin 8-15)
// Mode bits (0:1): 10 = Output 2 MHz
// CNF bits (2:3): 00 = Push-pull output
// Bit position: (13-8)*4 = 20
// Clear bits untuk pin 13
GPIOC->CRH &= ~(0xF << 20);
// Set: Output push-pull, 2 MHz
GPIOC->CRH |= (0x2 << 20); // Mode=10, CNF=00
}
void led_on(void) {
// BSRR: write 1 ke bit 13 = set HIGH (LED OFF untuk active-low PC13)
// BRR: write 1 ke bit 13 = set LOW (LED ON untuk active-low PC13)
GPIOC->BRR = (1 << 13);
}
void led_off(void) {
GPIOC->BSRR = (1 << 13);
}
void led_toggle(void) {
GPIOC->ODR ^= (1 << 13);
}
int main(void) {
system_init();
gpio_init();
while (1) {
led_toggle();
delay(HSI_FREQ / 10); // ~100ms @ 8 MHz
}
}5. Startup Code
/* STM32F103 Startup Code โ Bare Metal */
.syntax unified
.cpu cortex-m3
.thumb
/* ============ Vector Table ============ */
.section .isr_vector, "a"
.global g_pfnVectors
g_pfnVectors:
.word _estack /* 0x00: Initial stack pointer */
.word Reset_Handler /* 0x04: Reset */
.word NMI_Handler /* 0x08: NMI */
.word HardFault_Handler /* 0x0C: Hard Fault */
.word MemManage_Handler /* 0x10: MPU Fault */
.word BusFault_Handler /* 0x14: Bus Fault */
.word UsageFault_Handler /* 0x18: Usage Fault */
.word 0 /* 0x1C: Reserved */
.word 0 /* 0x20: Reserved */
.word 0 /* 0x24: Reserved */
.word 0 /* 0x28: Reserved */
.word SVC_Handler /* 0x2C: SVCall */
.word DebugMon_Handler /* 0x30: Debug Monitor */
.word 0 /* 0x34: Reserved */
.word PendSV_Handler /* 0x38: PendSV */
.word SysTick_Handler /* 0x3C: SysTick */
/* External IRQs (0-67 for STM32F103) */
.word WWDG_IRQHandler /* IRQ 0 */
.word PVD_IRQHandler /* IRQ 1 */
.word TAMPER_IRQHandler /* IRQ 2 */
.word RTC_IRQHandler /* IRQ 3 */
.word FLASH_IRQHandler /* IRQ 4 */
.word RCC_IRQHandler /* IRQ 5 */
.word EXTI0_IRQHandler /* IRQ 6 */
.word EXTI1_IRQHandler /* IRQ 7 */
.word EXTI2_IRQHandler /* IRQ 8 */
.word EXTI3_IRQHandler /* IRQ 9 */
.word EXTI4_IRQHandler /* IRQ 10 */
.word DMA1_Channel1_IRQHandler /* IRQ 11 */
.word DMA1_Channel2_IRQHandler /* IRQ 12 */
.word DMA1_Channel3_IRQHandler /* IRQ 13 */
.word DMA1_Channel4_IRQHandler /* IRQ 14 */
.word DMA1_Channel5_IRQHandler /* IRQ 15 */
.word ADC1_2_IRQHandler /* IRQ 18 */
.word TIM2_IRQHandler /* IRQ 28 */
.word TIM3_IRQHandler /* IRQ 29 */
.word TIM4_IRQHandler /* IRQ 30 */
.word USART1_IRQHandler /* IRQ 37 */
.word USART2_IRQHandler /* IRQ 38 */
.word SPI1_IRQHandler /* IRQ 35 */
/* ============ Reset Handler ============ */
.section .text.Reset_Handler
.global Reset_Handler
.type Reset_Handler, %function
Reset_Handler:
/* Set stack pointer */
ldr sp, =_estack
/* Copy .data dari flash ke SRAM */
ldr r0, =_sdata
ldr r1, =_edata
ldr r2, =_sidata
copy_data:
cmp r0, r1
bge zero_bss
ldr r3, [r2], #4
str r3, [r0], #4
b copy_data
/* Zero out .bss */
zero_bss:
ldr r0, =_sbss
ldr r1, =_ebss
movs r2, #0
zero_loop:
cmp r0, r1
bge call_main
str r2, [r0], #4
b zero_loop
/* Call main() */
call_main:
bl main
/* Infinite loop jika main return */
hang:
b hang
/* ============ Default Handlers ============ */
.global Default_Handler
.type Default_Handler, %function
Default_Handler:
b .
.weak NMI_Handler
.weak HardFault_Handler
.weak MemManage_Handler
.weak BusFault_Handler
.weak UsageFault_Handler
.weak SVC_Handler
.weak DebugMon_Handler
.weak PendSV_Handler
.weak SysTick_Handler
.weak EXTI0_IRQHandler
.weak TIM2_IRQHandler
.weak ADC1_2_IRQHandler
.weak DMA1_Channel1_IRQHandler
.weak USART1_IRQHandler
.set NMI_Handler, Default_Handler
.set HardFault_Handler, Default_Handler
.set MemManage_Handler, Default_Handler
.set BusFault_Handler, Default_Handler
.set UsageFault_Handler, Default_Handler
.set SVC_Handler, Default_Handler
.set DebugMon_Handler, Default_Handler
.set PendSV_Handler, Default_Handler
.set SysTick_Handler, Default_Handler
.set EXTI0_IRQHandler, Default_Handler
.set TIM2_IRQHandler, Default_Handler
.set ADC1_2_IRQHandler, Default_Handler
.set DMA1_Channel1_IRQHandler, Default_Handler
.set USART1_IRQHandler, Default_Handler6. Linker Script (.ld)
/* STM32F103C8T6 Linker Script */
/* 64KB Flash, 20KB SRAM */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
_estack = ORIGIN(SRAM) + LENGTH(SRAM);
SECTIONS
{
/* Vector table โ harus di awal flash */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} > FLASH
/* Code dan read-only data */
.text :
{
. = ALIGN(4);
*(.text)
*(.text*)
*(.rodata)
*(.rodata*)
. = ALIGN(4);
_etext = .;
} > FLASH
/* Data initialized: disimpan di flash, di-copy ke SRAM */
_sidata = LOADADDR(.data);
.data :
{
. = ALIGN(4);
_sdata = .;
*(.data)
*(.data*)
. = ALIGN(4);
_edata = .;
} > SRAM AT> FLASH
/* BSS: zero-initialized data */
.bss :
{
. = ALIGN(4);
_sbss = .;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} > SRAM
/* Heap & Stack info */
.heap (NOLOAD) :
{
. = ALIGN(8);
_end = .;
. = . + 0x1000; /* 4KB heap */
. = ALIGN(8);
} > SRAM
/DISCARD/ :
{
*(.ARM.exidx*)
*(.ARM.extab*)
}
}7. Interrupt Handling (NVIC)
#include "stm32f103.h"
// EXTI (External Interrupt) registers
#define EXTI_BASE (APB2_BASE + 0x0400UL)
#define AFIO_BASE (APB2_BASE + 0x0000UL)
typedef struct {
volatile uint32_t IMR; // Interrupt mask
volatile uint32_t EMR; // Event mask
volatile uint32_t RTSR; // Rising trigger select
volatile uint32_t FTSR; // Falling trigger select
volatile uint32_t SWIER; // Software interrupt event
volatile uint32_t PR; // Pending register
} EXTI_TypeDef;
typedef struct {
volatile uint32_t EVCR;
volatile uint32_t MAPR;
volatile uint32_t EXTICR[4];
} AFIO_TypeDef;
#define EXTI ((EXTI_TypeDef *)EXTI_BASE)
#define AFIO ((AFIO_TypeDef *)AFIO_BASE)
// Setup EXT0 interrupt pada PA0 (tombol)
void setup_exti0(void) {
// 1. Enable GPIOA dan AFIO clock
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | (1 << 0); // AFIO
// 2. Set PA0 as input with pull-up
GPIOA->CRL &= ~(0xF << 0); // Clear PA0 config
GPIOA->CRL |= (0x8 << 0); // Input with pull-up
GPIOA->ODR |= (1 << 0); // Pull-up
// 3. Map EXTI0 to PA0
AFIO->EXTICR[0] &= ~(0xF << 0); // EXTI0 = PA0
// 4. Configure EXTI0: falling edge trigger
EXTI->IMR |= (1 << 0); // Unmask EXTI0
EXTI->FTSR |= (1 << 0); // Falling edge
EXTI->RTSR &= ~(1 << 0); // Disable rising edge
// 5. Enable NVIC IRQ untuk EXTI0 (IRQ 6)
NVIC->ISER[6 / 32] = (1 << (6 % 32));
}
// Interrupt handler โ nama harus sesuai vector table
void EXTI0_IRQHandler(void) {
if (EXTI->PR & (1 << 0)) {
EXTI->PR = (1 << 0); // Clear pending (write 1 to clear)
GPIOC->ODR ^= (1 << 13); // Toggle LED
}
}8. DMA: Direct Memory Access
DMA memungkinkan transfer data antara peripheral dan memory tanpa campur tangan CPU. Ini sangat efisien untuk ADC sampling, UART transfer, dan SPI communication.
#include "stm32f103.h"
// DMA1 registers
#define DMA1_BASE (AHB_BASE + 0x0000UL)
typedef struct {
volatile uint32_t CCR; // Channel config
volatile uint32_t CNDTR; // Number of data
volatile uint32_t CPAR; // Peripheral address
volatile uint32_t CMAR; // Memory address
} DMA_Channel_TypeDef;
typedef struct {
volatile uint32_t ISR; // Interrupt status
volatile uint32_t IFCR; // Interrupt flag clear
} DMA_TypeDef;
#define DMA1 ((DMA_TypeDef *)DMA1_BASE)
#define DMA1_CH1 ((DMA_Channel_TypeDef *)(DMA1_BASE + 0x08))
// Buffer untuk ADC data via DMA
volatile uint16_t adc_buffer[100]; // 100 samples
void setup_adc_dma(void) {
// 1. Enable clocks
RCC->AHBENR |= (1 << 0); // DMA1 clock
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN | RCC_APB2ENR_IOPAEN;
// 2. Setup PA0 sebagai analog input
GPIOA->CRL &= ~(0xF << 0); // Analog mode
// 3. Configure ADC1
ADC1->CR2 = 0; // Reset
ADC1->SQR3 = 0; // Channel 0
ADC1->SMPR2 = (0b111 << 0); // 239.5 cycles sample time
ADC1->CR1 = (1 << 8); // SCAN mode
ADC1->CR2 = (1 << 1) // CONT: Continuous conversion
| (1 << 8) // DMA: Enable DMA
| (1 << 3); // RSTCAL: Reset calibration
// 4. Calibrate ADC
ADC1->CR2 |= (1 << 2); // CAL
while (ADC1->CR2 & (1 << 2)); // Tunggu selesai
// 5. Configure DMA1 Channel 1 (ADC1)
DMA1_CH1->CPAR = (uint32_t)&(ADC1->DR); // Source: ADC data register
DMA1_CH1->CMAR = (uint32_t)adc_buffer; // Dest: RAM buffer
DMA1_CH1->CNDTR = 100; // 100 transfers
DMA1_CH1->CCR = (0 << 10) // MSIZE: 16-bit
| (0 << 8) // PSIZE: 16-bit
| (1 << 7) // MINC: Memory increment
| (0 << 6) // PINC: No peripheral increment
| (1 << 5) // CIRC: Circular mode
| (0 << 4) // DIR: Peripheral to memory
| (1 << 0); // EN: Enable
// 6. Start ADC
ADC1->CR2 |= (1 << 0); // ADON: Power on
ADC1->CR2 |= (1 << 22); // SWSTART: Start conversion
}
int main(void) {
setup_adc_dma();
while (1) {
// adc_buffer selalu ter-update oleh DMA!
// CPU bisa melakukan hal lain
uint32_t sum = 0;
for (int i = 0; i < 100; i++) {
sum += adc_buffer[i];
}
uint16_t avg = sum / 100;
float voltage = (avg * 3.3f) / 4096.0f;
// Gunakan voltage...
(void)voltage;
}
}9. ADC: Analog to Digital Converter
STM32F103 memiliki ADC 12-bit dengan hingga 10 channel. Pengukuran analog sangat penting untuk sensor suhu, potensiometer, dan sensor analog lainnya.
| Parameter | Nilai | Penjelasan |
|---|---|---|
| Resolusi | 12-bit (0-4095) | Bisa di-set 6/8/10/12 bit |
| Vref | 3.3V (typical) | Reference voltage |
| Sampling Time | 1.5 - 239.5 cycles | Lebih lama = lebih akurat |
| Conversion Time | 1 ยตs minimum | Sample time + 12.5 cycles |
| Channels | 10 external | PA0-PA7, PB0-PB1 |