부트로더 기초 및 U-Boot을 통한 LED제어

kenGwon·2024년 2월 7일
0

[Embedded Linux] BSP

목록 보기
5/36

유인물 리뷰

/boot/cmdline.txt

/boot/cmdline.txt는 부팅할 때 커널에 명령행 arguments로 전달할 텍스트가 적혀있는 파일이다.

serial 통신 리뷰

속도인 baud rate가 11520라는 것은 1초에 11520비트를 보낸다는 것이다. 시리얼통신은 비동기통신이기 때문에, PC와 타겟머신의 baud rate가 반드시 같아야 한다.

시리얼 통신은 하나의 프레임이 있다. 여러 종류의 비트로 구성되어있다.
그중에서 가장 중요한 것은 정지비트(stop bit)이다.

  • start bit 1비트: 1
  • 데이터비트 8비트(8비트가 1바이트라서 이해하기 쉬어서 보통 데이터비트로 8비트를 쓴다. 개념적으로는 양쪽이 맞추기만 한다면 7비트든 9비트든 상관 없음)
  • 패리티비트 none/홀/짝, 정지비트 1비트
  • *stop bit 1비트 or 2비트 -> 이 정지비트가 너무너무 중요하다
    • 서로 같은 기종간의 UART장치라면 CPU 클럭속도가 정확히 같아서 상관없는데, 서로 다른 UART장치를 사용하고 있는 장비들은 CPU자체의 클럭속도가 미세하게 다르기 때문에 통신속도(보 레이트)가 정확하게 11520으로 떨어지지 않는다.
    • 그럴 때는 스탑비트를 보낼 때는 1, 받을 때는 2비트로 하거나 / 보낼 때 2, 받을 떄 2비트로 하거나 / 보낼 때 2, 받을 때 1비트로 해가면서 미세한 클럭속도의 차이로 인해 발생하는 데이터 깨짐현상을 극복할 수 있다.

셸에서 ; 과 && 의 사용

ubuntu@ubuntu14:~$ ls ; cd pi_bsp/
ubuntu@ubuntu14:~$ ls && cd pi_bsp/

이런식으로 명령어를 세미콜론(;)을 통해서 예약할 수 있다.
앞에 명령에 시간이 오래 걸리는 것을 해놓고 뒤에 명령을 예약해놓으면, 굳이 화면을 보지 않고 있어도 다음 것을 실행한다.

세미콜론 자리에 &&를 넣을 수도 있다. &&는 그야말로 and 연산자이기 때문에, 앞의 명령이 거짓이면(=오류가 있다면) 뒤의 명령을 실행하지 않는다.

그래서 보통은 안전하게 &&을 많이 사용한다.(세미콜론을 사용하면 앞에 것에 문제가 있어도 뒤의 명령을 무조건 실행해서 문제가 발생할 수도 있다.)

셸에서 파이프의 사용

앞의 명령어 프로세스와 뒤의 명령어 프로세스 간에 파이프를 놓겠다는 것이다.
앞의 프로세스가 내놓은 출력값을 뒤의 프로세스가 입력값으로 받아서 사용하라는 것이다.

$ cd /usr/bin
$ ls -l | wc -l
$ ls -l | more

wc는 word count의 약자로 "단어의 갯수를 세라는 것"이다. 거기 위헤 -l 옵션을 주면 라인의 갯수가 몇개인지 세주게 된다.

필수 패키지 설치

$ sudo apt-get install gawk git-core diffstat unzip texinfo gcc-multilib build-essential chrpath socat cpio python-setuptools python3-pip python3-pexpect xz-utils debianutils iputils-ping python3-git python3-jinja2 libegl1-mesa libsdl1.2-dev xterm rsync curl zstd lz4 bison flex

$ sudo apt install crossbuild-essential-armhf 	//arm 32bit 툴체인 설치

arm 32bit 툴체인을 설치함으로써, 이제 터미널에 "arm"까지만 치고 tab tab을 누르면 해당 툴체인에서 제공하는 모든 툴을 확인할 수 있다. 우리는 여기서 gcc를 써서 크로스 컴파일을 하는 것이다.

