임베디드와 하드웨어
전자레인지·키보드·자동차 속에서 도는 작은 컴퓨터, 마이크로컨트롤러가 레지스터·핀·타이머·버스로 바깥 세상과 주고받는 방법과, 실시간 스케줄링·저전력·디지털 논리·개발 도구까지 실제 수치를 계산한 그림으로 정리한다.
목차 (14)
MCU와 MPU·SoC
임베디드 시스템은 정해진 일 하나를 하려고 기계 안에 넣은 컴퓨터다. 그 중심이 마이크로컨트롤러(MCU)다. CPU 코어와 프로그램을 담는 플래시, 변수를 담는 SRAM, 그리고 타이머·ADC·통신 장치 같은 주변장치가 칩 하나에 다 들어 있다. 전원만 넣으면 바로 내 코드가 돈다. 클럭은 수십~수백 MHz, RAM은 KB 단위이고, 잠들면 마이크로암페어(μA) 단위 전류만 쓴다.
반대쪽 끝은 스마트폰이나 Raspberry Pi에 들어가는 SoC(애플리케이션 프로세서, MPU라고도 한다)다. GHz급 코어 여럿과 GPU, 가상 메모리를 위한 MMU가 있어 Linux·Android 같은 범용 OS를 올린다. 대신 GB 단위 DRAM과 eMMC·SD 같은 저장장치를 칩 밖에 붙여야 하고, 부팅에 몇 초가 걸리며, 전력도 훨씬 많이 쓴다. "μs 안에 반드시 반응", "전지 하나로 몇 년"이 필요하면 MCU, 화면·네트워크·파일이 많은 복잡한 소프트웨어가 필요하면 SoC다. CPU 자체의 구조는 CPU 노트에 있다.
메모리 맵과 레지스터
MCU에서 하드웨어를 다루는 방법은 놀랄 만큼 단순하다. 주변장치의 설정과 상태가 모두 레지스터이고, 레지스터는 정해진 주소에 있다. 그 주소에 값을 쓰면 핀이 켜지고, 읽으면 타이머 값이 나온다. 이를 메모리 맵 입출력이라 한다. Arm Cortex-M 계열은 32비트 주소 공간(4 GiB)을 용도별 구역으로 나눠 두었고, 칩 회사는 그 안에서 실제 주소를 정해 데이터시트의 "주소 맵" 표에 적는다.
레지스터 하나는 보통 32비트이고, 비트 하나하나가 서로 다른 기능을 맡는다(예: 출력 레지스터의 5번 비트 = 5번 핀). 그래서 한 비트만 바꾸려면 값을 읽어 그 비트만 고쳐 다시 쓴다(읽기-수정-쓰기). 켤 때는 OR, 끌 때는 AND NOT, 뒤집을 때는 XOR을 쓴다. 인터럽트가 그 사이에 같은 레지스터를 고치면 값이 엉키므로, 많은 칩이 "이 비트만 켜기/끄기" 전용 레지스터(STM32의 BSRR, RP2040의 SET·CLR 별칭 주소)를 따로 둔다. C 코드에서 레지스터 포인터에 volatile을 붙이는 이유는, 컴파일러가 "같은 주소를 두 번 읽을 필요 없다"며 읽기를 없애 버리지 못하게 하려는 것이다.
GPIO: 핀 하나로 켜고 읽기
GPIO(범용 입출력)는 소프트웨어로 쓰임을 정하는 핀이다. 출력으로 두면 1을 쓸 때 전원 전압(3.3 V 등), 0을 쓸 때 0 V를 낸다. LED는 전류를 제한하는 저항과 함께 단다. 핀이 낼 수 있는 전류는 보통 수~수십 mA라서, 모터·릴레이처럼 큰 부하는 트랜지스터나 드라이버 칩을 거쳐 켠다. 입력으로 두면 핀 전압이 문턱보다 높은지 낮은지를 1·0으로 읽는다.
아무것도 연결되지 않은 입력 핀은 전압이 떠서(floating) 잡음에 따라 1과 0을 오간다. 그래서 저항으로 평소 전압을 정해 둔다. 전원 쪽으로 끌어올리면 풀업, 접지 쪽으로 끌어내리면 풀다운이다. 대부분의 MCU에는 이 저항이 칩 안에 있어 레지스터로 켠다. 버튼은 누르는 순간 접점이 몇 ms 동안 튀어(bounce) 여러 번 눌린 것처럼 읽히므로, 일정 시간 같은 값이 유지될 때만 받아들이는 디바운스를 한다. 마이크로폰·가속도계 같은 센서 쪽 원리는 센싱 노트, 커넥터와 전기 규격은 포트 노트에 있다.
모듈끼리 이을 때 가장 흔한 사고는 전압 수준이다. Arduino Uno는 5 V로, Pico·ESP32 같은 요즘 칩은 3.3 V로 동작하고, 3.3 V 칩의 핀은 대개 5 V를 견디지 못한다. 5 V 출력을 3.3 V 입력에 바로 넣으면 핀이나 칩이 상할 수 있으니 데이터시트의 최대 입력 전압을 보고, 넘으면 저항 분압이나 레벨 시프터를 끼운다. 또 두 보드가 신호를 주고받으려면 GND를 서로 이어 기준(접지)을 공유해야 한다. 모터·릴레이 같은 코일 부하는 끌 때 역방향 전압이 튀므로 코일 양끝에 다이오드(플라이백 다이오드)를 단다.
ADC: 전압을 숫자로
온도 센서·마이크·가변 저항처럼 연속적인 전압을 내는 장치는 ADC(아날로그-디지털 변환기)로 읽는다. ADC는 0 V부터 기준 전압까지를 2N칸으로 나눠, 입력이 몇 번째 칸인지를 정수로 돌려준다. N이 분해능(비트 수)이고, 한 칸의 크기가 LSB다.
- 기준 전압 3.3 V, 10비트면 LSB = 3.3 / 1024 = 3.22 mV, 12비트면 3.3 / 4096 = 0.81 mV다.
- 12비트 ADC에 1.2 V가 들어오면 코드는 1489다. 거꾸로 코드 × LSB로 전압을 되돌린다.
- 양자화 때문에 오차가 최대 한 칸(반올림하는 ADC는 반 칸) 생긴다. 실제로는 잡음과 기준 전압의 오차 때문에 쓸 만한 비트(유효 비트 수, ENOB)가 표시된 비트보다 한두 개 적은 경우가 많다.
얼마나 자주 재는지(표본화 주파수)도 중요하다. 신호에 든 가장 높은 주파수의 두 배보다 빨리 재야 원래 모양을 되살릴 수 있다(나이퀴스트). 표본화와 앨리어싱 자체는 신호 처리 노트에서 다룬다. 반대 방향인 DAC는 숫자를 전압으로 바꾼다.
타이머와 PWM
타이머는 클럭에 맞춰 수를 세는 하드웨어 카운터다. 클럭을 분주기(prescaler)로 나눈 속도로 0부터 정해 둔 TOP까지 세고, TOP에 닿으면 0으로 돌아가며 인터럽트를 걸 수 있다. 그래서 넘침 주파수 = 클럭 / (분주비 × (TOP + 1))이다. 16 MHz 클럭을 64로 나누고 TOP을 249로 두면 1000 Hz, 곧 1 ms마다 한 번 넘친다. CPU가 다른 일을 하는 동안에도 정확한 박자를 지키고, 들어오는 펄스의 폭을 재거나(입력 캡처) 정해진 순간에 핀을 바꾸는 데 쓴다.
PWM(펄스 폭 변조)은 타이머로 만든다. 카운터가 비교값보다 작은 동안 핀을 켜고, 큰 동안 끈다. 켜진 시간의 비율이 듀티비다. 위 타이머에 비교값 64을 주면 듀티비 25.6%인 1000 Hz 신호가 된다. 디지털 핀은 켜짐·꺼짐밖에 못 내지만, 충분히 빠르게 깜빡이면 LED 밝기·모터 속도는 평균 전압(전원 × 듀티비)을 따라간다. 서보 모터는 반대로 펄스 폭 자체(보통 1~2 ms)를 각도 명령으로 읽는다.
통신 버스: UART · I²C · SPI · CAN
MCU는 센서·화면·메모리·다른 MCU와 몇 가닥 선으로 이야기한다. 핀 수, 속도, 연결할 장치 수, 거리·잡음에 따라 버스를 고른다.
| 버스 | 선 | 클럭 선 | 연결 | 속도 | 주 용도 |
|---|---|---|---|---|---|
| UART | TX·RX (+GND) | 없음(보율 약속) | 1:1 | 보통 9600 bit/s~수 Mbit/s | GPS·블루투스 모듈, 디버그 로그 |
| I²C | SDA·SCL | 있음 | 여럿이 공유, 7비트 주소 | 100 k · 400 k · 1 Mbit/s | 센서, 작은 화면, EEPROM |
| SPI | SCK·MOSI·MISO + 장치마다 CS | 있음 | 주 1 : 부 여럿 | 수~수십 MHz | 플래시 메모리, 화면, SD 카드 |
| CAN | CAN_H·CAN_L (차동) | 없음 | 다중 마스터 버스 | 최대 1 Mbit/s(CAN FD는 데이터 구간 더 빠름) | 자동차, 산업 장비 |
- UART: 클럭 선 없이 양쪽이 미리 같은 속도(보율)를 약속한다. 쉬는 동안 선은 높고, 0인 시작 비트로 "지금부터"를 알린 뒤 데이터를 LSB부터 보내고, 1인 정지 비트로 끝낸다. 가장 흔한 8N1(데이터 8, 패리티 없음, 정지 1)은 바이트 하나에 10비트라, 115,200 보율이면 1초에 11,520바이트다. PC의 USB 시리얼 포트도 이것이다.
- I²C: 데이터(SDA)·클럭(SCL) 두 선을 여러 장치가 함께 쓴다. 장치마다 7비트 주소가 있어서, 주 장치가 주소를 먼저 보내면 그 장치만 대답(ACK)한다. 선은 저항으로 끌어올려 두고 누구든 0으로 당기기만 해서(오픈 드레인), 두 장치가 동시에 말해도 합선되지 않는다.
- SPI: 클럭(SCK), 주→부(MOSI), 부→주(MISO) 세 선을 공유하고, 장치마다 고르는 선(CS)을 하나씩 둔다. 주소도 ACK도 없이 클럭마다 한 비트씩 양방향으로 동시에 주고받아 빠르다. 대신 장치가 늘 때마다 핀이 하나씩 든다.
- CAN: 꼬인 두 선의 전압 차로 신호를 보내(차동) 자동차 같은 잡음 많은 곳에서도 수십 m를 간다. 주 장치가 따로 없고, 여럿이 동시에 보내면 메시지 ID가 작은 쪽이 이겨 계속 보낸다(중재). 버스 양 끝에 120 Ω 종단 저항을 단다.
인터럽트와 폴링
버튼이 눌렸는지 알아내는 방법은 두 가지다. 메인 루프에서 핀을 계속 읽는 폴링은 단순하지만, 루프가 다른 일로 바쁘면 짧은 신호를 놓치고, 할 일이 없을 때도 CPU가 깨어 있어야 한다. 인터럽트는 핀 변화·타이머 넘침·수신 완료 같은 사건이 생기면 하드웨어가 하던 일을 멈추고 미리 등록한 함수(ISR, 인터럽트 서비스 루틴)를 부른다. Cortex-M은 인터럽트 컨트롤러(NVIC)가 우선순위를 관리하고, 레지스터 저장을 하드웨어가 해 주어 수십 클럭 안에 ISR로 들어간다.
ISR은 메인 코드 아무 곳에나 끼어들므로 짧아야 한다. 깃발을 세우거나 데이터를 버퍼에 넣는 정도만 하고, 무거운 처리는 메인 루프나 태스크에 넘긴다. ISR과 메인 코드가 함께 쓰는 변수는 volatile로 선언하고, 여러 바이트짜리 값은 읽는 동안 인터럽트를 잠깐 막아 반쯤 바뀐 값을 읽지 않게 한다. 같은 개념이 PC 운영체제에서 어떻게 쓰이는지는 운영체제 노트의 입출력 절에 있다.
RTOS와 실시간 스케줄링
일이 몇 가지뿐이면 "무한 루프 + 인터럽트"(superloop)로 충분하다. 통신·화면·센서·제어가 서로 다른 박자로 돌기 시작하면 RTOS(실시간 운영체제)를 쓴다. FreeRTOS·Zephyr·ThreadX 같은 RTOS는 일을 태스크로 나누고, 큐·세마포어·뮤텍스로 서로 주고받게 한다. 리눅스와 달리 가상 메모리·파일 시스템이 기본으로는 없고, 커널이 수 KB에 불과하다.
"실시간"은 빠르다는 뜻이 아니라 마감을 지킨다는 뜻이다. 그래서 RTOS 스케줄러는 대개 고정 우선순위 선점이다. 준비된 태스크 가운데 우선순위가 가장 높은 것이 늘 돈다. 공평하게 나누는 PC의 스케줄러(운영체제 노트의 CFS·EEVDF)와 달리, 낮은 태스크가 굶더라도 높은 태스크의 반응 시간이 예측 가능해야 한다. 주기가 짧은 태스크에 높은 우선순위를 주는 규칙(rate-monotonic)이 고전적인 배치 방법이다.
여기에 고전적인 함정이 있다. 우선순위 역전은 높은 태스크 H가 낮은 태스크 L이 쥔 뮤텍스를 기다리는 동안, 뮤텍스와 상관없는 중간 태스크 M이 L을 밀어내 H가 M보다도 늦어지는 현상이다. 1997년 화성 탐사선 Mars Pathfinder가 이 문제로 거듭 재부팅됐고, 지구에서 그 뮤텍스의 우선순위 상속을 켜는 설정 변경으로 고쳤다. 우선순위 상속은 뮤텍스를 쥔 태스크가 기다리는 태스크 가운데 가장 높은 우선순위를 잠시 물려받게 해, 임계 구역을 빨리 끝내고 놓게 한다. FreeRTOS의 뮤텍스는 이를 기본으로 한다.
L(낮음)이 0초에 시작해 1초부터 뮤텍스를 쥔다. H(높음)는 2초, M(중간)은 3초에 도착한다. 색칠된 칸은 뮤텍스를 쥐고 하는 일이다.
전력 관리: 잠들어 있는 것이 일
전지로 도는 장치의 수명은 깨어 있을 때가 아니라 잠들어 있을 때 정해진다. MCU는 여러 단계의 슬립 모드를 둔다. 가벼운 슬립은 CPU 클럭만 멈추고 주변장치와 RAM은 살려 두어 금방 깨고, 깊은 슬립은 대부분의 전원을 끄고 RTC 타이머나 핀 하나만 깨어 있어 μA 이하까지 내려가지만 깨어날 때 다시 초기화해야 한다. ESP32라면 Light-sleep은 CPU·RAM 상태를 보존하고, Deep-sleep은 CPU와 대부분의 RAM을 끈다.
평균 전류 = (깨어 있는 전류 × 깨어 있는 시간 + 잠든 전류 × 잠든 시간) / 주기이고, 수명 ≈ 전지 용량 / 평균 전류다. 아래 예에서 잠든 전류가 10 μA가 아니라 100 μA(잊고 끄지 않은 LED 하나, 풀업 저항으로 새는 전류)라면 수명이 약 344일에서 79일로 준다. 그래서 저전력 설계는 쓰지 않는 주변장치의 클럭 끄기, 핀 상태 정리, 깨어 있는 시간 줄이기(한꺼번에 모아 보내기)의 싸움이다. 실제 수명은 전지의 자가 방전과 온도, 전압이 떨어지는 끝부분 때문에 계산보다 짧다.
전력과 함께 보는 안전장치가 둘 있다. 워치독 타이머는 프로그램이 정해진 시간 안에 "살아 있다"고 신호를 주지 않으면 칩을 다시 시작시켜, 펌웨어가 멈춰도 기기가 스스로 살아나게 한다. 브라운아웃 감지는 전지가 닳아 전압이 기준 밑으로 내려가면 칩을 리셋 상태로 붙잡아, 불안정한 전압에서 플래시를 잘못 쓰는 일을 막는다.
디지털 논리: 게이트, 플립플롭, 클럭
MCU 안의 모든 것은 결국 0과 1을 다루는 논리 게이트로 만든다. AND·OR·NOT·XOR 같은 게이트는 트랜지스터 몇 개로 만들고, NAND 하나만 있으면 다른 모든 게이트를 조합할 수 있다. 입력이 바뀌면 출력이 (아주 짧은 지연 뒤) 곧바로 따라 바뀌는 회로를 조합 논리라 한다. 덧셈기·비교기·멀티플렉서가 그 예이고, 반가산기를 여러 개 이어 붙인 것이 CPU 산술 장치의 바탕이다.
기억하려면 순차 논리가 필요하다. 가장 흔한 부품인 D 플립플롭은 클럭이 0에서 1로 오르는 순간(상승 에지)의 입력 D를 붙잡아 다음 에지까지 출력 Q로 내보낸다. 플립플롭 32개를 나란히 두면 32비트 레지스터, 덧셈기와 이으면 카운터(타이머), 조건에 따라 다음 상태를 고르면 상태 기계가 된다. 모든 플립플롭이 같은 클럭에 맞춰 움직이기 때문에, 게이트 지연이 제각각이어도 다음 에지 전에만 값이 자리 잡으면 회로 전체가 질서 있게 돈다. 이 "다음 에지 전까지"가 최대 클럭 주파수를 정한다.
FPGA와 MCU
MCU는 이미 만들어진 CPU가 명령을 하나씩 실행한다. FPGA는 CPU가 아니라, 작은 진리표(LUT)와 플립플롭 수천~수백만 개를 배선으로 마음대로 이을 수 있는 칩이다. Verilog·VHDL 같은 하드웨어 기술 언어로 회로를 적으면, 도구가 그것을 LUT 배치와 배선으로 바꾼 비트스트림을 만든다. 칩에 올리면 그 회로가 실제로 생기고, 모든 부분이 같은 클럭에 동시에 돈다.
그래서 FPGA는 지연이 클럭 단위로 정확히 정해져야 하거나(모터 제어, 고속 통신 규격), 같은 계산을 대량으로 병렬 처리해야 할 때(영상 처리, 신호 처리, 칩을 만들기 전 시제품) 쓴다. 대신 같은 일을 MCU로 할 수 있다면 MCU가 훨씬 싸고, 전력이 적고, 개발이 쉽다. 아예 회로를 실리콘에 굳힌 것이 ASIC이다. RP2040의 PIO처럼, 작은 상태 기계를 MCU 안에 넣어 둘 사이를 메우는 칩도 있다.
개발 흐름: 크로스 컴파일, 굽기, 디버깅
MCU용 프로그램(펌웨어)은 PC에서 만들어 칩에 넣는다. PC와 다른 CPU용 기계어를 만드는 크로스 컴파일러(Arm Cortex-M이면 arm-none-eabi-gcc나 Clang)를 쓰고, 링커 스크립트가 코드는 플래시 주소에, 변수는 SRAM 주소에 놓도록 정한다. OS가 없으니 main 전에 스택 포인터를 정하고 변수를 초기화하는 시작 코드와, 리셋·인터럽트 때 뛸 주소를 담은 벡터 표도 이미지에 함께 들어간다. 컴파일러와 링커 자체는 컴파일러 노트에서 다룬다.
만든 이미지를 플래시에 넣는 것을 굽기(flashing)라 한다. 가장 강력한 길은 디버그 프로브다. Arm 칩은 선 두 가닥의 SWD나 네 가닥의 JTAG로 칩 내부의 디버그 장치에 닿아, 플래시를 쓰고, CPU를 멈추고, 한 줄씩 실행하고, 레지스터와 메모리를 들여다볼 수 있다. PC에서는 OpenOCD 같은 프로그램이 프로브를 GDB와 이어 준다. 프로브가 없어도 Pico는 BOOTSEL 버튼을 누른 채 연결해 나타나는 USB 드라이브에 .uf2 파일을 끌어 놓고, ESP32는 USB 시리얼로 esptool을 써서, Arduino는 미리 들어 있는 시리얼 부트로더로 굽는다.
많이 쓰는 보드
처음 시작하기 좋은 보드는 대개 아래 중 하나다. Arduino는 라이브러리와 예제가 가장 많고, Pico는 값싸고 데이터시트가 친절하며 MicroPython으로 바로 시작할 수 있고, ESP32는 Wi-Fi·Bluetooth가 칩에 들어 있어 IoT 장치에 많이 쓴다. 이 밖에 산업 현장에서 가장 흔한 것은 ST의 STM32 계열이고, Nordic nRF52(블루투스 저전력)도 많이 쓴다.
| 보드 | 칩 | 코어 | 클럭 | SRAM | 플래시 | 무선 |
|---|---|---|---|---|---|---|
| Arduino Uno R3 | ATmega328P | AVR 8비트 | 16 MHz | 2 KB | 32 KB(내장) | 없음 |
| Arduino Uno R4 Minima | Renesas RA4M1 | Arm Cortex-M4 | 48 MHz | 32 KB | 256 KB(내장) | 없음(WiFi 판은 ESP32-S3 추가) |
| Raspberry Pi Pico | RP2040 | Cortex-M0+ ×2 | 133 MHz | 264 KB | 2 MB(외장 QSPI) | 없음(Pico W는 Wi-Fi) |
| Raspberry Pi Pico 2 | RP2350 | Cortex-M33 ×2 또는 RISC-V Hazard3 ×2 | 150 MHz | 520 KB | 4 MB(외장 QSPI) | 없음(Pico 2 W는 Wi-Fi) |
| ESP32 개발 보드 | ESP32 | Xtensa LX6 ×2 | 240 MHz | 520 KB | 보통 4 MB(모듈 안 외장) | Wi-Fi · Bluetooth |
핵심 자료
칩 하나의 데이터시트를 처음부터 끝까지 읽어 보는 것이 가장 빠른 공부다. RP2040 데이터시트가 특히 읽기 좋다.
| 자료 | 종류 | 요지 | 링크 |
|---|---|---|---|
| RP2040 Datasheet | 데이터시트 | Raspberry Pi Pico의 칩. 주소 맵, GPIO·타이머·PWM·ADC·UART·SPI·I²C 레지스터, 독특한 PIO까지 한 문서에 다 있어 MCU 구조를 공부하기 좋다. | Raspberry Pi |
| FreeRTOS — Mutexes | 공식 문서 | 뮤텍스와 이진 세마포어의 차이, 우선순위 상속이 어떻게 역전을 줄이는지. | freertos.org |
| What really happened on Mars? (Mars Pathfinder) | 사례 | 화성 탐사선이 우선순위 역전으로 거듭 재부팅됐고, 지구에서 뮤텍스의 우선순위 상속을 켜 고친 이야기. | Cornell |
| Understanding the I2C Bus | 응용 노트 | 오픈 드레인과 풀업, 시작·정지 조건, ACK, 주소 지정을 파형으로 설명한다. | Texas Instruments |
| Introduction to the Controller Area Network (CAN) | 응용 노트 | 차동 신호, 우성·열성 비트, ID로 하는 중재와 프레임 구조. | Texas Instruments |
| From Nand to Tetris | 무료 강의 | NAND 게이트 하나에서 시작해 가산기·메모리·CPU·컴파일러·OS까지 직접 쌓아 올린다. | nand2tetris.org |
- RP2040 Datasheet — Raspberry Pi
- FreeRTOS — Mutexes — freertos.org
- What really happened on Mars? (Mars Pathfinder) — Cornell
- Understanding the I2C Bus — Texas Instruments
- Introduction to the Controller Area Network (CAN) — Texas Instruments
- From Nand to Tetris — nand2tetris.org
- Raspberry Pi Pico Datasheet — Raspberry Pi
- Raspberry Pi Pico 2 Datasheet — Raspberry Pi
- RP2350 Datasheet — Raspberry Pi
- Arduino UNO R3 — Arduino 문서
- Arduino UNO R4 Minima — Arduino 문서
- ESP32 — Espressif
- ESP32 Sleep Modes — ESP-IDF 문서
- Basics of PWM — Arduino 문서
- Serial Communication — SparkFun
- I2C — SparkFun
- Serial Peripheral Interface (SPI) — SparkFun
- Scheduling — Zephyr 문서
- OpenOCD