미쓰비시 PLC에서 CRC-16/MODBUS 체크코드를 XCALL 재사용 함수로 만들어, MODBUS RTU 시리얼 통신마다 매번 새로 짜지 않고 갖다 쓰는 방법을 다룹니다.
CRC-16 은 MODBUS RTU 교신에서 빠지지 않고 등장해 많은 사람의 골머리를 썩게 만드는 주제입니다.
한 번 함수로 만들어 두면 국번·어드레스만 바꿔 어느 유닛에서든 재사용할 수 있습니다.

1. 왜 CRC-16 을 함수로 만드는가
PLC는 그림 그리듯 래더라는 도식화된 언어와 메이커가 제공하는 펑션·명령어로 프로그램하는 뛰어난 자동화 제어장치입니다.
하지만 단점이 있습니다.
설비 규모가 커질수록 같은 동작의 프로세스라도 여러 유닛에 맞춰 어드레스를 일일이 변경하거나 복사해가며 번거롭게 코딩해야 한다는 점입니다.
PLC에서도 간단한 기능은 CALL 이나 XCALL 문으로 함수화할 수 있습니다.
이를 활용하면 프로그램이 훨씬 간단해집니다.
이번에는 MODBUS RTU 시리얼 통신마다 등장하는 CRC-16 체크코드를 생성하는 함수를 다룹니다.
사실 미쓰비시 PLC에서는 Q UDV 타입부터 CRC-16/MODBUS 명령어(CRC)가 신규 추가되어 명령어 펑션을 쓰거나, 사전정의프로토콜(Predefined Protocol) 기능이 제공하는 MODBUS 라이브러리를 활용해도 충분합니다.
다만 CRC-16 코드를 꼭 MODBUS 에서만 쓰는 것은 아니기에, XOR 다항식이 MODBUS 전용이 아니라 커스텀할 수 있도록 함수화했습니다.
CRC-16 이 어떻게 만들어지는가(나눗셈·XOR·시프트 생성 원리)가 궁금하다면 Modbus 입문 가이드 2편: CRC-16 생성 원리와 수신 데이터 파싱 을 먼저 읽으면 좋습니다. 그 글이 "생성 원리", 이 글이 "GX Works2 래더 구현과 재사용 함수화" 를 담당합니다.
CRC-16(순환 중복 검사)의 개념은 위키백과 설명을 참고하면 이해에 도움이 됩니다.
2. 함수 구조와 파라미터
이전 함수화 편에서 다룬 체크섬 코드 생성과 마찬가지로, CRC-16 도 송수신 데이터 프레임을 다루는 기능이다 보니 데이터 레지스터 영역의 소모량이 꽤 많습니다.
따라서 XCALL 에서 기본 사용 가능한 인수(FX, FY, FD)를 제한적으로 쓸 수밖에 없습니다.
이를 극복하기 위해 프로그램의 일부 영역을 함수 전용 영역으로 설정했습니다.
본 예제에서는 통신 영역을 D0200번대, 함수 전용 영역을 D0900·ZR0900번대로 사용합니다.
실제 설비에 적용할 때는 사용 중인 디바이스와 겹치지 않는 빈 영역으로 바꿔 주면 됩니다.


함수의 인수와 작업 영역은 다음과 같습니다.
| 구분 | 디바이스 | 역할 |
|---|---|---|
| 1번째 파라미터 | FD0 | CRC-16 생성 시 문자열 참조 시작 포인트 |
| 2번째 파라미터 | FD1 | CRC-16 생성 시 문자열 참조 종료 포인트 |
| 3번째 파라미터 | FD2 | CRC-16 생성 결과 리턴값 |
| 입력 영역 | ZR0900 | 원본 문자열 (입력값) |
| 작업 버퍼 | D0910 | 문자열을 바이트 캐릭터 단위로 분리 저장 (WTOB 버퍼) |
| 결과 영역 | D0990 | CRC-16 코드 생성 결과값 |
3. 사용 예제: LS 인버터 G100 MODBUS RTU
LS 인버터 G100 모델에 RS-485 MODBUS RTU 로 지령주파수 60.00Hz 를 쓰는 경우를 예로 들어 봅니다.

