하드웨어 추상화 계층 (std::hal)
std/hal/은 흔히 쓰이는 임베디드 주변장치를, 해당 주변장치의 MMIO 베이스
주소를 담고 있는 구조체 인스턴스의 메서드로 감쌉니다 —
정적 메서드 new()(GpioPin::new(...),
Uart::new(...))로 인스턴스를 만든 다음 그 위에서 메서드를 호출하면 됩니다.
각 드라이버는 그 주변장치의 레지스터를 아는 칩 타입에 대해 제네릭이며,
칩 타입은 변수의 타입에서 컴파일 타임에 정해지고(BST<K, V, C>의 순서
관계와 같은 방식) 칩 트레이트로 검사됩니다. 그래서 호출은
레지스터 직접 접근으로 컴파일됩니다.
모든 모듈이 프리스탠딩 환경에서 안전하며(libc 의존성 없음), 일반적인
--target 크로스 컴파일은 물론 --freestanding에서도 동작합니다.
#include <std/hal/gpio.h>
#include <std/hal/uart.h>
void blink(&volatile unsigned int gpioa, &volatile unsigned int usart2) {
struct GpioPin<Stm32F4Gpio> led = std::GpioPin::new(gpioa, 5, GPIO_OUTPUT);
led.write(1);
led.toggle();
struct Uart<Stm32F4Usart> con = std::Uart::new(usart2, 115200u, 42000000u);
con.write_str("blink\n");
}
주변장치 기준 주소는 &volatile unsigned int 참조이므로 모든 레지스터 접근이
volatile 로드나 스토어가 됩니다. 데이터시트의 주소로부터 unsafe 안에서 한 번
만드십시오 — 컴파일러는 그 주소가 유효한지 알 수 없습니다:
&volatile unsigned int GPIOA_BASE() {
unsafe { return (&volatile unsigned int)0x40020000UL; }
}
정적 메서드는 네임스페이스를 거쳐 호출하지만 구조체 타입 자체에는
네임스페이스가 적용되지 않는다는 점에 유의합니다 — 타입은 struct GpioPin이고,
namespace std 밖에서의 호출은 std::GpioPin::new(...)입니다
(네임스페이스 참고). 칩 타입은
선언된 변수에서 정해지므로 new에는 타입 인자를 주지 않습니다.
드라이버와 칩
| 모듈 | 헤더 | 타입 | 설명 |
|---|---|---|---|
| GPIO | hal/gpio.h |
GpioPin<C> |
GpioPin::new(base, pin, direction); set_direction, write, read, toggle, set_pull |
| I2C | hal/i2c.h |
I2cBus<C> |
I2cBus::new(base, speed, clock_hz); write, read, write_read(쓰기 후 읽기), probe; 전송이 실패해도 STOP을 보내 버스를 해제합니다 |
| SPI | hal/spi.h |
SpiDevice<C> |
SpiDevice::new(base, mode); transfer(1바이트, 전이중), cs_assert/cs_deassert, write(tx, rx)([]const unsigned char tx와 선택적 ?[]unsigned char rx. 입력을 버리려면 null), read(rx) |
| UART | hal/uart.h |
Uart<C> |
Uart::new(base, baud, clock_hz); write_byte, read_byte, write_str, rx_ready, tx_ready |
| Timer | hal/timer.h |
Timer<C> |
Timer::new(base, prescaler); set_period, start, stop, read, clear_flag, flag_set |
| Watchdog | hal/watchdog.h |
Watchdog<C> |
Watchdog::new(base, timeout); enable(일반적으로 한 번 활성화되면 비활성화할 수 없음), feed, caused_reset |
상수: GPIO_INPUT/GPIO_OUTPUT, GPIO_PULL_NONE/GPIO_PULL_UP/GPIO_PULL_DOWN; I2C 결과 I2C_OK/I2C_NACK/I2C_ERROR; SPI_MODE_0–SPI_MODE_3(CPOL/CPHA = 00, 01, 10, 11).
clock_hz는 주변장치의 입력 클록으로, 보 레이트나 버스 속도 분주값을
계산하는 데 씁니다. Uart::new에 clock_hz = 0을 주면 부트 펌웨어가 설정한
속도를 그대로 둡니다.
칩 타입
| 칩 타입 | 주변장치 |
|---|---|
GpioGeneric, UartGeneric, SpiGeneric, I2cGeneric, TimerGeneric, WatchdogGeneric |
아래의 기준 레지스터 레이아웃 — 이를 구현하는 FPGA 소프트 코어나 시뮬레이터용 |
Pl011 |
ARM PrimeCell PL011 UART(QEMU virt, Raspberry Pi, 여러 Arm SoC) |
Ns16550 |
바이트 폭 레지스터가 연속 주소에 있는 16550 호환 UART(QEMU RISC-V virt, 여러 SoC) |
Stm32F4Gpio |
STM32 GPIO 포트(MODER/PUPDR/IDR/ODR/BSRR; F2/F4/F7/L4/G0/H7 레이아웃). write는 BSRR를 쓰므로 읽기-수정-쓰기가 필요 없습니다 |
Stm32F4Usart |
STM32F1/F2/F4 USART(SR/DR/BRR/CR1) |
Stm32F4Spi |
마스터 모드 STM32F1/F2/F4 SPI, 소프트웨어 NSS, fPCLK/16. 칩 셀렉트는 일반 GPIO이므로 cs_assert/cs_deassert는 아무 일도 하지 않습니다 — GpioPin으로 구동합니다 |
Stm32F4I2c |
STM32F1/F2/F4 I2C("v1": CR1/CR2/DR/SR1/SR2/CCR/TRISE), 표준 모드와 고속 모드 |
Stm32F4Tim |
STM32 범용 타이머(TIM2–TIM5): PSC, ARR, CNT, 업데이트 플래그 UIF. 카운터는 clock / (prescaler + 1)로 증가합니다 |
Stm32F4Iwdg |
STM32 독립 워치독: timeout은 LSI / 256으로 세는 12비트 리로드 값(32 kHz에서 단위당 약 8 ms)이며, caused_reset은 STM32F4 RCC 주소의 RCC_CSR.IWDGRSTF를 읽습니다 |
칩 타입은 주변장치의 기준 주소를 받아 레지스터 작업을 하는 메서드를, 대응하는 칩 트레이트가 나열한 대로 가진 구조체입니다. 그래서 다른 부품은 그런 구조체를 하나 더 작성하면 지원됩니다.
Pl011, Ns16550, Stm32F4Usart, Stm32F4Tim, Stm32F4Spi 칩은
tests/stdlib/baremetal/qemu/run.sh에서 QEMU의 해당 주변장치 모델을 상대로
실행되며, 모든 칩의 레지스터 시퀀스는 호스트 테스트에서 RAM 기반 레지스터로
검사합니다.
칩 트레이트
각 드라이버의 칩 파라미터는 드라이버가 호출하는 레지스터 연산을 나열한
트레이트로 제한됩니다. 모든
메서드는 주변장치의 기준 주소를 unsigned long으로 받으며 const입니다 —
칩 타입은 자기 상태를 갖지 않습니다(std의 칩들은 쓰이지 않는 int 필드
하나만 가집니다).
| 트레이트 | 요구하는 메서드 | 제한하는 타입 | 만족하는 타입 |
|---|---|---|---|
GpioChip |
set_direction(base, pin, direction), write(base, pin, value), read(base, pin), toggle(base, pin), set_pull(base, pin, pull) |
GpioPin<C: GpioChip> |
GpioGeneric, Stm32F4Gpio |
UartChip |
init(base, baud, clock_hz), tx_ready(base), rx_ready(base), put(base, c), get(base) |
Uart<C: UartChip> |
UartGeneric, Pl011, Ns16550, Stm32F4Usart |
I2cChip |
init(base, speed, clock_hz), write(base, addr, data), read(base, addr, data)(바이트 슬라이스), probe(base, addr)(I2C_* 결과) |
I2cBus<C: I2cChip> |
I2cGeneric, Stm32F4I2c |
SpiChip |
init(base, mode), transfer(base, tx), cs(base, asserted) |
SpiDevice<C: SpiChip> |
SpiGeneric, Stm32F4Spi |
TimerChip |
init(base, prescaler), set_period(base, period), run(base, on), count(base), flag(base), clear_flag(base) |
Timer<C: TimerChip> |
TimerGeneric, Stm32F4Tim |
WatchdogChip |
init(base, timeout), enable(base), feed(base), caused_reset(base) |
Watchdog<C: WatchdogChip> |
WatchdogGeneric, Stm32F4Iwdg |
메서드가 하나라도 빠진 칩으로 드라이버를 지정하면 드라이버 내부 깊은 곳이 아니라 선언한 자리에서 오류가 납니다:
struct Uart<Stm32F4Gpio> con = Uart::new(base, 115200u, 0u);
error: type 'Stm32F4Gpio' does not satisfy constraint 'UartChip' for generic
parameter 'C' of struct 'Uart' (no method 'init')
직접 만든 부품은 메서드를 갖추는 것만으로 트레이트를 만족합니다 — 트레이트에 대해 따로 선언할 것은 없습니다. 트레이트에 대해 작성한 코드는 모든 칩에서 동작합니다:
#include <std/hal/uart.h>
// base + 0에 쓰기 전용 데이터 레지스터 하나가 있는 디버그 포트.
struct DebugPort {
int unused_;
void init(unsigned long base, unsigned int baud, unsigned int clock_hz) const;
int tx_ready(unsigned long base) const;
int rx_ready(unsigned long base) const;
void put(unsigned long base, unsigned char c) const;
unsigned char get(unsigned long base) const;
};
void DebugPort::init(unsigned long base, unsigned int baud, unsigned int clock_hz) const { }
int DebugPort::tx_ready(unsigned long base) const { return 1; }
int DebugPort::rx_ready(unsigned long base) const { return 0; }
void DebugPort::put(unsigned long base, unsigned char c) const {
unsafe { *(&volatile unsigned int)base = (unsigned int)c; }
}
unsigned char DebugPort::get(unsigned long base) const { return (unsigned char)0; }
// 모든 UART 칩에 대해 한 번만 작성합니다.
generic<C: UartChip>
void banner(&Uart<C> con) {
con.write_str("boot\n");
}
int main() {
unsigned int reg = 0u;
struct Uart<DebugPort> con = Uart::new(®, 0u, 0u);
banner(&con);
return reg == 10u ? 0 : 1; // 마지막으로 쓴 바이트는 '\n'
}
기준 레이아웃
*Generic 칩은 다음 레지스터를 씁니다(base로부터의 바이트 오프셋, 32비트
레지스터, GPIO는 핀마다 1비트):
| 칩 | 레지스터 |
|---|---|
GpioGeneric |
DIR +0x00(1 = 출력), OUT +0x04, IN +0x08, PULL_EN +0x0C, PULL_SEL +0x10(1 = 풀업, 0 = 풀다운) |
UartGeneric |
DATA +0x00, STATUS +0x04(비트 0 RX 준비, 비트 1 TX 준비), BAUD +0x08(baud를 기록) |
SpiGeneric |
DATA +0x00, STATUS +0x04(비트 0 사용 중, 비트 1 TX 준비), CTRL +0x08(mode | 0x80 활성화), CS +0x0C(0 = 선택) |
I2cGeneric |
DATA +0x00, STATUS +0x04(비트 0 사용 중, 비트 1 ACK, 비트 2 오류), CTRL +0x08(1 START, 2 STOP, 4 SEND, 8 READ+ACK, 16 READ+NACK), ADDR +0x0C, SPEED +0x10 |
TimerGeneric |
CTRL +0x00(비트 0 활성화), PRESCALER +0x04, PERIOD +0x08, COUNT +0x0C, STATUS +0x10(비트 0 플래그) |
WatchdogGeneric |
CTRL +0x00(비트 0 활성화), TIMEOUT +0x04, FEED +0x08(0x6969 기록), STATUS +0x0C(비트 0: 마지막 리셋이 워치독 리셋) |
모두 원시 MMIO 주소에 직접 작동하는 폴링 기반 드라이버입니다(인터럽트 기반
변형은 없음) — 주변장치가 완료를 비동기적으로 알려야 할 경우 인터럽트를
다루는 쪽은 std::interrupt(ISR 등록, 벡터
테이블, MMIO 비트필드 헬퍼)를 참고합니다.
ARM Cortex-M
hal/cortex_m.h — NVIC, SysTick, SCB를 Cortex-M 표준 메모리 맵 주소에
위치한 전역 인스턴스로 제공합니다. 전체 설명(HAL 사용법, DSP 확장
내장 함수, MVE)은 베어메탈 → ARM Cortex-M을
참고합니다.
#define NVIC_BASE 0xE000E100UL
#define SYSTICK_BASE 0xE000E010UL
#define SCB_BASE 0xE000ED00UL
struct Nvic { // global instance: nvic (nvic_init())
&volatile unsigned int base;
void enable(unsigned int irq);
void disable(unsigned int irq);
int is_pending(unsigned int irq) const;
void clear_pending(unsigned int irq);
void set_priority(unsigned int irq, unsigned char priority);
unsigned char get_priority(unsigned int irq) const;
};
struct SysTick { // global instance: systick (systick_init())
&volatile unsigned int base;
void start(unsigned int reload, int use_core_clock);
void stop();
unsigned int read() const;
int flag_set() const; // COUNTFLAG: wrapped since last read
};
struct Scb { // global instance: scb (scb_init())
&volatile unsigned int base;
void system_reset();
void enable_fault(unsigned int fault_mask); // SCB_FAULT_* bits
void disable_fault(unsigned int fault_mask);
void set_vtor(unsigned int addr); // relocate the vector table
int in_handler_mode() const; // 1 inside an exception handler
void set_psp(unsigned int addr); // process stack pointer
void set_msp(unsigned int addr); // main stack pointer
};
#define SCB_FAULT_USAGE (1u << 18)
#define SCB_FAULT_BUS (1u << 17)
#define SCB_FAULT_MEM (1u << 16)
AArch64
hal/aarch64.h — 위의 Cortex-M 마이크로컨트롤러 HAL과는 별개로,
ARMv8-A 애플리케이션 코어(Cortex-A53/A57/A72/A55/A76)를 대상으로 합니다.
시스템 레지스터(인스턴스 없이 사용하는 자유 함수):
aa64_read_mpidr()(프로세서 어피니티/CPU ID), aa64_read_currentel()
(예외 레벨 EL0–EL3), aa64_read_daif()/aa64_write_daif()(인터럽트
마스크 비트), aa64_irq_enable()/aa64_irq_disable(),
aa64_fiq_enable()/aa64_fiq_disable(), aa64_isb()/aa64_dsb_sy()/
aa64_dmb_sy()(배리어).
제네릭 타이머(Aa64Timer, 전역 인스턴스 aa64_timer):
read_cntpct()(시스템 카운터), read_cntfrq()(카운터 주파수),
set_tval(), enable()/disable(), fire_pending().
GIC(GICv2 호환; GicDist/GicCpu, 전역 인스턴스 gic_dist/
gic_cpu, gic_init(dist_base, cpu_base)로 초기화; 기본 주소는
GIC_DIST_BASE = 0x08000000, GIC_CPU_BASE = 0x08010000으로 QEMU
virt / Juno 배치): 분배기(distributor) 쪽의 enable_group0,
enable_irq/disable_irq, set_priority, set_target(CPU 어피니티
마스크), set_config(에지 트리거 대 레벨 트리거),
is_pending/clear_pending; CPU 인터페이스 쪽의 ack()(대기 중인 IRQ
번호를 반환), eoi()(인터럽트 종료), running_priority().
RISC-V
hal/riscv.h — 주소는 SiFive FE310 / 표준 RISC-V MMIO 맵을 따릅니다.
CSR 접근(인라인 asm을 사용하는 자유 함수):
rv_csr_read_mstatus/rv_csr_write_mstatus, rv_csr_read_mie/
rv_csr_write_mie, rv_csr_read_mip, rv_csr_read_mcause,
rv_csr_read_mepc/rv_csr_write_mepc, rv_csr_read_mtvec/
rv_csr_write_mtvec, rv_csr_read_time/rv_csr_read_cycle/
rv_csr_read_instret, 그리고 rv_global_irq_enable/
rv_global_irq_disable.
CLINT(Clint, 전역 인스턴스 clint, clint_init(base)로 생성; 기본 주소 CLINT_BASE = 0x02000000):
set_msip/clear_msip(하트별 소프트웨어 인터럽트), set_mtimecmp/
read_mtime, schedule(delta)(delta 틱 뒤에 타이머 인터럽트 발생).
PLIC(Plic, 전역 인스턴스 plic, plic_init(base)로 생성; 기본 주소 PLIC_BASE = 0x0C000000, IRQ는 PLIC_MAX_IRQ = 53까지):
set_priority, enable/disable, set_threshold, claim()(우선순위가
가장 높은 대기 중 IRQ, 0이면 없음), complete(irq).