아리송한 MODBUS-RTU 특이점

2021. 5. 28. 18:58 컴퓨터/프로그래밍

MODBUS-RTU 왜 이렇게 만들었을까?

통신으로 장비를 제어하는 시스템을 개발한다면 모드버스(MODBUS) 프로토콜이 편한데요, 다뤄야 할 장비가 모드버스를 지원한다면 반가울 정도입니다. 그런데 이렇게 편한 모드버스 프로토콜을 처음 접했을 때는 정말이지 왜 이렇게 만들었나 이해가 안 되는 부분이 한둘이 아니었습니다. 정리하면 이렇습니다.

  • 시리얼 통신인데 STX·ETX가 없다?
  • 바이트 순서가 CRC만 다르다?
  • 읽기 함수는 레지스터 개수, 응답은 byte 개수?
  • 레지스터와 어드레스의 시작은 0번? 1번?
  • 32bit 데이터의 바이트 전송 순서는?

MODBUS-RTU에 STX·ETX가 없다?

모드버스 통신에는 전송하는 바이트 형식에 따라 MODBUS-RTU와 MODBUS-ASCII가 있습니다. 이름에서 예상할 수 있듯이 MODBUS-ASCII는 개행 문자(0x0d 0x0a)로 패킷을 구분할 할 수 있습니다. 종료를 알 수 있고 시작을 준비할 수 있습니다. 그러나 바이너리 통신인 MODBUS-RTU에는 대부분의 바이너리 시리얼 통신에서 사용하는 STX와 ETX가 없습니다.

모드버스는 패킷 길이를 담아서 전송하므로 최소한 데이터의 시작인 STX만이라도 보내 주어야 하는데, STX·ETX 특수 문자보다는 시간으로 구분합니다. 즉, 통신 중에 3.5 character 이상 데이터가 없다면 패킷의 종료입니다. 3.5 character 전송 시간 이상 아무 소식 없다가 수신되는 데이터가 있다면 새로운 패킷의 시작인 것이죠.

시리얼 통신에서 STX와 ETX를 사용하던 습관이 오래돼서 모드버스 루틴을 처음 구현할 때는 이 부분이 매우 이해가 안 되었습니다. 더욱이 시리얼 포트의 입출력을 직접 제어한다면 3.5 char 시간을 구분할 수 있겠지만, 윈도우나 리눅스 프로그램에서는 구분하기 어렵습니다.

그나마 모드버스 통신은 polling 방식이어서 마스터의 입장은 편안 편입니다. 통신 우선순위를 마스터가 가지고 있고, 질의한 후에 응답을 기다리면 되니까요? 그러나 마스터와 슬레이브가 쉬지 않고 계속 주고받는 상태에서 또 다른 슬레이브가 통신에 참여했는데 패킷을 구분할 시점을 모르면 난감합니다. 수신 버퍼로 계속해서 데이터가 쌓이는데 어디가 시작인지 모르니 자기에게 보낸 패킷도 걸러내지 못합니다.

슬레이브가 정신이 없으니 마스터가 질의를 해도 응답을 못하니 정적이 흐를 테고 마스터는 대기하다가 다시 패킷을 전송하면 그때서야 슬레이브는 자기에게 보낸 패킷인 줄 알아서 응답할 것입니다. 이렇게도 통신이야 되겠지만, 마스터가 타임아웃 시간 동안 기다리는 일이 많아지는 것은 문제가 있습니다.

그러므로 마스터는 응답을 받았다고 바로 다음 패킷을 전송하기보다는 3.5char 전송 시간 이상을 대기했다가 보내 줍니다. 3.5char 정도의 짧은 시간을 구별하지 못하는 시스템을 위해 좀 더 여유있게 polling하는 것이 더욱 좋습니다.

어쩌면 너무 심한 비약인지 모르겠습니다. rs485 통신을 사용하는 시스템이 매우 고속으로 처리하는 경우가 적고, 마스터가 질의한다고 슬레이브가 바로 응답하기보다는 명령을 처리 후에 응답하는 경우도 많아서 마스터가 3.5 char 전송 시간 대기를 하지 않아도 자연적으로 시간 지연이 일어나기 때문입니다.

또한, 모든 슬레이브와 통신 후에 다음 루프 전에 잠시 대기하는 시간을 둔다면 이때 슬레이브에서 수신 버퍼를 초기할 수 있습니다. 이후로 마스터와 다른 슬레이브 간에 빠르게 통신한다고 해도 모드버스 프로토콜에 따라 분석할 수 있으므로 모든 슬레이브는 패킷과 패킷을 구분할 수 있습니다.

