SC6 코드 읽기 1 — EVerest는 보드를 모른다, 그리고 sc6-bsp.c 한 바퀴
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)
댓글남기기