繁体   English   中英

Raspberry PI 3 UART0 不传输(裸机)

[英]Raspberry PI 3 UART0 Not Transmitting (Bare-Metal)

介绍

我一直在为 Raspberry PI 编写自己的裸机代码,因为我积累了裸机技能并了解了 kernel 模式操作。 但是,由于复杂性、文档错误的数量以及信息的缺失/分散,最终在 Raspberry PI 上调出定制的 kernel 非常困难。 然而,我终于得到了这个工作。

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

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

问题

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

在代码中站稳脚跟

这是我的代码。 执行从二进制文件的最开头开始。 它是通过与 linker 脚本中的程序集源“entry.s”中的符号“my_entry_pt”链接来构建的。 在那里您可以找到进入代码。 但是,您可能只需要查看最后一个文件,即“base.c”中的 C 代码。 rest 只是引导至此。 请忽略一些没有意义的评论/名称。 这是我早期裸机项目的一个端口(主要是构建基础设施)。 该项目使用 RISC-V 开发板,该开发板使用 memory 映射的 SPI flash 来存储程序的二进制代码:

[制作文件]

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;
}

调试到这一点

所以事实证明,我们所有人都是对的。 我最初对@Xiaoyi Chen 的回应是错误的。 我重新启动到 Raspberry Pi OS 以检查预感。 我使用 3.3V UART 适配器连接到 PI,该适配器连接到引脚 8(GPIO 14、UART0 TX)、10(GPIO 15、UART0 RX)和 GND(当然是公共接地)。 我可以看到引导消息和可以登录的 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 使其再次在 header 引脚 8 和 10 上出现控制台,但检查进程列表显示这次 getty 正在使用 /dev/ttyAMA0。 如果然后使用我的自定义 kernel 在 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 的 output。 因此,原理图不完整,或者 SOC 内部发生了一些专有的魔法,它重定向了引脚,这与文档相矛盾。

当涉及到 config.txt 选项和在其他地方进行设置时,我也有关于优先级的问题。

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

我的建议:

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM