태그 보관물: C와 C++

[RPI-PICO] 오디오 쥬크박스 만들기 2

이전글들

우선 하드웨어 구성은 아래와 같습니다.

Waveshare Pico-Audio original revision
Waveshare Pico-LCD-1.44
Raspberry Pi Pico
microSD Card Adapter
Waveshare Expander Quad

이중에서 Pico-Audio는 I2C 연결
Pico-LCD-1.44는 SPI 연결
microSD Card Adapter도 SPI 연결이구요

라즈베리파이 피코가 SPI 채널이 두개라, 잘 안겹치게 배정해서 소스코드에 기재하면 됩니다. Expander를 쓰기 때문에 하드웨어적으로는 일단 조정을 잘 안해도 되고 다만 microSD Card Adapter의 경우 핀 구조상으로 Expander에 못끼우기에 점퍼케이블을 female to female로 연결해야 하네요.

본래 Expander Dual을 썼는데 모듈을 하나 더 달아야 해서 Quad로 바꾼 것도 특기해야 합니다.

일단 기존의 샘플 코드를 분석해서 흐름을 꿰찰려고 했는데요. Pico-LCD-1.44의 네개 버튼 시연 코드를 일부 떼어내어 가져와서 메뉴를 구성하고 키값을 구해서 눌린지 확인하는 조건문 코드 안에 Pico-Audio 시연 코드에 들어있는 톤 재생 코드를 두니 버튼 누르면 재생이 되었습니다.

그러나 같은 키를 한번 더 누르면 멈추게 하는 것을 살펴봐야 했고, 이는 제작사에서 제시한 라이브러리로 가능한데요. 음질의 경우 톤으로 내는 것은 소스코드에 텍스트 정보로 톤 정보로만 저장되어 재생되는 구조라 음질이 조금 좋은 부저 정도라, MP3 정도는 되어야 하기에 그만두고 microSD Card Adapter를 구해서 연결해보았습니다. microSD 카드에 MP3를 저장해서 불러오는 용도였죠.

microSD 카드는 킹스턴 제품으로, SDHC class 10 UHS-I 지원 제품인데요. 어떤 이유로 인해 FatFS를 사용하는 모든 기존의 프로젝트에서 f_mount() 실행시 error: physical drivce does not work 같은 오류가 나면서 마운트에 실패합니다.

몇가지 이상 증상은

  • LED 점멸로 상태작동을 보여주는 기능이 작동하다가 안하다가 하는데 무조건 마운트 실패
  • BOOTSEL 연결시 PC에 30초 가량 딜레이가 발생하면서 마우스 커서 버벅임
  • 라즈베리파이 피코 내장 플래시가 2MB인데, 무려 127MB로 60배가 뻥튀기되어 보임
  • 아무런 파일도 기록이 안되던 microSD 카드에 이상한 파일과 디렉토리가 발견

인데요.

뭔가 이상해서 macOS에서도 해보니 똑같이 f_mount()에서 안돕니다.

아무래도 FatFS에서 내부적으로 쓰는 코드 중에 SPI 핀과 SD 카드 명세는 잘 해놨어도 ff.c 같은 파일에서 지정하는 매크로 상수를 튜닝해야 제가 쓰는 microSD 카드와 호환된다는 것 같았는데 위 증상을 보면 석연치 않지만, 일단은 프로젝트 저자들도 SD 카드 브랜드가 문제라고 하기도 하네요. 잘 생각해보면 브랜드마다 사양도 다르고, 특히 블락 크기 설정 같은 설정도 잘 살펴서 해야 할 것 같습니다.

그래서 우선 재껴두고 집에 있는 USB 메모리를 달아 해보려고 하는데요. 라즈베리파이 피코의 USB 단자에 USB 메모리를 장착하고, Expander Quad에 별도로 있는 USB 단자로 전원공급을 해서 해볼 생각입니다. 라즈베리파이 피코를 MSC 호스트로 두고 해보는 것이죠. TinyUSB로 비슷한 작업을 하는게 되던데 잘 살펴보고 있습니다.

일단 USB 표준부터 보고 있고 단행본 입수전에 연구논문과 학위논문을 찾아서 일반적인 해설을 한 부분을 집중해서 보려고 하네요.

FatFS가 작동하면 128kbps 44.1kHz MP3 파일을 네개 저장해서 불러와서 LCD의 키로 메뉴 선택후 I2S로 재생하는 것을 할 것입니다.

FatFS로는 일단 집에 있는 USB 메모리로 해보고, 시간이 가면 microSD 카드를 최대한 작은 용량으로 오래전 사양으로 맞추어보려고 합니다. 둘다 안되면 FatFS 소스코드를 뒤적여서 하드웨어 명세를 맞추어줘야죠.

오디오 재생은 제가 쓰는 Pico-Audio original revision이 PCM5101A를 디코더로 쓰는데요. 32비트 384kHz까지 감지한다고 되어 있고 다이나믹 레인지가 106dB입니다. 44.1kHz 128kbps MP3 재생에 지장이 없습니다. 라즈베리파이 피코 C SDK에 포함된 USB 사운드 카드 모드로 돌려보면 음질이 좋습니다.

일단 USB MSC부터 살펴보면서 공부한 것을 정리해서 올리겠습니다.

[RPI-PICO] 라즈베리파이 피코의 USB 단자를 통한 Virtual COM Port 시리얼 모니터 사용법

vscode에 Raspberry Pi Pico 확장기능이 잘 설치되어 있다는 가정 하에 진행합니다. (특히 Serial Monitor)

