라즈베리파이 4 (BCM2711)에서 메모리 맵 I/O로 GPIO를 직접 제어해보는 예시 일부를 요약해서 필기해보았다.

메모리 맵 I/O (Memory-Mapped I/O)

개념:
CPU의 주소 공간에 메모리뿐 아니라 I/O 장치 레지스터(GPIO, UART, SPI 등)를 같은 주소 공간에 배치하는 방식.
→ CPU는 메모리에 접근하는 것과 동일한 방식으로 장치를 제어할 수 있음.
전체 메모리 공간을 메모리와 I/O 장치로 나눠서 사용

버스 요약

Address bus: CPU는 값을 가져오기 위해 접근할 메모리/장치 주소를 어드레스 버스를 통해 설정

Data bus: 데이터버스를 통해 값을 읽어옴.

Control bus: 읽기/쓰기 동작 제어 (입출력 방향 설정)

어드레스 버스를 통해 GPIO와 같은 H/W에 접근, 메모리 특정 주소를 사용해 GPIO에 값을 보내거나, 들어오는 값을 읽는다.

mmap()으로 라즈베리파이 led 제어해보기

bcm2711의 메모리 주소 맵핑

  1. BCM2711의 GPIO주소

GPIO base 주소: 0x7E200000 (I/O base + 0x200000)

  1. GPIO 주요 레지스터


GPFSELn (GPIO Function Select) : 각 GPIO 핀의 입출력 모드 설정 (핀당 3비트)

GPSETn : GPIO 출력 High (1로 설정)

GPCLRn : GPIO 출력 Low (0으로 클리어)

GPLEVn : 현재 입력 레벨 확인

  1. GPIO 방향 설정 (GPFSEL)

핀 하나당 3비트씩 할당하여 I/O 방향을 설정.

입력 : 000, 출력 : 001
특정 핀 번호를 GPFSEL 레지스터에서 찾는 방법:

총 0~5번으로 6개의 GPFSEL레지스터. gpio핀은 58개,
핀번호 / 10 → 사용할 GPFSEL 레지스터 선택

핀번호 % 10 → 그 레지스터 내에서의 비트 위치

mmap()

Memory-mapped I/O: I/O 장치를 메모리 주소 공간에 포함시켜 접근
Raspberry Pi 4 (BCM2711): GPIO base는 0xFE200000
직접 제어: /dev/mem → mmap()으로 매핑 → GPIO 레지스터 접근 → 토글
이제 mmap()을 알아보고 직접 제어를 해볼 것이다.

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);

(사물인터넷을 위한 리눅스 프로그래밍 with 라즈베리파이)

mmap() : fd가 가리키는 객체를 offset의 값을 기준으로 length 만큼 메모리에 매핑하도록 커널에 요청하면 매핑된 시작 주소가 반환된다.

munmap() : mmap()함수와 연결된 메모리 해제.

주요 코드 흐름

/dev/mem → mmap()으로 매핑 → GPIO 레지스터 접근 → 토글

  1. /dev/mem 열기커널이 관리하는 물리 메모리 공간에 접근하기 위해 /dev/mem 사용
  2. mem_fd = open("/dev/mem", O_RDWR | O_SYNC);
  3. mmap()으로 물리 주소를 가상 주소에 매핑GPIO 레지스터 주소 영역을 프로세스의 가상주소에 매핑
  4. gpio_map = mmap(NULL, GPIO_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, mem_fd, GPIO_BASE);
  5. GPIO 포인터 설정이제 gpio[]배열처럼 접근하면 실제 레지스터에 읽기, 쓰기 가능하다.
  6. gpio= (volatile unsigned *)gpio_map;
  1. GPIO를 출력으로 설정 하는예
GPIO\_OUT(gno); 

매크로로 해당 핀을 출력 모드(001)로 설정.

  1. LED 토글

    GPIO_SET(gno) //high
    sleep(1);
    GPIO_CLR(gno) //Low
    sleep(1);

    1초 간격으로 GPIO핀을 on/off하는 예.

  2. 리소스 해제!

    munmap(gpio_map, GPIO_SIZE);
    close(mem_fd);

- 문자 디바이스 파일 character device file

사용자 공간 애플리케이션이 /dev/ 아래 디바이스 파일을 통해

커널에 구현된 문자 디바이스 드라이버와 소통할 수 있게 하는 인터페이스.

  • 문자 디바이스는 데이터를 byte 스트림단위로 읽고 쓴다.
  • 키보드,프린터,마우스,콘솔,직렬 포트(UART)등.
  • file_operations 구조체의 read,write함수 등을 구현해 데이터 송수신 처리.

- 블록 디바이스 파일 Block device file

/dev/ 아래 존재, 사용자 공간 애플리케이션이 커널 모드의 블록 디바이스 드라이버와 소통하도록 하는 인터페이스.

  • 데이터를 블록 단위(512B~수KB)로 읽고 씀.→랜덤 액세스 가능.
  • HDD,SSD, eMMC,SD,USB저장장치,CD/DVD 등
  • block_device_operations 구조체와 I/O스케줄러를 이용해 읽기/쓰기 요청을 큐에 넣고 처리한다.

문자 디바이스 파일

디바이스의 주 번호와 부 번호


**디바이스 번호의 자료형**
dev_t : 주 번호 12비트/ 부 번호 20 비트 . 32비트의 크기

