경량 Modbus RTU 드라이버
ESP-Modbus 대비 ROM 92%, RAM 92% 절감한 동기식 Modbus RTU 구현. FSM 기반 프레임 파싱과 WiFi/BLE 공존 최적화
배경
- 시스템: ESP32-S3 기반 IoT 컨트롤러 (WiFi + BLE + RS485)
- 요구사항: ISV2-RS 서보 모터를 Modbus RTU로 제어하면서 WiFi/BLE 동시 사용
- 제약: ESP-Modbus 라이브러리가 WiFi와 리소스 경쟁, 제한된 RAM 환경
핵심 문제
ESP32-S3에서 Modbus와 WiFi를 함께 사용할 때 여러 문제가 발생합니다:
- ESP-Modbus가 50KB ROM, 20KB RAM을 사용하여 WiFi 버퍼 공간 부족
- 별도 태스크와 큐로 인한 컨텍스트 스위칭 오버헤드
- RS485 트랜시버 전환 시 garbage byte(0xFE, 0xFF) 발생
- WiFi 전송과 Modbus 타이밍 충돌
어려운 점: Modbus RTU는 T3.5(3.5 문자 시간) 프레임 구분이 핵심인데, WiFi 인터럽트가 이 타이밍을 방해합니다. 동시에 RS485 반이중 전환 시 발생하는 노이즈를 필터링해야 합니다.
핵심 아이디어
동기식 API와 FSM 기반 수신기로 태스크/큐 오버헤드를 제거하고, 프레임 간 지연 설정으로 WiFi에 CPU 시간을 양보합니다. 결과: ROM 92%, RAM 92% 절감.
왜 FSM인가? RS485 트랜시버 전환 시 garbage byte가 실제 응답 앞에 나타납니다. 단순 타임아웃 방식은 이를 프레임 시작으로 오인합니다. FSM은 주소+함수코드 패턴을 찾아 실제 프레임만 추출합니다.
접근 방식
1) 리소스 비교
| 항목 | ESP-Modbus | Modbus Lite | 절감률 |
|---|---|---|---|
| ROM | 50KB | 4KB | 92% |
| RAM | 20KB | 1.5KB | 92% |
| 태스크 스택 | 4KB | 0 | 100% |
| 큐 메모리 | 2KB | 0 | 100% |
2) FSM 기반 프레임 수신기
Garbage byte를 건너뛰고 유효한 프레임만 추출합니다:
상태 전이:
RX_SYNC → [주소 일치] → RX_GOT_ADDR_FUNC → [3번째 바이트] → RX_COLLECT_REST
↑ ↓
└────────────────── [CRC 실패: 재동기화] ──────────────────────┘
3단계 검증:
- 주소 + 함수코드 패턴 매칭
- 함수코드별 예상 길이 계산
- CRC16 검증 후 수락
// 슬라이딩 윈도우로 addr+func 패턴 탐색
if (prev == expected_addr && is_valid_func_response(expected_func, b)) {
rx[0] = prev;
rx[1] = b;
st = RX_GOT_ADDR_FUNC;
}
3) Modbus 표준 타이밍 구현
T3.5와 T1.5 문자 시간을 보레이트에 따라 정확히 계산합니다:
// 19200 bps 이하: 실제 계산
if (baudrate <= 19200) {
t15_us = (bit_time_us * bits_per_char * 15) / 10; // 1.5 문자
t35_us = (bit_time_us * bits_per_char * 35) / 10; // 3.5 문자
} else {
// 고속: 고정값 (ESP-Modbus 표준)
t15_us = 750;
t35_us = 1750;
}
4) WiFi/BLE 공존 최적화
프레임 간 지연으로 WiFi에 CPU 시간을 양보합니다:
// WiFi 부하에 따라 동적 조절
void modbus_lite_set_frame_delay(uint32_t delay_ms);
// 사용 예
if (wifi_is_busy()) {
modbus_lite_set_frame_delay(20); // WiFi 우선
} else {
modbus_lite_set_frame_delay(5); // 정상 모드
}
타이밍 시퀀스:
[T3.5 대기] → [프레임 딜레이] → [버스 idle 확인] → [TX] → [TX 완료 대기] → [RX]
5) RS485 하드웨어 모드
ESP32-S3의 하드웨어 RS485 반이중 모드를 활용하여 RTS 핀을 자동 제어합니다:
// 하드웨어가 RTS를 자동 제어
uart_set_mode(uart_num, UART_MODE_RS485_HALF_DUPLEX);
// RX 타임아웃으로 프레임 종료 감지
uart_set_rx_timeout(uart_num, tout_thresh);
uart_set_always_rx_timeout(uart_num, true);
6) 동기식 API
태스크와 콜백 없이 단순한 블로킹 호출:
// 초기화
modbus_lite_config_t config = {
.baudrate = 115200,
.uart_num = UART_NUM_1,
.tx_pin = GPIO_NUM_17,
.rx_pin = GPIO_NUM_18,
.rts_pin = GPIO_NUM_14,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_2, // 8N2
.timeout_ms = 100
};
modbus_lite_init(&config);
// 레지스터 읽기/쓰기
uint16_t position;
modbus_lite_read_registers(motor_id, REG_POSITION, 1, &position);
modbus_lite_write_register(motor_id, REG_SPEED, speed_value);
트레이드오프
| 결정 | 이유 | 대가 |
|---|---|---|
| 동기식 API | 태스크/큐 제거, 리소스 92% 절감 | 호출자 블로킹 |
| FSM 수신기 | Garbage byte 필터링, CRC 실패 복구 | 약간의 코드 복잡도 |
| 정적 버퍼 | 동적 할당 오버헤드 제거 | 512 bytes 고정 할당 |
| 3회 재시도 | CRC 에러 시 자동 복구 | 최악 300ms 지연 |
| 프레임 딜레이 | WiFi/BLE 공존 | Modbus 처리량 감소 |
결과
리소스 절감:
- ROM: 50KB → 4KB (92% 감소)
- RAM: 20KB → 1.5KB (92% 감소)
- 초기화 시간: 100ms → 10ms (90% 감소)
성능:
- 단일 요청 지연: 15-20ms → 5-10ms (50% 감소)
- CPU 점유율: 5-10% → <1% (90% 감소)
- CRC 에러율: ~10% → <0.1% (FSM + 재시도 효과)
지원 기능:
| 함수 코드 | 기능 |
|---|---|
| 0x03 | 홀딩 레지스터 읽기 |
| 0x04 | 입력 레지스터 읽기 |
| 0x06 | 단일 레지스터 쓰기 |
| 0x10 | 다중 레지스터 쓰기 |
핵심 교훈
경량화의 핵심은 “무엇을 뺄 것인가”입니다. ESP-Modbus의 범용 기능(마스터/슬레이브, TCP/RTU, 비동기 등)을 제거하고 실제 필요한 마스터 RTU 기능만 구현했습니다. FSM 기반 수신기는 RS485 노이즈 환경에서도 안정적인 프레임 파싱을 가능하게 했고, 프레임 간 지연으로 WiFi/BLE와의 공존 문제를 해결했습니다. 결과적으로 92% 리소스 절감과 함께 더 안정적인 통신을 달성했습니다.