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 단위이다.)
이 헤더 위치가 끝나면 바로 TPL 바이너리를 확인할 수 있다.
0x800 == 2KB
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에 정상로드 됐는지 확인하는 코드이다.
sub_10978 (TPL 메인함수)
실제 메인이다. 간단하게 코멘트를 달아놨다. MaskROM으로 돌아가는 lr을 계속 스택에서 보관하며 실행됐기 때문에 보라색 위치에서 종료되면 MaskROM 의 코드로 돌아가게 된다.
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 초기화 함수에서 사용된다.
sub_9a0 (시스템 설정)
GPIO는 SoC에서 범용적인 입출력용으로 사용할 수 있는 핀이며, RK3588은 GPIO0-4 까지 5개의 뱅크, A-D의 서브그룹, 0-7의 핀을 갖게되어 총 160개의 GPIO핀을 갖고있다.
대부분은 보드 내부에서 장치들을 제어하는데 사용하고 있고, 일부 핀은 디버깅핀으로 뽑혀져 보드에서 확인할 수 있다.
여기에서 세팅하는 값이 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에서 확인하지 못했을 것이다.
재밌는건 이 핀이 mux=1로 세팅하면 SDCard 데이터핀으로 동작(공식문서에도 적혀있음)하는데, 물리회로를 공유하니까 mux=5 일때 보드의 SDCard 슬롯을 통해서도 JTAG TCK/TMS 역할을 할 수 있게된다. 뭔소리인가 싶었지만 사진을 보면 바로 이해가 된다. 링크
RK3588은 ARM SWD (Serial Wire Debug) 방식을 사용해서 TMS 혼자서 TDI/TDO 기능을 모두 처리할 수 있다.
부트로더 초기에 JTAG로 동작하다가 나중에 SDCard 슬롯으로 mux를 변경하는 방식일거라서 저 이상한 커넥터를 연결하고 기기리셋하면 JTAG로 붙게될것이다.
이제야 깨달은거지만, TRM 문서에 다 있었다. 이해하지 못했던 것이였다..
sub_10a70 (타이머 초기화)
HPTIMER Base == FD8C8000
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 과 호환이다.
이것들도 마찬가지로 TRM 문서에 다 있다.
sub_9fc (DDR 초기화)
DDR 초기화까지 분석하려 했지만 클로드가 멈추라고 했다.
그래서 간단하게만 알아보려 한다.
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)
sub_9a90 진짜 DDR 초기화함수
간단하게 무슨일을 하는지 확인해보자. 자세한건 정말 의미가 없을듯.
-
초기 설정
DDR 컨트롤러 4개 채널의 레지스터 주소를 메모리에 저장하고 물리계층 기본값을 설정한다.- sub_d390() - 4개 채널의 DDR 컨트롤러 베이스 주소 설정
- sub_bf7c() - PHY 기본 설정
[UART] "Class B\n"- 특수 DDR 타입인 경우[UART] "pd/pu vd_ddr\n"- DDR 전원 도메인 제어 필요시
-
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"- 특정 채널 불량 의심시
-
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 모드 정보
-
트레이닝
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"- 트레이닝 실패시
-
ZQ 검증 (LPDDR4/4X)
신호 품질을 위해 DDR 출력 드라이버의 임피던스를 보드에 맞춘다.- sub_2ee4() - ZQ calibration 검증
[UART] "Please check the soldering and hardware design of DRAM ZQ...\n"- ZQ 에러시
-
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"
-
종료
트레이닝 결과를 저장하고 컨트롤러 최종 설정 및 테스트 후 완료처리한다.- sub_217c(), sub_d27c() - 최종 설정
- sub_16bc() - 메모리 검증
[UART] "out\n"