$ cd /usr/bin
$ ls arm-linux-grnueabihf-* | wc -l    

위 명령어로 실행가능한 arm 툴체인이 36개가 있다는 것을 알 수 있다.

굉장히 편리한 기능

cd - : 조금 전에 있었던 폴더의 경로로 바로 점프 할 수 있다.
cd ~[사용자명] : 리눅스에 여러명의 사용자가 있는경우 대괄호 안에 입력한 사용자의 홈 디렉토리로 갈수 있다.

리눅스의 비밀번호 관리

현재 리눅스에 있는 계정정보를 보고 싶다면

$ cat /etc/passwd

리눅스 root 권한 관리자도 사용자의 passwd를 "볼 수는 없다".
사용자의 비밀번호는 암호화 되어있다. 대신에 root권한으로 초기화는 할 수 있다.
암호화된 비밀번호는 아래 명령어를 통해 확인할 수 있다.

$ sudo cat /etc/shadow




u-boot로 부트로더 활성화 & 커널 빌드

부트로드와 u-boot의 개념을 설명한 블로그 글을 먼저 참고하자.
현재 우리가 가지고 있는 라즈베리파이에는 부트로더가 활성화되어있지 않다.
현재 라즈베리파이는 부팅하자마자 바로 커널 메세지가 뜨는 상태이다.

커널 메세지를 printk()라는 함수를 통해서 커널에 메세지를 출력한다.(printf가 아니라 printk를 쓰는 것이다!!)

$ dmesg   // 현재 라즈베리파이가 부팅될 때 나왔던 커널 메세지를 출력해주는 명령어

우리는 uboot 소스를 가져와서 빌드하고 부트로더를 활성화하려는 것이다.
이제 uboot의 최상위 폴더에서 빌드 명령을 날려야 한다.

$ git clone git://git.denx.de/u-boot.git
$ cd u-boot
$ cd arch    // 여기에 ARCH= 옵션으로 줄수 있는 아키텍처들이 나열되어있다.
$ cd ..
$ cd configs    // 여기에 빌드를 쉽게 하기 위한 컨피그 파일 설정들이 다 들어가 있다. (여기 안에 rpi_4_32b_defconfig라는 컨피그 옵션이 있는 것이다.)
$ cd ..

config파일 만들기

$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- rpi_4_32b_defconfig    // 이 명령어는 최초에 한번만 해야 한다. 한번 더 치면 기존에 존재하던 .config 파일을 덮어쓰는 방식으로 디폴타 값의 새로운 파일을 만들어 버린다. 그래서 .config를 수정했다면 그 파일을 반드시 다른 곳에 백업해두는 것이 좋다.


위 명령을 통해서 .config 파일이 생성되었다.
vi로 내용을 확인하면 교차개발용 부트로더 빌드를 위한 설정값들을 확인할 수 있다.
이 설정값들이 전부 조건부 컴파일들의 값으로 들어가면서 컴파일 되어 빌드가 되는 것이다.

$ vi .config

// 나중에 .config 파일을 수정하곤 할 것이다. 미리 맛을 보자면...
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

menuconfig를 통해 나오는 쉘기반 UI에서 값을 수정하면 그것이 .config파일에 반영된다. 이렇게 변경하면 된다는 것이다.

본격적으로 빌드하기

$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all
$ ls -lt      // t: 제일 최근에 만들어진 것부터 정렬해서 보여달라는 옵션

ls -lt로 봐서 u-boot.bin파일이 만들어져 있으면 제대로 빌드된 것이다.
.bin확장자는 순수하게 바이너리 값만 들어가 있기 때문에 이것이 도대체 무슨 파일인지 알 수가 없다.

ubuntu에서 컴파일된 u-boot.bin 파일을 타겟보드로 전송하기

NFS(Network File System) 보내면 편하겠다.

$ cp u-boot.bin /srv/nfs/

여기 아래부터는 raspberry 쪽에서 치는 명령어이다

bin파일을 /boot/ 경로로 복사해간다.

$ cd /mnt/ubuntu_nfs
$ sudo cp u-boot.bin /boot/firmware/

