본 프로젝트는 MAX485와 같은 RS485 통신 칩셋이 내장된 ESP32 디바이스를 위해 Modbus RTU 프로토콜을 사용하여 펌웨어 OTA(Over-The-Air) 업데이트 기능을 구현한 예제입니다.
산업 현장과 같이 인터넷 연결이 불안정하거나 Modbus 통신이 표준으로 사용되는 환경에서, 물리적인 연결 없이 장치의 펌웨어를 안정적으로 업데이트할 수 있는 솔루션을 제공합니다.
주요 기능은 다음과 같습니다:
- Modbus RTU 기반 펌웨어 전송: 시리얼 통신을 통해 펌웨어 바이너리 파일을 분할하여 전송합니다.
- 안정적인 데이터 전송: 각 데이터 패킷에 대해 CRC(Cyclic Redundancy Check)를 수행하여 전송 중 발생할 수 있는 데이터 손상을 검증합니다.
- 타겟 업데이트 지원: Modbus 슬레이브 ID와 장치 타입을 지정하여 특정 장치 또는 특정 타입의 모든 장치에 펌웨어를 동시 업데이트할 수 있습니다.
- ESP32 디바이스: 아래 두 가지 중 하나
- MAX485 칩셋이 내장된 ESP32 커스텀 보드
- 또는, 일반 ESP32 개발 보드 + RS485 to TTL 컨버터
- PC (Windows, macOS, Linux)
- PC와 RS485 네트워크 연결을 위한 USB to RS485 컨버터
- ESP32 최초 펌웨어 플래싱을 위한 USB 케이블
- ESP-IDF: Espressif IoT Development Framework (v4.4 이상 권장)
- Python 3: 호스트 스크립트 실행용
- Python 라이브러리:
minimalmodbus,pyserial
-
저장소 복제(Clone)
git clone https://github.com/your-username/Modbus_OTA.git cd Modbus_OTA -
파티션 테이블 설정 OTA 기능을 사용하려면, 최소 두 개의 애플리케이션 파티션 (
ota_0,ota_1)과 OTA 데이터 파티션 (otadata)이 포함된 파티션 테이블을 사용해야 합니다. 본 프로젝트는 루트 디렉토리에partitions_8.csv라는 샘플 파티션 파일을 제공하며, 이미 이 파일을 사용하도록 기본 설정되어 있습니다.만약 다른 파티션 구성이 필요하다면,
idf.py menuconfig명령을 실행한 후 아래 경로에서 커스텀 파티션 테이블 CSV 파일을 지정할 수 있습니다.Partition Table→Partition Table→Custom partition table CSV -
최초 펌웨어 빌드 및 플래싱 ESP32 보드를 PC에 연결하고, 아래 명령어를 실행하여 최초 펌웨어를 빌드하고 플래싱합니다.
<YOUR_ESP_PORT>는 ESP32가 연결된 포트(예:COM3,/dev/ttyUSB0)로 변경하세요.idf.py -p <YOUR_ESP_PORT> build flash
-
장치 로그 모니터링 플래싱이 완료되면 모니터링을 시작하여 장치의 동작 상태를 확인합니다.
app_main의#ifdef구문에 따라 "Before OTA Update" 메시지가 주기적으로 출력됩니다.idf.py -p <YOUR_ESP_PORT> monitor
-
새 펌웨어 빌드 ESP32 애플리케이션 코드를 수정합니다. 예를 들어,
main/main.c파일에서 아래와 같이 수정하여 OTA 후 동작이 변경되도록 합니다.// ... #define AFTER // #define BEFORE // ...
수정 후,
idf.py build명령어를 실행하여 새 펌웨어 바이너리(build/Modbus_OTA.bin)를 생성합니다. 플래싱은 하지 않습니다.idf.py build
-
호스트 PC 설정 PC에서
ota_modbus_writer디렉토리로 이동하여 필요한 Python 라이브러리를 설치합니다.cd ota_modbus_writer pip install -r requirements.txt(참고:
requirements.txt가 없다면pip install minimalmodbus pyserial명령어로 직접 설치하세요.) -
OTA 스크립트 실행 아래 명령어를 실행하여 새 펌웨어를 ESP32로 전송합니다.
--port: USB to RS485 컨버터가 연결된 시리얼 포트--file: 빌드된 새 펌웨어 바이너리 파일 경로--id: 업데이트할 ESP32의 Modbus 슬레이브 ID (ESP32 코드의DEFAULT_MODBUS_ID와 일치해야 함)
python ota_modbus_writer.py --port <YOUR_PC_PORT> --file ../build/Modbus_OTA.bin --id 1
-
업데이트 확인 스크립트가 성공적으로 완료되고 ESP32가 재부팅되면, ESP32 모니터링 창에서 "After OTA Update" 메시지가 출력되는 것을 확인할 수 있습니다.
- 프로토콜: Modbus RTU
- Baud Rate: 115200 (기본값)
- Data Bits: 8
- Parity: None
- Stop Bits: 1
| 주소 (Hex) | 주소 (Dec) | 레지스터 타입 | 설명 |
|---|---|---|---|
0x1001 |
4097 | Holding Register | OTA 제어 및 데이터 시작 주소 |
0x1FFE |
8190 | Holding Register | OTA 시작 신호 주소 |
0x1FFF |
8191 | Holding Register | OTA 종료 신호 주소 |
-
OTA 모드 진입 (Host → Slave)
- Host는
OTA_START_SIGNAL_ADDR (0x1FFE)에 특정 값을 Write 합니다.0xAAAA: 모든 타입의 장치를 대상으로 OTA를 시작합니다.0xAA00 | type(0~127): 특정type의 장치만 OTA를 시작합니다.
- Host는
-
펌웨어 데이터 전송 (Host → Slave)
- Host는 펌웨어 파일을 124바이트 단위의 청크(chunk)로 분할합니다.
- 각 청크에 대해 Host는 [데이터 길이(2B), CRC(2B), 데이터(최대 124B)] 형식의 패킷을 구성합니다.
- 구성된 패킷을
Write Multiple Registers (FC 0x10)명령을 사용하여OTA_CONTROL_ADDR (0x1001)부터 순차적으로 Write 합니다.
-
OTA 종료 및 재부팅 (Host → Slave)
- 모든 데이터 전송이 완료되면, Host는
OTA_END_SIGNAL_ADDR (0x1FFF)에0x5555값을 Write 합니다. - Slave는 이 신호를 수신하면 펌웨어 검증을 수행하고, 유효하면 장치를 재부팅하여 새 펌웨어로 부팅합니다.
- 모든 데이터 전송이 완료되면, Host는
Modbus_OTA/
├── main/
│ ├── main.c # 메인 애플리케이션 로직
│ └── protocol/
│ ├── modbus.c # Modbus RTU 설정 및 태스크
│ ├── modbus.h
│ └── modbus_ota.c # Modbus를 이용한 OTA 처리 로직
│ └── modbus_ota.h
├── ota_modbus_writer/
│ └── ota_modbus_writer.py # 펌웨어 전송용 Python 스크립트
└── partitions_8.csv # 파티션 테이블 (OTA용)