Raspberry Pi Bare Bones

来自osdev
跳到导航 跳到搜索

等等!你是否已阅读过 起步指南, 起步错误, 以及一些相关的 操作系统理论

难度等级
Difficulty 3.png
高级
内核设计
模型
其它概念

这是关于 Raspberry Pi 上的操作系统开发的教程。 本文章将作为如何创建最小系统的示例,而不是如何正确构建项目的示例。

有一个类似的教程 Raspberry Pi Bare Bones Rust 用Rust代替C。

准备

你即将开始开发新的操作系统。 也许有一天,你的新操作系统可以在其自身下开发。 这是一个被称为引导或自我托管的过程。 然而,这是未来的道路。 今天,我们只需要设置一个系统,该系统可以从现有的操作系统中编译你的操作系统。 这是一个被称为交叉编译的过程,这是操作系统开发的第一步。

本文假设你正在使用类似Unix的操作系统,例如Linux,它很好地支持操作系统开发。 Windows用户应该能够在MinGW或Cygwin环境中完成它。

构建交叉编译器

主条目: GCC Cross-CompilerWhy do I need a Cross Compiler?

你应该做的第一件事是为 arm-none-eabi 设置一个 GCC Cross-Compiler。 你还没有修改你的编译器来了解你的操作系统的存在,所以我们使用一个名为arm-none-eabi的通用目标,它为你提供了一个针对 system V ABI 的工具链。 你将 “无法” 在没有交叉编译器的情况下正确编译你的操作系统。

如果你想要一个64位内核,你应该设置目标为 aarch64-elf 。 这提供了相同的系统V ABI接口,但仅适用于64位。 使用elf不是强制性的,但是可以简化一些工作。

概述

到现在为止,你应该已经为适当的ABI设置了 cross-compiler (如上所述)。本教程提供了创建操作系统的最小解决方案。 它不作为项目结构的推荐框架,而是作为一个最小内核的例子。 在这种简单的情况下,我们只需要三个输入文件:

  • boot.S - 设置处理器环境的内核入口点
  • kernel.c - 你实际的内核例程
  • linker.ld - 用于链接上述文件

启动操作系统

现在,我们将创建一个名为boot.S的文件,并讨论其内容。 在这个例子中,我们使用的是GNU汇编程序,它是你之前构建的交叉编译器工具链的一部分。 这个汇编器与GNU工具链的其余部分集成得非常好。

每个Pi型号都需要不同的设置。 通常,你必须区分AArch32和AArch64模式,因为它们的启动方式不同。 后者只能从Pi 3及以上访问。 在一种模式下,你可以在运行时 detect the board,并相应地设置mmio基址。

Pi型号 A,B,A+,B和Zero

环境的设置和执行从 armstub.s 转移到 _start。

// AArch32 mode

// 将其保留在二进制文件的第一部分中。
.section ".text.boot"

// Make _start global.
.globl _start

        .org 0x8000
// 内核的入口点。
// r15 -> 应该从0x8000开始执行。
// r0 -> 0x00000000
// r1 -> 0x00000C42 - machine id
// r2 -> 0x00000100 - start of ATAGS
// 保留这些寄存器作为kernel_main的参数
_start:
	// 设置堆栈。
	mov sp, #0x8000

	// 清除bss。
	ldr r4, =__bss_start
	ldr r9, =__bss_end
	mov r5, #0
	mov r6, #0
	mov r7, #0
	mov r8, #0
	b       2f

1:
	// 在r4存储多个
	stmia r4!, {r5-r8}

	// 如果我们仍然小于bss_end,则循环。
2:
	cmp r4, r9
	blo 1b

	// 调用kernel_main
	ldr r3, =kernel_main
	blx r3

	// 暂停
halt:
	wfe
	b halt

“.text.boot” 部分将在链接器脚本中使用,以将boot.S作为我们内核映像中的第一件事。 代码在调用kernel_main函数之前初始化一个最小C环境,这意味着有一个堆栈并将BSS段归零。 请注意,该代码避免使用r0-r2,因此对于kernel_main调用保持有效。

然后,你可以使用以下方法组装boot.S:

arm-none-eabi-gcc -mcpu=arm1176jzf-s -fpic -ffreestanding -c boot.S -o boot.o

Pi 2

有了新版本的Pi,还有更多的事情要做。 Raspberry Pis 2和3 (第一个支持64位的型号) 有4个核心。 在引导时,所有内核都在运行并执行相同的引导代码。 因此,你必须区分核心,并且只允许其中一个运行,将其他核心置于无限循环中。

环境的设置和执行从 armstub7.S # L167 armstub7.s 转移到 _start。

// AArch32 mode

// 将其保留在二进制文件的第一部分中。
.section ".text.boot"

// Make _start global.
.globl _start

        .org 0x8000
// 内核的入口点。
// r15 -> 应该从0x8000开始执行。
// r0 -> 0x00000000
// r1 -> 0x00000C42 - 机器标识
// r2 -> 0x00000100-ATAGS的启动
// 保留这些寄存器作为kernel_main的参数
_start:
	// 关闭额外的内核
	mrc p15, 0, r5, c0, c0, 5
	and r5, r5, #3
	cmp r5, #0
	bne halt

	// 设置堆栈。
	ldr r5, =_start
	mov sp, r5

	// Clear out bss.
	ldr r4, =__bss_start
	ldr r9, =__bss_end
	mov r5, #0
	mov r6, #0
	mov r7, #0
	mov r8, #0
	b       2f

1:
	// 在r4存储多个
	stmia r4!, {r5-r8}

	// 如果我们仍然低于bss_end,则循环。
2:
	cmp r4, r9
	blo 1b

	// 调用kernel_main
	ldr r3, =kernel_main
	blx r3

	// 暂停
halt:
	wfe
	b halt

你可以使用以下方法组装boot.S:

arm-none-eabi-gcc -mcpu=cortex-a7 -fpic -ffreestanding -c boot.S -o boot.o

Pi 3, 4

值得一提的是,Pi 3和4通常将kernel8.img引导到64位模式,但是你仍然可以将AArch32与kernel7.img一起使用,以实现向后兼容。 请注意,在64位模式下,引导代码以0x80000而不是0x8000加载。 对于Pi 3和4,AArch64中的引导代码完全相同,但是Pi 4具有不同的外围基址 (请参见下面的C示例代码)。

使用最新的固件,只有主核心运行 (核心0),辅助核心正在循环中等待。 要唤醒它们,请在0xE0 (核心1),0xE8 (核心2) 或0xF0 (核心3) 上写一个函数的地址,然后它们将开始执行该函数。

环境的设置和执行从 armstub8.S # L154 armstub8.s 转移到 _start。

// AArch64模式

// 将其保留在二进制文件的第一部分中。
.section ".text.boot"

// 使 _start全局。
.globl _start

    .org 0x80000
// Entry point for the kernel. Registers:
// x0 -> 内存中DTB的32位指针 (仅主核)/0 (辅助核)
// x1 -> 0
// x2 -> 0
// x3 -> 0
// x4 -> 32位内核入口点,_start位置
_start:
    // 在我们的代码之前设置堆栈
    ldr     x5, =_start
    mov     sp, x5

    // 清除bss
    ldr     x5, =__bss_start
    ldr     w6, =__bss_size
3:  cbz     w6, 4f
    str     xzr, [x5], #8
    sub     w6, w6, #1
    cbnz    w6, 3b

    // 跳转到C代码,不应该返回
4:  bl      kernel_main
    // 对于故障保护,也请停止此核心
    b 1b

使用以下方法编译代码:

aarch64-elf-as -c boot.S -o boot.o

实现内核

到目前为止,我们已经编写了bootstrap程序集存根,用于设置处理器,以便可以使用高级语言 (例如C)。 也可以使用C等其他语言。

独立和托管环境

如果你在用户空间中进行了C或C编程,则使用了所谓的托管环境。 托管意味着有一个C标准库和其他有用的运行时功能。另外,还有独立式版本,这就是我们在这里使用的版本。 独立式意味着没有C标准库,只有我们自己提供的。 但是,有些头文件实际上不是C标准库的一部分,而是编译器。 即使在独立的C源代码中,这些仍然可用。 在这种情况下,我们使用 <stddef.h> 来获取size_t & NULL,并使用 <stdint.h> 来获取对于操作系统开发非常宝贵的intx_t和uintx_t数据类型,你需要确保变量是一个精确的大小 (如果我们使用一个短而不是uint16_t和短的大小改变,我们的代码将被破坏!)。 此外,你还可以访问 <float.h>,<iso646.h>,<limits.h> 和 <stdarg.h> 标头,因为它们也是独立的。 GCC实际上还提供了一些头,但这些都是特殊用途。