#define MAJOR(dev) ((unsigned int) ((dev) >> 20))
메이저 번호:  어떤 드라이버가 이 디바이스를 처리하는지 식별
#define MINOR(dev) ((unsigned int) ((dev) & 0xfffff))
마이너 번호: 동일 드라이버 내 어떤 장치인지(tty,tty0b

typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;

문자 디바이스 파일 번호 동적 할당과 등록

dev_t sk_dev;
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
alloc_chrdev_region(&my_dev, 0, 1, "my_dev");

*dev : 할당된 디바이스 번호를 저장할 포인터.(my_dev에 저장)
baseminor:할당 시작할 부 번호(minor) (0부터 시작) 
count: 필요한 부 번호 개수 (디바이스 수)                
name: /proc/devices에 등록될 이름 (정보용)       

문자 디바이스 번호 등록 (번호 공간 예약)

dev_t devno = MKDEV(GPIO_MAJOR,GPIO_MINOR);
미리 정한 메이저/마이너 번호를 dev_t로 생성
register_chrdev_region(devno,count,GPIO_DEVICE);
이미 정해진 번호를 커널에 예약 등록,이미 사용중->에러.

#include <linux/fs.h>
int register_chrdev_region(dev_t from, unsigned count, const char *name);

from : 등록할 첫번째 디바이스 번호(메이저,마이너가 포함된 dev_t)
count : 등록할 연속된 디바이스 번호 개수
name : 디바이스 이름 (/proc/devices에 표시)
리턴 : 성공 시 0 실패시 음수 에러 (-EBUSY)

void unregister_chrdev_region(dev_t from, unsigned count);
register_chrdev_region으로 예약한 번호 공간 해제하여 다른 드라이버가 쓸 수 있도록 함.
from : 해제할 첫번째 디바이스 번호(예약한 dev_t)
count :해제할 연속 디바이스 번호 개수

번호 충돌 가능성 있음
번호를 명확히 알고 있어야 함
레거시 코드나 특별한 번호 필요할 때 사용

설명
/proc/devices 커널에 등록된 모든 캐릭터 및 블록 디바이스의 리스트. register_chrdev_region()에 의해 등록됨.
./dev/my_dev 유저 공간에서 사용하는 장치 파일 (device node). 커널이 자동으로 만들지 않음. 직접 mknod하거나 udev 설정 필요.

file_operations 구조체에 연산 등록하여 open,read,write 등의 파일 연산 함수 사용

include./linux/fs.h

file_operations 구조체의 역할

  • 리눅스 커널에서 디바이스 드라이버가 파일 시스템 인터페이스를 구현할 수 있도록 해주는 콜백 함수 집합.
    이 구조체에 함수를 등록함으로써
    open(),read(),write(),ioctl(),mmap()등의 시스템 콜이 호출될 때, 해당 드라이버의 동작이 실행되도록 연결.

cdev 구조체 (linux/cdev.h)

struct cdev { struct kobject kobj;//sysfs와 연동하기 위한 커널 오브젝트 
struct module \*owner;//(THIS\_MODULE)이 모듈이 언로드 되지 않도록 참조를 잡아줌 
const struct file\_operations \*ops;//open,read,write 등 파일 연산 함수들 집합 
struct list\_head list;//내부 연결 리스트. cdev가 여러개 일때 리스트로 관리 
dev\_t dev;//major, minor 조합의 디바이스 번호 
unsigned int count;// cdev가 몇개의 마이너 번호 다루는지(대부분 1개) } __randomize_layout; 

cdev_init (fs/char_dev.c)

구조체를 초기화하고 file operations에 open,write 등의 연산을 연결한다.

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
    memset(cdev, 0, sizeof *cdev);
    INIT_LIST_HEAD(&cdev->list);
    kobject_init(&cdev->kobj, &ktype_cdev_default);
    cdev->ops = fops;
}
 /* cdev_init() - initialize a cdev structure
 * @cdev: the structure to initialize
 * @fops: the file_operations for this device
 */

cdev_add

커널 공간의 문자 디바이스 구조체(struct cdev)를 시스템에 연결하여, 유저가 /dev/~ 경로로 접근할 수 있도록 등록한다.

 /*cdev_add() - add a char device to the system
 * @p: the cdev structure for the device
 * @dev: the first device number for which this device is responsible
 * @count: the number of consecutive minor numbers corresponding to this
 *         device
 *
 * cdev_add() adds the device represented by @p to the system, making it
 * live immediately.  A negative error code is returned on failure.
 */
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
    int error;

    p->dev = dev;
    p->count = count;

    if (WARN_ON(dev == WHITEOUT_DEV)) {
        error = -EBUSY;
        goto err;
    }

    error = kobj_map(cdev_map, dev, count, NULL,
             exact_match, exact_lock, p);
             //cdev_map에 전역 맵에 해당 번호 등록. 
    if (error)
        goto err;

    kobject_get(p->kobj.parent);

    return 0;

err:
    kfree_const(p->kobj.name);
    p->kobj.name = NULL;
    return error;
}

mknod

/dev/에 노드 파일을 만들어서 ,등록된 문자 디바이스에 파일로 유저가 접근할 수 있게한다. 디바이스 번호(major,minor)를 가진 파일을 만든다.

실행 흐름 함수/명령어
1. 디바이스 번호 할당 커널에서 Major,Minor번호 할당 alloc_chrdev_region(&devmfirstminor,count,”name”)
2. cdev 구조체 초기화 및 등록 (cdev 구조체에 파일 연산자 연결) cdev_init(&cdev, &fops) cdev_add(&cdev,dev,count)
3. 유저 공간에서 디바이스 파일 생성 mknod /dev/name c (major) (minor)
4. 유저 프로그램에서 /dev/ 파일을 open() 호출하면 커널 드라이버의 open 함수 호출. open(”/dev/name”,flags)

유저 공간의 애플리케이션 코드에서 시스템 호출을 통해
커널을 통해 어떻게 디바이스 파일에 접근하여 장치를 사용하는지 함수 호출을 따라가보았다.

사물인터넷을 위한 리눅스 프로그래밍 with 라즈베리파이(서영진)

1. 애플리케이션 호출

    fd = open("/dev/sk_dev", O_RDWR);

2. 시스템 호출

AT_FDCWD : 현재 작업 디렉토리에서 상대경로로 파일을 연다.

커널이 제공하는 매크로 (→ syscall 테이블에 매핑되어 호출.)

do_sys_open→do_sys_openat2→do_filp_open


do_filp_open→path_openat→do_open→vfsopen


nd : 경로 파싱 과정의 상태 저장소

#define EMBEDDED_LEVELS 2 struct nameidata { struct path path; struct qstr last; struct path root; struct inode *inode; /* path.dentry.d_inode */ unsigned int flags, state; unsigned seq, next_seq, m_seq, r_seq; int last_type; unsigned depth; int total_link_count; struct saved { struct path link; struct delayed_call done; const char *name; unsigned seq; } *stack, internal[EMBEDDED_LEVELS]; struct filename *name; struct nameidata *saved; unsigned root_seq; int dfd; vfsuid_t dir_vfsuid; umode_t dir_mode; } __randomize_layout;

3. ** 가상 파일 시스템 do_open->vfs_open**

open.c/vfs_open

