Chapter 1. 개요
- 임베디드 시스템의 아키텍처는 MCU를 중심으로 peripheral 및 외부 세계와 통신하기 위한 특별한 인터페이스 모음을 갖고 있다. MCU는 프로세서, RAM, Flash, serial tx/rx 등을 포함한 SoC이다.
- 임베디드 프로그래밍에서 중요한 부분은 주변장치(Peripheral)와 통신하는 것이다. 전자공학에 대한 기본적인 지식, 회로도 및 datasheet를 이해하는 능력, 로직 분석기나 오실로스코프 같은 측정 도구에 대한 확신이 필요하다.
- 임베디드 SW 개발은 자원이 제한돼있음을 철저히 고려해 가장 효율적인 방식에 초점을 맞춰야 한다.
- 새로운 기능이나 복잡한 구조체를 구현하는데 필요한 충분한 공간이 RAM과 flash에 없을 수 있다.
- 프로세서가 데이터를 처리하기에 충분히 빠르지 않을 수 있다.
- 더 적은 전력을 소모하는 펌웨어가 필요할 수도 있다.
- MCU는 MMU를 가지고 있지 않고, 메모리가 항상 부족하며, 특정 순서로 모든 데이터를 처리한다. 모든 연산에 잠재적으로 필요한 메모리의 양과 buffer를 항상 고려해야 한다. 스와핑 및 메모리 relocation 같은 고급 OS 기능은 불가능한 경우가 많다. 따라서 성공적인 개발을 위해서는 잘 정의된 테스트 케이스, 성능 지표, 효율적인 프로토타입 단계 등으로 구성된 탄탄한 workflow가 필요하다.
- 펌웨어는 MCU 내부 flash에 저장되며, MMU가 없기 때문에 flash는 memory에 고정 물리 주소로 매핑되기 때문에 프로세서는 주소 변환 없이 flash에서 직접 instruction을 순차적으로 가져오는 XIP(Execute In Place) 매커니즘으로 실행된다.
- ARM Cortex-M 제품은 다음과 같은 특징을 가진다.
- 16개의 general purpose register.
- 코드 밀도 최적화를 위한 Thumb 16bit용 instruction format.
- Priority based NVIC (Nested Vector Interrupt Controller)
- 8개의 구역을 지원하는 MPU (Memory Protection Unit)
Chapter 2. 작업환경과 워크플로 최적화
이번 chapter에서는 특정 타겟을 위한 펌웨어 이미지를 생성하는 툴체인(toolchain)에 대해 배운다.
2.1. Compiler
- C 컴파일러는 GNU/리눅스 배포판의 GCC가 대표적이며
.c
소스코드를 .o
오브젝트 파일로 만든다.
- 컴파일을 수행한 host와 타겟 디바이스의 아키텍처가 동일하면 그냥 ‘컴파일러’
- 컴파일을 수행한 host와 타겟 디바이스의 아키텍처가 다르면 ‘크로스 컴파일러’라고 부른다.
- 프로그램에는 여러 모듈이 필요하고, 이 모듈 하나하나가 오브젝트 파일로 구성된다.
2.2. Linker
-
링커는 오브젝트 파일들의 집합인 모듈들의 심볼 의존성을 모두 처리해 실행파일로 만드는 역할을 한다.
-
결과물인 .elf
실행파일은 프로그램 코드와 데이터를 포함하는 등 다양한 section으로 구성된다.
.text
section: 프로그램 코드를 포함하며 읽기 전용이다.
.rodata
section: 런타임에 수정되지 않는 const
상수를 저장하기 위한 읽기 전용 공간이다.
.data
section: 초기화 된 변수나 static
같은 전역 변수들이 저장되는 공간이다. 읽기/쓰기 가능하다.
.bss
section: 초기화 되지 않은 변수들이 저장되는 공간이다. 읽기/쓰기 가능하다. 프로그램의 초기화 코드는 main() 함수 실행 이전에 이 영역의 변수들이 모두 0
으로 초기화되는 것을 보장해야 한다.
-
링커 스크립트는 타겟의 메모리 섹션을 서술한 파일이다. 여기에는 프로그램이 실행되기 전에 꼭 필요한 정보들이 포함되며 코드 내 모든 심볼에 대한 정보가 저장돼있다. 즉, 메모리에 매핑되는 각 section에 대한 설명과 타겟의 flash & RAM에 프로그램을 올릴 때 필요한 정보가 저장된 .ld
파일이다.
-
가장 간단한 링커 스크팁트는 적어도 RAM과 FLASH에 대한 정보를 담은 MEMORY section을 갖고 있다.
MEMORY {
FLASH(rx) : ORIGIN = 0x00000000, LENGTH = 256K
RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}
-
앞서 .text
와 .rodata
는 읽기 전용이라고 했다. 따라서 flash에 저장돼야 한다.
반면 .data
와 .bss
는 RAM에 매핑되며 수정이 가능하다.
2.3. Make
-
Make
는 binary 이미지 파일 생성을 자동화할 수 있는 표준 유닉스/POSIX 도구다.
-
사용자가 직접 레시피나 규칙을 적용해 입맛에 맞는 출력 파일을 자동으로 생성해줄 수 있는 강력한 도구다.
-
별도의 문법을 가지며 구조는 아래와 같다.
target: [prerequisites]
recipe1
recipe2
...
2.4. Debugger
- 가장 유명한 디버거는 GCC Debugger인 GDB이다.
임베디드 sw 개발은 보통 host와 타겟 플랫폼이 다르기 때문에 원격 디버그 세션을 만들어야 디버깅이 가능하다.
- 디버거는 CPU register와 memory의 현재 상태를 직접 살펴보면서 런타임 속 오류를 잡아낼 수 있도록 돕는다.
- ※ 개발 중 원활한 디버깅을 위해 컴파일러의 최적화 기능은 사용하지 않는 것이 좋다. 컴파일러가 코드의 실행 순서를 변경하고, 몇몇 변수는 숨겨버릴 수 있어서, 최적화된 코드 흐림이 디버깅 시 추적을 더 어렵게 만든다.
- GCC는 ARM 임베디드 툴체인을 배포하고 있다.
arm-none-eabi
라는 접두어로 시작한다.
- 내부에는 크로스 컴파일러와 디버거를 포함한다.
2.6. 타겟과의 상호작용
- 개발할 때 보통 우리는 타겟 보드와 JTAG 또는 SWD 인터페이스를 통해 flash에 펌웨어를 업로드하고 디버깅한다.
- 타겟의 JTAG/SWD 기능에 접근할 수 있는 강력한 오픈소스 도구로 로컬 소켓을 생성하는 OpenOCD (On-Chip Debugger)가 있다.
- 몇몇 개발 보드는 host와 통신하기 위한 추가 인터페이스가 부착된 경우도 있다. STMicrolectronics는 Cortex-M을 위한 ST-Link라는 인터페이스가 부착돼있다. 이를 통해 직접적인 디버깅 접근과 flash 업로드 기능을 지원한다.
2.7. 에뮬레이터
- Host PC에서 전체 타겟 플랫폼을
qemu
로 에뮬레이션하면 위험성을 줄이고 이식성 요구 없이 타겟 머신에 특화된 코드를 실행하고 디버깅할 수 있다.
Qemu
가 ARM의 하드웨어와 특수기능을 상당히 정확히 구현하고 있지만, 실제 하드웨어와 시스템 레이아웃이 완전 일치하지는 않기 때문에 분명히 한계를 가진다. 그렇지만, Qemu는 Cortex-M의 기능을 숙지하는 가장 빠른 방법이라 장담한다.
Chapter 3. 개발 및 설계 패턴과 작업 방식
3.1. 환경설정 관리
개발의 효율을 높이기 위해 협동 및 동기화를 최적화한다.
- 리비전 제어 - Git을 이용한 버전 관리가 여기에 해당한다.
- 이슈 추적 - 시스템의 알려진 버그와 활동을 계속 추적한다.
- 코드 리뷰
- 지속 통합 - 주기적으로 또는 코드 변경 시에 자동으로 동작을 수행하고, 테스트 결과를 수집하고, 동료 개발자 통지를 통해 빌드 및 테스트 실행 작업 등을 스케줄링한다.
3.2. 하드웨어 추상화
타겟 MCU의 종류는 굉장히 다양하고, 그 설계와 세부 구성이 모두 다르므로 하드웨어를 추상화해야만 개발자가 펌웨어를 개발하기 편해지고 재사용 가능한 코드를 작성할 수 있다.
ARM은 CMSIS (Cortex Microcontroller Software Interface Standard)라는 레퍼런스 표준을 만들어 하드웨어를 성공적으로 추상화했다. 서로 다른 하드웨어를 제어하기 위한 코드가 API 형태로 제공되므로 개발자는 API 내부의 복잡한 연산들에 대해 고려하지 않고도 CMSIS를 통해 서로 다른 두 하드웨어를 동일한 방법으로 개발할 수 있다.
Chapter 4. Boot-up Process ★★★
4.1. IVT (Interrupt Vector Table)
- IVT는 인터럽트를 처리하기 위한 핸들러 함수인 ISR (Interrupt Service Routain)의 포인터를 모아둔 배열이다.
- IVT는 보통 바이너리 이미지의 시작 부분에 있고, flash의 가장 낮은 시작 주소에 저장된다.
- ARM Cortex-M에서 정의한 시스템 인터럽트의 종류는 다음과 같다.
- Reset
- NMI (non-maskable interrupt)
- Hard fault
- Memory exception
- Bus fault
- Usage fault
- Supervisor call (Scheduler)
- Debug monitor (Breakpoint)
- PendSV (Shared resource, Semaphore …)
- System tick (Timer)
4.2. 1단계: 메모리 레이아웃
-
링커 스크립터의 .text
에는 가장 먼저 IVT에 대한 부분이 들어가야 한다.
.text : {
*(.isr_vector),
*(.text),
*(.rodata)
} > FLASH
-
시스템이 부트업 하기 위해서는 가장 먼저 IVT가 실행돼 reset handler 함수가 실행돼야 한다. 그래야 .bss
, .data
section에 정의된 심볼들이 초기화가 되기 때문이다. 따라서 .text
의 최상단에는 .isr_vector
에 대한 포인터가 들어가야 하고, 이 내용은 flash에 읽기 전용으로 기록된다.
4.3. 2단계: 시작 코드
-
부트 과정의 첫 단계는 IVT를 정의하고 포인터를 연결하는 것이다.
GCC의 attribute인 section
을 사용해서 IVT를 정의한다.
__attribute__ ((section(".isr_vector")))
void (* const IV[])(void) {
(void (*)(void))(END_STACK),
isr_reset,
isr_nmi,
...
}
void isr_reset(void) {
while(1) {}
}
void isr_user(void)__weak {
while(1) {}
}
-
MCU에 전원이 들어오면 IVT의 상단에 정의된 reset handler가 가장 먼저 실행된다.
- Reset handler는
.data
와 .bss
section의 초기화를 수행한다!
- RAM의
.bss
section을 0
으로 초기화하고, .data
와 .bss
section을 RAM의 실제 section에 복사하는 작업을 수행한다.
- 초기화 작업 이후 마침내
main()
함수를 호출할 수 있다.
-
링커 스크립터는 VMA (Virtual Memory Address)와 LMA (Load Memory Address) 를 분리하는 메커니즘을 제공한다. 물론 MCU는 MMU가 없어서 모두 physical address로 동작하지만, flash에서 정의된 .data
의 주소들이 실제 RAM의 어디로 매핑될지는 컴파일 타임에 결정할 수 없기 때문에 이 메커니즘은 꼭 필요하다. AT
키워드와 _stored_data
라는 링커 스크립터 변수를 이용한다. (내용이 복잡하고 번역이 이해하기 어려워 이정도만 언급하고 넘어간다.)
4.4. 3단계 : 스택(Stack) 할당
- CPU가 프로그램을 실행하기 위해서는 메모리에 공간을 할당해야 한다.
- IVT의 최상단에는
END_STACK
이라는 미사용 RAM 영역의 끝 주소가 저장돼있다. IVT는 flash에 저장되므로 런타임에 계산될 수 없기 때문에 사전에 정의된 상수값이어야 한다.
- 프로그램을 가장 효율적으로 실행하기 위한 적절한 stack size 할당을 위한 작업은 매우 민감하고 복잡하다.
(다중 부트와 다중 시스템 디버깅에 대한 내용 그리고 .utils
공유 라이브러리에 대한 내용 생략함.)
Chapter 5. Memory Management
5.1. 전체 메모리 구조
ARM Cortex-M의 총 주소 공간은 6개의 큰 구역으로 나뉘며 각 구역은 그 목적에 따라 각기 다른 권한을 가진다. 개발자가 링커 스크립터와 소스코드에서 사용 가능한 주소 공간을 파악하기 위해서는 타겟 플랫폼의 하드웨어의 메모리 섹션과 위치를 data sheet를 통해 올바르게 파악해야만 한다.
- Code region
.text
와 .rodata
section 처럼 읽기 전용으로 flash에 저장됐던 내용들은 이 구역으로 매핑된다. 따라서 IVT에 대한 내용도 여기에 매핑된다.
- 또한,
0
이 아닌 초깃값으로 정의된 심볼도 이 영역에 위치한다. 하지만, 런타임 시에 수정될 수 있기 때문에 이 쓰기 가능한 구역으로 복제 또는 재매핑되야 한다.
- SRAM region & RAM region
- Built-in & External Peripheral region
- System region
- 프로세서를 위한 시스템 제어 레지스터와 주변장치 제어 레지스터 등이 포함된다.
- 프로세서가 previleged level에서 동작할 때만 접근이 가능한 특수 영역이다.
5.2. 스택(Stack) 메모리 구조
우리가 작성하는 모든 펌웨어 애플리케이션은 적정 크기의 스택 영역을 할당받아 실행을 시작한다.
실행 스택은 branch 시 return address와 함수 local 변수가 저장되며 stack pointer는 위에서 아래로 자란다.
우리는 항상 코딩 중 스택 사용량을 인지하며 잠재적 overflow를 염두해야 한다. 따라서 재귀함수처럼 스택 사용량을 예측하고 추적하기 어려운 코드는 작성하지 않는 것이 좋다.
반복해서 말하지만 실행 스택의 stack pointer는 컴파일 타임에 상수로 결정돼서 IVT에 정의된다. 사용 가능한 가장 높은 주소에 위치시킨 다음 낮은 주소로 자라게끔 하는 것이 일반적인 전략이다. 이때 동적 메모리의 heap 영역이 반대 방향으로 자라기 때문에 충돌이 발생할 수 있음을 주의하자. 따라서 최선의 전략은 다른 메모리 섹션과 분리된 적절한 실행 스택 공간을 할당한 뒤 런타임에 스택 사용량을 계속 점검하는 것이다.
필요한 스택 공간의 양을 측정하는 효율적인 방법은 스택 페인팅 (Stack Painting)이라는 메커니즘을 사용하는 것이다.
5.3. 힙(Heap) 메모리 구조
임베디드 시스템은 안전이 가장 중요하기 때문에 일반적으로 어떠한 동적 메모리 할당도 불가능하도록 설계된다. 하지만 동적 할당은 메모리의 lifecycle과 size에 대한 사용자의 완벽한 제어를 주기 때문에 매력적이고 강력해 임베디드 시스템에서도 수요가 높다. 따라서 할당된 메모리의 상태와 크기에 대한 추적을 유지하고 메모리 해제와 재사용에 대한 원칙을 반드시 준수한다면 사용해도 좋으며 디버깅에 소요되는 방대한 시간을 절약할 수 있다.
Heap을 사용할 경우 고려해야 하는 주요 문제는 아래 5가지다.
- 할당된 메모리의 상태와 크기에 대한 추적 유지.
- Heap-Stack 충돌 방지를 위한 heap의 상위 boundary 설정.
- 여유 메모리 부족으로 할당이 불가능할 경우의 정책. (일반적으로는 요청 무시, 지연)
- 메모리의 external fragmentation을 처리할 방법과 최소한으로 유지할 방법.
- Heap 사용 오류에 대한 검사와 처리. (이중 free, free 후 사용, free 미사용으로 인한 메모리 누수 등)
(책에는 각 항목에 대한 조금 더 자세한 내용이 있지만, 번역이 너무 이해하기 어려워 이 정도만 언급한다.)
5.4. MPU (Memory Protection Unit)
MMU가 없어 가상 주소를 사용할 수 없는 MCU에서는 각 memory section 간 구분과 런타임 시 영역 침범을 예방하는 것이 어렵다. ARM은 MPU를 사용해 로컬 권한과 속성을 사용해 section을 명확히 분리한다.
MPU는 5개의 레지스터와 8개의 프로그래밍 가능한 구역을 지원한다. 엄격한 환경설정을 통해 1KB 짜리 guard region을 만들 수 있다. 예를 들어 위 그림처럼 heap과 stack 사이에 MPU로 guard region을 선언하면 충돌을 막을 수 있다.
Chapter 6. General Purpose Peripheral
이번 chapter는 NVIC, clock, tick interrupt & timer, GPIO, watchdog 5가지의 사용방법에 대해 배운다. 간단한 개념과 상세한 코드 상 구현에 대한 내용이 이어지므로 이해가 힘들고 정리가 어려워 개념 위주로 정리하고 넘어간다.
6.1. NVIC (Nested Vector Interrupt Controller)
- ARM Cortex-M 제품군이 유명한 이유는 인터럽트 컨트롤러에 대한 사용자 제어를 허용했기 때문이다.
- NVIC는 사용자 정의 interrupt를 포함한 각 interrupts 사이의 우선순위를 결정해 인터럽트가 연쇄되는 상황에서 interrupt의 지연을 현저히 줄여주는 역할을 수행한다.
- 이미 정의된 우선순위는 런타임에도 변경이 가능하고 각 인터럽트는 서로를 선점하기 때문에 프로그래머의 선택의 자유가 최대한 보장된다는 점에서 매력적이다.
6.2. Clock
프로그램 개발 때 우리는 시간을 계측할 상황을 빈번하게 만날 수 있다. 시간 계측에는 시스템의 clock을 측정하고 사용자가 원하는 시간을 계산할 수 있어야 한다.
- Clock의 환경설정은 RCC(Reset and Clock Control) 레지스터를 통해 가능하다. RCC의 환경설정은 PLL(Phase-Locked Loop) 로직에 따라 천차만별로 다르므로 반드시 datasheet를 참고한다.
- RCC 레지스터를 제어해 clock을 올바르게 설정했다면 ‘Peripheral Clock Source Register’의 대응하는 bit를 설정해서 주변장치를 위한 clock을 활성화할 수 있다.
- 안정된 CPU clock을 설정했다면, SysTick 타이머에 대한 환경설정을 진행한다. RVR(Reload Value Register) 레지스터를 사용하면 CPU clock을 n으로 나눠서 우리가 원하는 타이머 주기를 SysTick에 설정할 수 있다. SysTick의 CSR(Control State Register) 레지스터의 하위 3bit를 설정해서 SysTick을 활성화한다.
이때 timer 변수는 항상 volatile
로 선언한다! 컴파일러는 최적화 동작 때 예측 가능한 동작에 대해 멋대로 값을 변경하기 때문이다! 따라서 최적화를 사용하지 않도록 volatile
선언을 해줘야 한다.
6.3. GPIO
MCU의 GPIO는 몇 가지 그룹(A, B, C, D …)으로 나뉘고 각 그룹과 관련한 레지스터들이 존재한다. GPIO를 사용하고 디지털 출력 또는 입력을 하기 위해서는 이 레지스터 값을 변경하면 된다. (직접 해본 내용들이라 생략한다.)
간단하게 과정을 요약하면 다음과 같다.
- GPIO 그룹을 위한 clock source을 활성화한다.
- GPIO 핀에 대해 입출력 모드를 결정한다.
- GPIO 핀에 대해 풀업/풀다운 여부를 결정한다.
- GPIO 핀에 대해 set/reset bit를 설정해서 값을 출력하거나 입력받는다.
Chapter 7. Local Bus Interface
이번 chapter에서는 대표적인 serial bus protocol인 UART, SPI, I2C에 대해 배운다.
프로토콜 | 와이어 수 | Clock | 비트 순서 | 통신 모드 |
---|
UART | 2개 (TX/RX) | 비동기 | LSB | 1:1, 1:多 |
SPI | 3개 (MOSI/MISO/CLK) | 동기 | MSB/LSB | 1:1, 1:多 |
I2C | 2개 (SDA/SCL) | 동기 | MSB | 1:1, 1:多 |
7.1. UART (Universial Asynchronous Receiver/Transmitter)
- Serial communication을 위해서는 receiver와 transmitter 사이에 반드시 clock 동기화가 이뤄져야 한다. 하지만, UART는 clock 신호를 통해 동기화를 하지 않고, 상호간에 정해진 전송속도(Baud Rate)를 통해 동기화를 구현한다.
(인간적으로 번역 진짜 너무할 정도라 이해가 전혀 안감. 추가 자료조사로 이 부분은 보충해야겠음.)
- UART는 일반적으로
printf()
를 통해 host PC로 디버깅 문구를 출력할 때 사용한다.
- UART는 내부적으로 FIFO 형태 buffer를 사용해서 한 번에 하나 이상의 문자가 쓰여지지 않음을 보장한다. 이때 busy waiting이 발생할 수 있으므로 인터럽트 방식으로 구현하는 것이 cpu time을 낭비하지 않고 시스템 응답성을 유지하는 데 좋다.
7.2. SPI (Serial Peripheral Interface)
- Bus master의 clock 신호를 통한 동기화로 UART보다 빠르며 다수의 주변장치가 같은 버스를 공유할 수 있다.
- SPI 통신의 일련의 과정은 다음과 같다.
- Master는
SPI_CLK
을 활성화한다.
- Master는
SPI_MOSI
에 명령어를 slave로 전송한다.
- Slave는
SPI_MISO
로 데이터를 master로 전송한다.
- Slave로 전송할 데이터가 더 없더라도, clock을 유지하기 위해 master는 slave에게 dummy bytes를
SPI_MOSI
로 계속 보낸다. Slave는 이 dummy bytes를 무시한다.
- Master는
SPI_CLK
를 활성화한다.
- Master는 slave에게
0x8F
라는 명령어를 SPI_MISO
를 통해 보낸다. 이 명령어는 slave(가속도계)의 데이터를 읽는 명령어다. Master가 slave로부터 처음에 SPI_MOSI
로 받은 데이터 0xFF
는 slave의 dummy bytes로 무시한다.
- 명령을 받은 slave가 데이터를 준비하고,
SPI_MOSI
로 0x3B
라는 데이터를 master에게 전송한다. 이때 master는 더 이상 보낼 데이터가 없으므로 slave에게 0xFF
라는 dummy bytes를 SPI_MISO
로 보냈지만, slave는 무시한다.
- SPI 역시 UART처럼 내부 FIFO buffer를 사용하고, busy waiting을 막기 위해 인터럽트 방식을 사용하는 것이 좋다.
7.3. I²C (Inter-Integrated Circuit)
I2C는 전송속도가 느린만큼 저전력 통신이 가능한 serial communication protocol이다. SPI와 공통점이 많지만, 고정 주소로 slave를 선택할 수 있기 때문에 I2C_SDA
핀 하나만으로 통신이 가능하다. 이 과정에 대해서는 후술한다.
I2C는 두 가지 핀을 가지고있다.
I2C_SCL
: master와 slave 사이에 동기화된 통신을 진행.
I2C_SDA
: Address와 명령 그리고 데이터와 ACK/NACK 신호를 주고받음.
I2C 프로토콜 상세는 다음과 같다.
![image-20210808194343540](https://raw.githubusercontent.com/EmbeddedJune/typora_image_storage/main/img/image-20210808194343540.png)
-
START 조건
- 전송의 시작을 알리는 특수 조건이다.
SCL
은 HIGH
로 고정된 상태에서 SDA
가 HIGH → LOW
로 바뀌는 경우를 통신의 시작이라 간주한다.
-
1st frame - Address frame
- 통신은 최소 두 가지 frame으로 구성되며, 첫 frame은 address frame이다.
- Address는 slave 하나를 특정하는 주소이며 7bit로 구성돼있고, 끝에서 2번째 bit는 WRITE(
0
), READ(1
)을 의미한다.
- 마지막
ACK/NACK
bit는 slave에 의해 결정되며 0
이면 ACK, 1
이면 NACK를 의미한다.
-
2nd frame - Data frame
- READ 연산이라면, address에 매칭되는 slave가 데이터를 master에게 보낸다. 마지막에 master는 잘 받았다는 ACK 신호를 slave에게 보낸다.
- WRITE 연산이라면, address에 매칭되는 slave에게 데이터를 보낸다. 마지막에 slave는 잘 받았다는 ACK 신호를 master에게 한 번 더 보낸다.
-
END 조건
- 전송의 끝을 알리는 종료 조건이다.
SCL
은 HIGH
인 체로 SDA
가 LOW → HIGH
로 바뀌면 I2C 통신의 종료를 의미한다.
![image-20210808195528111](https://raw.githubusercontent.com/EmbeddedJune/typora_image_storage/main/img/image-20210808195528111.png)
- I2C의 가장 독특한 특징은 바로 slave의 clock stretching(클럭 늘리기)다.
- Master가 특정 데이터를 요청했는데, slave가 무언가의 이유로 아직 그 데이터를 전송할 준비가 안됐다면, clock stretching을 사용해서
SCL
을 LOW
로 유지해서 트랜잭션을 의도적으로 폴링할 수 있다.
- 하지만 master는 계속해서
SCL
을 HIGH
로 바꾸려고 시도할 것이기 때문에 결국 트랜잭션은 다시 재개(resume)된다.
- 이때 slave가 데이터가 준비됐다면 데이터를 보내고 master는 ACK를,
데이터가 준비되지 않았다면 master는 NACK 신호를 보내고 STOP 조건으로 통신을 종료한다.
- I2C의 이 기능은 전송속도가 느린 주변기기와 통신을 할 때 아주 적절하고 중요하다.
Chapter 8. Low Power Optimization
이 chapter에서 우리는 MCU의 저전력 동작을 위한 방법론과 ARM에서 제공하는 동작모드에 대해서 배운다.
8.1. 저전력 동작을 위한 방법론
1. HW 설계
- Active low 회로와 풀업 저항을 사용해서 전력 낭비를 막는다.
- 누설전류를 최소화 하는 회로를 설계한다.
2.Clock 낮추기
- (대충 지금 고성능 MCU는 70년대 컴퓨터보다 성능 좋다는 이야기.)
따라서 우리가 제작하는 대부분의 펌웨어는 고clock이 필요하지 않은 경우가 많음.
- 미사용 중인 내부 모듈과 인터페이스를 끄는 것이 좋다.
- PLL은 전력을 크게 소모하는 대표적인 모듈이다. PLL을 조절해 CPU clock을 낮추면 전력 소모를 줄이는 데 큰 도움이 된다.
- 애플리케이션의 동작을 세부동작들로 구분한 뒤, 각 동작마다 적정한 clock을 사용하는 것도 방법이다.
3. 전압 제어
- 시스템의 operation voltage의 하한에 가까운 낮은 전압을 인가한다.
- 슈미트 트리거 (Schmitt Trigger)가 작동하지 않도록 확실한 Vdd와 GND를 인가한다.
4. Busy waiting loop를 인터럽트 기반의 sleep mode로 교체
- 무한 루프를 돌면서 특정 조건이 활성화되길 기다리는 것은 CPU time과 전력 면에서 정말 큰 손해다.
- Sleep mode와 인터럽트 트리거를 활용하면 특정 조건이 활성화될 때까지 전력 소모를 매우 크게 줄일 수 있다.
- 정 어쩔 수 없다면 타이머를 사용해서 폴링 주기라도 느리게 하는 것도 한 가지 방법이다.
8.2. ARM Cortex-M의 저전력 운영 모드
1. Normal mode
- 말그대로 일반 작동 모드다. MCU의 기본 클럭 주파수로 동작한다.
2. Sleep mode
- CPU의 clock이 비활성화되며, 주변 장치는 일반적으로 작동한다.
낮은 clock의 코프로세서가 있다면 이때 동작한다.
- CPU가 실행되지 않기 때문에 전력 상당량이 절약된다.
- 인터럽트를 통해 wake-up 된다.
3. Stop mode (=Deep Sleep mode)
- CPU와 Bus의 clock이 비활성화되며, 주변 장치도 모두 꺼진다.
- 주 전압 레귤레이터는 켜져있어서 CPU register와 RAM은 정보를 계속 유지한다.
- 인터럽트를 통해 wake-up 된다.
4. Standby mode
- CPU와 Bus 그리고 주 전압 레귤레이터가 비활성화된다.
- CPU register와 RAM 모두 정보를 잃어버린다.
- μAh 급의 아주 낮은 전력만 사용한다.
- 외부 RTC 또는 하드웨어의 신호를 통해 wake-up 된다.
- Wake-up 됐을 때 IVT의 부팅 과정부터 새롭게 시작한다.
8.3. Wake-up 간격
- 저전력 모드에서 wake-up 돼 실행을 재개하기 까지 걸리는 시간을 말한다.
- sleep mode < deep sleep mode < standby mode 순으로 길어진다.
- 이 시간은 길어도 몇 ms 정도의 짧은 시간이지만, 만약 시스템이 자주 wake-up 된다면 무시할 수 없는 수준이다.
학부생이 쓰신 글이라고 생각되지 않을 정도로 깊이 있는 글이라서 잘 참조하고 갑니다~