음.. 설계에서 가장 먼저 다뤄 볼 건 래더다. PLC의 가장 근본이 되는 부분이기도 하고 유독 특성이 다른 부분이기 때문에 가장 먼저 짚고 넘어가도록 한다.

래더 분석 원리

래더는 회로다. 기호 늘어놓은 그림 같지만 사실 회로. 왼쪽 전원에서 오른쪽 출력까지 통하느냐 안 통하느냐. 결국 그걸 보는 거다.

래더 분석 예시 회로 - M0~M3 접점에서 P0·P1 출력까지의 경로

래더 분석 원리를 간단하게 풀어보면 이렇다.

  1. 모든 시작 노드에서 DFS로 경로 트리를 만든다.
M0-M1-M10-(P0)
M0-M1-M10-M11-(P1)
M2-M10-(P0)
M2-M10-M11-(P1)
M3-(P0)
M3-M11-(P1)
  1. 경로 목록을 출력 노드 위치 기준으로 LOOKUP 한다.
[P0 (1,10)]
M0-M1-M10-(P0)
M2-M10-(P0)
M3-(P0)

[P1 (3,10)]
M0-M1-M10-M11-(P1)
M2-M10-M11-(P1)
M3-M11-(P1)
  1. LOOKUP된 각 경로 목록은 전부 AND.
[P0 (1,10)]
R1 = M0 & M1 & M10
R2 = M2 & M10
R3 = M3

[P1 (3,10)]
R4 = M0 & M1 & M10 & M11
R5 = M2 & M10 & M11
R6 = M3 & M11
  1. 출력마다 AND 결과를 전부 OR 하면 출력 결과.
P0 = R1 | R2 | R3
P1 = R4 | R5 | R6

위 처리를 보면 쉽게 이해할 수 있다. 출력 노드 위치를 기준으로 LOOKUP을 수행하면 해당 위치로 종료하는 모든 경로를 취득할 수 있다. 한 경로의 노드를 전부 AND 한 결과가 참이면, 그 경로가 도통했다는 뜻이다. 그리고 그 모든 경로 중 하나라도 참이면, 회로적으로 연결된 것. 즉, 출력이 켜진 상태인 것이다.

NOT이나 엣지(상승·하강)는 좀 다르다. 특히 엣지는 이전 스캔 상태를 들고 있어야 하는 순차 요소라 따로 처리한다.

스캔 실행 모델

두 개의 루프가 분리되어 구동한다. 결정적인 래더 루프, 자유로운 C# 루프. 별도 Task라 서로의 타이밍을 막지 않는다. 단 둘은 상호 운용된다. 래더가 쓴 메모리 값을 C#이 읽고 C#에서 정의한 메소드(함수)는 래더에서 호출할 수 있다. 따로 돌지만 한 몸으로 묶인 셈이다.

래더 루프

입력 → 래더 로직 → 출력 순으로 10ms마다 도는 래더 루프

  • 입력 → 래더 루프 → 출력. 입력을 한 번에 모아 읽고 로직을 수행하고 결과를 한 번에 내보낸다.
  • 입력이 루프 시작에 고정되니 수행 도중 값이 바뀌지 않는다. 레이스 없음, 동작 예측 가능. 동시성을 시간으로 직렬화한 구조다.
  • 10ms 고정: 매 루프 sleep으로 쉬면 처리가 길어질 때 주기가 밀린다(드리프트). 그래서 경과 시간을 재 10ms 격자에 맞춰 밀린 만큼 따라잡는다. 부하가 흔들려도 10ms에 고정된다.

C# 루프

Setup 한 번 뒤 Loop를 반복하는 아두이노식 C# 루프

  • 아두이노와 같은 구조. Setup → Loop.
  • Setup: 시작에 한 번 도는 초기화 루틴.
  • Loop: 계속 반복 호출되는 루프. 호출 간격은 Loop 안에서 직접 정한다. 래더처럼 고정 주기가 아니라 자유롭게 잡는다.

.NET과 라즈베리파이

래더 실행 모델은 그대로 뒀다. 그 위에 .NET을 얹었다. 왜 .NET이고, 왜 라즈인가.

.NET ↔ PLC — 약점을 서로 메운다

  • PLC는 결정성·스캔·실시간이 강하다. 정해진 주기에 정해진 순서로 늘 같게 돈다는 뜻이다. 대신 폐쇄적이고 확장이 어렵고 벤더에 묶인다.
  • .NET은 생태계·확장·도구가 강하다. 남이 만든 기능을 가져다 붙이기 쉽다. 대신 메모리 정리(GC) 같은 게 중간에 끼어들어 실행 타이밍이 흔들릴 수 있어 단단한 실시간엔 약하다는 통념이 있다.
  • 각자의 약점을 상대의 강점으로 덮었다. .NET의 타이밍 흔들림은 빌드 때 굳혀(래더 다운로드처럼) 막고 PLC의 폐쇄성은 C#으로 연다.