$ cd /boot/
$ sudo vi config.txt

config.txt 파일의 맨 아래다가 "kernel=u-boot.bin"이라고 넣어줬다.

그리고 나서 재부팅을 해준다.

$ sudo reboot

부팅이 되면서 커널메세지가 뜨다가 중간에 딱 멈춰서 아래 이미지와 같은 프롬포트로 전환되면 정상적으로 u-boot 부트로더가 적용된 것이다.




u-boot

U-BOOT> printenv 
U-BOOT> printenv bootargs
## Error: "bootargs" not defined
U-BOOT> printenv bootcmd
bootcmd=bootflow scan

우리는 bootcmd를 수정할 것이다. 커널 부팅할 때 시퀀스를 건드리겠다(?)

U-Boot> setenv bootargs 8250.nr_uarts=1 console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait rw
U-Boot> printenv bootargs
U-Boot> printenv bootcmd  		  //bootcmd 확인 
bootcmd=bootflow scan

// user_mmc_boot U-boot 부팅 스프립트 작성
U-Boot> setenv bootcmd 'run user_mmc_boot'
U-Boot> setenv user_mmc_boot 'mmc dev 0; fatload mmc 0:1 ${kernel_addr_r} kernel7l.img; fatload mmc 0:1 ${fdt_addr_r} bcm2711-rpi-4-b.dtb; bootz ${kernel_addr_r} - ${fdt_addr_r} '
U-Boot> printenv user_mmc_boot
U-Boot> saveenv		// 환경변수를 sd카드에 영구히 저장
U-Boot> reset			//재부팅

기존에는 bootflow scan 으로 부팅이 되서 안됬던건데, 위에 명령을 통해서 부트 커맨드를 지정해줌으로써 부팅이 되도록 만든 것이다.

mmc라는 디바이스 장치는 0번으로 설정하겠다는 것이다. 파일시스템은 fat을 사용하겠다는 것이고 그 파일시스템을 load하겠다는 것이다. bcm2711 디바이스 트리에 대한 정보가 들어가있고 그것을 load한다. 그리고 나서

sdimage에 있는 kernel7l이라는 이미지를 가져다가 부팅하겠다는 의미이다.

이제부터 U-BOOT를 진입하고 싶으면, 부팅이 될 때 3..2..1..0 카운트가 될 때 임의의 키를 눌러주면 부팅을 중단하고 U-BOOT로 진입하게 된다.

이로써 우리의 라즈베리파이에는 부트로더 올라간 것이다. 이제부터 부트로더를 활용하여 다양한 작업을 해보겠다.




부트로더 이론 - 수업자료 ppt

부트로드의 가장 중요한 역할은 커널을 sdram 메모리에 적재시키고, start까지 시키는것이다.


  • 우리는 사실 intel과정 수업을 들을 때 멀티부팅을 한다고 부트로더를 설정해놨었다.

  • 임베디드 시스템에는 BIOS가 없다. 바이오스가 할 기능을 부트로더가 해주는 것이다.

  • 그래서 부트로더는 굉장히 많은 기능을 제공한다. 제공하는 기능들은 help 명령어를 통해 볼 수 있다.

    U-BOOT> help
  • 새로운 SoC 칩을 만들던 그걸로 임베디드 어플리케이션을 만들던, 그 모든 것의 첫번째 스텝은 부트로더를 포팅하는 것이다.

  • '부트로더를 개발한다'고는 거의 말하지 않는다. 새로운 보드나 SoC칩이 나오면 거기에 맞게 동작할 수 있도록 이미 다 개발되어있는 부트로더를 조금조금씩 수정해서 올리게 된다. 그리고 그러한 작업을 두고 '부트로더를 포팅한다'고 말한다.

u-boot 빌드 후 생성되는 파일


u-boot.map 맵파일에는 링커에 의해 컴파일된 실행파일의 각 오브젝트파일이 어디어디 주소에 맵핑되어있는지에 대한 정보를 가지고 있다.

부트로더 탑재

요즘은 대부분 swd(serial wired device)를 통해서 부트로더를 얹는다.
(STM32도 swd 방식을 통해 작업이 이루어진다.)

