Skip to content

Repository files navigation

Leak Guard — STM32 Firmware

전기차 세정 도크의 누출 감지·페일세이프 차단 시스템에서 실제로 부하를 끊는 노드입니다. NUCLEO-F411RE(Cortex-M4 @ 84 MHz) 베어메탈 슈퍼루프 펌웨어이며, RTOS 를 쓰지 않습니다.

판정은 Jetson, 차단은 STM32. Linux 는 실시간을 보장하지 않으므로, 릴레이를 떨어뜨리는 주체는 베어메탈 MCU 입니다. 상위 노드(Jetson·Raspberry Pi)가 죽어도 STM32 는 통신 두절을 스스로 감지해 차단합니다(COMM_TRIP).


1. 시스템 내 위치

 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 에코)

2. 하드웨어

보드: 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 대기 위치 = 안전측(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 이 스톨되어 전류가 계속 흐릅니다.


3. 빌드 & 플래싱

툴체인은 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 재생성 시 덮어써집니다).

4. 코드 구조

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 에서 하고 재생성합니다.


5. 실행 구조 — 협력형 슈퍼루프

main.cUSER 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 + SLOTmillis() 가 오버플로하는 약 49.7 일 시점에 깨집니다(unsigned 뺄셈은 wrap-safe).

6. 링크 프로토콜

프레임 레이아웃은 호스트 쪽 구현과 바이트 단위로 일치해야 합니다. 한쪽만 고치면 CRC 가 통과하지 못해 링크가 조용히 죽습니다.

수신 — Pi → STM32 (12 B, Little-endian)

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) 대상

송신 — STM32 → Pi (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 를 통과한 프레임에서만 갱신합니다. 깨진 바이트로 타임아웃이 리셋되면 "쓰레기 수신 = 통신 정상" 이 되어버리기 때문입니다.


7. 안전 상태 머신

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 를 늦게 켰다는 이유로 버튼을 눌러야 풀리는 래치를 걸면 브링업이 매번 막히기 때문입니다.

비전 판정 — 두 경로의 max

fsmEvalVision() 은 두 값을 계산해 높은 쪽을 택합니다.

  1. STATE 경로 — Jetson 이 이미 내린 판정. 이것이 있어 Jetson 화면이 DANGER 면 STM32 도 반드시 DANGER 입니다.
  2. AREA 경로 — STM32 가 같은 임계값으로 다시 계산한 면적. Jetson 판정 로직이 바뀌거나 state 가 오염돼도 숫자만으로 차단할 수 있는 독립 경로입니다.

그 위에서 zone != 0(충전 단자 침범)은 면적과 무관하게 무조건 DANGER 입니다. state == SENSOR_ERR 은 "누출 없음"이 아니라 "알 수 없음"으로 취급해 CAUTION 으로 올리고, 3 초 연속되면 LEAK 과 동급으로 승격합니다.

임계값 — Jetson config.yaml 과 동일한 값

매크로 의미 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_ERR
  • zone == 0
  • area < FSM_AREA_LEAK
  • Jetson 단계가 WARNING 미만

8. 설계 원칙

  • 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 / uartPrintfblocking 입니다. 안전 판정 경로에서 호출하지 마세요(텔레메트리 8 B ≈ 0.7 ms 는 차단 동작이 끝난 뒤라 무해합니다).
  • HW_UART_RX_BUF_LEN(256)은 반드시 2의 거듭제곱 — 마스크 기반 wrap 을 쓰고 % 를 쓰지 않습니다.

9. 브링업 셀프테스트

My/ap/ap.c 상단의 AP_USE_SELFTEST1 로 바꾸면 FSM 대신 셀프테스트가 돕니다(기본 0). 온보드 파란 버튼(B1)을 누를 때마다 다음 단계로 넘어가고, 각 단계 전환 시 항상 안전측으로 되돌립니다.

LED GREEN → LED YELLOW → LED YELLOW BLINK → LED RED BLINK
→ BUZZER → RELAY PUMP → RELAY DOCK → SERVO SWEEP → DONE

정상 동작과 동시에 켤 수 없습니다 — 리셋 버튼을 서로 소비하기 때문입니다(buttonGetPressed() 는 1회 소비형).


10. 하드웨어 확인 사항

  • 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() 사이에 코일이 잠깐 여자되는 틈을 없애기 위한 설정입니다.

11. 무엇을 바꿀 때 어디를 여는가

바꿀 것 열 파일
핀·포트·채널·보드레이트 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

12. 문서

문서 내용
README_구조.md 시스템 전체 구조·데이터 흐름·FSM 동작·검증 절차 (가장 상세한 설계 문서)
CLAUDE.md 이 디렉터리에서 작업할 때의 규약 요약
../CLAUDE.md 노드 간 통신 계약(프로토콜·FSM 열거 순서)과 두 프로젝트의 관계

About

stm32

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages