ESP32 SDK 4.4 부트로더 최신 SDK 호환성 검토

현재 양산된 펌웨어 SDK는 퇴사한 직원이 작업한 소스이며, 나는 이것을 인수인계 받아 양산 F/U을 진행하고 있었다. SDK의 기본 부트로더를 그대로 사용하고 있었다. 이미 그렇게 사용하고 있었기에 변경은 어려웠고, OTA 구조 역시 내가 원하는 방식이 아닌 듀얼뱅크 구조를 사용하고 있다. 그 당시 Application 소스코드 구조가 너무 엉망인 상태라 SW 구조를  다시 설계해야 하지만 시간이 없는 관계로, 적당선에서 타협하여 모듈 단위로 정리하고 매니저로 관리하는 구조로 급한대로 수정하여 현재까지 사용하고 있다. 그리고 이 부분은 전담하는 팀원에 장기 프로젝트로 시간날 때마다 리팩토링 가능성을 살펴보라고 하였다.

 

그런데 PnC 기능을 구현함에 있어, STM32G4 칩셋의 메모리 제약사항으로 ESP32에서 TLS 처리를 진행함에 있어 현재 구조에선 복잡도가 계속 높아지고, 메모리에 여유가 있어도 스파게티 소스 구조로 불필요한 메모리 사용과 이런식으로 계속 임시방편으로 수정할 경우 유지보수 측이나, 신규 기능 구현에 어려움이 발생할 것 같아. 리팩토링을 진행해야 할 시점이 온 것을 본능적으로 느끼게 되었다.

 

현재 제품은 ESP-IDF 4.4.3 계열을 사용하고 있으며 문제는 애플리케이션만 새 SDK에 맞춰 다시 만드는 것으로 끝나는지가 애매했다. 이미 현장에 들어간 장비에는 기존 부트로더와 파티션 테이블이 들어가 있고, OTA도 그 구조를 기준으로 동작하고 있기 때문이다. 부트로더가 ESP-IDF 표준 이미지 형식을 읽는 구조라면, 기존 부트로더는 그대로 두고 애플리케이션만 최신 SDK로 만들 수 있을 것 같았다. 하지만 4.4.3에서 6.x까지 버전 차이가 꽤 크다. 상식적으로 될 것 같다는 판단만으로 양산 펌웨어의 OTA 구조를 결정할 수는 없었다.

기존 부트 구조

오래된 프로젝트라 세부 구현은 거의 기억나지 않았고, 여러 사람이 거쳐 간 코드를 이후 담당자가 상당 부분 정리한 상태였다. 당시 나는 기본 구조를 잡아 넘긴 뒤 다른 업무를 했기 때문에 부트로더까지 다시 들여다볼 여유도 없었다.

 

일단 Codex에게 저장소를 분석한 결과 현재 SDK는 ESP-IDF 4.4.3 release/v4.4 기반의 사내 fork였다. 다만 부트로더 자체를 수정한 흔적은 없었고, ESP-IDF stock bootloader source에 설정만 적용한 형태였다. 실제 장비의 부팅 로그도 남아 있었다. 저장소에 보존된 bootloader.bin과 비교해 보니 entry address뿐 아니라 세 개 segment의 load 길이까지 정확히 일치했다. 로그만으로 바이너리가 완전히 동일하다고 증명할 수는 없지만, 적어도 현재 저장소와 부트로더 build 계열이라는 강한 근거는 확보됐다.

1MB OTA 파티션

파티션 테이블은 바꿀 계획이 없다. 현재 장비는 16MB flash를 사용하고 있고 factory, ota_0, ota_1을 각각 1MB로 잡아 두었다.

nvs       0x00009000   16KB
otadata   0x0000D000    8KB
phy_init  0x0000F000    4KB

factory   0x00010000    1MB
ota_0     0x00110000    1MB
ota_1     0x00210000    1MB

fs        0x00314000    2MB

현재 보존된 애플리케이션 이미지는 약 923KB라 1MB 슬롯 조금 팍팍한 상태다. 신규 모델에선 2MB로 늘릴 예정이지만, 새 SDK에서 이미지가 어느 정도 커지더라도 당장 파티션 크기가 발목을 잡을 상황은 아니었다. 중요한 것은 기존 제품에서 새 파티션 테이블을 배포하는 것이 아니라, 이미 flash에 존재하는 이 구조를 그대로 계약으로 유지하는 것이다.