1번국 인버터에 1속 주파수(주소 0x1D05)를 60.00Hz 로 설정하는 예시입니다.
송신할 전체 프레임은 0x01 06 1D04 1770 <CRC-16> 이 됩니다.
1번국에 쓰기 펑션(06)으로 0x1D05 어드레스에 0x1770(60.00Hz)을 쓰겠다는 의미입니다.
주소값을 쓸 때는 항상 1을 빼야 하기 때문에 0x1D05 를 바로 쓰지 않고 1을 뺀 0x1D04 를 씁니다.
위 샘플 래더를 보면 하위 바이트부터 상위로 프레임을 작성한 것을 확인할 수 있습니다.
여기서 [MOV H1D04 D5201] 부분을 주목합니다.
PLC 시리얼 통신을 바이트 단위로 설정했기 때문에, 이대로 송신하면 원래 보내려던 0x1D04 가 아니라 0x041D 로 뒤집혀 송신됩니다.
이를 보정하기 위해 스왑(swap)을 해 줍니다.
그래서 최종 프레임은 다음과 같이 됩니다 — 0x01 06 1D04 1770 <CRC-16>

이 송신 프레임을 소스로 CRC-16 코드만 생성해 실제로 송신하면 인버터의 1속값이 60.00Hz 로 설정될 것으로 기대됩니다.
이 데이터를 ZR0900 으로 옮긴 뒤 XCALL 로 펑션을 불러와 실행하면 CRC-16 코드가 생성됩니다.

XCALL 펑션으로 1번 문자부터 6번 문자까지 참조하고, MODBUS 용 다항식 XOR 코드 0xA001 을 이용해 CRC-16 코드를 생성하여 D0203 에 저장합니다.

결과는 리턴값 D0203 에 CRC-16 이 생성되어 0xC073 이 저장되는 것을 확인할 수 있습니다.
4. CRC 계산기로 결과 검증
생성된 값이 맞는지 CRC-16 계산기 웹페이지로 검증해 봅니다.
Online CRC-8 CRC-16 CRC-32 Calculator (crccalc.com)