STX·ETX를 사용했다면 이런 혼란이 없을 텐데. 물론, STX(0x02), ETX(0x03)을 사용하지 않아서 패킷 내에 0x02와 0x03을 그대로 데이터로 처리할 수 있습니다. 모드버스 프로토콜 내에는 길이 데이터가 있어서 그만큼만 수신하면 되므로 코딩이 간단하다는 장점이 있습니다.

모드버스는 빅엔디안이 기본? 그러나 CRC는?

MODBUS-RTU 통신에서 STX·ETX가 없는 것은 특성에 주의한다면 별 문제가 없어서 이해가 됩니다. 그러나 이제부터 소개할 내용은 모드버스를 이렇게 디자인한 깊은 뜻을 이해하지 못하겠습니다. 첫 번째로 CRC만 다른 전송 순서입니다. 모드버스 패킷 구성을 보면 바이트 2개인 word 데이터는 빅엔디안으로 처리합니다. 전송할 때도 큰 자리 숫자부터 보냅니다.

그런데 이상하게 모드버스의 CRC도 바이트 2개인 word 데이터인데 리틀엔디안으로 보내야 합니다. 예를 들어 장비 1번에게 모드버스 통신 함수 4번을 이용하여 10번 주소에서부터 레지스터 1개의 값을 요청하려고 합니다. 계산된 CRC는 0xC811입니다.

▲ 국번 [01], 함수 번호 [04], 주소는 10이므로 [00 0A], 길이도 1이라서 [00 01]입니다. 즉, word인 주소와 길이는 빅엔디안이지만, CRC는 [C8 11]이 아닌 [11 C8] 순서로 보내야 합니다. 왜 이렇게 정했을까요? 혹시 이유를 아시는 분은 댓글로 말씀을 부탁드립니다.

읽기 함수는 레지스터 개수, 응답은 byte 개수?

모드버스 통신에서 함수 3번과 4번은 레지스터 값을 읽습니다. 마스터가 주소를 지정해서 레지스터 몇 개 이런 식으로 요청합니다. 마스터에서 개수를 정해서 질의를 했음에도 슬레이브는 응답할 데이터의 갯수를 알려 주면서 전송해 주는데요, 문제는 알려 주는 숫자가 레지스터 개수가 아니라 바이트 개수입니다. 그런데 더욱 이상한 것은 마스터가 요청하는 개수의 바이트는 2개인데, 슬레이브의 응답 개수의 바이트는 1개입니다.

▲ 앞서 와 같이 국번 1번, 함수 4번, 주소 10번, 레지스터 1개를 요청해 보겠습니다. 요청한 길이는 바이트 2개이며 한 개를 요청했습니다.

▲ 이에 대해 슬레이브 응답을 보면 길이 단위가 바이트 한 개입니다. 더욱이 레지스터 개수가 아니라 데이터의 바이트 개수입니다. 이런 이유로 마스터가 요청할 때 레지스터 개수를 127까지만 요청할 수 있습니다. 128개 이상을 요청하면 응답 길이가 1개 바이트가 표현할 수 있는 숫자를 넘어 버리기 때문입니다. 이러다 보니 마스터의 길이에서 high 바이트는 항상 0입니다.

이렇게 구성하는 이유가 뭘까요? 어쩌면 3번과 4번 함수의 응답에 대한 구성에서 길이 1바이트일 것입니다. 응답에서 길이가 2바이트, 4바이트인 함수가 새로 생길 수 있을 것입니다. 모드버스 함수 15번, 16번의 경우 요청과 같이 응답도 2바이트입니다.

정확한 이유를 모르겠지만, 제일 많이 사용하는 3번과 4번의 길이가 레지스터에 2바이트 크기라면 오해할 수 있으므로 주의해야 합니다.

레지스터와 어드레스의 시작은 0번? 1번?

모드버스는 코일과 레지스터 번호에 맞추어 주소를 지정해서 읽거나 쓰기를 합니다. 그렇다면 첫 번째 코일과 레지스터 번호는 몇 번일까요? 0번부터 시작할까요? 아니면 1번부터 시작할까요? 모드버스와 관련하여 여러 자료를 찾아보았지만, 제일 정확하다고 생각하는 문서는 Simply Modbus 의 Q&A입니다.

