DeedOS BL12 - TPL 분석

ref

RK3588 부팅과정

BL1

전원이 켜지고 나서 CPU0(메인코어)는 MaskROM의 0x0000_0000 주소에서 하드코딩된 BL1 명령어를 하나씩 가져와서 실행한다.

기본 시스템 클럭을 설정하기 위해 PLL을 초기화하고, 하드웨어 STRAP 핀 레벨(SPINOR > eMMC > SDCard ..)에 따라 부팅 우선순위를 결정해서 BL2를 SRAM의 0xff72_0000 근처로 불러오게 된다. (이건 확실하지 않음. 나라면 헤더만 불러올거같긴 한데..)
실패 시 MaskROM 모드로 진입해서 정상 BL2가 플래시되길 기다린다.

이 시점에서 이미 EL3(시큐어모니터) + aarch64 상태로 CPU가 작동중이다.

불러오고나면 이미지 헤더에서 TPL을 먼저 실행시키고, TPL이 리턴되면 SPL을 실행시키는 순서대로 진행된다.

BL2

BL2는 시스템을 구축하기 시작한다.
rk3588 에서 idbloader.img 는 u-boot의 이미지 툴인 mkimage를 사용해서 rksd 헤더를 붙인 이미지여야 한다.

DDRBIN(TPL)은 bsp에서 바이너리로 제공되는 파일을 사용해야하지만, SPL 은 u-boot에서 제공하는 rk3588용 spl 코드를 빌드해서 쓸 수 있다.

git clone --depth 1 https://github.com/rockchip-linux/rkbin

DDRBIN=rkbin/bin/rk35/rk3588_ddr_lp4_2112MHz_lp5_2400MHz_v1.19.bin
SPL=rkbin/bin/rk35/rk3588_spl_v1.13.bin

./rkbin/tools/mkimage -n rk3588 -T rksd -d ${DDRBIN}:${SPL} idbloader.bin 

이미지 구조

// u-boot\tools\rkcommon.h
struct header0_info_v2 {           // 총 2048 bytes (2KB)
    uint32_t magic;                // 0x00: 0x534E4B52 ("RKNS")
    uint8_t  reserved[4];          // 0x04: 예약
    uint32_t size_and_nimage;      // 0x08: [31:16]=이미지 개수, [15:0]=해시 오프셋
    uint32_t boot_flag;            // 0x0C: [3:0]=해시 타입 (1=SHA256)
    uint8_t  reserved1[104];       // 0x10: 예약

    struct image_entry images[4];  // 0x78: 이미지 엔트리 (각 88 bytes)

    uint8_t  reserved2[1064];      // 패딩
    uint8_t  hash[512];            // 헤더 SHA256 해시
};

struct image_entry {               // 88 bytes
    uint32_t size_and_off;         // [31:16]=크기(블록), [15:0]=오프셋(블록)
    uint32_t address;              // 로드 주소 (0xFFFFFFFF = 기본값)
    uint32_t flag;                 // 미사용
    uint32_t counter;              // 이미지 번호 (1, 2, ...)
    uint8_t  reserved[8];          // 예약
    uint8_t  hash[64];             // 이미지 SHA256 해시
};

idbloader.bin 이미지를 분석하면 이렇게 나온다. (블록은 512byte 단위이다.)

f33813c9-2808-4d66-97dd-078b9ef188b8
f33813c9-2808-4d66-97dd-078b9ef188b8

이 헤더 위치가 끝나면 바로 TPL 바이너리를 확인할 수 있다.
0x800 == 2KB

40c92fe9-f069-45d4-9e40-9d51e48e6d61
40c92fe9-f069-45d4-9e40-9d51e48e6d61

image_entry에 TPL 로드 주소가 Default 값을 사용하도록 적혀있기 때문에 MaskROM에서 결정할 것이다. 메모리 레이아웃은 radxa가 공유하지 않기 떄문에 확실히 알수는 없다.

// TODO
아래에서 TPL 로드 image_base가 `0xff001000` 인 것을 검증하는 걸 보니 SRAM의 시작부분부터 idbloader.bin을 로드하지만 TPL이나 SPL의 코드섹션은 page_size 0x1000에 맞춰서 메모리에 로드하는 것으로 생각된다.      
(나중에 덤프해서 확인해보면 좋을듯) SPL 올린 후 UART 로 출력?

TPL