전통적인 방식은 NAND 플래시 메모리 영역에 있는 부트로더를 SRAM으로 복제해와서 동작하도록 되어있다. 그런데 우리는 SD카드를 사용하고 그 파일시스템 안에 이미지 파일로 그냥 다 올려버리고 복사해버린 거라서 훨씬 작업이 간단했던 것이다.

복제과정

NAND 플래시처럼 블록단위(128kB)로밖에 읽을 수 없는 ROM에서는 명령을 읽을 수 없다. 프로그램이 실행되기 위해서는 바이트 단위로 읽을 수 있는 SDRAM으로 복제해와야 하는 것이다.

라즈베리파이는 교육용이니까 sd카드를 꽂아서 하는거지, 원래 대부분의 임베디드 장비는 저장장치로 NAND플래시메모리(ROM) 같은 거를 쓴다. 그래서 ROM에 있는 U-Boot 부트로더 코드를 SDRAM으로 가져와야 하는 것이다.
최초에 internal SRAM으로 초기화 코드를 먼저 가져오고 그 코드로 말미암아 NAND 플래시에 있는 U-Boot의 모든 코드가 SDRAM에 복제되면서 본격적으로 부팅작업이 시작되는 것이다.

부트로더 폴더 구조 안에는 HW에 의존적인 폴더가 있고 그렇지 않은 폴더가 있다.


이 각각의 폴더 안에서 어디를 어떻게 바꿔야 포팅이 제대로 될 것인지를 알아야 한다.




부트로더 호출 순서

아래 플로우를 쫓아가면서 분석하면, 어떻게 해서 U-Boot에서 U_BOOT_CMD();라는 매크로를 통해 간편하게 기능구현이 가능해지는지 조금은 수 있를 것이다.

u-boot.lds ENTRY(_start)
arch/arm/lib/vectors.S _start
arch/arm/cpu/armv7/start.S reset
arch/arm/lib/crt0.S _main
common/board_r.c board_init_r
common/board_r.c init_sequence_r
common/board_r.c run_main_loop
common/main.c main_loop
common/autoboot.c autoboot_command
common/cli.c run_command_list
common/cli_simple.c cli_simple_run_command_list
common/cli_simple.c cli_simple_run_command
common/command.c cmd_process
common/command.c cmd_call --> result = (cmdtp->cmd)(cmdtp, flag, argc, argv);

커널 로드/booting
==>printenv
==>run bootcmd
bootcmd=mmc dev ${mmcdev}; if mmc rescan; then if run loadbootscript; then run bootscript; fi; udooinit; if run loadimage; then run mmcboot; else run netboot; fi; else run netboot; fi

/////////////////////////////////////////////////////////////////////////////////////////

mmc dev ${mmcdev}; //mmcdev = 0
if mmc rescan; then
if run loadbootscript; then
run bootscript;
fi;
udooinit;
if run loadimage; then //kernel image load
run mmcboot; //udoo-boot, kernel start
else run netboot;
fi;
else
run netboot;
fi



U-Boot 초기화 과정







부트로더 실습(1)

메모리 read/write 테스트

LED 모듈(led0~led7): gpio6 ~ gpio13, key 모듈(key0~key7): gpio16 ~ gpio23

우선 라즈베리파이의 데이터시트를 봐야 주변장치(led, key)들 제어하기 위한 GPIO 주소가 어딘지 알고 접근이 가능하다.
링크에 들어가서 우리가 쓰고 있는 라즈베리파이 모델에 대한 데이터시트를 보자.
(라즈베리파이 공식 도큐먼트도 보면 여러가지 정보를 찾아볼 수 있다. (교수님이 7인치 LCD를 뒤집기 위해서도 이 도큐멘트의 'accessories'를 참고하셨다고 한다.))

다시 돌아와서, 우리가 지금 봐야할 것은 라즈베리파이4 보드 도큐먼트인 "bcm2711-peripherals.pdf"이다. 해당 자료의 chapter5가 GPIO를 다루고 있다. pdf의 66쪽으로 가자. 그리고 블록도를 보자. STM32때랑 느낌은 비슷하다. (MCU가 cortex M이냐 A냐의 차이는 있지만, 같은 ARM이기 때문어 내부 동작방식은 거의 같다.)

