jacobhan.me

Embedded Firmware · 임베디드 펌웨어

삼상 인버터를 움직이는 펌웨어 — ARM Cortex-M MCU 8대 핵심 모듈

전동 킥보드를 밀어 보면 손바닥에 묵직한 가속이 전해진다. 그 힘을 만드는 모터는 사실 1초에 수천 번씩 켜졌다 꺼지는 전자 스위치들의 합주이고, 그 합주를 지휘하는 것은 손톱만 한 칩 하나다. 이 칩이 마이크로컨트롤러(MCU, Microcontroller Unit)다.

이 글은 32비트 ARM Cortex-M 계열 MCU로 삼상 인버터를 제어하기 위해 반드시 거쳐야 하는 펌웨어의 8가지 핵심 모듈을 한 편으로 정리한 것이다. 전공자가 아니어도 따라올 수 있도록 비유와 도해를 곁들였다.

대상: 임베디드·전력전자에 처음 발을 들이는 독자 · 예시 칩: ARM Cortex-M7 기반 32비트 MCU(최대 216 MHz, 3.3 V)

1MCU의 해부도 — 코어·버스·주변장치

하나의 칩 안에 무엇이 들어 있는가

MCU는 단순한 연산기가 아니라 작은 컴퓨터 한 대를 통째로 칩에 욱여넣은 부품이다. 그 안에는 명령을 처리하는 CPU 코어, 외부 세계와 신호를 주고받는 주변장치(peripheral), 그리고 둘을 잇는 통로인 버스(bus)가 함께 들어 있다.

여기서 혼동하기 쉬운 점이 있다. ARM은 코어 설계도만 만드는 회사다. 칩 제조사들은 이 ARM 코어를 사 와서 그 둘레에 GPIO·타이머·ADC·통신 모듈 같은 주변장치를 붙여 완성된 MCU로 판다. 그래서 제조사가 달라도 코어가 같으면(예: Cortex-M7) 코어 부분의 동작은 동일하고, 주변장치 구성만 제각각이다.

MCU 내부 구조하나의 칩 안에 코어 + 버스 + 주변장치가 함께 들어 있다CPU 코어ARM Cortex-M7AHB 버스 (최대 216 MHz · 고속)GPIO 포트핀 입출력메모리/SRAM데이터 저장브릿지 → APB2최대 108 MHz브릿지 → APB1최대 54 MHz고속 주변장치ADC · TIM1/8 · USART일반 주변장치TIM2~ · UART · I2C데이터는 모두 클럭에 동기되어 버스를 통해 오간다 → 클럭이 빠를수록 처리 속도가 빠르다
칩 한 개 안에 코어·버스·주변장치가 함께 들어 있다. 코어와 주변장치는 버스로 연결되어 데이터를 주고받는다.

ARM 코어는 용도에 따라 세 갈래로 나뉜다. Cortex-A는 스마트폰·태블릿처럼 고성능이 필요한 곳에, Cortex-R은 의료·항공·자동차처럼 실시간성이 중요한 곳에, Cortex-M은 저전력 임베디드 제어에 쓰인다. 모터·인버터 제어에 흔히 쓰는 것이 바로 이 M 계열이며, 그중 상급인 M7은 최대 216 MHz로 동작하면서도 전력 소비가 적다.

비유 — MCU는 작은 사무실

코어는 책상에 앉아 일하는 직원, 주변장치들은 회계·영업·총무 같은 각 부서, 버스는 부서를 잇는 복도다. 그리고 벽시계의 똑딱임이 클럭이다. 모두가 같은 시계의 박자에 맞춰 움직이기에, 시계가 빠를수록 사무실 전체가 더 빨리 돌아간다.

다만 모든 부서가 같은 속도로 일하지는 않는다. 버스에는 위계가 있다. 코어에 가장 가까운 고속 버스(AHB, Advanced High-performance Bus)는 216 MHz까지 달리고, 여기서 브릿지(bridge)를 거쳐 저속 버스(APB, Advanced Peripheral Bus)인 APB2(최대 108 MHz)와 APB1(최대 54 MHz)로 갈라진다. 어떤 주변장치가 어느 버스에 매달려 있는지에 따라 동작 클럭이 달라지므로, 모듈을 설정할 때 이 연결 관계를 알고 있어야 한다.

2레지스터로 말하는 법 — 메모리 맵

MCU를 제어한다는 것의 진짜 의미

MCU에게 "이 핀을 켜라", "이 속도로 통신하라"고 지시하는 일은 결국 특정 주소에 0과 1을 쓰는 것으로 귀결된다. 그 주소에 놓인 작은 기억 장소를 레지스터(register)라 부른다. 칩 안의 모든 자원 — 메모리, GPIO, 타이머, ADC — 은 저마다 고유한 주소를 부여받고 한 장의 지도 위에 배치되는데, 이 지도가 메모리 맵(memory map)이다.

메모리 맵주소 공간 — 모든 자원은 고유 주소를 가진다코어 내부 (NVIC 등)0xE000_0000외부 메모리(FMC)0xA000_0000주변장치 (GPIO·TIM·ADC…)0x4000_0000SRAM (변수·스택)0x2000_0000플래시 (프로그램)0x0800_0000부트 영역0x0000_0000높은 주소낮은 주소
주소 공간의 큰 그림. 0x4000_0000 대역이 주변장치 구역이며, 여기에 GPIO·타이머·ADC의 레지스터들이 놓인다.

레지스터는 역할에 따라 셋으로 나누어 생각하면 편하다. 통신 속도·동작 모드·인터럽트 사용 여부처럼 무엇을 어떻게 할지 정하는 컨트롤 레지스터, 변환이 끝났는지·인터럽트가 발생했는지처럼 상태를 읽는 스테이터스 레지스터, 그리고 변환된 값이나 받은 데이터가 담기는 데이터 레지스터다.

대부분의 32비트 MCU에서 레지스터는 32비트 폭이며, 각 비트마다 제조사가 의미를 부여해 두었다. 따라서 원하는 기능만 켜려면 다른 비트는 건드리지 않고 특정 비트만 조작하는 비트 연산이 필수다.

// 비트 켜기 — OR 연산 (다른 비트는 건드리지 않음)
REG |= (1 << 3);          // 3번 비트만 1로

// 비트 끄기 — AND + NOT
REG &= ~(1 << 3);         // 3번 비트만 0으로

// 2비트 묶음 설정 — 먼저 지우고(클리어) 다시 쓰기
REG &= ~(0b11 << 6);      // 7,6번 비트 클리어
REG |=  (0b01 << 6);      // 출력 모드(01)로 세팅
비유 — 레지스터는 거대한 스위치 보드

32비트 레지스터는 32개의 토글 스위치가 한 줄로 늘어선 제어판이다. 각 스위치는 "출력으로 쓸까", "풀업을 켤까" 같은 개별 기능을 담당한다. 전체 보드를 한꺼번에 갈아엎으면 멀쩡히 켜둔 다른 스위치까지 꺼지니, 원하는 스위치 하나만 골라 올리고 내리는 것이 비트 연산이다.

주소를 매번 직접 적는 방식은 정확하지만 지저분하고 실수하기 쉽다. 그래서 실무에서는 같은 구역의 레지스터들을 구조체(struct)로 묶고, 더 나아가 제조사가 제공하는 표준 헤더(CMSIS, Cortex Microcontroller Software Interface Standard)에 정의된 이름을 가져다 쓴다. 추상화 단계가 올라갈수록 코드가 읽기 쉬워진다.

// ① 주소를 직접 다루기 (가장 날것)
#define GPIOD_MODER  (*(volatile unsigned int *)(0x40020C00))
GPIOD_MODER |= (1 << 6);

// ② 구조체로 묶기 (레지스터가 4바이트 간격으로 나열됨)
typedef struct {
    volatile unsigned int MODER;   // +0x00
    volatile unsigned int OTYPER;  // +0x04
    volatile unsigned int OSPEEDR; // +0x08
    // ...
} GPIO_t;
#define GPIOD ((GPIO_t *)0x40020C00)
GPIOD->MODER |= (1 << 6);