用C编写内核

下面演示如何在C ++ 中创建一个简单的内核。 请花点时间理解代码。 要在运行时设置 “int raspi” 的值,请参见 检测板类型

#include <stddef.h>
#include <stdint.h>

static uint32_t MMIO_BASE;

// MMIO区域基地址,取决于主板类型
static inline void mmio_init(int raspi)
{
    switch (raspi) {
        case 2:
        case 3:  MMIO_BASE = 0x3F000000; break; // for raspi2 & 3
        case 4:  MMIO_BASE = 0xFE000000; break; // for raspi4
        default: MMIO_BASE = 0x20000000; break; // for raspi1, raspi zero etc.
    }
}

// 内存映射的I/O输出
static inline void mmio_write(uint32_t reg, uint32_t data)
{
	*(volatile uint32_t*)(MMIO_BASE + reg) = data;
}

// 内存映射I/O输入
static inline uint32_t mmio_read(uint32_t reg)
{
	return *(volatile uint32_t*)(MMIO_BASE + reg);
}

// 以编译器不会优化的方式循环 <延迟> 时间
static inline void delay(int32_t count)
{
	asm volatile("__delay_%=: subs %[count], %[count], #1; bne __delay_%=\n"
		 : "=r"(count): [count]"0"(count) : "cc");
}

enum
{
    // reach寄存器的偏移量。
    GPIO_BASE = 0x200000,

    // 控制所有GPIO引脚上/下拉的致动。
    GPPUD = (GPIO_BASE + 0x94),

    // Controls actuation of pull up/down for specific GPIO pin.
    GPPUDCLK0 = (gpio _ 基数0x98)

    // UART的基址。
    UART0_BASE = (GPIO_BASE + 0x1000), // for raspi4 0xFE201000, raspi2 & 3 0x3F201000, and 0x20201000 for raspi1

    // UART的reach register的偏移量。
    UART0_DR     = (UART0_BASE + 0x00),
    UART0_RSRECR = (UART0_BASE + 0x04),
    UART0_FR     = (UART0_BASE + 0x18),
    UART0_ILPR   = (UART0_BASE + 0x20),
    UART0_IBRD   = (UART0_BASE + 0x24),
    UART0_FBRD   = (UART0_BASE + 0x28),
    UART0_LCRH   = (UART0_BASE + 0x2C),
    UART0_CR     = (UART0_BASE + 0x30),
    UART0_IFLS   = (UART0_BASE + 0x34),
    UART0_IMSC   = (UART0_BASE + 0x38),
    UART0_RIS    = (UART0_BASE + 0x3C),
    UART0_MIS    = (UART0_BASE + 0x40),
    UART0_ICR    = (UART0_BASE + 0x44),
    UART0_DMACR  = (UART0_BASE + 0x48),
    UART0_ITCR   = (UART0_BASE + 0x80),
    UART0_ITIP   = (UART0_BASE + 0x84),
    UART0_ITOP   = (UART0_BASE + 0x88),
    UART0_TDR    = (UART0_BASE + 0x8C),

    // Mailbox寄存器的偏移量
    MBOX_BASE    = 0xB880,
    MBOX_READ    = (MBOX_BASE + 0x00),
    MBOX_STATUS  = (MBOX_BASE + 0x18),
    MBOX_WRITE   = (MBOX_BASE + 0x20)
};

// 设置时钟速率为PL011至3mhz标签的Mailbox message
volatile unsigned int  __attribute__((aligned(16))) mbox[9] = {
    9*4, 0, 0x38002, 12, 8, 2, 3000000, 0 ,0
};

