전자회로 실무 / 펌웨어

시리얼 수신에서는 배열(array) 용량과 실제 받은 길이를 따로 관리합니다

시리얼 데이터를 받은 뒤 엉뚱한 변수가 바뀐다면 버퍼(buffer)를 넘겨 썼을 수 있습니다. 받을 데이터 길이가 저장 공간을 넘지 않는지 확인하고, 실제로 받은 길이만큼만 처리해야 합니다.

먼저 알아둘 용어

바이트(byte)·배열(array) 용량 — 바이트는 여기서 8비트의 저장 단위입니다. 배열 용량은 쓸 수 있는 바이트 수이며, 외부에서 받은 길이 요청이 그 공간을 늘려 주지는 않습니다.

NUL(Null Character)·바이너리(binary) — NUL은 값이 0인 바이트로, C 문자열에서는 끝을 표시합니다. 바이너리 데이터에서는 0도 내용의 일부일 수 있어 문자열 함수로 처리하면 잘릴 수 있습니다.

타임아웃(timeout)·메시지(message) 경계 — 타임아웃은 기다릴 수 있는 최대 시간입니다. 메시지 경계는 하나의 데이터 묶음이 끝나는 위치이며 고정 길이·길이 필드·구분자 등으로 정합니다. 시간 초과가 정상 메시지의 끝이라는 뜻은 아닙니다.

문자를 차례로 저장한 뒤 문자열 끝에 NUL을 넣는 예시입니다. 바이너리 데이터에는 별도 길이가 필요합니다. 재생 속도와 움직임의 크기는 설명을 위한 것입니다.
정지 그림 확대
문자를 차례로 저장한 뒤 문자열 끝에 NUL을 넣는 예시입니다. 바이너리 데이터에는 별도 길이가 필요합니다. 재생 속도와 움직임의 크기는 설명을 위한 것입니다. · 그림을 클릭하면 확대됩니다.

요청한 길이가 저장 공간을 넘지 않는지 확인합니다

char buffer[10]에 Serial.readBytes(buffer,20)을 호출하면 함수가 최대 20바이트를 쓰려는 동안 10바이트 배열 밖의 메모리를 손상시킬 수 있습니다. 목적지 용량을 늘리거나 요청 길이를 줄여, 요청 길이가 용량을 넘지 않게 해야 합니다. 배열을 늘리는 것만으로 메시지가 항상 완전히 도착하는 것은 아닙니다.

함수의 약속을 확인합니다

Arduino AVR 계열의 readBytes는 요청 길이만큼 읽거나 타임아웃이 발생하면 멈추고 실제로 받은 바이트 수를 반환합니다. 문자열 끝의 NUL은 자동으로 붙이지 않습니다. 따라서 용량, 반환 길이, 종단 문자 처리를 각각 확인해야 합니다. 다른 코어(core)·라이브러리(library)에서는 함수 이름이 같아도 구현과 약속을 확인합니다.

고정 길이 20바이트의 바이너리라면 최소 20바이트의 목적지가 필요합니다. 수신 뒤에는 반환 길이가 20인지 확인하고, 짧게 들어왔다면 미완성 데이터로 처리합니다. 바이너리를 문자열 출력 함수에 넘기지 않습니다. 데이터 안의 0바이트는 내용의 일부일 수 있습니다.

최대 20바이트의 본문을 문자열로 다룬다면 끝 표시용 공간을 하나 더 확보합니다. 다음 예시는 최대 20바이트를 읽고 실제 받은 길이 바로 뒤에 종단 문자를 놓습니다.

char text[21];
size_t n = Serial.readBytes(text, sizeof(text) - 1);
text[n] = '\0';

이 코드는 메모리 끝을 넘기지 않고 종단 문자를 넣을 공간을 남기지만, 메시지 경계를 자동으로 이해하지는 않습니다. 반환된 길이가 요청한 길이보다 짧을 수 있고, 데이터 안에 NUL이 있다면 일반 문자열 처리에서 그 뒤의 내용이 보이지 않을 수 있습니다. “안전한 배열”과 “올바른 프로토콜 메시지”를 구분합니다.

입력을 사용하기 전에 길이와 형식을 검증합니다