우선 CMakeLists.txt에 아래 코드를 추가합니다

이 코드는 USB에 stdio를 허용하고 UART에는 비허용하는 코드입니다.

그리고 C 코드의 main() 함수에 아래 코드를 추가합니다.

이렇게 해두고 컴파일해서 uf2를 라즈베리파이 피코에 심습니다.

vscode에서 시리얼 모니터를 열고 Toggle Sent Message Echoing 버튼을 누르고 Start Monitoring 버튼을 누릅니다. 그러면 라즈베리파이 피코가 USB에 연결된 상태라면 BOOTSEL이든 일반 연결 모드든 위에 printf()문으로 보낸 정보가 시리얼 모니터에 뜹니다.

처음 연결했다면 printf() 문이 이미 실행되고나서 시리얼 모니터가 켜졌을때 표시가 안될 수 있으니, Toggle Sent Message Echoing을 켜고 Start Monitoring 상태에서 USB를 뺏다 꽂으면 됩니다.

이를 잘 활용하면 함수 실행 결과를 받아와서 조건문으로 검사하고 오류가 난 것을 보여줄 수 있습니다.

[RPI-PICO] 오디오 쥬크박스 만들기 1

라즈베리파이 피코로 쥬크박스를 만들어보려고 합니다. 하드웨어 구성은 살펴볼 필요가 없이 아래처럼 조합했습니다.

라즈베리파이 피코 (RP2040)
Waveshare Pico-LCD-1.44
Waveshare Pico-Audio
Waveshare Dual GPIO Expander

이구요.

https://www.waveshare.com/wiki/Pico-LCD-1.44
https://www.waveshare.com/wiki/Pico-Audio

에서 사양과 해설이 나옵니다. 라이브러리도 제공되네요.

LCD 모듈은 SPI 연결이고 TFT-LCD라고 되어있구요. 스위치가 제공되어 메뉴에서 항목 선택시 해당 기능을 실행하게 하는게 가능합니다. 쥬크박스 메뉴 표시와 선택기로 쓸 것이구요. 라이브러리가 잘 되어 있어서 LCD 구동후 표시한 메뉴에서 스위치문으로 신호를 받아 메뉴가 구현됩니다.

오디오 모듈은 I2C 연결이구요. PCM5101A 디코더가 탑재되어 있습니다. 32비트 384kHz의 사양에 다이나믹 레인지가 106dB이네요. 오디오 모듈 상품에 스피커가 포함되어 있습니다.

둘다 전원은 따로 연결하지 않고 라즈베리파이 피코와 연결된 핀으로 받는 것 같은데 자세한 것은 생략합니다.

우선 PCM 원리를 해설하고 들어가겠습니다.

우리 주변의 소리는 아날로그입니다. 음악소리가 스피커에서 나온다든지, 천둥 소리가 들린다는 것은 아날로그 형태의 음압이 발생해서 사람의 귀로 들어가 뇌가 인식하는 것입니다.

음압을 매질이 진동한다고 보면 파형이 되어 그래프처럼 표현하는게 됩니다.

아날로그 소리는 그래프가 매끄럽게 연결된 상태로 그려지구요. 이를 디지털 기기에서 처리할때는 샘플링이라고 해서 표본값을 얻어내서 좌표에 점찍고 처리가 되는 것으로 유비가 됩니다.

우리가 수학시간에 배웠던 것처럼 그래프를 그릴때 함수값에 따라 얻어진 변화값을 구해서 그래프 용지에 찍고나서 이들을 연결하라고 하죠? 이 연결 전의 점을 얻어내는 변화값 추출이 샘플링이고, 이를 이어주는 것이 고음질로 되는 비법입니다.

진폭을 Y축으로 시간을 X축으로 두고 파형을 그렸을때 1초의 소리 신호를 Y축에 따라 높낮이가 그려지는데요. 이때의 소리 신호를 1초에 몇개의 샘플로 얻어내는지에 의해 그래프가 더 매끄럽게 되듯이 샘플링 레이트가 중요한 사양이 됩니다.

즉 1초에 44100개의 샘플이 가능하면 샘플링 레이트는 44.1kHz가 되고 48000개로 가능하면 48kHz가 되죠. 이는 디지털화되었을때 아날로그값이 손실되는 정도를 줄여주고, 음질도 향상시켜줍니다. 그래서 오디오 CD와 DVD-Audio를 구분하기도 하네요.

이와 함께 몇비트라고 할때는 양자화 단계를 의미합니다. 8비트 양자화가 되면 2의 8승인 256 단계가 가능해지구요. 16비트는 2의 16승, 32비트는 2의 32승이 됩니다. 비트는 두가지 값만 가능하니 전체 가능한 단계가 비트로 표현되면 2의 멱수가 됩니다. 이역시도 비트수가 높아지면 정밀한 파형이 되어 음질이 좋아지게 되죠.

이를 펄스로 다룬다고 해서 pulse구요.

샘플이 취해지고 양자화가 이루어지면 각 샘플에는 이진수가 주어집니다. 16비트라면 0000 0000 0000 0000에서 1111 1111 1111 1111가 가능해지는데 이게 코드(code)입니다.

즉 PCM(pulse code modulation)은 소리를 펄스화해서 코드로 바꾸는 변조라는 의미입니다.

이는 ADC(Analog to Digital Converter)로 자연상태의 소리를 디지털화하게 되구요. DAC(Digital to Analog Converter)를 쓰면 음악파일을 스피커로 출력하는 모듈에 채택이 됩니다. 둘다 가진 모듈이 있고 하나만 가진 모듈도 있는 것 같습니다.