요점은 어느 방식을 쓰든 본질은 같다는 것이다. 주소를 찾아가 비트를 세운다. 이 한 문장이 임베디드 제어의 알파이자 오메가다.

3GPIO — 디지털 입출력의 기본

불을 켜는 일에서 시작한다

임베디드 학습의 첫걸음은 거의 예외 없이 LED 켜기다. 프로그래밍의 "Hello, World"처럼, 핀 하나를 켜고 끄며 칩과 처음 악수를 나누는 의식이다. 이때 쓰이는 것이 GPIO(General-Purpose Input/Output, 범용 입출력) 핀이다.

출력으로 설정한 핀은 내부적으로 푸시풀(push-pull) 구조를 갖는다. 위쪽 스위치가 켜지면 핀이 전원 전압(3.3 V)으로, 아래쪽 스위치가 켜지면 0 V로 끌린다. 출력 데이터 레지스터(ODR, Output Data Register)에 1을 쓰면 하이, 0을 쓰면 로우가 나간다.

GPIO 출력 드라이버푸시풀 출력과 보호 다이오드VDD 3.3VGND 0V상단 SW(P-MOS)하단 SW(N-MOS)출력 데이터레지스터(ODR)과전압 보호음전압 보호위/아래 스위치 중 하나만 켜져 핀을 3.3V 또는 0V로 만든다
GPIO 출력 드라이버. 위/아래 스위치 중 하나만 켜져 핀을 3.3V 또는 0V로 만들고, 보호 다이오드가 과전압·음전압으로부터 핀을 지킨다.

여기서 직관과 어긋나는 대목이 하나 있다. LED의 음극(캐소드)이 핀에 연결된 회로에서는, 핀을 0V로 만들어야 전위차가 생겨 불이 켜진다. 핀을 3.3 V로 두면 전원과 같아져 전류가 흐르지 못해 꺼진다.

// 1) 포트 클럭 켜기 (주변장치는 클럭부터)
RCC_AHB1ENR |= (1 << 3);        // GPIOD 클럭 enable

// 2) 핀을 출력 모드로
GPIOD_MODER  &= ~(0b11 << 6);
GPIOD_MODER  |=  (0b01 << 6);   // PD3 = 출력

// 3) 핀을 0V로 → LED 점등 (캐소드가 핀에 연결된 회로)
GPIOD_ODR &= ~(1 << 3);

반대로 핀을 입력으로 쓰면 외부 신호를 읽어들인다. 이때 핀은 풀업/풀다운 저항으로 평상시 전압을 고정하고, 슈미트 트리거(Schmitt trigger)로 노이즈에 둔감해지며, 보호 다이오드로 과전압에서 살아남는다. 입력 핀의 최대 허용 전압은 보통 3.3 V이지만, 일부 핀은 5 V까지 견디도록 설계(데이터시트에 FT, Five-volt Tolerant로 표기)되어 있어 5 V 센서를 직접 받을 수 있다.

비유 — 풀업저항은 자동으로 닫히는 문

스위치가 눌리지 않은 평소에 핀이 어떤 값일지 정해 두지 않으면, 핀은 바람에 흔들리는 문처럼 0과 1 사이를 떠돈다(플로팅). 풀업저항은 평소 문을 닫아두는 스프링이다. 누군가 손잡이를 당겨(스위치를 눌러) 0V로 끌어내리기 전까지는 안정적으로 3.3 V를 유지한다.

한 가지 더. 출력 핀의 전환 속도(슬루 레이트)도 조절할 수 있는데, 빠를수록 좋은 것은 아니다. 전환이 빠르다는 것은 곧 고주파 성분이 많다는 뜻이고, 이는 EMI(Electromagnetic Interference, 전자기 간섭)를 키운다. 고장 신호처럼 급한 것은 빠르게, 그렇지 않은 것은 적당한 속도로 — 요구 사항에 딱 맞게 설정하는 절제가 좋은 설계다.

4클럭 — MCU의 심장 박동

모든 동작은 박자 위에서 일어난다