▲ 정리하면 이렇습니다. 코일과 레지스터의 번호가 1번부터 시작하는군요. 그렇다면 1번 코일의 상태를 읽으려면 주소는 1일까요? 아니면 0으로 지정해야 할까요? 세상은 1부터 시작하지만, 프로그래머는 습관적으로 0부터 카운트합니다.

▲ 이런 이유로 잘 만들어진 모드버스 맵에는 레지스터 번호와 주소를 함께 명시해 놓습니다. 30001부터 시작하니 읽기·쓰기 레지스터 영역임을 알 수 있고 실제 읽기 요청할 때 주소는 0번부터 시작한다는 것을 알 수 있습니다.

레지스터 번호에서 30001을 빼세요~ 하는 안내 문장을 넣는 것보다는 수고스럽더라도 Address 열을 넣어 주는 것이 좋습니다. 왠지 비효율적으로 보이지만, 코딩하다 보면 실수로 하나 빼는 것을 잊어버리기도 합니다. 300012를 12로 입력하는 것이죠.

▲ 정리하면 위와 같이 정리할 수 있습니다. 모드버스에서는 30,000이니 40,000이니 하는 것은 영역을 구분하는 것이고 실제로는 0부터 카운트합니다. (주소를 1번부터 시작하는 장비가 있다고 하지만, ...)

▲ 그러나 인터넷으로 검색해 보면 자료마다 차이가 있고 장비마다 설명하는 내용이 다릅니다. 시작 주소를 HEX로 정의하는 경우입니다.

▲ HEX 값이지만, 레지스터 번호를 0번부터 정의했네요. 이런 경우는 주소도 0으로 시작하겠죠? 그래도 확인하는 것이 좋겠습니다.

▲ 이런 이유로 모드버스 맵을 정리할 때 레지스터 번호와 주소를 함께 명시하는 것이 혼란을 주지 않습니다.

32bit 데이터의 바이트 전송 순서는?

두 개의 레지스터로 32bit 값을 요청했다면 바이트 2개 레지스터 값처럼 빅엔디안 순서로 올 것 같은데 장비마다 다릅니다. 이것 참....

▲ PPA Voltage 값은 32bit 값입니다. 그래서 30010과 30011 레지스터를 읽어야 하지요. 그렇다면 높은 자리 숫자가 30010에 있을까요? 아니면 30011에 있을까요? 레지스터의 값을 빅엔디안으로 받으니 30010이 높고 30011이 낮은 자리 수의 값이 오는 것이 자연스럽지 않을까요?

▲ 예를 들어 PPA Voltage 값이 0x12345678이라고 한다면 어느 쪽이 맞을까요? 모두 빅엔디안을 충족한다면 왼쪽이 맞는 것 같은데요.

[0x12 0x34 0x56 0x78]

▲ 그래서 이런 순서로 수신될 것 같습니다만, 모든 장비가 이렇지는 않습니다. 이럴 수밖에 없는 것이 모드버스에서 32bit 데이터 전송에 대해 명확히 정의된 내용이 어디 있는지 찾지를 못하겠습니다. 여러 장비를 경험하다 보면 레지스터 값만 빅엔디안이고 레지스터 순서는 반대인 경우도 있습니다.

[0x56 0x78 0x12 0x34]

▲ 이렇게 말이죠. 더 심한 것은 아예 32bit의 값은 리틀엔디안을 따르는 장비도 있습니다.

흠.... 이런 이유로 모드버스 통신 테스터로 유명한 Modbus Poll 도 32bit, 64bit에 대해서는 순서를 지정할 수 있는 옵션이 있습니다. 모드버스 라이브러리 libmodbus에도 바이트 순서에 따라 조합해서 값을 구하는 abcd, badc, cdab, dcba 식으로 순서에 따라 숫자를 변환해 주는 함수가 있습니다.

  • modbus_get_float_badc()
  • modbus_set_float_abcd()
  • modbus_set_float_cdab()
  • modbus_set_float_dcba()

이게 머선 129.

두 개의 레지스터로 32bit, 4개의 레지스터로 64bit 값을 전송할 때도 순서를 명확한 정의 했다면 이런 혼란은 없었을 것입니다. 어쩌면 모드버스를 정의할 때 32bit 값을 생각할 필요를 느끼지 못한 것일 수 있겠습니다. PLC 같은 장비를 제어하는 용도였다면. 그러나 float 값이 그때도 필요했을 텐데, ...

이 댓글을 비밀 댓글로

티스토리 로그인이 풀리면 여기를 클릭하세요.

error: Content is protected !!