4. inode로 파일 연결 do_dentry_open

  • do_dentry_open
    static int do_dentry_open(struct file *f,
                  int (*open)(struct inode *, struct file *))
    {
        static const struct file_operations empty_fops = {};
        struct inode *inode = f->f_path.dentry->d_inode;
        int error;

        path_get(&f->f_path);
        f->f_inode = inode;
        f->f_mapping = inode->i_mapping;
        f->f_wb_err = filemap_sample_wb_err(f->f_mapping);
        f->f_sb_err = file_sample_sb_err(f);

        if (unlikely(f->f_flags & O_PATH)) {
            f->f_mode = FMODE_PATH | FMODE_OPENED;
            f->f_op = &empty_fops;
            return 0;
        }

        if ((f->f_mode & (FMODE_READ | FMODE_WRITE)) == FMODE_READ) {
            i_readcount_inc(inode);
        } else if (f->f_mode & FMODE_WRITE && !special_file(inode->i_mode)) {
            error = file_get_write_access(f);
            if (unlikely(error))
                goto cleanup_file;
            f->f_mode |= FMODE_WRITER;
        }

        /* POSIX.1-2008/SUSv4 Section XSI 2.9.7 */
        if (S_ISREG(inode->i_mode) || S_ISDIR(inode->i_mode))
            f->f_mode |= FMODE_ATOMIC_POS;

        f->f_op = fops_get(inode->i_fop);
        if (WARN_ON(!f->f_op)) {
            error = -ENODEV;
            goto cleanup_all;
        }

        error = security_file_open(f);
        if (error)
            goto cleanup_all;

        error = break_lease(file_inode(f), f->f_flags);
        if (error)
            goto cleanup_all;

        /* normally all 3 are set; ->open() can clear them if needed */
        f->f_mode |= FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE;
        if (!open)
            open = f->f_op->open;
        if (open) {
            error = open(inode, f);
            if (error)
                goto cleanup_all;
        }
        f->f_mode |= FMODE_OPENED;
        if ((f->f_mode & FMODE_READ) &&
             likely(f->f_op->read || f->f_op->read_iter))
            f->f_mode |= FMODE_CAN_READ;
        if ((f->f_mode & FMODE_WRITE) &&
             likely(f->f_op->write || f->f_op->write_iter))
            f->f_mode |= FMODE_CAN_WRITE;
        if ((f->f_mode & FMODE_LSEEK) && !f->f_op->llseek)
            f->f_mode &= ~FMODE_LSEEK;
        if (f->f_mapping->a_ops && f->f_mapping->a_ops->direct_IO)
            f->f_mode |= FMODE_CAN_ODIRECT;

        f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);
        f->f_iocb_flags = iocb_flags(f);

        file_ra_state_init(&f->f_ra, f->f_mapping->host->i_mapping);

        if ((f->f_flags & O_DIRECT) && !(f->f_mode & FMODE_CAN_ODIRECT))
            return -EINVAL;

        /*
         * XXX: Huge page cache doesn't support writing yet. Drop all page
         * cache for this file before processing writes.
         */
        if (f->f_mode & FMODE_WRITE) {
            /*
             * Depends on full fence from get_write_access() to synchronize
             * against collapse_file() regarding i_writecount and nr_thps
             * updates. Ensures subsequent insertion of THPs into the page
             * cache will fail.
             */
            if (filemap_nr_thps(inode->i_mapping)) {
                struct address_space *mapping = inode->i_mapping;

                filemap_invalidate_lock(inode->i_mapping);
                /*
                 * unmap_mapping_range just need to be called once
                 * here, because the private pages is not need to be
                 * unmapped mapping (e.g. data segment of dynamic
                 * shared libraries here).
                 */
                unmap_mapping_range(mapping, 0, 0, 0);
                truncate_inode_pages(mapping, 0);
                filemap_invalidate_unlock(inode->i_mapping);
            }
        }

        return 0;

    cleanup_all:
        if (WARN_ON_ONCE(error > 0))
            error = -EINVAL;
        fops_put(f->f_op);
        put_file_access(f);
    cleanup_file:
        path_put(&f->f_path);
        f->f_path.mnt = NULL;
        f->f_path.dentry = NULL;
        f->f_inode = NULL;
        return error;
    }

해당 inode를 파일에 연결한다.(f→f_path 참조 수 증가, 페이지 캐시용 mapping설정 등)

드라이버가 등록한 file operation을 읽어옴.

/dev/my_dev/에서 open이 null이면 op→에 등록된 .open 함수, (드라이버 등록한 함수,my_open,user_open...) 실행

리눅스 디바이스 드라이버 기본 뼈대 구조 (모듈로 구현할때)

1. init / exit 함수

드라이버 로딩 시 초기화 (init)

드라이버 제거 시 자원 해제 (exit)

2. register_chrdev(), platform_driver_register() 등으로 디바이스를 커널에 등록

3. file_operations 구조체

유저 공간 → 커널 공간 인터페이스

open, read, write, unlocked_ioctl, release 등 시스템 콜과 연결

4. 모듈 매크로

module_init(init_func) → 드라이버 init 함수 등록

module_exit(exit_func) → 드라이버 exit 함수 등록

5. 모듈 정보

MODULE_LICENSE("GPL") → 커널에 라이선스 정보 전달
(GPL이 아니면 커널 내부 심볼 접근 제한)

선택적으로 MODULE_AUTHOR, MODULE_DESCRIPTION, MODULE_VERSION 등을 추가

문자디바이스,블록디바이스, 플랫폼디바이스, 등 디바이스 드라이버 공부 시작때 정리해놓았던 것 처음부터 다시 블로그에 정리하려한다.

module_init부터 initcall섹션 까지 따라가보기

커널이 모듈을 로드하면 .initcall*.init 섹션을 순회하면서 함수 포인터를 하나씩 실행

그중 .initcall6.init에 들어간 initModule 함수가 호출!

이렇게, 작성한 초기화 함수가 모듈 로딩 시 자동 실행된다.

1.module_init(initModule)




2. __initcall(initModule)

include/linux/module.h (_init, _exit)

include/linux/init.h

  1. __define_initcall(initModule,init)
    )
  • fn 함수 포인터를
  • sectionc(__sec)라는 특정 섹션에 넣고 static으로 선언하며, __used 속성으로 컴파일러가 최적화하지 않도록 한다.

fn 함수 주소가 .initcallxxx같은 섹션에 기록되어서

커널이나 모듈 로드시 이 함수들을 찾아서 실행한다.

readelf -S hello_module.ko (정해진 섹션확인)

)


T initModule → init 함수가 실제 텍스트 섹션에 있음
syscall 등록 후 사용자 공간에서 호출하여 사용한다!

빌트인 vs 모듈 형태 시스템 콜

<1>테이블에 번호를 추가해 빌트인 사용하는 것과
<2>.ko 모듈로 올렸다 내렸다 사용하는 방법 정리 후
모듈로 디바이스 드라이버 구현 시 initcall로 어떻게 시스템 콜이 되는지! 따라가보겠따.

<1> syscall번호를 테이블에 넣어 커널에 빌트인하여 사용

  1. 커널 공간 코드 작성.

     // kernel/hello.c
     #include <linux/kernel.h>
     #include <linux/syscalls.h>
    
     SYSCALL_DEFINE0(hello)
     {
         printk(KERN_INFO "Hello, system call!\n");
         return 0;
     }
    1. Makefile에 추가
  2. linux/arch/arm64/tools/syscall_64.tbl에 번호 추가

  3. 재빌드

    cd linux
    KERNEL=kernel8
    make bcm2711_defconfig
    

time make ARCH=arm64 CROSS_COMPILE="ccache aarch64-linux-gnu-" -j6 Image.gz modules dtbs

sudo make -j6 modules_install

sudo cp /boot/firmware/$KERNEL.img /boot/firmware/$KERNEL-backup.img
sudo cp arch/arm64/boot/Image.gz /boot/firmware/$KERNEL.img
sudo cp arch/arm64/boot/dts/broadcom/.dtb /boot/firmware/
sudo cp arch/arm64/boot/dts/overlays/
.dtb* /boot/firmware/overlays/
sudo cp arch/arm64/boot/dts/overlays/README /boot/firmware/overlays/


