전기차 세정 도크의 누출 감지·페일세이프 차단 시스템에서 실제로 부하를 끊는 노드입니다. NUCLEO-F411RE(Cortex-M4 @ 84 MHz) 베어메탈 슈퍼루프 펌웨어이며, RTOS 를 쓰지 않습니다.
판정은 Jetson, 차단은 STM32. Linux 는 실시간을 보장하지 않으므로, 릴레이를 떨어뜨리는 주체는 베어메탈 MCU 입니다. 상위 노드(Jetson·Raspberry Pi)가 죽어도 STM32 는 통신 두절을 스스로 감지해 차단합니다(
COMM_TRIP).
Jetson Nano ──TCP 텍스트 1줄──▶ Raspberry Pi ──UART/HC-06 12B+CRC──▶ STM32F411RE
(U-Net 추론·누출 판정) (중계 + MariaDB 로깅) ◀── 텔레메트리 8B ── │
▼
릴레이 2ch / 서보 / LED 3색 / 부저
이 저장소(디렉터리)가 담당하는 것은 오른쪽 끝의 STM32 한 노드입니다.
| 받는 것 | 하는 것 | 내보내는 것 |
|---|---|---|
| 100 ms 주기 12 B 명령(state·area·spread·zone) | CRC 검증 → 안전 FSM 판정 → 릴레이/서보/표시 | 100 ms 주기 8 B 텔레메트리(FSM 상태·플래그·SEQ 에코) |
보드: NUCLEO-F411RE / STM32F411RETx 클럭: HSI 16 MHz → PLL(N=336, P=4) → SYSCLK 84 MHz, APB1 42 MHz(타이머 84 MHz), APB2 84 MHz 표시장치 없음 — 상태는 3색 LED + 부저 + 디버그 UART 로만 표현합니다.
| 기능 | 핀 | 페리페럴 | 비고 |
|---|---|---|---|
| 상태 LED 녹 / 황 / 적 | PB12 / PB13 / PB14 | GPIO Output | 연속 비트 — BSRR 단일 write 로 원자적 갱신 |
| 하트비트 LED (LD2) | PA5 | GPIO Output | 200 ms 토글, 메인 루프 생존 신호 |
| 릴레이 — 세정 펌프 | PC0 | GPIO Output | HW_RELAY_ACTIVE_LOW = 1, 초기 레벨 High |
| 릴레이 — 도크 전원 | PC1 | GPIO Output | 부하는 반드시 NO 접점에 배선 |
| 부저 | PA6 | TIM3_CH1 PWM | PSC 83 / ARR 369 → 약 2.7 kHz, 듀티 50 % |
| 서보 (와이퍼) | PA0 | TIM2_CH1 PWM | PSC 83 / ARR 19999 → 1 tick = 1 µs, 20 ms(50 Hz) |
| 리셋 버튼 (B1) | PC13 | GPIO Input (풀업) | EXTI 는 NVIC 에서 꺼둠 — 10 ms 폴링 디바운스 |
| 디버그 UART | PA2 / PA3 | USART2 | ST-Link VCP, 115200 8N1 |
| 링크 UART | PA9 / PA10 | USART1 (IT) | HC-06 / Raspberry Pi, 9600 8N1 |
핀이 바뀌면 My/hw/hw_def.h 하나만 고칩니다. 드라이버 .c 는 건드리지 않습니다.
서보는 밸브가 아니라 와이퍼입니다 — 암에 휴지를 물려 LEAK 상태에서 누출액을 닦아냅니다.
세정액 차단은 펌프 릴레이가 전담하므로 서보는 안전 액추에이터가 아닙니다.
| 매크로 | 값 | 뜻 |
|---|---|---|
HW_SERVO_ANGLE_PARK |
0° | 대기 위치 = 안전측(servoGotoSafe()) |
HW_SERVO_ANGLE_WIPE_L / _R |
30° / 150° | 스트로크 양 끝점 |
HW_SERVO_WIPE_STEP_DEG / _TICK_MS |
3° / 20 ms | 스윙 속도 150 °/s |
HW_SERVO_WIPE_MAX_CYCLES |
20 왕복 (약 32초) | 이후 자동 PARK — SG90 과열 방지 |
펄스 500 ~ 2400 µs. L/R 은 반드시 실제 기구부 가동 범위 안에서 잡습니다 — 끝에서 벽에 닿으면 SG90 이 스톨되어 전류가 계속 흐릅니다.
툴체인은 CMake + Ninja + arm-none-eabi-gcc 입니다(툴체인 파일 cmake/gcc-arm-none-eabi.cmake).
cmake --preset Debug # 구성 (Release 프리셋 동일)
cmake --build build/Debug # → build/Debug/AI_mini_project.elf (+ .map)- STM32CubeCLT 의 CMake / Ninja /
arm-none-eabi-gcc가 PATH 에 있어야 합니다. VS Code 에서는.vscode/settings.json이 STM32CubeIDE 번들의cube-cmake를 쓰도록 설정되어 있습니다. - 플래싱은 STM32CubeProgrammer / ST-Link 로 합니다. 테스트 스위트·린터는 없습니다 — 검증은 보드에 올려 동작을 보는 것입니다.
- 경고 옵션:
-Wall -Wextra -Wshadow -Wconversion -Wsign-conversion.My/소스는 경고 0건을 유지합니다. My/소스와 include 경로는 루트CMakeLists.txt에 등록합니다 (cmake/stm32cubemx/CMakeLists.txt는 CubeMX 재생성 시 덮어써집니다).
My/
├── common/def.h 표준 라이브러리 헤더만
├── hw/
│ ├── hw_def.h 유일한 보드 설정 파일 (핀·채널·보드레이트·_USE_HW_*)
│ ├── hw.h 드라이버 헤더 통합 지점
│ ├── hw.c hwInit() / millis()
│ └── driver/ led · relay · buzzer · servo · button · uart
└── ap/
├── ap.c 슈퍼루프 스케줄러
├── packet.c 링크 프로토콜 (파서 + 텔레메트리 + CRC16)
└── safety_fsm.c 안전 상태 머신
단방향 include 체인(원래 짐벌 코드의 hw_def.h ↔ uart.h 순환 참조를 해결한 구조):
def.h → hw_def.h → driver/*.h → hw.h → ap.h
계층별로 아는 것과 모르는 것이 명확히 나뉩니다.
| 계층 | 아는 것 | 모르는 것 |
|---|---|---|
driver/ |
레지스터·핀 | FSM, 프로토콜 |
packet.c |
바이트 레이아웃·CRC | 릴레이가 존재한다는 사실 |
safety_fsm.c |
판정 정책·임계값 | 프레임 포맷 |
덕분에 핀 변경 → hw_def.h / 임계값 변경 → safety_fsm.h / 프레임 변경 → packet.h·packet.c 만 고치면 됩니다.
Core/, Drivers/, startup_stm32f411xe.s, .ld, cmake/stm32cubemx/ 는 AI_mini_project.ioc 로부터 재생성되는
CubeMX 산출물입니다. 수동 편집은 반드시 /* USER CODE BEGIN */ … /* USER CODE END */ 안에서만 하고,
페리페럴·핀·클럭 변경은 CubeMX 에서 하고 재생성합니다.
main.c 의 USER CODE BEGIN 2 에서 apInit() → apMain() 이 호출되며 apMain() 은 리턴하지 않습니다.
| 주기 | 하는 일 |
|---|---|
| 매 루프 | packetUpdate() — 링버퍼 파싱 (1회 최대 64 B) |
| 10 ms | buttonUpdate() · ledUpdate() · buzzerUpdate() — 입력 샘플링과 표시 타이밍 |
| 100 ms | safetyFsmUpdate() — 판정 → 차단 → 표시 → 텔레메트리 송신 |
| 200 ms | 하트비트 LED 토글 |
- 패킷 파서를 슬롯에 넣지 않은 이유: 100 ms 슬롯에 묶으면 수신 인식이 최대 100 ms 늦어지고 그만큼 차단이 늦습니다.
- 슬롯 판정은 반드시
(now - prev) >= SLOT형태로 씁니다.now >= prev + SLOT는millis()가 오버플로하는 약 49.7 일 시점에 깨집니다(unsigned 뺄셈은 wrap-safe).
프레임 레이아웃은 호스트 쪽 구현과 바이트 단위로 일치해야 합니다. 한쪽만 고치면 CRC 가 통과하지 못해 링크가 조용히 죽습니다.
| off | 필드 | 값 |
|---|---|---|
| 0-1 | STX | 0x55 0xAA |
| 2 | LEN | 7 |
| 3 | SEQ | 0 ~ 255 순환 (텔레메트리로 에코) |
| 4 | STATE | 0 NORMAL / 1 SMALL_LEAK / 2 WARNING / 3 DANGER / 4 SENSOR_ERR(예약) |
| 5-6 | AREA | int16, 누출 면적 0.01 % 단위 (500 = 5.00 %) |
| 7-8 | SPREAD | int16, 확산 확정 플래그 0/1 |
| 9 | ZONE | 충전 단자 침범 0/1 |
| 10-11 | CRC16 | off 2~9 (8 B) 대상 |
| off | 필드 | 값 |
|---|---|---|
| 0-1 | STX | 0xA5 0x5A |
| 2 | LEN | 3 |
| 3 | SEQ | 마지막 수신 명령의 SEQ 에코 |
| 4 | FSM | fsm_state_t 현재 상태 |
| 5 | FLAGS | bit0 = latched, bit1 = comm_ok |
| 6-7 | CRC16 | off 2~5 (4 B) 대상 |
CRC16-CCITT-FALSE (poly 0x1021, init 0xFFFF, no reflect, no final XOR). 테이블 없이 비트 연산으로 계산합니다(12 B 기준 수 µs).
검증 벡터 : state=1, area=500, spread=-30, zone=1, seq=42
55 AA 07 2A 01 F4 01 E2 FF 01 55 33
깨진 바이트를 만나면 무조건 idx=0 으로 되돌리지 않고, 그 바이트가 0x55 면 새 프레임의 첫 바이트로 채택합니다.
페이로드 안에 55 AA 07 이 우연히 들어와 위장 헤더가 되더라도 CRC 에서 걸러지고 다음 프레임에서 정렬이 복구되므로,
최악의 손실은 1 프레임(100 ms) 이며 COMM_WARN 임계(500 ms) 안에 충분히 들어옵니다.
수신 시각(s_last_ms)은 CRC 를 통과한 프레임에서만 갱신합니다. 깨진 바이트로 타임아웃이 리셋되면
"쓰레기 수신 = 통신 정상" 이 되어버리기 때문입니다.
fsm_state_t 열거 순서는 텔레메트리로 그대로 나가므로 호스트의 fsm_name[] 및 MariaDB fsm_code 테이블과
반드시 일치해야 합니다: INIT NORMAL CAUTION LEAK DANGER COMM_WARN COMM_TRIP (0~6).
| 상태 | 릴레이 | 서보 | 래치 | LED | 부저 |
|---|---|---|---|---|---|
| INIT (첫 패킷 이전) | 전부 차단 | 닫힘 | – | 녹·황·적 동시 점멸 | OFF |
| NORMAL | 펌프 + 도크 통전 | 열림 | – | 녹 상시 | OFF |
| CAUTION | 펌프 + 도크 통전 | 열림 | – | 황 상시 | 짧게 1회 |
| LEAK | 펌프만 차단 | 닫힘 | – | 황 점멸 | 1 s 주기 |
| DANGER | 전부 차단 | 닫힘 | O | 적 빠른 점멸 | 200 ms 주기 |
| COMM_WARN | 펌프만 차단 | 닫힘 | – | 황↔적 교대 | 1 s 주기 |
| COMM_TRIP | 전부 차단 | 닫힘 | O | 적 빠른 점멸 | 200 ms 주기 |
통신 두절(≥1500 ms) > 래치 유지 > 통신 경고(≥500 ms) > 비전 판정
부팅 후 첫 유효 패킷 이전에는 COMM_TRIP 이 아니라 INIT 입니다 — INIT 자체가 이미 전부 차단된 안전 상태이고,
Pi 를 늦게 켰다는 이유로 버튼을 눌러야 풀리는 래치를 걸면 브링업이 매번 막히기 때문입니다.
fsmEvalVision() 은 두 값을 계산해 높은 쪽을 택합니다.
- STATE 경로 — Jetson 이 이미 내린 판정. 이것이 있어 Jetson 화면이 DANGER 면 STM32 도 반드시 DANGER 입니다.
- AREA 경로 — STM32 가 같은 임계값으로 다시 계산한 면적. Jetson 판정 로직이 바뀌거나 state 가 오염돼도 숫자만으로 차단할 수 있는 독립 경로입니다.
그 위에서 zone != 0(충전 단자 침범)은 면적과 무관하게 무조건 DANGER 입니다.
state == SENSOR_ERR 은 "누출 없음"이 아니라 "알 수 없음"으로 취급해 CAUTION 으로 올리고, 3 초 연속되면 LEAK 과 동급으로 승격합니다.
| 매크로 | 값 | 의미 | Jetson 대응 |
|---|---|---|---|
FSM_AREA_CAUTION |
30 | 0.30 % 이상 | leak_detect_ratio |
FSM_AREA_LEAK |
150 | 1.50 % 이상 | warning_ratio |
FSM_AREA_DANGER |
350 | 3.50 % 이상 | danger_ratio |
FSM_SPREAD_ESCALATE |
1 | 확산 확정 시 한 단계 승격 | spreading_escalates |
한쪽만 바꾸면 Jetson 화면과 STM32 LED 가 서로 다른 단계를 표시합니다. 값은 전부 safety_fsm.h 매크로이고 .c 에는 상수를 직접 쓰지 않습니다.
| 프레임 수 | 시간 | |
|---|---|---|
승격 (FSM_CONFIRM_UP / FSM_CONFIRM_DANGER) |
1 | 즉시 |
완화 (FSM_CONFIRM_DOWN) |
3 | 300 ms |
승격을 즉시로 둔 이유는 Jetson 이 이미 EMA(α 0.25)와 3 프레임 중 2 프레임 규칙으로 걸러 보내오기 때문입니다 — 여기서 또 지연을 넣으면 이중 디바운스가 되어 STM32 표시가 늘 한 박자 늦습니다. 완화에만 300 ms 를 남긴 이유는 잘못 올리면 잠깐 멈추는 것으로 끝나지만 잘못 내리면 누출 중에 펌프가 재기동되고, 면적이 임계 경계(1.49 % / 1.51 %)를 오갈 때 릴레이가 100 ms 단위로 채터링하기 때문입니다.
fsmResetAllowed() — 아래를 모두 만족해야 풀립니다. 거부 시 디버그 UART 로 사유를 출력합니다.
- 통신 회복 (
age < 500 ms) state != SENSOR_ERRzone == 0area < FSM_AREA_LEAK- Jetson 단계가 WARNING 미만
- de-energize-to-trip — 부하는 NO 접점에 배선되어 코일 여자 시에만 통전합니다. MCU 정지·단선 시 자동 차단됩니다.
- 초기화 순서가 곧 안전 설계 —
hwInit()은relayInit()을 가장 먼저 호출합니다. 이후 어떤 초기화가 실패해도 부하는 이미 차단 상태입니다. - 차단 먼저, 표시 나중, 로그 마지막 —
fsmEnter()는 항상relayCutAll()/servoGotoSafe()로 시작하고, blocking UART 로그는 가장 마지막에 찍습니다. - 릴레이 2채널 동시 차단 —
relayCutAll()은 BSRR 단일 write 로 두 채널을 동시에 떨어뜨립니다. 시간차 차단으로 "도크 전원만 살아있는" 위험 구간을 만들지 않습니다. - LED 3핀 원자적 갱신 — 녹·황·적을 연속 비트에 배치한 이유입니다. 3번 나눠 쓰면 그 사이 인터럽트가 끼어들어 "황 + 적 동시 점등" 같은 순간적 모순 표시가 생깁니다.
- 표시 타이밍과 상태 판정의 분리 — FSM 은 상태 진입 시
ledSetPattern()/buzzerSetMode()를 1회만 부르고, 실제 점멸 위상은ledUpdate(now_ms)가now_ms만으로 계산합니다(드라이버가 내부 상태를 갖지 않음). - ISR 은 최소한만 — UART RX 콜백은 링버퍼에 바이트를 넣고
HAL_UART_Receive_IT를 재무장하는 것만 합니다. ISR 안에서 파싱하지 않습니다.HAL_UART_ErrorCallback의 재무장은 필수입니다 — 오버런(ORE) 한 번으로 이후 모든 RX 가 조용히 죽습니다. - 버튼은 EXTI 가 아니라 폴링 — 기계식 채터링이 EXTI 폭주가 되어 메인 루프 실시간성을 깎는 것을 피하기 위한 의도적 선택입니다(10 ms × 3 = 30 ms 디바운스).
- 논리 채널 인덱스로 다룹니다 —
relayEnable(HW_RELAY_CH_PUMP),uartPrintf(HW_UART_CH_DEBUG, …)처럼 포트/핀을 상위 계층에 노출하지 않습니다. uartWrite/uartPrintf는 blocking 입니다. 안전 판정 경로에서 호출하지 마세요(텔레메트리 8 B ≈ 0.7 ms 는 차단 동작이 끝난 뒤라 무해합니다).HW_UART_RX_BUF_LEN(256)은 반드시 2의 거듭제곱 — 마스크 기반 wrap 을 쓰고%를 쓰지 않습니다.
My/ap/ap.c 상단의 AP_USE_SELFTEST 를 1 로 바꾸면 FSM 대신 셀프테스트가 돕니다(기본 0).
온보드 파란 버튼(B1)을 누를 때마다 다음 단계로 넘어가고, 각 단계 전환 시 항상 안전측으로 되돌립니다.
LED GREEN → LED YELLOW → LED YELLOW BLINK → LED RED BLINK
→ BUZZER → RELAY PUMP → RELAY DOCK → SERVO SWEEP → DONE
정상 동작과 동시에 켤 수 없습니다 — 리셋 버튼을 서로 소비하기 때문입니다(buttonGetPressed() 는 1회 소비형).
HW_RELAY_ACTIVE_LOW(현재 1)는 릴레이 모듈 실물과 반드시 일치해야 합니다. 값이 틀리면 "차단"이 곧 "통전"이 되어 페일세이프 설계가 정반대로 뒤집힙니다.- 링크 보드레이트는
HW_UART_BAUD_LINK(현재 9600)와 HC-06 모듈 설정값 둘이 실제 속도를 정합니다./dev/rfcomm0에서는 Pi 쪽server의 BAUD 인자가 무시됩니다(블루투스 소켓이라 termios 미적용). HC-06 을AT+BAUD8로 115200 에 맞췄다면hw_def.h도 함께 바꿉니다. .ioc에서 PC0/PC1 의 초기 출력 레벨은 High 로 지정되어 있습니다 — ACTIVE_LOW 모듈에서MX_GPIO_Init()과relayInit()사이에 코일이 잠깐 여자되는 틈을 없애기 위한 설정입니다.
| 바꿀 것 | 열 파일 |
|---|---|
| 핀·포트·채널·보드레이트 | My/hw/hw_def.h |
| 판정 임계값·확정 프레임 수·통신 타임아웃 | My/ap/safety_fsm.h |
| 프레임 레이아웃·CRC | My/ap/packet.h + My/ap/packet.c (+ 호스트 쪽 동시 수정) |
| 슬롯 주기·셀프테스트 스위치 | My/ap/ap.c |
| LED 점멸 패턴·부저 패턴 | My/hw/driver/led.c · buzzer.c |
| 페리페럴·클럭·타이머 파라미터 | CubeMX 에서 AI_mini_project.ioc 수정 후 재생성 |
새 My/ 소스 추가 |
루트 CMakeLists.txt |
| 문서 | 내용 |
|---|---|
README_구조.md |
시스템 전체 구조·데이터 흐름·FSM 동작·검증 절차 (가장 상세한 설계 문서) |
CLAUDE.md |
이 디렉터리에서 작업할 때의 규약 요약 |
../CLAUDE.md |
노드 간 통신 계약(프로토콜·FSM 열거 순서)과 두 프로젝트의 관계 |