새로운 칩이 도입됐다, 레지스터맵은 완전히 다르다

새로운 칩이 도입됐다, 레지스터맵은 완전히 다르다

새로운 칩이 도입됐다

월요일 아침

출근했다. 팀장이 부른다.

“이번 프로젝트, ESP32로 간다.”

STM32 쓰던 프로젝트였다. 5개월 작업했다. 이제 버린다.

“비용 문제래. ESP32가 싸.”

알겠다고 했다. 속으로는 욕했다.

책상으로 돌아왔다. ESP32 데이터시트를 받았다. PDF 648페이지다.

STM32 레퍼런스 매뉴얼은 1200페이지였다. 이게 짧아 보인다. 익숙해진 거다.

첫 페이지를 열었다. 익숙한 단어들이다. GPIO, SPI, I2C, UART. 같은 거 아닌가?

아니었다.

레지스터맵의 배신

STM32는 이랬다.

GPIOA->MODER = 0x28000000;
GPIOA->ODR |= (1 << 5);

간단했다. 모드 설정하고 비트 켜면 끝.

ESP32는 달랐다.

gpio_config_t io_conf = {};
io_conf.pin_bit_mask = (1ULL << GPIO_NUM_5);
io_conf.mode = GPIO_MODE_OUTPUT;
gpio_config(&io_conf);

구조체다. 함수다. HAL도 아니고 ESP-IDF다.

“왜 이렇게 만들었지?”

물어봐도 답 없다. 그냥 이렇게 쓰라는 거다.

레지스터 직접 접근도 가능하다. 하지만 권장하지 않는다.

GPIO.out_w1ts = (1 << 5); // 이렇게 쓸 수 있긴 하다

근데 ESP-IDF 업데이트되면 바뀔 수 있다고 한다. 그래서 다들 SDK 쓴다.

STM32는 레지스터가 정답이었다. HAL은 너무 무거웠다.

ESP32는 SDK가 정답이다. 레지스터는 문서화가 부족하다.

같은 펌웨어인데 철학이 다르다.

클록 트리의 지옥

STM32 클록 설정은 악명 높다.

PLL 설정, AHB 프리스케일러, APB1, APB2… CubeMX 없으면 못 한다.

하지만 익숙했다. 5년 동안 수십 번 했다.

ESP32는 더 간단하다. 대신 더 이상하다.

esp_clk_cpu_freq(); // 현재 CPU 클록

그냥 함수 부르면 나온다. 160MHz다.

“240MHz로 올리려면?”

menuconfig에서 설정한다. 코드가 아니다. 빌드 설정이다.

STM32는 코드로 다 했다.

RCC->PLLCFGR = ...;
RCC->CR |= RCC_CR_PLLON;
while(!(RCC->CR & RCC_CR_PLLRDY));

이게 정상이었다. 부팅 시퀀스를 내가 제어한다.

ESP32는 부트로더가 다 한다. 나는 main() 이후부터다.

제어권이 적다. 편한데 불안하다.

메모리 맵이 다르다

STM32 Flash는 0x08000000부터다. SRAM은 0x20000000이다.

외웠다. 디버거 없이도 메모리 덤프 보면 안다.

ESP32는 다르다.

Flash는 0x400xxxxx다. 근데 매핑된 거다. 실제 SPI Flash는 0x3F400000에 있다.

SRAM은 여러 개다. DRAM, IRAM, RTC 메모리…

uint8_t data[100];

이게 어디에 할당될까?

모른다. 링커 스크립트가 정한다.

STM32는 명확했다.

uint8_t data[100] __attribute__((section(".ccmram")));

이러면 CCM RAM에 간다. 내가 정한다.

ESP32도 가능하다. 하지만 섹션 이름이 다르다.

uint8_t data[100] DRAM_ATTR;

DRAM_ATTR, IRAM_ATTR, RTC_DATA_ATTR…

매크로다. 의미는 같은데 방식이 다르다.

