컴퓨터 구조 및 어셈블리어를 통한 LED제어

kenGwon·2024년 2월 8일
0

[Embedded Linux] BSP

목록 보기
6/36

stm32 cubeIDE에서 사용했던 API

우리가 그냥 편하게 C 라이브러리 함수 사용하듯이 썼던 HAL function들이, 사실은 어제 우리가 U-Boot를 통해서 레지스터에 값을 써줬던 것들을 전부 미리 구현해놓은 것이었다.




임베디드 시스템

임베디드 시스템에는 마이크로프로세서가 탑재되어 있기 때문에, 프로그램만 바꾸면 얼마든지 기능을 다양하게 바꿀 수 있다. 펌웨어 업데이트 만으로 기기의 기능을 바꿔주는 것들이 바로 그런 예인 것이다.

여름에 손선풍기도 그 안에 모터의 속도를 제어하기 위해 PWM을 제어하는 작은 MCU가 들어가 있따. 별의별 곳에 다 들어가 있는 것이다. 거의 우리 일상생활에 임베디드 시스템이 들어가있지 않는 것이 없다.
그중에서도 가장 임베디드 시스템이 멋있고 아름답게 되어있는 것이 공유기인 것이다.
임베디드 시스템에서 "네트워크 통신"의 중요성은 두말하면 입아프다.

도어락 같은 경우도 안에 while(1)문이 있는데, 평상시에는 sleep을 시켜놓는 것이다. 그래서 평상시에는 CPU 클럭이 돌지 않기 때문에 소비전력이 확 떨어져서 완전 저전력으로 오랫동안 건전지 하나로 유지할 수 있는 것이다. 그러다가 외부 센서로부터 interrupt가 들어오면 그때 비로소 CPU가 깨어나면서 클럭이 돌아서 작업을 처리하게 되는 것이다.

거의 자동차가 전자제품화되어가고 있다. 아주 작은 300개 이상의 센서 하나하나에 대해 MCU가 다 들어가 있는 것이 자동차이다. 이런 자동차 산업쪽 파이가 커지면서 그쪽에서 임베디드 시스템 소프트웨어 개발자에 대한 수요가 높아지고 있다.

ARM이 임베디드 시스템 MPU로 각광받는 이유

저전력이기 때문이다. STM32만 봐도 사용하지 않는 부분에는 쓸데 없이 할당하지 않도록 잘 구성되어있다. 이것은 ARM processor가 그러한 기능들을 지원하기 때문에 가능한 일이다.




컴퓨터 구조 개념리뷰 !!!

Memory Mapped I/O

32bit 시스템이라면 핵심은 시스템이 사용할 수 있는 주소의 범위가 0x00000000 ~ 0xffffffff라는 것이다. 그 범위를 여러 형태의 메모리가 나눠서 쓰고 있는 것이다. 어디부터 어디까지는 SRAM이 사용하고, 어디부터 어디까지는 시스템 레지스터가 사용하고, 어디부터 어디까지는 I/O peripheral이 사용하고 있는 것이다.

(내가 지금까지 윈도우에서 I/O mapped I/O 방식을 사용했다보니, Memory mapped I/O 방식에서도 32비트 시스템에서 사용가능한 주소의 범위를 전부 DRAM 혼자서 사용하고 있을 것이라고 착각하고 있었던 것이다.)




레지스터에 대하여...(범용 레지스터 / 시스템 레지스터)

산술논리장치(ALU)는 오직 범용 레지스터에 있는 데이터만 가져다가 연산할 수 있고, 결과도 범용 레지스터에만 저장할 수 있다. 그래서 필요한 값들을 다른 메모리로부터 가져와서 우선 레지스터에 저장해놓고 다음 작업을 진행하게 된다.

우리가 위에서 GPIO 사용을 하겠다고 사용했던 레지스터는 "시스템 레지스터"이다. 범용 레지스터와는 다른 것이다.

레지스터에서 1비트를 잠시 저장하기 위한 방법으로 Flip-flop이나 Latch를 사용한다.

레지스터 종류

  • 범용 레지스터
    • 프로그램 또는 데이터 처리에 필요한 작업을 수행하기 위해서 사용
  • 제어용 레지스터
    • 대표적으로 '프로그램 카운터'
  • 상태 레지스터
    • 프로세스의 상태를 나타낸다.




