Raspberry PI 3 UART0 不传输裸机

问题描述

简介

我一直致力于为 RaspBerry PI 编写自己的裸机代码,因为我积累了裸机技能并了解了内核模式操作。然而,由于复杂性、文档错误数量和信息丢失/分散,最终在 RaspBerry PI 上调出自定义内核非常困难。然而,我终于开始工作了。

对引导过程中发生的事情的非常广泛的概述

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

问题

但是,当谈到 UART 时,我遇到了困难。我正在使用 UART0 (PL011)。据我所知,UART 没有输出,尽管我的示波器可能会丢失它,因为我只有一个模拟示波器。输出字符串时代码卡住了。我已经通过数小时的刷新我的 SD 卡,对我的 LED 发出不同的“是”/“否”问题,确定它卡在无限循环中,等待 UART 发送 FIFO 已满标志清除。 UART 在变满之前只接受 1 个字节。我不明白为什么它不传输数据。我也不确定我是否正确设置了我的波特率,但我认为这不会导致 TX FIFO 保持填充状态。

代码中立足

这是我的代码。执行从二进制文件的最开头开始。它是通过与链接描述文件中汇编源代码“entry.s”中的符号“my_entry_pt”链接来构建的。您可以在那里找到入口代码。但是,您可能只需要查看最后一个文件,即“base.c”中的 C 代码。其余的只是引导到那个。请忽略一些没有意义的评论/名称。这是我早期裸机项目的一个端口(主要是构建基础设施)。该项目使用了 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,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;
}

解决方法

调试这一点

事实证明,我们所有人都是对的。我最初针对@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 是迷你 UART,而 /dev/AMA0 是 PL011,但它也说 UART0 是 PL011,UART1 是迷你 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 选项和其他地方设置的优先级问题。

无论如何,谢谢大家的帮助。

,

我的建议:

  • 将您的 SD 卡刷入 rpi 发行版以确保硬件仍在工作
  • 如果硬件良好,请检查您的代码与内核串行驱动程序的差异