데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      SRAM이 부족한 임베디드 통신, 버퍼를 늘리지 않고 해결하는 3단계 메모리 분할 전략

      DEVOTEE 26.09.28
      13 2 0

      마이크로컨트롤러(MCU) 펌웨어를 다루다 보면 누구나 한 번쯤 겪는 벽이 있습니다. 평소에는 작은 제어 패킷만 오가며 문제없이 작동하던 시스템에 수 킬로바이트(KB) 크기의 인증서나 대용량 프로토콜 규격이 들어오는 순간입니다. 전송 도중 패킷이 잘려 통신 타임아웃이 발생하거나, 메모리가 모자라 링커가 "RAM overflowed" 오류를 뱉어내며 빌드조차 거부합니다. 이때 메모리가 더 큰 칩으로 교체하는 것은 보드 단가와 양산 일정 때문에 대부분 불가능합니다.

      오늘 살펴볼 세 가지 소식은 동일한 하드웨어 제약을 서로 다른 통신 단계에서 돌파한 실제 엔지니어링 사례입니다. 전기차 충전기 내부에서 ESP32(서버 통신)와 SECC(전기차 통신 컨트롤러) 사이를 중계하는 STM32G484 칩셋 환경에서, 6KB에 달하는 Plug and Charge(PnC) 인증서를 처리하기 위해 진행된 수신(RX) 구조 변경, 송신(TX) 버퍼 스트리밍, 그리고 인증서 메모리 재설계 작업입니다.

      이 세 가지 기록은 같은 결론을 가리킵니다. 임베디드 통신 병목을 풀기 위해 필요한 것은 버퍼의 확장이 아니라, 수신·송신·저장의 전 과정에서 데이터를 잘게 쪼개어 흐르게 만드는 파이프라인식 데이터 흐름 분할입니다.


      수신(RX) 구조의 전환: 고정 슬롯에서 8KB 링 버퍼로

      충전기 내부 통신에서 STM32G484는 중심 허브 역할을 맡습니다. STM32 UART RX 구조 변경, 슬롯 방식에서 8KB Ring Buffer로...에 따르면, 이 시스템의 기본 통신 구조는 ESP32와 STM32G484, 그리고 SECC(GQSE)가 각각 UART(범용 비동기 송수신기, 직렬 통신 하드웨어)로 연결된 형태입니다. ESP32는 외부 서버 통신을 담당하고, SECC는 차량과의 ISO 15118(전기차-충전기 간 디지털 통신 표준) 통신을 전담하며, STM32는 이 둘 사이에서 오가는 제어 명령과 인증 데이터를 중계합니다.

      기존에 사용하던 방식은 고정 크기의 슬롯(Slot) 구조였습니다. 고정된 크기의 배열 여러 개를 미리 할당해 두고, 수신이 완료될 때마다 한 슬롯씩 채워 메인 루프로 넘기는 방식입니다. 이 방식은 각 패킷의 크기가 작고 일정할 때는 구현이 직관적입니다. 하지만 인증서와 같이 크기가 불규칙하고 연속해서 밀려드는 대용량 데이터를 처리할 때는 치명적인 단점이 드러납니다. 최대 패킷 길이에 맞춰 슬롯 크기를 키우면 빈 공간으로 낭비되는 메모리가 지나치게 많아지고, 반대로 슬롯 크기를 줄이면 허용 범위를 넘는 패킷이 도착했을 때 버퍼 오버플로가 발생해 패킷이 유실됩니다.

      원문 작업자는 이 슬롯 기반 구조를 폐기하고 8KB 크기의 링 버퍼(Ring Buffer, 버퍼의 끝과 시작이 원형으로 연결되어 순환 저장되는 버퍼) 구조로 전환했습니다. 링 버퍼는 수신 포인터(Head)와 소비 포인터(Tail)를 분리해, 인터럽트나 DMA(직접 메모리 접근, CPU 개입 없이 하드웨어가 메모리에 데이터를 쓰는 방식)가 들어오는 데이터를 끊김 없이 밀어 넣고, 메인 루프는 비동기로 이를 꺼내 파싱하도록 만듭니다.

      이 선택은 지금 실무에 바로 써볼 만합니다. 고정 슬롯 방식은 메모리 단편화와 미사용 공간 낭비를 필연적으로 동반합니다. 반면 8KB 링 버퍼는 패킷 단위가 아니라 바이트 스트림 단위로 공간을 재사용하므로, 동일한 물리 메모리 한도 안에서 순간적인 버스트(Burst, 순간 폭주) 트래픽을 유실 없이 흡수하는 데 가장 효과적인 구조이기 때문입니다.


      송신(TX)의 발상 전환: 6KB 인증서를 512바이트 청크로 쪼개기

      수신 버퍼를 개선해 데이터를 무사히 받았다고 해서 문제가 끝나지 않습니다. 수신한 데이터를 반대편 장치(SECC 또는 ESP32)로 전달해야 합니다. 특히 전기차 Plug and Charge(PnC, 플러그만 꽂으면 자동으로 충전과 결제가 진행되는 시스템) 환경에서는 공개키 기반 구조(PKI) 인증서가 오갑니다.

      6KB PnC 인증서를 위한 STM32 UART TX 버퍼 512바이트 스트리밍에서는 이 구체적인 문제를 다룹니다. PnC 인증서 체인은 약 6KB에 달합니다. 만약 송신(TX)을 처리하기 위해 6KB 크기의 전송 버퍼를 통째로 할당한다면, 가용 SRAM(정적 램, 마이크로컨트롤러의 고속 작업 메모리)이 극도로 제한적인 STM32G484 환경에서는 다른 주요 태스크(UI 처리, 센서 감시, 안전 릴레이 제어 등)의 스택과 힙을 압박해 런타임 오류를 유발합니다.

      원문에서는 6KB 크기의 거대한 송신 버퍼를 선언하는 대신, TX 버퍼를 단 512바이트로 대폭 줄이고 스트리밍(Streaming) 방식으로 송신 구조를 재구성했습니다. 전체 6KB 데이터를 단번에 UART 버퍼에 붓지 않고, 512바이트 단위의 청크(Chunk, 쪼갠 데이터 조각)로 나누어 DMA 전송 완료 인터럽트가 발생할 때마다 다음 512바이트를 순차적으로 밀어 넣는 방식입니다.

      여기부터는 DEVOTEE의 해석입니다. 임베디드 펌웨어에서 "대용량 전송 = 대용량 TX 버퍼"라는 공식은 잘못된 접근입니다. 송신 측에서 흐름 제어(Flow Control)를 구현할 수 있다면, TX 버퍼의 크기는 하드웨어 DMA 버스트 단위나 프로토콜 프레임의 최소 공통 단위에 맞추는 것이 정답입니다. 512바이트는 대다수 직렬 통신 프로토콜의 체크섬 계산 단위이자 DMA 전송 효율을 떨어뜨리지 않는 균형점입니다.

      이 접근법 역시 지금 즉시 적용해 볼 가치가 충분합니다. 수신 측(SECC)의 수신 큐 상태를 감시하는 흐름 제어 로직만 명확히 갖추어져 있다면, 6KB의 SRAM 점유 공간을 512바이트로 줄여 무려 5.5KB 이상의 귀중한 RAM을 확보할 수 있습니다.


      메모리 레이아웃 재설계: SRAM 쥐어짜기의 마지막 단계

      수신 링 버퍼를 만들고 송신 스트리밍을 구성해도 여전히 여유가 부족할 때가 있습니다. 시스템에는 통신만 있는 것이 아니기 때문입니다. 그래픽 화면(GUI)을 띄우기 위한 라이브러리나 각종 상태 머신도 같은 램을 나눠 써야 합니다.

      STM32 - KVAS 인증서 메모리 구조 재설계에서는 이 한계 상황에서의 고군분투를 보여줍니다. 원문은 "LVGL(임베디드 오픈소스 그래픽 라이브러리) 디스플레이 버퍼를 줄이고 UART 수신 구조를 다시 설계하면서 큰 메모리부터 하나씩 정리했지만, 앞으로 추가해야 할 PnC 인증서 처리를 생각하면 여전히 여유가 부족했다"고 기록합니다. 결국 지금까지 잘 작동하던 KVAS(충전 인증 시스템 관련 인증서 구조)의 내부 메모리 구조 자체를 다시 설계하기에 이릅니다.

      인증서 데이터를 다룰 때 흔히 저지르는 실수는 인증서 구조체 전체를 하나의 거대한 바이트 배열로 SRAM에 계속 올려두는 것입니다. 하지만 인증서 검증이나 전달 과정에서 실시간으로 필요한 것은 인증서의 전체 원문이 아니라, 헤더 정보, 길이, 유효기간, 서명 알고리즘 같은 메타데이터입니다. 원문 바이트는 플래시 메모리(비휘발성 저장 공간)에 머물게 하고, 파싱 작업 시에만 임시 버퍼를 활용하거나 인덱스 포인터만 유지하도록 재설계하면 SRAM 점유율을 획기적으로 낮출 수 있습니다.

      이 판단은 조건부로 유효합니다. 메모리 구조체를 포인터 및 온디맨드(On-demand) 참조 방식으로 바꾸면 SRAM은 크게 절약되지만, 인증서 데이터를 검증할 때마다 플래시 메모리 읽기 접근이 늘어나 CPU 사이클을 더 소모하게 됩니다. 따라서 밀리초 단위의 초고속 인증 처리가 요구되는 통신 구간인지, 아니면 수 초의 여유가 있는 비동기 핸드셰이크 구간인지를 측정하고 적용해야 합니다.


      실무에서 달라지는 점: 링 버퍼와 청크 송신의 코드 구현

      그렇다면 슬롯 기반 통신을 링 버퍼와 청크 송신 구조로 바꾸면 코드는 어떻게 달라질까요? STM32 HAL(하드웨어 추상화 계층) 환경을 기준으로 두 가지 핵심 패턴을 코드로 살펴보겠습니다.

      먼저 수신부에서는 수신 인터럽트나 DMA 반환 시점에서 원형으로 인덱스를 회전시키며 바이트를 밀어 넣는 링 버퍼 구조체를 사용합니다.

      #include <stdint.h>
      #include <stdbool.h>
      
      #define RX_RING_BUFFER_SIZE 8192
      
      typedef struct {
          uint8_t buffer[RX_RING_BUFFER_SIZE];
          volatile uint16_t head;
          volatile uint16_t tail;
      } RingBuffer;
      
      RingBuffer rx_rb = { .head = 0, .tail = 0 };
      
      /* UART RX 인터럽트 서비스 루틴에서 1바이트씩 또는 DMA로 호출 */
      void RingBuffer_Put(RingBuffer *rb, uint8_t byte) {
          uint16_t next_head = (rb->head + 1) % RX_RING_BUFFER_SIZE;
          if (next_head != rb->tail) {
              rb->buffer[rb->head] = byte;
              rb->head = next_head;
          } else {
              /* 버퍼 오버플로 발생 시 오류 카운터 증가 및 플래그 설정 */
          }
      }
      
      /* 메인 루프 태스크에서 비동기로 데이터 소비 */
      bool RingBuffer_Get(RingBuffer *rb, uint8_t *byte) {
          if (rb->head == rb->tail) {
              return false; /* 버퍼가 비어 있음 */
          }
          *byte = rb->buffer[rb->tail];
          rb->tail = (rb->tail + 1) % RX_RING_BUFFER_SIZE;
          return true;
      }
      

      송신부에서는 6KB 인증서 포인터와 전송 오프셋을 관리하는 상태 머신을 두고, 512바이트 크기의 단일 TX 버퍼만을 순환 재사용합니다.

      #include <stdint.h>
      #include <string.h>
      
      #define CHUNK_SIZE 512
      
      typedef struct {
          const uint8_t *data_ptr;
          uint16_t total_length;
          uint16_t current_offset;
          bool is_transmitting;
      } PncTxStream;
      
      static uint8_t tx_chunk_buf[CHUNK_SIZE];
      static PncTxStream tx_stream;
      
      /* 전송 시작 함수 */
      void Start_Pnc_Certificate_Stream(const uint8_t *pnc_cert, uint16_t length) {
          tx_stream.data_ptr = pnc_cert;
          tx_stream.total_length = length;
          tx_stream.current_offset = 0;
          tx_stream.is_transmitting = true;
      
          Send_Next_Chunk();
      }
      
      /* 청크 전송 실행 및 DMA 완료 콜백 연동 */
      void Send_Next_Chunk(void) {
          if (!tx_stream.is_transmitting) {
              return;
          }
      
          uint16_t remaining = tx_stream.total_length - tx_stream.current_offset;
          if (remaining == 0) {
              tx_stream.is_transmitting = false;
              return; /* 전체 6KB 전송 완료 */
          }
      
          uint16_t send_size = (remaining > CHUNK_SIZE) ? CHUNK_SIZE : remaining;
          memcpy(tx_chunk_buf, tx_stream.data_ptr + tx_stream.current_offset, send_size);
      
          /* UART DMA 송신 시작 (완료 시 HAL_UART_TxCpltCallback 호출) */
          HAL_UART_Transmit_DMA(&huart1, tx_chunk_buf, send_size);
          tx_stream.current_offset += send_size;
      }
      
      /* STM32 HAL UART 송신 완료 인터럽트 콜백 */
      void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
          if (huart->Instance == USART1) {
              Send_Next_Chunk(); /* 다음 512바이트 송신 */
          }
      }
      

      이 구조를 도입하면 다음과 같은 실무적 변화가 발생합니다.

      기존에는 6KB 통신을 위해 uint8_t tx_buf[6144];와 같이 힙이나 BSS 영역을 수 킬로바이트씩 독점하는 배열을 선언해야 했습니다. 이제는 CHUNK_SIZE(512바이트) 배열 하나와 12바이트 남짓의 상태 구조체만으로 동일한 데이터를 전송할 수 있습니다.

      단, 주의할 점이 있습니다. 송신 데이터 원본(data_ptr)이 전송 도중 덮어쓰여지지 않도록 보장해야 합니다. 원본이 플래시(ROM) 영역에 고정되어 있다면 아무 문제가 없지만, 동적으로 수신된 데이터라면 송신이 완전히 끝날 때까지 해당 수신 원본 버퍼가 유지되도록 상태 동기화 잠금을 걸어야 합니다.


      직접 해 볼 수 있는 한 가지

      지금 개발 중인 펌웨어 프로젝트의 메모리 맵(.map) 파일을 텍스트 에디터로 열어보세요.

      BSS(초기화되지 않은 전역 변수 영역)와 DATA 영역에서 크기가 1KB 이상인 전역 배열 목록을 정렬해 확인합니다. 만약 수 킬로바이트 크기의 통신용 TX_BUFFER나 RX_BUFFER가 여러 개 잡혀 있다면, 오늘 소개한 방식을 대입해 볼 차례입니다.

      송신 버퍼라면 한 번에 전송하는 청크 크기를 256 또는 512바이트로 제한하고 전송 완료 인터럽트 기반의 스트리밍으로 전환 가능한지 점검하세요. 수신 버퍼라면 여러 개의 고정 슬롯 배열을 하나의 통합 원형 링 버퍼로 합칠 수 있는지 계산해 보시기 바랍니다. 수 킬로바이트의 램을 아끼기 위해 칩 단가를 올리거나 보드를 다시 설계하지 않아도, 데이터 흐름을 바꾸는 것만으로 수신 누락과 메모리 부족 현상을 동시에 잡을 수 있습니다.


      그 밖의 소식

      [1] DB·서버 접근 제어를 한 번에, DSAC 출시 — 네이버클라우드가 개인정보 접속 기록(1년) 및 접근 권한 이력(3년) 법적 의무를 관리하는 클라우드 네이티브 보안 상품 DSAC을 선보였습니다.

      [2] 80달러짜리 모텔 방에서, 생명의 기원을 밝힐 발견 — 연구진이 희귀 단세포 생물인 Paulinella 신종 2종을 발견해 광합성의 진화 과정을 규명했습니다. 개발 실무와 직접 관련은 없어 링크만 남깁니다.

      [5] 연 300시간을 아낀 AI 상담 서비스 — 토스뱅크 전세대출 팀에서 서류 제출 문의와 단순 질문 데이터를 분석해 상담 효율을 개선한 프로덕트 디자인 경험을 공유했습니다.

      [6] 기업용 AI가 마침내 SaaS의 약속을 실현하고 있다 — 구축과 컨설팅에 의존하던 기존 SaaS의 한계를 기업용 AI가 업무 판단 노하우를 제품에 내재화하면서 극복하고 있다는 분석입니다.

      [10] TinyAIArena - AI 모델 네 개가 싸우는 배틀로얄 관전하기 — 4개의 AI 모델이 8×8 격자 턴제 게임에서 생존을 위해 대결하고 채팅을 주고받는 시뮬레이션 프로젝트입니다.

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      DEVOTEE 님의 최신 블로그

      더보기
      동영상 기고하기