Raspberry PI 3 UART0 不传输(裸机)

Raspberry PI 3 UART0 Not Transmitting (Bare-Metal)

简介

我一直致力于为 Raspberry PI 编写我自己的裸机代码,因为我建立了我的裸机技能并学习了内核模式操作。然而,由于复杂性、大量文档错误和 missing/scattered 信息,最终在 Raspberry PI 上启动自定义内核极其困难。不过,我终于成功了。

bootstrap 过程中发生的事情的非常广泛的概述

我的内核加载到 0x80000,将除核心 0 之外的所有核心发送到无限循环,设置堆栈指针,并调用 C 函数。我可以设置 GPIO 引脚并打开和关闭它们。使用一些额外的电路,我可以驱动 LED 并确认我的代码正在执行。

问题

但是说到UART,我就碰壁了。我正在使用 UART0 (PL011)。据我所知,UART 没有输出,尽管我可能会在示波器上遗漏它,因为我只有一个模拟示波器。输出字符串时代码卡住了。经过数小时的重新刷新我的 SD 卡并向我的 LED 提出不同的 YES/NO 问题,我确定它卡在无限循环中等待 UART 传输 FIFO 满标志清除。 UART 在变满之前只接受 1 个字节。我不明白为什么它没有将数据传输出去。我也不确定我是否正确设置了我的波特率,但我认为这不会导致 TX FIFO 保持填充状态。

在代码中站稳脚跟

这是我的代码。执行从二进制文件的最开头开始。它是通过链接器脚本中汇编源代码“entry.s”中的符号“my_entry_pt”链接而构建的。在那里您可以找到入口代码。但是,您可能只需要查看最后一个文件,即“base.c”中的 C 代码。剩下的就是 bootstrapping 了。请忽略一些没有意义的comments/names。这是我早期裸机项目的一个端口(主要是构建基础设施)。该项目使用了RISC-V开发板,该开发板使用内存映射SPI闪存来存储程序的二进制代码。:

[生成文件]

TUPLE   := aarch64-unknown-linux-gnu
CC      := $(TUPLE)-gcc
OBJCPY  := $(TUPLE)-objcopy
STRIP   := $(TUPLE)-strip
CFLAGS  := -Wall -Wextra -std=c99 -O2 -march=armv8-a -mtune=cortex-a53 -mlittle-endian -ffreestanding -nostdlib -nostartfiles -Wno-unused-parameter -fno-stack-check -fno-stack-protector
LDFLAGS := -static
GFILES  := 
KFILES  := 
UFILES  := 

# Global Library
#GFILES  := $(GFILES)

# Kernel
#  - Core (Entry/System Setup/Globals)
KFILES  := $(KFILES) ./src/kernel/base.o
KFILES  := $(KFILES) ./src/kernel/entry.o

# Programs
#  - Init
#UFILES  := $(UFILES)

export TUPLE
export CC
export OBJCPY
export STRIP
export CFLAGS
export LDFLAGS
export GFILES
export KFILES
export UFILES

.PHONY: all rebuild clean

all: prog-metal.elf prog-metal.elf.strip prog-metal.elf.bin prog-metal.elf.hex prog-metal.elf.strip.bin prog-metal.elf.strip.hex

rebuild: clean
    $(MAKE) all

clean:
    rm -f *.elf *.strip *.bin *.hex $(GFILES) $(KFILES) $(UFILES)

%.o: %.c
    $(CC) $(CFLAGS) $^ -c -o $@

%.o: %.s
    $(CC) $(CFLAGS) $^ -c -o $@

prog-metal.elf: $(GFILES) $(KFILES) $(UFILES)
    $(CC) $(CFLAGS) $^ -T ./bare_metal.ld $(LDFLAGS) -o $@

prog-%.elf.strip: prog-%.elf
    $(STRIP) -s -x -R .comment -R .text.startup -R .riscv.attributes $^ -o $@

%.elf.bin: %.elf
    $(OBJCPY) -O binary $^ $@

%.elf.hex: %.elf
    $(OBJCPY) -O ihex $^ $@

%.strip.bin: %.strip
    $(OBJCPY) -O binary $^ $@

%.strip.hex: %.strip
    $(OBJCPY) -O ihex $^ $@

emu: prog-metal.elf.strip.bin
    qemu-system-aarch64 -kernel ./prog-metal.elf.strip.bin -m 1G -cpu cortex-a53 -M raspi3 -serial stdio -display none

emu-debug: prog-metal.elf.strip.bin
    qemu-system-aarch64 -kernel ./prog-metal.elf.strip.bin -m 1G -cpu cortex-a53 -M raspi3 -serial stdio -display none -gdb tcp::1234 -S

debug:
    $(TUPLE)-gdb -ex "target remote localhost:1234" -ex "layout asm" -ex "tui reg general" -ex "break *0x00080000" -ex "break *0x00000000" -ex "set scheduler-locking step"