사양적으로 32비트, 384kHz를 제공하는 경우에는 이 전체 사양을 다 만족하는 오디오 데이터라고 해도 늘 이 전체를 다 쓰는건 아니구요. 다이나믹 레인지와 필터링, 인터폴레이션, 밴드 리미트 등의 처리를 해야 되는 알고리즘의 특성상, 사양 그대로보다는 입력 데이터와 처리 알고리즘의 특성에 의해 다 쓰이는 것은 아니죠.

다이나믹 레인지는 보통 6dB 마다 1비트씩 는다고 보면 된다는데, 106dB인 경우 대충 17.6 비트네요.

필터링, 인터폴레이션, 밴드 리미트와 같은 기술은 파형으로 다루는 소리 데이터에 노이즈를 적게 하는 용도로도 쓰이고, 파형 자체를 증폭하거나 커트해야 할 필요에 의해 제정된 기술인데 이게 제작사의 기술력과 관련이 있네요.

PCM5101A 데이터시트에 나온 사양도 이로부터 이해가 됩니다.

일단 이론 공부는 대충 이렇게 해두구요. 조만간 코딩도 해서 올려보겠습니다.

제작사에 문의해보니 제가 구한 제품이 rev2.1이라던데 다시 확인해보니 오리지날 리비전으로 밝혀졌습니다.

rev2.1은 시러스로직의 CS4344를 디코더로 쓰고 오리지날 리비전은 텍사스 인스트루먼트의 PCM5101A를 쓰는데요. 이둘이 거의 같아보여도 후자가 사양이 좋습니다. CS4344는 32비트 192kHz까지 감지가 되는 기종이고 PCM5101A는 32비트 384kHz까지 감지가 되는데요. 다이나믹 레인지가 106dB이니 출력되는 음질은 비슷할 수도 있습니다. 물론 여러 변인이 존재하니까요.

리비전 문제로 인해 며칠 확인작업을 했는데, 전에 쓴 글에 문의를 다시 보낸다고 언급했으나 새로 쓴 글에서 언급을 안해두어 인상이 나빠질 듯하여 추가해둡니다.

전산학에서 추상화 기술의 역할

전산학은 나날이 발전해서 1990년대의 멀티미디어가 주목받던 시절에 32 poly 출력이 가능한 사운드카드 기능은 이제는 9만원되는 메인보드에 내장된 형태로 공급이 됩니다. 이제는 4K를 처리하는 영상 인코더나 기본으로 16GB램을 요구하는 CG 처리 소프트웨어, 수백와트의 전력을 소모하는 그래픽 카드까지 아주 높은 성능의 멀티미디어 기술이 개발되어 일반화된지 오래입니다.

이들 작동은 물리적인 수준에서 들여다보면 아주 단순한 작동인데요. 컴퓨터와 주변기기가 작동하는 가장 기저의 반응은 두가지 상태밖에 없습니다. (1) 전류가 인가되었다. (2) 전류가 해제되었다. 이 두가지 상태가 반복적으로 나타남으로써 특정한 기능이 작동됩니다. 이 특정한 기능을 나타내는 상태의 연쇄는 임의적이구요. 예를 들면 전류가 두번 인가되고 두번 해제되면 (1′) 데이터를 쓰라, 반대로 전류가 두번 해제되고 두번 인가되면 (2′) 데이터를 읽어라 라고 개발자가 정의했다면 그렇게 작동하도록 하드웨어가 제작됩니다.

그런데 실재로 처리할때는 (1)과 (2)만으로는 안되고, 단위 시간당 예컨데 1초 동안 50억번의 연산을 수행한다면 아주 지루하고 긴 전류의 흐름만을 보게 됩니다.

사람이 다루어야 할 필요가 있었기에 초창기 디지털 컴퓨터들은 이진법을 도입합니다. 컴퓨터가 처리하는 상태는 두가지 이므로 숫자를 두가지 숫자로 처리하는 이진법을 도입해서 0은 전류가 안통하는 상태, 1은 전류가 통하는 상태로 정해서 (1′)은 1100 (2′)는 0011로 표기하자고 정했습니다. 그래서 컴퓨터가 작동하는 명령어나 데이터들을 나타내는 이진법 수는 000010101111010101010110 등으로 매우 길고 복잡합니다.

컴퓨터가 처리할 수 있는 데이터의 뭉치가 커지면서 64비트 CPU가 장착되면 2의 64승개의 경우가 표현됩니다. 이 거대한 수를 0과 1로만 나타내면 매우 길어서 지면이 낭비됩니다. 다 읽고 기억도 안되구요. 그래서 메모리를 구간으로 나누어 주소를 부여하고 그 주소는 16진수로 나타내기로 하게 되었죠. 7비트 메모리라면 0x0, 0x1, 0x2, 0x3, … 0x6까지 7개로 표현이 되고 이게 16GB 램으로 되면 더 길지만 이진수로 표현한 것보다는 짧은 형태로 메모리 주소가 표현되게 됩니다.

프로그래밍의 초창기에는 아주 복잡한 처리는 필요가 없었고, 메모리 특정 비트나 레지스터에서 값을 저장하고 꺼내와서 연산을 시키는 것만으로도 유의미한 작동이 보장되었습니다. 그래서

https://shutterpress.info/pc/2023/04/16/c-%ec%96%b8%ec%96%b4-%ec%86%8c%ea%b0%9c/

에서 소개한 것처럼