수신 형식을 고정 길이, 길이 필드, 구분자 중 무엇으로 정할지 먼저 확인합니다. 외부 데이터가 알려 준 길이를 그대로 readBytes에 넘기지 않습니다. 수신 길이가 버퍼의 남은 공간과 프로토콜에서 허용하는 최대 길이를 모두 넘지 않는지 검사합니다. 거부한 데이터가 다음 메시지의 시작으로 잘못 처리되지 않도록 재동기화 규칙도 정합니다.

타임아웃은 손상된 연결에서 무한히 기다리지 않게 하지만 제어 작업을 늦출 수 있습니다. 바깥 루프(loop)를 경과 시간 검사로 구성해도 수신 함수 안에서 길게 기다리면 반응은 늦습니다. 필요한 응답 시간에 맞춰 짧게 읽어 누적하고, 현재까지 받은 길이와 메시지 완성 상태를 별도로 관리할지 결정합니다.

시험 입력확인할 결과
0바이트·일부만 도착실제 길이를 사용하고 이전 데이터가 섞이지 않음
정확한 최대 길이배열 끝과 종단 공간을 넘지 않음
최대보다 긴 입력거부·버림·다음 경계 처리 방식이 명확함
잘못된 길이 필드·내장 NUL형식 검증에서 구별됨
입력이 중간에 끊김타임아웃과 루프 지연이 요구 범위 안임

수신 값이 그럴듯하다는 이유로 성공 처리하지 않습니다. 실제 반환 길이와 메시지 형식에 대한 검증을 통과한 데이터만 후단에서 사용합니다. 최대 길이뿐 아니라 0바이트, 부분 수신, 중간 끊김, 과도한 길이와 다음 메시지 복구까지 시험해야 배열과 프로토콜을 함께 검증할 수 있습니다.

송신 화면과 대상의 수신 결과를 구별합니다

무선 직렬 터미널에서도 Hello World처럼 구별하기 쉬운 시험 문자열을 입력하고 보내기 버튼을 누르는 단계와, 대상이 실제 받은 바이트를 확인하는 단계는 다릅니다. 송신 화면의 표시나 로컬 에코만으로 대상의 수신 성공을 판단하지 않습니다. 대상 수신부에서 길이와 내용을 확인하고, 그 뒤 명령 해석과 출력 동작을 별도로 검증합니다. 앱의 줄바꿈 추가 여부와 문자 인코딩도 송수신 양쪽에서 맞춰야 합니다. 장치 종류와 프로토콜을 모르는 상태에서 연결 설정이나 공급 전압을 추측하지 않습니다.

포트를 연결해도 메시지의 의미가 자동으로 변환되지는 않습니다

UART에서 받은 바이트를 다른 포트로 전달하는 브리지는 배열의 경계뿐 아니라 장치 역할, 메시지 경계, 오류 처리를 정해야 합니다. 여러 색의 포트 로그나 MCU가 만든 시험 데이터는 관찰 도구입니다. 외부 장치가 보낸 입력과 반대편 장치가 실제 받은 출력을 대조하지 않았다면 모든 포트 조합의 변환 성공으로 보지 않습니다.

  • SPI에서는 controller가 clock과 chip-select를 제어합니다. 대상 장치의 명령 형식, 클록 모드와 응답 길이를 맞춰야 하며 UART 바이트를 복사하는 것만으로 센서 명령이 되지는 않습니다.
  • I²C는 주소와 읽기/쓰기 방향, START·STOP, ACK/NACK을 포함한 transaction으로 다룹니다. register 읽기에서 repeated START가 필요한지도 해당 장치 문서로 확인합니다.
  • Classical CAN의 데이터 필드는 최대 8바이트입니다. 긴 메시지를 나눌 때는 조각 번호·길이·메시지 식별자·재조립 제한 시간을 정의하고 중복·누락·순서 변경을 시험합니다. CAN FD의 최대 64바이트와 혼동하지 않습니다.
  • RS-485는 드라이버와 수신기의 전기 특성을 정하는 인터페이스입니다. 메시지 형식과 반이중 방향 전환 정책은 따로 정해야 하며, 배선·종단 조건도 따로 확인합니다.
  • W5500의 TCP/IP socket을 사용해도 애플리케이션 메시지 규칙은 남습니다. TCP는 순서 있는 바이트 스트림이므로 송신 함수 한 번과 수신 함수 한 번을 하나의 메시지로 가정하지 않습니다. 누적 버퍼에서 길이·구분자 등을 해석하고 완성된 메시지만 후단에 넘깁니다.

