[{"content":"하드웨어 편에서 ADS1232 4개로 로드셀을 동시에 샘플링하는 구조를 설명했다. 이번에는 그 데이터를 어떻게 저장하고 관리하는지, 소프트웨어 파이프라인을 정리해보겠다.\n왜 Flash가 아닌 PSRAM인가 처음에는 측정 데이터를 SD카드나 SPIFFS(Flash)에 직접 저장하는 방식을 생각했다.\n세 가지 문제가 있었다.\n첫째, Flash 쓰기는 느리다.\nSPIFFS 기준으로 단순 쓰기도 수 ms가 걸린다. 40 SPS로 샘플링 중이라면 한 샘플마다 25ms 안에 모든 처리를 끝내야 하는데, Flash 쓰기가 이 시간을 잡아먹으면 샘플을 놓친다.\n둘째, 내부 SRAM이 부족하다.\nESP32 내부 SRAM은 약 520KB다. 여기에 Wi-Fi 스택이 100~200KB를 점유하고, AsyncWebServer와 WebSocket 라이브러리까지 올라가면 여유가 거의 없다. 1.65MB짜리 버퍼를 내부 SRAM에 올리는 건 처음부터 불가능하다. malloc 대신 ps_calloc을 쓴 이유가 바로 이것이다.\n셋째, Wi-Fi 스택과 메모리를 두고 경쟁하면 크래시가 난다.\nWi-Fi는 내부 SRAM에서 동작한다. 데이터 버퍼까지 같은 공간을 쓰면 메모리 단편화가 쌓이다가 어느 순간 Wi-Fi 재연결 중 OOM으로 재부팅된다. 이 버그는 재현도 불규칙하고 로그만 봐서는 원인을 잡기 어렵다. PSRAM은 내부 SRAM과 완전히 별도 영역이라 이 경쟁 자체를 피할 수 있다.\n반면 PSRAM은 ns 단위로 읽고 쓴다. 메인 루프에서 포인터 대입 한 번이면 끝이다. ESP32 Feather V2에는 2MB PSRAM이 탑재되어 있어 이걸 전부 측정 버퍼로 쓰기로 했다.\n데이터 구조 설계 저장 단위는 한 번의 샘플, 즉 로드셀 4채널 + 각도 1채널이다.\n1 2 3 4 5 6 7 8 struct sensors { volatile long DATA1; // 우측 팔 (4 bytes) volatile long DATA2; // 좌측 팔 (4 bytes) volatile long DATA3; // 우측 다리 (4 bytes) volatile long DATA4; // 좌측 다리 (4 bytes) unsigned long encoderAngle; // 크랭크 각도 (4 bytes) }; // 총 20 bytes / 샘플 volatile은 ISR 안에서 값이 바뀌기 때문에 컴파일러 최적화를 막기 위해 붙였다. 이게 없으면 컴파일러가 \u0026ldquo;이 값은 여기서 안 바뀌네\u0026quot;라고 판단하고 레지스터에 캐시해버려서 ISR이 업데이트한 최신 값을 못 읽을 수 있다.\n최대 기록 시간을 36분으로 잡았을 때 필요한 샘플 수와 메모리는:\n1 2 3 #define PACKET_SIZE (36 * 60 * 40) // 86,400 샘플 // 86,400 샘플 × 20 bytes = 1,728,000 bytes ≈ 1.65 MB 2MB PSRAM 안에 들어온다.\nPSRAM 초기화 setup() 안에서 PSRAM을 확인하고 메모리를 할당한다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 Serial.print(\u0026#34;PSRAM found : \u0026#34;); Serial.println(isPsram = psramFound()); if (isPsram) { if (psramInit()) { Serial.println((String)\u0026#34;Memory available in PSRAM : \u0026#34; + ESP.getFreePsram()); workingPointer = packetPointer = (struct sensors *)ps_calloc(PACKET_SIZE, sizeof(struct sensors)); } else { Serial.println(\u0026#34;PSRAM initilization failed\u0026#34;); isPsram = false; } } ps_calloc은 PSRAM에서 메모리를 할당하는 함수다. 일반 malloc을 쓰면 내부 SRAM에서 할당을 시도하는데, 앞서 말했듯 1.65MB는 처음부터 들어가지 않는다. calloc이기 때문에 할당과 동시에 0으로 초기화된다.\n포인터는 두 개를 사용한다.\n1 2 struct sensors *packetPointer; // 버퍼 시작 주소 (고정) struct sensors *workingPointer; // 현재 쓰기 위치 (이동) packetPointer는 버퍼의 시작점을 가리키며 절대 바뀌지 않는다. workingPointer는 데이터를 쓸 때마다 앞으로 이동한다. 나중에 SAVE 명령이 오면 workingPointer를 다시 packetPointer로 돌려 처음부터 전송한다.\n메인 루프와 40 SPS loop()의 핵심은 DRDY 핀의 Falling Edge 감지다.\nADS1232는 새 데이터가 준비되면 DRDY/DOUT 핀을 HIGH → LOW로 내린다. 이걸 소프트웨어 폴링으로 감지한다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void loop() { c = digitalRead(pin_ADC_DRDY_DOUT_1); if ((t == HIGH) \u0026amp;\u0026amp; (c == LOW)) { // Falling Edge 감지 DatSampleIdx++; WebSampleIdx++; if (DatSampleIdx \u0026gt;= DataSubSamplePoint) { // 2번마다 1번 처리 DatSampleIdx = 0; // 데이터 처리 } } t = c; } ADS1232는 최대 80 SPS로 동작한다. DataSubSamplePoint = 2로 설정해두었기 때문에 Falling Edge 2번 중 1번만 실제로 처리한다. 결과적으로 40 SPS가 된다.\n1 #define DataSubSamplePoint 2 // 80 / 2 = 40 SPS 80 SPS를 전부 쓰지 않은 이유는, 40 SPS면 재활 운동 데이터 수집에 충분하고 처리 시간과 저장 용량이 절반으로 줄기 때문이다. 80 SPS를 그대로 쓰면 36분이 아니라 18분 제한이 된다.\nFalling Edge마다 일어나는 일 Falling Edge를 감지하고 DatSampleIdx가 조건을 만족하면 다음 순서로 처리된다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // 1. 클럭 펄스 24회 발생 → ISR이 4채널 데이터 수집 idx = 0; for (int i = 0; i \u0026lt; 24; i++) { digitalWrite(pin_SCLK_OUT, HIGH); delayMicroseconds(10); digitalWrite(pin_SCLK_OUT, LOW); delayMicroseconds(10); } // 2. 각도 읽기 (I2C) if (isWire) { Sensors.encoderAngle = as5600.readAngle(); } else { Sensors.encoderAngle = 8192; // 미연결 표시 } // 3. NeoPixel로 실시간 시각화 (디버깅용) pixel.setPixelColor(0, Sensors.DATA1 \u0026lt;\u0026lt; 8); pixel.show(); // 4. PSRAM에 저장 (START 명령 이후에만) if (isPsram) { if (!quit \u0026amp;\u0026amp; packetCounter \u0026lt; PACKET_SIZE) { *workingPointer++ = Sensors; packetCounter++; } } 1번에서 SCLK_OUT으로 24개의 클럭 펄스를 직접 발생시킨다. 이 펄스마다 SCLK_IN 핀에 인터럽트가 걸려 SPI_ISR()이 실행되고, 4채널 데이터가 1비트씩 수집된다. 24클럭이 끝나면 Sensors.DATA1~4에 최종 값이 들어있다.\n2번에서 AS5600 각도를 I2C로 읽는다. 미연결 상태면 8192라는 예약값을 넣어 웹 UI에서 \u0026ldquo;N.C.\u0026ldquo;로 표시하게 했다.\n3번은 디버깅 용도다. NeoPixel의 G채널에 DATA1 값을 매핑하면 오른팔에 힘이 들어갈수록 LED가 밝아진다. 오실로스코프 없이 센서 동작을 육안으로 확인할 수 있어서 개발 중에 유용했다.\nPSRAM 저장 핵심 한 줄 1 *workingPointer++ = Sensors; C 포인터 문법이 익숙하지 않으면 낯설어 보이지만, 풀어쓰면 이렇다.\n1 2 *workingPointer = Sensors; // workingPointer가 가리키는 주소에 Sensors 구조체 복사 workingPointer++; // 포인터를 다음 구조체 위치로 이동 ++는 바이트 단위가 아니라 타입 크기 단위로 이동한다. struct sensors가 20 bytes이므로 workingPointer++는 주소를 20 증가시킨다. 이렇게 하면 배열처럼 PSRAM에 구조체가 순서대로 쌓인다.\n1 2 3 4 5 6 PSRAM ┌──────────────────────────────────────────────┐ │ Sensors[0] │ Sensors[1] │ ... │ Sensors[N] │ └──────────────────────────────────────────────┘ ↑ ↑ packetPointer workingPointer 실시간 화면 표시와 PSRAM 저장은 분리되어 있다 여기서 한 가지 흥미로운 점이 있다. 웹 화면을 열면 START를 누르지 않아도 센서 값이 실시간으로 표시된다. PSRAM에 저장하는 것과 화면에 보여주는 것이 완전히 분리된 구조이기 때문이다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if ((t == HIGH) \u0026amp;\u0026amp; (c == LOW)) { DatSampleIdx++; WebSampleIdx++; // quit 상태와 무관하게 항상 증가 if (DatSampleIdx \u0026gt;= DataSubSamplePoint) { // PSRAM 저장: quit = false (START 이후)일 때만 if (!quit \u0026amp;\u0026amp; packetCounter \u0026lt; PACKET_SIZE) { *workingPointer++ = Sensors; } } // 화면 전송: 항상 동작, 16샘플(0.4초)마다 JSON 전송 if (!NoNetwork \u0026amp;\u0026amp; (WebSampleIdx \u0026gt;= 16)) { WebSampleIdx = 0; sendSensorsWs(null); } } WebSampleIdx는 quit 여부와 상관없이 Falling Edge마다 계속 증가한다. 16샘플(= 0.4초)마다 현재 Sensors 구조체 값을 JSON으로 브라우저에 보낸다. ISR은 항상 돌고 있으니 Sensors에는 항상 최신 값이 들어있다.\n정리하면:\nPSRAM 저장 → START 버튼을 눌러야 시작 (quit = false) 화면 실시간 표시 → 항상 동작, 0.4초마다 갱신 STOP 후 SAVE: 데이터 전송 흐름 측정을 마치고 SAVE 버튼을 누르면 PSRAM에 쌓인 데이터를 처음부터 끝까지 브라우저로 전송한다.\n1 2 3 4 5 6 7 8 9 10 STOP → quit = true (측정 중단) SAVE → save = true (전송 시작) PSRAM [샘플0][샘플1]...[샘플N] ↓ 5분치(12,000 샘플)씩 청크1 전송 → 500ms 대기 청크2 전송 → 500ms 대기 ... 나머지 전송 1바이트 종료 신호 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #define PACKET_LENGTH (5 * 60 * 40) // 5분치 = 12,000 샘플 = 240 KB workingPointer = packetPointer; // 포인터를 처음으로 되돌림 int pn = packetCounter / PACKET_LENGTH; // 완전한 청크 수 int pr = packetCounter % PACKET_LENGTH; // 나머지 for (int p = 0; p \u0026lt; pn; p++) { ws.binary(client_id, (uint8_t*)workingPointer, PACKET_LENGTH * sizeof(struct sensors)); workingPointer += PACKET_LENGTH; delay(500); } if (pr) { ws.binary(client_id, (uint8_t*)workingPointer, pr * sizeof(struct sensors)); } ws.binary(client_id, (uint8_t*)workingPointer, 1); // 종료 신호 한 번에 전부 보내지 않고 5분치씩 나눠 보내는 이유는 WebSocket이 결국 TCP 위에서 동작하기 때문이다. ESP32의 TCP 송신 버퍼는 작아서 1.65MB를 한 번에 큐에 넣으면 버퍼가 넘친다. 청크 하나를 보내고 500ms를 기다리는 동안 TCP 스택이 실제로 데이터를 전송할 시간을 주는 것이다.\ndelay(500) 동안 loop()가 멈추기 때문에 이 시간에 Falling Edge가 와도 샘플을 놓친다. 그래서 SAVE는 반드시 STOP 이후에만 의미가 있고, 코드에서도 quit \u0026amp;\u0026amp; save 조건일 때만 전송 로직이 실행된다.\n브라우저 쪽에서는 청크를 받을 때마다 ArrayBuffer에 이어 붙이다가, 마지막 1바이트 종료 신호가 오면 파일로 저장한다.\n상태 플래그 관리 1 2 bool quit = true; // 초기 상태: 측정 안 함 bool save = false; // 저장 명령 플래그 WebSocket 명령에 따라 상태가 바뀐다.\n1 2 3 4 5 6 7 8 9 10 11 12 if (strcmp((char*)data, \u0026#34;START\u0026#34;) == 0) { workingPointer = packetPointer; // 포인터를 처음으로 packetCounter = 0; quit = false; // 측정 시작 } else if (strcmp((char*)data, \u0026#34;STOP\u0026#34;) == 0) { quit = true; } else if (strcmp((char*)data, \u0026#34;SAVE\u0026#34;) == 0) { client_id = client-\u0026gt;id(); save = true; } START 명령을 받으면 workingPointer를 packetPointer로 되돌리고 packetCounter를 0으로 리셋한다. 버퍼를 따로 초기화하지 않아도 이전 데이터는 논리적으로 없는 것과 같다. 36분이 지나 버퍼가 꽉 차면 자동으로 측정을 중단한다.\n1 if (packetCounter == PACKET_SIZE) quit = true; 마무리 이번 글에서 다룬 내용을 정리하면:\n왜 PSRAM: Flash 쓰기 지연, 내부 SRAM 용량 한계, Wi-Fi 스택과의 메모리 경쟁 세 가지를 동시에 해결 데이터 구조: struct sensors 20 bytes × 86,400 샘플 = 1.65 MB 40 SPS: DataSubSamplePoint = 2로 80 SPS를 절반만 처리 저장 방식: *workingPointer++ = Sensors로 구조체를 순서대로 적재 화면 표시와 저장 분리: WebSampleIdx는 항상 증가, 0.4초마다 JSON 전송 SAVE 전송: 5분치 청크로 나눠 전송, 1바이트 종료 신호로 완료 통보 다음 글에서는 측정 전 반드시 해야 하는 영점 보정(캘리브레이션) 로직과 EEPROM 레이아웃, 그리고 AS5600 각도 센서 연동 방법을 자세히 설명하겠다.\n","permalink":"https://bromine1997.github.io/posts/esp32/esp32-rehabilitation-bicycle-psram/","summary":"40 SPS로 4채널 센서 데이터를 최대 36분간 끊김 없이 기록하기 위해 PSRAM을 버퍼로 사용했다. 구조체 설계부터 포인터 기반 저장, 청크 전송, 실시간 표시 분리까지 정리했다.","title":"[ESP32] PSRAM 버퍼링과 데이터 수집 파이프라인"},{"content":"지난 글에서 ESP32 Feather V2를 선택한 이유에 대해 이야기했다. 오늘은 본격적으로 4개의 로드셀 데이터를 어떻게 정확하게 동시에 읽어왔는지, 하드웨어 측면에서 이야기해보겠다.\nESP32 내장 ADC도 있지 않나요? 프로젝트를 시작하면서 가장 먼저 고민했던 부분이 바로 로드셀 데이터를 어떻게 읽을 것인가였다.\n\u0026ldquo;ESP32에 ADC 있잖아? 그냥 연결하면 되는 거 아니야?\u0026rdquo;\n단순한 프로젝트라면 내장 ADC를 써도 되지만, 연구용이기 때문에 정확도와 정밀도 두 가지 모두 중요했다.\nESP32 내장 ADC의 한계:\n12비트 분해능: 0~4095 단계로만 구분 — 정밀 측정에는 부족 정확도 부족: 오차범위가 ±5% 수준 차동 입력 불가: 로드셀은 차동 신호인데 단일 입력만 지원 ADS123X가 뭔가 ADS123X는 Texas Instruments에서 만든 로드셀/압력센서 전용 24비트 ADC다.\n모델 입력 채널 ADS1231 1채널 ADS1232 2채널 ADS1234 4채널 주요 스펙:\n항목 사양 분해능 24비트 (16,777,216 단계) 입력 방식 차동 입력 내장 PGA 신호 증폭 기능 내장 최대 샘플링 80 SPS 노이즈 정밀 측정에 최적화 왜 ADS1232를 4개나 쓴 건가 나는 ADS1232를 총 4개 사용했다.\nADS1232는 2채널 차동 입력을 갖고 있어서, 이론상 ADS1232 2개로 4개의 로드셀 데이터를 얻을 수 있다.\n그런데 여기에 문제가 있었다.\nADS1232는 내부 멀티플렉서로 채널을 전환하는데, 이 전환에 시간이 걸린다.\n1 채널 1 측정 → 채널 전환 → 안정화 대기 → 채널 2 측정 데이터시트를 확인하면 채널 전환 후 안정화에 수 ms가 필요하다는 걸 알 수 있다. 두 채널이 정확히 같은 시점에 측정되지 않는 것이다.\n코끼리 재활자전거처럼 팔과 다리에서 발생하는 힘을 동시에 측정해야 하는 경우, 이 수 ms의 시간차가 치명적이다. 환자가 힘을 주는 순간을 정확히 잡아내야 하기 때문이다.\n그래서 ADS1232를 4개, 각 채널마다 하나씩 배치하는 방식을 택했다.\n오실레이터로 동기화 ADS1232는 내부 클럭으로 자동 측정이 가능하다. 그러나 내부 클럭을 각자 쓰면 4개가 정확히 같은 타이밍에 샘플링한다는 보장이 없다.\n이를 해결하기 위해 4.9152 MHz 외부 오실레이터를 하나 두고, 4개의 ADS1232 CLKIN 핀에 동시에 클럭을 공급했다.\n1 2 3 4 5 [4.9152 MHz 오실레이터] ↓ ┌─────┼─────┬─────┐ ADS1232 ADS1232 ADS1232 ADS1232 (우측팔) (좌측팔) (우측다리) (좌측다리) 같은 클럭 소스를 공유하면 4개의 ADC가 동시에 샘플링을 진행하므로 채널 간 시간차를 없앨 수 있다.\n채널 1, 2 (우측팔, 좌측팔) + ESP32 회로도\n채널 3, 4 (우측다리, 좌측다리) + 오실레이터 회로도\n회로도를 보면 SPI 통신을 하는 것처럼 생겼는데, 핀 연결이 조금 독특하다. 간단히 설명하면 SCLK의 Falling Edge마다 인터럽트를 발생시켜 DOUT 핀에서 1비트씩 데이터를 읽고, 이를 24번 반복해 4개 채널의 데이터를 동시에 수집한다. 이 부분은 다음 글인 소프트웨어 편에서 자세히 다루겠다.\n참고로 당시에 구할 수 있는 소자가 ADS1232 모듈이었기 때문에, ADS1231 4개가 아닌 ADS1232 4개를 사용했다.\nAS5600으로 크랭크 각도 측정 로드셀이 힘을 측정한다면, AS5600은 자전거 크랭크의 회전 각도를 측정한다.\nAS5600은 12비트 자기식 회전 위치 센서다.\n항목 사양 분해능 12비트 (0~4095, 즉 360° ÷ 4096 단계) 통신 I2C 측정 범위 0~360° 연속 측정 방식 비접촉 자기식 왜 AS5600인가 각도를 측정하는 방법은 여러 가지가 있다. 기계식 엔코더, 광학식 엔코더, 자기식 엔코더 등.\n기계식이나 광학식은 접촉하거나 슬릿 디스크가 필요해서 마모나 오염에 취약하다. 반면 AS5600은 샤프트 끝에 작은 자석 하나만 붙이면 된다. 구조가 단순하고, 비접촉이라 마모가 없다.\n자전거 크랭크처럼 연속 회전하는 구조에서는 비접촉 방식이 신뢰성 면에서 훨씬 낫다.\nI2C로 연결 AS5600은 I2C로 ESP32와 통신한다. Wire.h와 AS5600 라이브러리(Rob Tillaart)를 사용했다.\n1 2 3 4 5 6 7 #include \u0026lt;Wire.h\u0026gt; #include \u0026lt;AS5600.h\u0026gt; AS5600 as5600; // 각도 읽기 uint16_t rawAngle = as5600.readAngle(); // 0~4095 각도 오프셋은 영점 보정 시 EEPROM에 저장해두고, 부팅 시 자동으로 불러온다.\n1 2 3 // EEPROM에서 오프셋 로드 후 AS5600에 적용 short savedOffset = EEPROM.readShort(EEPROM_AngleOffset); as5600.setOffset(-savedOffset * AS5600_RAW_TO_DEGREES); 이렇게 하면 재부팅해도 캘리브레이션 값이 유지된다.\n완성된 하드웨어 최대한 노이즈를 줄이기 위해 작은 보드에 직접 납땜했다. 캐패시터와 저항도 모두 올라가 있다.\n전체 시스템 구성도\n최대한 노이즈를 줄이기 위해 작은 보드에 손납땜으로 제작했다. 손납땜으로 만든 이유는 테스트 단계에서 저항값, Gain 설정 등을 언제든지 바꿀 수 있게 하기 위해서였다. 이 작은 보드 하나에 ADS1232 4개, 오실레이터, 각종 필터 캐패시터가 다 들어가 있다.\n마무리 정리하면:\n내장 ADC 대신 ADS1232: 24비트 분해능과 차동 입력이 필요했기 때문 ADS1232 4개: 채널 전환 없이 4채널을 완전히 동시에 샘플링하기 위해 외부 오실레이터: 4개의 ADS1232를 동일 클럭으로 동기화하기 위해 AS5600: 비접촉 자기식 센서로 크랭크 각도 연속 측정 다음 글에서는 이 하드웨어에서 실제로 데이터를 어떻게 읽어오는지, ISR 기반 24비트 동시 수집과 2의 보수 부호 처리 코드를 설명하겠다.\n","permalink":"https://bromine1997.github.io/posts/esp32/esp32-rehabilitation-bicycle-hardware/","summary":"ESP32 내장 ADC 대신 ADS1232를 4개 사용해 로드셀 4채널을 동시에 측정한 이유, 그리고 크랭크 각도를 측정하는 AS5600까지. 하드웨어 설계 과정을 정리했다.","title":"[ESP32] 4개의 로드셀을 동시에 측정하기 - 하드웨어"},{"content":"ESP32를 Arduino IDE로 개발하면 처음에 다음 두 함수만 보인다.\n1 2 3 4 5 6 7 void setup() { // put your setup code here, to run once: } void loop() { // put your main code here, to run repeatedly: } 단순해 보이지만, 이 구조가 왜 이렇게 생겼는지 알고 쓰는 것과 모르고 쓰는 건 다르다고 생각한다.\n전원이 들어오면 실제로 무슨 일이 일어날까 1) MCU 리셋 전원이 인가되거나 리셋 버튼을 누르면, MCU 내부에서 가장 먼저 일어나는 일은 프로그램 카운터(PC) 초기화다.\n프로그램 카운터가 초기화되지 않으면 이전 실행 주소가 남아있게 된다. 그러면 엉뚱한 위치부터 코드가 실행되거나 무한루프에 빠져 시스템이 원하는 대로 동작하지 않는다.\n2) 부트로더 실행 프로그램 카운터가 초기화되면, 플래시 메모리에 저장된 부트로더 코드가 먼저 실행된다.\n부트로더가 하는 일:\n새로운 프로그램 업로드 요청이 있는지 확인 업로드가 없으면 → 사용자 프로그램으로 점프 Arduino Core가 숨겨놓은 진짜 시작점 사용자는 main()을 작성하지 않는다. 하지만 Arduino Core 내부에는 실제로 main 함수가 존재한다.\n다음은 Arduino의 main.cpp다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include \u0026lt;Arduino.h\u0026gt; int atexit(void (* /*func*/ )()) { return 0; } void initVariant() __attribute__((weak)); void initVariant() { } void setupUSB() __attribute__((weak)); void setupUSB() { } int main(void) { init(); initVariant(); #if defined(USBCON) USBDevice.attach(); #endif setup(); for (;;) { loop(); if (serialEventRun) serialEventRun(); } return 0; } setup()은 단 한 번 호출되고, 그 다음 for(;;) 안에서 loop()가 무한히 반복되는 구조다.\nsetup()은 왜 한 번만 실행될까 setup()은 초기화 전용 함수다. 여기서 주로 하는 일은 이렇다.\nGPIO 방향 설정 (pinMode) 통신 초기화 (Serial.begin, Wire.begin 등) 센서 초기 설정 변수 초기값 세팅 임베디드 개발 관점에서 보면 setup()은 시스템 초기화 단계에 해당한다.\n만약 setup()이 반복 실행된다면 어떻게 될까.\n통신이 계속 초기화되고 핀 상태가 계속 리셋되고 시스템 전체가 불안정해진다 그래서 Arduino Core는 setup은 1회, loop는 무한 반복이라는 구조를 강제한다.\nloop()에서 delay()를 쓰면 안 되는 이유 loop()의 실행 구조를 다시 보면:\n1 2 3 4 for (;;) { loop(); if (serialEventRun) serialEventRun(); } loop()는 for(;;) 안에서 계속 호출된다. 여기서 delay()를 사용하면 CPU가 그 시간 동안 아무것도 하지 못하고 멈춰버린다. 이걸 Blocking이라고 표현한다.\nLED 하나 깜빡이는 수준에서는 문제가 없다. 하지만 센서를 읽으면서 Wi-Fi 통신도 처리하고 웹 서버도 돌려야 하는 상황이라면 delay() 하나가 전체 흐름을 막아버린다. ESP32처럼 많은 걸 동시에 처리해야 하는 환경에서는 특히 문제가 된다.\n이걸 해결하는 방법은 크게 두 가지다.\n인터럽트 — 특정 이벤트가 발생했을 때만 CPU가 반응하도록 RTOS — 여러 작업을 시분할로 나눠 실행 (ESP32는 FreeRTOS가 기본 탑재) 마무리 취미로 ESP32를 쓴다면 setup에 초기화, loop에 반복 동작을 넣는 것만 알아도 충분하다.\n하지만 조금 더 나아가려면 이 구조가 왜 이렇게 설계됐는지, 내부에서 어떤 순서로 실행되는지를 이해하는 게 도움이 된다. 특히 delay() 대신 인터럽트나 타이머를 쓰기 시작하면, 결국 이 실행 구조를 알고 있어야 제대로 활용할 수 있다.\n","permalink":"https://bromine1997.github.io/posts/esp32/esp32-setup-loop-internals/","summary":"ESP32를 Arduino IDE로 개발하면 setup()과 loop()만 보인다. 그런데 실제로 전원이 켜진 순간부터 loop()가 돌기까지, 내부에서는 어떤 일이 일어날까.","title":"[ESP32] setup()과 loop()는 어떻게 실행되는가"},{"content":"최근 ASUS Tinker Board 2S를 이용해서 개발을 진행하고 있다. 그 과정에서 SPI 통신 파형이 이상하게 나오는 문제가 발생해서 이 부분을 기록으로 남겨두려 한다.\n개발 환경 Tinker Board 2S에서는 SPI1과 SPI5, 두 개의 SPI 버스를 사용할 수 있다.\nTinker Board 2S GPIO Pin Map\n개발 언어는 Android Studio에서 Java를 사용하고 있으며, GPIO 제어는 MRAA 라이브러리를 사용했다.\nMRAA 라이브러리에서 SPI Index 설정\nMRAA에서는 위 그림처럼 index를 설정해주면 SPI1과 SPI5를 각각 사용할 수 있다.\n문제 상황 AD5420과 MAX1032 클래스를 이용해 SPI를 초기화하고 디버깅 목적으로 SPI.write()를 호출해 오실로스코프로 파형을 확인해봤다.\nAndroid Studio에서 작성한 SPI Initialize 코드\n결과는 예상과 달랐다.\nSPI1 버스에서 측정한 MOSI와 CLK 파형 — 삼각파가 출력됨\nSPI5 버스에서 측정한 MOSI와 CLK 파형 — 오실레이션 발생\nSPI1: 사각파가 아닌 삼각파가 출력됨 SPI5: 파형에 오실레이션이 발생해 원본 데이터가 손상될 가능성이 커 보임 원인 분석 처음에는 하드웨어 문제를 의심했다. Bypass Capacitor를 하나씩 교체해가며 확인했지만 증상은 그대로였다.\n그러다 전혀 생각하지 못했던 부분에서 원인을 찾았다.\n기존 AD5420 SPI Class — Frequency가 20MHz로 설정되어 있었다\n바로 SPI1.Frequency였다. 기존에 20MHz로 아무 생각 없이 설정해두고 있었다.\n그런데 Tinker Board 2S와 AD5420 사이에는 Digital Isolator(ADUM1400) 가 삽입되어 있다. ADUM1400 데이터시트를 확인해보니 문제가 명확해졌다.\nADUM1400 데이터시트 — Maximum pulse width 1000ns\nADUM1400의 Maximum pulse width는 1000ns, 즉 최대 10MHz까지만 정상 동작을 보장한다. 20MHz 파형은 이 한계를 넘어서 왜곡이 발생한 것이었다.\n수정 결과 코드에서 SPI1 Frequency를 1MHz로 낮춘 뒤 파형을 다시 확인했다.\nFrequency 1MHz로 수정 후 SPI1 파형 — 정상적인 사각파 확인\n정상적인 사각파가 나오는 것을 확인했다.\n다만 SPI5(MAX1032) 쪽은 1MHz로는 부족했고, 50kHz까지 낮춰야 정상 파형이 나왔다. 같은 보드에서 두 SPI 라인이 이렇게 큰 차이를 보이는 이유는 아직 명확하지 않다. 추후에 더 공부해보려고 한다.\n정리 항목 기존 수정 후 SPI1 Frequency 20MHz 1MHz SPI1 파형 삼각파 (왜곡) 정상 사각파 SPI5 Frequency 20MHz 50kHz SPI5 파형 오실레이션 정상 사각파 Digital Isolator를 사용할 때는 반드시 해당 소자의 Maximum pulse width(최대 동작 주파수)를 확인해야 한다는 걸 이번에 직접 경험으로 배웠다. 하드웨어를 먼저 의심하다가 시간을 꽤 날렸는데, 데이터시트를 먼저 꼼꼼히 읽는 습관의 중요성을 다시 한번 느꼈다.\n","permalink":"https://bromine1997.github.io/posts/troubleshooting/tinkerboard2s-spi-frequency-issue/","summary":"ASUS Tinker Board 2S에서 SPI 통신 파형이 왜곡되는 문제를 겪었다. 원인은 하드웨어가 아닌 Digital Isolator의 주파수 제한이었다.","title":"[Tinker Board 2S] SPI 파형 왜곡 문제와 Digital Isolator 주파수 제한"},{"content":"첫 번째 글은 본격적인 코드리뷰에 들어가기 전에, 제가 사용한 MCU와 개발환경을 간단하게 정리해보려고 합니다.\n개발을 할 때마다 느끼는 거지만 개발환경 설정하는 게 정말 귀찮고 어렵습니다. 처음이면 아무것도 모르는데, 적응하면 또 적응하더라고요. 한번 같이 친해져봅시다.\n사용한 MCU: ATmega4809 보통 펌웨어를 처음 배우면 AVR 계열 MCU로 많이 학습하는 것 같습니다. 그 중에서도 ATmega128이 제일 많이 사용되는 것 같습니다. 저는 그 중 ATmega4809 시리즈를 사용했습니다.\n보통 MCU 뒤에 붙는 숫자들은 포트의 수나 메모리 크기 등 스펙을 의미합니다.\n실제 사용한 ATmega4809 개발 보드\n개발환경: Microchip Studio 개발환경은 Microchip Studio를 사용했습니다.\nAVR 계열 MCU를 다룰 때 가장 흔하게 쓰이는 IDE입니다. 프로젝트 생성부터 빌드, 디버깅까지 비교적 깔끔한 UI를 가지고 있습니다. 저는 꽤 만족스러웠던 개발환경이었습니다.\n특히 디버깅 환경이 편해서 문제를 찾기 수월했습니다.\nMicrochip Studio IDE 화면\n마무리 기본적인 소개는 여기까지입니다.\n다음 글부터 본격적인 코드리뷰를 시작하겠습니다.\n","permalink":"https://bromine1997.github.io/posts/atmega/atmega-code-review-01-environment/","summary":"본격적인 코드리뷰에 앞서 제가 사용한 ATmega4809 MCU와 Microchip Studio 개발환경을 간단하게 소개합니다.","title":"[ATMEGA4809] MCU 소개 및 개발환경 설정"},{"content":"안녕하세요.\nATMEGA4809 라는 제목으로, 제가 학부생 시절 수업 시간에 배웠던 ATmega4809를 이용하여 작성했던 펌웨어 코드를 하나씩 보면서 \u0026ldquo;코드리뷰\u0026rdquo; 형태로 정리해보는 기록입니다.\n왜 시작하게 됐나 처음 개발을 하다 보면 일단 돌아가게 만드는 것이 첫 번째 목표가 되기 쉽습니다. 저 또한 수업을 일찍 끝내고 싶어서, 가산점을 받고 싶어서 일단 돌아가게 만들었던 기억이 있습니다.\n그러나 프로젝트가 조금이라도 커지면 어느 순간부터 다음과 같은 문제점들이 발견됩니다.\n수정하면 다른 기능에 에러가 생긴다 시간이 지나면 내가 봐도 다시 이해하기 힘든 코드 수정조차 어렵다 그래서 이 코드리뷰를 진행하면서 제가 실제로 겪었던 코드의 문제점과 개선 과정들을 솔직하게 남기려고 합니다.\n단순히 동작하는 코드가 아니라 잘 짜여진 코드를 찾아가는 과정이라고 생각하시면 편합니다.\n진행 방식 각 글은 다음 순서로 진행합니다.\n코드 일부를 가져와서 현재 구조를 설명하고 어떤 점이 불편했는지(혹은 위험했는지) 짚고 개선 방향을 정한 뒤 리팩토링한 결과를 다시 비교해보는 방식 다룰 주제 main 구조 (초기화 순서, 루프 구성) 인터럽트 / 타이머 처리 방식 UART / I2C / SPI 같은 통신 코드 ADC / 센서 데이터 처리 (필터링, 캘리브레이션) 마치며 이 글이 완벽한 정답은 아닐 수 있습니다. 적어도 저한테는 코드를 더 깔끔하게 만들고, 잘 만들어진 코드가 무엇인지 이해하는 과정이 될 것 같아서 시작하게 되었습니다.\n비슷한 MCU나 임베디드 C언어를 공부하는 분들에게 도움이 되면 더 좋을 것 같습니다.\n다음 글부터 ATmega4809에 관한 간단한 소개와 전체 구조 설명을 시작으로 리뷰를 진행해보겠습니다.\n글 읽어주셔서 감사합니다.\n","permalink":"https://bromine1997.github.io/posts/atmega/atmega-code-review-intro/","summary":"학부 시절 ATmega4809로 작성했던 펌웨어 코드를 다시 꺼내 코드리뷰 형태로 정리하는 시리즈를 시작합니다. 단순히 동작하는 코드가 아닌, 잘 짜여진 코드를 찾아가는 과정입니다.","title":"[ATMEGA4809] 시작하며"},{"content":"1편에서는 GPIO의 구조와 내부 블록 다이어그램, 데이터시트 읽는 법까지 다뤘다. 마지막에는 아래 코드로 User Button을 눌렀을 때 LED가 켜지는 실습도 해봤는데,\n1 2 3 4 if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); } 사실 이 코드가 내부에서 뭘 하는지는 설명 안 하고 넘어갔다. HAL_GPIO_ReadPin()이 IDR을 읽는다는 건 알겠는데, 어떤 방식으로? HAL_GPIO_WritePin()은 왜 ODR 대신 BSRR을 쓰는 건지, 그리고 그게 왜 중요한지.\n이번 편에서는 그 안을 직접 열어볼 거다. HAL 소스 코드를 뜯으면서 레지스터 레벨까지 내려가 보고, 마지막엔 HAL 없이 레지스터만으로 똑같이 구현해볼 거다.\nHAL 소스 어떻게 보나 CubeIDE에서 함수에 커서 올리고 F3 누르면 해당 함수 정의로 바로 이동한다. 또는 함수명에서 Ctrl + 클릭.\n파일 위치는 프로젝트마다 다르지만 보통 이쪽이다.\n1 2 Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_gpio.c Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_gpio.h 여기가 HAL GPIO 관련 로직이 다 모여있는 곳이다. 이걸 직접 열어서 1편 코드와 대조해보는 게 이번 포스트 목표다.\nHAL_GPIO_ReadPin() 내부 1편에서 쓴 코드:\n1 HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) F3 눌러서 소스 열어보면 이렇다.\n함수 본문은 생각보다 단순하다. IDR(Input Data Register) 에서 해당 핀 비트를 읽어서 반환하는 게 전부다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin) { GPIO_PinState bitstatus; if ((GPIOx-\u0026gt;IDR \u0026amp; GPIO_Pin) != (uint32_t)GPIO_PIN_RESET) { bitstatus = GPIO_PIN_SET; } else { bitstatus = GPIO_PIN_RESET; } return bitstatus; } 반환 타입인 GPIO_PinState는 stm32f4xx_hal_gpio.h에 아래처럼 정의돼 있다.\n1 2 3 4 5 typedef enum { GPIO_PIN_RESET = 0, GPIO_PIN_SET } GPIO_PinState; GPIOx-\u0026gt;IDR \u0026amp; GPIO_Pin은 IDR 레지스터에서 특정 핀 비트만 마스킹해서 읽는 것이다. PC13이라면 GPIOC-\u0026gt;IDR \u0026amp; (1 \u0026lt;\u0026lt; 13) 이 된다.\n그래서 1편에서 == GPIO_PIN_RESET으로 버튼 눌림을 판단한 게, 실제로는 IDR의 13번 비트가 0인지 확인하는 거다. Active Low 버튼이니까 눌렸을 때 핀이 GND로 당겨지고 → IDR 비트가 0 → GPIO_PIN_RESET 반환.\nHAL_GPIO_WritePin() 내부 1 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); 소스를 열면 ODR이 아닌 BSRR(Bit Set/Reset Register) 을 쓰고 있다.\n1 2 3 4 5 6 7 8 9 10 11 void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState) { if (PinState != GPIO_PIN_RESET) { GPIOx-\u0026gt;BSRR = GPIO_Pin; // Set: 하위 16비트에 핀 마스크 기록 } else { GPIOx-\u0026gt;BSRR = (uint32_t)GPIO_Pin \u0026lt;\u0026lt; 16U; // Reset: 상위 16비트에 핀 마스크 기록 } } Set할 때는 GPIO_Pin(= 1 \u0026lt;\u0026lt; 5 = 0x0020)을 그대로 쓰고, Reset할 때는 GPIO_Pin \u0026lt;\u0026lt; 16(= 0x00200000)을 써서 상위 16비트에 넣는다.\nBSRR은 32비트 레지스터인데 상위/하위 16비트의 역할이 다르다.\n1 2 BSRR [31:16] → BR (Bit Reset): 해당 비트가 1이면 핀을 LOW로 BSRR [15:0] → BS (Bit Set) : 해당 비트가 1이면 핀을 HIGH로 동작 BSRR에 쓰는 값 예시 (PA5) PA5 HIGH GPIO_Pin 0x00000020 PA5 LOW GPIO_Pin \u0026lt;\u0026lt; 16 0x00200000 ODR은 \u0026ldquo;핀 상태 전체를 이 값으로 덮어써라\u0026quot;이고, BSRR은 \u0026ldquo;이 핀만 Set해라\u0026rdquo; 또는 \u0026ldquo;이 핀만 Reset해라\u0026quot;다. 건드리지 않을 핀은 0을 쓰면 그냥 무시된다.\n왜 HAL은 ODR 대신 BSRR을 쓰나 ODR로 특정 핀만 바꾸려면 이렇게 된다.\n1 GPIOA-\u0026gt;ODR |= (1 \u0026lt;\u0026lt; 5); // Read → Modify → Write 겉으로는 한 줄이지만 실제로는 세 단계다.\n1 2 3 ① ODR 읽기 (Read) ② 5번 비트 수정 (Modify) ③ ODR에 쓰기 (Write) 문제는 ①과 ③ 사이에 인터럽트가 끼어드는 경우다. 메인 루프에서 PA5를 SET하려는 도중, 인터럽트 핸들러가 PA6을 RESET했다고 하면:\n1 2 3 [메인] ODR 읽음 → PA5, PA6 모두 HIGH 상태 [인터럽트] PA6 RESET → ODR 반영됨 [메인 재개] Modify한 값 씀 → PA6이 다시 HIGH로 덮어씌워짐 인터럽트에서 바꾼 PA6 상태가 날아간다. 이게 Race Condition이다.\nBSRR은 Write 한 번으로 끝난다. 하드웨어가 해당 비트만 건드리기 때문에 Read → Modify → Write 과정 자체가 없다. 인터럽트가 끼어들 틈이 없다는 뜻이다. 이게 Atomic한 이유고, HAL이 BSRR을 쓰는 이유다.\n방식 동작 단계 인터럽트 안전 ODR |= Read → Modify → Write ❌ 위험 BSRR = Write ✅ 안전 (Atomic) 추후에 저런 Critical Section을 어떻게 관리해야 하는지도 따로 글을 써보려고 한다.\n레지스터 직접 접근 버전 HAL 없이 똑같이 동작하는 코드:\n1 2 3 4 5 6 7 8 9 10 11 while (1) { if (!(GPIOC-\u0026gt;IDR \u0026amp; (1 \u0026lt;\u0026lt; 13))) // PC13 LOW = 버튼 눌림 { GPIOA-\u0026gt;BSRR = (1 \u0026lt;\u0026lt; 5); // PA5 Set (LED ON) } else { GPIOA-\u0026gt;BSRR = (1 \u0026lt;\u0026lt; 5) \u0026lt;\u0026lt; 16; // PA5 Reset (LED OFF) } } HAL은 결국 이걸 함수로 감싼 것뿐이다. 이걸 알고 쓰는 것과 모르고 쓰는 건, 뭔가 이상하게 동작할 때 어디서 파고들지가 달라진다.\n마무리 HAL로 간단한 동작을 구현하는 데 있어서는 충분하다. 함수 이름만 봐도 뭘 하는지 알 수 있고, 포팅도 쉽다.\n하지만 막상 뭔가 원하는 대로 안 동작할 때 얘기가 달라진다. 핀이 왜 안 켜지는지, 인터럽트에서 왜 상태가 깨지는지 — 이걸 파고들려면 결국 IDR이 뭘 읽고 있는지, BSRR에 어떤 값이 들어가고 있는지를 봐야 한다. HAL 함수 이름만 알고 있으면 그 아래서 멈추게 된다.\n레지스터 수준을 알고 쓰는 것과 모르고 쓰는 건, 코드가 잘 돌아갈 때는 차이가 없다. 차이는 안 돌아갈 때 난다.\n다음 글에서는 EXTI(External Interrupt) 를 통해 GPIO 입력을 인터럽트 방식으로 처리하는 방법을 정리할 예정이다. 폴링 방식과 어떻게 다른지, NVIC 설정은 어떻게 연결되는지까지 이어서 다룰 생각이다.\n","permalink":"https://bromine1997.github.io/posts/stm32/stm32-hal-gpio-internals/","summary":"HAL_GPIO_ReadPin()과 HAL_GPIO_WritePin()이 내부에서 어떻게 동작하는지 분석하고, ODR 대신 BSRR을 사용하는 이유와 레지스터 직접 제어 방법까지 정리한 글","title":"[STM32] HAL GPIO 내부 뜯어보기 및 레지스터 직접 제어"},{"content":"이 카테고리를 테스트하기 위한 첫 번째 글입니다.\n","permalink":"https://bromine1997.github.io/posts/notes/first-note/","summary":"블로그에 Notes 카테고리를 만들기 위한 첫 글","title":"첫 번째 Notes 글"},{"content":" 한 줄 요약: HBOT 챔버 내 유체 거동을 수학적으로 모델링하고, 축소 실험 챔버 + MCU 기반 압력 제어 시스템으로 검증한 연구\n📌 논문 정보 항목 내용 제목 Advances in Hyperbaric Oxygen Therapy: Medical Benefits and Technical Perspectives 저자 Antoanela Naaji, Monica Ciobanu, Marius Popescu 저널 / 학회 Annals of Biomedical Engineering 연도 2026 DOI / 링크 https://doi.org/10.1007/s10439-026-04027-7 분야 biomedical 🎯 문제 정의 기존 HBOT 시스템은 대부분 empirical calibration에 의존하고 있으며, gas flow 및 pressure dynamics에 대한 predictive model이 부족하다. 또한 압력·산소 제어의 자동화 수준이 낮고, control loop의 feedback delay 문제가 있어 치료 재현성이 떨어진다. 이 논문은 이러한 한계를 수학적·실험적 framework으로 해결하고자 한다.\n💡 Contribution Reynolds, Froude, Archimedes 등 dimensionless number를 활용한 HBOT 챔버 내 oxygenated air flow 수학 모델 수립 Ruark transformation을 도입해 dimensionless coordinate로 일반화 → 유사 환경에 결과 확장 가능 Geometric similarity condition (K=10)을 적용한 축소 실험 챔버 설계 및 제작 ATMEL AT89C52 + solenoid valve 3개 (EV1~EV3) + pressure/vacuum pump 기반 자동 압력 제어 시스템 구현 LabWindows GUI를 통한 real-time monitoring 및 CRC 기반 통신 프로토콜 설계 🔬 방법론 Mathematical Modeling\nContinuity equation, Navier-Stokes, thermal energy equation, Fick\u0026rsquo;s diffusion equation을 무차원화 Similarity condition: Re idem (self-modeling 영역), Froude idem, geometric similarity 실제 챔버 대비 1/10 축소 모델 → 모델 유량 D=8.5 l/min, D₁=0.45 l/min 도출 Experimental Setup\nPressure transducer (MPX 5100 AP) → ADC → AT89C52 MCU → solenoid valve / pump 제어 PC ↔ MCU 간 4-byte frame [T, X, Y, CRC] 양방향 통신 EV1 (가압), EV2 (배기 차단), EV3 (감압) 로직으로 압력 제어 Validation\n산소 농도, pressure behavior, 열적 효과 측정 후 모델 예측값과 비교 Isothermal·단순화 boundary condition 하에서 안정성과 재현성 평가 📊 실험 및 결과 챔버 압력 10% 증가 시 모델 내 pO₂ 약 8~9% 향상 → 조직 산소화 관련 생리학적 반응과 일치 제안된 압력 제어 시스템은 안정적인 pressurization/depressurization cycle을 재현, 상업용 HBOT 챔버와 원리적으로 유사함을 확인 Vacuum pump는 pressure pump 대비 유량 3배 → 감압 시간 ≈ 가압 시간의 절반 한계: oxygen concentration mapping, flow visualization, CFD 비교는 수행하지 않았으며 향후 과제로 남김. CO₂, 습도 등 exhaled gas 조성도 미반영.\n✅ 장점 / ⚠️ 한계 장점\nMathematical model → 축소 실험 챔버 → 실제 제어 시스템까지 일관된 multidisciplinary pipeline Control architecture가 platform-independent → STM32, FPGA 등 현대 embedded platform으로 이식 가능하다고 명시 무차원화를 통해 결과의 generalizability 확보 한계\nAT89C52 + single sensor feedback loop → 임상용이 아닌 연구/교육 수준 Isothermal 조건에서 실험 → 실제 HBOT 챔버의 thermodynamic complexity 미반영 개인별 호흡 패턴, 대사율 차이 등 생리적 변수 미포함 실험적 검증 범위가 pressure stabilization 및 flow behavior에 한정 💬 내 생각 / 인사이트 석사 논문에서 Tinker Board 2S + dual PID + AD5420 DAC 기반으로 HBOT 시스템을 구현했던 경험이 있어서, 이 논문의 방향성이 꽤 친숙하게 읽혔다.\n모델 접근법의 차이: 이 논문은 Navier-Stokes + dimensionless number 기반의 fluid dynamics model에 집중한 반면, 내 연구는 PID control loop의 real-time tuning과 DAC 기반 analog output에 초점을 뒀다. 두 접근이 서로 보완적이라고 생각함.\nControl architecture: EV1~EV3 solenoid + on/off 제어 방식은 단순하지만 실용적. 다만 proportional valve를 쓰지 않아서 pressure ramping의 세밀함에 한계가 있을 것 같다. 논문도 이 점을 인정하고 있음.\nPlatform 이식 가능성 언급: STM32, Arduino/ARM 등으로 포팅 가능하다고 명시한 게 인상적. 지금 STM32F411RE Nucleo로 공부 중인 입장에서, 이 제어 로직을 STM32로 직접 구현해보면 좋은 포트폴리오가 될 것 같다.\n아쉬운 점: CFD 미수행, oxygen concentration distribution 미측정이 가장 큰 빈자리. 후속 연구로 OpenFOAM 기반 CFD와 연결하면 재밌을 것 같다.\n🔗 관련 논문 Flegg et al. (2010) - Mathematical model of HBOT for chronic diabetic wounds, Bull. Math. Biol. Kovtanyuk et al. (2023) - Mathematical modeling of cerebral oxygen transport, Front. Appl. Math. Stat. Gracia et al. (2018) - Identification and control of a multiplace hyperbaric chamber, PLoS ONE ","permalink":"https://bromine1997.github.io/posts/paper-review/review-naaji2026-hbot/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e한 줄 요약\u003c/strong\u003e: HBOT 챔버 내 유체 거동을 수학적으로 모델링하고, 축소 실험 챔버 + MCU 기반 압력 제어 시스템으로 검증한 연구\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"-논문-정보\"\u003e📌 논문 정보\u003c/h2\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e항목\u003c/th\u003e\n          \u003cth\u003e내용\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e제목\u003c/td\u003e\n          \u003ctd\u003eAdvances in Hyperbaric Oxygen Therapy: Medical Benefits and Technical Perspectives\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e저자\u003c/td\u003e\n          \u003ctd\u003eAntoanela Naaji, Monica Ciobanu, Marius Popescu\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e저널 / 학회\u003c/td\u003e\n          \u003ctd\u003eAnnals of Biomedical Engineering\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e연도\u003c/td\u003e\n          \u003ctd\u003e2026\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eDOI / 링크\u003c/td\u003e\n          \u003ctd\u003e\u003ca href=\"https://doi.org/10.1007/s10439-026-04027-7\"\u003ehttps://doi.org/10.1007/s10439-026-04027-7\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e분야\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003ebiomedical\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003ch2 id=\"-문제-정의\"\u003e🎯 문제 정의\u003c/h2\u003e\n\u003cp\u003e기존 HBOT 시스템은 대부분 empirical calibration에 의존하고 있으며, gas flow 및 pressure dynamics에 대한 predictive model이 부족하다. 또한 압력·산소 제어의 자동화 수준이 낮고, control loop의 feedback delay 문제가 있어 치료 재현성이 떨어진다. 이 논문은 이러한 한계를 수학적·실험적 framework으로 해결하고자 한다.\u003c/p\u003e","title":"[Paper Review] Advances in Hyperbaric Oxygen Therapy: Medical Benefits and Technical Perspectives"},{"content":"GPIO(General Purpose Input/Output)는 MCU가 외부 세계와 신호를 주고받는 가장 기본적인 수단이다.\n이름 그대로 특정 기능에 고정되지 않고, 소프트웨어 설정에 따라 입력 또는 출력으로 사용할 수 있다.\n크게 두 가지 역할이 있다.\nInput: 외부 신호(버튼, 센서 등)를 MCU가 읽어들임 Output: MCU가 외부 장치(LED, 모터 드라이버 등)에 신호를 인가함 출력 모드: Push-Pull vs Open-Drain Push-Pull MCU가 핀을 High와 Low 모두 직접 drive하는 방식이다. 내부에 PMOS와 NMOS 트랜지스터가 한 쌍으로 연결되어 있어서, High 명령이 오면 PMOS가 켜져 핀을 VDD에 연결하고, Low 명령이 오면 NMOS가 켜져 핀을 GND에 연결한다.\n외부 저항 없이도 원하는 레벨을 정확하게 출력할 수 있어서, 일반적인 LED 제어나 디지털 출력에 기본으로 사용한다.\nOpen-Drain NMOS 하나만 사용하는 방식이다. Low는 직접 drive할 수 있지만, High는 스스로 만들지 못한다. 대신 핀을 그냥 놔버리는데(Hi-Z), 이때 외부에 연결된 Pull-up 저항이 핀을 High로 끌어올린다.\n왜 Open-Drain을 사용할까? 대표적인 예가 I2C다. 여러 장치가 같은 버스 선 하나를 공유할 때, 누군가 Low를 당기면 버스 전체가 Low가 된다.\n만약 Push-Pull이었다면 문제가 생긴다. 장치 A가 High를 output하는 동시에 장치 B가 Low를 output하면, VDD와 GND가 직접 연결되는 short가 발생하기 때문이다.\nOpen-Drain은 Pull-up 저항의 전압을 MCU VDD와 다르게 설정할 수도 있어서, 3.3V MCU와 5V 버스를 연결할 때도 별도 level shift 없이 쓸 수 있다.\n입력 모드: Pull-up / Pull-down / Floating 버튼을 GPIO 입력으로 읽는다고 해보자. 버튼이 눌리지 않은 상태에서 핀이 아무 데도 연결되지 않으면 Floating 상태가 된다.\n이 상태에서는 공중에 떠 있는 핀이 주변 noise를 그대로 받아들여서 IDR을 읽으면 0이 될지 1이 될지 예측할 수 없다.\n그래서 평상시에 핀을 확실한 레벨로 고정해두는 것이 Pull-up / Pull-down이다.\n설정 평상시 버튼 누를 때 Pull-up HIGH LOW Pull-down LOW HIGH STM32는 외부 저항 없이 내부 Pull-up / Pull-down을 PUPDR 레지스터로 설정할 수 있다. 이전 글에서 PC13 버튼을 읽을 때 Pull-up을 설정했던 것이 바로 이 이유다.\nSTM32 GPIO 내부 블록 다이어그램 STM32 레퍼런스 매뉴얼에는 GPIO 핀 하나의 구조를 보여주는 블록 다이어그램이 있다.\nGPIO를 이해할 때 가장 중요한 그림 중 하나라서, 한 번 제대로 읽어두면 이후에 훨씬 덜 헷갈린다.\n크게 세 부분으로 나눠서 볼 수 있다.\n1) 실제 핀 (I/O Pin) 핀 양쪽에는 Protection Diode가 붙어 있다.\n핀에 VDD보다 높거나 VSS보다 낮은 전압이 들어왔을 때 내부 회로를 보호하는 역할을 한다.\n또한 Pull-up / Pull-down 저항이 스위치 형태로 표현되어 있는데, 이 스위치를 소프트웨어로 켜고 끄는 것이 PUPDR 레지스터다.\n2) 입력 경로 (Input Driver) 핀 신호는 TTL Schmitt Trigger를 거쳐 Input Data Register(IDR) 로 들어간다.\nSchmitt Trigger가 있는 이유는 noise 때문이다.\n신호가 천천히 변하거나 noise가 섞여 있어도, hysteresis 특성을 통해 보다 명확하게 0과 1로 해석할 수 있다.\nAnalog 모드에서는 이 디지털 입력 경로 자체를 끄기도 한다.\n즉, 펌웨어에서 입력을 읽는다는 것은 결국 IDR 값을 읽는 것이다.\n3) 출력 경로 (Output Driver) 출력은 ODR(Output Data Register)에 값을 쓰거나, BSRR(Bit Set/Reset Register)를 통해 제어된다.\n이 값이 Output Control을 거쳐 P-MOS / N-MOS 조합으로 전달된다.\nPush-Pull 모드: P-MOS와 N-MOS가 번갈아 동작하며 High / Low를 직접 drive Open-Drain 모드: N-MOS만 동작하고 P-MOS는 꺼짐 핵심은 MUX다.\nMODER 레지스터를 통해 이 핀이 GPIO로 동작할지, UART / SPI 같은 Alternate Function으로 동작할지 선택한다.\nAF(Alternate Function) 모드로 설정하면 MCU 내부 peripheral(USART, SPI, TIM 등)이 해당 핀을 직접 제어한다.\n실습: User Button으로 LED 제어 (STM32F411 Nucleo) 보드 기본 핀 정보 코드를 작성하기 전에 보드 회로도에서 먼저 확인해야 할 정보는 다음과 같다.\n기능 핀 특이사항 User LED (LD2) PA5 Active High User Button (B1) PC13 Active Low, 보드 내부 Pull-up 연결 여기서 중요한 점은 버튼이 Active Low라는 것이다.\n즉, 버튼을 누르지 않았을 때는 PC13 = High, 버튼을 눌렀을 때는 PC13 = Low가 된다.\n따라서 코드에서 GPIO_PIN_RESET은 버튼이 눌린 상태를 의미한다.\n예시: STM32F411 Nucleo 보드에서 LD2와 B1의 위치를 표시한 사진\nCubeMX 핀 설정 PA5 → GPIO_Output PC13 → GPIO_Input PC13의 Pull-up은 보드 하드웨어에 이미 연결되어 있으므로, CubeMX에서는 No pull로 두어도 된다.\nPA5를 출력, PC13을 입력으로 설정한 CubeMX 화면\nHAL 코드 1 2 3 4 5 6 7 8 9 10 11 12 while (1) { if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET) { // 버튼 눌림 (Active Low) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); } } 동작은 단순하다.\n버튼 상태를 읽고 눌려 있으면 LED를 켜고 아니면 LED를 끈다 버튼을 누르면 LED가 켜지는 실행 결과\n실행 결과 버튼을 누르고 있는 동안 LD2(초록 LED)가 켜지고, 버튼에서 손을 떼면 꺼진다.\n이 정도의 간단한 실습에서는 debouncing을 따로 넣지 않았다.\n버튼을 누르고 있는 동안 계속 켜는 구조라서 chattering이 큰 문제가 되지 않는다.\n다만 버튼을 한 번 눌렀다 뗐을 때 상태가 토글되는 구조로 바꾸면 debouncing이 필요해진다.\n추가 학습: OSPEEDR를 왜 설정할까? 출력 속도 설정은 slew rate(신호가 바뀌는 속도) 와 관련이 있다.\n속도가 빠를수록 엣지가 가팔라지고, 그만큼 EMI 도 커질 수 있다.\n그래서 고속 SPI 클럭처럼 빠른 신호가 필요한 경우에는 High Speed가 필요할 수 있지만,\n단순한 LED 제어 같은 저속 신호에 Very High Speed를 쓰면 불필요한 noise만 증가시킬 수 있다.\n핵심 키워드 Floating Pull-up Pull-down Push-Pull Open-Drain Active Low Schmitt Trigger Slew Rate Debouncing 마무리 이번 글에서는 GPIO의 기본 개념과 입력/출력 구조, 그리고 STM32F411 Nucleo에서 버튼 입력과 LED 출력을 제어하는 간단한 실습까지 정리했다.\n다음 글에서는 HAL_GPIO_ReadPin() 과 HAL_GPIO_WritePin() 이 실제로 내부에서 어떻게 동작하는지, 그리고 왜 ODR 대신 BSRR를 사용하는지까지 이어서 정리할 예정이다.\n","permalink":"https://bromine1997.github.io/posts/stm32/stm32-gpio-basic-with-image-placeholders/","summary":"GPIO의 기본 개념과 Push-Pull, Open-Drain, Pull-up, Pull-down, 그리고 STM32F411 Nucleo에서 버튼 입력과 LED 출력을 실습한 글","title":"[STM32] GPIO 기본개념 및 입출력 실습"},{"content":"지난 글에서 Arduino IDE가 뭔지, 개발환경에 대해 간단하게 소개했다. 이번에는 실제로 내가 사용한 보드인 ESP32 Feather V2에 대해 이야기해보려 한다.\n내 프로젝트는 뭐였나 본격적으로 ESP32 이야기를 하기 전에, 내가 만든 게 뭔지 간단히 설명하겠다.\n코끼리 재활자전거는 재활 치료용 자전거로, 이번 프로젝트에서는 센서를 부착해 환자의 운동 데이터를 실시간으로 측정하고 기록하는 장치를 만들었다.\n측정해야 했던 것들:\n4개의 로드셀(힘 센서): 양팔과 양다리에서 발휘하는 힘 측정 회전 엔코더: 자전거 페달의 각도 측정 실시간 데이터 전송: Wi-Fi로 웹 브라우저에 데이터 표시 데이터 저장: 최대 36분간의 측정 데이터를 메모리에 저장 대학원에서 처음으로 진행한 프로젝트였는데 쉽지 않았다.\nESP32가 뭔가 ESP32는 중국의 Espressif Systems에서 만든 MCU다.\n가장 큰 장점은 Wi-Fi와 블루투스가 기본 탑재되어 있다는 것이다. 일반적으로 다른 MCU에서 무선 통신을 하려면 별도의 Wi-Fi 모듈을 붙여야 하는데, ESP32는 처음부터 내장되어 있어 IoT 프로젝트에 적합하다.\n주요 스펙:\n항목 ESP32 Arduino Uno (비교) 코어 듀얼 코어 싱글 코어 클럭 최대 240MHz 16MHz Wi-Fi / BT 기본 탑재 없음 가격 저렴 저렴 처음 ESP32를 접했을 때 이 가격에 Wi-Fi와 블루투스까지 된다는 게 믿기지 않았다.\nESP32 Feather V2를 선택한 이유 Adafruit ESP32 Feather V2\n나는 Adafruit에서 나온 ESP32 Feather V2를 선택했다. 솔직히 가격은 비싼 편이었다. 일반 ESP32 보드가 5,000원 정도면 구할 수 있는데, 이건 2만 원이 넘었다.\n그런데도 선택한 이유가 있다.\n1. 넉넉한 메모리 일반 ESP32 보드는 보통 4MB Flash인데, Feather V2는 8MB Flash에 2MB PSRAM까지 있다.\n\u0026ldquo;메모리가 왜 중요해?\u0026rdquo; 싶었는데, 웹 서버를 돌리거나 여러 라이브러리를 쓰다 보면 메모리가 금방 부족해진다. 넉넉한 게 나중에 확장할 때 정말 도움이 됐다.\n2. USB-C 포트 Micro-USB나 5핀을 쓰는 보드도 많은데, USB-C는 은근히 신경 쓰이는 부분이었다. 어디서든 노트북으로 바로 연결해서 개발할 수 있었다.\n3. Wi-Fi 단순히 데이터를 수집하는 것만이 목적이 아니라, 웹에서 데이터를 실시간으로 확인하고 저장하는 것까지 목표로 뒀기 때문에 반드시 필요한 기능이었다.\n4. SPIFFS 파일 시스템으로 웹페이지 저장 웹 서버를 돌리려면 HTML, CSS, JavaScript 파일을 어딘가에 저장해야 한다. Feather V2의 8MB Flash 덕분에 SPIFFS 파일 시스템을 사용해 웹페이지를 보드 안에 저장할 수 있었다. 코드 메모리와 별개로 웹 파일을 저장할 공간이 따로 있어서 편했다.\n단점도 있었다 1. 가격 일반 ESP32 대비 4배 정도 비싸다. 연구 개발 단계에서는 충분히 투자할 가치가 있다고 보지만, 양산 단계에서는 더 저렴한 MCU로 대체하는 게 맞을 것 같다.\n2. 디버깅이 어렵다 Arduino IDE의 특성이기도 하고, 멀티코어 환경의 특성이기도 하다. 인터럽트를 쓰면서 웹 서버를 같이 돌리니 버그를 찾기가 상당히 어려웠다.\n3. 핀 배치가 아쉽다 Feather 폼팩터 특성상 모든 GPIO가 다 나와있지 않다. 이번 프로젝트에서는 충분했지만, 더 많은 센서를 붙여야 한다면 IO Expander를 따로 사용해야 할 것 같다.\n마무리 가격은 비쌌지만, 프로젝트 완성도와 안정성을 생각하면 후회 없는 선택이었다. 특히 연구용 장비였기 때문에 안정성과 신뢰성이 중요했고, 그 부분에서 Feather V2는 충분히 역할을 해줬다.\n다음 글에서는 Arduino IDE 설정부터 PSRAM 활성화, 웹 서버 구동까지 삽질했던 부분들을 위주로 정리해보겠다.\n","permalink":"https://bromine1997.github.io/posts/esp32/esp32-intro-feather-v2/","summary":"코끼리 재활자전거 IoT 프로젝트의 핵심이었던 ESP32, 그 중에서도 ESP32 Feather V2를 선택한 이유와 실제로 써보면서 느낀 장단점을 정리했다.","title":"[ESP32] ESP32가 뭐길래?"},{"content":" 한 줄 요약: 1인용 HBOT 환자에서 MEB 발생의 독립적 위험 인자는 altered mental status(OR 2.50)와 응급 치료군 분류(OR 6.75)였다.\n📌 논문 정보 항목 내용 제목 Risk Factors Associated with Middle Ear Barotrauma in Patients Undergoing Monoplace Hyperbaric Oxygen Therapy 저자 Yoon Sung Lee, Sang Won Ko, Hyoung Youn Lee, Kyung Hoon Sun, Tag Heo, Sung Min Lee 저널 / 학회 Yonsei Medical Journal 연도 2025 DOI / 링크 https://doi.org/10.3349/ymj.2024.0068 분야 biomedical clinical 🎯 문제 정의 MEB(Middle Ear Barotrauma)는 HBOT에서 가장 흔한 합병증 중 하나로 알려져 있다. 그러나 기존 연구 대부분은 multiplace chamber 환자를 대상으로 하고 있어, monoplace chamber에서의 MEB 위험 인자에 대한 데이터가 부족하다.\nMonoplace chamber는 환자가 supine position으로 치료를 받는다는 점에서 multiplace와 다른 역학을 가진다. Supine position은 중심정맥압을 높여 정맥 울혈을 유발하고, 이는 중이의 pressure equalization을 더 어렵게 만든다. 이 논문은 monoplace HBOT 환자에서 MEB 발생률과 독립적 위험 인자를 규명하고, 더 안전한 운영 프로토콜 수립에 기여하는 것을 목적으로 한다.\n💡 Contribution 단일기관 296명 규모의 monoplace HBOT 환자 데이터를 기반으로 MEB 발생률 및 위험 인자 분석 Video otoscopy + 수정 O\u0026rsquo;Neill grading system을 적용한 객관적 MEB 평가 Multivariable logistic regression으로 독립적 위험 인자 2개 규명: altered mental status, 응급 치료군(NHI A군) Restricted cubic spline graph를 통해 세션 수와 MEB 발생 확률의 비선형 관계 시각화 국내 건강보험 적응증 분류 체계(A/B/C군)를 위험 인자 분석에 적용한 점이 국내 임상 환경에 실질적 🔬 방법론 연구 설계\n기간: 2021년 5월 ~ 2023년 12월 설계: 단일기관 후향적 코호트 연구 챔버: BARA-MED Monoplace Hyperbaric Chamber (ETC Biomedical Systems) 치료 프로토콜: 총 90분 (compression 15분 + 치료 + decompression 15분), 최대 2.0 ATA 또는 2.8 ATA MEB 평가\nVideo otoscope(INSIGHT-I, MEDIANA)로 치료 전후 TM(Tympanic Membrane) 상태를 평가하고 수정 O\u0026rsquo;Neill grading system 적용:\nGrade 기준 0 이경 소견 없음 (증상만 존재) 1 TM 충혈, 장액성 삼출액, TM 뒤 공기 포착 중 하나 이상 2 명백한 출혈 또는 TM 천공 적응증 분류 (국민건강보험 기준)\n그룹 내용 A군 (응급) CO 중독, 감압병, 공기색전증, 가스괴저, 시안화물 중독, 중심망막동맥폐색, 중증 빈혈 B군 (만성) 버거병, 피판/이식편, 지연 방사선 손상, 당뇨발, 골수염, 뇌농양 등 C군 (기타) 돌발성 난청 통계 방법\n연속 변수: t-test 또는 Mann-Whitney U test 범주형 변수: chi-square test 또는 Fisher\u0026rsquo;s exact test Univariable 분석에서 p\u0026lt;0.1인 변수를 multivariable logistic regression에 포함 세션 수에 따른 MEB 발생 확률: restricted cubic spline graph로 시각화 분석 도구: Stata/SE 16.1 📊 실험 및 결과 환자 특성\n총 296명 (남성 68.6%, 평균 연령 49.0±17.2세) 주요 적응증: CO 중독 54.1%, 돌발성 난청 34.5%, CO 지연 신경정신 후유증 5.1% 응급 치료군(A군): 181명 (61.2%) 입원 시 altered mental status: 52명 (19.9%) MEB 발생 현황\n전체 MEB 발생률: 56.1% (166명) Video otoscopy 이상 소견: 58.8% (174명) — Grade 1: 56.4%, Grade 2: 2.4% 세션 수에 따른 MEB 발생 확률\n시점 MEB 발생 확률 1회차 약 60% 5회차 이후 20% 미만 18회차 이후 0% 세션이 늘어날수록 MEB 발생 확률이 지속 감소하는 경향이 관찰되었다. 반복 치료를 통해 환자가 pressure equalization 기술에 적응한 결과로 해석할 수 있다. 다만 후반 세션으로 갈수록 대상 환자 수가 급격히 줄어드는 만큼(\u0026gt;10회: 17명), 적응 효과만으로 단순하게 해석하기보다는 신중하게 볼 필요가 있다.\nUnivariable 분석 주요 결과\n변수 OR (95% CI) p값 응급 치료군 A 2.51 (1.55–4.05) \u0026lt;0.001 Altered mental status 3.18 (1.51–6.67) 0.002 증상 발생 후 7일 이내 치료 높은 MEB 발생 0.014 가압 속도 4 FSW/min (vs 2.2) 1.95 (1.21–3.13) 0.006 Multivariable logistic regression (독립적 위험 인자)\n독립적 위험 인자 OR 95% CI p값 Altered mental status 2.50 1.13–5.51 0.023 응급 치료군 A 6.75 1.33–34.20 0.021 참고: Compression 속도(4 FSW/min)는 univariable에서 유의했지만(p=0.006), multivariable에서는 유의성을 잃었다(p=0.105). 빠른 가압이 MEB에 영향을 미치는 경향은 있었지만, 응급 여부나 altered mental status 같은 다른 변수와 교란(confounding)되어 있을 가능성이 있다. 이 연구만으로 느린 가압이 MEB를 직접 줄인다고 단정하기는 어렵다.\n✅ 장점 / ⚠️ 한계 장점\nVideo otoscopy 기반의 객관적 MEB 등급화로 주관적 평가 오류를 줄임 국내 건강보험 적응증 분류를 위험 인자로 활용한 실용적 접근 Restricted cubic spline으로 세션 수와 MEB의 비선형 관계를 시각적으로 제시 한계\n단일기관 후향적 연구로 일반화에 제한이 있음 Monoplace chamber에만 해당하여 multiplace chamber에 직접 적용하기 어려움 MEB의 장기 예후 미평가 사전 이과 질환 등 교란 변수를 완전히 통제하지 못함 💬 내 생각 / 인사이트 석사 논문 주제가 HBOT 챔버 제어 시스템이었기 때문에, 이 논문의 임상 데이터가 특히 와닿았다.\nCompression 속도와 MEB: 펌웨어에서 압력 센서 피드백으로 compression 속도를 제어하는 로직을 짰는데, 그 파라미터가 임상적으로 이렇게 직결된다는 걸 데이터로 확인하니 흥미롭다. Multivariable에서 유의성을 잃었다는 점이 오히려 더 현실적인 메시지인 것 같다. 속도보다 환자 상태가 더 중요한 변수라는 뜻이기도 하니까.\nAltered mental status 환자 대응: 능동적으로 pressure equalization을 수행할 수 없는 환자라면, 시스템 레벨에서 보완이 필요하다는 생각이 든다. 단순히 \u0026ldquo;몇 ATA, 몇 분\u0026quot;으로 고정된 프로토콜이 아니라, 환자 의식 상태에 따라 compression 속도를 자동으로 조절하거나 운영자에게 알림을 주는 방식이 임상적으로 의미 있을 것 같다.\n세션 반복 효과: 18회차 이후 MEB 0%라는 결과가 인상적이지만, 그 시점에서의 환자 수(전체의 5.7%)를 감안하면 그대로 받아들이기보다는 경향성 정도로 이해하는 게 맞을 것 같다.\n🔗 관련 논문 Lima MA, et al. Update on middle ear barotrauma after hyperbaric oxygen therapy. Int Arch Otorhinolaryngol 2014 Karahatay S, et al. Middle ear barotrauma with hyperbaric oxygen therapy: incidence and predictive value. Ear Nose Throat J 2008 Vahidova D, et al. Does the slow compression technique of HBOT decrease the incidence of MEB? J Laryngol Otol 2006 ","permalink":"https://bromine1997.github.io/posts/paper-review/review-lee2025-hbot-meb/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e한 줄 요약\u003c/strong\u003e: 1인용 HBOT 환자에서 MEB 발생의 독립적 위험 인자는 altered mental status(OR 2.50)와 응급 치료군 분류(OR 6.75)였다.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"-논문-정보\"\u003e📌 논문 정보\u003c/h2\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e항목\u003c/th\u003e\n          \u003cth\u003e내용\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e제목\u003c/td\u003e\n          \u003ctd\u003eRisk Factors Associated with Middle Ear Barotrauma in Patients Undergoing Monoplace Hyperbaric Oxygen Therapy\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e저자\u003c/td\u003e\n          \u003ctd\u003eYoon Sung Lee, Sang Won Ko, Hyoung Youn Lee, Kyung Hoon Sun, Tag Heo, Sung Min Lee\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e저널 / 학회\u003c/td\u003e\n          \u003ctd\u003eYonsei Medical Journal\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e연도\u003c/td\u003e\n          \u003ctd\u003e2025\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003eDOI / 링크\u003c/td\u003e\n          \u003ctd\u003e\u003ca href=\"https://doi.org/10.3349/ymj.2024.0068\"\u003ehttps://doi.org/10.3349/ymj.2024.0068\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e분야\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003ebiomedical\u003c/code\u003e \u003ccode\u003eclinical\u003c/code\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003ch2 id=\"-문제-정의\"\u003e🎯 문제 정의\u003c/h2\u003e\n\u003cp\u003eMEB(Middle Ear Barotrauma)는 HBOT에서 가장 흔한 합병증 중 하나로 알려져 있다. 그러나 기존 연구 대부분은 multiplace chamber 환자를 대상으로 하고 있어, monoplace chamber에서의 MEB 위험 인자에 대한 데이터가 부족하다.\u003c/p\u003e","title":"[Paper Review] Risk Factors Associated with Middle Ear Barotrauma in Patients Undergoing Monoplace Hyperbaric Oxygen Therapy"},{"content":"","permalink":"https://bromine1997.github.io/search/","summary":"","title":""},{"content":" 학부 연구부터 석사 과정까지 진행한 프로젝트들을 정리했습니다. 주요 프로젝트 🫁 IoT 고압산소챔버 시스템 2023~2024 · 석사 학위논문 프로젝트 Tinker Board 2S 기반 고압산소챔버 원격 제어 및 모니터링 플랫폼. Android 앱이 SBC 위에서 직접 실행되며 GPIO·SPI·I2C로 하드웨어를 제어하고, PID 압력 제어 및 실시간 센서 스트리밍을 수행. NestJS 서버 + Vue3 대시보드와 WebSocket으로 연동. Android Java MVVM Tinker Board 2S MRAA PID NestJS Vue 3 MongoDB WebSocket GitHub (App) GitHub (Server) 🚲 재활 자전거 실시간 측정 시스템 2022 ~ 2023 · 학부연구 / 석사 ESP32 Feather V2 기반 4채널 로드셀 동기화 수집 시스템. ADS1232를 4개 병렬 운용해 팔·다리 페달의 힘을 동시에 측정하고, PSRAM에 버퍼링 후 WebSocket으로 브라우저 기반 실시간 모니터링 및 CSV 다운로드 제공. ESP32 Arduino ADS1232 ×4 AS5600 WebSocket SPIFFS PSRAM JavaScript GitHub 사이드 프로젝트 ⚙️ ATmega4809 Peripheral Driver 2021 · 마이크로컴퓨터시스템 수업 커스텀 PCB 직접 납땜 후 ATmega4809 펌웨어 구현. GPIO, UART, SPI, I2C, ADC 등 주변장치 드라이버를 외부 라이브러리 없이 레지스터 수준에서 직접 작성. ATmega4809 C Bare-metal Microchip Studio AVR GitHub 📌 Coming Soon — 정리 중입니다. 📌 Coming Soon — 정리 중입니다. ","permalink":"https://bromine1997.github.io/projects/","summary":"\u003cstyle\u003e\n.projects-section-title {\n  font-size: 1.2rem;\n  font-weight: 700;\n  color: var(--secondary);\n  text-transform: uppercase;\n  letter-spacing: 0.08em;\n  margin: 2.5rem 0 1.2rem;\n  padding-bottom: 0.4rem;\n  border-bottom: 1px solid var(--border);\n}\n\n.project-grid {\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));\n  gap: 1.5rem;\n  margin-bottom: 1rem;\n}\n\n.project-card {\n  background: var(--entry);\n  border: 1px solid var(--border);\n  border-radius: var(--radius);\n  overflow: hidden;\n  display: flex;\n  flex-direction: column;\n  transition: transform 0.15s ease, box-shadow 0.15s ease;\n}\n\n.project-card:hover {\n  transform: translateY(-3px);\n  box-shadow: 0 6px 20px rgba(0,0,0,0.12);\n}\n\n.project-card.placeholder {\n  opacity: 0.45;\n  cursor: default;\n  pointer-events: none;\n}\n\n.project-thumb {\n  width: 100%;\n  aspect-ratio: 16 / 9;\n  object-fit: cover;\n  background: var(--tertiary);\n  display: block;\n}\n\n.project-thumb-placeholder {\n  width: 100%;\n  aspect-ratio: 16 / 9;\n  background: var(--tertiary);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  font-size: 2rem;\n  color: var(--secondary);\n}\n\n.project-body {\n  padding: 1.1rem 1.2rem 1.2rem;\n  display: flex;\n  flex-direction: column;\n  flex: 1;\n}\n\n.project-name {\n  font-size: 1.05rem;\n  font-weight: 700;\n  color: var(--primary);\n  margin: 0 0 0.3rem;\n}\n\n.project-period {\n  font-size: 0.78rem;\n  color: var(--secondary);\n  margin: 0 0 0.7rem;\n}\n\n.project-desc {\n  font-size: 0.875rem;\n  color: var(--content);\n  line-height: 1.6;\n  margin: 0 0 1rem;\n  flex: 1;\n}\n\n.project-tags {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 0.35rem;\n  margin-bottom: 1rem;\n}\n\n.project-tag {\n  font-size: 0.72rem;\n  font-weight: 600;\n  padding: 0.2rem 0.55rem;\n  border-radius: 4px;\n  background: var(--code-bg);\n  color: var(--secondary);\n  white-space: nowrap;\n}\n\n.project-links {\n  display: flex;\n  gap: 0.6rem;\n  flex-wrap: wrap;\n}\n\n.project-link {\n  font-size: 0.8rem;\n  font-weight: 600;\n  padding: 0.35rem 0.8rem;\n  border-radius: 5px;\n  text-decoration: none !important;\n  border: 1px solid var(--border);\n  color: var(--primary);\n  background: transparent;\n  transition: background 0.12s, color 0.12s;\n}\n\n.project-link:hover {\n  background: var(--primary);\n  color: var(--theme);\n}\n\n.project-link.primary-link {\n  background: var(--primary);\n  color: var(--theme);\n  border-color: var(--primary);\n}\n\n.project-link.primary-link:hover {\n  opacity: 0.8;\n}\n\u003c/style\u003e\n\u003cp style=\"color: var(--secondary); font-size: 0.9rem; margin-top: -0.5rem; margin-bottom: 2rem;\"\u003e\n학부 연구부터 석사 과정까지 진행한 프로젝트들을 정리했습니다.\n\u003c/p\u003e","title":"Projects"}]