왜 라즈베리파이

  • 하드웨어 확장·커스텀이 쉽다. GPIO·보드·주변장치를 자유롭게 붙이고 바꾼다.
  • 리눅스 기반이라 다루기 쉽다. 익숙한 환경·도구로 개발하고 배포한다.
  • 자료가 많다. 막히면 찾을 게 있고 다양한 시도를 해볼 여지가 넓다.

시스템 구성

에디터·AI·런타임·I/O 보드·외부 장비로 이루어진 Senbrix 시스템 구성도

에디터·AI·런타임이 이 시스템의 핵심 구성요소다. 이 구성요소들은 각각 작성, 지원, 구동을 담당하고 있고 작성·지원·구동은 단방향이 아니라 서로 주고받는 고리다.

  • 에디터 — 사람이 래더와 C#을 짜는 작업 공간. 빌드한 결과를 런타임에 배포하고 런타임의 구동 상태를 되받아 모니터한다. AI도 MCP로 여기에 붙는다.
  • 런타임 — 타깃에 상주하며 빌드된 결과를 받아 스캔 루프로 돌린다. PLC의 ‘실행’을 맡는 본체.
  • AI — 에디터에 붙어 래더·코드 생성, 개발 진행 보고, 진단, 수정까지 거든다. 설계부터 운영 전반.
  • I/O 보드 — 현장 센서·액추에이터를 읽고 쓴다. CAN으로 런타임에 붙는 입출력 확장.
  • 외부 장비 — Modbus로 묶이는 계측기·다른 컨트롤러 등. 런타임이 통신으로 값을 주고받는다.

프로그램 구조

에디터와 런타임이 Senbrix.Controller 코어를 공유하고 보드·통신은 플러그인으로 붙는 프로그램 구조도

시스템 구성이 “누가 누구와 엮이나”였다면, 코드로 들어가면 핵심은 하나다. 에디터와 런타임이 각자 기능을 얹되, 하나의 코어를 공유한다.

  • 공유 코어 (Senbrix.Controller) — 메모리·심볼, 래더·분석, 실행 모델, 코드 생성, 플러그인 계약이 다 여기 있다. 앞서 푼 도통 경로·스캔 모델도 이 코어의 일부다. 두 앱이 같은 코어를 쓰니 에디터에서 짠 것과 런타임에서 도는 게 어긋날 수 없다. 래더와 C#이 한 프로그램처럼 묶이는 기반이다.
  • 확장은 플러그인 — 현장마다 IO 보드와 통신 프로토콜이 제각각이다. 코어에 다 넣으면 현장 하나 늘 때마다 코어를 다시 빌드해야 한다. 그래서 보드·통신은 떼어내 플러그인으로 뒀다. 계약만 맞추면 런타임이 끼워 로드하고 코어는 손대지 않는다.

프로그램간 통신

AI·에디터·런타임·현장 사이의 MCP, HTTP, TextComm, mDNS, CAN, Modbus 통신 경로

위에서 봤듯 에디터·AI·런타임은 핵심 구성요소이고 상호 호출 관계로 시스템 기능을 수행한다. 같은 컴퓨터 안이면 그냥 호출, 머신 경계를 넘는 선에만 프로토콜과 포트가 붙는다.

  • AI ↔ 에디터 (MCP
    ) — 래더·코드 생성, 빌드, 진행 보고. 설계와 작성을 거든다.
  • AI ↔ 런타임 — 래더 그래프를 기준으로 구동 상태를 읽고 제어하고 진단한다. 단 AI가 런타임을 직접 건드리는 데 보안 문제가 있어 손볼 곳으로 남겨뒀다.
  • 에디터 ↔ 런타임 — 기능과 목적에 따라 통신 형식을 분리했다.
    • 배포 (HTTP
      ): 빌드 산출물(ladder.json + DLL)을 올린다.
    • 모니터 (TextComm
      ): 실행 값 스냅샷을 받는다.
    • 발견 (mDNS): 장치명과 포트를 광고하고 찾는다.
  • 런타임 ↔ 현장 — CAN으로 I/O 보드 입출력을, Modbus로 외부 장비 값을 읽고 쓴다.

다음 편 — C#과 래더

본 편에서 설계를 다뤘다면, 다음 편부터는 실제로 어떻게 구현했고 구현된 모습은 어떤지를 다룬다. 구현의 첫 번째로 다룰 건 C#과 래더다. 한 프로그램 안에서 둘이 어떻게 묶이고 어떻게 호출하는지 이야기 해보겠다.

Senbrix 에디터 초기 화면 Senbrix 에디터의 래더 편집 화면