엔지니어를 위한 임베디드 소프트웨어: 개발자를 위한 안내

엔지니어링 수준에서 임베디드 소프트웨어와 범용 소프트웨어를 구분하는 것은 무엇인가

첫 임베디드 제품을 출시하는 팀은 종종 데스크톱 애플리케이션처럼 소프트웨어를 다룹니다. 기능을 추가하고, 벤치에서 테스트하고, 작동하면 출시합니다. 그런 다음 장치가 생산 환경에서 3주 동안 실행되다가 잠깁니다. 충돌 로그도 없고, 스택 추적도 없습니다. 그저 전원 사이클이 필요한 멈춘 시스템일 뿐입니다. 이 패턴은 임베디드 제품 개발 전반에 걸쳐 반복되는데, 이는 임베디드 소프트웨어를 지배하는 규칙이 애플리케이션 소프트웨어를 지배하는 규칙과 다르며, 이러한 차이는 지속적인 실제 조건에서만 나타나기 때문입니다.

최우선 설계 제약 조건으로서의 리소스 제약

임베디드 소프트웨어는 실리콘에 의해 설정된 엄격한 제한 하에서 실행됩니다: 고정된 RAM, 제한된 플래시, 가상 메모리 없음, 그리고 안전이 중요한 컨텍스트에서는 동적 힙이 전혀 없습니다. 이러한 것들은 설계가 완료된 후 달성해야 하는 성능 목표가 아닙니다. 처음부터 모든 설계 선택을 형성하는 경계입니다.

스택 깊이 분석이 한 예입니다. 데스크톱 시스템에서 스택 오버플로우는 복구 가능한 예외입니다. 총 8KB RAM을 가진 마이크로컨트롤러에서는 예상보다 깊은 호출 체인이 인접 메모리를 조용히 손상시키고 스택 문제와 전혀 닮지 않은 실패를 발생시킵니다. 엔지니어는 작업별로 스택 공간을 할당하고, 정적 분석 도구를 사용하여 최악의 호출 깊이를 측정하며, 위반 사항을 경고가 아닌 차단 문제로 취급합니다.

정적 메모리 할당도 동일한 논리를 따릅니다. 컴파일 시점에 모든 버퍼를 할당하면 링커가 펌웨어가 하드웨어에서 실행되기 전에 사용 가능한 RAM에 총량이 맞는지 확인할 수 있습니다. 이러한 결정성은 유연성 저하라는 대가를 치를 만한 가치가 있습니다.

목표는 나중에 최적화하는 것이 아니라, 설계 시점에 리소스 제한을 파악하여 수정 비용이 적게 들도록 하는 것입니다.

하드웨어 종속 실행 모델

임베디드 소프트웨어는 컴파일된 하드웨어의 맥락에서만 올바르게 동작합니다. 애플리케이션은 특정 메모리 주소의 UART 상태 레지스터를 읽습니다. 특정 데이터시트에 정의된 비트 필드를 사용하여 타이머 주변 장치를 구성합니다. 특정 벡터 오프셋에 등록된 핸들러로 인터럽트를 라우팅합니다. MCU를 변경하면 이 중 어느 것도 유효성을 보장할 수 없습니다.

이러한 긴밀한 결합은 효율성을 높입니다. 하드웨어 레지스터에 직접 통신하는 코드는 여러 추상화 계층을 통해 라우팅되는 코드보다 더 빠르게 실행되고 메모리를 덜 사용합니다. 여기서의 단점은 재작업 비용입니다. 하드웨어 개정판이 주변 장치의 기본 주소를 변경하거나 새 MCU가 다른 인터럽트 컨트롤러를 사용하는 경우, 이전 레이아웃을 가정했던 소프트웨어는 종종 명확하지 않은 방식으로 오류가 발생합니다. 깨끗한 HAL 경계에 투자하는 팀은 초기 오버헤드 비용을 약간 부담하고 하드웨어 개정 시 훨씬 더 큰 비용을 피할 수 있습니다.

결정성과 실시간 동작을 올바른 기준점으로

웹 애플리케이션에서 50ms 지연 스파이크는 UX 문제입니다. 모터 컨트롤러에서는 동일한 스파이크가 하드웨어를 손상시킬 수 있습니다. 임베디드 소프트웨어의 올바른 동작은 시간적 올바름을 포함합니다. 즉, 마감일을 준수하는 것이 사양의 일부이며, 달성하기 어려운 목표가 아닙니다.