[bare_metal.ld]

/*
This is not actually needed (At least not on actual hardware.), but 
it explicitly sets the entry point in the .elf file to be the same 
as the true entry point to the program. The global symbol my_entry_pt
is located at the start of src/kernel/entry.s.  More on this below.
*/
ENTRY(my_entry_pt)

MEMORY
{
    /*
    This is the memory address where this program will reside.
    It is the reset vector.
    */
    ram (rwx)  : ORIGIN = 0x00080000, LENGTH = 0x0000FFFF
}

SECTIONS
{
    /*
    Force the linker to starting at the start of memory section: ram
    */
    . = 0x00080000;
    
    .text : {
        /*
        Make sure the .text section from src/kernel/entry.o is 
        linked first.  The .text section of src/kernel/entry.s 
        is the actual entry machine code for the kernel and is 
        first in the file.  This way, at reset, exection starts 
        by jumping to this machine code.
        */
        src/kernel/entry.o (.text);
        
        /*
        Link the rest of the kernel's .text sections.
        */
        *.o (.text);
    } > ram
    
    /*
    Put in the .rodata in the flash after the actual machine code.
    */
    .rodata : {
        *.o (.rodata);
        *.o (.rodata.*);
    } > ram
    
    /*
    END: Read Only Data
    START: Writable Data
    */
    .sbss : {
        *.o (.sbss);
    } > ram
    .bss : {
        *.o (.bss);
    } > ram
    section_KHEAP_START (NOLOAD) : ALIGN(0x10) {
        /*
        At the very end of the space reserved for global variables 
        in the ram, link in this custom section.  This is used to
        add a symbol called KHEAP_START to the program that will 
        inform the C code where the heap can start.  This allows the 
        heap to start right after the global variables.
        */
        src/kernel/entry.o (section_KHEAP_START);
    } > ram
    
    /*
    Discard everything that hasn't be explictly linked.  I don't
    want the linker to guess where to put stuff.  If it doesn't know, 
    don't include it.  If this casues a linking error, good.  I want 
    to know that I need to fix something, rather than a silent failure 
    that could cause hard to debug issues later.  For instance, 
    without explicitly setting the .sbss and .bss sections above, 
    the linker attempted to put my global variables after the 
    machine code in the flash.  This would mean that ever access to 
    those variables would mean read a write to the external SPI flash 
    IC on real hardware.  I do not believe that initialized globals 
    are possible since there is nothing to initialize them.  So I don't
    want to, for instance, include the .data section.
    */
    /DISCARD/ : {
        * (.*);
    }
}

[src/kernel/entry.s]

.section .text

.globl my_entry_pt

// This is the Arm64 Kernel Header (64 bytes total)
my_entry_pt:
  b end_of_header // Executable code (64 bits)
  .align 3, 0, 7
  .quad my_entry_pt // text_offset (64 bits)
  .quad 0x0000000000000000 // image_size (64 bits)
  .quad 0x000000000000000A // flags (1010: Anywhere, 4K Pages, LE) (64 bits)
  .quad 0x0000000000000000 // reserved 2 (64 bits)
  .quad 0x0000000000000000 // reserved 3 (64 bits)
  .quad 0x0000000000000000 // reserved 4 (64 bits)
  .int 0x644d5241 // magic (32 bits)
  .int 0x00000000 // reserved 5 (32 bits)

end_of_header:
  // Check What Core This Is
  mrs x0, VMPIDR_EL2
  and x0, x0, #0x3
  cmp x0, #0x0
  // If this is not core 0, go into an infinite loop
  bne loop

  // Setup the Stack Pointer
  mov x2, #0x00030000
  mov sp, x2
  // Get the address of the C main function
  ldr x1, =kmain
  // Call the C main function
  blr x1

loop:
  nop
  b loop

.section section_KHEAP_START

.globl KHEAP_START

KHEAP_START:

[src/kernel/base.c]

void pstr(char* str) {
    volatile unsigned int* AUX_MU_IO_REG = (unsigned int*)(0x3f201000 + 0x00);
    volatile unsigned int* AUX_MU_LSR_REG = (unsigned int*)(0x3f201000 + 0x18);
    while (*str != 0) {
        while (*AUX_MU_LSR_REG & 0x00000020) {
            // TX FIFO Full
        }
        *AUX_MU_IO_REG = (unsigned int)((unsigned char)*str);
        str++;
    }
    return;
}