MCU 내부의 거의 모든 일은 제멋대로가 아니라 클럭(clock)의 박자에 맞춰 일어난다. 클럭 신호의 상승/하강 모서리(엣지)마다 명령어가 한 단계씩 처리되고 데이터가 한 칸씩 이동한다. 따라서 클럭 주파수가 높을수록 더 빠르게 일한다.

클럭의 원천은 네 가지다. 외부 크리스탈로 만드는 HSE(High-Speed External), 칩 내부 RC 발진기로 만드는 HSI(High-Speed Internal, 16 MHz 고정), 그리고 저속용인 LSE·LSI(Low-Speed External/Internal, 실시간 시계나 워치독용)이다. HSI는 외부 부품이 필요 없어 저렴하지만 정밀도가 떨어지고, HSE는 부품값이 들지만 정확하다.

문제는 16 MHz로는 모터 제어에 한참 모자란다는 것이다. 그래서 PLL(Phase-Locked Loop, 위상 고정 루프)이라는 주파수 체배 회로로 입력 클럭을 끌어올린다.

클럭 트리클럭이 만들어져 코어와 주변장치로 분배되는 경로HSE (외부)크리스탈 16 MHzHSI (내부)RC 16 MHzPLL주파수 체배 회로SYSCLK216 MHzAHB 프리스케일러÷1HCLK / 코어216 MHzAPB2 프리스케일러÷4 → 54 MHzAPB1 프리스케일러÷4 → 54 MHz16 MHz ÷8 = 2 MHz → ×216 = 432 MHz → ÷2 = 216 MHz (오버드라이브로 216 MHz 도달)
클럭이 만들어져 코어와 주변장치로 분배되는 경로. SYSCLK가 AHB·APB 프리스케일러를 거쳐 각 버스로 나뉜다.

PLL 내부의 발진기(VCO, Voltage-Controlled Oscillator)는 입력 주파수가 1~2 MHz일 때 가장 안정적이며, 지터(jitter)를 줄이려면 2 MHz가 권장된다. 그래서 16 MHz를 먼저 8로 나눠 2 MHz로 만든 뒤, 216을 곱해 432 MHz(VCO 출력은 192~432 MHz 범위)를 얻고, 다시 2로 나눠 최종 216 MHz를 만든다.

비유 — PLL은 자전거 기어, 프리스케일러는 그 반대

천천히 밟는 페달(저속 입력)을 기어비로 바꿔 바퀴를 빠르게 돌리는 것이 PLL이다. 반대로 프리스케일러(prescaler)는 빠른 클럭을 적당히 느리게 나눠 주는 변속기다. 굳이 모든 부서가 최고 속도로 달릴 필요는 없다 — 빠르면 전력만 더 먹기 때문이다.

클럭 설정에는 정해진 순서가 있다. 전원이 켜진 직후 MCU는 일단 HSI(16 MHz)로 깨어나고, 그 상태에서 외부 클럭과 PLL을 준비한 다음, 안정화가 확인되면 비로소 시스템 클럭을 PLL 출력으로 갈아탄다. 게다가 216 MHz는 그냥 도달되지 않는다. 기본 한계인 180 MHz를 넘으려면 내부 전압 레귤레이터의 출력을 한 단 높이는 오버드라이브(overdrive)를 켜야 한다.

한 가지 더 — 플래시는 코어보다 느리다

216 MHz에서 한 사이클은 약 4.6 ns다. 그런데 프로그램이 저장된 플래시 메모리는 한 번 읽는 데 약 35 ns가 걸린다. 그래서 코어가 플래시를 기다려 주도록 웨이트 스테이트(wait state)를 7로 설정하고, 자주 쓰는 명령·데이터는 빠른 캐시(cache)에 담아 속도 차이를 메운다.

// (개념 흐름) 16MHz HSE → PLL → 216MHz
// 1) HSE/HSI 켜고 안정화 대기
// 2) 안정화 전까지는 HSI(16MHz)를 시스템 클럭으로 임시 사용
// 3) PLL 분주비 설정:  16MHz ÷8 = 2MHz  →  ×216 = 432MHz  →  ÷2 = 216MHz
// 4) 플래시 wait state = 7, 캐시 enable, 오버드라이브 on
// 5) PLL을 시스템 클럭으로 전환, APB1/APB2 프리스케일러로 54MHz 분배