적응해야 한다.

인터럽트가 이상하다

STM32 인터럽트는 NVIC다. ARM Cortex-M 표준이다.

NVIC_EnableIRQ(EXTI0_IRQn);
NVIC_SetPriority(EXTI0_IRQn, 1);

우선순위 숫자 낮을수록 높다. 헷갈리지만 익숙하다.

ESP32는 Xtensa 코어다. ARM이 아니다.

인터럽트 컨트롤러가 다르다.

esp_intr_alloc(ETS_GPIO_INTR_SOURCE, 0, gpio_isr_handler, NULL, &handle);

동적 할당이다. 런타임에 인터럽트를 등록한다.

STM32는 컴파일 타임이다. 벡터 테이블이 고정이다.

“왜 이렇게?”

찾아봤다. Xtensa는 인터럽트 레벨이 7개다. ARM은 우선순위 기반이고.

아키텍처가 다르니 당연하다. 하지만 코드는 전부 다시 써야 한다.

타이머도 다르다

STM32 타이머는 복잡하다.

TIMx 18개, 각각 모드가 다르다. PWM, 인코더, 원샷…

근데 강력하다. 하드웨어로 다 된다.

ESP32 타이머는 4개다.

timer_config_t config = {
    .divider = 80,
    .counter_dir = TIMER_COUNT_UP,
    .counter_en = TIMER_PAUSE,
    .alarm_en = TIMER_ALARM_EN,
    .auto_reload = TIMER_AUTORELOAD_EN,
};

또 구조체다. 하지만 단순하다.

PWM은 LEDC로 한다. LED Control의 약자다.

“LED 깜빡이려고 만든 건데 PWM도 된대.”

문서에 그렇게 써있다. 솔직하다.

STM32 고급 타이머 쓰다가 이거 쓰니까 허전하다.

하지만 동작은 한다. 그걸로 됐다.

DMA의 차이

STM32 DMA는 채널이다.

DMA1_Stream0->CR = ...;
DMA1_Stream0->NDTR = size;
DMA1_Stream0->PAR = (uint32_t)&peripheral;
DMA1_Stream0->M0AR = (uint32_t)buffer;

레지스터 몇 개 설정하면 끝이다.

ESP32는 각 페리페럴마다 다르다.

SPI는 SPI DMA가 있다. I2S는 I2S DMA가 있다.

범용 DMA가 없다.

spi_bus_config_t buscfg = {
    .mosi_io_num = PIN_NUM_MOSI,
    // ...
    .max_transfer_sz = 4096,
};

설정하면 알아서 DMA 쓴다.

편하다. 하지만 제어권이 적다.

DMA 채널 번호가 뭔지도 모른다. SDK가 관리한다.

“나는 그냥 쓰기만 하면 돼.”

맞는 말이다. 하지만 찝찝하다.

디버깅이 다르다

STM32는 SWD다. ST-Link 꽂으면 끝이다.

Eclipse, Keil, IAR… 다 된다.

중단점 잡고 레지스터 보고 메모리 덤프 뜨고.

ESP32는 JTAG이다. 근데 USB로 바로 된다.

openocd -f board/esp32-wrover-kit-3.3v.cfg

OpenOCD 돌리고 GDB 붙인다.

되긴 한다. 하지만 느리다.

그래서 다들 printf 디버깅한다.

ESP_LOGI(TAG, "Value: %d", value);

로그 레벨 있다. 색깔도 나온다.

STM32에서 printf는 UART로 빼야 했다. 귀찮았다.

ESP32는 USB-UART 칩이 보드에 있다. 그냥 된다.

편하긴 하다.

하지만 중단점 없이 디버깅하는 게 익숙하지 않다.

빌드 시스템도 다르다

STM32는 Makefile이었다.

SOURCES = main.c system.c
OBJECTS = $(SOURCES:.c=.o)

간단했다. arm-none-eabi-gcc 부르면 끝.

