| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | ||
| 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| 13 | 14 | 15 | 16 | 17 | 18 | 19 |
| 20 | 21 | 22 | 23 | 24 | 25 | 26 |
| 27 | 28 | 29 | 30 |
- error-passive
- CAN 필터
- bus-off
- i.MX8MP
- 임베디드 리눅스
- xHCI
- 리눅스 CAN
- Azure RTOS
- HAL_Delay
- 오디오 클럭
- 임베디드
- PTN5150A
- 리눅스 커널
- 하드웨어 디버깅
- assigned-clocks
- can-utils
- vcan
- CAN FD
- restart-ms
- EILSEQ
- 커스텀 보드
- SocketCAN
- can_filter
- fsl-sai
- EASRC
- 44.1kHz
- CC 로직
- TIM17
- 타임베이스
- 임베디드 오디오
- Today
- Total
임베디드 일기장
리눅스 RS-485 DE 핀 제어, libmodbus에 맡기지 마세요 — TIOCSRS485로 커널에 위임하기 본문
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. 정리
- RS-485 DE 타이밍의 정답은 커널/하드웨어 자동 토글이다 — 유저스페이스 토글은 마지막 바이트 잘림의 위험을 항상 안고 간다
- libmodbus에는 serial_rs485 플래그를 제어할 API가 없다
- 그러니 미리 TIOCSRS485(또는 DT)로 포트를 설정해두고, libmodbus는 일반 시리얼로 쓰게 하라 —
modbus_rtu_set_serial_mode()는 부르지 않는다 - 설정은 fd가 아니라 포트에 귀속되므로 이 패턴이 성립한다
- GPIO DE 배선은 마지막 수단이고, 새 보드라면 RTS-DE 직결을 설계에 반영하라
i.MX8MP + libmodbus 환경에서 실측한 내용입니다. 로직애널라이저로 DE 토글 타이밍을 캡처한 파형은 추후 추가 예정.
'Linux' 카테고리의 다른 글
| 커스텀 보드에서 USB 3.0이 2.0으로만 잡힐 때 — dmesg 판독법과 Type-C CC 로직(PTN5150A)/크로스바 추적 (0) | 2026.07.12 |
|---|---|
| SocketCAN CAN FD 실전 설정 — ip link 구성, write() EILSEQ 에러, ID 마스크 필터링까지 (0) | 2026.07.11 |
| 리눅스 ISO-TP에서 read()가 "Communication error on send"를 뱉는 이유 — sk_err와 커널 에러 전파 메커니즘 (0) | 2026.07.11 |
| 운영체제란 (0) | 2026.07.11 |
| i.MX8MP에서 TX700(Qualcomm MDM9207) LTE 모뎀 RNDIS 연결하기 — lsusb에 안 보이는 것부터 인터넷 연결까지 (0) | 2026.07.11 |