目录
编写 RK3588 裸机串口程序(输出 Hello World)
在 RK3588 开发板开发过程中,串口是最基础也是最重要的调试工具之一。无论是裸机程序调试,还是 U-Boot 环境配置,都离不开串口的交互。本文将结合实际操作,详细记录编写 RK3588 裸机串口程序的完整过程。
从U-Boot获得开发板的信息
可以使用bdinfo命令获得开发板的基本信息:
=> bdinfo
arch_number = 0x00000000
boot_params = 0x00000000
DRAM bank = 0x00000000
-> start = 0x00200000
-> size = 0x08200000
DRAM bank = 0x00000001
-> start = 0x09400000
-> size = 0xE6C00000
baudrate = 115200 bps
TLB addr = 0xEFFF0000
relocaddr = 0xEDC15000
reloc off = 0xEDA15000
irq_sp = 0xEB9FA9B0
sp start = 0xEB9FA9B0
Early malloc usage: 34d0 / 80000
fdt_blob = 0000000008300000
根据提供的 bdinfo 输出,可以分析这个系统的内存和启动信息:
内存配置分析
DRAM Bank 分布
-
Bank 0:
-
起始地址:
0x00200000(2MB) -
大小:
0x08200000= 130MB -
范围:
0x00200000-0x08400000
-
-
Bank 1:
-
起始地址:
0x09400000(148MB) -
大小:
0xE6C00000= 3,868MB ≈ 3.78GB -
范围:
0x09400000-0xF0000000
-
总内存
-
总物理内存: 130MB + 3,868MB = 3,998MB ≈ 4GB
-
内存布局存在一个14MB的间隙 (
0x08400000-0x09400000)
关键系统信息
通信配置
-
串口波特率: 115200 bps
-
这是标准的调试和控制台通信速率
内存管理
-
TLB地址:
0xEFFF0000 -
重定位地址:
0xEDC15000 -
重定位偏移:
0xEDA15000 -
表明U-Boot已执行了重定位操作
栈信息
-
IRQ栈指针:
0xEB9FA9B0 -
栈起始地址:
0xEB9FA9B0 -
栈位于高地址内存区域
内存使用情况
-
Early malloc使用: 0x34d0 / 0x80000
-
已使用: 13,520 字节
-
总空间: 524,288 字节
-
使用率: 约 2.6%
-
设备树
-
FDT地址:
0x0000000008300000 -
设备树blob位于Bank 0的内存区域
架构特征
从内存映射和地址模式来看,这很可能是一个:
-
64位ARM架构 (AArch64) 系统
-
使用U-Boot作为引导加载程序
-
具有非连续的内存映射,可能是由于硬件保留区域或内存映射设备
串口地址的确定
不过从bdinfo中并不能知道是Elf2开发板使用的是哪个串口进行调试输出。刚开始我想当然认为是UART0,不过测试了一下该端口的地址发现不对。
看了一眼开发板的启动信息:
U-Boot 2017.09 (Nov 23 2024 - 13:04:29 +0800)
Model: ELFBoard elf2 Board
MPIDR: 0x0
PreSerial: 2, raw, 0xfeb50000
这行日志表明:
PreSerial: 这个关键词表明,这是 U-Boot 在早期初始化阶段(甚至早于完整的设备树初始化)所使用的串口。在这个阶段,U-Boot 还不能通过设备树来找到和配置串口,所以它会使用一个 “硬编码” 的、预先定义好的串口来输出早期的调试信息。这个串口通常就是最终的控制台串口。2: 这是 串口控制器的索引号。它表示这是 SoC 上的第 3 个 UART 控制器(因为索引号通常从 0 开始)。在 Rockchip 平台上,这个索引号通常对应ttyS2。raw: 这个参数描述了串口的配置模式。raw意味着 U-Boot 使用最基本、最直接的方式来操作串口寄存器,不涉及任何复杂的驱动或协议。0xfeb50000: 这是 该 UART 控制器的物理基地址。
可以通过 U-Boot 命令(如 mw)直接向当前正在使用的串口写数据来验证一下:
=> mw.b 0xfeb50000 0x41
此时控制台看到“A=> ” ,这说明这个地址确实是串口输出的地址。
编写 RK3588 裸机串口程序(输出 Hello World)
目标是在 U-Boot 引导下,运行裸机程序通过串口输出 “Hello World”。需准备 4 个核心文件:汇编启动文件、C 语言主程序、链接脚本、Makefile。
汇编启动文件(start.s)
负责初始化栈、设置异常向量表,最终跳转到 C 语言 main 函数:
.cpu cortex-a76
.arch armv8-a
.section .text.entry
.global _start
_start:
/* 设置异常向量表 */
adrp x0, vectors
add x0, x0, :lo12:vectors
msr vbar_el1, x0
/* 初始化栈 */
/* 从 bdinfo 看,0x00200000 是代码起始地址,我们把栈设在它前面一点的安全位置 */
/* 假设栈大小为 16KB */
adrp x0, _stack_end
add x0, x0, :lo12:_stack_end
mov sp, x0
/* 跳转到 C 语言 main 函数 */
bl main
/* main 函数不应该返回,如果返回了,就进入一个死循环 */
loop:
b loop
/* 异常向量表 (简化版,仅处理复位异常) */
vectors:
.align 11
b _start // 0x00: Current EL with SP0
b . // 0x08: Current EL with SPx
b . // 0x10: Lower EL using AArch64
b . // 0x18: Lower EL using AArch32
b . // 0x20: Current EL with SP0 (SError)
b . // 0x28: Current EL with SPx (SError)
b . // 0x30: Lower EL (SError)
b . // 0x38: Lower EL (SError)
.section .bss.stack
.align 16
_stack_start:
.space 16384 // 16KB 栈空间
_stack_end:
C 语言主程序(main.c)
实现 UART 初始化、字符发送、字符串发送功能,核心是直接操作串口寄存器:
#include <stdint.h>
// RK3588 UART 寄存器定义 (部分关键寄存器)
// 假设基地址为 UART2
#define UART_BASE 0xfeb50000
// 寄存器偏移量
#define UART_THR_DLAB 0x00 // Transmit Holding Register / Divisor Latch (Low)
#define UART_RBR_DLAB 0x00 // Receive Buffer Register / Divisor Latch (Low)
#define UART_DLL 0x00 // Divisor Latch LSB
#define UART_DLH 0x04 // Divisor Latch MSB
#define UART_IER_DLAB 0x04 // Interrupt Enable Register / Divisor Latch (High)
#define UART_IIR_FCR 0x08 // Interrupt Identification Register / FIFO Control Register
#define UART_LCR 0x0C // Line Control Register
#define UART_MCR 0x10 // Modem Control Register
#define UART_LSR 0x14 // Line Status Register
#define UART_MSR 0x18 // Modem Status Register
#define UART_SCR 0x1C // Scratch Register
// 函数声明
static void uart_init(uint32_t baudrate);
static void uart_putc(char c);
void uart_puts(const char *str);
// 简单的延时函数,用于等待硬件状态稳定
static void delay(int count) {
while (count--) {
asm volatile("nop");
}
}
// UART 初始化函数
// 参考 RK3588 datasheet,UART 时钟源为 24MHz
#define UART_CLK 24000000
static void uart_init(uint32_t baudrate) {
volatile uint32_t *uart_reg = (volatile uint32_t *)UART_BASE;
// 1. 设置 LCR 寄存器的 DLAB 位,允许访问 DLL 和 DLH 寄存器
uart_reg[UART_LCR >> 2] |= (1 << 7); // DLAB = 1
// 2. 计算并设置波特率除数 (Divisor)
uint16_t divisor = UART_CLK / (16 * baudrate);
uart_reg[UART_DLL >> 2] = divisor & 0xFF; // 写入除数低8位
uart_reg[UART_DLH >> 2] = (divisor >> 8) & 0xFF; // 写入除数高8位
// 3. 配置数据格式: 8位数据位, 1位停止位, 无校验
// DLAB 位会被清除
uart_reg[UART_LCR >> 2] = 0x03; // 0b00000011, DLAB=0, 8N1
// 4. (可选) 开启 FIFO
// uart_reg[UART_IIR_FCR >> 2] = 0x01; // 启用FIFO
// 等待UART稳定
delay(1000);
}
// 发送一个字符
static void uart_putc(char c) {
volatile uint32_t *uart_reg = (volatile uint32_t *)UART_BASE;
// 等待发送缓冲区为空 (THRE bit)
while (!(uart_reg[UART_LSR >> 2] & (1 << 5)));
// 将字符写入发送缓冲区
uart_reg[UART_THR_DLAB >> 2] = c;
}
// 发送一个字符串
void uart_puts(const char *str) {
while (*str != '\0') {
uart_putc(*str);
str++;
}
}
// 主函数
int main(void) {
// 初始化 UART,波特率 115200
uart_init(115200);
// 发送 "Hello, World!"
uart_puts("Hello, RK3588 Bare-metal World!\r\n");
uart_puts("UART is working!\r\n");
// 进入死循环
while (1);
return 0; // 永远不会执行到这里
}
链接脚本(linker.ld)
定义程序在内存中的布局,指定代码段、数据段起始地址(需与 U-Boot bdinfo 中的 DRAM 起始地址一致):
ENTRY(_start)
SECTIONS
{
. = 0x00200000; /* 程序起始地址,由 U-Boot 的 bdinfo 得知 */
.text : { /* 代码段 */
*(.text.entry) /* 启动代码放在最前面 */
*(.text) /* 其他代码 */
}
.rodata : { /* 只读数据段 */
*(.rodata)
}
.data : { /* 已初始化数据段 */
*(.data)
}
.bss : { /* 未初始化数据段 (会被清零) */
*(COMMON)
*(.bss)
}
. = ALIGN(8);
. = . + 0x4000; /* 在 bss 段后预留 16KB 空间给栈,不过我们在 start.s 里也定义了栈 */
_heap_start = .;
. = . + 0x10000; /* 堆大小为 64KB */
_heap_end = .;
}
Makefile(自动化编译)
指定交叉编译器、编译参数,实现一键编译:
# 定义交叉编译器
CROSS_COMPILE ?= aarch64-linux-gnu-
CC := $(CROSS_COMPILE)gcc
AS := $(CROSS_COMPILE)as
LD := $(CROSS_COMPILE)ld
OBJCOPY := $(CROSS_COMPILE)objcopy
OBJDUMP := $(CROSS_COMPILE)objdump
# 目标文件名
TARGET := hello_world
# 源文件
SRCS := start.s main.c
# 目标文件
OBJS := $(SRCS:.s=.o)
OBJS := $(OBJS:.c=.o)
# 编译 flags
# -march=armv8-a: 针对 ARMv8-A 架构
# -mtune=cortex-a76: 优化编译器以适应 Cortex-A76 核心
# -ffreestanding: 告诉编译器这是一个独立环境,没有标准库
# -nostdlib: 不链接标准库
# -O0: 无优化,便于调试
CFLAGS := -march=armv8-a -mtune=cortex-a76 -ffreestanding -nostdlib -O0 -Wall -Wextra
AFLAGS := -march=armv8-a
all: $(TARGET).bin
# 链接
$(TARGET).elf: $(OBJS) linker.ld
$(LD) -T linker.ld -o $@ $(OBJS) # 注意:此行开头是一个制表符
# 生成二进制文件
$(TARGET).bin: $(TARGET).elf
$(OBJCOPY) -O binary $< $@
# 编译 C 文件
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# 编译汇编文件
%.o: %.s
$(AS) $(AFLAGS) -c $< -o $@
# 清理
clean:
rm -f $(OBJS) $(TARGET).elf $(TARGET).bin
编译与 U-Boot 加载运行
编译步骤
- 安装交叉编译器(Ubuntu 环境):
sudo apt-get install gcc-aarch64-linux-gnu - 将上述 4 个文件放在同一目录,执行编译:
编译成功后会生成makehello_world.bin二进制文件。
U-Boot 加载运行(TFTP 方式)
- 将
hello_world.bin放入 TFTP 服务器根目录; - 开发板进入 U-Boot 命令行,配置网络并加载程序:
=> dhcp # 获得开发板 IP => setenv serverip 192.168.1.216 # 电脑 IP => tftp 0x00200000 hello_world.bin # 加载到 DRAM 0x00200000 => go 0x00200000 # 运行程序
现在可以看到程序的运行结果了
总结
- 串口地址匹配是关键:裸机程序的串口地址必须与 U-Boot 控制台串口地址一致,否则无输出;
- U-Boot 日志是重要线索:
PreSerial日志直接提供串口地址和索引号,避免盲目尝试; - U-Boot 命令辅助验证:
mw/md命令可快速验证串口地址。
通过以上步骤,可完整实现 RK3588 开发板的 U-Boot 串口调试,从裸机程序编写到串口定位,覆盖开发中常见的坑点,为后续更复杂的裸机开发或驱动调试打下基础。


1554

被折叠的 条评论
为什么被折叠?