제어장치(Contol Unit, CU)

  • ARM 32비트 명령어셋은 조건부 명령이 가능하기 때문에 속도가 빠르다. 앞에 명령어를 true/false여부에 따라서 다음 명령을 지정하는 식의 조건부 명령이 가능하기 때문에 속도가 빨라지는 것이다.

  • Thumb 16비트 명령어셋은 조건부 명령이 불가능해서 속도는 좀 떨어지지만, 메모리를 절약할 수있다는 장점이 있다.

  • 이 두가지의 장점을 합쳐서 만든것이 cortex이다. 예전에는 두개의 명령어셋이 동시에 돌 수 없었다. 그런데 cortex부터는 두개의 32비트의 명령어셋과 16비트의 명령어 셋이 혼재할 수 있도록 된 것이다.

  • cortex는 메모리도 절약하면서 속도도 향상시킨 획기적인 명령어셋이다. 그래서 cortex2 명령어셋이라는 새로운 이름을 붙이게 되었다.




gcc 패키지 파헤치기

gcc는 4단계를 자동으로 관리해주는 패키지일 뿐이다.
원래는 각각의 단계를 담당하는 프로그램이 따로 존재하는 것이다.

  1. 전처리기 (main.i 생성)
  2. 컴파일러 (main.s 생성)
  3. 어셈블러 (main.o 생성)
  4. 링커 (main.out 생성)

gcc -save-temps : 교재 79쪽

gcc 컴파일러 단계별로 생성되는 파일 눈으로 확인하기

$ gcc main.c -o main -save-temps
$ ls
main  main.c  main.i  main.o  main.s

1. 전처리기의 동작 결과로 생성된 main.i

726번째까지의 라인은 전부 "#include <stdio.h>"라는 문장에 의해 삽입된 내용들이다.

729~740번째 라인까지의 내용을 보면 733번째 라인에 "#define BUFFSIZE 100"이라고 했던 전처리기 내용이 적용되어 "char buf[100];"이라고 되어있는 것을 볼 수 있다.

2. 컴파일러의 동작 결과로 생성된 main.s

여기 있는 것들은 전부 어셈블리 언어로 변환된 코드들이다.

3. 어셈블러의 동작 결과로 생성된 main.o

8비트로 구성되어있기 때문에 제대로 볼 수가 없다. 그래서 아래 명령어를 통해서 16비트로 변환해서 보겠다.

$ hexdump main.o

또는 다음 명령어를 통해서도 볼 수 있다.

4. 링커의 동작 결과로 생성된 main.out
$ xxd main.out

참고
전역변수에 int num[10000]; 이라고 선언해버리면 쓰지도 않는 40000바이트 영역이 실행파일 사이즈에 잡혀버린다(전역변수이기 때문에 데이터 영역에 잡힘). 이것은 엄청난 낭비라는 것이다. 그래서 초기화 되지 않은 전역변수에 대해 스타트 코드는 실행파일의 사이즈를 줄이기 위한 별도의 작업을 실시한다.
이러한 낭비를 줄이기 위해서 스타트 코드는 저 공간이 필요하다는 정보만 들고 있는 상태에서, 저 공간의 내용을 0으로 초기화 해버린다. 그렇게 함으로써 실행파일의 크기를 작게 유지해주는 것이다.

결과적으로 C언어가 태동한 근본적 이유...

"어셈블리어 때문이다."
어셈블리어는 하드웨어마다 명령어셋이 다 다르다. 그래서 데니스 리치가 유닉스를 개발할 때 C언어를 만들기 시작한 것이다. C언어는 호환성이 이식성이 좋은 것을 목표로 했다.

C언어에서 함수명도 주소다. 함수 포인터는 함수 코드의 가장 첫줄 부분이 저장되어있는 주소를 가리키게 된다. 그게 바로 함수 포인터인 것이다.




명령어 수행과정

명령어 수행과정이 3단계를 거친다는 말은 3개의 클럭이 필요하다는 것이다.
하지만 그렇다고 해서 2개의 명령을 실행하기 위해서 6개의 클럭이 필요한 것은 아니다. 왜냐하면 "파이프라인" 기법이 적용되기 때문이다.

파이프라인과 inline function !!!!

함수를 자주 호출하면 해당 위치로 점프해야 하는데, 그 때 파이프라인 상의 연속된 주소 조건이 유지될수 없게 되어 파이프라인이 깨지는 상황이 발생한다. 그래서 너무 잦은 함수 호출은 파이프라인이 제대로 동작할 수 없게 만들어서 오버헤드가 커지는 것이다.

