8 분 소요

M7 계획을 세운 날 오후에 첫 세 단계를 앞당겨 끝냈다. 원래 11/05부터 11/10까지 잡았던 분량이다.

단계 내용 결과
M7-1 이미지에 든 EVerest 실행, CPU·메모리 실측 CPU 24.7%, 메모리 약 40 MB. 오프라인 부팅 2분 지연을 찾아 고침
M7-2 저항 지그로 CP 상태별 ADC 기준표 A/B/C = +12.0 / +8.95 / +6.01 V. PWM 5%에서도 판정 가능
M7-3 C로 쓴 첫 BSP (sc6-bsp) CP 상태 판정, PWM, 주 릴레이 K1. 충전 순서 흉내 시험 통과

1. M7-1: 이미지에 든 EVerest 실행

1-1. 이미지에 무엇이 들어 있나

먼저 빌드 PC에서 everest-core 패키지를 풀어 봤다.

항목 내용
버전 프레임워크 2026.10.0-dev, 모듈 119개
10/29 실측과의 관계 같은 레시피, 같은 빌드 → 10/29 설정 파일을 그대로 쓸 수 있음
Python 이미지에 없음. 10/29 설정은 Python 모듈(가상 차량의 ISO 15118 부분)을 이미 빼 둔 것이라 문제없음
서비스 everest.service가 들어 있음

서비스 파일에는 이런 조건이 붙어 있었다.

[Unit]
Requires=mosquitto.service
After=mosquitto.service
ConditionFileNotEmpty=/etc/everest/config.yaml

[Service]
ExecStart=/usr/bin/manager --config /etc/everest/config.yaml --db /var/lib/everest/everest.db --settings /etc/everest/settings-mqtt-uds.yaml

/etc/everest/config.yaml이 없으면 부팅 때 그냥 건너뛴다. 지금 이미지에 이 파일이 없어서 EVerest가 조용히 안 떴던 것이다. 실제 충전기 설정이 정해지면 이 파일만 넣으면 된다.

1-2. MQTT는 유닉스 소켓으로

EVerest 레이어는 mosquitto 설정 패키지(everest-mosquitto-config)도 같이 넣는다.

listener 0 /run/mosquitto/mosquitto.sock    ← 유닉스 소켓
listener 1883 localhost
acl_file /etc/mosquitto/everest-external.acl   ← TCP 에서는 everest/# 거부