signed int kmain(unsigned int argc, char* argv[], char* envp[]) {
    char* text = "Test Output String\n";
    volatile unsigned int* AUXENB = 0;
    //AUXENB = (unsigned int*)(0x20200000 + 0x00);
    //*AUXENB |= 0x00024000;
    //AUXENB = (unsigned int*)(0x20200000 + 0x08);
    //*AUXENB |= 0x00000480;

    // Set Baud Rate to 115200
    AUXENB = (unsigned int*)(0x3f201000 + 0x24);
    *AUXENB = 26;
    AUXENB = (unsigned int*)(0x3f201000 + 0x28);
    *AUXENB = 0;

    AUXENB = (unsigned int*)(0x3f200000 + 0x04);
    *AUXENB = 0;
    // Set GPIO Pin 14 to Mode: ALT0 (UART0)
    *AUXENB |= (04u << ((14 - 10) * 3));
    // Set GPIO Pin 15 to Mode: ALT0 (UART0)
    *AUXENB |= (04u << ((15 - 10) * 3));

    AUXENB = (unsigned int*)(0x3f200000 + 0x08);
    *AUXENB = 0;
    // Set GPIO Pin 23 to Mode: Output
    *AUXENB |= (01u << ((23 - 20) * 3));
    // Set GPIO Pin 24 to Mode: Output
    *AUXENB |= (01u << ((24 - 20) * 3));

    // Turn ON Pin 23
    AUXENB = (unsigned int*)(0x3f200000 + 0x1C);
    *AUXENB = (1u << 23);

    // Turn OFF Pin 24
    AUXENB = (unsigned int*)(0x3f200000 + 0x28);
    *AUXENB = (1u << 24);

    // Enable TX on UART0
    AUXENB = (unsigned int*)(0x3f201000 + 0x30);
    *AUXENB = 0x00000101;

    pstr(text);

    // Turn ON Pin 24
    AUXENB = (unsigned int*)(0x3f200000 + 0x1C);
    *AUXENB = (1u << 24);

    return 0;
}

我的建议:

  • 将您的 SD 卡刷入 rpi 发行版以确保硬件仍在工作
  • 如果硬件没问题,检查一下你的代码和内核串口驱动的区别

调试到此为止

事实证明我们所有人都是对的。我最初针对@Xiaoyi Chen 的故障排除是错误的。我重新启动回到 Raspberry Pi OS 以检查预感。我使用连接到引脚 8(GPIO 14、UART0 TX)、10(GPIO 15、UART0 RX)和 GND(当然是公共接地)的 3.3V UART 适配器连接到 PI。我可以看到引导消息和可以登录的 getty 登录提示。我认为这意味着 PL011 正在工作,但是当我实际检查 htop 中的进程列表时,我发现 getty 实际上是 /dev/ttyS0 上的 运行 而不是 /dev/ttyAMA0。 /dev/ttyAMA0 实际上是在另一个进程列表中使用 hciattach 命令绑定到蓝牙模块。

根据此处的文档:https://www.raspberrypi.org/documentation/configuration/uart.md,/dev/ttyS0 是 mini UART 而 /dev/AMA0 是 PL011,但它也说 UART0 是 PL011,UART1 是 mini UART .此外,GPIO 引脚分配和 BCM2835 文档说 GPIO 引脚 14 和 15 用于 UART0 TX 和 RX。因此,当 Linux 使用迷你 UART 时,如果我能在针脚 14 和针脚 15 上看到登录提示,但我应该物理连接到 PL011。如果我通过 SSH 登录并尝试用 minicom 打开 /dev/ttyAMA0,我看不到任何事情发生。但是,如果我对 /dev/ttyS0 做同样的事情,它会与登录终端发生冲突。这向我证实 /dev/ttyS0 实际上用于引导控制台。

答案

如果我在 config.txt 中设置“dtoverlay=disable-bt”,上述行为会发生变化以符合预期。重新启动 PI 使其再次在头针 8 和 10 上出现控制台,但检查进程列表显示这次 getty 使用的是 /dev/ttyAMA0。如果然后使用我的自定义内核在 config.txt 中设置“dtoverlay=disable-bt”,程序将按预期执行,打印出我的字符串并打开第二个 LED。由于 PL011 的输出从未真正设置过,因为它被某种魔法重定向了,所以它不会像@PMF 建议的那样工作是有道理的。整个交易再次证实了我的断言,即这个所谓的“学习计算机”的文档非常糟糕。

对于那些好奇的人,这是我 config.txt 的最后几行:

[all]
dtoverlay=disable-bt
enable_uart=1
core_freq=250
#uart_2ndstage=1
force_eeprom_read=0
disable_splash=1
init_uart_clock=48000000
init_uart_baud=115200
kernel_address=0x80000
kernel=prog-metal.elf.strip.bin
arm_64bit=1

剩余问题

有几件事仍然困扰着我。我可以发誓我已经尝试设置“dtoverlay=disable-bt”。

其次,这似乎确实在幕后执行了某种未记录的魔法(我知道没有关于它的文档。)我不明白。我在已发布的原理图中找不到任何重定向来自 SOC 的 GPIO 14 和 15 输出的内容。因此,原理图不完整,或者 SOC 内部发生了一些专有魔法,重定向了引脚,与文档相矛盾。

我也对 config.txt 选项和其他设置的优先级有疑问。

总之,谢谢大家的帮助。