void uart_init(int raspi)
{
	mmio_init(raspi);

	// 禁用UART0。
	mmio_write(UART0_CR, 0x00000000);
	// 设置GPIO引脚14 & & 15。

	// 禁用所有GPIO引脚的上拉/下拉,并延迟150周期。
	mmio_write(GPPUD, 0x00000000);
	delay(150);

	// 禁用引脚14、15的上拉/下拉,并延迟150个周期。
	mmio_write(GPPUDCLK0, (1 << 14) | (1 << 15));
	delay(150);

	// 将0写入GPPUDCLK0以使其生效。
	mmio_write(GPPUDCLK0, 0x00000000);

	// 清除挂起的中断。
	mmio_write(UART0_ICR, 0x7FF);

	// Set integer & fractional part of baud rate.
	// Divider = UART_CLOCK/(16 * Baud)
	// Fraction part register = (Fractional part * 64) + 0.5
	// Baud = 115200.

	// 对于Raspi3和4,默认情况下,UART_CLOCK依赖于系统时钟。
	// 将其设置为3Mhz,以便我们可以一致地设置波特率
	if (raspi >= 3) {
		// UART_CLOCK = 30000000;
		unsigned int r = (((unsigned int)(&mbox) & ~0xF) | 8);
		// wait until we can talk to the VC
		while ( mmio_read(MBOX_STATUS) & 0x80000000 ) { }
		// 将我们的消息发送到属性通道并等待响应
		mmio_write(MBOX_WRITE, r);
		while ( (mmio_read(MBOX_STATUS) & 0x40000000) || mmio_read(MBOX_READ) != r ) { }
	}

	// 分频器 Divider = 3000000/(16*115200) = 1.627 = ~ 1。
	mmio_write(UART0_IBRD, 1);
	// 小数部分寄存器 = (.627*64) 0.5 = 40.6 = ~ 40。
	mmio_write(UART0_FBRD, 40);

	// 启用FIFO & 8位数据传输 (1个停止位,无奇偶校验)。
	mmio_write(UART0_LCRH, (1 << 4) | (1 << 5) | (1 << 6));

	// 屏蔽所有中断。
	mmio_write(UART0_IMSC, (1 << 1) | (1 << 4) | (1 << 5) | (1 << 6) |
	                       (1 << 7) | (1 << 8) | (1 << 9) | (1 << 10));

	// 启用UART0,接收和传输UART的一部分。
	mmio_write(UART0_CR, (1 << 0) | (1 << 8) | (1 << 9));
}

void uart_putc(unsigned char c)
{
	// 等待UART准备好传输。
	while ( mmio_read(UART0_FR) & (1 << 5) ) { }
	mmio_write(UART0_DR, c);
}

unsigned char uart_getc()
{
    // 等待UART收到东西。
    while ( mmio_read(UART0_FR) & (1 << 4) ) { }
    return mmio_read(UART0_DR);
}

void uart_puts(const char* str)
{
	for (size_t i = 0; str[i] != '\0'; i ++)
		uart_putc((unsigned char)str[i]);
}

#if defined(__cplusplus)
extern "C" /* Use C linkage for kernel_main. */
#endif

# ifdef AARCH64
// AArch64的参数
void kernel_main(uint64_t dtb_ptr32, uint64_t x1, uint64_t x2, uint64_t x3)
#else
// arguments for AArch32
void kernel_main(uint32_t r0, uint32_t r1, uint32_t atags)
#endif
{
	// 为Raspi2初始化UART
	uart_init(2);
	uart_puts("Hello, kernel World!\r\n");

	while (1)
		uart_putc(uart_getc());
}

GPU引导加载程序通过r0-r2将参数传递给AArch32内核,引导确保保留这3个寄存器。 它们是C函数调用中的前3个参数。参数基本传染数包含RPi从其启动的设备的代码。 通常为0,但其实际值取决于主板的固件。 r1包含 “arm Linux机器类型”,其对于RPi是3138 (0xc42) 标识BCM2708 CPU。 可以从 这里 获得ARM机器类型的完整列表。 r2包含ATAGs的地址。

对于AArch64,寄存器有点不同,但也作为参数传递给C函数。 第一,x0是DTB的32位地址 (即内存中的 [1])。 小心,它是32位地址,高位可能无法清除。 其他参数,x1-x3现在被清除为零,但保留供将来使用。 你的boot.S应该保存它们。

请注意,我们希望如何使用通用的C函数strlen,但这个函数是C标准库的一部分,我们没有。 相反,我们依靠独立的标头 <stddef.h> 来提供size_t,我们只是声明自己的strlen实现。 你将必须为你希望使用的每个函数执行此操作 (因为独立标头仅提供宏和数据类型)。

GPIO和UART的地址是与外围基地址的偏移量,对于Raspberry Pi 1为0x20000000,对于Raspberry Pi 2和Raspberry Pi 3为0x3F000000。 对于 Raspberry Pi 4,基址是0xFE000000。 你可以在BCM2835手册中找到寄存器的地址以及如何使用它们。 可以通过 读取板id 在运行时检测基址。

