簡體   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