세 가지 실행 모델이 설계 공간을 정의합니다. 하드 실시간 시스템은 예외 없이 모든 마감일을 충족해야 하며, 마감일 누락은 정의상 시스템 오류입니다. 소프트 실시간 시스템은 가끔 누락을 허용하지만 점진적으로 성능이 저하됩니다. 최선 노력 시스템은 처리량을 지연 시간보다 우선시하고 가변 타이밍을 수용합니다. 잘못된 모델을 조기에 선택하면 나중에 값비싼 재작업이 강제됩니다. 최선 노력으로 설계되었으나 나중에 하드 실시간 보장이 필요한 시스템은 일반적으로 튜닝 단계를 넘어 전체 아키텍처 변경이 필요합니다. 이러한 스케줄링 결정이 개발 워크플로와 연결되는 방법에 대해서는 다음을 참조하십시오. 임베디드 소프트웨어 개발 라이프사이클 및 툴체인.

실행 중인 시스템 내부의 임베디드 소프트웨어 구조

계층형 소프트웨어 모델: HAL, 미들웨어 및 애플리케이션

잘 구조화된 임베디드 소프트웨어는 세 계층에 걸쳐 관심사를 분리하며, 각 계층은 정의된 작업과 인접 계층과의 정의된 인터페이스를 가집니다.

  • HAL (하드웨어 추상화 계층): 모든 주변 장치 레지스터 접근을 소유합니다. 이 계층 위의 어떤 것도 하드웨어 주소에 직접 접근하지 않습니다.
  • 미들웨어: 프로토콜 스택, RTOS 서비스, 파일 시스템. 하드웨어 또는 제품별 종속성이 없습니다.
  • 애플리케이션 계층: 미들웨어 및 HAL API를 호출하여 제품의 동작을 구현합니다.

더 깊은 추상화는 하드웨어 변경 시 포팅 비용을 줄여줍니다. 하지만 각 계층 경계는 함수 호출 오버헤드와 스택 깊이를 추가합니다. 48MHz에서 실행되는 16KB RAM의 Cortex-M0에서는 해당 오버헤드가 측정 가능합니다. 리소스 제약이 있는 타겟에서 작업하는 엔지니어는 때때로 HAL과 미들웨어를 단일 계층으로 통합하여 해당 헤드룸을 복구합니다. 올바른 답은 하드웨어 변경 빈도와 실제 리소스 예산이 얼마나 타이트한지에 따라 달라집니다.

베어메탈 vs. RTOS 기반 실행 아키텍처

두 가지 런타임 모델이 대부분의 임베디드 시스템을 다룹니다. 베어메탈 펌웨어는 상태를 폴링하고 핸들러를 호출하는 슈퍼 루프(while(1))를 실행하거나 인터럽트에 전적으로 의존합니다. RTOS 기반 시스템은 선점형 스케줄러 하에서 우선순위 수준, 태스크 간 큐, 세마포어가 동시 작업을 관리하는 여러 태스크를 실행합니다.

요소 베어메탈 RTOS 기반
오버헤드 최소 스케줄러 + 컨텍스트 스위치 비용
동시성 단일 실행 경로 다중 우선순위 태스크
데드라인 다양성 마감이 균일할 때 작동 마감이 매우 다양할 때 필요
디버그 복잡성 낮음 높음 (경쟁 조건, 우선순위 역전)

선택은 개인적인 선호도가 아닌 작업 수와 마감일의 다양성에 따라 결정됩니다. 단일 센서 데이터 로거와 하나의 통신 채널은 베어메탈에서 깔끔하게 실행됩니다. 디스플레이, CAN 버스, USB 스택 및 안전 워치독을 동시에 관리하는 장치는 해당 문제들을 분리하고 스케줄링 가능하도록 만들기 위해 RTOS가 필요합니다. RTOS 대상에 대한 구체적인 프로그래밍 패턴은 다음을 참조하십시오. RTOS 대상용 임베디드 시스템 프로그래밍 패턴.

부트로더는 애드온이 아닌 구조적 구성 요소입니다.

SWD programming cable connected to PCB debug header on electronics assembly bench

부트로더는 애플리케이션보다 먼저 실행됩니다. 핵심 하드웨어를 초기화하고 애플리케이션 이미지를 검증하며, 검증이 통과된 경우에만 제어를 이전합니다. 프로젝트 후반에 부트로더를 추가하거나 "간단한" 제품에 대해 완전히 건너뛰는 엔지니어는 필드 업데이트가 장치를 손상시키고 복구 경로가 없을 때 종종 격차를 발견합니다.