이중 버퍼는 지속적인 입력·출력 속도 차이를 없애지 못합니다

이중 버퍼는 한쪽을 처리하는 동안 다른 쪽을 채울 수 있게 하지만, 소비보다 생산이 계속 빠르면 결국 공간이 부족해집니다. 실제 payload 유입률을 Rin, 유출률을 Rout, 남은 큐 공간을 B라고 하면, 두 속도가 일정하고 Rin > Rout인 단순 모델에서 포화 시간은 B / (R_in - R_out)입니다. 예를 들어 빈 공간 4,096바이트, 유입 12,000바이트/초, 유출 8,000바이트/초라면 1.024초입니다. 이는 계산 예시이지 특정 보드의 실측 성능이 아닙니다. 헤더·재전송·서비스 중단과 burst가 있으면 여유는 더 작아질 수 있습니다.

포트별 큐 상한과 high-water mark를 기록하고, 지원되는 흐름 제어로 생산자를 늦출지, 완성 메시지를 거부할지, 오래된 데이터를 버릴지 결정합니다. 임의 바이트 삭제는 다음 메시지까지 손상시킬 수 있습니다. 오류·재시도·재동기화 정책을 명시하고 한 포트의 정체가 다른 포트의 처리까지 막는지 확인합니다. DMA 완료와 UART IDLE 검출은 전송·타이밍 사건이며 프로토콜이 그렇게 정의하지 않았다면 애플리케이션 메시지 완료와 동일하지 않습니다.

시험은 한 포트 쌍부터 시작합니다. 입력과 출력의 원시 바이트, 길이와 시퀀스, 설정 속도, 손실·중복 수, 지연 및 overflow·timeout을 함께 기록합니다. 정상 속도 외에 느린 출력, 최대 길이, 부분 수신, 연결 끊김과 재연결을 넣습니다. 저장장치의 write·erase 명령은 명령 전송 로그만 보지 말고 완료 상태와 readback을 별도로 대조합니다.

활성 구간의 전류와 전체 소비전력은 다른 측정입니다

ADC 파형이 표시됐다고 해서 외부 하드웨어 트리거까지 검증된 것은 아닙니다. Software trigger와 hardware trigger를 구별하고 실제 설정, 트리거 배선, 표본 주기와 포착 구간을 남깁니다. 활성 구간의 RMS 전류만으로 활성·대기 시간을 합친 평균 소비전력이나 배터리 수명을 구할 수도 없습니다.

일반적으로 순간 전력은 p(t) = v(t) × i(t), 에너지는 그 전력을 시간에 대해 적분한 값입니다. 반복 작업의 평균 전력은 활성 시간과 대기 시간을 포함한 전체 측정 구간의 에너지를 그 구간 길이로 나눕니다. 일정한 공급 전압이라면 이 구간의 평균 전류를 사용하며, 전압·전류가 변하면 시간 정렬된 두 파형의 곱을 평균합니다. Analog Devices의 RMS와 평균 전력 해설도 평균 전력과 전력 파형의 RMS를 구별합니다. 여기서는 특정 프로젝트의 영상·회로·펌웨어를 실행 검증한 것이 아니라, 공개 기술 문서에 따라 브리지와 계측의 검증 기준을 정리했습니다.

English summary. Define roles and message framing for each bridge pair. Finite buffering cannot absorb a sustained rate mismatch. Verify received payloads independently of GUI logs, and include active and idle periods when measuring average device power.

길이가 맞아도 부호를 잃으면 센서 값이 달라집니다