와 같은 어셈블리 언어로 번안이 되도록 했습니다. 이는 이진법으로 표기하면

처럼 되는데요. 컴파일하면 이런 형태로 저장이 되게 됩니다. 컴퓨터가 즉시 이해가능한 형태라고 흔히 말합니다.

하지만 현대 소프트웨어 처리 요구사항처럼 몇백개의 폴리곤을 표현한다든지, 4K 영상을 무압축으로 캡처받아 처리하려면 저위의 코드만으로는 매우 지난한 처리가 필요하고 가능하지도 않습니다. 그래서 더욱 더 번안을 해서 사람이 알아보기 쉽게 만든 코드로 보다 더 고차원의 번안(컴파일)이 가능한 언어들이 개발되어 지금도 쓰이고 있습니다.

와 같은 C 언어는 위의 명령과 유사합니다. 하지만 알아보기 쉽고, 조합의 가지수가 더 다채롭습니다. 이 장점의 의미는 보다 더 추상화가 고도화된 체제에서 프로그래밍이 가능해지고 더 견고하고 구조화된 작업이 가능하게 된다는 것입니다.

여기서 더 추상화가 진행되면 자료구조, 알고리즘, 디자인 패턴, MVC 이론 등등이 고안이 되죠.

정리하면

(a) 하드웨어의 작동은 전류의 인가와 해제 둘뿐이다
(b) 이진법과 16진법으로 (a)를 표현할 수 있다
(c) 프로그래밍은 (a)와 (b)을 추상화한 것으로 이해도 쉽고 결과물도 구조화되게 해준다
(d) 추상화의 수준이 올라가면 각종 프로그래밍 방법론으로 발전한다

이 두가지구요. 이들의 관련성을 알면 전산학을 배울때 그냥 넘어감없이 찜찜하지 않게 학습이 됩니다. 이러한 개념을 소개하는 책을 자주 읽고 여러권 참고하면 좋습니다.

C 언어의 함수 실행과 메모리에서 있는 일들 1

프로그램이 실행되면 메인 메모리에서 코드 세그먼트, 데이터 세그먼트, 스택, 빈 공간, 힙 메모리로 로드된다. 함수를 실행하면 스택에서 작동이 이루어진다. 스택은 LIFO(Last In First Out) 구조로 나중에 집어넣어진 것이 제일 먼저 빼내어지는 구조다. 이 구조가 필요한 이유는 함수 실행시 호출 순서에 따라 먼저 실행된 함수가 대기상태에 있고 나중에 실행된 함수가 먼저 실행되어야 하는 C 언어의 절차형 실행 방식 때문이다.

아래와 같은 소스코드가 실행된다고 생각해보자.

모든 C 프로그램은 main()부터 실행하므로 실행직후의 스택 메모리 상태는 아래와 같다.

함수를 실행할때마다 스텍 메모리에 스택 프레임이 만들어진다. 매개변수는 함수에 넘겨진 변수로 위의 예제에서는 void가 왔으므로 아무런 매개변수가 없다. 지역 변수는 피호출 함수 내부의 변수로 a, b, sum에 해당한다. 리턴주소는 함수 실행후 실행할 바로 다음 주소다. 4바이트 명령으로 실행되는 체제라면 2036번지에 main(void)가 적재되고 끝나면 2040번지의 명령어를 실행한다. 리턴값은 예제에서는 0이다.

main() 함수에서는 add() 함수를 호출한다. 이미 main() 함수가 실행중이었다면 main() 함수 실행이 잠시 멈추고 add() 함수가 스택 메모리에 적재된다. 아래와 같이 얹혀진 것으로 그려볼 수 있다.

스택은 LIFO 구조이므로 add()의 작업이 우선 실행된다. 실행이 마쳐지면 다시 main() 함수로 돌아와 남은 라인을 실행한다.

메모리 번지 체제에서는 PC(Program Counter)에 다음에 실행할 명령어 주소가 저장되어 있을때 그 주소가 1008번지라면 현재 실행 주소는 1004번지다. 1004번지 실행할때 1016번지를 점유하는 함수를 실행한다면 PC 레지스터 값을 1016으로 바꿔야 하는데, 이 경우 그전에 스택 프레임의 리턴 주소란에 1008번지를 미리 저장해두어야 복귀후 원래 하던 일을 이어서 할 수 있다.

스택 프레임의 중요한 기능은 변수의 유효 영역의 제한이다. 보통 실행 스코프라는 말을 하는데, add()가 실행중이라면 f와 s, total에만 접근할 수 있고, main()의 a, b, sum에는 불가능하다. 어느 순서의 스택 프레임인지에 따라 사용 가능한 변수가 제한된다. 그래서 다음과 같은 구조에서 main()과 add()의 변수들은 실행되는 스코프가 다르다.

add()가 실행을 마침과 동시에 팝 되어 다시 main()이 실행된다.

스택은 함수 실행마다 늘었다 줄었다 하면서 메모리 사용을 효율적으로 관리한다. 만약 무한루프가 걸리면 스택 프레임이 무한정 쌓여서 힙 메모리 영역과 맞닿게 되어 가용 메모리가 없게 되기도 한다. 이를 스택 오버플로우라고 한다.

이 글은 주우석 선생님의 저서를 보고 요약식으로 정리하였습니다.

변수 원리와 메모리에서 있는 일들 3 – 값 호출, 참조 호출