5인터럽트 — 사건이 생기면 달려간다

기다리지 말고, 불릴 때 응답하라

외부 사건을 처리하는 방식에는 두 가지가 있다. 하나는 폴링(polling) — 사건이 올 때까지 다른 일은 제쳐 두고 계속 들여다보는 것. 다른 하나는 인터럽트(interrupt) — 평소엔 자기 일을 하다가, 사건이 발생한 그 순간에만 하던 일을 잠시 멈추고 처리하는 것이다.

폴링 vs 인터럽트이벤트를 기다리는 두 가지 방식폴링문 앞에서 계속 대기 (다른 일 못 함)이벤트 도착인터럽트평소엔 다른 일을 수행ISR 처리이벤트 도착인터럽트는 평소 자원을 낭비하지 않고, 사건이 생긴 순간에만 처리한다
폴링은 사건을 기다리느라 다른 일을 못 한다. 인터럽트는 평소 다른 일을 하다가 사건이 온 순간에만 처리(ISR)한다.
비유 — 치킨 배달을 기다리는 두 사람

폴링은 치킨이 올 때까지 현관문 앞에 붙어 서서 다른 아무 일도 못 하는 사람이다. 인터럽트는 거실에서 할 일을 하다가, 초인종이 울리면 그제야 문으로 가는 사람이다. 사건이 자주·즉각 온다면 폴링이 단순해서 낫고, 그렇지 않다면 인터럽트가 자원을 아낀다.

인터럽트를 총괄하는 것은 NVIC(Nested Vectored Interrupt Controller, 중첩 벡터 인터럽트 컨트롤러)로, Cortex-M 코어가 공통으로 갖춘 부품이다. 인터럽트가 발생하면 미리 정해진 벡터 테이블에서 해당 사건의 처리 함수, 즉 ISR(Interrupt Service Routine)의 주소를 찾아 그곳으로 점프한다. 여러 인터럽트가 동시에 몰리면 정해 둔 우선순위에 따라 차례를 정한다.

GPIO 핀으로 들어오는 외부 신호는 EXTI(External Interrupt/Event Controller)가 받는다. EXTI는 신호의 상승/하강 엣지를 검출해 NVIC로 넘기고, NVIC가 사용자 함수를 불러낸다.

외부 인터럽트 경로핀 입력 → EXTI → NVIC → 사용자 함수입력 핀엣지 발생EXTI엣지 검출·마스크NVIC우선순위 정렬ISR 함수사용자 코드여러 인터럽트가 겹치면 NVIC가 정해 둔 우선순위에 따라 차례로 처리한다
외부 인터럽트의 전달 경로. 핀의 엣지를 EXTI가 검출하고, NVIC가 우선순위를 매겨 사용자 ISR을 호출한다.
// EXTI4 인터럽트 서비스 루틴 (사용자가 내용을 채움)
void EXTI4_IRQHandler(void) {
    if (EXTI_PR & (1 << 4)) {   // 정말 4번 라인에서 왔는가?
        EXTI_PR = (1 << 4);     // 1을 써서 플래그 클리어
        GPIOD_ODR ^= (1 << 3);  // LED 토글
        // ※ 여기에 긴 delay를 넣지 말 것!
    }
}
금기 — ISR 안의 긴 지연

인터럽트 처리 함수 안에서 1초씩 멈춰 버리면, 그 사이 더 급한 다른 인터럽트가 와도 처리하지 못해 시스템 전체가 꼬인다. ISR은 짧고 빠르게 끝내고, 무거운 작업은 본문으로 넘기는 것이 원칙이다.

이 모든 것이 모터 제어에서 결정적이다. 회전자의 위치를 알려주는 홀센서(Hall sensor) 신호를 인터럽트로 받아, 그 순간 삼상 인버터의 전류 위상을 정확히 전환하는 것이 핵심 동작이기 때문이다.

6ADC — 아날로그를 숫자로

전압·전류·온도를 디지털 세계로 옮기는 다리

