8 분 소요

M7~M9를 거치며 충전 한 사이클, OCPP 서버 연동, 전력량계, TLS까지 붙였다. 기능은 돌아가는데 “왜 이렇게 돌아가는지”를 설명할 수 있느냐는 다른 문제다. 오늘은 새 기능 대신 지금 코드를 읽고 이해하는 날로 정했다.

단계 내용 이 글
1 시작 흐름: 무엇이 어떤 순서로 뜨는가 ✅
2 sc6-bsp.c 뼈대 ✅
3 sc6-bsp CP 판정 ✅
4 sc6-bsp 명령 처리 ✅
5 sc6-meter: Modbus → EVerest 다음 글
6 EVerest 내부: 충전 한 사이클의 상태 변화 다음 글
7 OCPP: 인증, 거래, MeterValues, 서버 끊김 다음 글

0. 큰 그림: 모두가 MQTT 브로커 하나에 붙는다

                         보드 내부 mosquitto (MQTT 브로커)
             ┌──────── 유닉스 소켓 /run/mosquitto/mosquitto.sock ────────┐
             │                    + TCP localhost:1883                    │
  EVerest manager + 모듈들 ──"everest/..."── (EVerest 내부, 모듈끼리)       │
     ├ evse_board_support_API (bsp_1) ══"everest_api/1/evse_board_support/bsp_1/..."══ sc6-bsp
     ├ powermeter_API (powermeter_1)  ══"everest_api/1/powermeter/powermeter_1/..."══ sc6-meter
     ├ EvseManager (충전 상태 머신)
     └ OCPP ══ wss ══ SteVe 서버   (이것만 MQTT가 아니라 WebSocket)
  • EVerest 모듈끼리는 everest/... 토픽으로 이야기한다. 접속 설정은 /etc/everest/settings-mqtt-uds.yaml(유닉스 소켓, 접두어 everest).
  • *_API 모듈이 EVerest 내부 인터페이스를 everest_api/1/<종류>/<모듈 이름>/{e2m,m2e}/<동작> 토픽으로 바꿔 준다. 우리 C 프로그램은 이 토픽만 알면 되고, EVerest 내부 구조는 몰라도 된다.
  • e2m = EVerest → 우리(명령), m2e = 우리 → EVerest(보고).
  • 토픽과 메시지 형식은 보드의 /usr/share/everest/EVerest_API/evse_board_support_API.yaml, powermeter_API.yaml에 있다.

EVerest는 우리 보드가 무엇인지 모른다

EVerest가 아는 것은 세 가지뿐이다.

아는 것 어디서
AC 충전, 230V, 차단기 32A 단상 구성 YAML
최대 32A, 최소 6A, 단상, 커넥터 종류 sc6-bsp가 보내는 m2e/capabilities
CP 상태, K1 켜짐/꺼짐, 비상정지, 전압·전류·전력량 sc6-bsp m2e/event, sc6-meter powermeter_values

STM32MP157인지, CP가 어느 ADC인지, K1이 어느 GPIO인지는 sc6-bsp와 sc6-meter만 안다. 보드가 바뀌면 이 두 파일만 고치면 된다.

충전 전류도 EVerest가 직접 정하지 않는다. EVerest는 한도(BSP 최대, 차단기, 서버 제한 중 가장 작은 값)를 PWM 듀티로 바꿔 보낼 뿐이다. 32A면 32 ÷ 0.6 = 53.3%다. 실제로 몇 A를 끌어갈지는 차의 탑재 충전기(OBC)가 듀티를 읽고 정한다.

반대로 말하면, EVerest는 BSP의 보고를 그대로 믿는다. sc6-bsp가 “PowerOn”이라고 하면 K1 접점이 정말 붙었는지 확인하지 않는다. 실제 확인은 BSP 몫이다.


1. 시작 흐름: run-m7-bsp.sh