编译使用:

arm-none-eabi-gcc -mcpu=arm1176jzf-s -fpic -ffreestanding -std=gnu99 -c kernel.c -o kernel.o -O2 -Wall -Wextra

或64位:

aarch64-elf-gcc -ffreestanding -c kernel.c -o kernel.o -O2 -Wall -Wextra

请注意,上面的代码使用了一些扩展,因此我们将其构建为c99的GNU版本。

链接内核

要创建完整的和最终的内核,我们必须将这些目标文件链接到最终的内核程序中。 在开发用户空间程序时,你的工具链附带了用于链接此类程序的默认脚本。 但是,这些不适合内核开发,我们需要提供自己的自定义链接器脚本。

64位模式的链接器脚本看起来完全一样,除了起始地址。

ENTRY(_start)

SECTIONS
{
    /* 从LOADER_ADDR开始。*/
    . = 0x8000;
    /* 对于AArch64,使用。= 0x80000; */
    __start = .;
    __text_start = .;
    .text :
    {
        KEEP(*(.text.boot))
        *(.text)
    }
    . = ALIGN(4096); /* align to page size */
    __text_end = .;

    __rodata_start = .;
    .rodata :
    {
        *(.rodata)
    }
    . = ALIGN(4096); /* align to page size */
    __rodata_end = .;

    __data_start = .;
    .data :
    {
        *(.data)
    }
    . = ALIGN(4096); /* align to page size */
    __data_end = .;

    __bss_start = .;
    .bss :
    {
        bss = .;
        *(.bss)
    }
    . = ALIGN(4096); /* align to page size */
    __bss_end = .;
    __bss_size = __bss_end - __bss_start;
    __end = .;
}

这里有很多文字,但不要绝望。 如果你一点一点地看,这个脚本是相当简单的。

ENTRY(_start) 声明内核映像的入口点。 该符号已在boot.S文件中声明。由于我们实际上是在启动二进制映像,因此该条目完全无关紧要,但是它必须存在于我们作为中间文件构建的elf文件中。

SECTIONS声明节。 它决定了我们的代码和数据的点点滴滴,并设置了一些符号来帮助我们跟踪每个部分的大小。

    . = 0x8000;
    __start = .;

“.” 表示当前地址,因此第一行告诉链接器将当前地址设置为0x8000 (或0x80000),即内核开始的位置。 当链接器添加数据时,当前地址会自动递增。 然后,第二行创建一个符号 “__start”,并将其设置为当前地址。

之后,为文本 (代码),只读数据,读写数据和BSS (0初始化内存) 定义了部分。 除了名称之外,这些部分是相同的,所以让我们看看其中一个:

    __text_start = .;
    .text : {
        KEEP(*(.text.boot))
        *(.text)
    }
    . = ALIGN(4096); /* align to page size */
    __text_end = .;

第一行为该部分创建一个 __text_start符号。 第二行打开一个。输出文件的文本部分,在第五行中关闭。 第3行和第4行声明输入文件中的哪些部分将放置在输出中。文本部分。 在我们的案例中,“.text.boot” 将首先放置在更一般的 “.text” 之后。“.text.boot” 仅在boot.S中使用,并确保它最终位于内核映像的开头。“.text” 然后包含所有剩余的代码。 链接器添加的任何数据都会自动增加当前地址 (“.”)。 在第6行中,我们显式地增加它,使其与4096字节边界对齐 (这是RPi的页面大小)。 最后一行7创建一个 __text_end符号,以便我们知道该部分的结尾。

__ text_start和 __ text_end是什么?为什么使用页面对齐? 可以在内核源中使用2个符号,然后链接器将正确的地址放入二进制文件中。 例如,在boot.S中使用 __ bss_start和 __ bss_end。 但是你也可以通过先声明它们extern来使用来自C的符号。 虽然不是必需的,但我使所有部分都与页面大小对齐。 稍后,这允许将它们映射到具有可执行、只读和读写权限的页表中,而不必处理重叠 (一页中有2个部分)。

    __end = .;

声明所有部分后,将创建 __end符号。 如果你想知道你的内核在运行时有多大,你可以使用 __start和 __end来找出答案。

有了这些组件,你现在可以实际构建最终内核。 我们使用编译器作为链接器,因为它允许它更好地控制链接过程。 请注意,如果你的内核是用C编写的,则应改用C编译器。