전압, 전류, 온도, 빛 같은 물리량은 끊김 없이 이어지는 아날로그(연속) 신호다. 그러나 디지털 칩은 숫자만 이해한다. 이 둘을 잇는 다리가 ADC(Analog-to-Digital Converter, 아날로그-디지털 변환기)다. 실제로 대부분의 센서는 물리량을 먼저 전압으로 바꾸고, 그 전압을 ADC로 읽는다.

주의할 한 가지는 입력 전압이 기준 전압(여기서는 3.3 V)을 넘으면 안 된다는 점이다. 예컨대 전기차의 800 V 배터리 전압을 그대로 물리면 칩이 즉시 망가진다. 그래서 저항 분압으로 800 V를 3.3 V 이내로 낮춰 입력한다.

변환은 세 단계를 거친다. 일정 간격으로 표본을 뜨는 샘플링, 그 표본을 가장 가까운 단계로 반올림하는 양자화, 그 단계에 숫자를 매기는 부호화다.

ADC 3단계샘플링·양자화·부호화로 연속 신호를 디지털로연속 신호 → ① 샘플링 → ② 양자화 → ③ 부호화07디지털 코드일정 간격으로 표본 추출(샘플링) → 가장 가까운 레벨로 반올림(양자화)
ADC 3단계. 가로축은 시간(샘플링), 세로축은 디지털 코드(양자화·부호화). 연속 곡선이 점과 계단으로 옮겨진다.

세로축을 얼마나 잘게 쪼개느냐가 분해능(resolution)이다. 12비트 ADC는 0~4095(2¹²−1)의 4,096단계로 나눈다. 기준 3.3 V를 4,096으로 나누면 한 단계가 약 0.81 mV다. 비트가 클수록 더 촘촘해진다 — 8비트면 한 단계가 약 12.9 mV, 16비트면 약 0.05 mV다.

// ADC 결과를 전압으로 환산 (12비트, 기준 3.3V)
//   ADCout = (2^n - 1) × Vin / Vref
//   예) Vin=1V, Vref=3.3V, n=12  →  4095 × 1/3.3 ≈ 1240
float volt = (float)adc_raw * 3.3f / 4095.0f;

변환에는 오차가 따른다. 연속을 계단으로 근사하며 생기는 양자화 오차(비트가 높을수록 작아짐), 일정한 상수만큼 위/아래로 치우치는 오프셋 오차(상수를 더하거나 빼서 보정), 기울기가 달라지는 이득 오차(특정 값을 곱해 보정)다. 뒤의 둘은 소프트웨어로 비교적 쉽게 잡을 수 있다.

우리가 쓰는 칩의 ADC는 SAR(Successive Approximation Register, 축차 비교) 방식이다. 입력 전압을 커패시터에 잠시 가둔 뒤(샘플&홀드), 내부 DAC 값과 비교를 거듭하며 가장 가까운 디지털 값을 찾아낸다.

SAR ADC 구조표본을 잡아두고(샘플·홀드) 비교로 디지털 값을 찾는다VinS/H 스위치커패시터비교기SAR 로직DAC코드0~409512비트 → 0~4095 단계. 변환 시간 = 샘플링 시간 + 12클럭
SAR ADC의 골격. 표본을 커패시터에 가두고, 비교기와 DAC를 통해 자릿수를 좁혀 가며 코드를 확정한다.

샘플링 시간은 짧으면 빠르지만 정밀도가 떨어질 수 있어, 실제로는 실험으로 적절한 값을 찾는다 — 속도와 정밀도의 맞교환이다. 인버터에서는 이 ADC로 상전류·DC링크 전압·소자 온도를 읽어 제어와 보호의 근거로 삼는다.

7타이머/카운터 — 시간과 PWM의 공장

정확한 주기를 만들고, 모터를 구동한다

같은 모듈이지만 부르는 이름이 둘이다. 내부 클럭으로 일정 주기를 재면 타이머(timer), 외부 신호의 개수를 세면 카운터(counter)다. 내부 클럭을 쓰느냐 아니냐가 갈림길이다.