EVerest 모듈끼리 쓰는 내부 토픽(everest/#)은 유닉스 소켓으로만 오가게 막아 두었다. TCP 1883은 외부 프로그램용이다. 10/29 chroot 실측 때는 mosquitto를 기본 설정으로 따로 띄워서 몰랐던 부분이다. 손으로 실행할 때도 소켓 설정을 줘야 한다.

exec manager --config /home/root/m7/config-sil-board.yaml \
	--settings /etc/everest/settings-mqtt-uds.yaml \
	--db /home/root/m7/everest.db

1-3. 실측

10/29와 같은 설정이다. 가상 차량이 시작 6초 뒤 32A 단상 기본 충전을 자동으로 시작한다. 바뀐 것은 OS 하나(MYiR 5.4 위 chroot → 6.6 이미지)다.

항목 11/04 (6.6 이미지) 10/29 (5.4 chroot)
모든 모듈 초기화 3,080 ms 3,608 ms
충전 준비 → 충전 시작 약 7초 약 15초 (첫 실행 DB 생성 포함)
메모리 PSS 합계 49 MB (17 프로세스) 50 MB
CPU (충전 중 10초, 코어 1개 기준) 24.7% 31.3%
사용 가능 메모리 (실행 중 / 정지) 349 / 389 MB 284 MB

EVerest를 멈추면 사용 가능 메모리가 40 MB 늘었다. PSS 합계와 비슷하다. CPU는 energy_manager 5.9%, mosquitto 4.7%, EvseManager 3.2% 순이었고, 코어 두 개(200%)의 1/8 정도를 쓴다.

로그의 error|fail 검색에 두 줄이 걸려서 잠깐 긴장했다. 모듈 이름 error_history였다.

1-4. 네트워크 없이 켜면 충전이 2분 늦다

M6에서 보류했던 문제가 있었다. 네트워크 없이 부팅하면 systemd-networkd-wait-online이 2분을 기다리다 실패해 시스템이 degraded가 된다. “표시만 거슬리는 것”인지 보려고 누가 network-online.target을 기다리는지 봤다.

$ systemctl list-dependencies --reverse network-online.target
network-online.target
* `-mosquitto.service

딱 하나, 하필 mosquitto였다.

네트워크 없음 → wait-online 2분 → network-online.target
              → mosquitto (After=network-online.target)
              → EVerest   (Requires/After=mosquitto)      ← 여기서야 충전 가능

충전기는 설치 현장이나 통신 장애로 네트워크 없이 켜질 수 있다. 그때마다 부팅 후 2분 동안 충전이 안 된다. 그런데 이 mosquitto는 유닉스 소켓과 localhost에서만 받는다. 외부 네트워크가 전혀 필요 없다.

처음에는 drop-in으로 두 줄을 지우려 했다. 그런데 systemd는 drop-in에서 After=나 Wants= 같은 의존 관계를 지우는 것을 지원하지 않는다(추가만 된다). 그래서 bbappend로 설치된 서비스 파일을 직접 고쳤다.

# recipes-connectivity/mosquitto/mosquitto_%.bbappend
do_install:append() {
	sed -i '/network-online\.target/d' ${D}${systemd_unitdir}/system/mosquitto.service
}

효과가 하나 더 있었다. network-online.target을 원하는 서비스가 하나도 없으면 systemd는 wait-online을 아예 실행하지 않는다. 보드에서 같은 내용의 사본을 /etc에 두고 재부팅하니 wait-online은 inactive (dead), 시스템은 running이었다. 2분 대기와 degraded가 함께 사라졌다.


2. M7-2: CP 기준표

2-1. 무엇을 재나

충전기는 CP 선에 ±12V, 1kHz PWM을 내보낸다. 차량은 저항으로 High 쪽 전압을 끌어내려 상태를 알린다.

상태 차량 쪽 CP High
A 연결 안 됨 +12V
B 연결됨 +9V (2.7kΩ)
C 충전 요청 +6V (2.7kΩ ∥ 1.3kΩ)

차량 쪽에는 다이오드가 있어서 Low(-12V) 쪽에는 전류가 흐르지 않는다. 그래서 Low는 상태와 상관없이 -12V여야 한다. 지그는 J6의 1번(CP)과 2번(PE) 사이에 다이오드와 저항 두 개, 스위치 두 개로 만들었다.

2-2. 시험 함수 파일

보드에서 반복해서 쓸 셸 함수들을 파일 하나로 만들었다. 실행하는 파일이 아니라 불러오는 파일이다.

. /home/root/m7/cp-test.sh     # 현재 셸에 함수가 생긴다
cp_init                        # PWM 1kHz 100% ON
cp_state                       # 상태 B  High +8.95V  다이오드 정상
cp_dist 53                     # PWM 53% 로 돌리며 ADC 200번 읽어 분포 출력

cp_both나 cp_state는 PWM을 100%(+12V 고정)와 0%(-12V 고정)로 바꿔 가며 High와 Low를 따로 읽는다.

시험 중에 한 번 헷갈렸다. cp_set 100을 했는데 계속 -12V가 나왔다. 그 직전에 PWM을 꺼 두었기 때문이다(enable = 0). PWM이 꺼지면 핀이 Low에 머물러 듀티와 상관없이 CP는 -12V다. 그래서 cp_set이 PWM이 꺼져 있으면 경고를 내도록 고쳤다.

2-3. 정적 측정

상태 High ADC High 전압 규격 Low ADC
A 47,957 +12.0 V (기준) 11 ~ 13 V 986
B 41,994 +8.95 V 8 ~ 10 V ✅ 987
C 36,237 +6.01 V 5 ~ 7 V ✅ 990

전압은 상태 A의 두 값(986 = -12V, 47,957 = +12V)을 직선으로 이어 환산했다. 1V당 약 1,957 카운트다. Low가 세 상태 모두 같으니 다이오드 방향도 맞다.

판정 경계는 규격 전압의 중간으로 잡았다.

경계 전압 ADC
A / B 10.5 V 45,021
B / C 7.5 V 39,150
C / D 4.5 V 33,279
D / E 1.5 V 27,407
다이오드 이상 (Low 가 이보다 높으면) -10.5 V 3,922

2-4. PWM이 돌 때 읽기

실제 충전 중에는 PWM이 돌고 있으니 ADC가 High와 Low를 섞어 읽는다. 리눅스 sysfs로 읽으면 읽는 시점이 PWM과 맞지 않는다. 그래도 많이 읽으면 두 묶음으로 갈라질 것이라고 보고 분포를 봤다(지그 B).

듀티 5%, 샘플 200개, 1회 읽기 약 7969 us
  최소    887 (-12.05V)   최대  42073 (+8.99V)
  0V 위 샘플 비율 8%
   +6V ~  +9V   14 ####
   +0V ~  +3V    1 #
   -6V ~  -3V    2 #
  -12V ~  -9V  183 ##############################################
듀티 High 샘플 High 값 전환 순간 값
53% 110 / 200 (56%) +8.98 V 3개
5% (가장 나쁜 경우) 14 / 200 (7%) +8.99 V 3개

5%에서도 High가 정확히 잡혔다. 다만 처음 생각한 “상위 5% 값”을 쓰면 안 된다는 것도 보였다. 5% 듀티에서는 High 샘플이 겨우 7%라서, 백분위 하나만 밀려도 Low 쪽 값이 나온다. 그래서 판정은 0V 위 샘플들(High 묶음)의 중앙값으로 하기로 했다. 전환 순간에 찍힌 중간값도 이렇게 하면 걸러진다.

문제는 속도였다. 셸에서 cat으로 읽으니 1회에 8 ms였다. 이건 C로 해결할 일이라고 보고 다음 단계로 넘어갔다.


3. M7-3: 첫 BSP

3-1. 구조

EVerest에서 보드 하드웨어를 맡는 부분이 BSP 모듈이다. EVerest는 이를 위한 공개 MQTT API(evse_board_support_API)를 제공한다. EVerest 쪽에는 이 API 모듈을 두고, 우리 프로그램은 MQTT로 붙는다.

EvseManager ── evse_board_support_API (EVerest 모듈) ──MQTT── sc6-bsp ── PWM / ADC / GPIO

EVerest를 다시 빌드하지 않아도 되고, 판정 기준을 고칠 때마다 우리 프로그램만 바꾸면 된다. 언어는 C로 정했다. 이미지에 Python이 없고, 필요한 라이브러리(libmosquitto, libgpiod v2, cJSON)는 모두 이미 이미지에 들어 있었다. cJSON은 EVerest가 쓰려고 넣은 것이다.

명세는 패키지 안의 AsyncAPI 파일(/usr/share/everest/EVerest_API/evse_board_support_API.yaml)에 있다. 토픽은 everest_api/1/evse_board_support/<모듈 ID>/ 아래다.

EVerest → BSP (e2m/) BSP 동작
enable 충전 허용/금지. 금지면 CP F(-12V), K1 OFF
cp_state_X1 CP +12V 고정
pwm_on (듀티 %) CP 1kHz PWM
cp_state_F CP -12V 고정
allow_power_on K1을 켜도 된다는 허가
BSP → EVerest (m2e/) 언제
event A~E CP 상태가 바뀌었을 때
event PowerOn / PowerOff K1을 켜거나 끈 직후
capabilities 연결할 때 (최대 전류, 상 수, 커넥터 종류)
communication_check 1초마다
raise_error DiodeFault PWM 중 Low가 -10.5V까지 안 내려갈 때

첫 버전 범위는 CP, PWM, 주 릴레이 K1까지다. 비상정지(EM_INT)와 누전(RCD_INT)은 입력 극성을 보드에서 먼저 시험한 뒤 다음 단계에 넣는다. 커넥터 방식은 아직 정하지 않아서 케이블 일체형, 최대 32A를 설정 파일에 넣어 두었다.

3-2. CP 판정

M7-2에서 정한 방식 그대로다. 40번 읽어서 세 묶음으로 나눈다.

for (i = 0; i < n; i++) {
	int r = adc_read();            /* pread(fd, ..., 0): 파일을 열어 둔 채 다시 읽기 */
	if (r >= t_hi)        hi[n_hi++] = r;   /* +1.5V 이상 */
	else if (r <= t_lo)   lo[n_lo++] = r;   /* -1.5V 이하 */
	else                  n_mid++;          /* 전환 순간, 또는 0V 근처(E) */
}
/* High 묶음이 2개 이상이면 그 중앙값으로 A~D 판정 */

파일을 열어 둔 채 pread로 다시 읽으니 1회에 80~100 µs가 걸렸다. 셸의 8 ms보다 약 80배 빠르다. 40샘플 한 묶음이 3~4 ms면 끝난다. 같은 상태가 3번 연속 나와야 EVerest에 보고한다.

3-3. 릴레이 규칙

K1이 붙으면 충전 출력에 220V가 나간다. 그래서 켜는 조건과 끄는 조건을 다르게 했다.

  조건
켜기 enable && allow_power_on && CP가 C/D로 3번 연속
끄기 위 조건이 깨지면 바로. CP는 한 번이라도 C/D가 아니면 끔
시작 / 종료 신호 / MQTT 끊김 K1 OFF + CP F

처음 짠 코드에는 구멍이 있었다. “C에서 벗어나면 끈다”만 있고 다시 켜는 경로가 상태 변화에 묶여 있었다. 그래서 순간적인 잡음 한 번에 K1이 떨어지면 상태 보고는 여전히 C라서 다시 붙지 않았다. 빌드 전에 발견해서, 켜기와 끄기를 같은 함수에서 “C/D 연속 횟수”로 판단하게 바꿨다.

static void relay_apply(const char *why)
{
	bool cd = (cand == 'C' || cand == 'D') && same >= C.debounce;
	bool want = connected && enabled && allow_power_on && cd;
	...
}

3-4. EVerest 없이 시험

EVerest를 붙이기 전에 BSP만 먼저 확인했다. EVerest가 보낼 명령을 mosquitto_pub으로 대신 보냈다. mosquitto 클라이언트는 이미지에 없어서 빌드 결과에서 꺼내 보드에 올렸다.

e2m() { ... mosquitto_pub -t everest_api/1/evse_board_support/bsp_1/e2m/$1 -m "$2"; }
e2m enable true; e2m cp_state_X1
e2m pwm_on 53
e2m allow_power_on '{"allow_power_on":true,"reason":"FullPowerCharging"}'
17:09:50.600  CP - -> A (High 12.01V, 40/40 샘플)
17:09:58.539  CP A -> B (High 8.96V, 40/40 샘플)
17:10:00.854  PWM 53.0%
17:10:05.980  CP B -> C (High 6.01V, 18/40 샘플)        ← PWM 중 판정
17:10:13.783  K1 ON (allow_power_on)                    ← 딸깍
17:10:18.921  K1 OFF (CP 가 C/D 에서 벗어남)            ← 지그를 B 로
17:10:18.948  CP C -> B (High 8.95V, 20/40 샘플)
17:10:31.679  CP B -> A (High 12.01V, 21/40 샘플)

지그를 C에서 B로 바꾸자 K1이 먼저 떨어지고, 27 ms 뒤에 B 상태가 보고됐다. 끄기는 첫 창에서, 보고는 3창 확인 뒤라는 설계 그대로다. K1이 실제로 딸깍 붙고 떨어지는 것도 확인했다.

덧붙여, 보드에서 scp host:'dir/{a,b}' .처럼 원격 쪽 중괄호로 여러 파일을 받으려 하면 protocol error: filename does not match request가 난다. 서버가 펼친 이름이 요청한 이름과 달라서 scp가 안전 검사로 거부하는 것이다. *로 받으면 된다.


4. 정리

커밋

de28bb7 M7-3: sc6-bsp 첫 버전 (EVerest evse_board_support MQTT 외부 모듈, C)
f90eb82 M7-1: mosquitto 가 network-online.target 을 기다리지 않게

오늘 배운 것

  1. “표시만 이상한 문제”도 누가 기다리는지 확인하자. degraded는 거슬리기만 한 줄 알았는데, 의존 관계를 따라가니 오프라인 충전이 2분 늦어지는 문제였다.
  2. systemd drop-in으로는 의존을 지울 수 없다. 추가만 된다. 지우려면 서비스 파일 자체를 바꿔야 한다.
  3. 기준값은 내 하드웨어에서 재자. 규격의 9V, 6V가 우리 회로에서 ADC 얼마인지는 지그로 재 보기 전까지 몰랐다. 이 표가 BSP 판정의 바탕이 됐다.
  4. 백분위 하나로 판정하지 말자. 5% 듀티에서는 “상위 5%”가 High와 Low의 경계에 걸린다. 묶음으로 나눈 뒤 중앙값을 쓰자.
  5. 켜기와 끄기는 다르게. 위험한 쪽(켜기)은 확인을 여러 번, 안전한 쪽(끄기)은 즉시. 그리고 끈 뒤 다시 켜는 경로까지 확인하자.
  6. 큰 시스템에 붙이기 전에 혼자 돌려 보자. mosquitto_pub으로 명령을 흉내 내니 EVerest 없이도 BSP의 판정과 릴레이 동작을 하나씩 확인할 수 있었다.

다음 할 일

순서 할 일
M7-4 실제 EVerest와 BSP 연결: 보드용 설정(evse_board_support_API, EvseManager, 인증 생략, 가상 전력량계), 지그로 충전 한 사이클
M7-5 비상정지(EM_INT)·누전(RCD_INT) 입력 극성 시험 후 BSP에 오류 보고 추가
M7-6 sc6-bsp 서비스, EVerest 자동 시작, 이미지 반영, 새 카드 확인

댓글남기기