자동 시작(M7-6)이 아직 보류라서 보드에서 ./run-m7-bsp.sh config-sc6-ocpp.yaml로 띄운다.

① mosquitto 확인 (13행)     꺼져 있으면 켬
② sc6-bsp 실행 (15~17행)     먼저 남아 있는 것을 pkill. 로그 /tmp/bsp.log
③ sc6-meter 실행 (19~24행)   YAML에 powermeter_API가 있을 때만. 로그 /tmp/meter.log
④ 1초 대기                   두 프로그램이 MQTT에 붙을 시간
⑤ 설정 DB 삭제 (32~33행)
⑥ manager 실행 (35~37행)     EVerest 본체. 스크립트는 여기서 멈춰 있음
⑦ trap (25행)                스크립트가 끝나면 sc6-bsp, sc6-meter도 kill
  • ⑤는 M8 때 밟은 함정이다. manager는 --db 파일이 있으면 YAML 대신 DB에 저장된 예전 설정으로 부팅한다. 그래서 매번 지운다. OCPP 거래 DB는 다른 파일이라 남는다.
  • ⑦에서 sc6-bsp는 종료 신호를 받으면 K1 OFF, CP F(-12V)로 만들고 끝난다. EVerest가 죽었는데 릴레이만 붙어 있는 일을 막는다.

manager는 따로 설치한 것이 아니다. everest-core 패키지에 들어 있다. 10/26 Yocto에 meta-everest 레이어를 붙여 처음 빌드했고, 11/04 이미지 레시피(meta-sc6/recipes-core/images/sc6-image.bb)의 IMAGE_INSTALL로 옮겼다. 반면 sc6-bsp와 sc6-meter는 아직 이미지에 없고 /home/root/m7/에 복사해 쓰고 있다.


2. EVerest 구성 YAML 읽는 법

YAML은 예전에 쓰던 INI와 하는 일이 같다. 다른 점은 들여쓰기로 계층을 몇 단계든 만들 수 있고 - 로 목록을 쓸 수 있다는 것이다. EVerest 구성은 “모듈 안에 배선, 배선 안에 목록”처럼 깊어서 YAML을 쓴다. 우리 프로그램 설정(sc6-bsp.default)은 단순해서 키=값 형식이다.

active_modules:
  connector_1:                # 이름: 내가 정함. 다른 모듈이 이 이름으로 찾아옴
    module: EvseManager       # 종류: EVerest 소스 modules/EVSE/EvseManager
    mapping:
      module:
        evse: 1               # 1번 충전구 것 (OCPP connectorId와 연결)
    config_module:            # 모듈 전체 설정. 쓸 수 있는 키는 모듈의 manifest.yaml에
      charge_mode: AC
    connections:              # 배선
      bsp:                    #   이 모듈에 필요한 것(requires) 이름
        - module_id: bsp_1    #   상대 모듈 이름
          implementation_id: main   # 상대가 제공하는 것(provides) 이름

connections가 핵심이다. 우리 파일(board/m7/config-sc6-ocpp.yaml)의 배선은 이렇다.

 connector_1 (EvseManager) ─ bsp ──────────────► bsp_1        ══ sc6-bsp
                           ─ powermeter_grid_side ► powermeter_1 ══ sc6-meter
 auth ─ token_provider ─► token_provider (고정 카드 SC6TEST01), ocpp (원격 시작)
      ─ token_validator ─► ocpp
 ocpp ─ evse_manager ─► connector_1,  evse_energy_sink ─► grid_connection_point (서버 전류 제한)
 energy_manager ─► grid_connection_point (fuse_limit_A 32, 단상) ─► connector_1
바꾸고 싶은 것 고칠 곳
차단기 정격 grid_connection_point → fuse_limit_A (+ sc6-bsp.default의 MAX_CURRENT_A)
서버 주소, 보안 ocpp가 가리키는 JSON 두 개
실제 RFID 리더 token_provider를 리더 모듈로 바꾸고 auth 배선 수정
충전구 2개 connector_2 + bsp_2 추가