TPL에서 많은 일을 하지만 바이너리를 직접 분석해보면 별거 없다.

  • 기준 바이너리 : rkbin 프로젝트의 rk3588_ddr_lp4_2112MHz_lp5_2400MHz_v1.19.bin
  • 사용한 도구 : binary ninja + mcp

첫번째 만나는 함수

MaskROM 코드에서 호출된 첫 함수이다. 스택에 x0, x4, fp, lr(MaskROM 리턴주소) 를 백업해둔다. 아래쪽에서는 j_sub_10978 호출 직전에 복원하기 때문에 중요하진 않다.

bl sub_2c 명령으로 인해 lr이 TPL의 주소인 imagebase + 0x14 로 업데이트 되면서 TPL이 로드된 imagebase가 0xff0010xx 형태인지 확인하는 코드가 된다.
실패시 0x1c로 제자리 점프해서 CPU를 멈추는 역할을 하고 성공하면 j_sub_10978 로 점프한다.

RK3588 TRM V1.0 - Part 1.pdf 문서에 FF000000 주소가 SYSTEM_SRAM 으로 나와있기 때문에 이 코드는 현재 코드가 SRAM의 1000 offset에 정상로드 됐는지 확인하는 코드이다.

ed063d73-99a6-4a5f-adef-52f3d91be80d
ed063d73-99a6-4a5f-adef-52f3d91be80d

sub_10978 (TPL 메인함수)

실제 메인이다. 간단하게 코멘트를 달아놨다. MaskROM으로 돌아가는 lr을 계속 스택에서 보관하며 실행됐기 때문에 보라색 위치에서 종료되면 MaskROM 의 코드로 돌아가게 된다.

70228e60-ceef-4f0d-a613-ce6ab6b61d45
70228e60-ceef-4f0d-a613-ce6ab6b61d45

ic, isb 명령은 모든 CPU의 Instruction 캐시를 초기화하는 역할을 한다.
캐시가 초기화된 이후엔 CPU가 다음 명령을 캐시에서 가져오는게 아니라 메모리에서 가져오게된다.

000109c8  ic    ialluis    ; I-Cache 전체 무효화 (Inner Shareable) 
000109cc  isb              ; Instruction Synchronization Barrier (캐시동기화)

sub_9a68 (파라미터 복사)

MaskROM으로부터 전달받은 arg1 에서 TPL의 영역인 0xff016f58로 32byte를 복사한다.
나중에 DDR 초기화 함수에서 사용된다.

574ba863-6472-4f60-b634-2e93a86e4021
574ba863-6472-4f60-b634-2e93a86e4021

sub_9a0 (시스템 설정)

8c449bdb-585d-478e-800b-6b2b9cf659a2
8c449bdb-585d-478e-800b-6b2b9cf659a2

GPIO는 SoC에서 범용적인 입출력용으로 사용할 수 있는 핀이며, RK3588은 GPIO0-4 까지 5개의 뱅크, A-D의 서브그룹, 0-7의 핀을 갖게되어 총 160개의 GPIO핀을 갖고있다.

대부분은 보드 내부에서 장치들을 제어하는데 사용하고 있고, 일부 핀은 디버깅핀으로 뽑혀져 보드에서 확인할 수 있다.

fd5adcdb-98e6-435f-be0f-eff0121c6f82
fd5adcdb-98e6-435f-be0f-eff0121c6f82

여기에서 세팅하는 값이 gpio4d_iomux_sel_l 레지스터를 의미하고 GPIO4D 그룹의 하위 4핀(D0-D3) iomux를 선택할 때 접근한다.
이 unsigned int의 각 바이트를 핀 하나로 보고 0xFF00_5500을 쓰는것은 D3, D2 핀의 mux에 값 5를 쓴다는 의미이다. (gpio 핀에 쓰는게 아님)

// arch/arm/include/asm/arch-rockchip/ioc_rk3588.h
#define BUS_IOC_BASE    0xfd5f8000

struct rk3588_bus_ioc {
    unsigned int reserved0000[3];      // 0x0000 - 0x0008
    unsigned int gpio0b_iomux_sel_h;   // 0x000C
    ...
    unsigned int gpio4d_iomux_sel_l;   // 0x0098
    unsigned int gpio4d_iomux_sel_h;   // 0x009C
};

