임베디드 소프트웨어를 공부하다 보면 MCU, ARM, Cortex-M, STM32, Firmware, bare-metal, cross compile 같은 단어가 계속 등장한다.
각 단어를 따로 보면 어렵지 않은데, 처음에는 이들이 서로 어떤 관계인지가 상당히 헷갈린다.
이번 글에서는 세부적인 개발 방법보다는 임베디드 소프트웨어를 이해하기 위해 필요한 기본적인 구조와 용어의 관계만 정리해본다.
Firmware
임베디드 시스템은 특정한 목적을 수행하기 위해 하드웨어와 소프트웨어가 함께 구성된 시스템이다. 키보드, 가전제품, 자동차 제어기, 센서 장치 등 우리가 흔히 사용하는 많은 장치가 여기에 해당한다.
이러한 장치 내부에서 하드웨어를 제어하며 실행되는 소프트웨어를 보통 Firmware라고 부른다.
Embedded Device
├─ Hardware
└─ Firmware
일반적인 PC에서 실행되는 응용 프로그램과 달리 Firmware는 하드웨어와 매우 가까운 위치에서 동작한다.
Firmware가 실제로 실행되는 대표적인 하드웨어가 MCU다.
MCU
Microcontroller Unit
간단히 말하면 특정 장치를 제어하기 위해 만들어진 작은 컴퓨터라고 볼 수 있다. 일반적인 PC에서는 CPU, RAM, 저장장치 등이 각각 별도의 부품으로 존재하지만 MCU는 하나의 칩 안에 여러 기능이 함께 들어있는 경우가 많다.
MCU
├─ CPU Core
├─ Flash
├─ RAM
├─ GPIO
├─ Timer
├─ UART
├─ SPI
├─ I2C
└─ 기타 Peripheral
- CPU Core가 프로그램을 실행하고, Flash에는 Firmware가 저장되며 RAM은 프로그램 실행 중 필요한 데이터를 저장한다.
- GPIO, Timer, UART, SPI, I2C 등은 외부 장치를 제어하거나 통신하기 위한 기능으로, 이러한 하드웨어 기능을 Peripheral이라고 부른다.
- 따라서 MCU 하나만으로도 외부 센서를 읽거나 모터를 제어하고 통신을 수행하는 등의 작업을 할 수 있다.
ARM과 Cortex-M
MCU 안에는 실제 명령어를 실행하는 CPU Core가 존재한다.
ARM은 CPU 아키텍처를 설계하는 회사이자 그 아키텍처 계열을 의미한다. 임베디드에서 자주 보는 Cortex-M은 ARM이 MCU를 위해 설계한 CPU Core 계열이다.
ARM Architecture
Cortex-A
Cortex-R
Cortex-M
├─ Cortex-M0
├─ Cortex-M3
├─ Cortex-M4
├─ Cortex-M7
└─ ...
MCU 제조사는 ARM의 Cortex-M과 같은 CPU Core를 사용하고, 여기에 Flash, RAM, GPIO, UART, SPI 같은 주변장치를 붙여 실제 MCU 제품을 만든다.
즉 ARM Cortex-M 자체가 하나의 완성된 MCU 제품은 아니고, MCU 내부에서 명령어를 실행하는 CPU Core에 가깝다.
STM32와 Board
STM32는 STMicroelectronics가 만드는 MCU 제품군의 이름이다. 많은 STM32 MCU가 ARM의 Cortex-M CPU Core를 사용한다.
관계를 단순하게 보면 다음과 같다.
ARM
↓
Cortex-M CPU Core
↓
STM32 MCU
- 예를 들어 어떤 STM32 제품은 Cortex-M4를 사용하고, 다른 제품은 Cortex-M7을 사용할 수 있다.
- 같은 Cortex-M 계열을 사용하더라도 제품마다 Flash 크기, RAM 크기, 사용할 수 있는 GPIO나 통신 Peripheral 등이 달라진다.
- 따라서 Cortex-M4용 코드와 특정 STM32 칩용 설정은 완전히 같은 의미가 아니다.
여기에 Board라는 개념이 하나 더 있다. Board는 MCU와 전원 회로, 클록, 디버깅 인터페이스 등의 부품을 실제 기판 위에 구성한 것이다.
Board
├─ MCU
├─ Clock
├─ Power Circuit
├─ Debug Interface
└─ 기타 부품
즉 Cortex-M은 CPU Core, STM32는 MCU 제품군, Board는 MCU와 주변 회로를 실제로 구성한 기판이라고 구분할 수 있다.
Thumb
ARM을 공부하다 보면 thumb이라는 이름도 자주 등장한다.
Thumb은 ARM 계열 CPU에서 사용하는 명령어 집합 계열 중 하나다. 특히 Cortex-M 프로세서는 Thumb 계열 명령어를 사용하기 때문에 크로스 컴파일 환경에서도 다음과 같은 타깃 이름을 볼 수 있다.
thumbv7em-none-eabihf
- 여기서 thumbv7em은 ARMv7E-M 계열의 Thumb 명령어를 대상으로 한다는 의미다.
- 따라서 ARM과 Thumb을 서로 다른 CPU 종류라고 생각하기보다는, ARM 계열 CPU가 사용하는 명령어 집합과 관련된 이름이라고 이해하는 편이 좋다.
Bare-metal
작은 MCU에서는 Windows나 Linux 같은 운영체제 없이 Firmware가 하드웨어 위에서 바로 실행되는 경우가 많다. 이러한 환경을 bare-metal이라고 한다.
Firmware
↓
MCU Hardware
- bare-metal 환경에서는 중간에 운영체제가 없기 때문에 개발자가 직접 하드웨어 초기화, 메모리 배치, Interrupt, Peripheral 제어 등을 고려해야 한다.
- 모든 임베디드 환경에 운영체제가 없는 것은 아니다.
- 조금 더 복잡한 시스템에서는 RTOS를 사용하기도 하고, 더 큰 시스템에서는 Linux가 동작하기도 한다.
Peripheral과 MMIO
MCU 안에는 GPIO, UART, SPI, Timer 등 다양한 Peripheral이 존재한다. CPU가 이러한 하드웨어를 제어하는 대표적인 방식이 Memory-Mapped I/O, 즉 MMIO다.
- Peripheral의 제어 상태를 특정 메모리 주소에 연결해두고, CPU가 해당 주소를 읽거나 쓰면 실제 하드웨어의 상태가 변경되는 방식이다.
- 예를 들어 개념적으로 다음과 같은 주소가 있다고 해보자.CPU가 해당 주소에 값을 쓰면 UART 설정이 변경되고, 값을 읽으면 현재 UART의 상태를 확인할 수 있다.그래서 임베디드 소프트웨어에서는 일반적인 애플리케이션 개발보다 메모리 주소나 레지스터라는 개념을 훨씬 자주 접하게 된다.
- CPU ↓ Memory Address ↓ Peripheral Register ↓ Hardware
- 0x4000_1000 → UART Control Register 0x4000_1004 → UART Status Register
Interrupt
임베디드에서 자주 등장하는 또 하나의 개념이 Interrupt다.
CPU가 계속해서
버튼이 눌렸나?
UART 데이터가 들어왔나?
Timer가 끝났나?
를 확인하는 것은 비효율적이다.
대신 특정 하드웨어 이벤트가 발생했을 때 CPU에게 알려주는 방식이 있는데, 이것을 Interrupt라고 한다.
CPU가 다른 작업 수행
↓
UART 데이터 도착
↓
Interrupt 발생
↓
Interrupt Handler 실행
어떤 Interrupt가 발생했을 때 어느 코드를 실행할지 연결하는 정보가 Vector Table에 들어간다. 따라서 MCU Firmware의 메모리 구조를 보다 보면 Vector Table이라는 용어도 자주 등장한다.
Host와 Target
임베디드 개발에서는 프로그램을 만드는 컴퓨터와 실제 프로그램이 실행되는 컴퓨터가 다른 경우가 많다. 예를 들어 개발은 x86-64 Windows PC에서 하지만 실제 프로그램은 ARM Cortex-M에서 실행할 수 있다.
Host
Windows x86-64 PC
│
│ Compile
▼
Target
ARM Cortex-M MCU
여기서
- Host는 컴파일을 수행하는 환경
- Target은 만들어진 프로그램이 실제로 실행되는 환경
이다.
Cross Compile
Host와 Target이 다를 때 대상 시스템에 맞는 프로그램을 만드는 것을 Cross Compile이라고 한다. 예를 들어 x86 PC에서 ARM MCU용 Firmware를 빌드한다면 크로스 컴파일이다.
x86 PC
│
│ Cross Compile
▼
ARM Firmware
그래서 임베디드 개발에서는 다음과 같은 크로스 컴파일러를 흔하게 볼 수 있다.
arm-none-eabi-gcc
이 컴파일러는 ARM CPU에서 실행되는 GCC라는 의미가 아니다. x86 등의 개발 PC에서 실행되면서 ARM bare-metal용 코드를 만들어주는 GCC다.
Compiler와 Linker
프로그램 하나를 만든다고 해서 컴파일러가 소스 전체를 바로 하나의 Firmware로 만드는 것은 아니다. 대략적으로 다음과 같은 과정을 거친다.
Source Code
↓
Compiler
↓
Object Files
↓
Linker
↓
Executable / Firmware
- Compiler는 소스 코드를 대상 CPU의 기계어가 들어있는 Object File로 변환한다.
- Linker는 여러 Object File과 Library를 연결해 최종 실행 파일을 만든다.
임베디드에서는 링커가 특히 중요하다. MCU 내부에는 Flash와 RAM의 주소가 정해져 있기 때문에 코드와 데이터를 어느 위치에 배치할지 결정해야 하기 때문이다. 예를 들어 개념적으로는 다음과 같다.
Flash
├─ Vector Table
├─ Program Code (.text)
└─ Constants (.rodata)
RAM
├─ Initialized Data (.data)
├─ Zero Initialized Data (.bss)
├─ Heap
└─ Stack
이러한 메모리 배치를 링커에게 알려주기 위해 Linker Script를 사용한다.
Toolchain
Toolchain은 프로그램을 빌드하기 위해 필요한 여러 개발 도구의 묶음을 의미한다.
예를 들어 ARM용 GNU Toolchain에는 다음과 같은 도구들이 포함될 수 있다.
arm-none-eabi-gcc
arm-none-eabi-g++
arm-none-eabi-as
arm-none-eabi-ld
arm-none-eabi-objdump
...
각각 컴파일, 어셈블, 링크, 바이너리 분석 등의 역할을 담당한다. 따라서 임베디드에서 "어떤 툴체인을 사용한다"는 것은 단순히 컴파일러 하나를 선택한다는 의미보다 대상 시스템의 프로그램을 만들기 위한 전체 빌드 도구 집합을 선택한다는 의미에 가깝다.
HAL
Peripheral을 제어하기 위해 매번 레지스터 주소를 직접 읽고 쓰는 것은 상당히 불편하다. 그래서 실제 개발에서는 하드웨어를 조금 더 사용하기 쉬운 형태로 추상화한 계층을 사용한다. 대표적인 것이 HAL(Hardware Abstraction Layer)이다.
- HAL은 GPIO, UART, SPI 같은 MCU의 하드웨어 기능을 사용하기 쉬운 API 형태로 제공한다.
- 예를 들어 UART 레지스터를 직접 수정하는 대신와 같은 형태로 사용하는 식이다.
- uart.write(...) uart.read(...)
즉 HAL은 하드웨어의 세부 레지스터 구조를 직접 다루지 않고도 Peripheral을 사용할 수 있도록 중간에서 감싸주는 계층이라고 보면 된다.
이 개념은 이후 Rust의 embedded-hal을 이해할 때도 중요하다.
전체 관계 정리
지금까지 나온 개념을 하드웨어 기준으로 정리하면 다음과 같다.
Embedded Device
│
├─ Board
│ │
│ └─ MCU
│ ├─ ARM Cortex-M CPU Core
│ ├─ Flash
│ ├─ RAM
│ └─ Peripheral
│
└─ Firmware
개발 과정은 다음과 같다.
개발 PC (Host)
│
│ Cross Compile
▼
Toolchain
│
▼
Firmware
│
▼
MCU (Target)
소프트웨어에서 하드웨어를 바라보면 다음과 같은 구조가 된다.
Application
↓
Driver
↓
HAL
↓
Peripheral Register
↓
Hardware
처음에는 MCU와 CPU, ARM과 STM32가 모두 비슷한 의미처럼 보일 수 있다.
하지만 Cortex-M은 프로그램을 실행하는 CPU Core, STM32는 이를 포함한 실제 MCU 제품군, Board는 MCU와 주변 회로를 구성한 기판, Firmware는 그 하드웨어에서 실행되는 소프트웨어라고 구분하면 이후 임베디드 관련 용어가 훨씬 이해하기 쉬워진다.