3. sc6-bsp.c 뼈대

meta-sc6/recipes-evse/sc6-bsp/files/sc6-bsp.c, 892행. 주석 줄로 여섯 구역이 나뉜다.

행 구역 하는 일
1~47 머리말 핀 정리, 안전 원칙
49~220 설정 기본값 구조체 C, 설정 파일 읽기
222~388 하드웨어 PWM, K1, 비상정지·누전, ADC
390~546 상태 상태 변수, MQTT 발행, K1 판단, 비상정지 처리
548~629 CP 판정 ADC → 전압 → A~E
631~746 MQTT EVerest 명령 받기
748~892 main 시작 → 반복 → 종료

설정: 기본값 위에 덮어쓰기

코드 안에 기본값(static struct cfg C, 92행)이 있고, cfg_load()(189행)가 /etc/default/sc6-bsp를 한 줄씩 읽어 = 앞뒤로 나눈 뒤 cfg_set()(154행)에서 키 이름을 strcmp로 비교해 넣는다. 파일이 없어도 기본값으로 돈다. 비상정지(EM_)와 누전(RCD_)은 IN[] 배열(84행)에 같은 모양으로 들어 있어 코드 하나로 둘을 다룬다.

하드웨어는 전부 “파일”

하드웨어 리눅스에서 보이는 모습
CP PWM (PA15, TIM2, 1 kHz) /sys/class/pwm/pwmchip0/pwm0/duty_cycle에 ns 단위 숫자 쓰기
CP 전압 /sys/bus/iio/devices/iio:device0/in_voltage0_raw 읽기
K1 (PB8) /dev/gpiochip1 8번 라인 (libgpiod)
비상정지 PH14, 누전 PH15 /dev/gpiochip7 14·15번 라인

듀티 53.3%는 duty_cycle에 533000을 쓰는 것으로 끝난다. 입력은 libgpiod의 active_low 설정에 극성을 맡겨서, 코드에서는 항상 “1 = 이상”으로 읽는다. NC 버튼이든 반대 극성이든 설정 EM_ACTIVE 한 줄로 바뀐다.

K1 판단은 한 곳에만 있다

/* relay_apply(), 452행 */
bool want = connected && enabled && allow_power_on && cd && !any_fault();
//          MQTT 연결   사용 허락   켜기 허락      CP C/D  비상정지·누전 없음

명령이 오든 CP가 바뀌든 비상정지가 눌리든, 이 함수를 다시 부르면 된다. 하나라도 거짓이면 꺼진다. 안전 판단을 한 함수에 모아 둔 것이 이 코드의 중심이다.

K1에는 보조 접점 피드백이 없다. 그래서 켜고 끄는 명령을 내린 즉시 PowerOn/PowerOff를 보고한다(463행). 접점 용착 확인(C_AC_IN)은 설계자에게 물어 둔 상태다.

스레드 둘과 lock

[메인 스레드] while 반복              [MQTT 스레드] mosquitto_loop_start()
  비상정지 → CP 판정 → K1 → 생존 신호     EVerest 명령 → on_message()
              └──── 같은 상태 변수 ────┘  →  pthread_mutex lock(394행)

main() 흐름

시작   설정 읽기 → 토픽 앞부분 만들기 → ADC·K1·PWM·입력 열기(실패 시 종료)
       → safe_state("시작"): K1 OFF, CP F  → MQTT 비동기 연결(끊기면 1~5초 재시도)
반복   poll_inputs → sample_window → window_state → poll_inputs
       → 3번 연속이면 m2e/event → relay_apply → 다이오드 검사
       → 1초마다 m2e/communication_check → (−v면) 2초마다 상태 로그 → 10 ms 쉼
종료   safe_state("종료 신호") → MQTT 끊기 → GPIO·ADC 반납

safe_state()를 부르는 곳은 시작(800행), MQTT 끊김(on_disconnect, 744행), 종료(880행) 세 곳이다. 어떤 길로 빠지든 K1 OFF, CP -12V로 끝난다.


4. CP 판정: ADC 숫자가 A~E가 되기까지

전압 환산

CP 신호는 ±12V이고, 회로가 ADC 범위로 줄여 준다. 11/04 지그 시험에서 PWM 0%(-12V)일 때 986, 100%(+12V)일 때 47957이 읽혔다. 그 사이를 직선으로 본다.

/* volt_of(), 555행 */
return -12.0 + (raw - C.raw_lo) * 24.0 / (C.raw_hi - C.raw_lo);

상태 경계: 규격 전압의 중간값

상태 규격 High 전압 판정 (566행)
A 차 없음 +12V ≥ 10.5V
B 연결됨 +9V ≥ 7.5V
C 충전 요청 +6V ≥ 4.5V
D 환기 필요 충전 +3V ≥ 1.5V
E 단락·이상 0V 그 아래

PWM과 맞추지 않고 읽는다

PWM 중에는 CP가 1 ms마다 +V와 -12V를 오간다. 차가 바꾸는 것은 +쪽 전압뿐이므로 High 구간만 보면 된다. 하드웨어로 PWM에 맞춰 ADC를 찍는 대신, 연속으로 40번 읽고 나눠서 본다(M7-2 시험에서 정한 방식).

sample_window() 585행
  각 샘플을  ≥ +1.5V → hi[] / ≤ -1.5V → lo[] / 그 사이 → 중간  으로 분류
  hi[]와 lo[]를 정렬해 가운데 값(중앙값)을 씀  ← 경사 구간·튀는 값은 끝으로 밀려남
window_state() 620행
  CP F 출력 중        → 판정 안 함 (-12V 고정이라 차 쪽 저항이 안 보임)
  High 샘플 2개 이상   → classify(High 중앙값)
  중간이 절반 이상     → E (CP가 0V 근처에 붙음 = CP-PE 단락)
  그 밖               → 판정 보류 (듀티가 낮아 High를 못 잡음, 다음 창에서)

흔들림 거르기: 켤 때는 신중, 끌 때는 즉시

/* main 834~847행 */
같은 후보(cand)가 DEBOUNCE(3)번 연속   → cp_state 변경, m2e/event 보고

K1을 켜는 조건은 C/D가 3번 연속일 때다. 하지만 relay_apply()는 cand(방금 창의 결과)를 보므로, 창 하나만 B로 읽혀도 바로 끈다. 차가 충전을 멈추라고 하면 기다리지 않는다.

다이오드 검사

차 쪽 CP 회로에는 다이오드가 있어서 PWM의 Low 구간은 -12V 그대로여야 한다. B/C/D에서 PWM 중인데 Low 중앙값이 -10.5V보다 높으면 다이오드 단락으로 보고 DiodeFault를 보고한다(850~857행).

비상정지·누전: 감지는 즉시, 해제는 천천히

poll_inputs()(515행)는 CP 판정 앞뒤로 두 번 불린다. 한 번만 이상으로 읽혀도(읽기 실패 포함) 바로 K1 OFF + m2e/raise_error. 정상으로 돌아온 뒤 INPUT_DEBOUNCE_MS(20 ms) 동안 유지돼야 m2e/clear_error를 보낸다.


5. 명령 처리: on_message()

MQTT 스레드에서 e2m/ 메시지를 받으면 토픽 끝부분(op)으로 나눈다(647행).

e2m 토픽 처리
enable false → CP F, K1 판단. true이고 지금 F면 → CP X1(+12V). 그리고 capabilities, 현재 CP 상태, 걸려 있는 오류를 다시 보고 (EVerest가 재시작했을 수 있으므로)
pwm_on 듀티 ≥100 → X1, ≤0 → F, 그 사이 → PWM
cp_state_X1 / cp_state_F 고정 출력. F면 K1 판단
cp_state_E 하드웨어로 0V를 못 만든다 → F로 대신 (capabilities에 supports_cp_state_E:false로 알려 둠)
allow_power_on 값을 저장하고 relay_apply()
reset RCD 리셋 미지원 → replyTo 토픽으로 false 회신
그 밖 heartbeat, lock/unlock, 과전류, 3상 전환, self_test: 하드웨어 없음, 무시

enable 처리에 M7-4 때 배운 것이 들어 있다. EvseManager는 enable 뒤 따로 X1을 보내 주지 않고, BSP가 상태 A를 보고하기를 기다린다. 그래서 BSP가 스스로 +12V를 낸다.

MQTT에 연결될 때(on_connect, 716행)도 e2m/# 구독 → capabilities → 생존 신호 → 현재 상태·오류를 보고한다. 연결이 끊기면(on_disconnect) 바로 안전 상태로 간다.

충전 한 사이클을 MQTT로 따라가 보면

sc6-bsp → m2e/event {"event":"A"}      enable 뒤 +12V, 차 없음
sc6-bsp → m2e/event {"event":"B"}      차 연결 (+9V)
          … 카드 승인 (Auth ↔ OCPP ↔ SteVe) …
EVerest → e2m/pwm_on 53.3              32A 허용
sc6-bsp → m2e/event {"event":"C"}      차가 충전 요청 (+6V)
EVerest → e2m/allow_power_on {"allow_power_on":true}
sc6-bsp → m2e/event {"event":"PowerOn"}   K1 ON
          (sc6-meter → m2e/powermeter_values 1초마다)

보드 로그(/tmp/bsp.log)에도 같은 순서로 찍힌다.

sc6-bsp: 시작: module_id=bsp_1, 최대 32A, ...      ← 783행
sc6-bsp: 안전 상태: K1 OFF, CP F (시작)             ← 483행
sc6-bsp: MQTT 연결 localhost:1883, 구독 .../e2m/#  ← 729행
sc6-bsp: enable = true                             ← 665행
sc6-bsp: CP X1 (+12V, enable)
sc6-bsp: CP - -> A (High 11.9xV, ...)              ← 843행

정리

  • EVerest는 보드를 모른다. YAML 구성, BSP의 capabilities, 실시간 보고만 안다. 보드에 따라 바뀌는 코드는 sc6-bsp.c와 sc6-meter.c에만 있다.
  • 우리 프로그램과 EVerest는 everest_api/1/<종류>/<이름>/{e2m,m2e}/... 토픽으로만 이야기한다.
  • YAML의 connections가 모듈 사이 배선도다. requires 이름 → 상대 모듈 이름 + provides 이름.
  • sc6-bsp의 안전은 두 함수에 모여 있다. K1 판단은 relay_apply() 하나, 빠져나가는 길은 safe_state() 하나.
  • CP는 PWM과 맞추지 않고 연속으로 읽어 High 중앙값으로 판정한다. 켤 때는 3번 연속, 끌 때는 즉시.

2편에서는 sc6-meter와, EVerest 안에서 이 메시지들이 어떻게 충전 상태 머신(EvseManager)과 OCPP 거래로 이어지는지를 본다.

참고

  • 보드 구성: sc6-work/board/m7/config-sc6-ocpp.yaml, run-m7-bsp.sh
  • BSP: meta-sc6/recipes-evse/sc6-bsp/files/sc6-bsp.c, sc6-bsp.default
  • EVerest API 명세: EVerest/docs/source/reference/EVerest_API/evse_board_support_API.yaml
  • EVerest API 모듈: EVerest/modules/API/EVerestAPI/evse_board_support_API/
  • 이전 글: M7-4 실제 EVerest와 BSP로 충전 한 사이클 (2026-11-05)

댓글남기기