임베디드 일기장

리눅스 RS-485 DE 핀 제어, libmodbus에 맡기지 마세요 — TIOCSRS485로 커널에 위임하기 본문

Linux

리눅스 RS-485 DE 핀 제어, libmodbus에 맡기지 마세요 — TIOCSRS485로 커널에 위임하기

jaeuuu 2026. 7. 11. 12:48
반응형

RS-485 half-duplex 버스에서 Modbus RTU를 돌리려면 트랜시버의 DE/RE 핀을 제때 토글해야 한다. 송신 중에는 DE=1, 송신이 끝나는 즉시 DE=0으로 내려서 슬레이브 응답을 받을 준비를 해야 한다. 이 타이밍을 누가 책임지느냐가 이 글의 주제다.

 

결론부터: libmodbus에 맡기지 말고 커널 드라이버에 위임하라.

 

환경

  • 보드: i.MX8MP (imx-uart 드라이버)
  • 라이브러리: libmodbus (Modbus RTU 마스터)
  • 트랜시버: DE/RE 제어가 필요한 일반적인 RS-485 트랜시버

1. 문제: 누가 DE를 토글하나

구성은 이렇다.

[i.MX8MP UART] ──TX/RX──> [RS-485 트랜시버] ──> [버스]
                  RTS ────> DE/RE

libmodbus 문서를 보면 modbus_rtu_set_serial_mode(ctx, MODBUS_RTU_RS485)라는 함수가 있어서 이것만 부르면 될 것 같다. 그런데 실제로 써보면 환경에 따라 에러를 리턴하거나, 성공해도 DE 타이밍이 어긋난다. 이유를 알려면 리눅스에서 RS-485를 제어하는 두 가지 경로를 구분해야 한다.

2. 두 가지 경로: 커널 자동 토글 vs 유저스페이스 수동 토글

경로 A — 커널 위임 (TIOCSRS485): UART 드라이버가 하드웨어 레벨에서 RTS를 자동 제어한다. 송신 시작 시 어서트하고, 마지막 바이트가 시프트 레지스터에서 빠져나간 뒤 디어서트한다. 애플리케이션은 write()만 하면 된다.

#include <linux/serial.h>

struct serial_rs485 rs485 = {
    .flags = SER_RS485_ENABLED       /* RS-485 모드 활성화 */
           | SER_RS485_RTS_ON_SEND,  /* 송신 중 RTS 어서트 */
    .delay_rts_before_send = 0,      /* ms */
    .delay_rts_after_send  = 1,      /* ms */
};
ioctl(fd, TIOCSRS485, &rs485);

경로 B — 유저스페이스 토글: libmodbus가 write() 전후로 TIOCMBIS/TIOCMBIC ioctl로 RTS를 직접 올렸다 내린다.

modbus_rtu_set_serial_mode(ctx, MODBUS_RTU_RS232);  /* 자동 토글 끔 */
modbus_rtu_set_rts(ctx, MODBUS_RTU_RTS_UP);
modbus_rtu_set_rts_delay(ctx, 1000);  /* µs */

경로 B의 치명적 문제는 타이밍이 유저스페이스에 있다는 것이다. write()가 리턴한 시점은 데이터가 커널 버퍼에 들어간 시점이지 전선으로 다 나간 시점이 아니다. libmodbus가 tcdrain()을 호출하긴 하지만, 스케줄링 지연까지 겹치면 마지막 바이트가 실제로 송출되기 전에 RTS가 떨어져 마지막 바이트가 잘리는 증상이 나온다. 반대로 너무 늦게 떨어지면 슬레이브 응답의 첫 바이트와 충돌한다. 경로 A는 이 판정을 UART 하드웨어의 시프트 레지스터 상태로 하기 때문에 원리적으로 정확하다.

3. libmodbus의 공백: TIOCSRS485 플래그를 세밀하게 못 만진다