OTA 헤더와 부트로더의 분리

OTA 쪽에는 사내에서 추가한 1KB 헤더가 있다. 이것 때문에 처음에는 새 SDK 이미지와 기존 OTA의 관계도 다시 확인할 필요가 있어 보였지만 이 헤더는 부트로더가 읽는 이미지 형식과는 관계가 없다. 기존 OTA와의 호환을 위해 전송 패키지 앞에 붙인 헤더이고, flash에 기록할 때는 이를 제거한다. 이후 ESP-IDF가 생성한 application bin을 그대로 esp_ota_write()로 기록한다.

OTA package
┌──────────────────────┐
│ OTA 1KB header       │
├──────────────────────┤
│ ESP-IDF app.bin      │
└──────────────────────┘
          ↓
    header 검사/제거
          ↓
    esp_ota_write()
          ↓
┌──────────────────────┐
│ ESP-IDF app image    │
└──────────────────────┘
      OTA partition

따라서 기존 4.4.3 부트로더가 실제로 보는 것은 사내 OTA 헤더가 아니라 ESP-IDF 표준 application image다. OTA slot 선택과 otadata 갱신 역시 raw offset을 직접 만지는 구조가 아니라 esp_ota_begin(), esp_ota_write(), esp_ota_end(), esp_ota_set_boot_partition() 같은 공개 API에 맡기고 있었다. 결국 확인해야 할 문제는 훨씬 단순해졌다. ESP-IDF 4.4.3에서 만들어진 기존 second-stage bootloader가 ESP-IDF 6.x에서 생성한 application image를 정상적으로 읽고 부팅할 수 있는가...?

4.4.3 부트로더와 6.x 이미지

이 부분은 Codex에 범위를 좁혀 다시 분석시켰다. UART, TLS, ADC, W5500 같은 애플리케이션 API 변경은 전부 제외했다. 어차피 새 애플리케이션은 6.x 방식에 맞춰 다시 만들 예정이기 때문이다.

 

확인 결과는 COMPATIBLE WITH CONDITIONS였다. ESP-IDF는 OTA 특성상 기존 부트로더가 더 새로운 ESP-IDF에서 만들어진 애플리케이션을 부팅하는 방향의 호환성을 유지한다. 실제 image header를 4.4.3과 6.x에서 비교해도 기본 계약은 유지되고 있었다.

  • image magic 0xE9
  • 24-byte image header
  • segment header 구조
  • 최대 segment 수 16
  • entry address 위치
  • SPI mode, speed, size field
  • ESP32 chip ID
  • checksum
  • SHA-256 validation hash

6.x에서 추가된 chip revision 정보는 예전 header의 reserved 영역을 사용한다. 4.4.3 부트로더는 자신이 아는 legacy revision 값을 검사하고 새 필드는 해석하지 않는다. 새 필드가 추가됐다고 image header 전체가 깨지는 구조는 아니었다.

otadata 호환성

OTA를 계속 사용할 것이기 때문에 otadata도 따로 확인했다. 새 6.x 애플리케이션이 esp_ota_set_boot_partition()으로 기록한 정보를 기존 4.4.3 부트로더가 읽지 못하면 이미지 자체가 호환돼도 소용이 없다. 4.4.3과 6.x의 OTA metadata 구조를 비교한 결과 이 부분도 그대로 호환됐다. 32-byte entry의 ota_seq, ota_state, CRC 구조와 state 값이 유지되고 있었고, rollback을 사용하지 않는 현재 조건에서는 6.x 애플리케이션이 기록한 otadata를 4.4.3 부트로더가 읽어 ota_0 또는 ota_1을 선택할 수 있다.

 

파티션 subtype도 factory 0x00, ota_0 0x10, ota_1 0x11 구조가 그대로다. 기존 8KB otadata 파티션 역시 변경할 이유가 없었다.

SRAM1 IRAM 제한

