Cortex-M
고친 사람 github-actions[bot]
Cortex-M 은 작은 기기 속 칩에서 프로그램을 돌려 주는 프로세서 설계입니다. Arm 이 만든 설계도를 칩 회사들이 사서 자기 칩 안에 넣습니다. 온도 센서, 무선 이어폰, 세탁기 제어판 같은 곳에 이렇게 만든 칩이 들어갑니다. 그래서 칩 회사가 달라도 같은 도구와 같은 방식으로 프로그램을 짭니다.
쉽고 빠른 이해
Cortex-M 은 손톱만 한 칩 안에서 계산을 맡는 두뇌 부분입니다. 무선 이어폰 속 칩이 버튼 입력을 읽고 소리를 켜는 일을 이 두뇌가 합니다.
칩 회사마다 두뇌를 따로 설계하면 회사를 바꿀 때마다 프로그램과 도구를 새로 익혀야 합니다. 두뇌를 하나로 맞춰 두면 칩을 바꿔도 짜던 방식을 그대로 가져갑니다.
돌아가는 순서는 이렇습니다.
- 전원이 들어오면 칩 맨 앞에 적힌 두 값을 읽습니다. 처음 쓸 메모리 위치와 첫 명령의 위치입니다
- 버튼이나 타이머가 신호를 보내면 하던 일을 잠깐 멈추고 그 신호를 처리합니다
- 처리가 끝나면 멈췄던 곳으로 돌아가 하던 일을 잇습니다
얻는 만큼 내려놓은 것이 있습니다. 메모리 주소를 바꿔 주는 장치가 없어서 리눅스 같은 큰 운영체제는 못 올립니다. 그런 일은 같은 회사의 더 큰 프로세서가 맡습니다.
상세
Cortex-M 이 무엇이고 무엇을 내려놓았는지는 칩 한 개의 구성에서 출발하면 잘 보입니다. 그다음 부팅, 인터럽트 처리, 타이머 설정을 차례로 따라가며 이 프로세서가 작은 기기에 맞는 까닭을 짚습니다.
설계도를 파는 회사
Arm 은 칩을 직접 만들어 팔지 않습니다. 프로세서의 설계도를 만듭니다. 칩 회사는 그 설계도를 쓸 권리를 삽니다. Cortex-M 은 그 설계도 가운데 작은 기기용 묶음의 이름입니다.
칩 회사는 이 설계도를 받아 자기 칩의 한가운데에 넣습니다. STMicroelectronics 의 STM32, Nordic 의 nRF52, 라즈베리 파이 재단의 RP2040 이 모두 안쪽에 Cortex-M 을 품은 칩입니다. 겉의 상표는 달라도 계산을 맡는 부분은 같은 설계에서 나왔습니다.
마이크로컨트롤러 안의 두뇌
Cortex-M 이 들어가는 칩을 마이크로컨트롤러라고 부릅니다. 줄여서 MCU(Microcontroller Unit, 마이크로컨트롤러 장치)라고 씁니다. MCU 는 컴퓨터 한 대에 필요한 부품을 칩 하나에 몰아넣은 물건입니다. 노트북이라면 따로 꽂을 메모리와 저장 장치가 이 칩 안에 같이 들어 있습니다.
그 가운데 계산을 맡는 부분이 CPU(Central Processing Unit, 중앙 처리 장치)입니다. Cortex-M 은 이 CPU 부분의 설계입니다. 나머지는 칩 회사가 직접 채웁니다. 프로그램을 담는 플래시 메모리, 실행 중 값을 담는 SRAM(Static Random Access Memory, 정적 램), 바깥과 신호를 주고받는 주변장치가 그 나머지입니다.
flowchart TD
subgraph MCU["마이크로컨트롤러 칩 한 개"]
subgraph CORE["Cortex-M · Arm 설계"]
C["CPU 코어"]
N["인터럽트 정리 장치"]
S["기본 타이머"]
end
F["플래시 메모리 · 프로그램"]
R["SRAM · 실행 중 값"]
P["주변장치 · 통신 · 핀 · 타이머"]
end
CORE --> F
CORE --> R
CORE --> P
그림의 바깥 상자가 칩 한 개입니다. 안쪽 상자가 Arm 이 설계한 부분입니다. 나머지 셋은 칩 회사가 붙인 부분입니다.
안쪽 상자의 두 장치는 인터럽트를 다룹니다. 인터럽트는 바깥 장치가 「지금 봐 달라」고 보내는 신호입니다. 인터럽트 정리 장치는 이 신호가 여럿 몰릴 때 무엇부터 처리할지 정합니다. 기본 타이머는 정해 둔 간격마다 이 신호를 스스로 만듭니다.
두 장치가 코어 안쪽에 있으니 칩 회사가 달라도 똑같이 생겼습니다. 이 둘을 다루는 코드는 칩을 바꿔도 거의 고치지 않습니다.
세 갈래 가운데 가장 작은 것
Arm 의 Cortex 계열은 세 갈래입니다. 이름 끝 글자가 쓰임새를 가리킵니다.
| 갈래 | 끝 글자의 뜻 | 주로 들어가는 곳 |
|---|---|---|
| Cortex-A | Application, 응용 | 스마트폰 · 리눅스를 돌리는 보드 |
| Cortex-R | Real-time, 실시간 | 저장 장치 컨트롤러 · 자동차 제어기 |
| Cortex-M | Microcontroller, 마이크로컨트롤러 | 센서 · 가전 · 소형 기기 |
Cortex-A 는 큰 운영체제를 올리려고 만든 쪽입니다. Cortex-M 은 반대로 전기를 적게 먹고 칩 면적이 작은 쪽을 골랐습니다. 그 선택 때문에 몇 가지 기능을 내려놓았습니다.
내려놓은 것
첫째로 MMU(Memory Management Unit, 메모리 관리 장치)가 없습니다. MMU 는 프로그램이 쓰는 주소를 실제 메모리 주소로 바꿔 주는 장치입니다. 이 장치가 있어야 프로그램마다 따로 떨어진 주소 공간을 주는 가상 메모리가 됩니다.
가상 메모리가 없으면 리눅스 같은 범용 운영체제를 올리기 어렵습니다. 그래서 Cortex-M 에서는 운영체제 없이 프로그램 하나만 돌리곤 합니다. 이 방식이 베어메탈입니다.
아니면 작은 실시간 운영체제를 올립니다. 실시간 운영체제는 신호에 정해진 시간 안에 반응하도록 만든 운영체제입니다. 여러 작업을 번갈아 돌리되 급한 일이 늦지 않게 합니다. 이런 운영체제로는 Zephyr 와 FreeRTOS 가 흔히 쓰입니다.
둘째로 명령어 집합을 하나만 받습니다. 명령어 집합은 CPU 가 알아듣는 명령의 목록과 규칙입니다. 프로그램은 결국 이 목록의 명령을 늘어놓은 것입니다.
Arm 프로세서는 전통적으로 두 가지 명령어 집합을 받았습니다. 하나는 명령 하나가 늘 32비트인 쪽입니다. 다른 하나는 Thumb 이라 부르는 짧은 명령 쪽입니다. Cortex-M 은 Thumb 만 받습니다. Thumb 은 16비트와 32비트 명령을 섞어 쓰므로 같은 프로그램이 플래시를 덜 차지합니다. 작은 칩에서는 플래시가 클수록 칩 가격이 오르니 이 선택이 맞습니다.
내려놓은 대신 얻은 것은 예측할 수 있는 반응입니다. 구조가 단순합니다. 인터럽트 처리의 앞뒤 일은 하드웨어가 맡습니다. 처리 전에 하던 계산의 값을 챙겨 두는 일과 끝난 뒤 되돌려 놓는 일입니다. 덕분에 신호가 들어와서 처리가 시작되기까지 걸리는 시간이 매번 거의 같습니다. 모터를 제어하거나 센서 값을 일정한 간격으로 읽는 기기는 이 일정함이 빠름보다 중요합니다.
모든 것이 주소다
Cortex-M 은 32비트 주소 하나로 메모리와 장치를 전부 가리킵니다. 주소 공간은 4GB 입니다. 어느 구간에 무엇을 둘지 큰 틀이 정해져 있습니다.
block-beta columns 1 a["0x00000000 부터 · 코드 · 플래시가 보통 여기"] b["0x20000000 부터 · SRAM · 실행 중 값"] c["0x40000000 부터 · 주변장치"] d["0xE0000000 부터 · 코어 자신의 설정 · 인터럽트 정리 장치 · 기본 타이머"]
0xE0000000 부터 시작하는 마지막 구간은 Cortex-M 코어 자신의 몫입니다. 칩 회사가 누구든 인터럽트 정리 장치와 기본 타이머의 설정 주소가 이 구간 안에서 같습니다. 그 앞의 구간들은 세부 배치가 칩마다 다릅니다. 어느 주소에 어느 주변장치가 있는지는 그 칩의 설명서가 정합니다.
주변장치도 메모리처럼 읽고 씁니다. 특정 주소에 값을 쓰면 핀에 전기가 흐릅니다. 다른 주소를 읽으면 버튼 상태가 나옵니다. 이런 방식이 메모리 맵 입출력입니다. 장치를 다루는 별도 명령이 필요 없으니 C 언어의 포인터 하나로 장치를 움직일 수 있습니다.
전원이 들어온 직후
Cortex-M 은 켜지자마자 주소 0 근처에 놓인 표를 읽습니다. 이 표가 벡터 테이블입니다. 무슨 일이 생겼을 때 어디로 뛰어갈지를 적어 둔 주소 목록입니다.
표의 한 칸은 4바이트입니다. 칸마다 할 일이 정해져 있습니다. 표에 적히는 「무슨 일」을 묶어 예외라고 합니다. 바깥 장치가 보내는 인터럽트에 코어 안에서 생기는 신호까지 더한 말입니다.
그림에는 이름 붙은 예외가 둘 나옵니다. NMI(Non-Maskable Interrupt, 막을 수 없는 인터럽트)는 꺼 둘 수 없는 비상 신호입니다. HardFault 는 잘못된 주소를 읽는 것처럼 프로그램이 크게 어긋났을 때 들어오는 신호입니다.
block-beta columns 1 a["0번 칸 · 처음 쓸 스택 주소"] b["1번 칸 · 리셋 처리 함수의 주소"] c["2번 칸 · NMI 처리 함수의 주소"] d["3번 칸 · HardFault 처리 함수의 주소"] e["… 15번 칸까지 · 코어 자신의 예외"] f["16번 칸부터 · 칩 회사가 붙인 주변장치의 인터럽트"]
전원이 들어오면 코어는 먼저 0번 칸 값을 레지스터 하나에 넣습니다. 레지스터는 CPU 안의 아주 작은 저장 칸입니다. 계산 중인 값을 메모리보다 훨씬 빨리 꺼내 쓰려고 둡니다.
0번 칸 값이 들어가는 레지스터는 스택 포인터입니다. 스택은 함수가 지역 변수를 쌓아 두는 메모리 구간입니다. 스택 포인터는 그 구간의 꼭대기를 가리킵니다. 그다음 코어는 1번 칸에 적힌 주소로 뛰어가 거기서부터 명령을 실행합니다. 이 함수가 리셋 핸들러입니다.
이 두 단계를 코어가 하드웨어로 합니다. 스택이 이미 준비돼 있으니 함수 호출이 바로 됩니다. 덕분에 첫 함수부터 C 언어로 짤 수 있습니다.
인터럽트와 우선순위
앞에서 인터럽트를 바깥 장치가 「지금 봐 달라」고 보내는 신호라고 했습니다. 버튼이 눌리거나 통신 데이터가 도착하면 주변장치가 이 신호를 보냅니다. CPU 는 하던 일을 멈춥니다. 그다음 벡터 테이블에서 그 신호에 맞는 함수를 찾아 실행합니다.
신호가 여럿 몰리면 무엇부터 처리할지 정해야 합니다. 이 일을 NVIC(Nested Vectored Interrupt Controller, 중첩 벡터 인터럽트 컨트롤러)가 맡습니다. NVIC 는 코어에 붙은 인터럽트 컨트롤러입니다. 맨 앞 그림의 「인터럽트 정리 장치」가 NVIC 입니다.
NVIC 는 신호마다 우선순위를 둡니다. 낮은 순위의 신호를 처리하는 도중에 높은 순위가 들어오면 그쪽을 먼저 처리합니다. 다 끝나면 하던 처리로 돌아옵니다. 이름의 「중첩」이 이것입니다.
Cortex-M 의 인터럽트 처리에는 손이 덜 갑니다. 신호가 들어오면 코어가 지금 쓰던 레지스터 여덟 개를 스택에 스스로 저장합니다. 처리 함수가 끝나면 그 값을 스스로 되돌립니다.
그러니 인터럽트 처리 함수도 평범한 C 함수로 씁니다. 어셈블리는 기계어 명령을 사람이 읽을 수 있는 글자로 적은 언어입니다. 다른 프로세서에서는 이 저장과 복원을 어셈블리로 직접 짜야 하는 경우가 많습니다.
sequenceDiagram
participant 장치 as 주변장치
participant NVIC
participant 코어
participant 스택
장치->>NVIC: 신호를 보낸다
NVIC->>코어: 지금 처리 중인 일보다 높으면 넘긴다
코어->>스택: 레지스터 여덟 개를 저장한다
코어->>코어: 벡터 테이블의 처리 함수를 실행한다
코어->>스택: 저장한 값을 되돌린다
Note over 코어: 멈췄던 명령부터 다시 잇는다
그림에서 코어가 하는 화살표 셋 중 처리 함수를 실행하는 것만 프로그래머가 짠 코드입니다. 저장과 복원은 하드웨어가 맡습니다.
SysTick
SysTick 은 Cortex-M 코어 안에 든 작은 타이머입니다. 맨 앞 그림의 「기본 타이머」가 이것입니다. Cortex-M0 같은 가장 작은 코어에서는 칩 회사가 빼고 만들 수도 있습니다.
SysTick 은 정해 둔 수에서 0 까지 한 칸씩 거꾸로 셉니다. 0 이 되면 예외를 하나 보냅니다. 그다음 처음 수부터 다시 셉니다.
이 타이머가 코어 안에 있다는 점이 쓸모입니다. 실시간 운영체제는 이 타이머로 「다른 작업으로 바꿀 때가 됐나」를 일정한 간격으로 확인합니다. 칩 회사마다 타이머가 달라도 SysTick 은 같습니다. 운영체제가 칩마다 타이머 코드를 새로 짤 필요가 없습니다.
클록은 칩 안의 회로가 박자를 맞추는 신호입니다. SysTick 은 그 박자 한 번에 한 칸씩 셉니다.
아래는 메모리 맵 입출력으로 SysTick 을 켜는 C 코드입니다. 여기 나오는 레지스터는 CPU 안의 칸이 아니라 장치의 설정 칸입니다. 메모리 주소로 읽고 쓰는 칸이라 포인터로 다룹니다. 두 레지스터의 주소는 Cortex-M 이라면 어느 칩이든 같습니다.
SYST_RVR 에는 셀 수를 넣습니다. SYST_CSR 에 쓰는 0x7 은 아래 세 비트를 한꺼번에 켭니다. 타이머 켜기, 0 이 되면 예외 보내기, 코어 클록으로 세기입니다.
#define SYST_CSR (*(volatile uint32_t *)0xE000E010)
#define SYST_RVR (*(volatile uint32_t *)0xE000E014)
SYST_RVR = 15999; // 0 까지 16000번
SYST_CSR = 0x7; // 1ms 마다 SysTick
코어 클록이 16MHz 인 칩이라고 둡니다. 1초에 1600만 번 세는 셈이니 16000번은 1밀리초입니다. 15999 에서 0 까지가 16000번이라 1을 뺀 수를 넣습니다.
volatile 은 컴파일러에게 「이 주소는 값이 저절로 바뀔 수 있으니 읽고 쓰기를 줄이지 마라」고 알리는 낱말입니다. 장치 주소를 다룰 때는 이 낱말이 빠지면 쓰기가 사라질 수 있습니다.
이런 주소와 비트를 매번 손으로 적지는 않습니다. Arm 은 CMSIS(Common Microcontroller Software Interface Standard, 공통 마이크로컨트롤러 소프트웨어 인터페이스 표준)라는 헤더 묶음을 내놓습니다. CMSIS 를 쓰면 위 두 줄을 SysTick_Config() 함수 한 번으로 대신합니다.
두 가지 실행 상태와 두 개의 스택
Cortex-M 의 코어는 두 상태를 오갑니다. 평소 프로그램이 도는 상태는 스레드 모드입니다. 인터럽트나 예외를 처리하는 동안은 핸들러 모드입니다. 상태를 나눠 두면 처리 함수가 도는 동안과 평소 코드가 도는 동안에 서로 다른 규칙을 걸 수 있습니다. 쓰는 스택을 따로 두는 것이 대표적인 예입니다.
스택 포인터도 두 개입니다. 하나는 핸들러 모드와 운영체제가 씁니다. 다른 하나는 스레드 모드의 작업이 씁니다.
실시간 운영체제는 작업마다 스택을 따로 둡니다. 작업을 바꿀 때는 작업용 스택 포인터만 갈아 끼웁니다. 이렇게 작업을 바꾸는 일이 문맥 교환입니다.
stateDiagram-v2
스레드모드: 스레드 모드 · 작업용 스택
핸들러모드: 핸들러 모드 · 운영체제용 스택
[*] --> 스레드모드
스레드모드 --> 핸들러모드: 인터럽트나 예외가 들어온다
핸들러모드 --> 스레드모드: 처리가 끝난다
문맥 교환에는 미뤄 둘 수 있는 예외인 PendSV 를 씁니다. PendSV 는 우선순위를 가장 낮게 둡니다. 그래야 급한 인터럽트를 다 처리한 뒤에 작업이 바뀝니다. 흔한 구성에서는 운영체제가 SysTick 으로 시간을 잽니다. 바꿀 때가 되면 PendSV 를 걸어 둡니다.
메모리 보호
MMU 는 없지만 일부 Cortex-M 은 MPU(Memory Protection Unit, 메모리 보호 장치)를 갖춥니다. MPU 는 주소를 바꿔 주지 않습니다. 대신 「이 구간은 읽기만」·「이 구간은 실행 금지」 같은 규칙을 겁니다. 작업 하나가 남의 메모리를 덮어쓰면 MPU 가 그 접근을 막고 예외를 보냅니다. 주소 공간을 나누지는 못해도 사고가 번지는 것은 막습니다.
더 새로운 세대에는 TrustZone 이 붙습니다. 칩 안을 안전한 쪽과 일반 쪽 두 세계로 갈라, 암호 키 같은 것을 일반 쪽 코드가 못 읽게 합니다.
같은 이름 아래의 여러 코어
Cortex-M 은 코어 하나가 아니라 여러 코어의 묶음 이름입니다. 뒤에 붙는 숫자가 코어를 가릅니다. 각 코어는 Arm 이 정한 아키텍처 판 하나를 따릅니다. 아키텍처 판은 「이 코어가 받아야 하는 명령과 동작」을 정한 문서입니다. 코어는 그 문서를 회로로 옮긴 설계입니다.
| 코어 | 따르는 아키텍처 | 눈에 띄는 차이 |
|---|---|---|
| Cortex-M0 · Cortex-M0+ | Armv6-M | 가장 작고 전기를 가장 적게 먹는다 |
| Cortex-M3 | Armv7-M | 나눗셈 명령 등 명령이 늘었다 |
| Cortex-M4 · Cortex-M7 | Armv7E-M | 신호 처리 명령이 붙고 소수점 계산 장치를 고를 수 있다 |
| Cortex-M23 | Armv8-M Baseline | 작은 코어에 TrustZone 이 붙었다 |
| Cortex-M33 | Armv8-M Mainline | 큰 코어에 TrustZone 이 붙었다 |
| Cortex-M55 · Cortex-M85 | Armv8.1-M | 여러 값을 한 번에 계산하는 Helium 명령이 붙었다 |
표의 「신호 처리 명령」은 소리나 센서 값처럼 연속된 숫자를 빠르게 계산하는 명령입니다. 이런 일을 DSP(Digital Signal Processing, 디지털 신호 처리)라 부릅니다. 「소수점 계산 장치」는 FPU(Floating-Point Unit, 부동소수점 장치)입니다. FPU 가 없는 코어에서는 소수점 계산을 정수 명령 여러 개로 흉내 내므로 느립니다.
Armv6-M 같은 앞 세대 아키텍처용으로 컴파일한 프로그램은 Armv7-M 같은 뒤 세대 코어에서도 대개 돕니다. 반대는 안 됩니다. Cortex-M4 에만 있는 명령을 쓴 프로그램은 Cortex-M0 에서 그 명령을 만나는 순간 멈춥니다.
언제 고르고 언제 피하나
배터리로 오래 버텨야 하거나, 신호에 일정한 시간 안에 반응해야 하거나, 칩 값이 싸야 하는 기기라면 Cortex-M 을 고릅니다. 센서 노드, 가전 제어판, 웨어러블이 그렇습니다. 리눅스를 올려 여러 프로그램을 돌려야 하거나 화면과 네트워크를 넉넉히 다뤄야 하면 Cortex-A 쪽으로 갑니다. Cortex-M 에는 가상 메모리가 없어 그런 운영체제가 못 올라갑니다.
같은 쓰임새를 두고 겨루는 다른 설계도 있습니다. 명령어 집합을 공개해 사용료가 없는 RISC-V(리스크 파이브) 기반 마이크로컨트롤러가 늘고 있습니다. RISC-V 의 앞 토막 RISC(Reduced Instruction Set Computer, 축소 명령어 집합 컴퓨터)는 명령 하나가 맡는 일을 작게 자르는 설계 방향입니다.
더 작은 기기에는 AVR(Microchip 이 만드는 8비트 마이크로컨트롤러 계열) 같은 오래된 계열도 남아 있습니다.
관련 항목
이것이 속하는 상위 분류
Arm · 마이크로컨트롤러 · 임베디드 · CPU · 코어 · 명령어 집합
이것을 이루는 구성 요소
NVIC · SysTick · 벡터 테이블 · 스택 포인터 · 레지스터 · MPU · FPU · TrustZone · Helium
이것이 따르는 아키텍처와 명령어
Thumb · Thumb-2 · Armv6-M · Armv7-M · Armv7E-M · Armv8-M · Armv8.1-M · DSP · SIMD
이것이 다루는 신호와 예외
인터럽트 · 인터럽트 컨트롤러 · 하드웨어 예외 · NMI · HardFault · PendSV · 리셋 핸들러 · 인터럽트 우선순위
이것 위에서 도는 소프트웨어
베어메탈 · 실시간 운영체제 · Zephyr · FreeRTOS · CMSIS · 펌웨어 · 부트로더 · 문맥 교환 · 메모리 맵 입출력
이것을 품은 칩
STM32 · nRF52 · RP2040 · ESP32
이것과 같은 회사의 형제 프로세서
Cortex-A · Cortex-R · Arm Neoverse · ARM7TDMI
이것과 겨루는 다른 설계
이것이 내려놓은 기능
이것과 맞닿은 하드웨어
플래시 메모리 · SRAM · 주변장치 · 클록 · GPIO · UART · SPI · I2C · DMA · 워치독
다른 이름: Arm Cortex-M · 코텍스-M · Cortex-M 프로세서