그래서 그러한 상황에 inline 함수를 사용해서 대응하게 된다. inline 함수는 약간 전처리기의 매크로처럼 단순히 함수 호출 위치에다가 함수 내을 붙여 넣는 것이기 실질적으로 함수가 위치한 메모리 주소로 점프하는 동작이 일어나지 않기 때문에, 파이프라인을 깨지 않으면서 프로그램의 유지보수를 용이하게 만들어준다.




가상메모리-MMU-물리메모리

32비트 시스템이라면 4GB용량의 주소를 사용할 수 있는 것이다. 하지만 32비트 시스템이라 하더라도 거기에 달려있는 물리적 메모리가 512MB밖에 안될수 있다. 그러면 512MB용량이 주소만 사용할 수 있는 것이다.
하지만 부족한 메모리를 파일시스템을 통해서 땡겨와서 마치 메모리인 것마냥 쓰고 싶어서 "스왑메모리"라는 기술이 나온 것이다.




어셈블리 실습

라즈베리는 좀 달라서 그냥 바로 커널이 부팅되었었다. 하지만 대부분의 보드들은 u-boot을 먼저 실행하고 u-boot에서 커널을 실행하도록 되어있다. 그래서 우리가 지난 시간에 u-boot을 먼저 실행하고 커널이 실행되도록 바꾼 것이다.

지금부터 ~/pi_bsp/u-boot 경로에서 여러가지 파일들을 수정해보겠다.

$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- rpi_4_32b_defconfig

위 명령어는 어제 단 한번 친것으로 끝이다. 이제부터는 그냥 빌드하고 make하기만 하면 된다.

지금부터 앞으로는 build.sh 쉘 스크립트를 만들겠다.

make는 .o파일과 .c파일의 생성시간을 비교하여 소스파일의 수정시점이 .o파일보다 전이라면 수정이 없었다고 보고 컴파일을 하지 않고 링킹 시간만 최신화한다.

그래서 "make clean"을 통해서 컴파일 기록을 날리고 나서 다시 ./build.sh를 해보면 다시 원래대로 하나하나 쫙 다 컴파일 하는 것을 볼 수 있다.

어셈블리어가 '끝나는 부분'에 실습 코드를 작성해보겠다.

우선 tag파일 작성하기
~/pi_bsp/u-boot$ ARCH=arm make tags

위 명령어로 태그가 만들어졌다. "ls -l tags"를 쳐보면 아래처럼 ctags를 가리키는 tags 파일이 만들어진 것을 확인할 수 있을 것이다.

.vimrc에 tags 추가


그리고 tags의 순서를 바꿔주었다. 저렇게 해준이유는 vim이 각종 tags를 찾을 때 우리가 가장 필요로 하는 u-boot관련 것들을 '먼저' 찾을 것이기 때문이다.

짱 편한 "vi -t [함수명or구조체]"

아래 명령어는 vi 에디터를 열 때 커서를 [함수명or구조체] 태그 위치로 이동시키는 명령어입니다. 이 명령어를 사용하려면 ctags와 같은 도구로 생성된 태그 파일이 존재해야 합니다.

u-boot.lds

u-boot.lds 파일은 U-Boot 부트로더의 Linker Script입니다. Linker Script는 프로그램의 링킹 및 로딩 동작을 지정하는 스크립트로, 주로 메모리 주소 및 섹션 배치와 같은 로우 레벨의 메모리 구성을 제어하는 데 사용됩니다.

이 파일을 모르겠어도 차근차근 따라가면서 볼 필요가 있다.

위 부분에서 8~13번째 라인이 부트로더가 처음 시작될 때 ROM에 있는 내용을 메모리 쪽으로 복사오라는 명령이 적혀있는 스크립트 부분이다.

우리가 이 파일을 본 이유는 3번째 라인에 적혀있는 엔트리포인트인 ENTRY(_start)를 보기 위함이었다.

우리가 실습할 위치

~/pi_bsp/u-boot/arch/arm/cpu/armv7/start.S

위 파일의 127번째 라인에 "bl kcci_led_test"라는 코드(kcci_led_test라는 라벨로 분기(branch)하면서 돌아올 현위치 주소를 link register에 넣어놓으라는 코드)을 정의했고,

그리고 맨 아래로 내려가서 kcci_led_test 라벨에 대한 코드를 추가했다.

상수는 프로그램의 데이터 영역에 Read Only로 올라간다고 했다.
그래서 상수값 =0xFE200000도 실제로 기계어로 번역될 때는 상수가 저장되어있는 메모리의 주소로 변환되게 된다!!!

