【ELF2开发板】裸机串口程序开发

「“芯”想事成,造物无敌」嵌入式硬件创意项目征集令 1.9w人浏览 28人参与

AI助手已提取文章相关产品:

目录

从U-Boot获得开发板的信息

内存配置分析

DRAM Bank 分布

总内存

关键系统信息

通信配置

内存管理

栈信息

内存使用情况

设备树

架构特征

串口地址的确定

编写 RK3588 裸机串口程序(输出 Hello World)

汇编启动文件(start.s)

C 语言主程序(main.c)

链接脚本(linker.ld)

Makefile(自动化编译)

编译与 U-Boot 加载运行

编译步骤

U-Boot 加载运行(TFTP 方式)

​编辑

总结


在 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

这行日志表明:

  1. PreSerial: 这个关键词表明,这是 U-Boot 在早期初始化阶段(甚至早于完整的设备树初始化)所使用的串口。在这个阶段,U-Boot 还不能通过设备树来找到和配置串口,所以它会使用一个 “硬编码” 的、预先定义好的串口来输出早期的调试信息。这个串口通常就是最终的控制台串口。
  2. 2: 这是 串口控制器的索引号。它表示这是 SoC 上的第 3 个 UART 控制器(因为索引号通常从 0 开始)。在 Rockchip 平台上,这个索引号通常对应 ttyS2
  3. raw: 这个参数描述了串口的配置模式。raw 意味着 U-Boot 使用最基本、最直接的方式来操作串口寄存器,不涉及任何复杂的驱动或协议。
  4. 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 加载运行

编译步骤

  1. 安装交叉编译器(Ubuntu 环境):
    sudo apt-get install gcc-aarch64-linux-gnu
    
  2. 将上述 4 个文件放在同一目录,执行编译:
    make
    
    编译成功后会生成 hello_world.bin 二进制文件。

U-Boot 加载运行(TFTP 方式)

  1. 将 hello_world.bin 放入 TFTP 服务器根目录;
  2. 开发板进入 U-Boot 命令行,配置网络并加载程序:
    => dhcp  # 获得开发板 IP
    => setenv serverip 192.168.1.216  # 电脑 IP
    => tftp 0x00200000 hello_world.bin  # 加载到 DRAM 0x00200000
    => go 0x00200000  # 运行程序
    

现在可以看到程序的运行结果了

总结

  1. 串口地址匹配是关键:裸机程序的串口地址必须与 U-Boot 控制台串口地址一致,否则无输出;
  2. U-Boot 日志是重要线索PreSerial 日志直接提供串口地址和索引号,避免盲目尝试;
  3. U-Boot 命令辅助验证mw/md 命令可快速验证串口地址。

通过以上步骤,可完整实现 RK3588 开发板的 U-Boot 串口调试,从裸机程序编写到串口定位,覆盖开发中常见的坑点,为后续更复杂的裸机开发或驱动调试打下基础。

您可能感兴趣的与本文相关内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

神一样的老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值