호환성 조사에서 실제로 주의해야 할 조건도 하나 나왔다. CONFIG_ESP_SYSTEM_ESP32_SRAM1_REGION_AS_IRAM이다. ESP-IDF 5.1 이전 부트로더는 이 옵션으로 만들어질 수 있는 확장 IRAM load 영역을 제대로 처리하지 못할 수 있다. 기존 부트로더가 4.4.3이므로 새 6.x 애플리케이션에서는 이 옵션을 사용하지 않아야 한다.

CONFIG_ESP_SYSTEM_ESP32_SRAM1_REGION_AS_IRAM=n

다행히 6.x에서도 기본적으로 활성화되는 옵션은 아니다. 그래도 기존 부트로더를 제품 계약으로 계속 가져갈 예정이라면 sdkconfig.defaults에 명시적으로 고정하고 build 결과의 map까지 확인하는 편이 안전하다.

유지할 부트 계약

최종적으로 새 애플리케이션이 지켜야 할 조건은 복잡하지 않았다.

Target        : ESP32
Revision      : rev.3 호환
Flash         : 16MB
Mode          : DIO
Frequency     : 40MHz

Partition     : 기존 구조 유지
App slot      : 1MB

Secure Boot   : OFF
Encryption    : OFF
Rollback      : OFF
Anti-rollback : OFF

SRAM1 as IRAM : OFF

DIO와 40MHz 자체가 4.4.3과 6.x 사이의 image format 호환성을 위해 반드시 같아야 하는 값은 아니다. 기존 부트로더는 선택된 application header의 값을 이용해 flash 설정을 다시 잡을 수 있다. 하지만 이미 현장 하드웨어에서 검증된 조건을 새 펌웨어에서 굳이 바꿀 이유도 없다.

 

Secure Boot나 Flash Encryption, rollback 같은 기능도 이번에 새로 켜지 않는다. 이것들은 애플리케이션 하나의 설정으로 끝나는 문제가 아니라 기존 부트로더와 eFuse까지 포함하는 별도의 lifecycle이기 때문이다.

애플리케이션은 새로

이번 SDK 변경에서는 기존 4.4 애플리케이션을 6.x에 억지로 포팅할 생각이 없다. 세 명이 작업에 들어갈 예정이고, 애플리케이션은 최신 SDK의 라이브러리와 API 구조에 맞춰 다시 만들 계획이다. 기존 코드에서 UART driver를 수정했던 흔적이나 Mbed TLS, ADC, W5500, WebSocket API 차이는 부트로더 호환성 문제와 분리해서 본다. 4.4 시절 구현을 그대로 살리기보다 6.x에서 제공하는 방식에 맞춰 새 application을 구성하면 된다.

 

그래서 이번 조사에서 가장 중요했던 것은 기존 코드를 얼마나 많이 옮길 수 있느냐가 아니었다. 이미 현장에 남아 있는 부트로더와 파티션을 건드리지 않고 새 애플리케이션으로 넘어갈 수 있는지를 확인하는 일이었다.

ESP-IDF 6.1

새 애플리케이션의 기준 SDK는 현재 안정 버전인 ESP-IDF 6.1로 잡았다. 다음 단계에서는 거대한 기존 애플리케이션부터 옮기지 않는다. 먼저 ESP-IDF 6.1로 최소 application을 만들고 기존 4.4.3 bootloader.bin과 기존 partition table 조합에서 실제 ESP32 rev.3 보드가 부팅되는지 확인할 예정이다. 그 뒤 factory에서 ota_0, ota_0에서 ota_1으로 전환하고 software reset과 power cycle까지 통과시키면 된다. 문서와 source 구조 비교에서는 호환성이 확인됐지만, 양산 펌웨어에서 마지막 판단 기준은 결국 실제 장비다. 현재 결론은 명확하다.

 

기존 ESP-IDF 4.4.3 부트로더와 파티션은 그대로 유지한다. 새 애플리케이션은 ESP-IDF 6.1에서 다시 만든다. 다만 새 application이 기존 boot contract를 지키는지는 build 단계에서 계속 검사한다. 부트로더까지 함께 갈아엎지 않아도 되는지 확인하려고 시작한 조사였는데, 적어도 지금까지 확인한 구조에서는 그럴 필요가 없었다.