함수 호출시 매개변수로 원본을 넘겨주는 방식을 참조 호출( call by reference)라고 하고 사본을 넘겨주는 방식을 값 호출(call by value)라고 한다. C에서는 함수 호출시 매개변수로 변수를 넘기면, 그 변수 자체가 아니라 그 변수의 값이 복사되어 피호출 함수로 전달된다. 그래서 피호출 함수가 작동을 마치면, 호출을 실행한 함수의 변수는 값이 그대로 유지된다.

이 이유는 스택 프레임의 작동 방식 때문이다. 피호출 함수에 변수가 매개변수로 전달되면 넘긴 변수의 값만 복사되어 피호출 함수에 전달된다. 그래서 피호출 함수에서 그 값을 바꾸더라도, 스택 프레임의 특징인 변수 유효 범위가 피호출 함수 내부로 국한되기 때문에 다시 호출을 실행한 함수로 돌아오면 바꿔진 유효성이 사라지는 원리인 것이다.

C 언어에서 값 호출이 유일한 이유는 함수끼리의 독립성을 지키기 위해서다.

리턴값도 비슷한 원리로 복사된 값을 넘긴다.

리턴값은 하나만 전달되는데 전역 변수를 쓰면 리턴값이 하나만 넘겨져도 바뀐 값이 효력이 있게 된다.

그러나 한번에 하나의 값만 리턴되므로 이 방법이 아니라면, 구조체나 포인터로 넘기는 방법이 있다. 구조체로 여러 변수를 선언하여 구조체 전체를 넘기거나, 포인터로 넘기면 된다.

변수 원리와 메모리에서 있는 일들 2

변수의 영역 또는 변수 스코프는 변수가 영향을 주는 유효 범위를 말한다. 지역 변수의 예를 들면 그 변수를 선언한 블록 내에서만 영향을 준다.

위 코드에서 add() 함수는 main() 함수의 c에 접근할 수 없다. 이는 스택 프레임의 구조상 c에 접근하지 못하기 떄문이다. 아래와 같은 경우에도 마찬가지다.

main()과 add()에서 쓰이는 a와 b라는 변수는 서로 다른 유효 영역에서 실행되므로 독립적이다. 이 경우 구별을 위해 서로 다른 이름으로 선언하기도 하지만, 수식의 일관성을 위해 같은 이름으로 선언해두기도 한다. 서로 다른 함수에서 지역변수를 같은 이름으로 지정하더라도 별개의 변수가 되는 것이다.

모든 함수에서 사용이 가능하고 프로그램 종료전까지 유효한 변수를 선언하려면 전역변수 문법을 쓴다. 전역변수는 모든 함수 블록 바깥에 최외곽에 선언한 변수다. 아래와 같은 예제를 보자.

위 예제에서 sum의 변화는 sum_up()에서 주어졌지만, main()에서 printf() 함수로 찍어보면 그 변화를 호출한 main()에서도 유효한 접근이 가능하다. 전역변수로 선언되었기 때문이다.

함수끼리 변수에 대해 상호작용을 하려면 아래와 같은 방법들이 제공된다.

(1) 함수 매개변수를 넘긴다
(2) 리턴값을 받아온다
(3) 전역 변수를 쓴다

한편 스택 프레임의 작동이 될때 스택 프레임과 함께 자동으로 생성되고 소멸하는 변수를 자동 변수라고 하고, 그반대로 프로그램 시작과 함께 생성되어 프로그램이 끝날 때까지 소멸하지 않는 변수를 정적 변수(static variable)이라고 한다. C에서는 아래 구문대로 선언된다.

위의 예제에서 나온 것처럼 sum 변수는 sum_up() 내부에서 선언되었으나 static 지정자에 의해 선언되어 main()에서도 접근이 가능하게 되었다. 이를 잘쓰면 프로그램 구조적인 문제와 변수의 유효 범위를 조정할 수 있다.

변수 중에서 C 언어에서 존재하는 선언 지정자로 register도 있다. register 지정자를 붙여 선언하는 변수는 레지스터 변수인데, 메인 메모리가 아닌 CPU 레지스터에 저장하라는 의미다. 자주 사용된다면 레지스터 변수로 선언해볼만 하다.

때로는 상수 변수가 필요할 때가 있다. 상수 변수는 const 지정자로 선언한다.

이라고 선언해두면 a 값은 프로그램 종료때까지 10으로 고정된다. 매크로 상수와 다른 점은 매크로 상수는 치환의 의미로 프로그램 전체에 영향을 주지만, const 상수 변수는 변수 유효 영역 규칙에 따라 지역 변수처럼 작동한다는 것이다.

이번글도 일단 주우석 선생님 저서에서 읽은 내용을 요약해서 정리해두었다. 어려운 문법 사항은 아니지만 일단 구체적으로 정리해두는 것에서 의미를 찾는다.

변수 원리와 메모리에서 있는 일들 1

C 언어의 변수 처리에 대해 알려면 컴퓨터의 메모리에서 있는 일들을 알면 좋다.

현대적인 디지털 컴퓨터는 폰 노이만 구조를 따른다. 폰 노이만 구조에서 핵심적인 부분은 CPU와 메모리다. CPU는 컴퓨터의 두뇌 역할을 하고 메모리는 해마와 같은 기억에 관련된 역할을 한다. 모든 데이터는 CPU가 처리에 관여하고 메모리에 불러져와서 그에 맞는 기능을 수행한다.

프로그램은 실행전에는 보조기억장치에 파일로 존재하게 된다. 이 파일이 실행되면 메모리에 올라와 실행이 된다. 코드 세그먼트에 명령어가 저장되고 데이터 세그먼트에 데이터가 저장되어 작동한다. 아래와 같은 그림을 보면 된다.

