아두이노 메모리 관리

    

아두이노 IDE에서 스케치를 업로드할 때 표시되는 "dynamic memory" 사용량은 프로그램이 시작할 때 이미 할당된 글로벌 변수와 정적 변수에 대한 메모리 사용량을 나타냅니다. 이는 프로그램 실행 전에 미리 계산되는 고정된 값입니다.


실행 중에 추가적으로 RAM을 사용하는 것은 다음과 같은 경우에 발생할 수 있습니다:


1. **로컬 변수**: 함수 내부에서 선언된 로컬 변수는 함수가 호출될 때 스택 메모리에 할당됩니다. 함수가 종료되면 이 메모리는 해제됩니다.


2. **동적 할당**: `malloc()`, `calloc()`, `new` 같은 함수를 사용하는 동적 메모리 할당은 실행 중에 RAM을 추가로 사용합니다. 이 메모리는 `free()`나 `delete`를 통해 명시적으로 해제해야 합니다.


3. **재귀 호출**: 재귀 함수는 각 호출마다 스택 메모리를 사용합니다. 너무 깊은 재귀 호출은 스택 오버플로를 일으킬 수 있습니다.


4. **String과 기타 동적 객체**: 아두이노의 `String` 객체는 내부적으로 동적 할당을 사용합니다. 문자열 연산(결합, 추가, 수정 등)은 실행 중에 추가 메모리를 사용할 수 있으며, 이는 메모리 단편화를 일으킬 수 있습니다.


### 실시간 메모리 사용 모니터링

아두이노에서 실시간으로 메모리 사용량을 모니터링하는 것은 기본적으로 간단하지 않습니다. 하지만, 몇 가지 방법으로 현재 사용 가능한 메모리의 양을 추정할 수 있습니다:


- **자유 메모리 계산 함수**: 사용 가능한 힙(Heap)과 스택(Stack) 메모리 사이의 간격을 계산하는 함수를 작성하여 현재 남은 메모리의 양을 추정할 수 있습니다.

- **외부 라이브러리 사용**: 메모리 사용을 모니터링하는 데 도움이 되는 여러 아두이노 라이브러리가 있습니다. 예를 들어, `MemoryFree` 라이브러리 등이 있습니다.


### 주의사항

- 동적 메모리 할당은 메모리 누수와 단편화를 일으킬 위험이 있으므로, 가능한 한 피하는 것이 좋습니다.

- 아두이노와 같은 제한된 메모리를 가진 시스템에서는 메모리 사용을 신중하게 관리해야 합니다.


89%의 동적 메모리 사용은 아두이노와 같은 제한된 메모리 환경에서 상당히 높은 사용률입니다. 이렇게 높은 메모리 사용률은 프로그램의 안정성과 신뢰성에 영향을 줄 수 있으며, 예기치 못한 동작이나 오류를 일으킬 수 있습니다.


### 메모리 사용률이 높을 때 발생할 수 있는 문제들:


1. **메모리 부족**: 동적 메모리 사용률이 높으면 실행 중에 필요한 추가 메모리(로컬 변수, 동적 할당 등)에 대한 여유가 부족할 수 있습니다.


2. **스택 오버플로우**: 함수 호출과 로컬 변수는 스택 메모리를 사용합니다. 메모리가 거의 차 있으면, 스택 오버플로우가 발생할 수 있습니다.


3. **메모리 단편화**: 특히 `String` 객체와 같이 동적 메모리를 사용하는 객체를 많이 사용하면 메모리 단편화가 발생할 수 있으며, 이는 메모리 사용 효율을 떨어뜨립니다.


### 메모리 문제 해결 방법:


1. **전역 변수 최소화**: 필요하지 않은 전역 변수를 제거하거나 최소화합니다.


2. **동적 할당 사용 최소화**: `String`과 같은 동적 할당을 많이 사용하는 객체 대신 `char` 배열을 사용합니다.


3. **로컬 변수 관리**: 함수 내부에서 크기가 큰 로컬 변수를 사용할 때 주의가 필요합니다. 가능하면 전역 변수나 정적 변수로 대체합니다.


4. **PROGMEM 사용**: 상수 데이터를 프로그램 메모리에 저장합니다.


5. **함수 최적화**: 코드를 최적화하여 메모리 사용을 줄입니다. 재귀 호출을 줄이고, 함수의 크기를 작게 유지합니다.


6. **메모리 사용 모니터링**: 프로그램 실행 중 메모리 사용을 모니터링하는 코드를 추가하여, 메모리가 부족해지는 시점을 파악합니다.


7. **하드웨어 업그레이드 고려**: 메모리가 더 큰 아두이노 보드로 전환하는 것도 하나의 방법입니다.


메모리 관리는 특히 아두이노와 같은 임베디드 시스템에서 중요한 고려사항입니다. 효율적인 메모리 사용은 프로그램의 안정성과 신뢰성을 향상시키는 데 중요합니다.


---