동작 원리는 단순하다. 클럭 박자마다 숫자를 하나씩 올리다가, ARR(Auto-Reload Register, 자동 재적재 레지스터)에 적어 둔 최댓값에 닿으면 0으로 떨어지며 오버플로우(overflow) 사건을 일으킨다. 이 사건마다 인터럽트를 발생시키면 정확한 주기의 시계가 된다. 한편 CCR(Capture/Compare Register, 캡처/비교 레지스터)은 카운터 값과 비교되어, 일치하는 순간 출력 신호를 바꾼다 — 이것이 PWM(Pulse-Width Modulation, 펄스 폭 변조)의 원리다.

타이머 업카운트와 PWM 생성톱니파 카운터와 CCR 비교로 PWM 출력카운터가 0→ARR을 반복하며, CCR과 비교해 PWM을 만든다ARR=4CCR=30시간 (클럭에 동기)PWM1/0
톱니파처럼 0→ARR을 반복하는 카운터가 CCR과 비교되어 PWM 출력을 만든다. CCR을 키우면 켜져 있는 시간(듀티)이 늘어난다.

듀티(duty), 즉 한 주기 중 켜져 있는 비율을 바꾸려면 CCR 값만 조절하면 된다. 카운터는 0→ARR만 반복하는 업카운트, 0↔ARR을 오르내리는 업/다운(센터 얼라인) 모드로 동작한다. 16비트 타이머라면 0~65,535까지 셀 수 있어 분해능이 매우 높다. 주기는 클럭 주파수 ÷ (프리스케일러 × ARR)로 정해진다.

비유 — PWM은 조광 스위치

형광등은 켜짐과 꺼짐뿐이지만, 아주 빠르게 껐다 켜기를 반복하면 사람 눈에는 밝기가 중간으로 보인다. 켜진 시간의 비율(듀티)을 바꾸는 것이 곧 밝기 조절, 모터라면 속도 조절이다.

고급 타이머(TIM1·TIM8)는 모터 제어에 특화되어 있다. 한 채널과 그 반전 채널을 자동으로 상보(complementary) 출력하기 때문이다. 인버터의 한 상은 위·아래 두 스위치로 이루어지는데, 둘이 동시에 켜지면 전원이 단락되는 암 쇼트(arm short)가 일어나 큰 사고가 난다. 그래서 둘은 번갈아 켜져야 하고, 전환 순간에는 둘 다 잠시 꺼두는 데드타임(dead-time)이 반드시 필요하다.

상보 출력과 데드타임위/아래 스위치 신호와 둘 사이의 데드타임두 스위치가 동시에 켜지지 않도록 '데드타임'을 둔다상단 SW하단 SW데드타임데드타임
상보 출력과 데드타임. 한쪽이 꺼지고 다른 쪽이 켜지기 전, 둘 다 꺼져 있는 짧은 공백을 둬 단락을 막는다.
비유 — 데드타임은 교대 근무의 인수인계 공백

야간 근무자가 자리를 비우기 전에 주간 근무자가 먼저 앉아 버리면 한순간 둘이 겹쳐 충돌한다. 그래서 한 사람이 완전히 떠난 뒤에야 다음 사람이 앉도록 짧은 빈 시간을 둔다. 실제 스위치는 켜지고 꺼지는 데 시간이 걸리므로, 이 공백이 없으면 위·아래가 순간적으로 함께 켜져 단락된다.

8UART — 가장 단순한 대화

선 두 가닥으로 주고받는 약속

통신은 데이터를 한 비트씩 줄지어 보내는 직렬(serial)과 여러 비트를 한꺼번에 보내는 병렬(parallel)로 나뉜다. 병렬은 빠르지만 선이 많고 거리가 길어지면 간섭에 약하다. 임베디드에서는 배선이 단순하고 장거리에 강한 직렬이 압도적으로 많이 쓰인다.

또 다른 분류 축도 있다. 동시에 보내고 받을 수 있으면 전이중(full-duplex), 한 번에 한 방향만 되면 반이중(half-duplex, 무전기처럼)이다. 그리고 별도의 클럭 선에 맞춰 주고받으면 동기(synchronous), 클럭 없이 양쪽이 속도만 약속하고 주고받으면 비동기(asynchronous)다.