5. application (유저 코드) 작성
```c
#include <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>

// 커널에서 지정한 syscall 번호 정의
#define __NR_hello 463

int main() {
    long ret;
    ret = syscall(__NR_hello);
    printf("syscall returned: %ld\n", ret);
    return 0;
}

<2> 모듈로 사용할때에 initcall 메커니즘.

  • 대부분 모듈 내에서 디바이스 드라이버 코드로 구현이 가능하다.

비교하여 정리하면,

  • 빌트인 → 안정적, 항상 사용 가능, 커널 수정 필요
    syscall_table[__NR_mycall] = &my_syscall_func;
  • 모듈 → 유연, 테스트 용이, 런타임 등록/해제 가능

재컴파일이 필요없는 커널의 오브젝트가되어 모듈로 올렸다 내렸다 사용하는 방법에 대해 정리에 앞서, 다음 글로 나눠 initcall로 모듈 로드 후 syscall 번호로 어떻게 호출되는지. 알아보겠다.

[Linux Foundation](- https://youtu.be/INWghYZH3hI?si=8K44yBoVqtcxhiFB)

위 U-Boot 부트로더 설명 영상을 보며 정리해보고,
코드를 타고타고 따라가보았다.
코드는 원래 노션 토글로 간추렸던 것들을 여기에 일단 다 옮겨보았는데, 좀 긴 편이니 아랫 부분에 두었습니다.
이글 전에 첫번재 작성한 U-Boot글이 더 쉽게 정리해놓았긴합니다만

U-Boot 사, Linux Foundation의 영상을 바탕으로도 정리해보았습니다. 부족한 내용정리라, 많은 조언 부탁드립니다~~

1. Boot ROM

  • 전원 인가를 하게 되면 SoC칩 내부에서 실행이 되는 코드(ROM). (EL3 익셉션 레벨3)
  • 칩 내부의 H/W의 기본 유닛을 초기화 설정.
  • Resides on reset vector(전원인가→리셋 벡터→boot ROM)
  • BootROM: on-chip boot loader 라고도 함.

  • Power-On Self-Test(POST) 자가 진단 수행 후, 비휘발성 메모리(NVRAM이나 CMOS)에 저장된 부팅 설정을 읽는다.
  • 부팅 디바이스 탐색 및 선택 후 1차 Bootloader를 로드한다
  • (Broadcom BCM2711)

2. 1st Bootloader

  • DRAM 컨트롤러 초기화 전. (가상 메모리X, DRAMX).
  • First bootloader는 램에서 실행되는게 아니라 칩 내부에 있는 Static RAM에서 실행되도록 보통 설계된다.
  • PMU,GPIO,UART,PMIC(전원 칩),DDR 컨트롤러와 같은 여러 IP를 초기화하는 역할(전원이나 클럭같은 기본적인 설정)
  • 2차 부트로더 로드(EL3→EL2)

  1. bootcode.bin
  2. U-Boot SPL(secondary program loader) (부트로더로 U-Boot를 쓸때)
    • u-boot.bin은 바로 boot ROM이 로드 하기에 커서, SPL으로 대신 로드(SRAM).
    • First user-controlled code(Boot ROM이후)
    • Responsible for additional HW initialization(DRAM,UART,i2C, PMIC, MMC등 H/W 초기화)
    • Loads U-Boot or Kernel directly
    • https://docs.u-boot.org/en/latest/usage/spl_boot.html

3. 2nd Boot loader(User boot loader, U-boot)

  • RAM에서 실행 (EL2)
  • 전체 하드웨어 초기화 board_init_f(),crto.s,board_init_r()
  • GPIO, UART, I2C, 타이머 등 주변 장치 초기화, 메모리 재배치,초기화
  • SD카드로 부터 mmc를 이용해 디바이스 트리(device tree)와 커널 이미지를 메모리에 로드.
  • Rootfs 경로를 bootargs로 커널에 전달.
  • 하드웨어 구성 정보를 담고 있는 구조체의 포인터 전달.
  • 커널에 커맨드 라인 포인터를 전달.
  • 제어권을 커널에게 넘겨주고, 메모리를 반환후 종료.

4. Linux Kernel

  • 커널 이미지 실행되면서 디바이스 트리 참조해서 초기화 수행(EL1)
  • startup code (head.S)
  • 주요 동작
    • MMU/캐시 설정
    • 디바이스 트리 파싱
    • 메모리/디바이스 초기화
    • mnt/root의 ext4 파일시스템을 “/”루트파일시스템로 mount 시도
    • →부팅 중 메시지로 출력

5. root FS- User space Process(init)

  • kernel_init()으로 PID1인 init 프로세스 실행
  • 루트 파일시스템에서 /sbin/init, systemd, init.d 스크립트 등 실행
  • system call, 쉘 등 사용자 공간 프로세스 시작

리셋부터, u-boot 부트로더 동작, init/main.c start_kernel()까지 소스코드

start.S ENTRY(_start)부터 start_kernel()까지 (cmd처리 중심)

  1. ENTRY(_start)
    파일 : arch/arm/cpu/armv8/start.S

  2. bl lowlevel_init


board_init_f()까지 코드를 안전하게 실행하기 위한 최소한의 초기화.
가능한 아무것도 하지 않는다.

  arch/arm/mach-bcm283x/lowlevel\_init.S  
![](https://velog.velcdn.com/images/yez_hong/post/42aff80b-2a3c-4984-86f1-192aefa5cdca/image.png))![](https://velog.velcdn.com/images/yez%5C_hong/post/42aff80b-2a3c-4984-86f1-192aefa5cdca/image.png)

3.start.s로 돌아와 bl _main으로 crto.s의 main으로 점프한다.

4.crt0.s:arch/arm/lib/crt0_64.s

-   crt0\_64.s 설명 주석코드

아래 보다시피

메모리 영역 재배치, bss영역 초기화 후 board_init_f로 분기한다.

crt0.s⇒다음 세가지 포함.

  • board_init_f
  • 하드웨어 초기사용을 위한 함수. 초기 메모리가 부족한 시점에서 실행되어 시스템 메모리(DRAM) 셋업 등 필수 환경을 준비.
  • relocate_code
  • U-Boot 바이너리를 실행 가능한 위치(DRAM 등)로 복사하고, 내부 주소를 재조정해 코드가 정상 실행되게 함.
  • board_init_r
  • 부팅 후반부 초기화 함수. 파일 시스템, 네트워크, 콘솔 등 다양한 장치를 초기화하며, 사용자 명령 입력 대기 상태로 전환.

1.board_init_f()
u-boot/common/board_f.c

board_init_r()로 넘어가기 전까지 시스템 메모리 초기화(SDRAM) 및 UART 등 기본 I/O준비.

다시 crto_64.s의 main으로 돌아간 후

메모리 재배치, bss영역 초기화 등 다음, board_init_r 호출

2.board_init_r()
u-boot/common/board_r.c

board_init f, r()로 2차 부트로더의 하드웨어, 주변장치 초기화 단계를 수행 후,

main_loop()→main.c로 진입한다.

3.common/main.c로 진입

사용자 버튼 처리(예 : GPIO 버튼 누를때 동작)
bootdelay처리,

부팅할 명령어 실행

  1. cli.c→ cli_loop()→cli_simple_loop호출

    common/cli_simple.c

    )

입력받기 위해 cli_readline 호출

common/cli_readline.c

cli_readline_into_buffer

cread_line_simple

  • cread_line_simple 전체코드
            static int cread_line_simple(const char *const prompt, char *p)
            {
             char *p_buf = p;
             int n = 0; /* buffer index */
             int plen = 0; /* prompt length */
             int col; /* output column cnt */
             int c;

             /* print prompt */
             if (prompt) {
             plen = strlen(prompt);
             puts(prompt);
             }
             col = plen;

             for (;;) {
             if (bootretry_tstc_timeout())
             return -2; /* timed out */
             schedule(); /* Trigger watchdog, if needed */

             c = getchar();

             /*
              * Special character handling
              */
             switch (c) {
             case '\r': /* Enter */
             case '\n':
             *p = '\0';
             puts("\r\n");
             return p - p_buf;

             case '\0': /* nul */
             continue;

             case 0x03: /* ^C - break */
             p_buf[0] = '\0'; /* discard input */
             return -1;

             case 0x15: /* ^U - erase line */
             while (col > plen) {
             puts(erase_seq);
             --col;
             }
             p = p_buf;
             n = 0;
             continue;

             case 0x17: /* ^W - erase word */
             p = delete_char(p_buf, p, &col, &n, plen);
             while ((n > 0) && (*p != ' '))
             p = delete_char(p_buf, p, &col, &n, plen);
             continue;

             case 0x08: /* ^H  - backspace */
             case 0x7F: /* DEL - backspace */
             p = delete_char(p_buf, p, &col, &n, plen);
             continue;

             default:
             /* Must be a normal character then */
             if (n >= CONFIG_SYS_CBSIZE - 2) { /* Buffer full */
             putc('\a');
             break;
             }
             if (c == '\t') { /* expand TABs */
             if (IS_ENABLED(CONFIG_AUTO_COMPLETE)) {
             /*
              * if auto-completion triggered just
              * continue
              */
             *p = '\0';
             if (cmd_auto_complete(prompt,
                   console_buffer,
                   &n, &col)) {
             p = p_buf + n; /* reset */
             continue;
             }
             }
             puts(tab_seq + (col & 07));
             col += 8 - (col & 07);
             } else {
             char __maybe_unused buf[2];

             /*
              * Echo input using puts() to force an LCD
              * flush if we are using an LCD
              */
             ++col;
             buf[0] = c;
             buf[1] = '\0';
             puts(buf);
             }
             *p++ = c;
             ++n;
             break;
             }
             }
            }
  • 사용자가 U-Boot 프롬프트에 입력한 문자열은 p_buf에 저장됨.

  • 엔터 치면 cread_line_simple은 입력한 문자열 길이를 리턴하고,

  • 그 다음 이 문자열은 명령어 파싱기로 넘어감.

  • simple_loop로 돌아와 문자열 길이를 받으면 last_command에 문자열을 복사해 rum_command를 실행

  • run_command_list (cli.c)

            int run_command_list(const char *cmd, int len, int flag)
            {
             int need_buff = 1;
             char *buff = (char *)cmd; /* cast away const */
             int rcode = 0;

             if (len == -1) {
             len = strlen(cmd);
            #ifdef CONFIG_HUSH_PARSER
             /* hush will never change our string */
             need_buff = 0;
            #else
             /* the built-in parser will change our string if it sees \n */
             need_buff = strchr(cmd, '\n') != NULL;
            #endif
             }
             if (need_buff) {
             buff = malloc(len + 1);
             if (!buff)
             return 1;
             memcpy(buff, cmd, len);
             buff[len] = '\0';
             }
            #ifdef CONFIG_HUSH_PARSER
             if (use_hush_old()) {
             rcode = parse_string_outer(buff, FLAG_PARSE_SEMICOLON);
             } else {
             rcode = parse_string_outer_modern(buff, FLAG_PARSE_SEMICOLON);
             }
            #else
             /*
              * This function will overwrite any \n it sees with a \0, which
              * is why it can't work with a const char *. Here we are making
              * using of internal knowledge of this function, to avoid always
              * doing a malloc() which is actually required only in a case that
              * is pretty rare.
              */
            #ifdef CONFIG_CMDLINE
             rcode = cli_simple_run_command_list(buff, flag);
            #else
             rcode = board_run_command(buff);
            #endif
            #endif
             if (need_buff)
             free(buff);

             return rcode;
            }
  • cmd_process (command.c)
            enum command_ret_t cmd_process(int flag, int argc, char *const argv[],
                    int *repeatable, ulong *ticks)
            {
             enum command_ret_t rc = CMD_RET_SUCCESS;
             struct cmd_tbl *cmdtp;

            #if defined(CONFIG_SYS_XTRACE)
             char *xtrace;

             xtrace = env_get("xtrace");
             if (xtrace) {
             puts("+");
             for (int i = 0; i < argc; i++) {
             puts(" ");
             puts(argv[i]);
             }
             puts("\n");
             }
            #endif

             /* Look up command in command table */
             cmdtp = find_cmd(argv[0]);
             if (cmdtp == NULL) {
             printf("Unknown command '%s' - try 'help'\n", argv[0]);
             return 1;
             }

             /* found - check max args */
             if (argc > cmdtp->maxargs)
             rc = CMD_RET_USAGE;

            #if defined(CONFIG_CMD_BOOTD)
             /* avoid "bootd" recursion */
             else if (cmdtp->cmd == do_bootd) {
             if (flag & CMD_FLAG_BOOTD) {
             puts("'bootd' recursion detected\n");
             rc = CMD_RET_FAILURE;
             } else {
             flag |= CMD_FLAG_BOOTD;
             }
             }
            #endif

             /* If OK so far, then do the command */
             if (!rc) {
             int newrep;

             if (ticks)
             *ticks = get_timer(0);
             rc = cmd_call(cmdtp, flag, argc, argv, &newrep);
             if (ticks)
             *ticks = get_timer(*ticks);
             *repeatable &= newrep;
             }
             if (rc == CMD_RET_USAGE)
             rc = cmd_usage(cmdtp);
             return rc;
            }

/* Look up command in command table */
cmdtp = find_cmd(argv[0]);
find_cmd()(command.c)

find_cmd(”boot”)는 cmdtp→cmd_rep(아래 설명)== do_booti_wrapper인 구조체를 찾아 반환한다.

cmd_call(command.c)

cmd_tbl을 통해 cmd_rep()으로 호출 (command.h)

  • cmd_rep는 struct cmd_tbl 내 명령어 실행 함수 포인터
    • 명령어 등록 매크로(U_BOOT_CMD 등)가 이 함수 포인터를 연결

command.h

  1. cmd/booti.c

이 매크로는 내부적으로 __u_boot_cmd라는 특수 섹션에 struct cmd_tbl을 등록, 이 구조체의 .cmd_rep 필드는 do_booti(), do_booti_wrapper()포인터가 된다.

  • do_booti함수 실행 (cmd/booti.c)
            int do_booti(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[])
            {
             struct bootm_info bmi;
             int states;
             int ret;

             /* Consume 'booti' */
             argc--; argv++;

             bootm_init(&bmi);
             if (argc)
             bmi.addr_img = argv[0];
             if (argc > 1)
             bmi.conf_ramdisk = argv[1];
             if (argc > 2)
             bmi.conf_fdt = argv[2];
             bmi.boot_progress = true;
             bmi.cmd_name = "booti";
             /* do not set up argc and argv[] since nothing uses them */

             if (booti_start(&bmi))
             return 1;

             /*
              * We are doing the BOOTM_STATE_LOADOS state ourselves, so must
              * disable interrupts ourselves
              */
             bootm_disable_interrupts();

             images.os.os = IH_OS_LINUX;
             if (IS_ENABLED(CONFIG_RISCV))
             if (IS_ENABLED(CONFIG_64BIT))
             images.os.arch = IH_ARCH_RISCV64;
             else
             images.os.arch = IH_ARCH_RISCV;
             else if (IS_ENABLED(CONFIG_ARM64))
             images.os.arch = IH_ARCH_ARM64;

             states = BOOTM_STATE_MEASURE | BOOTM_STATE_OS_PREP |
             BOOTM_STATE_OS_FAKE_GO | BOOTM_STATE_OS_GO;
             if (IS_ENABLED(CONFIG_SYS_BOOT_RAMDISK_HIGH))
             states |= BOOTM_STATE_RAMDISK;

             ret = bootm_run_states(&bmi, states);

             return ret;
            }
  1. 커널 entry로 점프
  2. - boot_jump_linux (bootm.c) 커널 entry주소로 점프 (보통 Image or vmlinux 시작 주소)
            int do_bootm_linux(int flag, struct bootm_info *bmi)
            {
             struct bootm_headers *images = bmi->images;

             /* No need for those on ARM */
             if (flag & BOOTM_STATE_OS_BD_T || flag & BOOTM_STATE_OS_CMDLINE)
             return -1;

             if (flag & BOOTM_STATE_OS_PREP) {
             boot_prep_linux(images);
             return 0;
             }

             if (flag & (BOOTM_STATE_OS_GO | BOOTM_STATE_OS_FAKE_GO)) {
             boot_jump_linux(images, flag);
             return 0;
             }

             boot_prep_linux(images);
             boot_jump_linux(images, flag);
             return 0;
            }

            #if defined(CONFIG_BOOTM_VXWORKS)
            void boot_prep_vxworks(struct bootm_headers *images)
            {
            #if defined(CONFIG_OF_LIBFDT)
             int off;

             if (images->ft_addr) {
             off = fdt_path_offset(images->ft_addr, "/memory");
             if (off > 0) {
             if (arch_fixup_fdt(images->ft_addr))
             puts("## WARNING: fixup memory failed!\n");
             }
             }
            #endif
             cleanup_before_linux();
            }

            void boot_jump_vxworks(struct bootm_headers *images)
            {
            #if defined(CONFIG_ARM64) && defined(CONFIG_ARMV8_PSCI)
             armv8_setup_psci();
             smp_kick_all_cpus();
            #endif

             /* ARM VxWorks requires device tree physical address to be passed */
             ((void (*)(void *))images->ep)(images->ft_addr);
            }
            #endif
            /* Subcommand: GO */
            static void boot_jump_linux(struct bootm_headers *images, int flag)
            {
            #ifdef CONFIG_ARM64
             void (*kernel_entry)(void *fdt_addr, void *res0, void *res1,
             void *res2);
             int fake = (flag & BOOTM_STATE_OS_FAKE_GO);

             kernel_entry = (void (*)(void *fdt_addr, void *res0, void *res1,
             void *res2))images->ep;

             debug("## Transferring control to Linux (at address %lx)...\n",
             (ulong) kernel_entry);
             bootstage_mark(BOOTSTAGE_ID_RUN_OS);

             announce_and_cleanup(fake);

             if (!fake) {
             do_nonsec_virt_switch();

             update_os_arch_secondary_cores(images->os.arch);
            #else
             if ((IH_ARCH_DEFAULT == IH_ARCH_ARM64) &&
                 (images->os.arch == IH_ARCH_ARM))
             armv8_switch_to_el2(0, (u64)gd->bd->bi_arch_number,
                 (u64)images->ft_addr, 0,
                 (u64)images->ep,
                 ES_TO_AARCH32);
             else
             armv8_switch_to_el2((u64)images->ft_addr, 0, 0, 0,
                 images->ep,
                 ES_TO_AARCH64);
            #endif
             }
            #else
            #endif
            }
  • arch/arm/cpu/armv8/transition.S
            .pushsection .text.armv8_switch_to_el2, "ax"
            ENTRY(armv8_switch_to_el2)
             bl armv8_switch_to_el2_prep
             nop
             switch_el x6, 1f, 0f, 0f
            0:
             cmp x5, #ES_TO_AARCH64
             b.eq 2f
             /*
              * When loading 32-bit kernel, it will jump
              * to secure firmware again, and never return.
              */
             bl armv8_el2_to_aarch32
            2:
             /*
              * x4 is kernel entry point or switch_to_el1
              * if CONFIG_ARMV8_SWITCH_TO_EL1 is defined.
                     * When running in EL2 now, jump to the
              * address saved in x4.
              */
             br x4
            1: armv8_switch_to_el2_m x4, x5, x6
            ENDPROC(armv8_switch_to_el2)
            .popsection

starting kernel 메세지 출력 (common/bootm.c) 메세지는 u-boot에서 출력된다.

(하드웨어관련 메세지도 추가하기)

커널 이미지 로딩 후 , head.S 실행. ~/linux/arch/arm64/kernerl/head.S에서 start_kernel()로 진입

init/main.c :start_kernel() 진입해 초기화

        [U-Boot]
        cli_loop()
         └─> cli_simple_loop()
             └─> cli_readline()             // 콘솔에서 사용자 명령어 입력 대기 및 읽기
             └─> run_command("booti 80000 - 90000")  // 입력받은 명령어 실행 요청
                 └─> run_command_list()     // 명령어가 여러 개일 경우 분리해서 처리
                     └─> cmd_process()      // 명령어 문자열에서 명령어 구조체 검색 및 실행
                         └─> find_cmd("booti")   // 명령어 테이블에서 'booti' 명령어 찾음
                             └─> do_booti()       // 'booti' 명령어의 실제 실행 함수 호출 (cmd/booti.c)
            → booti_start(&bmi)
            → bootm_disable_interrupts()
            → bootm_run_states()
               └─> BOOTM_STATE_OS_PREP
               └─> BOOTM_STATE_OS_GO
                   └─> arch/arm/lib/bootm.c: do_bootm_linux()
                       └─> kernel 이미지 주소로 분기 → 커널 실행!


        [Linux Kernel]
        arch/arm64/kernel/head.S
         └─> stext
             └─> start_kernel()
                 └─> rest_init()
                     └─> kernel_init()

⇒ 전원을 켰을 때 main loop 안의 autoboot에 의해 시간 내에

키보드 입력이 들어오면 u-boot 프롬프트로 진입하고, run bootcmd(디폴트 부팅 커맨드)를 입력하면 부팅하여 start kernel 한다.

이제 명령어 해석기 (CLI)에서 계속 커맨드 받는 동작!!

start_kernel()이후 첫 프로세스 실행까지

  • start_kernel()이후 소스코드 동작 (보완하기)
    // Kernel startup entry point
    - 부트로더가 ARM64의 레지스터 x0에 디바이스 트리의 주소를 넣어주고,
     커널이 하드웨어 정보를 읽는다.
     (MMU랑 데이터 캐시 등이 비활성화된 상태여야 함)

    x0 = __pa(KERNEL_START)
    bl start_kernel

init/main.c 의 start_kernel()

  • 여기서 시스템 전역 데이터 구조 초기화, 스케줄러, 인터럽트, 타이머, 콘솔 등 핵심 서브시스템이 순차적으로 초기화된다.
  • start_kernel()의 마지막 rest_init()이 호출되고, 하는 역할

  • 첫번째 커널 스레드 생성 kernel_init()→ do_initcalls()를 통해 각종 서브시스템, 드라이버, 모듈을 초기화하는 스레드이다.

![](https://velog.velcdn.com/images/yez_hong/post/2fd2bf70-d4a2-4f30-93ae-ed3cdad6a9f2/image.png)
        static noinline void __ref __noreturn rest_init(void)
        {
         struct task_struct *tsk;
         int pid;

         rcu_scheduler_starting();
         /*
          * We need to spawn init first so that it obtains pid 1, however
          * the init task will end up wanting to create kthreads, which, if
          * we schedule it before we create kthreadd, will OOPS.
          */
          // 유저모드 스레드 생성1
         pid = user_mode_thread(kernel_init, NULL, CLONE_FS);
         // 커널모드 스레드 생성2
         pid = kernel_thread(kthreadd, NULL, NULL, CLONE_FS | CLONE_FILES);
         system_state = SYSTEM_SCHEDULING;
         /* Call into cpu_idle with preempt disabled */
         cpu_startup_entry(CPUHP_ONLINE);
        }
-----------------------------------------------------------------
        void cpu_startup_entry(enum cpuhp_state state)
        {
         current->flags |= PF_IDLE;
         arch_cpu_idle_prepare();
         cpuhp_online_idle(state);
         while (1)
         do_idle();
        }
  • kernel_init()내부 주요 흐름
  • - 루트 파일 시스템 마운트 시도 - initrd나 initramfs처리 - do_basic_setup()→do_initcalls()호출
            static int __ref kernel_init(void *unused)
            {
             kernel_init_freeable();
             ->  do_basic_setup(); -> do_initcalls(); 호출
             => 유저 모드를 사용하기 전, 
             커널의 주요 기능을 사용하기 위한 초기화 완료
  • do_initcalls()는
  • - 커널에 등록된 각종 초기화 함수들을 실행 (드라이버, 서브시스템 등) - __initcall_start ~ __initcall_end 섹션의 함수들을 순차적으로 호출 - 두번째 스레드 : do_idle()로 진입해 스케줄러가 일을 줄 때까지 대기

사용자 공간(userspace) 진입

  • kernel_init() 함수가 마지막으로 init 프로세스(PID 1)를 실행.
  • /sbin/init, 또는 systemd, upstart, busybox init 등이 사용된다.
  • 이후 사용자 공간에서 서비스, 로그인, 데스크탑 환경 등 다양한 프로세스가 실행됨.
static int run_init_process(const char *init_filename)
{
 const char *const *p;

 argv_init[0] = init_filename;
 pr_info("Run %s as init process\n", init_filename);
 pr_debug("  with arguments:\n");
 for (p = argv_init; *p; p++)
 pr_debug("    %s\n", *p);
 pr_debug("  with environment:\n");
 for (p = envp_init; *p; p++)
 pr_debug("    %s\n", *p);
 return kernel_execve(init_filename, argv_init, envp_init);
}

라즈베리파이에서 UART3 시리얼 통신을 이용해 PC에서 Tera Term으로 문자를 출력됨을 확인해보기(minicom이용).(dtoverlay 작성을 해보는 간단한 연습)

예전에 했던 실습인데, 장치를 활성화 할때, menuconfig 설정으로 활성화를 하는 간단한 방법도 있지만,
이렇게 기존 dts 방식을 찾아가 수정하거나, 새로운 오버레이를 작성하는 연습을 했던 이유를 프로젝트 끝나서 느꼈다.
라즈베리파이 처럼 잘 알려지지 않은 타사의 보드에서 원하는 통신 모듈이나, 장치를 이용하고자 할때 호환이 되거나, 테스트된 정보가 없는 경우가 많다. 이럴때에 아래와 같은 dts 디바이스 트리 노드가 있는지 찾아본 후 , 수정하거나 추가함으로써 포팅을 시도하여 쓸 수 있다.

1. uart3 4,5번 핀 → TX,RX

  • 핀맵

2. /boot/fimware/config.txt 추가

3. 연결 확인

4. minicom 실행해 연결 확인

    sudo minicom -b 115200 -D /dev/ttyAMA3

minicom에서 문자 입력하여 tera term 터미널에 출력 확인

5. 프로그램 코드 구현

    #include <stdio.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <string.h>

    #define BUFFER_SIZE 1024

    int main(int argc, char *argv[]) {
        if (argc < 2) {
            printf("Usage: %s <string to send>\n", argv[0]);
            return 1;
        }

        char *message = argv[1];

        // UART3 포트 열기
        int fd = open("/dev/ttyAMA3", O_RDWR | O_NOCTTY | O_SYNC);
        // 읽기/쓰기 모드로 열며, 제어 터미널로 사용되지 않도록 설정하고, 쓰기 연산이 완료될 때까지 대기하는 동기식 모드로 열겠
        if (fd < 0) {
            perror("Failed to open /dev/ttyAMA3");
            return -1;
        }

        //  (UART3 포트로 보내기)
        ssize_t bytes_written = write(fd, message, strlen(message));
        if (bytes_written < 0) {
            perror("Failed to write to UART3");
            close(fd);
            return -1;
        }

        printf("Written %zd bytes: %s\n", bytes_written, message);

        // (UART3 포트에서 읽기)
        char buffer[BUFFER_SIZE];  // 버퍼 크기
        ssize_t bytes_read;
        int i = 0;


        while ((bytes_read = read(fd, &buffer[i], 1)) > 0) {

            if (buffer[i] == '\n' || buffer[i] == '\r') {
                break;
            }
            if(i<BUFFER_SIZE -1){
            i++;}
        }

        if (bytes_read < 0) {
            perror("Failed to read from UART3");
            close(fd);
            return -1;
        }

        buffer[i] = '\0';  
        printf("Read %d bytes: %s\n",i+1, buffer);

        close(fd);
        return 0;
    }
    gcc -o uart3_test uart3_test.c

실행 파일 생성

6. .dtbs 작성

기존 .dtsi 참고

bcm2711.dtsi

)

  1. 첫번째 시도 (/dev/ttyAMA3 장치파일 안뜸)
        /dts-v1/;
        /plugin/;

        / {
            compatible = "brcm,bcm2711";

            // UART3 포트 활성화 및 하드웨어 설정
            uart3: uart@7e201600 {
                compatible = "arm,pl011";  // UART3에 대한 호환성 설정
                reg = <0x7e201600 0x200>;  // UART3의 레지스터 주소와 크기
                status = "okay";  // UART3 포트 활성화
            };
        };

기존 .dtbs파일의 status 만 활성화하여 사용해보기

  1. 두번째 시도 (시리얼 통신 동작함)

*인터럽트, 클럭을 지우고 함(기존 파일 참고해 써볼 수는 있을 것 같다)

        /dts-v1/;
        /plugin/;

        / {
            compatible = "bcm2711,pi17";  // Raspberry Pi 4에 맞는 설정

            // fragment 0: UART3 포트 활성화
            fragment@0 {
                target = <&uart3>;
                __overlay__ {
                    status = "okay";  // UART3 포트 활성화
                };
            };

            // fragment 1: UART3 하드웨어 설정
            fragment@1 {
                target = <&uart3>;
                __overlay__ {
                    compatible = "arm,pl011";  // UART3에 대한 호환성 설정
                    reg = <0x7e201600 0x200>;  // UART3의 레지스터 주소와 크기
                //    interrupts = <GIC_SPI 121 IRQ_TYPE_LEVEL_HIGH>;  // 인터럽트 설정
                //    clocks = <&clocks BCM2835_CLOCK_UART>, <&clocks BCM2835_CLOCK_VPU>;  // 클록 설정
                 //   clock-names = "uartclk", "apb_pclk";  // 클록 이름 설정
                };
            };
        };

7. .dtbo 빌드

    dtc -@ -I dts -O dtb -o uart3-overlay.dtbo uart3-overlay.dts

인터럽트,클럭 관련 지워보고 시도 

warning이 나지만 시도해봄.

8. .dtbo파일 /boot/overlays에 복사

    sudo cp uart3-overlay.dtbo /boot/overlays
    sudo nano /boot/firmware/config.txt

config.txt에 추가

9. reboot 후 실행

   ls -l /dev/ttyA*로 장치 파일 뜨는지 확인.

문자열 helloimpi17 쓰기 pc→teraterm 송신한 출력 결과

teraterm에서 byebye 읽어 출력.

)

확인 완료!

Linux-Foundation

[RPI U-Boot](- https://elinux.org/RPi_U-Boot)

[Environment Variables](- https://docs.u-boot.org/en/latest/usage/environment.html)

- 부트로더의 역할

booting: 전원 인가 후 하드웨어 초기화(CPU,메모리, 클럭 설정 등)

loading: 운영체제인 커널을 로딩함.

역할 완료 후 제어권을 OS에 넘기면서 더 이상 메모리에 상주하지 않음.

U-Boot 빌드 및 부팅 흐름 요약

  • U-Boot(Universal Bootloader)

    • 오픈소스 2차 부트로더. 다양한 아키텍처(PowerPC,ARM,MIPS등)를 지원함

    • boot ROM→SPL1차 부트로더→2차부트로더 로드. (boot ROM에서 로드된다)

    • 하드웨어를 초기화하거나, 테스트를 한다.

    • 또한 애플리케이션을 다운로드,실행을 할 수 있다.

    • 리눅스 이미지 로드.

    • 같은 쉘에서 다른 보드의 명령어를 실행할 수 있도록하고 새로운 커맨드를 추가할 수 있는 확장성과 confiurable한 특징이 있다.

    • readme 설명

        This directory contains the source code for U-Boot, a boot loader for
        Embedded boards based on PowerPC, ARM, MIPS and several other
        processors, which can be installed in a boot ROM and used to
        initialize and test the hardware or to download and run application
        code.
      
        The development of U-Boot is closely related to Linux: some parts of
        the source code originate in the Linux source tree, we have some
        header files in common, and special provision has been made to
        support booting of Linux images.
      
        Some attention has been paid to make this software easily
        configurable and extendable. For instance, all monitor commands are
        implemented with the same call interface, so that it's very easy to
        add new commands. Also, instead of permanently adding rarely used
        code (for instance hardware test utilities) to the monitor, you can
        load and run it dynamically.

U-Boot 빌드 과정 (Make→ Link 까지)

1. Make

  • 어떤 C/ASM 소스가 빌드 대상이 될지 결정된다.
  • Makefile + Kconfig 시스템으로 보드/아키텍처 설정 반영
    • make → .config 생성
    • .config → include/autoconf.mk, include/autoconf.h 생성
    • 여기서 어떤 소스가 컴파일되고, 어떤 드라이버와 아키텍처 설정이 필요한지 결정

2. 링크 스크립트 생성
- u-boot.lds.S 파일을 전처리(cpp_하여 u-boot.lds 생성

예)

 cpp -E -P -include autoconf.h -include config.h -D__ASSEMBLY__ [u-boot.lds](http://u-boot.lds).S > u-boot.lds → GNU ld에 전달
    cpp -E -P \
        -include autoconf.h -include config.h \
        -D__ASSEMBLY__ \
        u-boot.lds.S > u-boot.lds
이 스크립트는 u-boot 이미지의 메모리 배치를 결정!

u-boot.lds 주요 내용

  • ENTRY(_start) → 실행 시작 심볼 지정

  • .text → 코드 영역 (start.o가 맨 앞에 배치)

  • .rodata, .data, .bss → 데이터 섹션

  • .u_boot_list →U-boot의 드라이버 / 명령어 등록용 리스트 섹션.

    → 이 단계 에서 실행 이미지의 “지도”가 만들어진다고 보면 된다!

3. 최종 링크
- ld -T u-boot.lds -o u-boot 명령으로 링크 수행
- start.o 포함 (텍스트 섹션 앞, 주소 0에 위치)→reset vector 역할.
- ENTRY(_start)로 _start 심볼을 실행 진입점으로 지정


리셋 이후 실행 흐름

Reset Vector→ _start (arch/xxx/cpu/start.S)

  • SoC 리셋 후 PC는 리셋 벡터 주소 가리킴(0x00000000 또는 0xFFFF0000)
  • U-Boot의 start.s 안 ENTRY(_start)가 이 위치에 배치!
  • 주요 역할
    • CPU 레지스터 초기화, 스택 포인터 설정, 캐시,MMU 설정 등 진행
  • 이후 reset : 레이블로 분기

reset 함수(start.S)

  • 하드웨어 초기화를 위한 아주 초반 단계
    • ROM/부트로더에서 넘긴 부트 파라미터 저장 (save_boot_params)
    • PIE(Position Independent Execution) 지원을 위한 코드/데이터 재배치 작업 수행 (.rela.dyn 처리)
  • 준비 끝나면 crt0.S 내의 _main 함수 호출! 진입!

_main (arch/xxx/lib/crt0.S)

  • 진짜 C 런타임으로 넘어가기 전의 준비 코드
  • 주요 동작
    • 초기 스택 재설정 (SRAM 및 캐시 영역)
    • board_init_f(0) 함수 호출

board_init_f (arch/xxx/lib/board_f.c)

  • 초기 하드웨어 초기화

  • 여기선 아직 DRAM을 쓰지 않아, 필수 장치만 켜준다.

    UART(디버깅 출력), 클럭,PLL , DRAM 컨트롤러 초기화 수행

    DRAM 주소 및 리로케이션 대상 주소 설정

    초기화 완료 후 _main 함수로 복귀

_main 복귀 이후 (crt0.s)

  • 이제 DRAM 사용 가능!
  • DRAM에 새로운 스택 포인터(SP)와 GD(Global Data) 구조체 설정
  • relocate_code 호출:
    • U-Boot 이미지를 ROM/Flash에서 DRAM으로 복사
    • PC를 DRAM 내 새 U-Boot 위치로 점프
  • relocate_vectors, c_runtime_cpu_setup호출 → 예외 벡터 설정, BSS 영역 초기화 진행

board_init_r(gd, gd->relocaddr) 호출(arch/xxx/lib/board_r.c)

  • 본격적인 런타임 단계.U-Boot 실행 시작
  • 모든 드라이버와 하위 시스템 초기화 (콘솔출력,eMMC/SD/NAND,네트워크, 환경변수로드)
  • U-Boot 메인 루프 진입. → autoboot(커널 로드,실행), CLI (명령어 해석기)진입!

+ Recent posts