19개 기능 코드와 진단
이 튜토리얼은 헤드리스 시뮬레이터 하나로 모든 기능 코드를 실행합니다. 구성은 RTU 서버지만 연결(connect)하지 않으므로 직렬 포트가 필요 없습니다. RTU로 구성하는 이유는 직렬 전용 기능(07, 08, 11, 12, 17)을 실행하기 위해서입니다. TCP로 구성하면 이 기능들은 예외 01이 됩니다.
모든 결과는 execute의 local 결과입니다. 같은 PDU를 실제 장비에 보내려면 클라이언트에서 request를 쓰면 됩니다.
- 네 테이블, 파일 레코드, FIFO, 장치 식별 객체가 있는 모델을 준비합니다.
- 19개 기능 코드의 요청과 응답 바이트를 확인합니다.
- 진단 카운터, 청취 전용 모드와 복구, 브로드캐스트, 주소가 다른 요청의 처리를 확인합니다.
- 예외 01, 02, 03이 나는 요청을 구분합니다.
준비물
섹션 제목: “준비물”- PDComm Modbus SDK와 환경 변수
MODBUS_SDK(빠른 시작 참고) - 터미널 하나
1. 모델을 준비합니다
섹션 제목: “1. 모델을 준비합니다”헤드리스 시뮬레이터를 시작합니다.
"$MODBUS_SDK/bin/pdcomm-modbus-headless"RTU 서버로 구성하고 모델 전체를 바꿉니다. 각 명령은 한 줄로 입력합니다.
{"action":"configure","config":{"transport":"rtu","role":"server","address":"127.0.0.1:0","port":"","unit":1,"baud":19200,"parity":"even","stop_bits":1,"timeout_ms":1000}}{"action":"model_set","model":{"coils":{"start":0,"values":[0,0,0,0,0,0,0,0]},"discrete_inputs":{"start":0,"values":[1,0,1,0,0,0,0,0]},"holding_registers":{"start":0,"values":[42,4660,0,0]},"input_registers":{"start":0,"values":[123,0,0,0]},"files":{"1":[0,0,0,0]},"fifos":{"0":[18,52]},"objects":{"0":[80,68,67],"1":[77,66],"2":[49]},"server_id":[1,255],"exception_status":5,"individual_access":true}}model_set의 결과는 {"ok":true,"result":{"updated":true}}입니다. 이 모델은 다음과 같습니다.
| 항목 | 값 |
|---|---|
| 코일 0~7 | 모두 0 |
| 이산 입력 0~7 | 1, 0, 1, 0, 0, 0, 0, 0 |
| 보유 레지스터 0~3 | 42, 4660(0x1234), 0, 0 |
| 입력 레지스터 0~3 | 123, 0, 0, 0 |
| 파일 1 | 레코드 0~3, 모두 0 |
| FIFO 0 | 18, 52 |
| 장치 식별 객체 | 0 = PDC, 1 = MB, 2 = 1 |
| 서버 ID | 01 FF(ID 1, 운전 중) |
| 예외 상태 | 5 |
model_set은 모든 필드가 있어야 합니다. 빠지면missing field `discrete_inputs`처럼 실패합니다.- 비트 테이블에 2 같은 값을 넣으면
Bit table values must be zero or one으로 실패합니다. - 장치 식별 객체 0, 1, 2는 반드시 있어야 하고, 예약 범위 7~127은 쓸 수 없습니다. 어기면
Invalid file, FIFO or device identification model입니다. - 실패한
model_set은 기존 모델을 바꾸지 않습니다.
2. 19개 기능 코드를 실행합니다
섹션 제목: “2. 19개 기능 코드를 실행합니다”다음 표의 요청을 위에서부터 차례로 실행합니다. 명령 형식은 {"action":"execute","pdu":"<요청 PDU>"}입니다. 응답은 10진수 바이트 배열이며 순서대로 실행해야 같은 값이 나옵니다.
| 순서 | 기능 | 요청 PDU | 응답 response |
설명 |
|---|---|---|---|---|
| 1 | 01 Read Coils | 0100000001 |
[1,1,0] |
코일 0은 0입니다. |
| 2 | 02 Read Discrete Inputs | 0200000003 |
[2,1,5] |
1, 0, 1이 0b101로 묶였습니다. |
| 3 | 03 Read Holding Registers | 0300000001 |
[3,2,0,42] |
|
| 4 | 04 Read Input Registers | 0400000001 |
[4,2,0,123] |
|
| 5 | 05 Write Single Coil | 050000FF00 |
[5,0,0,255,0] |
요청을 그대로 돌려줍니다. |
| 6 | 01 Read Coils | 0100000001 |
[1,1,1] |
코일 0이 켜졌습니다. |
| 7 | 06 Write Single Register | 0600000009 |
[6,0,0,0,9] |
|
| 8 | 03 Read Holding Registers | 0300000001 |
[3,2,0,9] |
|
| 9 | 07 Read Exception Status | 07 |
[7,5] |
모델의 exception_status |
| 10 | 08 Diagnostics, 하위 0 | 0800001234 |
[8,0,0,18,52] |
데이터를 그대로 돌려줍니다. |
| 11 | 11 Get Comm Event Counter | 0B |
[11,0,0,0,9] |
상태 0000(바쁘지 않음), 이벤트 수 9 |
| 12 | 12 Get Comm Event Log | 0C |
[12,29,0,0,0,9,0,12,128,64,...] |
바이트 수, 상태, 이벤트 수, 메시지 수, 최근 이벤트 |
| 13 | 15 Write Multiple Coils | 0F000000010100 |
[15,0,0,0,1] |
코일 0에 0을 씁니다. |
| 14 | 16 Write Multiple Registers | 100000000102000A |
[16,0,0,0,1] |
보유 레지스터 0에 10을 씁니다. |
| 15 | 17 Report Server ID | 11 |
[17,2,1,255] |
바이트 수 2, 모델의 server_id |
| 16 | 20 Read File Record | 140706000100000001 |
[20,4,3,6,0,0] |
파일 1, 레코드 0, 1워드 |
| 17 | 21 Write File Record | 1509060001000000010009 |
요청과 같음 | 파일 1, 레코드 0에 9를 씁니다. |
| 18 | 20 Read File Record | 140706000100000001 |
[20,4,3,6,0,9] |
쓴 값이 보입니다. |
| 19 | 22 Mask Write Register | 160000FF000055 |
요청과 같음 | (10 AND FF00) OR (0055 AND NOT FF00) = 85 |
| 20 | 03 Read Holding Registers | 0300000001 |
[3,2,0,85] |
|
| 21 | 23 Read/Write Multiple Registers | 170000000200000001020009 |
[23,4,0,9,18,52] |
레지스터 0에 9를 쓴 뒤 0~1을 읽습니다. |
| 22 | 24 Read FIFO Queue | 180000 |
[24,0,6,0,2,0,18,0,52] |
바이트 수 6, 개수 2, 값 18과 52. 읽어도 큐는 그대로입니다. |
| 23 | 43/14 Read Device Identification | 2B0E0100 |
[43,14,1,129,0,0,3,0,3,80,68,67,1,2,77,66,2,1,49] |
기본(basic) 객체 0~2 |
| 24 | 43/14, 개별 접근 | 2B0E0401 |
[43,14,4,129,0,0,1,1,2,77,66] |
객체 1만 |
몇 가지 응답을 풀어 보면 다음과 같습니다.
- 11 Get Comm Event Counter: 이벤트 수는 진단 관리 기능(08, 11, 12)을 뺀 정상 처리 메시지 수입니다. 앞선 9개 요청 가운데 08을 뺀 값입니다.
- 43/14 응답:
14 01은 MEI 14, 읽기 코드 1입니다.129(0x81)는 적합성 수준(conformity level)으로, 기본 수준 1에 개별 접근 지원 비트0x80이 붙었습니다. 그 뒤0 0은 이어지는 페이지 없음과 다음 객체 ID,3은 객체 수입니다. 이어서 객체마다 ID, 길이, 값이 옵니다.
3. 진단 카운터를 확인합니다
섹션 제목: “3. 진단 카운터를 확인합니다”진단 레지스터에 시험 값 0x1234를 넣고 하위 기능을 실행합니다.
{"action":"diagnostics_set","register":4660,"busy":false}| 요청 PDU | 응답 | 설명 |
|---|---|---|
0800020000 |
[8,0,2,18,52] |
진단 레지스터 0x1234 |
08000B0000 |
[8,0,11,0,27] |
버스 메시지 수. 이 세션의 요청 수에 따라 다릅니다. |
08000E0000 |
[8,0,14,0,28] |
이 서버 앞으로 온 메시지 수 |
08000D0000 |
[8,0,13,0,1] |
보낸 예외 응답 수 |
08000A0000 |
[8,0,10,0,0] |
카운터와 진단 레지스터를 지웁니다. |
08000B0000 |
[8,0,11,0,1] |
지운 뒤 이 요청 하나가 셌습니다. |
0800030A00 |
[8,0,3,10,0] |
ASCII 종료 문자를 LF(0x0A)로 정합니다. 응답은 바꾸기 전 종료 문자로 보냅니다. |
0800140000 |
[8,0,20,0,0] |
오버런 카운터와 플래그를 지웁니다. |
카운터는 16비트이며 65535 다음은 0입니다. 값은 실제 처리한 요청에 따라 달라지므로 고정 숫자보다 증가와 초기화를 확인합니다. 전체 카운터와 이벤트 로그는 {"action":"diagnostics"}로 읽습니다.
4. 청취 전용 모드에 들어갔다가 복구합니다
섹션 제목: “4. 청취 전용 모드에 들어갔다가 복구합니다”{"action":"execute","pdu":"0800040000"}{"action":"execute","pdu":"060000002A"}{"action":"diagnostics"}{"ok":true,"result":{"response":null,"source":"local","status":"no_response"}}{"ok":true,"result":{"response":null,"source":"local","status":"no_response"}}{"ok":true,"result":{"counters":{...,"no_responses":2,...},...,"listen_only":true,"register":0}}- 청취 전용 모드 진입 요청에는 응답이 없습니다(
no_response). - 그 뒤의 쓰기 요청도 응답이 없고 적용되지도 않습니다.
재시작 요청으로 복구합니다. 이 요청도 응답이 없습니다.
{"action":"execute","pdu":"0800010000"}{"action":"diagnostics"}{"action":"execute","pdu":"0300000001"}{"ok":true,"result":{"response":null,"source":"local","status":"no_response"}}{"ok":true,"result":{"counters":{"bus_messages":0,...},...,"listen_only":false,"register":0}}{"ok":true,"result":{"response":[3,2,0,9],"source":"local","status":"success"}}listen_only가 false로 돌아왔고 카운터가 지워졌습니다. 청취 전용 중 보낸 42는 적용되지 않았으므로 값은 9입니다. 실제 직렬 회선에서는 복구 요청에도 응답이 없으므로, 시간 초과만 보고 복구에 실패했다고 판단하지 않습니다. 복구 뒤 읽기 요청으로 확인합니다.
5. 브로드캐스트와 다른 주소를 확인합니다
섹션 제목: “5. 브로드캐스트와 다른 주소를 확인합니다”{"action":"execute","unit":0,"pdu":"060000002A"}{"action":"execute","pdu":"0300000001"}{"action":"execute","unit":5,"pdu":"0300000001"}{"ok":true,"result":{"response":null,"source":"local","status":"no_response"}}{"ok":true,"result":{"response":[3,2,0,42],"source":"local","status":"success"}}{"ok":true,"result":{"response":null,"source":"local","status":"no_response"}}- 유닛 0의 쓰기는 브로드캐스트입니다. 응답은 없지만 값은 바뀌었습니다.
- 유닛 5는 이 서버(유닛 1)의 주소가 아니므로 응답하지 않습니다. 직렬 버스의 다른 서버 앞으로 온 요청을 무시하는 동작입니다.
- 브로드캐스트로 읽기를 보내면 값을 얻을 수 없습니다. 브로드캐스트는 쓰기에만 씁니다. RTU·ASCII 클라이언트에서 유닛 0으로 읽기를
request하면 PDComm은 보내지 않고InvalidConfig오류를 돌려줍니다.
6. 예외 응답을 구분합니다
섹션 제목: “6. 예외 응답을 구분합니다”| 요청 PDU | 응답 | 예외 | 이유 |
|---|---|---|---|
0300000000 |
[131,3] |
03 | 수량 0은 허용 범위(1~125) 밖입니다. |
0F0000000101 |
[143,3] |
03 | 바이트 수 1을 적었지만 데이터 바이트가 없습니다. |
03FFFF0001 |
[131,2] |
02 | 주소 65535는 모델에 없습니다. |
1800FF |
[152,2] |
02 | FIFO 255는 모델에 없습니다. |
2B0D00 |
[171,1] |
01 | MEI 13은 기본 서버가 처리하지 않습니다. |
5A |
[218,1] |
01 | 기능 코드 90은 지원하지 않습니다. |
마지막으로 같은 진단 요청을 TCP 구성에서 실행해 봅니다.
{"action":"disconnect"}{"action":"configure","config":{"transport":"tcp","role":"server","address":"127.0.0.1:0","port":"","unit":1,"baud":19200,"parity":"even","stop_bits":1,"timeout_ms":1000}}{"action":"execute","pdu":"0800001234"}{"ok":true,"result":{"response":[136,1],"source":"local","status":"exception"}}직렬 전용 기능은 TCP 서버에서 예외 01입니다. 직렬 전용 기능을 시험하려면 RTU나 ASCII 구성으로 돌아갑니다.
7. 정리합니다
섹션 제목: “7. 정리합니다”{"action":"shutdown"}을 입력해 끝냅니다. 이 튜토리얼의 명령에는 서비스 오류가 없으므로 종료 코드는 0입니다. 같은 모델로 다시 시작하려면 1단계의 두 명령을 새 세션에 다시 입력하거나, 프로젝트 파일로 저장해 둡니다.
다음 단계
섹션 제목: “다음 단계”- RTU와 ASCII 통신에서 같은 요청을 직렬 회선으로 보냅니다.
- 기능 코드별 최대 수량과 명세 절은 PDU, ADU와 기능 코드에 있습니다.