然后,你可以使用以下方式链接你的内核:

arm-none-eabi-gcc -T linker.ld -o myos.elf -ffreestanding -O2 -nostdlib boot.o kernel.o -lgcc
arm-none-eabi-objcopy myos.elf -O binary kernel7.img

或64位:

aarch64-elf-gcc -T linker.ld -o myos.elf -ffreestanding -O2 -nostdlib boot.o kernel.o -lgcc 
aarch64-elf-objcopy myos.elf -O binary kernel8.img

启动内核

过一会儿,你会看到你的内核在运行。

测试你的操作系统 (真实硬件)

当你在上面测试硬件时,你是否仍然拥有带有原始Raspbian映像的sd卡? 太好了。 所以你已经有一个带有引导分区和所需文件的sd卡。 如果没有,请下载原始Raspberry引导映像之一并将其复制到sd卡。

现在从sd卡挂载第一个分区,看看它:

bootcode.bin  fixup.dat     kernel.img            start.elf
cmdline.txt   fixup_cd.dat  kernel_cutdown.img    start_cd.elf
config.txt    issue.txt     kernel_emergency.img

如果没有raspbian映像,则可以从官方存储库中创建一个FAT32分区,然后 下载固件文件。 你只需要三个文件:

  • bootcode.bin: 这是首先加载的,在GPU上执行的 (RPi4不需要,因为该模型在ROM中具有bootcode.bin)
  • fixup.dat: 此数据文件包含与硬件相关的重要信息,必须具有
  • start.elf: 这是RPi固件 (与IBM PC上的BIOS相同)。这也在GPU上运行。

简化当RPi上机时,ARM CPU停止并且GPU运行。 GPU从ROM加载引导加载程序并执行 然后找到sd卡并加载bootcode.bin (RPi4除外,它有一个足够大的ROM来包含bootcode.bin)。 bootcode加载固件start.elf,它处理config.txt和cmdline.txt。 start.elf加载内核 *.img,最后ARM CPU开始运行该内核映像。

要在ARM模式之间切换,你必须重命名内核.img文件。 如果将其重命名为 kernel7.img,则将在AArch32模式 (ARMv7) 下执行。 对于AArch64模式 (ARMv8),你必须将其重命名为 kernel8.img

因此,现在我们用自己的umount,sync替换原始的kernel.img,将sd卡插入RPi并打开电源。 然后,你的微型计算机应该显示以下内容:

Hello, kernel World!

测试你的操作系统 (QEMU)

QEMU支持用机器类型 “raspi2” 模拟Raspberry Pi 2。 在撰写本文时,此功能在大多数软件包管理器中不可用,但可以在此处找到的最新QEMU来源中找到: https://github.com/qemu/qemu

检查你的QEMU安装是否有qemu-system-arm,并且它支持选项 “-M raspi2”。 在QEMU中进行测试时,请务必使用源代码中注明的raspi2基址。

使用QEMU,你无需将内核objcopy到纯二进制文件中; QEMU还支持ELF内核:

$YOURINSTALLLOCATION/bin/qemu-system-arm -m 256 -M raspi2 -serial stdio -kernel kernel.elf

更新了对AArch64 (raspi2,raspi3) 的支持

从QEMU 2.12 (2018年4月) 开始,针对64位ARM “qemu-system-aarch64” 的仿真现在支持使用机器类型 “raspi2” 和 “raspi3” 直接仿真Raspberry Pi 2和3。分别。 这应该允许测试64位系统代码。

qemu-system-aarch64 -M raspi3 -serial stdio -kernel kernel8.img

请注意,在大多数情况下,32位ARM代码和64位ARM代码之间几乎没有差异,但是代码的行为方式可能会有所不同,尤其是在内核内存管理方面。 此外,一些AArch64实现可以支持在它们的32位对应物中的任何一个上找不到的特征 (例如,加密扩展、增强的霓虹灯SIMD支持)。

另一个非常重要的注意事项: 从Raspberry Pi 3开始,SoC更改为 1888662/ BCM2837,并且PL011时钟 (UART0) 不再固定,而是从系统时钟派生。 因此,要正确设置波特率,首先必须设置时钟频率。 这可以通过 [2] 来完成。 或者你可以使用AUX miniUART (UART1) 芯片,这更容易编程。 下面的链接包括有关如何同时进行的教程。

= 另见 =

文章

外部链接