다양한 주변기기들에다가 전부 전용pin을 부여하면 pin의 갯수가 엄청나게 많아져야 한다. 그러면 자연스레 MCU가 커지게 된다. MCU가 커지는 것은 소형화를 지향하는 MCU의 철학에 반하는 것이다. 그래서 보통은 하나의 핀이 여러개의 역할 할수 있도록 해놓는다. 그래서 라즈베리파이의 핀들도 보면 평상시에는 GPIO로 쓰다가, config에서 레지스터값을 바꿔주면 다른 용도로 동작하게 되는식으로 구성되어 있다.

메모리 직접접근으로 register를 조작하여 LED 켜보기

U-Boot> help
U-Boot> md			// md 명령어 사용법 보기

Usage:
md [.b, .w, .l] address [# of objects]         // 바이트, 워드, 롱

U-Boot> md.l 0xfe200000 4           
fe200000: 00000000 00012000 12000000 3ffff000  ..... .........?
U-Boot> md.l 0xfe200000 8
fe200000: 00000000 00012000 12000000 3ffff000  ..... .........?
fe200010: 00000064 00000000 00000000 6770696f  d...........oipg
U-Boot> md.l 0xfe200000 4

GPIO6에 꽂혀있는 LED의 주소를 확인한 것이다.
32bit 주소체계로 되어있기 때문에, 16진수 8자리로 되어있다.(fe200000)
(GPFSEL0 = GPIO Function Select 0)

즉, 위의 명령은 "0xfe200000" 주소에서부터 4개의 32비트 롱을 읽어와서 출력하라는 것이다. 결과적으로 16바이트(4바이트 * 4)의 데이터가 해당 메모리 주소에서 읽혀지고 출력될 것이다.

GPIO6 output으로 바꾸기 vis "GPFSEL 레지스터"

레지스터 관련 데이터시트 읽는 방법!!!

교재 806페이지를 적극적으로 참조하라!!

The GPIO has the following registers. All accesses are assumed to be 32-bit. The GPIO register base address is 0x7e200000.

다양한 레지스터(32비트)를 통해서 GPIO 포트에 대해 다양한 작업을 진행할 수 있다.
아래는 GPFSEL0 Register에 대한 description이다.

하나의 레지스터에는 32비트가 들어갈 수 있다. 하나의 GPFSEL0은 3개의 비트로 표현되는 GPIO포트에 대한 주소가 10개 들어가 있다. GPFSEL0에는 GPIO0번부터 GPIO9번까지에 대해 역할을 부여할 수 있는 것이다(3 * 10 = 30). 그리고 남은 2개의 비트는 reserved bit로 쟁여놓는다.

"하나의 레지스터셀"에 있는 10개의 FSEL의 비트값을 어떻게 설정하느냐에 따라서 해당 핀의 역할을 지정할 수 있다는 것이다.

3개의 비트열을 통해서 부가기능을 설정할 수 있다는 것이다.
FSEL0(Function select 0)에는 2:0니까 0, 1, 2의 3개의 비트를 통해서 GPIO0번에 대한 function select를 할 수 있다는 것이다.
FSEL1(Function select 1)에는 5:3니까 3, 4, 5의 3개의 비트를 통해서 GPIO1번에 대한 function select를 할 수 있다는 것이다.
FSEL2(Function select 2)에는 8:6니까 6, 7, 8의 3개의 비트를 통해서 GPIO2번에 대한 function select를 할 수 있다는 것이다.
FSEL3(Function select 3)에는 11:6니까 9, 10, 11의 3개의 비트를 통해 GPIO3번에 대한 function select를 할 수 있다는 것이다.

...

우리는 우선 GPIO 6번에 대해서만 출력을 지정하고 싶기 때문에 딱 하나의 FSEL만 제대로 지정하면 된다.(나머지는 어떻게 쓰이든 신경쓰지 않는다.)
FSEL6(Function select 6)에는 9:11니까 9, 10, 11의 3개의 비트를 통해서 GPIO6번에 대한function select를 할 수 있다는 것이다.

그 세개의 비트값을 어떻게 설정해주느냐에 따라서 그 핀의 역할을 정해줄 수 있다는 것이다.

우리의 LED는 GPIO6부터 시작하니까 위 그림에서 FSEL6번부터 보면 된다.
그리고 우리의 목표는 LED를 점등하는 것이니까 GPIO output에 해당하는 001이라는 3비트 값을 써주고 싶은 것이다.

U-Boot> mw.l 0xfe200000 0x00040000 

그래서 쓰고 싶은 코드는 위와 같다.
어디서 '4'라는 값이 튀어나온 것인가?
논리적으로 따질 때는 3비트씩 따지는데, 실제로 컴퓨터에게 값을 줄때는 16진수 단위로 읽을 때는 4비트씩 읽는다.

그래서 결과적으로 논리적으로 따질 때는 아래와 같이 되는데,
|reserved|GPIO9|8|7|6|5|4|3|2|1|0|
|00|000|000|000|001|000|000|000|000|000|000|

?실제로 컴퓨터에 값을 쓸 때는 4비트(1바이트)씩 따지기 때문에
0000|0000|0000|0100|0000|0000|0000|0000
라고 해서 "0x00040000"라는 값이 튀어나온 것이다.


완전 로우레벨 명령을 통해서 GPIO 포트의 상태가 GPIO Output으로 바뀐 것을 알 수 있다.

GPIO6 1로 set하기 via "GPSET 레지스터"

데이터시트의 71쪽을 보자.

GPSET0 레지스터에 값을 쓰는 것으로 GPIO 포트에 값을 적을 수 있다.(위에서 건드렸던 것은 GPFSEL0이다. 헷갈리지 마라!!!)

GPSET0라는 레지스터에는 32개의 비트가 있기 때문에, GPIO0번~GPIO31번 포트에 대한 0/1 값을 부여할 수 있다.

우리는 위에서 GPFSEL0을 통해서 GPIO6번 포트에 대해서만 output을 설정했다.
그래서 결과적으로 0b00000000000000000000000000100000이란 값을 GPSET0에 입력하면 된다. 0b00000000000000000000000000100000는 0x00000040이다.

고로 코드는 다음과 같다.

U-BOOT> mw.l 0xfe20001c 0x00000040      // GPSET0 gpio6 Set(LED0 on)
U-BOOT> mw.l 0xfe200028 0x00000040      // GPCLR0 gpio6 clear(LED0 off)


데이터 시트를 보고 set한 값을 clear하기 위한 레지스터가 따로 있다는 것을 알았다. 그래서 다른 레지스터 주소(0xfe200028)에 값을 써줬다.

로우레벨 건드리다가 UART 끊어지지 않게 조심하기

GPFSEL1을 건드릴 때는 조심해야 한다. 현재 gpio15, 16번에 uart가 연결되어 있기 때문에 거기 값을 000, 000으로 주면 UART 통신이 끊어져버리는 상황이 발생할 수 있다. 그래서 데이터 시트에 따라서 UART통신이 끊어지기 않기 위해서는 해당 gpio 포트의 alternative function0번 부분이 uart 통신이기 때문에, "100, 100"이라는 값을 반드시 잊지 말고 줘야한다.

이 내용을 참고하여 아래의 코드가 어떻게 저런 값이 나오는지 수기로 하나하나 적어가면서 체크해봐야겠다.
(우리는 uart를 tx1, rx1로 사용하고 있다. = 데이터시트 상 alternative function5)

U-BOOT> mw.l 0xfe200000 0x09240000        //GPFSEL0 gpio6~9 output
U-BOOT> mw.l 0xfe200004 0x00012249        //GPFSEL1 gpio10~13 output
U-BOOT> mw.l 0xfe20001c 0x00003fc0        //GPFSET0 gpio6~13 Set(LED all on)
U-BOOT> mw.l 0xfe200028 0x00003fc0        //GPFCLR0 gpio6~13 Clear(LED all off)
profile
스펀지맨

0개의 댓글