UART(Universal Asynchronous Receiver/Transmitter, 범용 비동기 송수신기)는 이름 그대로 비동기 직렬 통신이다. 클럭 선이 없는 대신, 한 프레임을 시작 비트 → 데이터 비트 → 정지 비트라는 약속(프로토콜)으로 감싼다.

UART 프레임시작/데이터/정지 비트로 한 글자를 전송문자 'A'(0x41)를 UART로 보내는 한 프레임IdleStartD01D10D20D30D40D50D61D70StopIdle10↓ 라인이 0으로 내려가며 시작
문자 'A'(0x41)를 보내는 한 프레임. 라인이 0으로 떨어지며 시작하고, 데이터 비트를 LSB부터 보낸 뒤 1로 올라가며 끝난다.

송신부(TX)와 수신부(RX)는 데이터 폭(보통 8비트)과 보레이트(baud rate, 초당 비트 수)를 똑같이 맞춰야 한다. 보레이트는 사용하는 버스 클럭을 원하는 속도로 나눠 레지스터에 적는 것으로 간단히 설정된다. 또 한 가지 — 배선할 때 TX는 상대의 RX에, RX는 상대의 TX에 엇갈려 연결해야 통신이 된다. 같은 이름끼리 잇는 실수가 가장 흔하다.

비유 — 보레이트는 같은 말 속도의 약속

지휘자가 없는(클럭이 없는) 두 사람이 또박또박 대화하려면, 미리 "1초에 몇 글자 속도로 말하자"고 약속해야 알아듣는다. 약속한 속도가 어긋나면 같은 문장도 뒤죽박죽이 된다. UART의 보레이트가 바로 그 약속이다.

UART는 가장 단순한 만큼 가장 널리 쓰인다. MCU 내부 변수를 PC로 찍어 보며 디버깅하거나, 블루투스 모듈과 연결해 전동 킥보드의 속도·온도·전류를 스마트폰으로 받아보는 텔레메트리(원격 계측)에 흔히 동원된다.


9하나의 제어 루프로 수렴하다

여덟 조각이 만나 모터를 돌린다

지금까지의 여덟 모듈은 따로 노는 지식이 아니라, 하나의 제어 루프를 이루는 톱니바퀴들이다. 삼상 인버터로 BLDC·PMSM 모터를 돌리는 과정에 그대로 포개진다.

삼상 인버터 제어 루프8개 모듈이 하나의 제어 루프로 수렴한다MCU 코어+ 클럭타이머/PWM상보+데드타임게이트 드라이버삼상 인버터6개 스위치BLDC/PMSM모터센서전류·전압·온도ADC아날로그→디지털홀센서회전자 위치GPIO + 인터럽트위상 전환 트리거UART텔레메트리📱 모니터링제어 루프: 위치 측정 → 위상 결정 → PWM 출력 → 전류/전압 되먹임 → 보정
여덟 모듈이 하나의 제어 루프로 수렴한다. 위치를 측정해 위상을 정하고, PWM으로 스위치를 구동하며, 전류·전압을 되먹임받아 끊임없이 보정한다.

순서를 따라가 보면 이렇다. 클럭으로 칩 전체를 깨우고, 레지스터로 각 주변장치를 설정한다. 홀센서 신호가 GPIO 인터럽트로 들어와 회전자의 현재 위치를 알리면, MCU는 다음 전류 위상을 정한다. 그 위상을 타이머/PWM이 여섯 개 스위치의 상보 신호로 — 데드타임을 둬 단락을 막으며 — 인버터에 내보낸다. 모터가 돌면 그 결과인 상전류·DC전압·온도를 ADC가 되읽어 제어를 보정하고, 그 모든 상태를 UART로 바깥에 중계한다.

요컨대 작은 칩 하나가 1초에 수천 번씩 "지금 어디인가 → 어떻게 켤 것인가 → 정말 그렇게 되었는가"를 반복하며 모터를 빚어낸다. 펌웨어의 여덟 모듈은 그 질문을 던지고 답하기 위한 최소한의 어휘다. 이 어휘를 손에 익히는 순간, 비로소 칩과 같은 언어로 대화할 수 있게 된다.