=0xFE200000는 그 값 자체로 32비트이기 때문에, 다른 명령어와 한번에 쓸 수 없다. 그래서 ldr같은 명령어로 값을 레지스터에 저장한 것이다.
그런데 #4는 mov라는 32비트 명령어 안에 4라는 값이 한번에 담겨서 들어갈 수 있다. 이렇게 하면 똑같이 레지스터에 값을 쓰는 것인데 ldr보다 속도가 더 빨라진다. 그래서 이렇게 값이 들어갈 수 있으면 mov를 쓰는 것이다.

그래서 딜레이를 먹이면서 LED가 켜져있는것처럼 보여줘야 한다. 그래서 1씩 카운트를 줄여가면서 카운트가 0이될때까지 빛이 켜져있게 된다. 그 기능은 subs라는 명령어로 구현되었는데, subs는 sub와 명백히 다른 것이다. subs는 "연산 결과를 상태레지스터에 반영하라"는 의미이다.

bne = branch not equal...
바로 윗 분장에서 "subs r3, r3, #1"을 반복하면 계속 r3가 0x400000에서 1씩 줄어드는데 0이 되기 전까지는 계속 true상태이다가, 0이 되는순간 상태레지스터가 false가 된다. 그러면 바로 그 때 "bne delay"구문을 탈출하여 다음으로 진행된다.

mov r1, r1, LogicalShiftLeft #1 구문을 통해서 비트시프트 연산을 통해서 목표 GPIO를 하나씩 옆으로 옮겨가면서 LED를 켜고 끄는 동작을 반복하는 것이다.

결과적으로 구조를 가만히 잘 보면 C언어의 for문과 다를바가 없다. 차분하게 레지스터 칸에 대한 그림을 그려가면서 잘 따라가기만 하면 그냥 프로그래밍 언어와 다를것이 없는 것이다.

이제 그렇게 하고 모든 작업이 끝나고 "mov pc, lr" 명령을 통해 아까 여기 어셈블리 라벨에 들어오기 전에 저장해놨던 link register 값을 program counter에 넣어주고 끝이난다. 즉 bl 명령과 "mov pc, lr"은 세트인 것이다.

어셈블리 명령

  • LDR(load register)은 [베이스레지스터, 오프셋]에 있는 값을 load해서 대상레지스터에 넣겠다는 의미이다.
  • STR(store register)은 대상레지스터에 있는 값을 [베이스레지스터, 오프셋] 자리에 넣겠다는 의미이다.




어셈블리 프로그래밍

  • 보통 r15를 프로그램 카운터 레지스터로 사용한다.
  • 그리고 r14는 링크 레지스터이다. 링크 레지스터는 서브루틴을 호출하고 나서 돌아올 위치값을 저장하고 있다. 함수호출 직전에 프로그램 카운터에 있는 값이 여기에 들어간다.
  • r13번은 스택 포인터 레지스터로 사용된다.

위의 3개는 특수한 레지스터이다. 나머지가 범용레지스로 사용하게 된다. 그 버것을 염두하고 아래 코드를 보자.

어드레스는 어셈플리어 명령어가 위치하는 프로그램 카운터 주소를 의미한다.

파이프라인

PC가 연속된 메모리를 접근한다고 할 때 파이프라인이 성립하는 것이다. 만약 함수 호출 등의 이유로 PC가 연속된 메모리를 접근할 수 없게 되면 파이프라인이 깨진다.

메모리에 저장되는 데이터는 오직 '수'이다.

일반 숫자

그냥 숫자

보수

컴퓨터가 마이너스 값을 저장할 때는 보수로 저장한다. 정수에 대한 이진수의 MSB(most significant bit)가 1라면 음수라는 표시인데, 그 경우에 음수 이진수를 바로 10진수로 바꿔 읽으면 안된다는 소리다. 왜냐하면 그 이진수는 보수가 취해져 있는 숫자이기 때문이다. 그래서 10진수로 어떤 음수값을 나타내는건지 알고 싶으면 보수의 역 연산을 해줘야 한다는 것이다.

부동소수점

실수에 대한 C언어의 기본 자료형은 double이다.
그래서 그냥 31.2라고 하면 double형 64비트 배정밀도 부동소수점으로 저장된다.
만약 31.2f라고 하면 float형 32비트 단정밀도 부동소수점으로 저장된다.

profile
스펀지맨

0개의 댓글