계산기 결과와 PLC 생성값이 일치하니 정상적으로 생성된 것을 확인할 수 있습니다.
MODBUS 는 상대 기기에 따라 CRC-16 생성 코드를 그대로 송신해야 교신되는 경우와 스왑해야 교신되는 경우가 있습니다.
또한 통신 프로토콜에 따라 CRC 생성 알고리즘이 달라, 다항식 코드를 0x8005 그대로 쓰는 경우(LSB 우선)와 뒤집은 0xA001 을 쓰는 경우(MSB 우선)가 나뉩니다.
통신 전 상대 기기가 사용하는 CRC 알고리즘을 반드시 확인하고 그에 맞춰 생성해야 합니다.
5. 함수 내부 동작: 래더는 CRC-16 을 어떻게 만드는가
함수를 갖다 쓰는 데는 내부 구현을 몰라도 됩니다.
다만 CRC 값이 틀어질 때 디버깅하려면 래더가 안에서 무슨 일을 하는지 알아두면 좋습니다.
CRC-16 의 생성 원리(자리올림 없는 나눗셈 = XOR·시프트) 자체는 Modbus 입문 가이드 2편 에서 손계산까지 다뤘으므로, 여기서는 그 원리를 GX Works2 래더 명령어로 옮기는 부분만 정리합니다.
두 겹 FOR~NEXT 루프
래더에서는 FOR~NEXT 루프를 두 겹으로 사용합니다.
- 외부 루프 — 참조 시작 바이트부터 끝 바이트까지, 바이트 수만큼 반복. 각 바이트를 CRC 레지스터 하위 바이트(Low Byte)와 XOR 한 뒤 내부 루프를 호출합니다.
- 내부 루프 —
K8고정, 비트 시프트를 8회 반복합니다.
스캔타임 주의 — 참조 데이터가 길수록 루프 횟수가 늘어납니다. 예를 들어 25바이트 기준으로 외부 23회 × 내부 8회 = 184회 반복입니다. 처리량이 수십 바이트 이상이면 스캔타임 영향을 확인하고, 필요하면 여러 스캔에 분산하는 것도 고려하세요.
의사코드 → Q-series 실제 IL 매핑
일반 CRC-16 의사코드에 등장하는 SHR(우시프트) 와 AND 0x0001(bit0 검사) 은 Mitsubishi Q-series 에 그대로 존재하지 않습니다.
다음 두 가지로 매핑해야 합니다.
| 의사코드 | Q-series 실제 IL | 동작 |
|---|---|---|
CRC SHR 1 (우시프트 1회) |
SFR D K1 |
D 를 우측으로 1비트 시프트. 밀려난 bit0 이 캐리 플래그 SM700 에 보존됨 |
CRC AND 0x0001 (bit0 검사) |
AND SM700 |
직전 SFR 로 밀려난 bit0 을 SM700 으로 읽음 |
AND K1 으로 bit0 을 검사하려는 시도는 Q-series 에서 K1 을 상수가 아니라 접점 번호(M1·X1 등과 같은 디바이스)로 해석해 항상 ON 이 됩니다. CRC 계산이 통째로 틀어지는 대표적 함정이므로, 반드시 SFR 직후의 캐리 플래그 SM700 으로 분기하세요.
SM700 캐리 플래그로 bit0 분기
CRC 레지스터에 SFR 우시프트를 실행하면 두 가지가 동시에 일어납니다.
- CRC 레지스터 자체가 오른쪽으로 1비트 이동합니다.
- 밀려난 bit0(시프트 전의 최하위 비트)이 시스템 캐리 비트
SM700에 보존됩니다.
따라서 시프트 직후에 SM700 을 읽으면 시프트 전 bit0 값을 알 수 있고, 이 값으로 "다항식 0xA001 을 XOR 할지, 시프트만 유지할지" 를 분기합니다.
입문 2편에서 손으로 따라간 첫 두 회차를 SFR·SM700 관점으로 그대로 옮기면 비트가 어떻게 움직이는지 보입니다.
[회차1] bit0 = 0
1111 1111 1111 1110 (0xFFFE) 시프트 전
0111 1111 1111 1111 (0x7FFF) SFR 후, SM700 = 0 -> 시프트만 유지
[회차2] bit0 = 1
0111 1111 1111 1111 (0x7FFF) 시프트 전
0011 1111 1111 1111 (0x3FFF) SFR 후, SM700 = 1 -> 0xA001 XOR
1010 0000 0000 0001 (0xA001) XOR
1001 1111 1111 1110 (0x9FFE) 결과
SFR 가 밀어낸 bit0 이 곧 SM700 값이고, 그 값에 따라 다음 동작(XOR 또는 시프트 유지)이 갈리는 흐름이 회차2에 그대로 나타납니다.
두 회차의 결과(0x7FFF·0x9FFE)는 입문 2편 손계산 표의 1·2회차 결과와 정확히 일치합니다.
[비트 처리 회차 < 8]───────────────[ CRC 우시프트 1회 (밀려난 bit0 → 캐리 비트) ]
[비트 처리 회차 < 8]─[캐리 비트 ON]──────────( 다항식 0xA001 XOR )
[비트 처리 회차 < 8]─[캐리 비트 OFF]─────────( 시프트만 유지 )
SM700 은 시프트·로테이트 명령(SFR/SFL/RCR/RCL) 실행 직후의 캐리 상태를 보존하는 시스템 비트이므로, 다음 시프트 명령이 실행되기 전에 값을 확인하고 분기해야 합니다.
시프트가 한 번 더 실행되면 SM700 값이 새로 덮어써져 이전 값은 사라집니다.
입력 반전(RefIn) 규칙에 따라 시프트 방향이 갈립니다. LSB-first(RefIn=True, MODBUS)는 우시프트 SFR 로 bit0 을, MSB-first(RefIn=False)는 좌시프트 SFL 로 bit15 를 SM700 에 캡처합니다. 다항식·초기값과 함께 이 방향까지 가변 지원하는 범용 버전은 함수화 7편: 모든 CRC-16 코드 생성 이 MSB용 P4003·LSB용 P4004 두 펑션으로 나눠 둔 이유입니다.
마무리
MODBUS RTU 교신마다 따라붙던 CRC-16 코드를 한 번 함수로 만들어 두면, 이후로는 국번·어드레스만 바꿔 어디서든 재사용할 수 있습니다.
이 함수는 MODBUS 전용 다항식(0xA001)에 맞춰져 있습니다.
CCITT·XMODEM·ARC 등 다른 다항식까지 가변으로 지원하는 범용 CRC-16 함수는 함수화 7편: 모든 CRC-16 코드 생성 에서 이어집니다.
CRC-16 의 생성 원리(나눗셈·XOR·시프트)는 Modbus 입문 가이드 2편 에서 확인할 수 있습니다.
'PLC 전기제어 기술자료 > PLC를 함수화 해보자!' 카테고리의 다른 글
| PLC 범용 CRC-16 함수화 : 다항식 가변으로 CCITT·XMODEM·ARC까지 (함수화 7편) (0) | 2023.05.09 |
|---|---|
| 미쓰비시 PLC 랜덤 문자열 생성 함수화 : 테스트 데이터 만들기 (함수화 5편) (0) | 2022.04.26 |
| 미쓰비시 PLC 범위지정 랜덤값 생성 함수화 : RND 한계 극복 (함수화 4편) (0) | 2022.04.25 |
| 미쓰비시 PLC 체크섬 생성 함수화 : 시리얼 통신 데이터 검증 (함수화 3편) (1) | 2022.02.28 |
| 미쓰비시 PLC 문자열 스플릿 함수화 : 콤마 구분 데이터 분리 (함수화 2편) (1) | 2022.02.20 |
| 미쓰비시 PLC 스케일 변환 함수화 : XCALL로 아날로그 환산 재사용 (함수화 1편) (0) | 2022.02.19 |