우선 CPU는 CU(제어 유니트)와 ALU(산술 및 논리 연산 유니트), 레지스터로 구성되어 있다. 제어 유니트는 메모리에서 가져온 명령어를 해독하고 실행 명령을 한다. ALU는 산술 및 논리 연산을 담당한다. 이 CU와 ALU가 작동하려면 작업 명령어와 데이터를 임시 저장할 공간이 필요하다. 이를 레지스터라고 한다. 레지스터는 CPU 내부에 존재하는 임시 기억장치로 속도가 매우 빠르고 용량은 적은 메모리 소자로 이루어져 있다.

명령어와 데이터는 CPU 외부의 메인 메모리에 적재되어 실행되기도 한다. 이 경우 CPU와 메인 메모리는 일정한 통로로 연결되어 작업을 수행한다. 이 통로를 버스(bus)라고 부른다. 보통 컨트롤 버스, 어드레스 버스, 데이터 버스가 있고 각각의 기능이 다르다. 어드레스 버스는 주소를 나르고 데이터 버스는 데이터를 나른다. 컨트롤 버스는 메모리에 데이터를 쓸지, 읽어올지를 제어하는 버스다. 컨트롤 버스 값이 1이면 쓰기, 0이면 읽기를 하라고 지정할 수 있다. 어드레스 버스에 이진수 1000 즉 8이 실려있으면 주소 8번지를 의미하고 데이터 0011이 실리면 데이터가 3임을 의미한다. 여기에 컨트롤 버스에 실린 값이 1이면 메모리 8번지에 3을 쓰는 작동이 실행된다.

CPU 내부에서는 레지스터가 임시 저장소로 쓰인다.

  • MAR(Memory Address Register) – 접근하려는 메모리 주소를 저장
  • MBR(Memory Buffer Register) – 메모리에서 읽은 데이터나 쓸 데이터를 저장
  • IBR(Instruction Buffer Register) – 메모리에서 읽은 명령어를 저장
  • PC(Program Counter) – 바로 다음에 실행할 명령어 주소 저장
  • General Purpose Register – 범용으로 쓰이는 레지스터

이들 레지스터는 CPU 내부에 두고 빠른 처리를 위한 임시 저장소로 쓰인다.

한편 메모리는 효율적인 사용을 위해 몇가지의 크기 지정을 유형화한다. 우선 CPU가 몇비트인지에 따라 메모리 단위 구성이 달라진다. 32비트 CPU는 내부적으로 32비트로 메모리를 단위짓고, 64비트 CPU는 64비트로 단위짓는다. 즉 32비트 프로세서는 32비트 즉 4바이트가 1워드가 되고 64비트 프로세서는 64비트 즉 8바이트가 1워드가 된다. 1워드는 풀워드이고 풀워드의 1/2배는 하프워드, 풀워드의 2배는 더블 워드로 부른다. 워드가 크면 그만큼 한 번에 많은 데이터를 이동하게 한다는 의미다.

데이터 버스 크기는 곧 어드레스 버스 크기와 같고, 이는 워드 크기와 같은데, 이는 정수형 크기와 같다. 64비트 프로세서와 운영체제라면 정수형 8바이트지만 컴파일러에 따라 제한이 가해지기도 한다.

CPU가 명령어를 처리할때는 레지스터끼리 명령어나 데이터를 주고받기도 하고 레지스터와 메모리로 주고받기도 하며 메모리끼리 주고받기도 한다. 이 세가지 작업을 어셈블리 언어로 나타내면 RR 명령어, RS 명령어, SS 명령어로 구분한다.

RR 명령어는 레지스터 사이의 이동
RS 명령어는 레지스터와 메모리 사이의 이동
SS 명령어는 메모리 사이의 이동

인데 SS는 CPU를 거치므로 두개의 RS 명령어로 변환된다.

프로그램 실행시 메모리 구조는 코드 세그먼트, 데이터 세그먼트, 스택 메모리, 힙 메모리로 계층되어 구조화된다. 코드 세그먼트에는 목적 코드 및 상수가, 데이터 세그먼트에는 정적 변수, 전역 변수가, 스택 메모리에는 지역 변수가, 힙 메모리에는 동적 변수가 저장된다.

메모리에 저장할 데이터 0x40302010을 바이트 단위로 저장할 경우 40, 30, 20, 10 순으로 저장하면 빅 엔디언, 10, 20, 30, 40의 순서로 저장하면 리틀 엔디언이다. CPU마다 다른 규격이고, IBM 호환 PC에서는 리틀 엔디언이 쓰인다.

  • 주우석 교수님이 저술하신 교재를 참고해서 요약식으로 서술했습니다.

C 언어에서 변수와 상수에 대하여 1

프로그래밍 언어에서 제일 중요하고 기초적인 요소가 변수와 상수일 것입니다. 프로그램이 작동하려면 데이터를 처리해야 하고 데이터를 처리하려면 데이터를 메모리 상에 적재해야 합니다. 변수와 상수는 데이터를 메모리에 적재하는 방법을 정의하는 문법 요소로 이들이 반드시 지정되어 있어야 처리가 됩니다. 변수란 값을 담는 그릇이라고 비유될 수 있습니다. 그릇 종류가 정해져 있고 요리사는 이들 그릇의 종류에 따라 알맞은 음식을 담아냅니다. 국그릇에는 국을 담고, 밥그릇에는 밥을 담습니다. 이와 같이 프로그래밍 언어에서도 데이터를 담을 데이터 타입을 지정해주어야 합니다. 언어마다 다르지만, C 언어에서는 상당히 정확해야 하기에, 프로그래머가 직접 변수의 타입 (자료형) 을 지정해주어야 하고 이에 따라 있게 되는 기술적 특징이 결정됩니다.