초기의 비디오 게임 콘솔들, 예를 들어 초기 닌텐도 시스템들에서는 매우 제한된 메모리와 처리 능력을 가지고 있었습니다. 따라서 게임 개발자들은 이러한 하드웨어 제한 내에서 창의적인 방법으로 메모리 사용을 최적화해야 했습니다.


### 풀과 구름의 그래픽 재사용

- 닌텐도의 초기 게임들에서 풀과 구름이 같은 그래픽을 사용한 것은 대표적인 메모리 절약 기법입니다. 같은 그래픽 타일을 다른 컨텍스트와 색상으로 재사용하여 메모리를 절약할 수 있었습니다.

- 예를 들어, "슈퍼 마리오 브라더스"에서 구름과 풀은 같은 스프라이트를 사용하지만 색상만 다르게 적용되어 있었습니다. 이러한 방법은 매우 제한된 메모리를 효율적으로 사용할 수 있도록 해줬습니다.


### 초기 게임 개발에서의 메모리 최적화

- 초기 게임 콘솔은 KB 단위의 RAM을 가지고 있었기 때문에, 그래픽, 사운드, 게임 로직 등 모든 것을 매우 제한된 메모리 안에서 처리해야 했습니다.

- 개발자들은 그래픽 타일 재사용, 메모리에 있는 데이터의 효율적인 사용, 그리고 알고리즘 최적화와 같은 다양한 기법을 사용했습니다.


### 개발자의 창의성

- 이러한 제한은 개발자들로 하여금 창의적이고 효율적인 방법을 찾도록 했습니다. 결과적으로 이러한 제약은 게임 디자인과 프로그래밍 기술의 발전에 중요한 역할을 했습니다.


아두이노와 같은 제한된 환경에서 작업할 때도 비슷한 원칙이 적용됩니다. 메모리와 처리 능력의 제한은 최적화와 효율적인 코드 설계를 필요로 하며, 때로는 창의적인 해결책을 요구합니다.


## `snprintf`

`snprintf`는 C 언어의 표준 라이브러리에 포함된 함수로, 문자열을 포맷팅하고 지정된 크기의 버퍼에 저장하는 데 사용됩니다. 이 함수는 `printf`와 유사한 포맷팅 기능을 제공하지만, 출력을 콘솔이나 터미널로 보내는 대신 문자 배열(즉, 문자열 버퍼)에 저장합니다.


### `snprintf`의 기능과 사용법

1. **포맷팅**: `snprintf`는 다양한 데이터 타입(정수, 부동 소수점, 문자열 등)을 문자열로 변환하고, 이를 지정된 포맷에 맞추어 저장합니다.

2. **버퍼 크기 제한**: 이 함수는 지정된 버퍼의 크기를 초과하지 않도록 보장하여, 버퍼 오버플로우를 방지합니다.

3. **반환 값**: `snprintf`는 포맷팅된 문자열의 전체 길이를 반환합니다. 이는 버퍼에 저장되지 않은 문자의 수를 포함할 수 있습니다.


### 사용 예시

```cpp

char buffer[50];

int value = 123;

float number = 45.67;

snprintf(buffer, sizeof(buffer), "Value: %d, Number: %.2f", value, number);

```


위 예시에서, `snprintf`는 `buffer`에 "Value: 123, Number: 45.67"라는 문자열을 저장합니다. `%d`는 정수를, `%.2f`는 소수점 아래 두 자리까지의 부동 소수점 수를 의미합니다. `sizeof(buffer)`는 `buffer`의 크기를 바이트 단위로 지정하며, 이는 `snprintf`가 `buffer`의 크기를 넘지 않도록 보장합니다.


### 안전한 문자열 처리

- `snprintf`는 버퍼 오버플로우를 방지하기 때문에, 문자열을 안전하게 처리하는 데 유용합니다.

- 크기가 고정된 문자열 버퍼에서 문자열 조작이 필요한 경우 특히 권장되는 방법입니다.


int 대안


아두이노와 같은 임베디드 시스템에서 메모리 사용량을 줄이고 싶을 때 `char` 이외에도 다른 데이터 타입을 고려할 수 있습니다. `int` 타입 대신 사용할 수 있는 대안은 주로 작은 크기의 데이터 타입입니다.


### 1. `byte` 또는 `uint8_t`

- `byte` 또는 `uint8_t`은 부호 없는 8비트 정수형으로, 0에서 255까지의 값을 저장할 수 있습니다.

- 메모리를 1바이트만 사용합니다.

- 예: `byte myByte = 255;`, `uint8_t myUint8 = 200;`


### 2. `short` 또는 `int16_t`

- `short` 또는 `int16_t`은 16비트 정수형으로, -32768에서 32767까지의 값을 저장할 수 있습니다 (부호 있는 경우).

- 메모리를 2바이트 사용합니다.

- 예: `short myShort = 12345;`, `int16_t myInt16 = -10000;`


### 3. `uint16_t`

- `uint16_t`은 부호 없는 16비트 정수형으로, 0에서 65535까지의 값을 저장할 수 있습니다.

- 메모리를 2바이트 사용합니다.

- 예: `uint16_t myUint16 = 60000;`


### 4. 비트 필드 사용

- 여러 개의 불리언 값이나 작은 범위의 값들을 저장할 때, 비트 필드 또는 비트 연산을 사용할 수 있습니다.

- 예: `unsigned int flags : 3;` (3비트만 사용하는 정수형)


### 5. 구조체와 비트 연산

- 여러 변수를 하나의 구조체에 묶고, 각 변수에 비트 수를 지정하여 메모리 사용을 최적화할 수 있습니다.


### 주의사항

- 사용하는 데이터 타입이 변수가 저장해야 할 값의 범위를 적절히 커버할 수 있는지 확인하세요.

- `char`와 달리 `byte` 또는 `uint8_t`는 부호 없는 정수형이므로 음수 값을 저장할 수 없습니다.

- 메모리 사용량을 줄이려고 할 때 프로그램의 정확성과 안정성을 저해하지 않도록 주의해야 합니다. 데이터 타입의 선택이 프로그램 로직에 영향을 미칠 수 있습니다. 


이러한 대안들을 통해 메모리 사용을 최적화할 수 있지만, 사용하는 변수의 값 범위와 프로그램의 요구 사항에 따라 적절한 타입을 선택하는 것이 중요합니다.


아두이노에서 프로그래밍을 할 때 PROGMEM (프로그램 메모리)에 있는 변수나 문자열을 가져오는 것은 일반적으로 SRAM (정적 RAM)에 있는 변수를 가져오는 것보다 더 많은 시간이 걸릴 수 있습니다. 이는 프로그램 메모리와 SRAM 사이의 데이터 액세스 속도 차이 때문입니다.


메뉴 텍스트 문자열을 PROGMEM에 넣는 것은 특히 SRAM이 제한적인 경우에 유용할 수 있습니다. 아두이노 Uno와 같은 작은 메모리 용량을 가진 보드에서는 SRAM을 효과적으로 활용하기 위해 PROGMEM을 사용하는 것이 좋습니다.


그러나 PROGMEM을 사용할 때 주의해야 할 몇 가지 사항이 있습니다. PROGMEM에 저장된 데이터를 읽고 사용하기 위해서는 일반적으로 `pgm_read_byte`, `pgm_read_word`, `pgm_read_dword` 등의 함수를 사용해야 합니다. 또한 PROGMEM에 저장된 데이터를 수정하려면 추가적인 작업이 필요합니다.


메뉴 텍스트 문자열과 같이 변경되지 않는 데이터를 저장하거나 대용량의 문자열을 처리해야 하는 경우 PROGMEM을 사용하여 메모리를 효율적으로 관리하는 것이 좋습니다. 그러나 실행 속도와 편의성도 고려해야 합니다. 데이터 액세스 속도가 중요하거나 코드가 복잡할 경우 PROGMEM을 사용하는 것이 유용할 수 있지만, 간단한 프로젝트에서는 SRAM을 사용해도 문제가 없을 수 있습니다. 프로젝트의 요구사항에 따라 적절한 방식을 선택하는 것이 중요합니다.


`pgm_read_byte`, `pgm_read_word`, `pgm_read_dword`는 PROGMEM에 저장된 데이터를 읽어오는 데 사용되는 함수입니다. 이 함수들은 데이터 타입에 따라 다르게 사용됩니다. 각 함수의 주요 차이점은 데이터 타입의 크기와 읽어올 데이터의 바이트 수입니다.


1. `pgm_read_byte`:

   - 데이터 타입: `uint8_t` (바이트, 1 바이트)

   - 주로 8비트 또는 작은 데이터 타입을 읽어올 때 사용됩니다.

   - 반환 값: 읽어온 바이트 데이터를 `uint8_t` 형태로 반환합니다.


2. `pgm_read_word`:

   - 데이터 타입: `uint16_t` (워드, 2 바이트)

   - 16비트 데이터를 읽어올 때 사용됩니다. 보통 정수, 주소 또는 문자열의 인덱스를 읽을 때 활용됩니다.

   - 반환 값: 읽어온 워드 데이터를 `uint16_t` 형태로 반환합니다.


3. `pgm_read_dword`:

   - 데이터 타입: `uint32_t` (더블 워드, 4 바이트)

   - 32비트 데이터를 읽어올 때 사용됩니다. 큰 정수 값이나 주소를 읽을 때 활용될 수 있습니다.

   - 반환 값: 읽어온 더블 워드 데이터를 `uint32_t` 형태로 반환합니다.


따라서 데이터 타입과 읽어올 데이터의 크기에 따라서 `pgm_read_byte`, `pgm_read_word`, `pgm_read_dword` 중 적절한 함수를 선택하여 PROGMEM에 저장된 데이터를 읽어와야 합니다. 이러한 함수를 사용하면 프로그램 메모리에 저장된 데이터를 SRAM으로 복사하지 않고도 데이터를 읽어올 수 있으므로 메모리 효율성을 높일 수 있습니다.








댓글

이 블로그의 인기 게시물

js 스트링에서 요소 갯수 세기

STUDY

javascript cheatsheet