그럼 libmodbus의 RS485 모드가 내부적으로 경로 A를 써주면 되지 않나? 부분적으로는 그렇게 하는데, 문제는 serial_rs485 구조체의 플래그(RTS_ON_SEND/RTS_AFTER_SEND 극성, before/after 딜레이)를 지정하는 API가 없다는 것이다. 게다가 다음 조건 중 하나라도 걸리면 함수 자체가 무력화된다.

  • libmodbus 3.0.x 계열: RS-485 지원이 부실 (3.1.x 이상 권장)
  • 빌드 시점에 <linux/serial.h>의 TIOCSRS485 정의가 없었으면 해당 코드가 아예 빠짐
  • UART 드라이버가 TIOCSRS485를 미구현이면 ENOTTY/ENOIOCTLCMD

즉 라이브러리 API에 의존하면 "플래그 극성이 하드웨어와 안 맞는데 바꿀 방법이 없는" 상황에 갇힌다.

4. 권장 패턴: 미리 설정하고, libmodbus는 모르게 하라

핵심 아이디어는 이것이다. 커널은 RS-485 설정을 포트별로 유지한다. fd를 닫아도 설정이 남는다. 그러니 앱 초기화 시점에 포트를 한 번 열어 원하는 플래그로 TIOCSRS485를 호출하고 닫은 뒤, libmodbus에는 같은 디바이스 노드를 그냥 일반 시리얼처럼 열게 하면 된다. modbus_rtu_set_serial_mode()호출하지 않는다.

/* init에서 한 번 */
int fd = open("/dev/ttymxc2", O_RDWR);
struct serial_rs485 rs485 = {
    .flags = SER_RS485_ENABLED | SER_RS485_RTS_ON_SEND,
    .delay_rts_before_send = 0,
    .delay_rts_after_send  = 1,
};
ioctl(fd, TIOCSRS485, &rs485);
close(fd);   /* 설정은 포트에 남는다 */

/* libmodbus는 RS-485의 존재를 모른다 */
modbus_t *ctx = modbus_new_rtu("/dev/ttymxc2", 9600, 'N', 8, 1);
modbus_connect(ctx);

더 깔끔한 변형은 Device Tree에서 부팅 시점에 못 박는 것이다.

&uart3 {
    linux,rs485-enabled-at-boot-time;
    rs485-rts-active-high;
    rs485-rts-delay = <0 1>;   /* before, after (ms) */
    status = "okay";
};

이러면 유저스페이스 코드에는 RS-485 관련 코드가 한 줄도 안 남는다. 포트의 물리적 성격은 보드 기술(DT)의 영역이라는 리눅스의 철학과도 맞는다.

5. RTS가 아니라 GPIO로 DE를 제어하는 보드라면

하드웨어가 UART의 RTS 핀이 아닌 임의 GPIO로 DE를 제어하도록 배선됐다면 두 가지 선택지가 있다.

첫째, DT에 rts-gpios를 지정하면 시리얼 코어가 그 GPIO를 RTS처럼 다뤄준다(드라이버 지원 필요). 둘째, libmodbus의 커스텀 RTS 콜백을 쓴다.

static void rts_callback(modbus_t *ctx, int on)
{
    gpio_set(DE_GPIO, on);   /* libgpiod 등으로 구현 */
}
modbus_rtu_set_custom_rts(ctx, rts_callback);

단 이 방식은 다시 유저스페이스 타이밍 문제로 돌아가는 것이므로, 가능하면 배선 단계에서 UART RTS 핀을 DE에 연결하는 게 맞다.

6. 정리

  1. RS-485 DE 타이밍의 정답은 커널/하드웨어 자동 토글이다 — 유저스페이스 토글은 마지막 바이트 잘림의 위험을 항상 안고 간다
  2. libmodbus에는 serial_rs485 플래그를 제어할 API가 없다
  3. 그러니 미리 TIOCSRS485(또는 DT)로 포트를 설정해두고, libmodbus는 일반 시리얼로 쓰게 하라modbus_rtu_set_serial_mode()는 부르지 않는다
  4. 설정은 fd가 아니라 포트에 귀속되므로 이 패턴이 성립한다
  5. GPIO DE 배선은 마지막 수단이고, 새 보드라면 RTS-DE 직결을 설계에 반영하라

i.MX8MP + libmodbus 환경에서 실측한 내용입니다. 로직애널라이저로 DE 토글 타이밍을 캡처한 파형은 추후 추가 예정.

반응형