문법적으로 다음과 같은 형태로 지정됩니다.

또는

이렇게 되고, 이를 프로그래밍 언어 일반론으로 번안하면

이 됩니다.

이를 실재 C 언어로 표현하면

이렇게 표현합니다. 예에서는 a라는 변수명을 가진 메모리 구간을 정수형(int)으로 지정해서 공간을 할당한 후, 100을 대입하는 구문입니다.

다시 말하자면 int a; 구문으로 정수형 데이터를 저장할테니 준비해달라는 요청이구요. a = 100; 은 a가 100과 같다는 말이 아니라, a라는 변수에 100을 대입하라는 의미입니다.

맨위에 비유와 함께 말하자면 그릇은 변수이고 변수는 바뀔 수 있다는 의미로, 프로그램 실행시 바뀔수 있는 데이터를 대입해서 씁니다. 변수 개념과 대조되는 개념은 상수로, 한번 지정되면 바뀔 수 없는 값이 상수입니다.

상수는 두가지 방법으로 C 언어에서 지정됩니다. 매크로 상수가 있고 const 한정자를 붙여서 지정하는 상수가 있습니다.

이 두가지 방법은 기능은 흡사하지만 매크로 상수는 전처리기에 의해 컴파일시 지정한 이름표(MAX_SCORE)를 소스코드에서 찾아내면 100으로 치환하라는 의미이고, const 상수는 변수처럼 작동하지만 역시 치환 기능이 되도록 작동합니다. 아래처럼 활용이 됩니다.

변수의 특징은 데이터 타입을 반드시 잘 지정해두어야 한다는 것이구요. 데이터 타입을 잘 지정하지 않고 변수를 선언하고 초기화하면 타입 불일치 오류 (type mismatch error) 가 나게 됩니다. 타입이 서로 다를때 이후에 오류가 생길 수 있어서 미리 오류를 뿜고 컴파일을 중단하는 것입니다. 프로그래머는 이를 발견하면 소스코드를 적절하게 고쳐야 합니다.

데이터 타입은 때때로 타입이라고 간단하게 지칭되거나 자료형으로 불리우기도 합니다. 엄밀히 보면 구별점이 있는데 타입은 기본 자료형과 확장된 자료형 (클래스나 구조체 등으로 지정되는 데이터 타입) 이라는 정의도 있고, 자료형은 기본 자료형(primitive data type) 이라고도 하는데 사실 둘다 혼용되는 경우가 많습니다. 이 글에서는 구별을 안하고 쓰기로 합니다.

C 언어의 자료형은 정수형, 부동소수형, 문자형, 문자열 등으로 구분됩니다. 정수형은 short int, int, long int, long long int가 있고, int를 생략하고 기재하기도 합니다. short int는 short라고 하면 되죠. 부동소수형은 float, double, long double이 있습니다. 각각의 자료형은 하드웨어 아키텍처와 컴파일러에 따라 허용하는 바이트가 다릅니다.

바이트수가 커진다는 것은 표현할 수 있는 수의 범위가 커진다는 것으로 정수형인 경우 큰 수를 표현할 수 있다는 의미이고, 부동소수의 경우 소수점 이하 정밀도가 높아진다는 의미이다. 32비트 컴퓨터 아키텍처에서는 int는 4바이트로 표현되어 2의 보수로 나타낼 경우 -2의 31승부터 2의 31승 빼기 1의 범위의 정수를 나타낼 수 있다. 대략 플러스마이너스 21억이 넘는 숫자다. 그러나 이를 초과해야 할때는 8바이트 long long을 쓰면 대략 플러스마이너스 922경 단위까지 수용할 수 있다.

자료형의 크기가 어느 정도인지 알아보려면 printf() 함수에 sizeof() 연산자를 대입해서 쓰면 된다.

참고로 sizeof는 함수가 아니라 연산자로 변수 자체나 자료형의 크기를 알아야 할때 쓴다. sizeof 의 괄호안에 C가 지원하는 자료형을 기재하면 자료형의 크기를 출력한다. 자료형의 크기는 1부터 16바이트의 정수이므로 printf 문에서 %d 자리에 오게 하면 된다. INT_MIN이나 DBL_MAX와 같은 상수는 limits.h에 정의된대로다.

한편 숫자를 담는 변수 선언시 별다른 한정자가 없으면 signed로 선언된다. 이는 한 변수에 음수와 양수를 모두 담을 수 있게 하겠다는 선언이다.

unsigned가 붙여진 숫자용 변수는 양수만 표현하는데, 최상위 비트도 숫자 표현이 쓰게 되므로 최대 표현 가능한 수가 거의 두배로 늘어난다. 4바이트 signed int는 -2의 31승부터 2의 31승 빼기 1까지의 정수를 담을 수 있지만, unsigned int로는 0부터 2의 32승 빼기 1까지의 정수를 담을 수 있다.

변수에 담을 수에 접미사를 붙이기도 한다.

때로는 한 자료형의 값을 다른 자료형의 변수에 대입할때가 있다. 이 경우 자료형이 서로 호환된다면 무사히 마쳐지지만, 자료형이 다르거나, 표현가능한 바이트를 넘어가면 앞부분 비트가 날아가서 원하는 값이 대입이 안될 수도 있다. 아래 예를 보자.

