update firmware

This commit is contained in:
Dongubak
2026-08-22 19:14:55 +09:00
parent f75318d889
commit 226b081aee
11 changed files with 887 additions and 149 deletions
+8 -8
View File
File diff suppressed because one or more lines are too long
+18 -16
View File
@@ -61,28 +61,30 @@ void Error_Handler(void);
#define B1_GPIO_Port GPIOC #define B1_GPIO_Port GPIOC
#define MCO_Pin GPIO_PIN_0 #define MCO_Pin GPIO_PIN_0
#define MCO_GPIO_Port GPIOH #define MCO_GPIO_Port GPIOH
#define CLK_Pin GPIO_PIN_0 #define G1_Pin GPIO_PIN_0
#define CLK_GPIO_Port GPIOC #define G1_GPIO_Port GPIOC
#define T3_Pin GPIO_PIN_1 #define G2_Pin GPIO_PIN_1
#define T3_GPIO_Port GPIOC #define G2_GPIO_Port GPIOC
#define T2_Pin GPIO_PIN_3 #define G9_Pin GPIO_PIN_2
#define T2_GPIO_Port GPIOC #define G9_GPIO_Port GPIOC
#define G3_Pin GPIO_PIN_3
#define G3_GPIO_Port GPIOC
#define USART_TX_Pin GPIO_PIN_2 #define USART_TX_Pin GPIO_PIN_2
#define USART_TX_GPIO_Port GPIOA #define USART_TX_GPIO_Port GPIOA
#define USART_RX_Pin GPIO_PIN_3 #define USART_RX_Pin GPIO_PIN_3
#define USART_RX_GPIO_Port GPIOA #define USART_RX_GPIO_Port GPIOA
#define LD2_Pin GPIO_PIN_5 #define LD2_Pin GPIO_PIN_5
#define LD2_GPIO_Port GPIOA #define LD2_GPIO_Port GPIOA
#define T1_Pin GPIO_PIN_4 #define G4_Pin GPIO_PIN_4
#define T1_GPIO_Port GPIOC #define G4_GPIO_Port GPIOC
#define H3_Pin GPIO_PIN_5 #define G5_Pin GPIO_PIN_5
#define H3_GPIO_Port GPIOC #define G5_GPIO_Port GPIOC
#define H2_Pin GPIO_PIN_6 #define G6_Pin GPIO_PIN_6
#define H2_GPIO_Port GPIOC #define G6_GPIO_Port GPIOC
#define H1_Pin GPIO_PIN_7 #define G7_Pin GPIO_PIN_7
#define H1_GPIO_Port GPIOC #define G7_GPIO_Port GPIOC
#define H0_Pin GPIO_PIN_8 #define G8_Pin GPIO_PIN_8
#define H0_GPIO_Port GPIOC #define G8_GPIO_Port GPIOC
#define TMS_Pin GPIO_PIN_13 #define TMS_Pin GPIO_PIN_13
#define TMS_GPIO_Port GPIOA #define TMS_GPIO_Port GPIOA
#define TCK_Pin GPIO_PIN_14 #define TCK_Pin GPIO_PIN_14
+6 -6
View File
@@ -50,8 +50,8 @@ void MX_GPIO_Init(void)
__HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE();
/*Configure GPIO pin Output Level */ /*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOC, CLK_Pin|T3_Pin|T2_Pin|T1_Pin HAL_GPIO_WritePin(GPIOC, G1_Pin|G2_Pin|G3_Pin|G4_Pin
|H3_Pin|H2_Pin|H1_Pin|H0_Pin, GPIO_PIN_RESET); |G5_Pin|G6_Pin|G7_Pin|G8_Pin|G9_Pin, GPIO_PIN_SET);
/*Configure GPIO pin Output Level */ /*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_RESET);
@@ -62,10 +62,10 @@ void MX_GPIO_Init(void)
GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(B1_GPIO_Port, &GPIO_InitStruct); HAL_GPIO_Init(B1_GPIO_Port, &GPIO_InitStruct);
/*Configure GPIO pins : CLK_Pin T3_Pin T2_Pin T1_Pin /*Configure GPIO pins : G1_Pin G2_Pin G3_Pin G4_Pin
H3_Pin H2_Pin H1_Pin H0_Pin */ G5_Pin G6_Pin G7_Pin G8_Pin G9_Pin */
GPIO_InitStruct.Pin = CLK_Pin|T3_Pin|T2_Pin|T1_Pin GPIO_InitStruct.Pin = G1_Pin|G2_Pin|G3_Pin|G4_Pin
|H3_Pin|H2_Pin|H1_Pin|H0_Pin; |G5_Pin|G6_Pin|G7_Pin|G8_Pin|G9_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
+60 -2
View File
@@ -24,7 +24,7 @@
/* Private includes ----------------------------------------------------------*/ /* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */ /* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */ /* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/ /* Private typedef -----------------------------------------------------------*/
@@ -45,7 +45,17 @@
/* Private variables ---------------------------------------------------------*/ /* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */ /* USER CODE BEGIN PV */
#define LED_MASK 0x01FFu /* PC0..PC8 */
#define N_POINTS 9
#define COUNT_MIN 1
#define COUNT_MAX 15
#define TICKS_BURST 30 /* 15회 x 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]; /* 지점별 목표 블링크 횟수 */
/* USER CODE END PV */ /* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/ /* Private function prototypes -----------------------------------------------*/
@@ -56,7 +66,27 @@ void SystemClock_Config(void);
/* Private user code ---------------------------------------------------------*/ /* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */ /* USER CODE BEGIN 0 */
/* printf -> USART2 (ST-LINK VCP) */
int __io_putchar(int ch)
{
HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
return ch;
}
/* 결정적 회전 패턴: 지점마다 다른 값, 사이클마다 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));
}
/* USER CODE END 0 */ /* USER CODE END 0 */
/** /**
@@ -91,7 +121,9 @@ int main(void)
MX_TIM6_Init(); MX_TIM6_Init();
MX_USART2_UART_Init(); MX_USART2_UART_Init();
/* USER CODE BEGIN 2 */ /* USER CODE BEGIN 2 */
load_counts();
drive(0); /* 전 지점 소등 */
HAL_TIM_Base_Start_IT(&htim6);
/* USER CODE END 2 */ /* USER CODE END 2 */
/* Infinite loop */ /* Infinite loop */
@@ -153,7 +185,33 @@ void SystemClock_Config(void)
} }
/* USER CODE BEGIN 4 */ /* USER CODE BEGIN 4 */
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");
}
}
/* USER CODE END 4 */ /* USER CODE END 4 */
/** /**
+33 -17
View File
@@ -26,6 +26,7 @@ Mcu.Pin15=PA13
Mcu.Pin16=PA14 Mcu.Pin16=PA14
Mcu.Pin17=VP_SYS_VS_Systick Mcu.Pin17=VP_SYS_VS_Systick
Mcu.Pin18=VP_TIM6_VS_ClockSourceINT Mcu.Pin18=VP_TIM6_VS_ClockSourceINT
Mcu.Pin19=PC2
Mcu.Pin2=PC15-OSC32_OUT Mcu.Pin2=PC15-OSC32_OUT
Mcu.Pin3=PH0-OSC_IN Mcu.Pin3=PH0-OSC_IN
Mcu.Pin4=PC0 Mcu.Pin4=PC0
@@ -34,7 +35,7 @@ Mcu.Pin6=PC3
Mcu.Pin7=PA2 Mcu.Pin7=PA2
Mcu.Pin8=PA3 Mcu.Pin8=PA3
Mcu.Pin9=PA5 Mcu.Pin9=PA5
Mcu.PinsNb=19 Mcu.PinsNb=20
Mcu.ThirdPartyNb=0 Mcu.ThirdPartyNb=0
Mcu.UserConstants= Mcu.UserConstants=
Mcu.UserName=STM32L073RZTx Mcu.UserName=STM32L073RZTx
@@ -73,17 +74,19 @@ PA5.GPIO_ModeDefaultOutputPP=GPIO_MODE_OUTPUT_PP
PA5.GPIO_PuPd=GPIO_NOPULL PA5.GPIO_PuPd=GPIO_NOPULL
PA5.Locked=true PA5.Locked=true
PA5.Signal=GPIO_Output PA5.Signal=GPIO_Output
PC0.GPIOParameters=GPIO_PuPd,GPIO_Label,GPIO_ModeDefaultOutputPP PC0.GPIOParameters=GPIO_PuPd,GPIO_Label,GPIO_ModeDefaultOutputPP,PinState
PC0.GPIO_Label=CLK PC0.GPIO_Label=G1
PC0.GPIO_ModeDefaultOutputPP=GPIO_MODE_OUTPUT_OD PC0.GPIO_ModeDefaultOutputPP=GPIO_MODE_OUTPUT_OD
PC0.GPIO_PuPd=GPIO_NOPULL PC0.GPIO_PuPd=GPIO_NOPULL
PC0.Locked=true PC0.Locked=true
PC0.PinState=GPIO_PIN_SET
PC0.Signal=GPIO_Output PC0.Signal=GPIO_Output
PC1.GPIOParameters=GPIO_PuPd,GPIO_Label,GPIO_ModeDefaultOutputPP PC1.GPIOParameters=GPIO_PuPd,GPIO_Label,GPIO_ModeDefaultOutputPP,PinState
PC1.GPIO_Label=T3 PC1.GPIO_Label=G2
PC1.GPIO_ModeDefaultOutputPP=GPIO_MODE_OUTPUT_OD PC1.GPIO_ModeDefaultOutputPP=GPIO_MODE_OUTPUT_OD
PC1.GPIO_PuPd=GPIO_NOPULL PC1.GPIO_PuPd=GPIO_NOPULL
PC1.Locked=true PC1.Locked=true
PC1.PinState=GPIO_PIN_SET
PC1.Signal=GPIO_Output PC1.Signal=GPIO_Output
PC13.GPIOParameters=GPIO_Label,GPIO_ModeDefaultEXTI PC13.GPIOParameters=GPIO_Label,GPIO_ModeDefaultEXTI
PC13.GPIO_Label=B1 [Blue PushButton] PC13.GPIO_Label=B1 [Blue PushButton]
@@ -96,41 +99,54 @@ PC14-OSC32_IN.Signal=RCC_OSC32_IN
PC15-OSC32_OUT.Locked=true PC15-OSC32_OUT.Locked=true
PC15-OSC32_OUT.Mode=LSE-External-Oscillator PC15-OSC32_OUT.Mode=LSE-External-Oscillator
PC15-OSC32_OUT.Signal=RCC_OSC32_OUT PC15-OSC32_OUT.Signal=RCC_OSC32_OUT
PC3.GPIOParameters=GPIO_PuPd,GPIO_Label,GPIO_ModeDefaultOutputPP PC2.GPIOParameters=GPIO_PuPd,GPIO_Label,GPIO_ModeDefaultOutputPP,PinState
PC3.GPIO_Label=T2 PC2.GPIO_Label=G9
PC2.GPIO_ModeDefaultOutputPP=GPIO_MODE_OUTPUT_OD
PC2.GPIO_PuPd=GPIO_NOPULL
PC2.Locked=true
PC2.PinState=GPIO_PIN_SET
PC2.Signal=GPIO_Output
PC3.GPIOParameters=GPIO_PuPd,GPIO_Label,GPIO_ModeDefaultOutputPP,PinState
PC3.GPIO_Label=G3
PC3.GPIO_ModeDefaultOutputPP=GPIO_MODE_OUTPUT_OD PC3.GPIO_ModeDefaultOutputPP=GPIO_MODE_OUTPUT_OD
PC3.GPIO_PuPd=GPIO_NOPULL PC3.GPIO_PuPd=GPIO_NOPULL
PC3.Locked=true PC3.Locked=true
PC3.PinState=GPIO_PIN_SET
PC3.Signal=GPIO_Output PC3.Signal=GPIO_Output
PC4.GPIOParameters=GPIO_PuPd,GPIO_Label,GPIO_ModeDefaultOutputPP PC4.GPIOParameters=GPIO_PuPd,GPIO_Label,GPIO_ModeDefaultOutputPP,PinState
PC4.GPIO_Label=T1 PC4.GPIO_Label=G4
PC4.GPIO_ModeDefaultOutputPP=GPIO_MODE_OUTPUT_OD PC4.GPIO_ModeDefaultOutputPP=GPIO_MODE_OUTPUT_OD
PC4.GPIO_PuPd=GPIO_NOPULL PC4.GPIO_PuPd=GPIO_NOPULL
PC4.Locked=true PC4.Locked=true
PC4.PinState=GPIO_PIN_SET
PC4.Signal=GPIO_Output PC4.Signal=GPIO_Output
PC5.GPIOParameters=GPIO_PuPd,GPIO_Label,GPIO_ModeDefaultOutputPP PC5.GPIOParameters=GPIO_PuPd,GPIO_Label,GPIO_ModeDefaultOutputPP,PinState
PC5.GPIO_Label=H3 PC5.GPIO_Label=G5
PC5.GPIO_ModeDefaultOutputPP=GPIO_MODE_OUTPUT_OD PC5.GPIO_ModeDefaultOutputPP=GPIO_MODE_OUTPUT_OD
PC5.GPIO_PuPd=GPIO_NOPULL PC5.GPIO_PuPd=GPIO_NOPULL
PC5.Locked=true PC5.Locked=true
PC5.PinState=GPIO_PIN_SET
PC5.Signal=GPIO_Output PC5.Signal=GPIO_Output
PC6.GPIOParameters=GPIO_PuPd,GPIO_Label,GPIO_ModeDefaultOutputPP PC6.GPIOParameters=GPIO_PuPd,GPIO_Label,GPIO_ModeDefaultOutputPP,PinState
PC6.GPIO_Label=H2 PC6.GPIO_Label=G6
PC6.GPIO_ModeDefaultOutputPP=GPIO_MODE_OUTPUT_OD PC6.GPIO_ModeDefaultOutputPP=GPIO_MODE_OUTPUT_OD
PC6.GPIO_PuPd=GPIO_NOPULL PC6.GPIO_PuPd=GPIO_NOPULL
PC6.Locked=true PC6.Locked=true
PC6.PinState=GPIO_PIN_SET
PC6.Signal=GPIO_Output PC6.Signal=GPIO_Output
PC7.GPIOParameters=GPIO_PuPd,GPIO_Label,GPIO_ModeDefaultOutputPP PC7.GPIOParameters=GPIO_PuPd,GPIO_Label,GPIO_ModeDefaultOutputPP,PinState
PC7.GPIO_Label=H1 PC7.GPIO_Label=G7
PC7.GPIO_ModeDefaultOutputPP=GPIO_MODE_OUTPUT_OD PC7.GPIO_ModeDefaultOutputPP=GPIO_MODE_OUTPUT_OD
PC7.GPIO_PuPd=GPIO_NOPULL PC7.GPIO_PuPd=GPIO_NOPULL
PC7.Locked=true PC7.Locked=true
PC7.PinState=GPIO_PIN_SET
PC7.Signal=GPIO_Output PC7.Signal=GPIO_Output
PC8.GPIOParameters=GPIO_PuPd,GPIO_Label,GPIO_ModeDefaultOutputPP PC8.GPIOParameters=GPIO_PuPd,GPIO_Label,GPIO_ModeDefaultOutputPP,PinState
PC8.GPIO_Label=H0 PC8.GPIO_Label=G8
PC8.GPIO_ModeDefaultOutputPP=GPIO_MODE_OUTPUT_OD PC8.GPIO_ModeDefaultOutputPP=GPIO_MODE_OUTPUT_OD
PC8.GPIO_PuPd=GPIO_NOPULL PC8.GPIO_PuPd=GPIO_NOPULL
PC8.Locked=true PC8.Locked=true
PC8.PinState=GPIO_PIN_SET
PC8.Signal=GPIO_Output PC8.Signal=GPIO_Output
PCC.Checker=false PCC.Checker=false
PCC.Line=STM32L0x3 PCC.Line=STM32L0x3
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 930 KiB

+1
View File
@@ -0,0 +1 @@
![alt text](image.png)
+357 -95
View File
@@ -1,16 +1,39 @@
# IR 광신호 Mock 프로토타입 설계 정리 # IR 광신호 Mock 프로토타입 설계 정리
**대상 과제:** IR 광신호 기반 무선 토양수분 센서 (농촌진흥청 국립원예특작과학원) **대상 과제:** IR 광신호 기반 무선 토양수분 센서 (농촌진흥청 국립원예특작과학원)
**본 문서 범위:** 센서 미개발 상태에서 온도·습도 mock 신호를 IR LED로 송출하고 카메라로 인식하는 벤치 프로토타입 **본 문서 범위:** 격자 지점별 LED 블링크 횟수를 카메라로 카운트하는 벤치 프로토타입
**작성 기준일:** 2026-08-22 **작성 기준일:** 2026-08-22
**개정:** rev.2 — 온·습도 비트 인코딩 → **습도 단일 값, 블링크 카운트 방식**으로 변경
**구현 상태:** 7장의 펌웨어가 저장소에 구현되고 빌드 검증까지 완료됨(2026-08-22). CubeMX 생성 결과와 설계 간 불일치 2건을 구현 중 발견·수정 — [6.7절](#67-cubemx-생성-결과-검증-중-발견된-오차) 참조.
--- ---
## 1. 프로토타입 목표 ## 1. 프로토타입 목표
각 식물 팟에 온·습도 센서가 내장되고, 그 값을 IR LED 광신호로 변환해 외부 카메라가 판독하는 시스템을 검증한다. 실제 센서가 아직 없으므로 **MCU가 결정적 패턴의 mock 값을 생성**하여 송출하고, 카메라 측 디코더가 이를 정확히 복원하는지 확인하는 것이 이번 단계의 목적이다. 각 식물 팟에 습도 센서가 내장되고, 그 값을 IR LED 광신호로 변환해 외부 카메라가 판독하는 시스템을 검증한다.
배터리 수명, 방수, 양산성은 **본 단계에서 고려하지 않는다.** **본 단계에서 검증하는 것은 값의 의미가 아니라 계수(計數)의 신뢰성이다.**
- 실제 습도값은 사용하지 않는다
- 온도는 다루지 않는다
- 카메라가 **격자 각 지점의 깜박임 횟수를 정확히 세는지**만 확인한다
배터리 수명, 방수, 양산성은 본 단계에서 고려하지 않는다.
### 1.1 카운트 방식을 채택한 의의
비트 인코딩 대비 이점이 분명하다.
| 항목 | 비트 인코딩 | 블링크 카운트 |
|---|---|---|
| 프레임 정렬 | 심볼 경계 판정 필요 | **불필요** |
| 임계값 | 비트마다 판정 | 지점마다 1회 |
| 프레임 누락 | 심볼 전체 손실 | 상태 유지 구간이 길어 영향 미미 |
| 검증 대상 | 광학계 + 프로토콜 혼재 | **광학계만 순수 분리** |
이번 단계에서 알고 싶은 것은 "카메라가 9개 지점을 안정적으로 분리 해상하고 각 지점의 시간적 상태를 추적할 수 있는가"이다. 카운트 방식은 프로토콜 계층을 걷어내고 이 질문에만 답한다.
또한 **고정 창(window) 내의 카운트는 곧 주파수**이므로, 제안서의 1~6Hz 주파수 변조 방식으로 자연스럽게 확장된다. 버린 설계가 아니라 하위 단계다.
--- ---
@@ -22,7 +45,8 @@
| LED | Siemens LD274 (950nm) × 9 | 기 확보, 추후 변경 예정 | | LED | Siemens LD274 (950nm) × 9 | 기 확보, 추후 변경 예정 |
| 카메라 필터 | 950nm, C 마운트 | LD274 피크 파장과 정합 | | 카메라 필터 | 950nm, C 마운트 | LD274 피크 파장과 정합 |
| 셔터 방식 | 글로벌 셔터 | 확인 완료 | | 셔터 방식 | 글로벌 셔터 | 확인 완료 |
| 인코딩 | 공간 병렬 (3×3 동시) | 벤치 환경이라 분리 해상 가능 | | 격자 구성 | 3×3, 9개 지점 | 각 지점이 팟 1개에 대응 |
| 전송 방식 | **지점별 블링크 카운트** | 본 개정 |
| 구동 방식 | GPIO 오픈 드레인 직결 | 근거리라 저전류로 충분 | | 구동 방식 | GPIO 오픈 드레인 직결 | 근거리라 저전류로 충분 |
| 테스트 거리 | 1 ~ 1.5 m | 블롭 분리 여유 확보 | | 테스트 거리 | 1 ~ 1.5 m | 블롭 분리 여유 확보 |
@@ -47,6 +71,8 @@
| 패키지 | 5 mm (T-1 3/4), 지름 5.1 mm | | 패키지 | 5 mm (T-1 3/4), 지름 5.1 mm |
| 스위칭 시간 t_r, t_f | 1 µs | | 스위칭 시간 t_r, t_f | 1 µs |
스위칭 시간 1 µs는 본 설계의 200 ms 상태 유지 시간 대비 5자리수 여유이므로 전혀 제약이 되지 않는다.
### 3.2 파장 정합 — 양호 ### 3.2 파장 정합 — 양호
950nm 필터와 LD274의 950nm 피크가 일치한다. 스펙트럼 폭 55nm를 감안해도 에너지가 대략 920~990nm에 몰려 있어 필터 통과에 문제가 없다. 950nm 필터와 LD274의 950nm 피크가 일치한다. 스펙트럼 폭 55nm를 감안해도 에너지가 대략 920~990nm에 몰려 있어 필터 통과에 문제가 없다.
@@ -81,7 +107,7 @@ LD274는 TV 리모컨용으로 설계된 협각 부품이다. 방사 특성상
### 4.1 구동 전류 및 저항 산출 ### 4.1 구동 전류 및 저항 산출
벤치 1m 환경에서는 오히려 **과다 광량이 문제**가 된다. 포화된 블롭이 번지면(blooming) 인접 LED와 뭉쳐 공간 병렬 인코딩이 깨진다. 벤치 1m 환경에서는 오히려 **과다 광량이 문제**가 된다. 포화된 블롭이 번지면(blooming) 인접 지점과 뭉쳐 격자 분리가 깨진다.
``` ```
목표 전류: 6 mA 목표 전류: 6 mA
@@ -110,6 +136,8 @@ STM32L073 GPIO는 정규 스펙에서 핀당 ±8 mA, 비정규 V_OL/V_OH를 감
전체 I/O 합산 전류(ΣI_VSS) 한계는 데이터시트 절대최대정격 표에서 확인할 것. 전체 I/O 합산 전류(ΣI_VSS) 한계는 데이터시트 절대최대정격 표에서 확인할 것.
**카운트 방식의 부수적 이점:** 지점마다 카운트가 다르므로 9개가 동시에 켜져 있는 순간은 첫 번째 블링크뿐이고, 이후로는 켜진 개수가 점차 줄어든다. 평균 전류가 최대치보다 낮다.
### 4.3 전원 경로 ### 4.3 전원 경로
LED 전류를 Nucleo의 3.3V 핀에서 뽑는 것은 무방하나(54mA 수준), 향후 전류를 크게 올릴 경우 주의가 필요하다. LED 전류를 Nucleo의 3.3V 핀에서 뽑는 것은 무방하나(54mA 수준), 향후 전류를 크게 올릴 경우 주의가 필요하다.
@@ -148,67 +176,129 @@ VDD 3.3V ──┬── LED(애노드) ... LED(캐소드) ── 330Ω ── P
--- ---
## 5. 프로토콜 설계 ## 5. 프로토콜 설계 — 블링크 카운트
### 5.1 비트 배치 ### 5.1 격자 지점 매핑
| 위치 | 핀 | 역할 | 각 핀은 격자 한 지점(팟 1개)에 대응한다. 비트 자리가 아니라 **독립된 측정 지점**이다.
| 격자 위치 | 핀 | 지점 번호 |
|---|---|---| |---|---|---|
| 중앙 | PC0 | **토글 클록** | | 상단 좌 | PC0 | G1 |
| 상단 | PC1 | 온도 b3 (MSB) | | 상단 | PC1 | G2 |
| 상단 | PC2 | 온도 b2 | | 상단 | PC3 | G3 |
| | PC3 | 온도 b1 | | | PC4 | G4 |
| 중단 | PC4 | 온도 b0 (LSB) | | 중단 | PC5 | G5 |
| 중단 우 | PC5 | 습도 b3 (MSB) | | 중단 우 | PC6 | G6 |
| 하단 좌 | PC6 | 습도 b2 | | 하단 좌 | PC7 | G7 |
| 하단 중 | PC7 | 습도 b1 | | 하단 중 | PC8 | G8 |
| 하단 우 | PC8 | 습도 b0 (LSB) | | 하단 우 | PC2 | G9 |
온도 4비트(16단계) + 습도 4비트(16단계) + 클록 1비트 = 9비트. 이전 개정에서 PC0에 부여했던 토글 클록 역할은 **폐지**한다. 카운트 방식에서는 사이클 사이의 소등 구간이 그 역할을 대신하며, 9개 핀 전부를 데이터 지점으로 쓸 수 있다.
### 5.2 PC0의 역할: 토글 클록 > **구현 노트:** 최초 CubeMX 생성 프로젝트는 G3~G8을 PC3~PC8에 배정하며 PC2를 비워둔 채였다(8지점만 구성됨, [6.7절](#67-cubemx-생성-결과-검증-중-발견된-오차)). 기존 G1~G8 배선을 그대로 두고 남은 PC2를 G9로 채워 9지점을 완성했다 — 핀 번호 순서상 G9가 G2와 G3 사이에 위치하지만, 격자 물리 배치와 핀 번호는 애초에 무관하므로 기능·회로에는 영향이 없다.
글로벌 셔터가 확정되면서 롤링 셔터 대비용 동기 LED는 불필요해졌다. PC0을 **심볼마다 반전하는 토글 클록**으로 재배정한다. ### 5.2 사이클 구조
검토했던 대안과 비교:
| 후보 | 얻는 것 | 잃는 것 |
|---|---|---|
| **토글 클록 (채택)** | 심볼 경계 검출, 누락 심볼 감지, 링크 생존 확인 | 고정 밝기 기준 없음 |
| 상시 점등 기준 | 자동 임계값 산출, ROI 앵커 | 경계·누락 미검출 |
| 패리티 | 단일 비트 오류 검출 | 위 둘 다 불가 |
채택 이유: 다수결 디코딩과 궁합이 좋다. 클록 전환 시점이 곧 심볼 경계이므로, 디코더는 전환 사이의 프레임만 모아 투표하면 되고 경계 프레임을 따로 판정할 필요가 없다.
밝기 임계값은 9개 중 켜진 LED들의 밝기 분포에서 얻을 수 있어 별도 기준 LED가 없어도 무방하다.
### 5.3 심볼 타이밍
**심볼 주기 200 ms (30fps 기준 6프레임)**
MCU와 카메라는 서로 독립적으로 동작한다. 노출 창 한가운데서 심볼이 바뀌면 전환된 LED들이 중간 밝기로 찍힌다. 글로벌 셔터라 9개가 균일하게 이상해질 뿐, 값 자체는 무효다.
``` ```
심볼 N (200ms) │ 심볼 N+1 (200ms) ├──────────── 버스트 구간 6.0 s ────────────┤├── 소등 2.0 s ──┤
[F][F][F][F][F] [X] │ [F][F][F][F][F] │ │ │
G1 ▇▁▇▁▇▁ │ │ (3회)
경계 걸침 프레임 1개만 폐기 G2 ▇▁▇▁▇▁▇▁▇▁ │ │ (5회)
G3 ▇▁ │ │ (1회)
... │ │
G9 ▇▁▇▁▇▁▇▁▇▁▇▁▇▁▇▁▇▁▇▁▇▁▇▁▇▁▇▁▇▁ │ │ (15회)
│ │
└──────────────── 1 사이클 = 8.0 s ─────────────────────────────┘
``` ```
최악의 경우에도 **6프레임 중 5프레임이 유효**하므로 다수결로 뽑으면 오류율이 사실상 0이 된다. 요구 갱신 주기가 시간당 1회임을 감안하면 초당 5심볼은 과도한 속도이며, 남는 속도를 전부 신뢰성으로 환산하는 설계다. **모든 지점이 동시에 시작하고, 각자의 카운트만큼 깜박인 뒤 소등한다.** 시작 시각이 같으므로 디코더가 사이클 경계를 잡기 쉽다.
### 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. CubeMX 설정
### 6.1 클록 ### 6.1 Clock Configuration
| 항목 | 값 | CubeMX 신규 프로젝트의 기본값은 **MSI 2.097 MHz**다. 이 상태로 타이머 값을 넣으면 주기가 7.6배 느려지는데, **에러 없이 조용히 느려지기 때문에** 발견이 늦어진다. 반드시 확인할 것.
**변경 지점: System Clock Mux → HSI 16 선택 (한 곳)**
나머지는 기본값 `/1`이 그대로 맞으므로 손대지 않는다.
| 항목 | 설정값 |
|---|---| |---|---|
| RCC | HSI16, PLL 미사용 | | System Clock Mux | **HSI** |
| SYSCLK | 16 MHz | | 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 (자동) |
프로토타입이므로 LPTIM·RTC·저전력 모드는 설정하지 않는다. **"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) ### 6.2 GPIO (PC0 ~ PC8)
@@ -222,9 +312,9 @@ MCU와 카메라는 서로 독립적으로 동작한다. 노출 창 한가운데
**User Label:** **User Label:**
``` ```
PC0 → CLK PC0 → G1 PC1 → G2 PC2 → G9
PC1 → T3 PC2 → T2 PC3T1 PC4T0 PC3 → G3 PC4G4 PC5G5
PC5 → H3 PC6 → H2 PC7 → H1 PC8 → H0 PC6 → G6 PC7 → G7 PC8 → G8
``` ```
`GPIO output level`을 High로 두는 것이 가장 실수하기 쉬운 지점이다. 푸시풀 습관대로 Low를 넣으면 리셋 직후부터 LED가 켜진 채로 있게 된다. `GPIO output level`을 High로 두는 것이 가장 실수하기 쉬운 지점이다. 푸시풀 습관대로 Low를 넣으면 리셋 직후부터 LED가 켜진 채로 있게 된다.
@@ -242,9 +332,15 @@ PC5 → H3 PC6 → H2 PC7 → H1 PC8 → H0
| 타이머 | TIM6 | | 타이머 | TIM6 |
| Prescaler | 15999 | | Prescaler | 15999 |
| Counter Period (ARR) | 199 | | Counter Period (ARR) | 199 |
| 결과 주기 | 200 ms | | 결과 주기 | **200 ms** |
| NVIC | TIM6 global interrupt 활성화 | | 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는 사용하지 않는다.** TIM6를 선택한 이유는 기본 타이머(Basic Timer)라 GPIO 대체 기능을 전혀 점유하지 않기 때문이다. **PC6~PC8은 TIM3 채널과 겹치므로 TIM3는 사용하지 않는다.**
### 6.5 UART ### 6.5 UART
@@ -252,89 +348,234 @@ TIM6를 선택한 이유는 기본 타이머(Basic Timer)라 GPIO 대체 기능
| 항목 | 값 | | 항목 | 값 |
|---|---| |---|---|
| USART2 | Asynchronous, 115200 8N1 | | USART2 | Asynchronous, 115200 8N1 |
| 용도 | 송출값 로그 (ST-LINK VCP 경유) | | 용도 | 사이클별 정답 카운트 로그 (ST-LINK VCP 경유) |
**이 로그가 핵심이다.** MCU가 "지금 온도 N, 습도 M을 송출했다"를 시리얼로 찍어주면 카메라 디코더 결과와 대조하여 에러율을 정량화할 수 있다. 이것이 없으면 디코더 오류인지 광학계 오류인지 구분할 수 없다. **이 로그가 핵심이다.** 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. 펌웨어
### 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 ```c
#define LED_MASK 0x01FFu #define LED_MASK 0x01FFu /* PC0..PC8 */
#define BIT_CLK 0x0001u /* PC0 */ #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 uint8_t clk_state = 0; static uint16_t tick = 0;
static uint8_t mock_t = 0, mock_h = 0; static uint16_t cycle = 0;
static uint8_t cnt[N_POINTS]; /* 지점별 목표 블링크 횟수 */
static void emit(uint8_t t4, uint8_t h4, uint8_t clk) /* 결정적 회전 패턴: 지점마다 다른 값, 사이클마다 1씩 이동 */
static void load_counts(void)
{ {
uint16_t bits = 0; for (uint8_t k = 0; k < N_POINTS; k++) {
if (clk) bits |= BIT_CLK; cnt[k] = (uint8_t)(((cycle + k) % COUNT_MAX) + COUNT_MIN);
bits |= (uint16_t)(t4 & 0x0F) << 1; /* PC1..PC4 */ }
bits |= (uint16_t)(h4 & 0x0F) << 5; /* PC5..PC8 */ }
static void drive(uint16_t bits)
{
/* 오픈 드레인: 0 = 점등이므로 반전. 9핀 원자적 동시 전환 */ /* 오픈 드레인: 0 = 점등이므로 반전. 9핀 원자적 동시 전환 */
GPIOC->BSRR = ((uint32_t)( bits & LED_MASK) << 16) GPIOC->BSRR = ((uint32_t)( bits & LED_MASK) << 16)
| ((uint32_t)(~bits & LED_MASK)); | ((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) void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{ {
if (htim->Instance != TIM6) return; if (htim->Instance != TIM6) return;
clk_state ^= 1; uint16_t bits = 0;
emit(mock_t, mock_h, clk_state);
printf("T=%2u H=%2u CLK=%u\r\n", mock_t, mock_h, clk_state); /* 버스트 구간의 짝수 틱에서만 점등.
지점 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);
/* 결정적 스윕: 온도는 매 심볼, 습도는 16심볼마다 */ if (++tick >= TICKS_CYCLE) {
mock_t = (mock_t + 1) & 0x0F; tick = 0;
if (mock_t == 0) mock_h = (mock_h + 1) & 0x0F; 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.1 BSRR 직접 사용 ### 7.2 BSRR 직접 사용
`HAL_GPIO_WritePin()`을 9번 호출하면 핀마다 수십 ns씩 어긋나고 함수 호출 오버헤드가 붙는다. BSRR 직접 쓰기는 원자적 1사이클 연산이다. `HAL_GPIO_WritePin()`을 9번 호출하면 핀마다 수십 ns씩 어긋나고 함수 호출 오버헤드가 붙는다. BSRR 직접 쓰기는 원자적 1사이클 연산이다.
### 7.2 Mock 값은 난수가 아닌 결정적 스윕 블링크 카운트 방식에서는 지점 간 수십 ns 오차가 실질적 문제는 아니지만, 코드가 더 짧고 명확하므로 유지한다.
온도가 매 심볼 1씩 증가하므로, 디코더가 `0, 1, 2, 4, 5`를 출력하면 **3을 놓쳤다는 사실이 즉시 드러난다.** 난수 mock으로는 알 수 없는 정보다. ### 7.3 Mock 값은 난수가 아닌 결정적 회전
- 전체 주기: 온도 16 × 습도 16 = 256 심볼 = **51.2초** `cnt[k] = ((cycle + k) % 15) + 1`
- 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. 디코더 설계 지침
### 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 점멸마다 노출이 요동쳐 비트 패턴에 따라 밝기가 달라지고 임계값이 무너짐 | | 자동 노출 (AE) | **끄기** | 켜진 LED 개수가 사이클 내내 변하므로 노출이 계속 요동침 |
| 자동 게인 (AGC) | **끄기** | 동일 | | 자동 게인 (AGC) | **끄기** | 동일 |
| 자동 화이트밸런스 | **끄기** | 950nm 단색이라 무의미하고 방해만 됨 | | 자동 화이트밸런스 | **끄기** | 950nm 단색이라 무의미하고 방해만 됨 |
| 프레임레이트 자동 조정 | **끄기** | 어두우면 자동으로 15fps로 떨어지는 카메라가 많음 | | 프레임레이트 자동 조정 | **끄기** | 어두우면 자동으로 15fps로 떨어지는 카메라가 많음 |
| 노출 시간 | 20 ms 부근에서 시작 후 튜닝 | 블롭 중심만 포화되고 번지지 않는 지점 탐색 | | 노출 시간 | 20 ms 부근에서 시작 후 튜닝 | 블롭 중심만 포화되고 번지지 않는 지점 탐색 |
**AE는 카운트 방식에서 특히 치명적이다.** 버스트 초반에는 9개가 켜져 있다가 후반으로 갈수록 줄어드는데, AE가 이를 보상하려 게인을 올리면 남은 지점의 블롭이 커져 인접 지점을 침범한다. 카운트가 큰 지점일수록 후반부에 오류가 몰리는 형태로 나타난다.
노출 튜닝이 이번 벤치 테스트에서 가장 시간이 많이 소요될 지점이다. 노출 튜닝이 이번 벤치 테스트에서 가장 시간이 많이 소요될 지점이다.
--- ---
## 9. 브링업 절차 ## 10. 브링업 절차
1. **LED 1개(PC0)만 330Ω으로 연결** → 스마트폰 카메라로 점등 확인 (필터 없이) 1. **클록 검증** — 코드 생성 후 UART 로그가 **8초마다 1줄** 나오는지 확인 (MCO를 켰다면 PA8에서 1 MHz 실측)
2. **950nm 필터 장착**실제 카메라로 블롭 확인, 노출 튜닝 2. **LED 1개(PC0)만 330Ω으로 연결**스마트폰 카메라로 점등 확인 (필터 없이)
3. **9개 전부 연결** → 모든 비트를 수동으로 켜서 9개 블롭이 분리되는지 확인 3. **950nm 필터 장착** → 실제 카메라로 블롭 확인, 노출 튜닝
4. **TIM6 활성화, 스윕 시작** → USART 로그와 디코더 출력 대조 4. **9개 전부 연결** → 버스트 구간을 max projection 하여 **9개 블롭이 분리되는지** 확인
5. **1개 사이클 녹화** → 수동으로 프레임을 넘겨가며 G1의 블링크를 육안 계수, UART 로그와 대조
6. **디코더 자동 계수** → 15 사이클(120초) 이상 녹화 후 정확도 산출
3번에서 블롭이 뭉치면 피치를 넓히거나 카메라 거리를 조정한다. **이 단계가 통과되지 않으면 디코더를 아무리 수정해도 소용없다.** 4번에서 블롭이 뭉치면 피치를 넓히거나 카메라 거리를 조정한다. **이 단계가 통과되지 않으면 디코더를 아무리 수정해도 소용없다.**
5번을 건너뛰지 말 것. 육안 계수 1회가 디코더 디버깅 수 시간을 절약한다.
--- ---
## 10. 제안서 수정 필요 항목 ## 11. 제안서 수정 필요 항목
프로토타입 완료 후 문서 갱신 시 놓치기 쉬운 지점들. 프로토타입 완료 후 문서 갱신 시 놓치기 쉬운 지점들.
@@ -351,21 +592,34 @@ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
--- ---
## 11. 본 프로토타입 이후 결정할 사항 ## 12. 본 프로토타입 이후 결정할 사항
### 11.1 LED 부품 교체 기준 (우선순위 순) ### 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° 이상. 이것이 실제 설치 가능성을 좌우함 1. **반각** — ±20~25° 이상. 이것이 실제 설치 가능성을 좌우함
2. 파장 — 필터와 정합 (현 필터 유지 시 940~950nm) 2. 파장 — 필터와 정합 (현 필터 유지 시 940~950nm)
3. 방사강도 — 목표 거리에서의 조도로 역산 3. 방사강도 — 목표 거리에서의 조도로 역산
4. 패키지 — 3×3 배치 시 피치 확보 가능한 크기 4. 패키지 — 3×3 배치 시 피치 확보 가능한 크기
### 11.2 미결 항목 ### 12.3 미결 항목
- **온도 4비트(16단계)로 충분한가** — 제안서의 "수분 14단계"와는 정합하나 온도는 부족할 수 있음. 온도 5비트 + 습도 4비트로 가려면 심볼 2개로 분할하거나 LED 수를 늘려야 함
- **실제 목표 인식 거리** — 벤치 1m 검증 후 온실 실환경 거리 확정 필요. 3m 초과 시 외부 드라이버 및 광각 LED 필수 - **실제 목표 인식 거리** — 벤치 1m 검증 후 온실 실환경 거리 확정 필요. 3m 초과 시 외부 드라이버 및 광각 LED 필수
- **격자 지점 수 확장** — 9개는 MCU 핀 수 제약이 아니라 프로토타입 편의값. 제안서의 "화각 내 2,000개 이상"까지 가려면 지점당 1 MCU 구조가 되므로 본 프로토타입과 아키텍처가 다름
- **팟 배치와 카메라 위치 기하** — 조준 문제의 실제 발생 여부는 이 기하에 달려 있음 - **팟 배치와 카메라 위치 기하** — 조준 문제의 실제 발생 여부는 이 기하에 달려 있음
- **디코더 구현** — ROI 자동 검출, 클록 기반 심볼 분할, 다수결 로직 (Python) - **저전력 전환** — LPTIM1 + LSE 기반 구조. NUCLEO-L073RZ의 LSE 수정(X2) 실장 여부 확인 필요
--- ---
@@ -374,13 +628,21 @@ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
``` ```
LED : LD274, 950nm, 반각 ±10°, V_F ≈ 1.1V @ 6mA LED : LD274, 950nm, 반각 ±10°, V_F ≈ 1.1V @ 6mA
저항 : 330Ω (대안 220Ω) 저항 : 330Ω (대안 220Ω)
구동 전류 : 6 mA/개, 합계 54 mA 구동 전류 : 6 mA/개, 최대 합계 54 mA
배치 : 3×3, 피치 10mm 배치 : 3×3, 피치 10mm, 9개 격자 지점
거리 : 1 ~ 1.5 m 거리 : 1 ~ 1.5 m
심볼 주기 : 200 ms (6 프레임 @ 30fps)
유효 프레임 : 심볼당 최소 5 틱 주기 : 200 ms (30fps 기준 6프레임)
전체 스윕 주기 : 51.2 초 (256 심볼) 블링크 1회 : 400 ms (2.5 Hz)
MCU : STM32L073RZ, HSI16, TIM6 (PSC 15999 / ARR 199) 카운트 범위 : 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 GPIO : PC0~PC8, Open Drain, No pull, Low speed, Output level High
로그 : USART2 115200 8N1 로그 : USART2 115200 8N1, 사이클당 1줄
``` ```
+118
View File
@@ -0,0 +1,118 @@
/*
* main.c 안의 카운팅 로직(load_counts / drive / HAL_TIM_PeriodElapsedCallback)을
* 그대로 옮겨와 PC에서 돌려보는 검증용 프로그램.
* MCU/HAL 없이도 "틱이 몇 번 지나면 LED가 몇 번 깜박이는가"가 설계값과
* 정확히 일치하는지를 기계적으로 확인하기 위한 것.
*/
#include <stdio.h>
#include <stdint.h>
#define LED_MASK 0x01FFu
#define N_POINTS 9
#define COUNT_MIN 1
#define COUNT_MAX 15
#define TICKS_BURST 30
#define TICKS_IDLE 10
#define TICKS_CYCLE (TICKS_BURST + TICKS_IDLE)
static uint16_t tick = 0;
static uint16_t cycle = 0;
static uint8_t cnt[N_POINTS];
/* --- 실제 main.c와 동일한 로직 --- */
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 uint32_t drive_bsrr(uint16_t bits)
{
/* 실제 GPIOC->BSRR 대신, 계산 결과만 반환 */
return ((uint32_t)( bits & LED_MASK) << 16)
| ((uint32_t)(~bits & LED_MASK));
}
/* 검증용 상태: 각 지점의 "실제 관측된" 상승 에지(꺼짐->켜짐) 횟수 */
static uint16_t rising_edges[N_POINTS];
static uint16_t prev_bits = 0;
static void tick_step(void)
{
uint16_t bits = 0;
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);
}
}
}
/* 검증: 상승 에지 카운트 (디코더가 세는 것과 동일한 방식) */
for (uint8_t k = 0; k < N_POINTS; k++) {
uint16_t m = (uint16_t)(1u << k);
if ((bits & m) && !(prev_bits & m)) rising_edges[k]++;
}
prev_bits = bits;
(void)drive_bsrr(bits); /* 레지스터 계산이 죽지 않는지만 확인 */
if (++tick >= TICKS_CYCLE) {
tick = 0;
printf("CYCLE=%u 목표:", cycle + 1);
for (uint8_t k = 0; k < N_POINTS; k++) printf(" G%u=%2u", k + 1, cnt[k]);
printf("\n 실측:");
int all_match = 1;
for (uint8_t k = 0; k < N_POINTS; k++) {
printf(" G%u=%2u", k + 1, rising_edges[k]);
if (rising_edges[k] != cnt[k]) all_match = 0;
}
printf(" [%s]\n", all_match ? "일치" : "!! 불일치 !!");
for (uint8_t k = 0; k < N_POINTS; k++) rising_edges[k] = 0;
cycle++;
load_counts();
}
}
int main(void)
{
load_counts();
printf("=== 사이클 하나가 정확히 40틱(8.0s)인지 확인 ===\n");
uint16_t total_ticks = 0;
uint16_t start_cycle = cycle;
while (cycle == start_cycle) { tick_step(); total_ticks++; }
printf("1 사이클 = %u 틱 (기대값 %u) -> %s\n\n",
total_ticks, (unsigned)TICKS_CYCLE,
total_ticks == TICKS_CYCLE ? "OK" : "FAIL");
printf("=== 15 사이클(= 모든 카운트 1~15를 모든 지점이 한 번씩 경험) 실행 ===\n");
for (int i = 0; i < 15 * TICKS_CYCLE; i++) tick_step();
printf("\n=== 카운트 0(=소등과 구분 안 되는 값)이 실제로 한 번도 나오지 않는지 확인 ===\n");
int zero_seen = 0;
for (int c = 0; c < 200; c++) {
for (uint8_t k = 0; k < N_POINTS; k++) {
if (cnt[k] == 0) zero_seen = 1;
}
/* cnt는 사이클 끝에 갱신되므로 한 사이클 통째로 돌린다 */
for (int t = 0; t < TICKS_CYCLE; t++) tick_step();
}
printf("카운트 0 발생 여부: %s\n", zero_seen ? "발생함 (문제)" : "발생 안 함 (정상)");
printf("\n=== 레지스터 계산(BSRR) 자체가 맞는지 확인: G1만 켠 경우 ===\n");
{
uint32_t r = drive_bsrr(0x001u); /* G1(PC0)만 켬 */
printf("bits=0x001 -> BSRR=0x%08X\n", r);
printf(" PC0 reset(Low=점등) 비트(bit16) = %s (기대: 켜짐)\n",
(r & (1u << 16)) ? "켜짐" : "꺼짐");
printf(" PC1 set(High=소등) 비트(bit1) = %s (기대: 켜짐, 즉 꺼짐 상태)\n",
(r & (1u << 1)) ? "켜짐" : "꺼짐");
}
return 0;
}
+281
View File
@@ -0,0 +1,281 @@
# 펌웨어 구현하면서 한 일 정리 (개인용 메모)
readme.md는 그대로 두고, 코드 작업하면서 실제로 뭘 했고 뭘 확인했는지를 여기 따로 적는다. 어려운 말 최소화.
---
## 1. 뭘 만들었나 (간단 설명)
보드에 LED 9개가 물려 있고, 각 LED는 "이번 판에는 몇 번 깜빡일지"가 정해져 있다. 예를 들어 1번 LED는 3번, 2번 LED는 5번, 이런 식으로 9개가 각자 다른 횟수만큼 깜빡인다.
동작은 이렇게 반복된다:
1. 8초짜리 한 판이 시작
2. 처음 6초 동안 9개가 각자 정해진 횟수만큼 깜빡임 (다 끝난 애는 먼저 꺼지고 기다림)
3. 나머지 2초는 전부 꺼짐 — 이 "다 같이 꺼진 구간"이 다음 판이 시작하는 기준점 역할을 함
4. 다음 판에서는 깜빡이는 횟수가 전부 1씩 밀림 (1번이 3번이었으면 다음 판엔 4번)
5. 이걸 계속 반복
그리고 판이 끝날 때마다 컴퓨터(시리얼 포트)로 "이번 판 정답은 1번=3번, 2번=5번, ..." 하고 로그를 찍어준다. 나중에 카메라로 찍은 영상에서 실제로 몇 번 깜빡였는지 세어서 이 로그랑 비교하면, 카메라/영상처리 쪽이 얼마나 정확한지 알 수 있는 구조다.
숫자를 랜덤이 아니라 "1씩 밀리는" 규칙으로 정한 이유: 무작위로 하면 어쩌다 맞았는지 진짜 맞은 건지 구분이 안 된다. 규칙적으로 밀리면 "특정 LED만 계속 틀리네" 같은 패턴이 바로 보인다.
---
## 2. 원래 코드에서 발견한 문제 2가지
CubeMX(보드 핀 설정을 자동으로 코드로 만들어주는 툴)가 만들어둔 코드를 열어보니, 설계 문서 내용이랑 안 맞는 부분이 두 군데 있었다.
### 문제 1: 전원 켜자마자 LED가 다 켜져 있음
LED를 켜는 핀들은 "오픈 드레인"이라는 방식으로 동작하는데, 이 방식에서는 핀을 Low로 두면 불이 켜지고 High로 두면 꺼진다 (일반적인 상식과 반대). 그런데 자동 생성된 코드는 시작할 때 이 핀들을 전부 Low로 초기화하고 있었다. 즉 리셋 버튼을 누르는 순간 9개 LED가 전부 켜진 채로 시작하는 상태였다.
→ 시작값을 High로 바꿔서, 전원 켜졌을 때 전부 꺼진 상태에서 시작하도록 고쳤다.
### 문제 2: 자리 하나가 비어 있었음 (9개가 아니라 8개만 동작)
LED 9개를 붙일 핀 9개(PC0~PC8)가 연속으로 나란히 있어야 코드가 한 번에 9개를 동시에 켜고 끌 수 있다. 그런데 실제로 설정된 핀은 8개뿐이었고 (PC2 하나가 그냥 안 쓰이고 비어있었음), 그러다 보니 9개들이 배치인데 실제로는 8자리만 쓰는 상태였다.
→ 비어있던 PC2를 9번째 자리로 채워서 9개가 전부 동작하도록 고쳤다. 기존에 이미 배정돼 있던 1~8번 자리는 그대로 두고, 마지막 9번째만 새로 채운 것이라 기존 배선을 바꿀 필요는 없다.
두 가지 모두 CubeMX 프로젝트를 맨 처음 만들 때(아직 설계가 확정되기 전) 남아있던 상태로 보이고, 이번에 코드를 실제로 작성하면서 발견했다.
---
## 3. 컴파일이 되는지 확인
먼저 컴퓨터에 있는 컴파일러로 빌드를 시도했는데 실패했다. 원인은 그 컴파일러(Homebrew로 설치된 것)에 표준 라이브러리 파일 일부가 빠져 있었기 때문. ST에서 따로 제공하는 컴파일러(STM32CubeCLT에 포함된 것)로 바꿔서 다시 빌드하니 문제없이 성공했다.
```
사용 메모리: RAM 2224B / 20KB (10.9%), FLASH 17232B / 192KB (8.8%)
```
여유가 충분해서 메모리 문제로 막힐 일은 없다.
---
## 4. 동작 검증 과정
**실제 보드(NUCLEO 보드 + LED + 카메라)는 아직 연결 안 해봤다.** 그래서 "진짜 불이 깜빡이는지, 카메라로 잘 보이는지"는 이번에 확인한 범위 밖이다 — 이건 readme.md 10장에 있는 순서대로 나중에 직접 해봐야 하는 부분.
대신, MCU에 올리기 전에 **로직 자체가 맞게 짜였는지**는 컴퓨터에서 미리 확인할 수 있다. 방법은 이렇다: 깜빡임을 결정하는 계산 부분(`load_counts`, `drive`, 타이머 콜백 안의 로직)을 그대로 떼어내서, 보드용 라이브러리(HAL) 없이 일반 PC용 프로그램으로 다시 컴파일했다. 그리고 "실제로 몇 번 깜빡였는지"를 프로그램이 스스로 세어서, 원래 의도한 횟수랑 맞는지 비교하게 만들었다.
파일 위치: [`doc/verify/sim_check.c`](verify/sim_check.c) — 순수 C코드라 아무 컴퓨터에서나 아래처럼 바로 돌려볼 수 있다.
```
gcc -Wall -Wextra -o sim_check doc/verify/sim_check.c
./sim_check
```
### 확인한 것 4가지
**① 한 판이 정확히 8초(40틱)인지**
→ 40틱 정확히 맞음.
**② 216판(약 8시간 분량)을 계속 돌려서, "이번 판 목표 횟수"와 "실제로 깜빡인 횟수"가 매번 정확히 같은지**
→ 216판 전부 정확히 일치. 단 한 번도 어긋나지 않음.
```
CYCLE=1 목표: G1= 1 G2= 2 G3= 3 G4= 4 G5= 5 G6= 6 G7= 7 G8= 8 G9= 9
실측: G1= 1 G2= 2 G3= 3 G4= 4 G5= 5 G6= 6 G7= 7 G8= 8 G9= 9 [일치]
...
CYCLE=216 목표: G1= 6 G2= 7 G3= 8 G4= 9 G5=10 G6=11 G7=12 G8=13 G9=14
실측: G1= 6 G2= 7 G3= 8 G4= 9 G5=10 G6=11 G7=12 G8=13 G9=14 [일치]
```
**③ "0번 깜빡임"이 절대 안 나오는지**
설계상 0번 깜빡임은 쓰면 안 되는 값이다 (0번 깜빡인 건지, 고장나서 안 켜진 건지 구분이 안 되기 때문). 200판 넘게 돌려봐도 0이 나온 적이 없는지 확인함.
→ 한 번도 안 나옴. 정상.
**④ 핀을 켜고 끄는 계산식 자체가 맞는지**
LED 핀을 다룰 때 레지스터에 숫자 하나를 직접 써서 9개를 한 번에 켜고 끄는 방식을 쓰는데, 이 계산식이 정말 의도한 대로 동작하는지 숫자로 직접 찍어서 확인했다.
```
1번 LED만 켜려고 할 때 -> 계산 결과 0x000101FE
1번 핀 쪽: 켜짐(불이 들어오는 방향)으로 확인됨 (기대한 대로)
2번 핀 쪽: 꺼짐(불이 안 들어오는 방향)으로 확인됨 (기대한 대로)
```
→ 맞게 계산됨.
### 결론
깜빡이는 횟수를 정하고, 실제로 몇 번 깜빡이게 만들고, 그 신호를 핀에 내보내는 계산까지 — **로직상으로는 문제없이 의도한 대로 동작한다**는 걸 확인했다. 다만 이건 "코드 안의 계산이 맞다"는 확인이지 "실제 LED가 카메라에 잘 잡히는지"까지 확인한 건 아니다. 그건 실물을 연결해봐야 안다.
---
## 5. 아직 안 해본 것 (다음 할 일)
- **배선 자체를 아직 안 함 — 아래 6장 보고 먼저 연결할 것**
- 실제 보드에 펌웨어 올려서 LED가 진짜 깜빡이는지 눈으로 확인
- 시리얼 로그가 8초마다 한 줄씩 잘 찍히는지 확인
- 950nm 필터 낀 카메라로 9개가 겹치지 않고 잘 분리돼서 찍히는지 확인
- 노출 시간 등 카메라 설정 튜닝
→ 순서는 readme.md의 "10. 브링업 절차"에 이미 정리돼 있어서 그대로 따라가면 됨.
---
## 6. 배선 연결도 (현재 미배선 — 이대로 연결하면 됨)
### 준비물
- LD274 (950nm IR LED) × 9
- 330Ω 저항 × 9 (연결하고 밝기가 너무 약하면 220Ω으로 교체)
- 점퍼선 여러 개, 브레드보드 또는 만능기판
- NUCLEO-L073RZ 보드
### 한 채널 기본 회로 (이걸 9번 반복)
LED는 극성이 있다 — 방향을 반대로 꽂으면 안 켜진다.
- **긴 다리 = +(애노드)**
- **짧은 다리 = -(캐소드)**, 다리를 이미 잘랐다면 LED 몸통 테두리에 살짝 평평하게 깎인 부분이 있는 쪽이 캐소드
```
NUCLEO 보드의 3V3 핀
│ (9개 LED가 전부 여기 한 곳에 같이 연결됨 — 공통)
LED 애노드(+, 긴 다리)
LED 캐소드(-, 짧은 다리)
330Ω 저항
MCU 핀 (아래 표에서 해당 LED에 배정된 핀)
```
전류가 3V3 → LED → 저항 → MCU 핀 순서로 흐르고 MCU 안에서 빠지는 구조라 별도로 GND를 연결할 필요는 없다 (보드가 USB로 컴퓨터에 물려 있으면 그걸로 충분).
### 어떤 LED를 어떤 핀에 연결할지 (9개 전부)
| LED 번호 | 연결할 MCU 핀 | 저항 |
|---|---|---|
| G1 | PC0 | 330Ω |
| G2 | PC1 | 330Ω |
| G3 | PC3 | 330Ω |
| G4 | PC4 | 330Ω |
| G5 | PC5 | 330Ω |
| G6 | PC6 | 330Ω |
| G7 | PC7 | 330Ω |
| G8 | PC8 | 330Ω |
| G9 | PC2 | 330Ω |
핀 이름(PC0, PC1 ...)은 NUCLEO 보드의 커넥터(모포 헤더, CN10 쪽)에 실크스크린으로 그대로 인쇄되어 있어서 이름으로 찾으면 된다. 3V3 핀도 마찬가지로 보드에 "3V3"라고 인쇄돼 있는 핀을 쓰면 된다.
> G9가 PC2인 게 좀 이상해 보일 수 있는데, G1~G8을 먼저 PC0~PC1, PC3~PC8에 배정해놓은 상태에서 마지막으로 남는 자리가 PC2였기 때문이다 (자세한 사연은 readme.md 6.7절). 표 그대로 연결하면 문제없다.
### 카메라 쪽에서 봤을 때 실제 배치 (3×3 격자, 간격 10mm)
패널에 LED를 심을 때 이 배치를 따른다 — 카메라가 찍는 화면 기준으로 자리를 잡은 것이다.
```
좌 중 우
┌──────────┬──────────┬──────────┐
상단 │ G1 │ G2 │ G3 │
│ (PC0) │ (PC1) │ (PC3) │
├──────────┼──────────┼──────────┤
중단 │ G4 │ G5 │ G6 │
│ (PC4) │ (PC5) │ (PC6) │
├──────────┼──────────┼──────────┤
하단 │ G7 │ G8 │ G9 │
│ (PC7) │ (PC8) │ (PC2) │
└──────────┴──────────┴──────────┘
LED 중심 간 간격 10mm, LED 몸통 지름 5.1mm
```
### 연결 순서 (체크리스트)
1. [ ] LED 9개 + 저항 9개 극성/자리 맞춰서 위 표대로 하나씩 연결
2. [ ] LED 애노드(+) 9개를 전부 한 군데로 모아서 3V3 핀 하나에 연결
3. [ ] 저항 반대쪽 끝을 표에 있는 MCU 핀에 각각 연결
4. [ ] 전부 연결했으면 다시 한번 표랑 대조 — 특히 G9가 PC2로 갔는지 확인
5. [ ] readme.md 10장 "브링업 절차"대로, 처음엔 LED 1개(G1/PC0)만 켜보고 정상이면 나머지 연결
**주의할 것:**
- LED 극성 반대로 끼우면 안 켜짐 (고장 아니니 방향만 바꿔서 다시 확인)
- 저항 없이 LED를 직접 핀에 연결하면 과전류로 LED나 핀이 망가질 수 있음 — 저항 절대 빼먹지 말 것
- 9개 중 하나라도 다른 핀에 잘못 꽂으면, 코드 로그(G1=.. G2=..)랑 실제 불빛 위치가 안 맞게 되니 표 대조를 꼼꼼히
### 정답 로그(시리얼)는 배선 필요 없음
LED와 달리 UART 로그(USART2, PA2/PA3)는 **추가로 연결할 게 없다.** NUCLEO 보드 안에서 PA2/PA3가 보드에 내장된 ST-LINK로 이미 연결돼 있어서, 지금 펌웨어 업로드용으로 꽂는 USB 케이블 하나가 곧 가상 COM 포트(VCP) 역할도 한다.
- PC에서 터미널 프로그램(PuTTY, screen, CoolTerm, VS Code 시리얼 모니터 등) 실행
- 보드가 잡힌 COM 포트를 **115200 8N1**로 열기
- 8초마다 `CYCLE=.. G1=.. G2=.. ... G9=..` 한 줄씩 찍히면 정상
### CoolTerm에 로그가 안 뜬 문제 — 해결됨 (원인은 보드 배선, 펌웨어는 정상이었음)
**→ SB13, SB14 브리지하고 나서 CoolTerm에 로그 정상적으로 뜨는 거 확인함 (2026-08-22).**
2026-08-22, CoolTerm으로 열었는데 아무것도 안 뜬다는 얘기가 나와서 ST-LINK로 보드에 직접 붙어 확인했다. 순서대로:
1. 최신 펌웨어를 다시 플래시하고 리셋 → CoolTerm 대신 터미널에서 시리얼 포트를 직접 열어서 20초 넘게 지켜봐도 **0바이트**. CoolTerm 설정 문제가 아니라 보드에서 아무것도 안 나오고 있다는 뜻.
2. ST-LINK로 MCU에 붙어서 **보드가 정지 없이 실제로 돌아가는 상태 그대로(Hot Plug 모드)** 레지스터를 직접 읽어봄:
- GPIOA/C/H 클럭 켜짐, TIM6·USART2 클럭 켜짐 — 전부 코드에서 의도한 그대로
- PA2/PA3가 USART2용 핀 모드(AF4)로 정확히 설정돼 있음
- PC0~PC8(G1~G9, PC2 포함) 전부 출력모드로 정확히 설정돼 있음 — 6.7절에서 고친 내용이 실제로 반영돼서 동작 중
- USART2 레지스터 확인 결과 **송신 기능이 켜져서 실제로 활성 상태**(TEACK=1), 보레이트 값도 계산했던 값(139)과 정확히 일치
- RAM에 있는 카운트 값도 우리가 만든 공식과 한 자리도 안 틀리고 일치
- 반대로 USART2 수신 쪽에는 "깨진 데이터를 받았다"는 에러 플래그가 떠 있었음 — 그 핀이 뭔가에 연결 안 된 채 붕 떠서 잡음을 줍고 있다는 신호
**MCU 내부에서는 데이터를 제대로 내보내고 있는데, 그게 컴퓨터까지 한 바이트도 안 온다.** 펌웨어/설정 쪽은 이걸로 결백이 증명된 셈이고, 남은 용의선상은 **PA2/PA3와 ST-LINK를 이어주는 보드 위의 연결(솔더 브리지)이 끊겨 있는 경우**뿐이다.
**정확한 솔더 브리지 번호 (ST 공식 UM1724 매뉴얼, "5.7 USART communication" / "Table 9. Solder bridges" 확인함):**
NUCLEO-64 보드(L073RZ 포함)는 PA2/PA3를 어디로 보낼지 솔더 브리지 4개로 정한다.
| 브리지 | 정상(VCP 사용) 상태 | 역할 |
|---|---|---|
| **SB13, SB14** (ST-LINK-USART) | **ON (이어져 있어야 함)** | PA2/PA3 ↔ ST-LINK MCU의 USART를 연결 — 이게 켜져 있어야 시리얼 로그가 PC로 나옴 |
| **SB62, SB63** (USART) | **OFF (끊겨 있어야 함)** | PA2/PA3를 대신 Arduino 커넥터(CN9의 D1/D0)나 Morpho 커넥터(CN10) 쪽으로 돌림. 이게 켜져 있으면 ST-LINK 쪽 연결이 끊긴다 |
**SB13·SB14가 열려있거나(OFF), SB62·SB63가 닫혀있으면(ON)** 지금 겪는 증상(펌웨어는 정상인데 로그가 안 뜸)과 정확히 일치한다.
**확인 방법:**
1. 보드에서 SB13, SB14, SB62, SB63 실크스크린 표시를 찾는다 (ST-LINK 쪽 회로와 CN9/CN10 커넥터 사이 어딘가에 작은 두 패드로 표시돼 있음)
2. 멀티미터 연속성(continuity) 모드로 SB13, SB14 각각의 두 패드를 찍어본다 → **삑 소리(연결됨)가 나야 정상.** 안 나면 납땜으로 이어줘야 함
3. SB62, SB63도 같은 방식으로 찍어본다 → **삑 소리가 안 나야(끊겨 있어야) 정상.** 소리가 나면 커터로 끊어야 함
4. 넷 다 정상 상태로 맞춘 뒤 다시 CoolTerm으로 확인
**급하면:** 별도 USB-TTL 시리얼 어댑터를 PA2(TX)/PA3(RX)/GND에 직접 물려서 ST-LINK VCP를 아예 안 거치고 확인. 아니면 다른 NUCLEO-L073RZ 보드로 교체 — 펌웨어는 이미 검증됐으니 보드만 바꾸면 바로 될 가능성이 큼.
출처: [UM1724 User manual - STM32 Nucleo-64 boards (MB1136)](https://www.st.com/resource/en/user_manual/um1724-stm32-nucleo64-boards-mb1136-stmicroelectronics.pdf), §5.7 USART communication / Table 9 Solder bridges (DocID025833 Rev 8)
이걸로 카메라 쪽 판독 결과랑 대조하면 된다. 안 찍히면 보드레이트(115200)부터 다시 확인.
---
## 7. LED가 잘 안 보임 — 저항/노출 얼마까지 밀어붙여도 되나
2026-08-22, 노출 20ms + 330Ω→220Ω로 바꿨는데도 LED가 협각(±10°) 때문에 잘 안 보인다는 얘기가 나와서, STM32L073 공식 데이터시트(DS10685)로 실제 한계를 확인했다.
### 저항 — 220Ω에서 더 낮추지 말 것
데이터시트 Table 24(절대최대정격) 기준:
| 항목 | 절대최대값 |
|---|---|
| 핀 1개가 sink하는 전류 | 16 mA |
| **9개 핀 합계로 sink하는 전류** | **90 mA** |
Table 61(출력전압특성)을 보면 15mA 지점에서 V_OL이 **최대 1.3V**까지 올라간다(8mA 기준으로 잡았던 0.4V가 아님). 즉 저항을 낮춰도 핀 자체의 전압강하가 커져서 기대만큼 전류가 안 오를 수 있다.
**더 중요한 건 합계 90mA 쪽이다.** 사이클 시작 직후엔 설계상 9개가 동시에 켜지는 순간이 항상 있는데, 지금 220Ω(약 8mA×9≈72mA)만 해도 이미 90mA 한도에 꽤 붙어 있다. 여기서 더 낮추면 그 동시 점등 순간에 절대정격을 넘길 수 있다.
**220Ω보다 낮추지 말 것.** 180Ω 정도가 실질적 하한선.
### 노출 — 60ms 근처까지는 늘려도 됨
지금 20ms는 30fps 프레임 주기(33ms) 안에 들어가 안전. 5.4절에서 이미 "카메라가 15fps로 떨어져도 상태당 최소 3프레임 확보"를 설계 마진으로 잡아뒀으므로, **15fps로 떨어지는 걸 감수하면 노출을 60ms 근처까지 늘려도 이 마진 안에 있음.** 그 이상(10fps대)은 5.4절 표에서 이미 "위험"으로 표시해둔 구간이라 피할 것.
전류보다 노출을 늘리는 쪽이 안전하고 효과도 큼 — 3.4절에서 이미 권장했던 방향과 같음.
### 그래도 안 보이면: 협각 자체가 원인일 가능성
±10° 협각은 전류·노출로 완전히 못 고친다. 카메라가 LED 패널 정면 중앙에 잘 정렬돼 있는지(각 LED가 카메라를 정확히 향하고 있는지) 먼저 확인. 정렬이 어긋나 있으면 아무리 밝게/오래 노출해도 카메라가 빔 가장자리(광량 30~50% 이하 구간)만 보게 되므로 근본 해결이 안 됨.
출처: [Datasheet - STM32L073x8 STM32L073xB STM32L073xZ (DS10685)](https://www.st.com/resource/en/datasheet/stm32l073v8.pdf), §6.2 Table 24 Current characteristics / §6.3.13 Table 61 Output voltage characteristics