// /dts/upstream/src/arm64/rockchip/rk3588-base-pinctrl.dtsi
// 이건 u-boot에서 pin을 세팅하는 dtsi 인데, 이걸보면 값 5가 jtag_tck_m0 임을 알수있다.
jtag {
    jtagm0_pins: jtagm0-pins {
        rockchip,pins =
            /* jtag_tck_m0 */
            <4 RK_PD2 5 &pcfg_pull_none>,   // GPIO 4D2 = 5 와 동일
            /* jtag_tms_m0 */
            <4 RK_PD3 5 &pcfg_pull_none>;
    };

이 GPIO 핀들은 하드와이어된 물리적인 배선 위에서 mux 값을 변경하며 그때그때 필요한 기능으로 동작하는데, GPIO4D2의 5번은 JTAGM0_TCK, GPIO4D3의 5번은 JTAGM0_TMS 인 것이다.

mux=5 세팅 이후부터는 해당 GPIO에서 나오는 신호는 JTAG 신호가 되고, 그것과 연결된 보드의 어떤 포트에서 읽거나 쓸 수 있게 된다.

운좋게 dtsi에서 정의를 확인할 수 있었지만, TPL에서만 쓰는 기능이였다면 dtsi에서 확인하지 못했을 것이다.

280b96fe-7408-4330-bc63-40ce5b262f48
280b96fe-7408-4330-bc63-40ce5b262f48

재밌는건 이 핀이 mux=1로 세팅하면 SDCard 데이터핀으로 동작(공식문서에도 적혀있음)하는데, 물리회로를 공유하니까 mux=5 일때 보드의 SDCard 슬롯을 통해서도 JTAG TCK/TMS 역할을 할 수 있게된다. 뭔소리인가 싶었지만 사진을 보면 바로 이해가 된다. 링크

RK3588은 ARM SWD (Serial Wire Debug) 방식을 사용해서 TMS 혼자서 TDI/TDO 기능을 모두 처리할 수 있다.

부트로더 초기에 JTAG로 동작하다가 나중에 SDCard 슬롯으로 mux를 변경하는 방식일거라서 저 이상한 커넥터를 연결하고 기기리셋하면 JTAG로 붙게될것이다.

이제야 깨달은거지만, TRM 문서에 다 있었다. 이해하지 못했던 것이였다..

12a62c64-5622-46ae-a869-0e96f10e975e
12a62c64-5622-46ae-a869-0e96f10e975e

sub_10a70 (타이머 초기화)

HPTIMER Base == FD8C8000

5406a420-3ece-41f8-9ecc-6bafebba243a
5406a420-3ece-41f8-9ecc-6bafebba243a

sub_10438 (UART 초기화)

UART의 타입은 16550이 산업 표준으로 되어있는데, SoC에 있는 컨트롤러가 타입을 결정하기 때문에 SoC 종속적이다.
(PC는 컨트롤러가 CPU에 없고 보드에 있지만, SoC는 칩 내부에 CPU GPU 컨트롤러 등 시스템 대부분이 들어가 있기 때문에 SoC라고 하는 것이다.)

u-boot의 rk3588 디바이스 트리를 보면 compatible = "rockchip,rk3588-uart", "snps,dw-apb-uart" 이런식으로 적어두는데 이게 16550 과 호환이다.

fa39e69d-8346-4f8c-9906-8fc945ee19b6
fa39e69d-8346-4f8c-9906-8fc945ee19b6

이것들도 마찬가지로 TRM 문서에 다 있다.

8ff1a5f0-fc64-4e0d-8e19-733747a20fd6
8ff1a5f0-fc64-4e0d-8e19-733747a20fd6

sub_9fc (DDR 초기화)

DDR 초기화까지 분석하려 했지만 클로드가 멈추라고 했다.

ee7ecaf0-c418-4fdd-8a4c-a06c86c81d76
ee7ecaf0-c418-4fdd-8a4c-a06c86c81d76

그래서 간단하게만 알아보려 한다.

OTP(eFuse) 관련 데이터는 TRM에도 없다. 그래서 u-boot의 코드를 신뢰하려 한다.

// drivers\misc\rockchip-otp.c
#define RK3588_OTPC_AUTO_CTRL         0x0004
#define RK3588_ADDR_SHIFT             16
#define RK3588_ADDR(n)                ((n) << RK3588_ADDR_SHIFT)
#define RK3588_BURST_SHIFT            8
#define RK3588_BURST(n)               ((n) << RK3588_BURST_SHIFT)
#define RK3588_OTPC_AUTO_EN           0x0008
#define RK3588_AUTO_EN                BIT(0)
#define RK3588_OTPC_DOUT0             0x0020
#define RK3588_OTPC_INT_ST            0x0084
#define RK3588_RD_DONE                BIT(1)

5466ff25-df2f-4a3e-9dcb-084e0fd5a4c6
5466ff25-df2f-4a3e-9dcb-084e0fd5a4c6

sub_9a90 진짜 DDR 초기화함수

간단하게 무슨일을 하는지 확인해보자. 자세한건 정말 의미가 없을듯.

  1. 초기 설정
    DDR 컨트롤러 4개 채널의 레지스터 주소를 메모리에 저장하고 물리계층 기본값을 설정한다.

    • sub_d390() - 4개 채널의 DDR 컨트롤러 베이스 주소 설정
    • sub_bf7c() - PHY 기본 설정
    • [UART] "Class B\n" - 특수 DDR 타입인 경우
    • [UART] "pd/pu vd_ddr\n" - DDR 전원 도메인 제어 필요시
  2. DDR 타입 감지
    보드에 어떤 LPDDR이 납땜됐는지 알 수 없기 떄문에 각 타입마다 하나씩 시도해본다.

    • Cold boot이면 타입 7 → 8 → 9 순서로 시도 (LPDDR4 → LPDDR4X → LPDDR5)
    • Warm boot이면 저장된 타입 사용
    • sub_6f1c() - 각 타입으로 컨트롤러+PHY 초기화 시도
    • [UART] "unknown device\n..." - 모든 타입 실패시
    • [UART] "may be ch%d soldering abnormality\n" - 특정 채널 불량 의심시
  3. DDR 정보 읽기 (Mode Register)
    DDR 칩 내부에서 density(용량), 버스 폭, 제조사ID 등을 읽어온다.

    • sub_2e88(MR8) - density, I/O width
    • sub_2e88(MR5) - manufacturer ID
    • [UART] "Manufacturer ID:0x%x\n"
    • [UART] "L%d: CH%d CS0 x16 mode..." - bus width 모드 정보
  4. 트레이닝
    CPU와 DDR 사이 신호가 도착하는 시간이 보드마다 (배선 길이, 발열, 전압 등으로) 다르기 때문에 동일 램 동일 CPU에서도 타이밍이 달라진다. (노후화 등으로 인해서 달라질수도 있어서 부팅마다 트레이닝한다.)
    정상적인 데이터가 읽어지는 지연 시간 범위를 확인하고, 그 중앙값으로 맞추는 작업이다.

    • sub_8b40() - gate training, read/write leveling
    • sub_3c48() - Vref 트레이닝
    • [UART] "DQS rds: %d,%d,...\n" - DQS delay 결과
    • [UART] "DQ rds: %d %d...\n" - DQ delay 결과
    • [UART] "CH%d RX Vref:%d.%d%%, TX Vref:%d.%d%%,%d.%d%%" - Vref 결과
    • [UART] "ch%d dq%d fail,write:0x%x,read:0x%x\n" - 트레이닝 실패시
  5. ZQ 검증 (LPDDR4/4X)
    신호 품질을 위해 DDR 출력 드라이버의 임피던스를 보드에 맞춘다.

    • sub_2ee4() - ZQ calibration 검증
    • [UART] "Please check the soldering and hardware design of DRAM ZQ...\n" - ZQ 에러시
  6. DVFS (Dynamic Voltage and Frequency Scaling) 테스트
    DDR이 여러 주파수로 동작할 수 있기 때문에 모든 주파수로 전환해보면서 동작을 확인한다. 나중에 OS가 저전력 동작을 위해 주파수를 변경할 수 있도록 미리 검증.

    • sub_7730(freq, 1) → sub_7730(freq, 2) → sub_7730(freq, 3) → sub_7730(freq, 0)
    • [UART] "change to F1: xxxMHz\n"
    • [UART] "change to F2: xxxMHz\n"
    • [UART] "change to F3: xxxMHz\n"
    • [UART] "change to F0: xxxMHz\n"
  7. 종료
    트레이닝 결과를 저장하고 컨트롤러 최종 설정 및 테스트 후 완료처리한다.

    • sub_217c(), sub_d27c() - 최종 설정
    • sub_16bc() - 메모리 검증
    • [UART] "out\n"
ESC
Type to search...