이 경우 length는 정수형인데 대입은 double이 되었다. 그러면 소수점 이하 표현이 잘려나가 3이 된다.

명시적으로 변환을 하기도 한다.

이 경우에도 잘려나가지 않게 생각을 잘 하면 좋다.

영문자가 담기는 char 는 1바이트 크기로, 문자 하나가 들어간다. ASCII 문자표에 정의된대로 0부터 127까지의 숫자로 대입해도 되고 작은따옴표로 둘러싸인 대문자나 소문자를 대입해도 된다. 알파뱃 A는 10진수 65, a는 97로 표시된다. 유의할 것은 큰따옴표로 지정하면 C 언어에서는 문자열로 인식해서 오류가 난다. 그리고 10진수로 표현할 경우 ‘a’ – ‘A’ 처럼의 연산이 된다. 이는 97 – 65 = 32 이므로 문자를 처리할 필요가 있을때, 문자끼리의 변환에 응용하면 좋다. ASCII 문자표를 확인해서 판단하면 된다.

그리고 char 자료형을 배열이나 포인터로 두면 문자열도 표현이 된다.

이렇게 선언하면 마지막 비트에 \0 (백슬래시 제로)가 붙어 문자열의 끝임을 알려주는 식으로 메모리가 할당된다.

변수를 지정할때는 C에서 쓰이는 예약어를 피해야 한다.

그리고 변수명 작명 또한 일정한 규약을 따르면 좋은데, C 언어에서는 sum_of_salary나 salary_sum을 쓰는게 추천되고, C++에서는 SumSalary, SumOfSalary, sumOfSalary 등으로 쓴다.

컴퓨터에서 숫자 표현 2

메모리에 정수를 표현하는 방법은 (1) 부호절대값 (2) 1의 보수 (2) 2의 보수와 같은 개념 체제 하에서 한다. 컴퓨터가 표현가능한 비트수는 아키텍처에 따르고, 컴파일러에도 조건되어진다. 우선 8비트라면 최상위 비트(MSB)가 부호 비트가 되고 나머지 7비트는 숫자를 이진법으로 표현하는 것인데, 부호 비트가 0이면 양수를 1이면 음수를 표현한다.

부호를 달아 표현하면 음수 표현이 되고, 아니면 양수만 표현이 된다. 부호를 안달면 모든 비트가 양수 표현에 쓰이므로 더 큰 범위의 정수가 표현된다.

그런데 비트수가 한정되어 있는데 정수 연산이 요청되어 비트수를 넘어가버리면 오버플로우가 발생한다.

이는 컴퓨터에서 실수(real numbers)를 표현할때도 그러한데 실수를 표현하는 방식인 부동소수(floating point number) 또한 표현가능한 비트가 한정되어 있으면 실수 공간에서 한정된 개수의 실수만 취해서 띄엄띄엄(discrete) 표현하게 된다.

우선 IEEE 754에서 정의한대로 C 컴파일러가 구현하는 얼개다. 32비트의 정밀도는  필수이고, 64비트 정밀도는 옵션 사항으로 원안에서는 그러하다.

32비트인 경우 float일때 부호 1비트, 지수 8비트, 가수 23비트

64비트인 경우 double일때 부호 1비트, 지수 11비트, 가수 52비트

이렇게 메모리를 차지하고 역시 오버플로우 원리가 있게 되면 숫자 표현에 난항이 있으나, 32비트와 64비트만 되더라도 그 범위가 충분해서 근사값이 되더라도 여러 기술들이 동원되어 정밀도가 보장된다. 비트수가 커지면 위의 표준안에서 보여지듯이 64비트 이중 정밀도인 경우 지수가 11비트라, 1023개의 경우의 수가 부여되어 정밀도가 높고, 가수에서도 52비트가 할당되므로 역시 표현 범위가 늘어난다. 즉 띄엄띄엄 표현해도 소수점 아래 표현 범위가 늘어나서 근사값의 정밀도가 높다.

소수점 이하 숫자도 이진수로 표현된다. 십진법으로 12.375라면, 정수부 12에 해당하는 이진표현이 1100이므로, 정수부는 쉽게 구해진다. 소수부 0.375는 매단계 소수부에 2를 곱해 나타내어진 식에서 정수만 취하고, 그 식에서 소수부만 0이 될때까지 2를 곱해 정수만 취해서 먼저번 연산에서 나중의 연산으로 얻어진 수가 비트값이 되어 이들을 읽으면 소수부의 이진 표현이 얻어진다. 예를 들면

0.375 * 2 = 0.750 = 0 + 0.750  — 0

0.750 * 2 = 1.500 = 1 + 0.500 —— 1

0.500 * 2 = 1.000 = 1 + 0.000 —— 1

이므로 0.375 = 0.011 이고, 12.375에 취하면 1100.011이 된다.

부동소수 표현에도 오버플로우 오류와 언더플로우 오류가 존재한다. 32비트 부동소수 표현에서는 -(1 – 2의 -24승) 곱하기 2의 128승인 범위보다 작아지면 음수 오버플로우가, -0.5*2의 -127승보다 커지면 음수 오버플로우가 나고, 0.5*2의 -127승보다 작아지면 양수 언더플로우가, (1-2의 -24승) 곱하기 2의 128승보다 커지면 양수 오버플로우가 발생한다. 오버플로우가 발생하면 때로는 비트 표현 범위를 넘게 되어 연산 결과가 제대로 유도되지 않는다.