Files
2026-08-22 19:14:55 +09:00

648 lines
30 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# IR 광신호 Mock 프로토타입 설계 정리
**대상 과제:** IR 광신호 기반 무선 토양수분 센서 (농촌진흥청 국립원예특작과학원)
**본 문서 범위:** 격자 지점별 LED 블링크 횟수를 카메라로 카운트하는 벤치 프로토타입
**작성 기준일:** 2026-08-22
**개정:** rev.2 — 온·습도 비트 인코딩 → **습도 단일 값, 블링크 카운트 방식**으로 변경
**구현 상태:** 7장의 펌웨어가 저장소에 구현되고 빌드 검증까지 완료됨(2026-08-22). CubeMX 생성 결과와 설계 간 불일치 2건을 구현 중 발견·수정 — [6.7절](#67-cubemx-생성-결과-검증-중-발견된-오차) 참조.
---
## 1. 프로토타입 목표
각 식물 팟에 습도 센서가 내장되고, 그 값을 IR LED 광신호로 변환해 외부 카메라가 판독하는 시스템을 검증한다.
**본 단계에서 검증하는 것은 값의 의미가 아니라 계수(計數)의 신뢰성이다.**
- 실제 습도값은 사용하지 않는다
- 온도는 다루지 않는다
- 카메라가 **격자 각 지점의 깜박임 횟수를 정확히 세는지**만 확인한다
배터리 수명, 방수, 양산성은 본 단계에서 고려하지 않는다.
### 1.1 카운트 방식을 채택한 의의
비트 인코딩 대비 이점이 분명하다.
| 항목 | 비트 인코딩 | 블링크 카운트 |
|---|---|---|
| 프레임 정렬 | 심볼 경계 판정 필요 | **불필요** |
| 임계값 | 비트마다 판정 | 지점마다 1회 |
| 프레임 누락 | 심볼 전체 손실 | 상태 유지 구간이 길어 영향 미미 |
| 검증 대상 | 광학계 + 프로토콜 혼재 | **광학계만 순수 분리** |
이번 단계에서 알고 싶은 것은 "카메라가 9개 지점을 안정적으로 분리 해상하고 각 지점의 시간적 상태를 추적할 수 있는가"이다. 카운트 방식은 프로토콜 계층을 걷어내고 이 질문에만 답한다.
또한 **고정 창(window) 내의 카운트는 곧 주파수**이므로, 제안서의 1~6Hz 주파수 변조 방식으로 자연스럽게 확장된다. 버린 설계가 아니라 하위 단계다.
---
## 2. 확정된 설계 결정
| 항목 | 결정 | 근거 |
|---|---|---|
| MCU 보드 | NUCLEO-L073RZ (STM32L073RZ) | 기 확보 |
| LED | Siemens LD274 (950nm) × 9 | 기 확보, 추후 변경 예정 |
| 카메라 필터 | 950nm, C 마운트 | LD274 피크 파장과 정합 |
| 셔터 방식 | 글로벌 셔터 | 확인 완료 |
| 격자 구성 | 3×3, 9개 지점 | 각 지점이 팟 1개에 대응 |
| 전송 방식 | **지점별 블링크 카운트** | 본 개정 |
| 구동 방식 | GPIO 오픈 드레인 직결 | 근거리라 저전류로 충분 |
| 테스트 거리 | 1 ~ 1.5 m | 블롭 분리 여유 확보 |
### 2.1 기본 카메라 필터 제거 건
카메라에 기본 장착돼 있던 필터(IR-cut 추정)를 제거하고, 별도 950nm 필터를 C 마운트로 장착했다. 제거한 기본 필터는 이후 설계와 무관하다.
---
## 3. LED 특성 (LD274) 및 그 함의
### 3.1 데이터시트 주요 값
| 항목 | 값 |
|---|---|
| 피크 파장 λ_peak | 950 nm |
| 스펙트럼 폭 (50%) | 55 nm |
| **반각 φ** | **±10°** |
| 방사강도 I_e (I_F = 100 mA) | 최소 50 mW/sr |
| 순방향 전압 V_F (I_F = 100 mA) | 1.30 V (max 1.5 V) |
| 최대 연속 순방향 전류 | 100 mA |
| 패키지 | 5 mm (T-1 3/4), 지름 5.1 mm |
| 스위칭 시간 t_r, t_f | 1 µs |
스위칭 시간 1 µs는 본 설계의 200 ms 상태 유지 시간 대비 5자리수 여유이므로 전혀 제약이 되지 않는다.
### 3.2 파장 정합 — 양호
950nm 필터와 LD274의 950nm 피크가 일치한다. 스펙트럼 폭 55nm를 감안해도 에너지가 대략 920~990nm에 몰려 있어 필터 통과에 문제가 없다.
### 3.3 반각 ±10° — 최대 리스크 요인
LD274는 TV 리모컨용으로 설계된 협각 부품이다. 방사 특성상 ±10°에서 50%로 떨어지고 약 15° 이후 급락한다.
거리별 50% 빔 지름:
| 거리 | 빔 지름 |
|---|---|
| 1 m | 0.35 m |
| 3 m | 1.06 m |
| 10 m | 3.5 m |
**문제의 본질은 거리가 아니라 조준이다.** 팟이 흩어져 있고 카메라가 한쪽에 있으면 팟마다 LED가 향해야 할 방향이 전부 다르다. LED를 모두 수직으로 세우면 카메라 바로 아래 팟만 인식된다.
벤치 테스트(카메라를 LED 정면에 배치)에서는 문제가 되지 않으나, **양산 부품 선정 시 반각을 1순위 기준으로 삼아야 한다.** 파장과 전류는 나중에 맞출 수 있지만 반각은 고칠 수 없다. 권장: ±20~25° 급 광각 부품.
### 3.4 펄스 구동은 채택하지 않음
서지 3A, 펄스 내량상 D=0.05에서 1A까지 가능하나, 방사강도 그래프상 1A에서 광량은 100mA 대비 약 7배에 그친다(전류는 10배). 효율이 떨어진다.
카메라는 노출 시간 동안 에너지를 적분하므로, 같은 평균 전류라면 DC 구동이 유리하다. 펄스가 유리한 경우는 카메라 노출 창에 동기하여 구동할 때뿐인데, 프리러닝 카메라에서는 동기를 잡을 수 없다.
**대안: 카메라 노출 시간을 늘린다.** 33ms 프레임에서 노출을 1ms → 20ms로 늘리면 LED 에너지를 20배 적분한다. 950nm 필터가 주변광을 이미 억제하고 있다.
---
## 4. 하드웨어 구성
### 4.1 구동 전류 및 저항 산출
벤치 1m 환경에서는 오히려 **과다 광량이 문제**가 된다. 포화된 블롭이 번지면(blooming) 인접 지점과 뭉쳐 격자 분리가 깨진다.
```
목표 전류: 6 mA
V_F ≈ 1.1 V (순방향 특성 그래프의 저전류 영역 판독값)
V_OL ≈ 0.4 V (STM32L0 오픈 드레인 Low 출력)
R = (3.3 - 1.1 - 0.4) / 0.006 ≈ 300 Ω → 330 Ω (E24)
```
- 9개 합계: 약 54 mA
- 밝기 부족 시 220 Ω으로 하향 (합계 약 72 mA)
거리별 조도 추정 (8 mA 기준, I_e ≈ 4 mW/sr):
| 거리 | 조도 | 판단 |
|---|---|---|
| 1 m | 4 mW/m² | 20ms 노출로 충분히 포화 |
| 3 m | 0.44 mW/m² | 가능, 노출 조정 필요 |
| 10 m | 0.04 mW/m² | 외부 드라이버 필요 |
**결론: 벤치 범위에서는 MOSFET·ULN2803A 등 외부 드라이버가 불필요하다.**
### 4.2 GPIO 전류 한계
STM32L073 GPIO는 정규 스펙에서 핀당 ±8 mA, 비정규 V_OL/V_OH를 감수하면 ±15 mA까지 sink/source 가능하다. 6 mA 구동은 정규 스펙 내에 안전하게 들어간다.
전체 I/O 합산 전류(ΣI_VSS) 한계는 데이터시트 절대최대정격 표에서 확인할 것.
**카운트 방식의 부수적 이점:** 지점마다 카운트가 다르므로 9개가 동시에 켜져 있는 순간은 첫 번째 블링크뿐이고, 이후로는 켜진 개수가 점차 줄어든다. 평균 전류가 최대치보다 낮다.
### 4.3 전원 경로
LED 전류를 Nucleo의 3.3V 핀에서 뽑는 것은 무방하나(54mA 수준), 향후 전류를 크게 올릴 경우 주의가 필요하다.
- ST-LINK USB 급전 시 열거 전에는 100 mA 제한
- 온보드 LDO를 MCU와 공유
전류를 20mA/개 이상으로 올릴 경우 **5V 핀 또는 별도 전원에서 공급하고 GND만 공유**한다. 이때는 반드시 MOSFET 또는 ULN2803A를 경유해야 한다 — **PC0~PC5는 ADC 겸용 핀이라 5V 톨러런트가 아니다.**
### 4.4 LED 물리 배치
**3×3 배열, 피치 10 mm**
LD274 패키지 지름이 5.1 mm이므로 몸통 간 5 mm 간격이 확보된다.
1 m 거리, FHD, 6 mm 렌즈 기준 약 2 px/mm:
| 항목 | 픽셀 |
|---|---|
| LED 중심 간 거리 (10 mm) | 약 21 px |
| 렌즈 돔 블롭 (5 mm) | 약 10 px |
| 블롭 사이 어두운 골 | 약 11 px |
3 m로 물러나면 분리 7 px, 블롭 3.4 px로 제안서의 "3×3 픽셀 이상 투영" 조건을 겨우 만족한다. **벤치는 1~1.5 m 권장.**
### 4.5 회로 요약
```
VDD 3.3V ──┬── LED(애노드) ... LED(캐소드) ── 330Ω ── PC0 (오픈 드레인)
├── LED ── 330Ω ── PC1
├── ...
└── LED ── 330Ω ── PC8
오픈 드레인: 핀 Low = 전류 싱크 = 점등 / 핀 High = Hi-Z = 소등
```
---
## 5. 프로토콜 설계 — 블링크 카운트
### 5.1 격자 지점 매핑
각 핀은 격자 한 지점(팟 1개)에 대응한다. 비트 자리가 아니라 **독립된 측정 지점**이다.
| 격자 위치 | 핀 | 지점 번호 |
|---|---|---|
| 상단 좌 | PC0 | G1 |
| 상단 중 | PC1 | G2 |
| 상단 우 | PC3 | G3 |
| 중단 좌 | PC4 | G4 |
| 중단 중 | PC5 | G5 |
| 중단 우 | PC6 | G6 |
| 하단 좌 | PC7 | G7 |
| 하단 중 | PC8 | G8 |
| 하단 우 | PC2 | G9 |
이전 개정에서 PC0에 부여했던 토글 클록 역할은 **폐지**한다. 카운트 방식에서는 사이클 사이의 소등 구간이 그 역할을 대신하며, 9개 핀 전부를 데이터 지점으로 쓸 수 있다.
> **구현 노트:** 최초 CubeMX 생성 프로젝트는 G3~G8을 PC3~PC8에 배정하며 PC2를 비워둔 채였다(8지점만 구성됨, [6.7절](#67-cubemx-생성-결과-검증-중-발견된-오차)). 기존 G1~G8 배선을 그대로 두고 남은 PC2를 G9로 채워 9지점을 완성했다 — 핀 번호 순서상 G9가 G2와 G3 사이에 위치하지만, 격자 물리 배치와 핀 번호는 애초에 무관하므로 기능·회로에는 영향이 없다.
### 5.2 사이클 구조
```
├──────────── 버스트 구간 6.0 s ────────────┤├── 소등 2.0 s ──┤
│ │ │
G1 ▇▁▇▁▇▁ │ │ (3회)
G2 ▇▁▇▁▇▁▇▁▇▁ │ │ (5회)
G3 ▇▁ │ │ (1회)
... │ │
G9 ▇▁▇▁▇▁▇▁▇▁▇▁▇▁▇▁▇▁▇▁▇▁▇▁▇▁▇▁▇▁ │ │ (15회)
│ │
└──────────────── 1 사이클 = 8.0 s ─────────────────────────────┘
```
**모든 지점이 동시에 시작하고, 각자의 카운트만큼 깜박인 뒤 소등한다.** 시작 시각이 같으므로 디코더가 사이클 경계를 잡기 쉽다.
### 5.3 타이밍 파라미터
| 항목 | 값 | 30 fps 기준 |
|---|---|---|
| 틱(상태 유지) 주기 | 200 ms | 6 프레임 |
| 블링크 1회 (점등+소등) | 400 ms | 12 프레임 |
| 블링크 주파수 | 2.5 Hz | — |
| 최대 카운트 | 15회 | — |
| 버스트 구간 | 6.0 s (30 틱) | 180 프레임 |
| 소등 구간 | 2.0 s (10 틱) | 60 프레임 |
| **사이클 전체** | **8.0 s (40 틱)** | 240 프레임 |
### 5.4 왜 200 ms인가
**프레임 여유가 결정 요인이다.**
| 프레임레이트 | 상태당 프레임 수 | 판정 |
|---|---|---|
| 30 fps | 6 | 여유 |
| 15 fps (저조도 자동 강등) | 3 | 최소 허용 |
| 10 fps | 2 | 위험 |
상태당 최소 3프레임을 확보해야 경계 프레임 1개를 버려도 판정이 가능하다. 200 ms는 카메라가 15 fps로 떨어지더라도 살아남는 값이다.
나이퀴스트 한계(30 fps → 15 Hz)만 보면 훨씬 빠르게 가도 될 것 같지만, **한계 근처에서는 프레임 하나만 밀려도 카운트가 틀어진다.** 요구 갱신 주기가 시간당 1회이므로 속도를 버리고 여유를 사는 것이 옳다.
### 5.5 사이클 길이 8초의 의미
제안서 4.가의 "신호 송출 지속 시간 1회 구동당 8초"와 일치시켰다. 프로토타입 결과를 제안서 수치로 그대로 인용할 수 있다.
또한 6초 버스트 안의 최대 15회는 **2.5 Hz**이고, 제안서의 1~6 Hz 대역과 같은 스케일이다. 카운트 방식에서 주파수 변조 방식으로 넘어갈 때 타이밍 재설계가 필요 없다.
### 5.6 분해능
카운트 1~15의 15단계. 제안서 4.나의 "수분 0~70% 구간 5% 단위 14단계"와 정합한다.
카운트 0은 사용하지 않는다. 0은 "소등"과 구분되지 않아 **고장난 지점과 값이 0인 지점을 혼동**하게 되기 때문이다. 최소 1회는 반드시 깜박이게 하여, 한 번도 깜박이지 않은 지점은 이상으로 판정한다.
---
## 6. CubeMX 설정
### 6.1 Clock Configuration
CubeMX 신규 프로젝트의 기본값은 **MSI 2.097 MHz**다. 이 상태로 타이머 값을 넣으면 주기가 7.6배 느려지는데, **에러 없이 조용히 느려지기 때문에** 발견이 늦어진다. 반드시 확인할 것.
**변경 지점: System Clock Mux → HSI 16 선택 (한 곳)**
나머지는 기본값 `/1`이 그대로 맞으므로 손대지 않는다.
| 항목 | 설정값 |
|---|---|
| System Clock Mux | **HSI** |
| SYSCLK | 16 MHz |
| AHB Prescaler | /1 → HCLK 16 MHz |
| APB1 Prescaler | /1 → PCLK1 16 MHz |
| **APB1 timer clocks** | **16 MHz (배율 X 1)** |
| APB2 Prescaler | /1 |
| HSI RC 분주 (HSI16DIVEN) | /1 |
| USART2 Source Mux | PCLK1 |
| USART2CLK | 16 MHz |
| Flash Latency | 0 WS (자동) |
| Power Regulator | Range 1 (자동) |
**"Resolve Clock Issues" 버튼이나 HCLK 직접 입력은 피할 것.** CubeMX가 PLL 경로를 자동으로 잡아 불필요하게 PLL을 켜는 경우가 있다. Mux를 직접 클릭하는 편이 의도한 경로를 보장한다.
#### 타이머 2배 규칙
```
APB 분주 = 1 → 타이머 클록 = PCLK (16 MHz)
APB 분주 ≠ 1 → 타이머 클록 = PCLK × 2 (자동 2배)
```
APB1을 `/1`로 고정하면 이 규칙을 생각할 필요가 없다. 클록 트리의 `X 1` 표시가 확인 지점이다.
#### MSI가 아닌 HSI16을 쓰는 이유
전력이 무관한 프로토타입이므로 판단 기준은 정확도뿐이다.
| 클록원 | 정확도 | UART 115200 오차 |
|---|---|---|
| MSI 2.097 MHz | ±3% 이상 | USARTDIV 18.2 → 18, 약 1.1% + 클록 오차 |
| **HSI16** | ±1% 수준 | USARTDIV 138.9 → 139, **-0.08%** |
UART는 누적 오차 2.5% 부근에서 깨진다. **로그가 이번 테스트의 검증 수단**이므로 여기서 여유를 확보해야 한다.
### 6.2 GPIO (PC0 ~ PC8)
| 항목 | 설정값 | 이유 |
|---|---|---|
| GPIO mode | Output Open Drain | 요구 사양 |
| Pull-up/Pull-down | No pull-up and no pull-down | 풀업은 외부 LED+저항이 담당. 내부 풀업 시 소등 상태에서도 누설 |
| Maximum output speed | Low | 저속 신호에 고속 슬루 불필요, EMI·소비전류 감소 |
| **GPIO output level** | **High** | OD에서 High = Hi-Z = 소등. Low로 두면 부팅 즉시 9개 전부 점등 |
**User Label:**
```
PC0 → G1 PC1 → G2 PC2 → G9
PC3 → G3 PC4 → G4 PC5 → G5
PC6 → G6 PC7 → G7 PC8 → G8
```
`GPIO output level`을 High로 두는 것이 가장 실수하기 쉬운 지점이다. 푸시풀 습관대로 Low를 넣으면 리셋 직후부터 LED가 켜진 채로 있게 된다.
### 6.3 PC0~PC8 선택 이유
- **단일 포트 연속 배치** → `BSRR` 한 번의 쓰기로 9개를 완전히 동시에 전환 가능
- 회피 대상 핀과 충돌 없음: PA13/PA14(SWD), PA2/PA3(ST-LINK VCP), PC13(B1 버튼), PC14/PC15(LSE), PA5(LD2)
- NUCLEO-64 모포 헤더 실제 핀맵은 보드 문서에서 확인할 것
### 6.4 타이머
| 항목 | 값 |
|---|---|
| 타이머 | TIM6 |
| Prescaler | 15999 |
| Counter Period (ARR) | 199 |
| 결과 주기 | **200 ms** |
| NVIC | TIM6 global interrupt 활성화 |
```
f_TIM6 = 16,000,000 Hz
카운터 클록 = 16,000,000 / (15999+1) = 1,000 Hz
주기 = (199+1) / 1,000 = 0.200 s
```
TIM6를 선택한 이유는 기본 타이머(Basic Timer)라 GPIO 대체 기능을 전혀 점유하지 않기 때문이다. **PC6~PC8은 TIM3 채널과 겹치므로 TIM3는 사용하지 않는다.**
### 6.5 UART
| 항목 | 값 |
|---|---|
| USART2 | Asynchronous, 115200 8N1 |
| 용도 | 사이클별 정답 카운트 로그 (ST-LINK VCP 경유) |
**이 로그가 핵심이다.** MCU가 사이클마다 "G1=3, G2=5, …"를 찍어주면 디코더 결과와 대조하여 지점별 에러율을 정량화할 수 있다. 이것이 없으면 디코더 오류인지 광학계 오류인지 구분할 수 없다.
### 6.6 (선택) MCO — 클록 실측용
MCO Source Mux를 SYSCLK에 두고 **PA8을 RCC_MCO로 활성화, 분주 `/16`** 으로 설정하면 PA8에 정확히 **1 MHz**가 나온다.
```
16 MHz / 16 = 1.000 MHz
```
`HAL_RCC_GetSysClockFreq()`는 레지스터 설정값을 계산한 것이라 **실제 발진 주파수를 검증하지 못한다.** MCO는 물리적 증거를 준다. PA8은 CN9의 D7에 나와 있고 PC0~PC8과 충돌하지 않는다. 브링업 1회 측정 후 꺼도 무방하다.
### 6.7 CubeMX 생성 결과 검증 중 발견된 오차
7장 펌웨어를 실제 코드에 적용하는 과정에서 `.ioc`가 생성한 `gpio.c`를 본 문서의 6.2절 스펙과 대조한 결과, 두 가지가 어긋나 있었다. 둘 다 `.ioc``gpio.c`/`main.h`에서 함께 고쳐 CubeMX로 재생성해도 유지되도록 했다.
| 항목 | CubeMX 생성 결과 | 문제 | 조치 |
|---|---|---|---|
| GPIO output level 초기값 | `GPIO_PIN_RESET` (Low) | 오픈 드레인에서 Low = 점등이므로, **리셋 직후 9개 LED가 전부 켜진 채로 부팅**된다. 6.2절이 "가장 실수하기 쉬운 지점"으로 미리 경고한 바로 그 실수 | `GPIO_PIN_SET` (High)으로 수정 |
| 지점 수 | G1~G8 (PC0, PC1, PC3~PC8) 8개만 구성, **PC2 미사용** | 3×3=9지점 설계와 불일치. PC2가 비어 연속 배치가 깨지므로 7.2절이 전제하는 BSRR 단일 쓰기 방식도 그대로는 성립하지 않음 | PC2를 G9로 추가해 PC0~PC8 9핀 연속 배치 완성 (기존 G1~G8 배선은 유지 — 상세는 [5.1절 구현 노트](#51-격자-지점-매핑)) |
두 항목 모두 최초 CubeMX 프로젝트 생성 시점(설계 확정 이전)의 산출물이 그대로 남아 있던 것으로 보이며, 이번 구현 작업 중 발견했다.
---
## 7. 펌웨어
### 7.1 전체 코드
아래 코드는 [`Core/Src/main.c`](../Core/Src/main.c)에 CubeMX의 `USER CODE BEGIN/END` 구획을 따라 그대로 구현되어 있다 (재생성 시에도 보존됨). 실제 배치는 매크로·전역변수 → `PV`, `load_counts`/`drive`/`__io_putchar``0`, `HAL_TIM_PeriodElapsedCallback``4`, 초기 구동 호출 → `main()``2` 구획이다. `app_start()`는 설명용 표기이며 실제로는 별도 함수로 분리하지 않고 `main()`의 해당 구획에 인라인되어 있다.
```c
#define LED_MASK 0x01FFu /* PC0..PC8 */
#define N_POINTS 9
#define COUNT_MIN 1
#define COUNT_MAX 15
#define TICKS_BURST 30 /* 15회 × 2틱 = 6.0 s */
#define TICKS_IDLE 10 /* 2.0 s */
#define TICKS_CYCLE (TICKS_BURST + TICKS_IDLE)
static uint16_t tick = 0;
static uint16_t cycle = 0;
static uint8_t cnt[N_POINTS]; /* 지점별 목표 블링크 횟수 */
/* 결정적 회전 패턴: 지점마다 다른 값, 사이클마다 1씩 이동 */
static void load_counts(void)
{
for (uint8_t k = 0; k < N_POINTS; k++) {
cnt[k] = (uint8_t)(((cycle + k) % COUNT_MAX) + COUNT_MIN);
}
}
static void drive(uint16_t bits)
{
/* 오픈 드레인: 0 = 점등이므로 반전. 9핀 원자적 동시 전환 */
GPIOC->BSRR = ((uint32_t)( bits & LED_MASK) << 16)
| ((uint32_t)(~bits & LED_MASK));
}
/* printf -> USART2 (ST-LINK VCP). syscalls.c의 weak _write()가 문자마다 호출한다. */
int __io_putchar(int ch)
{
HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
return ch;
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance != TIM6) return;
uint16_t bits = 0;
/* 버스트 구간의 짝수 틱에서만 점등.
지점 k의 j번째 블링크는 틱 2j(점등) / 2j+1(소등)을 점유 */
if (tick < TICKS_BURST && (tick & 1u) == 0u) {
for (uint8_t k = 0; k < N_POINTS; k++) {
if (tick < (uint16_t)cnt[k] * 2u) {
bits |= (uint16_t)(1u << k);
}
}
}
drive(bits);
if (++tick >= TICKS_CYCLE) {
tick = 0;
cycle++;
load_counts();
printf("CYCLE=%u G:", cycle);
for (uint8_t k = 0; k < N_POINTS; k++) printf(" %2u", cnt[k]);
printf("\r\n");
}
}
/* main()의 while 루프 진입 전 */
void app_start(void)
{
load_counts();
drive(0); /* 전 지점 소등 */
HAL_TIM_Base_Start_IT(&htim6);
}
```
### 7.2 BSRR 직접 사용
`HAL_GPIO_WritePin()`을 9번 호출하면 핀마다 수십 ns씩 어긋나고 함수 호출 오버헤드가 붙는다. BSRR 직접 쓰기는 원자적 1사이클 연산이다.
블링크 카운트 방식에서는 지점 간 수십 ns 오차가 실질적 문제는 아니지만, 코드가 더 짧고 명확하므로 유지한다.
### 7.3 Mock 값은 난수가 아닌 결정적 회전
`cnt[k] = ((cycle + k) % 15) + 1`
| 사이클 | G1 | G2 | G3 | … | G9 |
|---|---|---|---|---|---|
| 1 | 1 | 2 | 3 | … | 9 |
| 2 | 2 | 3 | 4 | … | 10 |
| 3 | 3 | 4 | 5 | … | 11 |
| … | | | | | |
| 15 | 15 | 1 | 2 | … | 8 |
**이 패턴이 주는 것:**
- 한 사이클 안에서 9개 지점이 **전부 다른 값**을 가지므로, 디코더가 지점을 혼동하면 즉시 드러난다
- 사이클마다 값이 1씩 이동하므로 **특정 지점만 계속 틀리는지**(위치 의존 오류) 판별 가능
- 15 사이클 = **120초**면 모든 지점이 모든 카운트를 한 번씩 경험한다
난수 mock으로는 이 중 어느 것도 알 수 없다.
### 7.4 printf 리다이렉트
`syscalls.c`(CubeMX 생성)는 `_write()`가 문자 단위로 `__io_putchar()`를 호출하도록 이미 `weak` 참조를 걸어두고 있으나, 정작 `__io_putchar()` 본체는 어디에도 정의돼 있지 않았다. `main.c``HAL_UART_Transmit(&huart2, ...)`으로 구현해 연결했다 (7.1절 코드 참조).
빌드는 newlib-nano(`--specs=nano.specs`)를 링크하므로 `_write()` 경로가 실제로 사용된다. `__io_putchar()``weak extern`으로 선언돼 있어 정의가 없어도 링크 자체는 성공하지만, 그 상태에서 `printf()`를 호출하면 널 포인터를 호출하는 셈이라 하드폴트로 즉시 정지한다 — "링크되니 괜찮겠지"로 넘어가면 안 되는 지점이다.
### 7.5 빌드 검증
```
RAM: 2224 B / 20 KB (10.86%)
FLASH: 17232 B / 192 KB (8.76%)
```
**툴체인 주의:** Homebrew의 `arm-none-eabi-gcc`는 newlib 헤더(`stdint.h` 등)가 빠져 있어 `stm32l0xx_hal_tim.c` 등 표준 헤더를 include하는 모든 파일에서 빌드가 실패한다. STM32CubeCLT에 포함된 `GNU-tools-for-STM32`(`/opt/ST/STM32CubeCLT_*/GNU-tools-for-STM32/bin`)를 PATH에 우선 배치해야 한다. `cmake/gcc-arm-none-eabi.cmake``arm-none-eabi-gcc`를 PATH에서만 찾으므로 툴체인 파일 수정 없이 PATH 순서만으로 해결된다.
```
export PATH="/opt/ST/STM32CubeCLT_*/GNU-tools-for-STM32/bin:$PATH"
cmake --preset Debug
cmake --build build/Debug
```
---
## 8. 디코더 설계 지침
### 8.1 ROI 자동 검출
버스트 구간에서는 모든 지점이 최소 1회 점등하므로, **버스트 구간 프레임들의 최대값 투영(max projection)** 을 취하면 9개 블롭이 한 장에 모두 나타난다.
```
roi_map = max(frame[t] for t in burst_window)
→ 임계값 이진화 → 연결 요소 라벨링 → 9개 중심 좌표
```
수동 ROI 지정보다 안정적이고, 격자가 흔들려도 사이클마다 재계산할 수 있다.
### 8.2 사이클 경계 검출
소등 구간 2.0초(60프레임)는 어떤 블링크 간격(200ms, 6프레임)보다 압도적으로 길다. **전 ROI가 동시에 어두운 상태가 30프레임 이상 지속되면 사이클 경계**로 판정한다.
### 8.3 카운트 산출
지점마다 독립적으로 처리한다.
1. ROI 내 픽셀 밝기 평균 → 시계열
2. 임계값으로 이진화 (임계값은 해당 지점의 min/max 중간값)
3. **상승 에지(off→on) 개수**를 센다
4. 각 on/off 구간이 3프레임 미만이면 노이즈로 간주하고 병합
3번에서 하강 에지가 아닌 상승 에지를 세는 이유는, 버스트 종료 후 소등 상태가 이어지므로 마지막 하강 에지가 사이클 경계와 겹칠 수 있기 때문이다.
### 8.4 검증 지표
| 지표 | 산출 |
|---|---|
| 지점별 정확도 | (일치 사이클 수) / (전체 사이클 수), 9개 각각 |
| 카운트별 정확도 | 카운트 1~15별로 집계 — 큰 값에서 오류가 느는지 확인 |
| 오류 방향 | 과소 계수(블링크 놓침) vs 과다 계수(노이즈) 구분 |
**과소 계수가 많으면** 노출 부족 또는 임계값이 높음. **과다 계수가 많으면** blooming으로 인접 지점 간섭 또는 임계값이 낮음. 오류의 방향이 곧 조치 방향이다.
---
## 9. 카메라 설정 — 반드시 수동 고정할 항목
여기서 실패하는 경우가 많다.
| 항목 | 조치 | 이유 |
|---|---|---|
| 자동 노출 (AE) | **끄기** | 켜진 LED 개수가 사이클 내내 변하므로 노출이 계속 요동침 |
| 자동 게인 (AGC) | **끄기** | 동일 |
| 자동 화이트밸런스 | **끄기** | 950nm 단색이라 무의미하고 방해만 됨 |
| 프레임레이트 자동 조정 | **끄기** | 어두우면 자동으로 15fps로 떨어지는 카메라가 많음 |
| 노출 시간 | 20 ms 부근에서 시작 후 튜닝 | 블롭 중심만 포화되고 번지지 않는 지점 탐색 |
**AE는 카운트 방식에서 특히 치명적이다.** 버스트 초반에는 9개가 켜져 있다가 후반으로 갈수록 줄어드는데, AE가 이를 보상하려 게인을 올리면 남은 지점의 블롭이 커져 인접 지점을 침범한다. 카운트가 큰 지점일수록 후반부에 오류가 몰리는 형태로 나타난다.
노출 튜닝이 이번 벤치 테스트에서 가장 시간이 많이 소요될 지점이다.
---
## 10. 브링업 절차
1. **클록 검증** — 코드 생성 후 UART 로그가 **8초마다 1줄** 나오는지 확인 (MCO를 켰다면 PA8에서 1 MHz 실측)
2. **LED 1개(PC0)만 330Ω으로 연결** → 스마트폰 카메라로 점등 확인 (필터 없이)
3. **950nm 필터 장착** → 실제 카메라로 블롭 확인, 노출 튜닝
4. **9개 전부 연결** → 버스트 구간을 max projection 하여 **9개 블롭이 분리되는지** 확인
5. **1개 사이클 녹화** → 수동으로 프레임을 넘겨가며 G1의 블링크를 육안 계수, UART 로그와 대조
6. **디코더 자동 계수** → 15 사이클(120초) 이상 녹화 후 정확도 산출
4번에서 블롭이 뭉치면 피치를 넓히거나 카메라 거리를 조정한다. **이 단계가 통과되지 않으면 디코더를 아무리 수정해도 소용없다.**
5번을 건너뛰지 말 것. 육안 계수 1회가 디코더 디버깅 수 시간을 절약한다.
---
## 11. 제안서 수정 필요 항목
프로토타입 완료 후 문서 갱신 시 놓치기 쉬운 지점들.
| 위치 | 현재 기재 | 수정 방향 |
|---|---|---|
| 2. 다. 4단계 | 850nm 파장의 IR LED | 950nm |
| 3. 가. 광출력 발광부 | 고효율 850nm IR LED | 950nm |
| 4. 가. 신호 발광 소자 | 850nm 고출력 IR 광 LED | 950nm |
| 4. 나. 수집 카메라 사양 | 850nm 밴드패스 필터 일체형 | 950nm 필터, C 마운트 별도 장착 |
| 6. BOM 2번 | 850nm 파장 5mm 외형 | 950nm |
| 2. 다. 1단계 | Stop2 초저전력 슬립 모드 | **Stop2는 STM32L4 계열 용어.** L0는 Stop 모드 |
**Stop 모드 관련 보충:** STM32L0의 Stop 모드는 RTC 유지 시 수 µA대로, 제안서에 기재된 5µA보다 오히려 유리하다. 수치는 문제없으나 모드 명칭이 발주처 기술 검토에서 지적될 수 있다.
---
## 12. 본 프로토타입 이후 결정할 사항
### 12.1 카운트 → 주파수 변조 전환
제안서 본안은 1~6 Hz 주파수 변조 + FFT 방식이다. 본 프로토타입의 카운트 방식은 그 하위 단계이며, 전환 시 바뀌는 것은 다음과 같다.
| 항목 | 프로토타입 (카운트) | 제안서 본안 (주파수) |
|---|---|---|
| 송출 | 6초간 N회 블링크 | 8초간 f Hz 연속 점멸 |
| 판독 | 상승 에지 계수 | FFT / 제로크로싱 |
| 노이즈 내성 | 프레임 누락에 취약 | 주파수 영역이라 강함 |
| 필요 프레임레이트 | 30 fps로 충분 | 6 Hz 판독 시 최소 15 fps 이상 |
카운트 방식에서 확보한 ROI 검출·임계값 산출 코드는 그대로 재사용된다. 바뀌는 것은 시계열 처리 단계뿐이다.
### 12.2 LED 부품 교체 기준 (우선순위 순)
1. **반각** — ±20~25° 이상. 이것이 실제 설치 가능성을 좌우함
2. 파장 — 필터와 정합 (현 필터 유지 시 940~950nm)
3. 방사강도 — 목표 거리에서의 조도로 역산
4. 패키지 — 3×3 배치 시 피치 확보 가능한 크기
### 12.3 미결 항목
- **실제 목표 인식 거리** — 벤치 1m 검증 후 온실 실환경 거리 확정 필요. 3m 초과 시 외부 드라이버 및 광각 LED 필수
- **격자 지점 수 확장** — 9개는 MCU 핀 수 제약이 아니라 프로토타입 편의값. 제안서의 "화각 내 2,000개 이상"까지 가려면 지점당 1 MCU 구조가 되므로 본 프로토타입과 아키텍처가 다름
- **팟 배치와 카메라 위치 기하** — 조준 문제의 실제 발생 여부는 이 기하에 달려 있음
- **저전력 전환** — LPTIM1 + LSE 기반 구조. NUCLEO-L073RZ의 LSE 수정(X2) 실장 여부 확인 필요
---
## 부록: 핵심 수치 요약
```
LED : LD274, 950nm, 반각 ±10°, V_F ≈ 1.1V @ 6mA
저항 : 330Ω (대안 220Ω)
구동 전류 : 6 mA/개, 최대 합계 54 mA
배치 : 3×3, 피치 10mm, 9개 격자 지점
거리 : 1 ~ 1.5 m
틱 주기 : 200 ms (30fps 기준 6프레임)
블링크 1회 : 400 ms (2.5 Hz)
카운트 범위 : 1 ~ 15 (0은 미사용)
버스트 구간 : 6.0 s (30 틱)
소등 구간 : 2.0 s (10 틱)
사이클 : 8.0 s (40 틱)
전 조합 커버리지 : 15 사이클 = 120 초
MCU : STM32L073RZ
클록 : HSI16 → SYSCLK 16MHz, AHB /1, APB1 /1, 타이머 X1
TIM6 : PSC 15999 / ARR 199 → 200 ms
GPIO : PC0~PC8, Open Drain, No pull, Low speed, Output level High
로그 : USART2 115200 8N1, 사이클당 1줄
```