ESP32는 CMake다. ESP-IDF 프레임워크다.

idf.py build
idf.py flash
idf.py monitor

명령어 세 개면 된다.

편하다. 하지만 내부를 모르겠다.

빌드 옵션 바꾸려면 menuconfig 들어가야 한다.

Makefile은 그냥 열어서 고치면 됐다.

제어권이 없다. 프레임워크에 맡긴다.

이게 현대적인 방식이라고 한다.

맞는 말이다. 하지만 답답하다.

코드를 다시 쓴다

결국 이렇게 됐다.

기존 STM32 코드 5000줄.

재사용 가능한 건 로직뿐이다. HAL 호출 부분은 전부 버린다.

// Before (STM32)
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);

// After (ESP32)
gpio_set_level(GPIO_NUM_5, 1);

의미는 같다. 문법이 다르다.

GPIO만 그런 게 아니다. SPI, I2C, UART… 전부 다시 쓴다.

“비슷한 건데 왜 다시 써야 해?”

팀원이 물었다. 맞는 말이다.

하지만 어쩔 수 없다. 칩이 다르니까.

2주가 지났다

GPIO 포팅 끝났다. SPI도 거의 끝났다.

I2C에서 막혔다. 타이밍이 안 맞는다.

STM32는 400kHz에서 잘 됐다. ESP32는 100kHz로 낮춰도 에러다.

오실로스코프 꺼냈다.

SCL 파형이 이상하다. Glitch가 보인다.

“하드웨어 문제 아니에요?”

HW팀에 물었다.

“ESP32 레퍼런스 디자인 그대로인데요.”

그럼 내 코드가 문제다.

밤을 샜다.

알고 보니 pull-up 저항 설정이 문제였다.

io_conf.pull_up_en = GPIO_PULLUP_ENABLE;

이거 하나 추가하니 됐다.

STM32는 외부 저항 달았었다. ESP32는 내부 풀업이 있다.

알았으면 10분이다. 몰라서 8시간 걸렸다.

익숙함의 가치

STM32에서 5년 일했다.

레지스터 주소 외웠다. 에러 패턴 안다. 회로 문제인지 코드 문제인지 감으로 안다.

ESP32는 처음이다.

같은 문제를 만나도 시간이 2배 걸린다.

“왜 안 되지?” 할 때가 많다.

STM32였으면 바로 알았을 텐데.

익숙함은 속도다. 속도는 돈이다.

“그래서 ESP32 쓰면 얼마나 아끼는데?”

팀장한테 물어봤다.

“BOM 비용 칩 하나에 2달러.”

1000개 만들면 200만원이다.

내 야근 수당이 그것보다 많다.

하지만 회사는 칩 값을 본다. 인건비는 안 본다.

이미 주는 월급이니까.

한 달이 지났다

이제 좀 익숙하다.

ESP-IDF 문서 찾는 속도가 빨라졌다.

menuconfig 구조도 알겠다.

코드 포팅 90% 끝났다.

테스트 중이다. 버그 잡는 중이다.

STM32 때보다 빠르다고 할 수는 없다.

하지만 느리지도 않다.

새로운 걸 배웠다. ESP32도 나쁘지 않다.

WiFi, Bluetooth가 내장이다. STM32는 외부 칩이었다.

전력 소모도 적다. Deep sleep 모드가 좋다.

장단이 있다.

근데 또 바뀔 것 같다.

다음 프로젝트는 nRF52래.

Nordic 칩이다. 또 새로운 SDK다.

“또요?”

물었다.

“BLE가 더 좋대.”

그렇다고 한다.

펌웨어는 이렇다. 칩이 바뀐다. 레지스터가 바뀐다.

매번 다시 배운다.

웹 개발자가 부럽다. API는 안 바뀐다.

우리는 하드웨어에 종속된다.

하드웨어가 바뀌면 다 바뀐다.


레지스터맵이 바뀌면 코드도 바뀐다. 당연한데 매번 힘들다.