수신 바이트 수를 검증한 다음에는 원시 표현을 해석해야 합니다. 바이트 순서, 부호 비트, 유효 비트 수, 배율과 오류용 예약 코드는 센서 문서에 따릅니다. 모든 센서가 16비트 2의 보수 온도를 전송한다고 가정하지 않습니다. 아래 계산은 이미 정수 온도 −25로 해석된 값을 잘못 변환한 예시이며, 특정 센서의 원시 데이터를 복호화하는 절차가 아닙니다.

C에서 int16_t temperature = -25;를 uint16_t로 변환하면 결과는 65,511입니다. C11 초안 N1570 §6.3.1.3의 unsigned 변환 규칙에 따라 −25 + 65,536 = 65,511이 됩니다. 65,535는 이 변환에서 −1에 대응하며 −25의 결과가 아닙니다. 변환한 값을 100보다 큰지 검사하면 음수 온도인데도 상한 초과처럼 판단할 수 있습니다. 명시적 cast는 범위 검사가 아닙니다.

음수가 필요한 물리량은 그 범위를 보존하는 형식으로 유지하고, 상·하한 검사 후 필요한 형식으로 변환합니다. 부호 있는 값과 없는 값을 섞어 비교할 때는 정수 승격과 공통 형식 변환도 확인합니다. −25, −1, 0, 경계값과 예약 오류 코드를 시험하고, 송수신의 형식·단위를 함께 기록합니다. 시간 카운터의 modulo 차이를 의도적으로 쓰는 경우와 센서 값의 부호를 실수로 잃는 경우는 다른 문제입니다.

UART 수신 DMA는 복사만 맡을 뿐 버퍼 검증은 하지 않습니다

CPU가 UART 수신 데이터 레지스터를 읽어 RAM에 쓰는 대신, DMA가 주변장치의 수신 요청에 따라 데이터를 옮길 수 있습니다. 바이트 폭 전송의 설명 예시로 소스는 UART 수신 레지스터, 목적지는 RAM 버퍼, 개수는 4바이트, 요청은 UART 수신으로 정합니다. 3A 7F 12 A6가 순서대로 저장되면 잔여 개수는 4→3→2→1→0이 됩니다. 이는 논리 흐름 예시이지 실제 버스 트레이스나 무손실 수신 측정이 아닙니다.

STM32G0 HAL의 고정 커밋 289ef6c8에서 UART_Start_Receive_DMA()는 수신 버퍼·개수를 보관하고 완료·절반 완료·오류 callback을 연결합니다. DMA 시작에는 UART의 RDR 주소와 버퍼 주소·개수를 전달하고, 시작 실패를 처리한 뒤 UART의 DMA 수신 요청을 활성화합니다. API의 Size는 데이터 요소 수이며, 9비트·패리티 없음 같은 설정을 위의 8비트 예시와 동일하게 취급하지 않습니다.

채널·요청 매핑, 주변장치/메모리 전송 폭, 주소 증가, 버퍼 용량과 수명을 실제 MCU 설정과 맞춥니다. 완료 인터럽트 경로가 설정·허용된 경우에만 그 통지로 CPU에 인계할 수 있습니다. DMA가 쓰는 영역을 CPU가 동시에 수정하거나, 순환 DMA가 아직 처리하지 않은 데이터를 덮지 않도록 소유권을 정합니다. 데이터 캐시가 있는 다른 시스템은 그 플랫폼의 DMA 일관성 규칙도 따라야 하며 STM32G071에 같은 캐시 구조가 있다고 일반화하지 않습니다.

버스 중재로 지연될 수 있고 UART overrun·프레이밍 오류·DMA 오류도 남습니다. 정상 모드의 재시작 공백과 순환 모드의 소비 속도를 따로 확인합니다. DMA 완료는 설정한 전송 단위의 완료이며, 위에서 설명한 애플리케이션 메시지의 무결성과 경계 검증은 여전히 필요합니다. 이 글의 검산과 소스 대조만으로는 실제 보드에서 DMA가 동작하는지 확인할 수 없습니다.

작업 순서를 다시 확인합니다

시리얼 수신에서는 배열 용량과 실제 받은 길이를 따로 관리합니다 — 설명용 도해
시리얼 수신에서는 배열 용량과 실제 받은 길이를 따로 관리합니다 — 설명용 도해 · 클릭하면 확대됩니다.