부트로더 경계는 업데이트 표면을 정의합니다. 해당 경계 위의 모든 것은 부트로더 자체를 건드리지 않고 무선(OTA) 또는 프로그래밍 인터페이스를 통해 업데이트할 수 있습니다. 그 아래의 모든 것은 물리적 연결과 전체 재플래시가 필요합니다. 해당 경계를 잘못 설정하면 업데이트 위험에 중요한 초기화 코드를 과도하게 노출하거나 업데이트 가능해야 하는 애플리케이션 코드를 과소 노출하게 됩니다. 자세한 부트로더 설계 패턴은 임베디드 소프트웨어 개발 가이드를 참조하십시오.

실제 하드웨어에서 작동하는 임베디드 소프트웨어 작성: 주요 구현 결정

main() 이전의 시작 코드 및 시스템 초기화

애플리케이션은 여기서 시작되지 않습니다. main(). 해당 함수가 호출되기 전에 시작 코드가 실행됩니다. 이 코드는 스택 포인터를 설정하고, 플래시에서 RAM으로 초기화된 변수를 복사하고, BSS 세그먼트를 0으로 만들고, 시스템 시계를 구성합니다. 많은 MCU에서 워치독은 하드웨어 기본값으로 활성화되며, 클럭 구성이 완료되기 전에 서비스되거나 비활성화되어야 합니다. 그렇지 않으면 시스템이 재설정됩니다. main() 이전에 도달하는.

공급업체에서 제공하는 시작 파일은 표준 구성의 경우 대부분 이 작업을 올바르게 처리합니다. 프로젝트에서 기본값이 아닌 메모리 레이아웃, 기본 타임아웃이 허용하는 시간보다 더 오래 걸리는 외부 발진기 또는 BSS 섹션을 잘못 배치하는 사용자 정의 링커 스크립트를 사용하는 경우 문제가 발생합니다. 시작 코드를 블랙박스로 취급하는 엔지니어는 명확한 원인 없이 하드 폴트가 발생하는 초기화 실패 시 진단 시간을 잃게 됩니다. 시작 파일을 한 번 읽고, 작동 방식을 이해하고, 애플리케이션 코드를 추가하기 전에 클럭 트리를 확인하면 나중에 상당한 디버그 시간을 절약할 수 있습니다.

인터럽트 서비스 루틴 설계 및 공유 데이터 위험

Firmware engineer reading oscilloscope ISR timing waveform on embedded development bench

ISR은 임베디드 소프트웨어가 실시간으로 하드웨어 이벤트에 응답하는 방식입니다. ISR 설계는 시스템 지연 시간과 데이터 무결성에 직접적인 영향을 미칩니다. ISR을 짧게 유지하십시오. 블로킹되지 않도록 유지하십시오. ISR 내에서 소비되는 모든 사이클은 메인 실행 컨텍스트가 실행할 수 없는 사이클입니다.

ISR과 메인 루프 간의 공유 데이터는 신중한 처리가 필요합니다. ISR 내에서 수정되고 메인 루프에서 읽는 변수는 선언해야 합니다. volatile 컴파일러가 레지스터에 오래된 값을 캐싱하는 것을 방지합니다. MCU의 네이티브 워드 크기보다 큰 변수의 경우 메인 루프에서의 단일 읽기는 원자적이지 않을 수 있습니다. ISR은 두 개의 로드 명령 사이에 상위 바이트를 업데이트할 수 있습니다. 읽기 주위에 인터럽트를 비활성화하는 것이 올바른 수정이며, 해결책이 아닙니다.

ISR에서 지연된 처리로 작업을 이동하면(ISR에서 플래그를 설정하고 메인 루프 또는 낮은 우선순위 작업에서 작업을 처리) 응답성이 향상됩니다. ISR과 처리 작업 간의 링 버퍼는 UART 수신 처리에 일반적인 패턴입니다. 단점은 복잡성이 추가된다는 것입니다. 버퍼는 오버플로 감지가 필요하고, 처리 작업은 데이터가 대기 중인지 알 수 있는 방법이 필요합니다.

제약 조건이 있는 타겟을 위한 메모리 관리 전략

동적 할당 (via malloc 및 free 대부분의 임베디드 타겟에서 사용 가능합니다. 실제 임베디드 소프트웨어는 이를 완전히 피하는 경우가 많습니다. 몇 달 동안 실행되는 시스템의 힙 단편화는 재현하기 어렵고 현장에서 진단하기 더 어려운 종류의 오류인 할당 오류를 발생시킬 수 있습니다. 할당 시간 또한 결정적이지 않으므로 하드 실시간 요구 사항과 충돌합니다.

운영체제 시스템은 동적 할당을 대체하기 위해 세 가지 전략을 사용합니다:

  • 정적 할당: 모든 버퍼가 컴파일 시간에 선언됩니다. 링커가 적합성을 검증합니다. 런타임 오버헤드가 없습니다.
  • 메모리 풀: 미리 할당된 영역에서 할당된 고정 크기 블록입니다. 할당 시간은 일정합니다. 풀 내에서는 단편화가 불가능합니다.
  • 스택 기반 할당: 태스크 또는 함수의 스택에 있는 지역 변수입니다. 반환 시 자동으로 해제됩니다. 수명이 짧고 크기가 제한된 데이터에 적합합니다.

정적 할당은 고정되고 예측 가능한 데이터 흐름을 가진 시스템에 적합합니다. 메모리 풀은 힙 위험 없이 메시지 버퍼 할당 및 해제와 같이 동적 동작이 필요한 시스템에 적합합니다. 스택 할당은 임시 계산에 적합합니다. 대부분의 프로덕션 시스템은 데이터 수명과 크기 예측 가능성에 따라 할당되는 세 가지 모두를 사용합니다. 이러한 전략이 디스플레이 기반 및 HMI 시스템에 특히 적용되는 방법에 대해서는 리소스 제약 HMI 시스템을 위한 프로그래밍 모델을 참조하십시오.

임베디드 소프트웨어: 엔지니어링 질문에 대한 직접적인 답변

임베디드 소프트웨어는 펌웨어와 같은 것입니까?
펌웨어는 임베디드 소프트웨어의 하위 집합입니다. 펌웨어는 비휘발성 메모리에 저장되어 최저 수준에서 하드웨어를 초기화하고 제어하는 소프트웨어를 구체적으로 지칭합니다. 임베디드 소프트웨어는 펌웨어, 미들웨어, 제약된 하드웨어에서 실행되는 애플리케이션 계층을 포함하는 더 넓은 범주입니다. 프로젝트 범위를 정할 때 이 구분이 중요합니다. 펌웨어 엔지니어와 임베디드 애플리케이션 엔지니어는 동일한 제품에서 다른 작업을 수행할 수 있습니다.

임베디드 소프트웨어는 운영 체제 없이 실행될 수 있습니까?
예 - 위 시스템 아키텍처 섹션의 베어메탈 대 RTOS 논의를 참조하십시오. 안전 필수 컨트롤러 및 간단한 센서 노드를 포함한 많은 생산 임베디드 시스템은 RTOS의 오버헤드가 시스템의 복잡성에 의해 정당화되지 않기 때문에 설계상 베어메탈로 실행됩니다.

임베디드 소프트웨어를 애플리케이션 소프트웨어보다 디버깅하기 어렵게 만드는 요인은 무엇입니까?
세 가지 요인이 난이도를 복합적으로 만듭니다. 대상에 디스플레이 출력이 제한적이거나 전혀 없다는 점, 디버그 출력 문을 삽입할 때 변경되는 실시간 동작, 시뮬레이터에서 재현되지 않는 하드웨어 종속 실패입니다. JTAG/SWD 디버그 인터페이스와 로직 분석기가 주요 도구이며, 이는 " 임베디드 타겟을 위한 디버그 및 추적 도구 " 가이드에서 자세히 다룹니다.

임베디드 소프트웨어는 사용자 정의 컴파일러 또는 툴체인이 필요합니까?
사용자 정의 컴파일러가 아니라 항상 대상별 컴파일러입니다. 임베디드 소프트웨어는 호스트 머신에서 ARM Cortex-M, RISC-V 등과 같은 다른 대상 아키텍처용으로 크로스 컴파일됩니다. 링커 스크립트는 대상의 메모리 맵과 정확히 일치해야 합니다. 올바른 링커 스크립트가 없는 일반 컴파일러는 부팅 코드가 잘못된 주소에 배치되므로 부팅되지 않는 바이너리를 생성합니다.

생산용 임베디드 소프트웨어 개발은 시작 코드, 메모리 전략, 현장 업데이트 설계에 이르기까지 모든 계층에서 이러한 종류의 프로세스 규율을 요구합니다. 모든 계층의 격차는 확장된 런타임 후에야 나타나는 경향이 있으며, 이때 수정 비용이 가장 높습니다. STONE HMI는 산업용 HMI 시스템을 위한 생산 등급 펌웨어를 개발합니다. 임베디드 개발 파트너를 평가하는 프로젝트 팀의 경우 펌웨어 수준에서의 프로세스 규율은 배송 위험에 대한 직접적인 지표입니다. 부트로더 설계, ISR 안전성 및 메모리 전략을 기본값으로 취급하는 파트너는 현장에서 견딜 수 있는 시스템을 생산할 가능성이 낮습니다.