hnu操作系统-实验踩坑实录

实验文档

建议大家一边做一边给虚拟机拍个快照;用wsl2的友友们给子系统备个份。
血泪教训。


lab1

实验目的

  • 完成实验环境配置
  • 熟悉cmake构建系统
  • 学会调试

安装工具链

宿主机: Description: Ubuntu 24.04.2 LTS

找一个合适的目录下载工具链,解压并重命名工具链目录:

1
2
3
4
5
6
下载工具链,以下载工具链版本为11.2,宿主机为x86 64位 Linux机器为例
$ wget https://developer.arm.com/-/media/Files/downloads/gnu/11.2-2022.02/binrel/gcc-arm-11.2-2022.02-x86_64-aarch64-none-elf.tar.xz
# 解压工具链
$ tar -xf gcc-arm-(按Tab键补全)
# 重命名工具链目录
$ mv gcc-arm-(按Tab键补全) aarch64-none-el

arrch64环境变量配置

添加环境变量:

1
2
vim ~/.bashrc 

进入vim编辑器后按进入insert模式,在文件末尾插入:

1
export PATH="/你的路径/aarch64-none-elf/bin:$PATH"

重新加载~/.bashrc文件

1
source ~/.bashrc

测试工具链是否安装成功

1
aarch64-none-elf-gcc --version

QEMU安装

命令行安装qemu

1
2
3
sudo apt-get update
sudo apt-get install qemu
sudo apt-get install qemu-system

==报错: Package ‘qemu’ has no installation candidate==

查阅官方文档

直接输入以上命令即可

安装cmake

Linux

1
sudo apt-get install cmake

创建裸机(Bare Metal)程序

由于我们的目标是编写一个操作系统,所以我们需要创建一个独立于操作系统的可执行程序,又称 独立式可执行程序(freestanding executable) 或 裸机程序(bare-metal executable) 。这意味着所有依赖于操作系统的库我们都不能使用。比如 std 中的大部分内容(io, thread, file system, etc.)都需要操作系统的支持,所以这部分内容我们不能使用。

创建项目

项目结构

最好跟实验文档的项目目录创建得一模一样,省得后续改一堆路径。
命令使用:

1
2
3
man // 查看命令的帮助文档
mkdir //创建目录
vim //用vim编辑器创建文件

==注意:prt_typedef.h,arrch64-qemu.ld需要下载完整文件。==

main.c源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "prt_typedef.h"

#define UART_REG_WRITE(value, addr)  (*(volatile U32 *)((uintptr_t)addr) = (U32)value)

S32 main(void)
{
    char out_str[] = "AArch64 Bare Metal";
    int length = sizeof(out_str) / sizeof(out_str[0]);
    // 逐个输出字符

    for (int i = 0; i < length - 1; i++) {
        UART_REG_WRITE(out_str[i], 0x9000000);
    }

}

prt_typedef.h源码

https://os2024lab.readthedocs.io/zh-cn/latest/_static/prt_typedef.h

start.S 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    .global   OsEnterMain //声明OsEnterMain为全局符号,其他文件可访问
.extern __os_sys_sp_end //声明__os_sys_sp_end为外部符号(在链接脚本中定义)

.type start, function //声明start的类型为函数
.section .text.bspinit, "ax"//将后续代码放入 .text.bspinit 段,属性为可分配(alloc)和可执行(exec)

.balign 4 //按四字节对其代码

.global OsElxState //声明OsElxState为全局符号
.type OsElxState, @function //声明 OsElxState的类型为函数
OsElxState:
MRS x6, CurrentEL // 把系统寄存器 CurrentEL 的值读入到通用寄存器 x6 中
MOV x2, #0x4 // CurrentEL EL1: bits [3:2] = 0b01
CMP w6, w2 //比较x6和x2的低32位

BEQ Start // 若 相等,CurrentEl 为 EL1 级别,跳转到 Start 处执行,否则死循环。

OsEl2Entry:
B OsEl2Entry

Start:
LDR x1, =__os_sys_sp_end // 符号在ld文件中定义,加载到x1
BIC sp, x1, #0xf // 设置栈指针

B OsEnterMain

OsEnterReset:
B OsEnterReset //系统复位的入口点,此处为为实现的占位代码

prt_reset_vector.S 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
DAIF_MASK = 0x1C0       // disable SError Abort, IRQ, FIQ

.global OsVectorTable// 声明全局符号 OsVectorTable(异常向量表)
.global OsEnterMain// 声明全局符号 OsEnterMain(操作系统入口函数)

.section .text.startup, "ax"

OsEnterMain:
BL main // 调用 main 函数(跳转并保存返回地址到 LR 寄存器)


MOV x2, DAIF_MASK // bits [9:6] disable SError Abort, IRQ, FIQ
MSR DAIF, x2 // 把通用寄存器 x2 的值写入系统寄存器 DAIF 中

EXITLOOP:
B EXITLOOP


​关键硬件与概念​

  1. ​异常级别(Exception Level, EL)​
    • ​EL0​​:用户态(应用程序)。
    • ​EL1​​:内核态(操作系统)。
    • ​EL2​​:虚拟化监控态(Hypervisor)。
    • ​EL3​​:安全监控态(Secure Monitor)。
  2. ​CurrentEL 寄存器​
    • ARMv8 系统寄存器,存储当前异常级别信息。
    • 位 [3:2] 表示当前 EL:
      • 0b00 → EL0
      • 0b01 → EL1
      • 0b10 → EL2
      • 0b11 → EL3
  3. ​DAIF 寄存器​
    • ARMv8 的系统寄存器,控制中断和异常的处理状态:
      • ​D​​ (Debug):调试异常使能。
      • ​A​​ (SError Abort):系统错误中止使能。
      • ​I​​ (IRQ):普通中断使能。
      • ​F​​ (FIQ):快速中断使能。
    • 写入 1 到对应位会禁用相关事件响应。
  4. ​BL 指令​
    • 分支链接(Branch with Link)指令,用于调用函数:
      • 跳转到目标地址(如 main)。
      • 将返回地址(下一条指令地址)保存到 LR(x30)寄存器

链接脚本 aarch64-qemu.ld 脚本

工程构建

操作系统是一个复杂的工程。如当前版本的 UniProton 包含了近 500 个文件,超过 10 万行的代码及说明,而 Linux 内核则包含有 6 万多个文件,超过 2700 万行的代码 (2020)。如果纯手工构建这样的系统是不可想象的,所以我们需要构建系统的帮助。

CMake 是一个跨平台的开源构建系统。CMake 通过简单的、与平台和编译器无关的配置文件来控制软件编译过程

CMakeList.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
cmake_minimum_required(VERSION 3.12)

set(CMAKE_SYSTEM_NAME "Generic") # 目标系统(baremental):  cmake/tool_chain/uniproton_tool_chain_gcc_arm64.cmake 写的是Linux
set(CMAKE_SYSTEM_PROCESSOR "aarch64") # 目标系统CPU

set(TOOLCHAIN_PATH "/home/tutu/aarch64-none-elf") # 修改为交叉工具链实际所在目录 build.py config.xml中定义
set(CMAKE_C_COMPILER ${TOOLCHAIN_PATH}/bin/aarch64-none-elf-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PATH}/bin/aarch64-none-elf-g++)
set(CMAKE_ASM_COMPILER ${TOOLCHAIN_PATH}/bin/aarch64-none-elf-gcc)
set(CMAKE_LINKER ${TOOLCHAIN_PATH}/bin/aarch64-none-elf-ld)

# 定义编译和链接等选项
set(CC_OPTION "-Os   -Wformat-signedness    -Wl,--build-id=none   -fno-PIE -fno-PIE --specs=nosys.specs -fno-builtin -fno-dwarf2-cfi-asm -fomit-frame-pointer -fzero-initialized-in-bss -fdollars-in-identifiers -ffunction-sections -fdata-sections -fno-aggressive-loop-optimizations -fno-optimize-strlen -fno-schedule-insns -fno-inline-small-functions -fno-inline-functions-called-once -fno-strict-aliasing -finline-limit=20  -mlittle-endian -nostartfiles -funwind-tables")
set(AS_OPTION "-Os   -Wformat-signedness    -Wl,--build-id=none   -fno-PIE -fno-PIE --specs=nosys.specs -fno-builtin -fno-dwarf2-cfi-asm -fomit-frame-pointer -fzero-initialized-in-bss -fdollars-in-identifiers -ffunction-sections -fdata-sections -fno-aggressive-loop-optimizations -fno-optimize-strlen -fno-schedule-insns -fno-inline-small-functions -fno-inline-functions-called-once -fno-strict-aliasing -finline-limit=20  -mlittle-endian -nostartfiles -funwind-tables")
set(LD_OPTION " ")
set(CMAKE_C_FLAGS "${CC_OPTION} ")
set(CMAKE_ASM_FLAGS "${AS_OPTION} ")
set(CMAKE_LINK_FLAGS "${LD_OPTION} -T ${CMAKE_CURRENT_SOURCE_DIR}/aarch64-qemu.ld") # 指定链接脚本
set(CMAKE_EXE_LINKER_FLAGS "${LD_OPTION} -T ${CMAKE_CURRENT_SOURCE_DIR}/aarch64-qemu.ld") # 指定链接脚本
set (CMAKE_C_LINK_FLAGS " ")
set (CMAKE_CXX_LINK_FLAGS " ")

set(HOME_PATH ${CMAKE_CURRENT_SOURCE_DIR})

set(APP "miniEuler") # APP变量,后面会用到 ${APP}
project(${APP} LANGUAGES C ASM) # 工程名及所用语言
set(CMAKE_BUILD_TYPE Debug) # 生成 Debug 版本



include_directories( # include 目录
   ${CMAKE_CURRENT_SOURCE_DIR}/include
    ${CMAKE_CURRENT_SOURCE_DIR}/bsp
)


add_subdirectory(bsp) # 包含子文件夹的内容

list(APPEND OBJS $<TARGET_OBJECTS:bsp>) #CMake 生成表达式,获取 bsp 目标的所有对象文件。
add_executable(${APP} main.c ${OBJS}) # 可执行文件

==记得修改交叉工具链aarch64-none-elf所在目录,即重命名后的gcc-arm-11.2-2022.02-x86_64-aarch64-none-elf。==

1
pwd aarch64-none-elf //查看目录所在

src/bsp/下的CMakeLists.txt

1
2
set(SRCS start.S prt_reset_vector.S )
add_library(bsp OBJECT ${SRCS})  # OBJECT类型只编译生成.o目标文件,但不实际链接成库

编译

在lab1中新建目录build,cd进build执行以下命令,”..”代表上级目录

1
2
cmake ../src
cmake --build .

[!tip] tip
输入命令时,要时刻注意所在目录,根据所在目录调整命令输入的路径

运行

在lab1下执行,输入命令

1
qemu-system-aarch64 -machine virt -m 1024M -cpu cortex-a53 -nographic -kernel build/miniEuler  -s

输出以下,即成功

调试

这里真的卡了我很久…先装的vscode,报错了一步一步debug,郁闷。
参考公众号:爱吃罐头的熊二,感谢前辈,感谢为我提供公众号的cy!

安装,运行vscode

命令行使用snap安装

1
2
3
4
5
sudo apt update
sudo apt install snapd
sudo reboot//重启
sudo snap install code --classic
code //运行vscode,弹出窗口

vscode打开lab1(一定要是这一级目录,不然要在CMakeLists.txt改路径)
根据实验文档添加配置
在左边面板顶部选择刚添加的 aarch64-gdb 选项,点击旁边的绿色 开始调试(F5) 按钮开始调试。
此时报错:

deepseek告诉我,它找不到 libncursesw.so.5 共享库

检查一下gdb依赖项

1
ldd aarch64-none-elf/bin/aarch64-none-elf-gdd

输出这一大段,大概就是这几个共享库没有,非常遗憾,apt-get也没有这几个包:

1
2
3
libncursesw.so.5 => not found 
libtinfo.so.5 => not found
libpython3.6m.so.1.0 => not found

想办法装上,有点忘了这块当时怎么搞的,弄了个软连接还是啥的
历史记录翻到这个,可以试试看
项目首页 - Ubuntulibncurses.so.5离线安装包:Ubuntu libncurses.so.5 离线安装包 - GitCode

python3.6.15安装

我们还缺一个libpython3.6m.so.1.0库,按照公众号的做法一步一步下来,连接国外源可能会有点问题,可以换个镜像源或者在主机上下好拉进虚拟机里
Python Release Python 3.6.15 | Python.org
python-release-source安装包下载_开源镜像站-阿里云
解压(找一个合适的目录装python哈)

1
2
3
4
tar -xzf Python-3.6.15.tgz
cd Python-3.6.15
sudo LDFLAGS="-L/usr/lib/x86_64-linux-gnu" ./configure
sudo ./configure --enable-shared

到这一步可能会报错

1
Segmentation fault (core dumped) make: *** [Makefile:1112: altinstall] Error 139

可能是gcc版本导致的不相容
装个gcc10,修改一下默认的gcc版本,记得下完之后再改回来

1
2
3
4
gcc -v//看一下gcc版本
//是11的话试试下面的命令
sudo apt-get install gcc-10 -y
sudo update-alternatives --config gcc

自动化脚本

makeMiniEuler.sh

1
2
3
4
5
6
7
#sh makeMiniEuler.sh 不打印编译命令
#sh makeMiniEuler.sh -v 打印编译命令等详细信息
rm -rf build/*
mkdir build
cd build
cmake ../src
cmake --build . $1

runMiniEuler.sh

1
2
3
4
5
6
#sh runMiniEuler.sh 直接运行
#sh runMiniEuler.sh -S 启动后在入口处暂停等待调试

echo qemu-system-aarch64 -machine virt,gic-version=2 -m 1024M -cpu cortex-a53 -nographic -kernel build/miniEuler  -s $1

qemu-system-aarch64 -machine virt,gic-version=2 -m 1024M -cpu cortex-a53 -nographic -kernel build/miniEuler  -s $1

vscode调试

打开 main.c 文件,点击 vscode左侧的运行和调试按钮,弹出对话框选择创建 launch.json文件,增加如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"version": "0.2.0",
"configurations": [
{
"name": "aarch64-gdb",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/miniEuler",
"stopAtEntry": true,
"cwd": "${fileDirname}",
"environment": [],
"externalConsole": false,
"launchCompleteCommand": "exec-run",
"MIMode": "gdb",
"miDebuggerPath": "/usr/local/aarch64-none-elf/bin/aarch64-none-elf-gdb", // 修改成交叉调试器gdb对应位置
"miDebuggerServerAddress": "localhost:1234",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
],

}

在lab1下新建makeMiniEuler.sh脚本编译项目,runMiniEuler.sh脚本运行项目
==在vscode下方终端里运行实验文档的连接,sh runMiniEuler.sh -S,一定要接-S哈==

开始调试

  • 在左边面板顶部选择刚添加的 aarch64-gdb 选项,点击旁边的绿色 开始调试(F5) 按钮开始调试。
  • 在调试控制台输入-exec gdb调试指令
    • 查看指定地址的内存内容。在调试控制台执行 -exec x/20xw 0x40000000 即可,其中 x表示查看命令,20表示查看数量,x表示格式,可选格式包括 Format letters are o(octal), x(hex), d(decimal), u(unsigned decimal),t(binary), f(float), a(address), i(instruction), c(char) and s(string).Size letters are b(byte), h(halfword), w(word), g(giant, 8 bytes).,最后的 w表示字宽,b表示单字节,h表示双字节,w表示四字节,g表示八字节。还可以是指令:-exec x/20i 0x40000000; 字符串:-exec x/20s 0x40000000
    • 显示所有寄存器。-exec info all-registers
    • 查看寄存器内容。-exec p/x $pc
    • 修改寄存器内容。-exec set $x24 = 0x5
    • 修改指定内存位置的内容。-exec set {int}0x4000000 = 0x1 或者 -exec set *((int *) 0x4000000) = 0x1
    • 修改指定MMIO 寄存器的内容。 -exec set *((volatile int *) 0x08010004) = 0x1
    • 退出调试 -exec q
  • 要打断点的话,在settings里面勾选此项:

gdb调试

首先运行一下两个自动化脚本
新开一个终端,输入以下命令

1
aarch64-none-elf-gdb build/miniEuler

在gdb窗口输入

1
target remote localhost:1234

gdb基本调试命令

作业1

请操作 NZCV 寄存器获取 start.S 中执行 CMP w6, w2 前后 NZCV 寄存器的变化。

预备知识

CPSR(Current Program Status Register,当前程序状态寄存器)是ARM处理器中的一个重要寄存器,用于存储当前处理器的状态信息。CPSR包含条件码标志、中断禁止位、当前处理器模式以及其他状态和控制信息

NZCV标志位存储在cpsr寄存器

标志位 名称 触发条件
​N​ Negative 运算结果的最高位为 1(结果为负数)时置 1
​Z​ Zero 运算结果为零时置 1
​C​ Carry 无符号运算​​时发生进位(加法)或​​无借位​​(减法))
​V​ oVerflow 有符号运算结果超出可表示范围时置 1

调试过程

  • 打上断点
  • 查看cpsr值

    -CPSR = 0x60003c5
    转换为二进制:
    0100 0000 0000 0000 0011 1100 0101
    - ​​NZCV ([31:28])​​: 0110 → N=0Z=1C=0V=0
    - ​​含义​​:初始状态为 ​​运算结果为零(Z=1
  • 执行CMP后查看cpsr值
    • CPSR = 0x60003c5
      转换为二进制:
      0110 0000 0000 0000 0011 1100 0101
      • ​NZCV ([31:28])​​: 0110 → N=0Z=1C=1V=0
        • ​含义​​:初始状态为 ​​运算结果为零(Z=1)且无符号运算无进位(C=1)​​。

作业2

商业操作系统都有复杂的构建系统,试简要分析 UniProton 的构建系统。
脚本链接
整个Uniproton系统项目如下

通过build.py脚本对系统进行构建

  • 类功能表格
类/方法 功能描述
​Compile类​ 主构建控制器,管理整个编译流程
get_config() 根据CPU类型和平台加载编译配置(编译器路径、库类型等)
setCmdEnv() 设置日志文件和构建时间标签
SetCMakeEnviron() 配置CMake所需的环境变量(如HCC_PATHCPU_TYPE
prepare_env() 准备构建环境(调用get_configSetCMakeEnviron
getOsPlatform() 检测主机架构(x86/arm64/riscv64)并设置工具链路径
CMake() 生成CMake构建目录并执行CMake命令
make() 执行Make命令(编译和安装)
UniProton_clean() 清理构建生成的临时文件和日志
SdkCompaile() 组合流程:生成构建定义 → CMake → Make
==UniProtonCompile()== ==对外接口,根据参数选择全量或部分构建==
MakeBuildef() 调用外部脚本生成构建定义文件(如prt_buildef.h
  • 流程解析

1. 初始化与环境准备

  • 类实例化: def init
    • 脚本通过Compile类的构造函数初始化编译所需的参数和路径。
    • 参数包括cpu_type(CPU类型)、compile_option(编译选项)、lib_run_type(库运行平台)、make_choice(编译目标选择)、make_phase(编译阶段)等。
    • 初始化过程中调用getOsPlatform方法获取当前操作系统平台信息,并将其写入环境变量。
  • 环境变量设置
    • SetCMakeEnviron方法将一系列编译相关的配置(如CPU类型、平台类型、库类型、编译选项等)写入环境变量,供后续CMake使用。
    • 设置了PATH环境变量,确保编译工具链路径正确。
  • 日志环境初始化
    • setCmdEnv方法生成构建时间标签,并设置日志目录和日志文件路径,为后续的日志记录做好准备。

2. 编译环境配置

  • 获取编译配置
    • get_config方法根据cpu_typecpu_plat从全局配置中加载编译所需的信息,包括库类型、平台类型、HCC路径、KConfig目录、系统类型和核心配置。
    • 如果未找到正确的配置,会记录错误日志并退出程序。
  • 准备编译环境
    • prepare_env方法整合了上述步骤,完成编译环境的全面准备。
    • 包括调用get_config获取配置信息、调用setCmdEnv设置日志环境、调用SetCMakeEnviron设置环境变量

3. 清理缓存

  • 清理操作
    • 如果cpu_typeclean,调用UniProton_clean方法清理项目构建过程中生成的临时文件和缓存。
    • 清理范围包括logsoutput__pycache__等目录以及特定文件(如prt_buildef.h)。

4. CMake配置

  • 生成Makefile
    • CMake方法负责调用CMake生成Makefile文件。
    • 根据不同的编译选项(如normalfortifyhllt等),拼接相应的CMake命令。
    • 如果生成失败,记录错误日志并返回False

5. 执行Make构建

  • Make构建过程
    • make方法根据make_phase的值决定是否执行Make构建。
    • 首先执行make clean清理之前的构建结果。
    • 然后执行make all构建目标,最后根据编译选项决定是否执行make install安装目标。
    • 构建过程中会记录日志,并在失败时返回False

6. SDK编译

  • 编译流程

    • SdkCompaile方法是整个编译的核心逻辑,包含以下几个步骤:
      1. 判断当前环境中是否需要编译。如果不需要,则直接返回True
      2. 调用MakeBuildef生成buildef文件。
      3. 调用CMake生成Makefile文件。
      4. 调用make并返回
      5. 如果编译失败,则记录失败日志并返回False

    如果所有步骤都成功,则记录成功日志并返回

 

7.主入口逻辑

  • 脚本执行入口
    • if __name__ == "__main__":块中,脚本解析命令行参数或使用默认参数。
    • 遍历指定的平台列表,为每个平台创建Compile对象并调用==UniProtonCompile==方法进行编译。
    • 如果任意一个平台的编译失败,则退出程序并返回非零状态码。

作业3

学习调试项目

gdb调试

运行makeMiniEuler.sh runMiniEuler.sh 两个脚本,启动调试服务器,默认端口1234

1
2
sh makeMiniEuler.sh 
sh runMiniEuler.sh -S//在入口处等待调试


新建终端,启动调试客户端

1
aarch64-none-elf-gdb build/miniEuler

连接服务器

逐步调试

vscode调试

在终端启动调试服务器
创建launch.json文件->设置允许在任何文件 设置断点->F5开始调试->F10逐步 调试

调试控制台调试:


lab2 Hello miniEuler

实验目的

print函数是学习几乎任何一种软件开发语言时最先学习使用的函数,同时该函数也是最基本和原始的程序调试手段,但该函数的实现却并不简单。本实验的目的在于理解操作系统与硬件的接口方法,并实现一个可打印字符的函数(非系统调用),用于后续的调试和开发。

实验过程

​Virt 机器详解​

  1. 查看QEMU关于 virt的描述 , 或者查看QEMU的源码,如github上的 virt.h 和 virt.c。virt.c中可见如下有关内存映射的内容。

​1. 基本概念​

​Virt 机器​​(Virt Machine)是一种​​轻量级虚拟机(Virtual Machine)​​,通常由 ​​QEMU​​ 或类似模拟器提供,专为​​快速启动、开发和测试​​设计。

  • ​核心特点​​:
    • 无需完整操作系统镜像(如磁盘文件),直接加载内核和内存文件运行。
    • 虚拟化标准硬件(如 UART、RAM、CPU),不模拟特定物理设备。
    • 常用于 ​​嵌入式开发​​ 和 ​​操作系统内核调试​​(如 ARM Cortex-M/A、RISC-V)。

​2. 典型应用场景​

​场景​ ​说明​
​嵌入式开发​ 测试裸机程序(如 start.S 启动代码)或 RTOS(如 UniProton)。
​操作系统内核调试​ 快速启动 Linux 内核,无需完整磁盘镜像(如 qemu-system-arm -kernel zImage)。
​教学与原型验证​ 提供干净的虚拟硬件环境,避免物理开发板的复杂性。

​3. Virt 机器的硬件组成​

Virt 机器模拟的硬件通常是​​最小化通用设备​​,例如:

  • ​CPU​​:ARM Cortex-A53/A72、RISC-V 64 等。
  • ​内存​​:默认 128MB~1GB(可通过参数调整)。
  • ​外设​​:
    - 串口(UART)用于输入输出。
    - 虚拟中断控制器(GIC)。
    - 简单帧缓冲区(可选,用于图形显示)。

通过QEMU导出设备树

1
2
3
apt-get install device-tree-compiler
qemu-system-aarch64 -machine virt,dumpdtb=virt.dtb -cpu cortex-a53 -nographic
dtc -I dtb -O dts -o virt.dts virt.dtb

QEMU 导出 Virt 虚拟机的硬件设备树二进制(virt.dtb)。将二进制 DTB 转换为可读的 DTS 文件,用于分析或修改硬件配置。

virt.dtb转换后生成的virt.dts中可找到如下内容

由上可以看出,virt机器包含有pl011的设备,该设备的寄存器在0x9000000开始处。pl011实际上是一个UART设备,即串口。可以看到virt选择使用pl011作为标准输出,这是因为与PC不同,大部分嵌入式系统默认情况下并不包含VGA设备。

实现PRR_Printf函数

宏定义

技术手册
在 print.c 中包含所需头文件,并定义后续将会用到的宏,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdarg.h>
#include "prt_typedef.h"

#define UART_0_REG_BASE 0x09000000 // pl011 设备寄存器地址

#define DW_UART_THR 0x00 // UARTDR(Data Register) 寄存器
#define DW_UART_FR 0x18 // UARTFR(Flag Register) 寄存器
#define DW_UART_LCR_HR 0x2c // UARTLCR_H(Line Control Register) 寄存器
#define DW_XFIFO_NOT_FULL 0x020 // 发送FIFO未满时,缓冲区满置位 100000
#define DW_FIFO_ENABLE 0x10 // 启用发送和接收FIFO

#define UART_BUSY_TIMEOUT 1000000 //发送超时阈值
#define OS_MAX_SHOW_LEN 0x200 //格式化输出缓冲区大小(512字节)


#define UART_REG_READ(addr) (*(volatile U32 *)(((uintptr_t)addr))) // 读设备寄存器
#define UART_REG_WRITE(value, addr) (*(volatile U32 *)((uintptr_t)addr) = (U32)value) // 写设备寄存器

[!NOTE] Title
注意到我们只是向UART0写入,而没从UART0读出(如果读出会读出其他设备通过串口发送过来的数据,而不是刚才写入的数据,注意体会这与读写内存时是不一样的,详情参见pl011的技术手册),编译器在优化时可能对这部分代码进行错误的优化,如把这些操作都忽略掉。在 ==UART_REG_READ 宏和 UART_REG_WRITE 宏中使用 volatile 关键字==的目的是告诉编译器,这些读取或写入有特定目的,不应将其优化(也就是告诉编译器不要瞎优化,这些写入和读出都有特定用途。如连续两次读,编译器可能认为第二次读就是前次的值,所以优化掉第二次读,但对外设寄存器的连续读可能返回不同的值。再比如写,编译器可能认为写后没有读所以写没有作用,或者连续的写会覆盖前面的写,但对外设而言对这些寄存器的写入都有特定作用)。

串口的初始化

1
2
3
4
5
6
7
8
9
10
U32 PRT_UartInit(void)
{
U32 result = 0;
U32 reg_base = UART_0_REG_BASE; //寄存器基址

result = UART_REG_READ((unsigned long)(reg_base + DW_UART_LCR_HR)); //读取LCR寄存器,result存储寄存器值
UART_REG_WRITE(result | DW_FIFO_ENABLE, (unsigned long)(reg_base + DW_UART_LCR_HR)); // 启用 FIFO,将result写回寄存器

return OS_OK;
}

往串口发送字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 读 reg_base + offset 寄存器的值。 uartno 参数未使用
S32 uart_reg_read(S32 uartno, U32 offset, U32 *val)
{
S32 ret;
U32 reg_base = UART_0_REG_BASE;


*val = UART_REG_READ((unsigned long)(reg_base + offset));
return OS_OK;//OS_OK为0
}

// 通过检查 FR 寄存器的标志位确定发送缓冲是否满,满时返回1.
S32 uart_is_txfifo_full(S32 uartno)
{
S32 ret;
U32 usr = 0;

ret = uart_reg_read(uartno, DW_UART_FR, &usr);
if (ret) {
return OS_OK;//OS_OK为0
}

return (usr & DW_XFIFO_NOT_FULL);
}

// 往 reg_base + offset 寄存器中写入值 val。
void uart_reg_write(S32 uartno, U32 offset, U32 val)
{
S32 ret;
U32 reg_base = UART_0_REG_BASE;

UART_REG_WRITE(val, (unsigned long)(reg_base + offset));
return;
}

// 通过轮询的方式发送字符到串口
void uart_poll_send(unsigned char ch)
{

S32 timeout = 0;
S32 max_timeout = UART_BUSY_TIMEOUT;

// 轮询发送缓冲区是否满
int result = uart_is_txfifo_full(0);
while (result) {
timeout++;
if (timeout >= max_timeout) {
return;
}
result = uart_is_txfifo_full(0);
}

// 如果缓冲区没满,通过往数据寄存器写入数据发送字符到串口
uart_reg_write(0, DW_UART_THR, (U32)(U8)ch);
return;
}

// 轮询的方式发送字符到串口,且转义换行符
void TryPutc(unsigned char ch)
{
uart_poll_send(ch);
if (ch == '\n') {
uart_poll_send('\r');
}
}

支持格式化输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
extern int  vsnprintf_s(char *buff, int buff_size, int count, char const *fmt, va_list arg);
int TryPrintf(const char *format, va_list vaList)
{
int len;
char buff[OS_MAX_SHOW_LEN];
for(int i = 0; i < OS_MAX_SHOW_LEN; i++) {
buff[i] = 0;
}
char *str = buff;
len = vsnprintf_s(buff, OS_MAX_SHOW_LEN, OS_MAX_SHOW_LEN, format, vaList);
if (len == -1) {
return len;
}

while (*str != '\0') {
TryPutc(*str);
str++;
}

return OS_OK;
}

U32 PRT_Printf(const char *format, ...)
{
va_list vaList;
S32 count;

va_start(vaList, format);
count = TryPrintf(format, vaList);
va_end(vaList);

return count;
}

实现vsnprintf_s 函数

源码
新建 src/bsp/vsnprintf_s.c 实现 vsnprintf_s 函数
vsnprintf_s 函数的主要作用是依据格式控制符将可变参数列表转换成字符列表写入缓冲区。

调用PRT_Printf函数

main.c 修改为调用 PRT_Printf 函数输出信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
S32 main(void)
{
PRT_UartInit();

PRT_Printf(" _ _ _____ _ _ _ _ _ _ _ _ \n");
PRT_Printf(" _ __ ___ (_)_ __ (_) ____| _| | ___ _ __ | |__ _ _ | | | | \\ | | | | | ___ _ __ \n");
PRT_Printf(" | '_ ` _ \\| | '_ \\| | _|| | | | |/ _ \\ '__| | '_ \\| | | | | |_| | \\| | | | |/ _ \\ '__|\n");
PRT_Printf(" | | | | | | | | | | | |__| |_| | | __/ | | |_) | |_| | | _ | |\\ | |_| | __/ | \n");
PRT_Printf(" |_| |_| |_|_|_| |_|_|_____\\__,_|_|\\___|_| |_.__/ \\__, | |_| |_|_| \\_|\\___/ \\___|_| \n");
PRT_Printf(" |___/ \n");


PRT_Printf("Test PRT_Printf int format %d \n\n", 10);
}

将新建文件纳入构建系统

修改 src/bsp/CMakeLists.txt 文件加入新增文件 print.c 和 vsnprintf_s.c

1
2
set(SRCS start.S prt_reset_vector.S print.c vsnprintf_s.c)
add_library(bsp OBJECT ${SRCS}) # OBJECT类型只编译生成.o目标文件,但不实际链接成库

启用 FPU

构建项目并执行发现程序没有任何输出。 需启用 FPU (src/bsp/start.S)。

1
2
3
4
5
6
7
8
9
10
11
12
Start:
LDR x1, =__os_sys_sp_end // ld文件中定义,堆栈设置
BIC sp, x1, #0xf

//参考: https://developer.arm.com/documentation/den0024/a/Memory-Ordering/Barriers/ISB-in-more-detail
Enable_FPU:
MRS X1, CPACR_EL1
ORR X1, X1, #(0x3 << 20)
MSR CPACR_EL1, X1
ISB

B OsEnterMain

作业1

查询文档

DAT寄存器的transmit 和 receive fifo所在处

fifo和单字节传输规则:

LCR寄存器的FEN位控制FIFO使能

FLAG_REGISTER的TXFF,若FIFO使能为1且发送FIFO已满,设为1;

FLAG_REGISTER的TXFE,若FIFO使能为9且发送FIFO底位已空,设为1;

DATA REGISTER:

如果 ​​FIFO 被禁用​​,UART 会退化为 ​​单字节传输模式​​(即每个字节单独发送/接收),数据通过 ​​数据寄存器(UARTDR)​​ 直接传输。

修改代码使其可以动态切换数据传输模式

  • 新增宏定义
1
2
3
#define TX_REG_EMPTY_FLAG    (1 << 7)  // TXFE位(单字节模式下寄存器空置1)

#define enable_fifo 0 //修改此处:切换数据发送模式 1为fifo 0为单字节

宏定义解释

  • 修改串口初始化函数,使其兼容两种数据发送模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
U32 PRT_UartInit(void)

{

    U32 result = 0;
    U32 reg_base = UART_0_REG_BASE;

    result = UART_REG_READ((unsigned long)(reg_base + DW_UART_LCR_HR));//读取LCR寄存器
    if(enable_fifo)
    {
        result|=DW_FIFO_ENABLE;
    }
    else{
        result&=~DW_FIFO_ENABLE;
    }
    UART_REG_WRITE(result , (unsigned long)(reg_base + DW_UART_LCR_HR)); // 启用 FIFO
    //按位或,将FEN位置1或 0
 
    return OS_OK;

}
  • 修改标志位检查函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
S32 uart_is_txfifo_full(S32 uartno)
{
    S32 ret;
    U32 usr = 0;

    ret = uart_reg_read(uartno, DW_UART_FR, &usr);//ret表示函数调用是否成功
    if (ret) {
      return OS_OK;
    }
   
    if(enable_fifo)
    return (usr & DW_XFIFO_NOT_FULL ); //fifo:已满时返回1
    else
    return !(usr & TX_REG_EMPTY_FLAG);//单字节:已满时返回0

}
  • 重新构建系统,运行成功

作业2

采用 UniProton 提供的 libboundscheck 库实现 vsnprintf_s 函数。
从gitee上下载libboundsheck库

将src中的文件放入项目目录下的bsp中,include下的头文件放入项目目录下的include中

修改bsp中的cmakelist.txt

重新构建系统,运行工程,虽然有很多警告XD,但是能跑

ds

(1) 数据写入发送FIFO​

  • ​用户或程序​​通过向UART数据寄存器(如 UARTDR)写入数据,数据被推送到发送FIFO。

    1
    2
    3
    4
    5
    6
    // 示例:向UART发送字符串
    const char *msg = "Hello, World!\n";
    for (int i = 0; msg[i] != '\0'; i++) {
    while (UART_TX_FIFO_FULL); // 等待FIFO有空位
    UARTDR = msg[i]; // 写入发送FIFO
    }

​(2) UART硬件自动发送​

  • ​UART控制器​​从发送FIFO中按顺序取出数据,将其转换为串行信号:
    • ​添加起始位​​:逻辑低电平(0)。
    • ​数据位​​:8位(或5-9位,取决于配置)。
    • ​校验位​​(可选):奇偶校验位。
    • ​停止位​​:逻辑高电平(1),通常1-2位。
  • ​信号输出​​:通过UART的TX引脚以配置的波特率(如115200 bps)发送信号。

​(3) 接收端UART接收​

  • ​接收端UART​​(如PC的串口或嵌入式设备的UART)通过RX引脚捕获串行信号。
  • ​信号解析​​:去除起始位、停止位,校验数据完整性。
  • ​存入接收FIFO​​:解析后的数据字节和状态信息(如校验错误)被推送到接收FIFO。

​(4) 驱动程序/操作系统处理​

  • ​操作系统驱动​​(如Linux的TTY驱动)从接收FIFO中读取数据:

    1
    2
    3
    4
    5
    // 示例:Linux驱动读取UART数据
    while (!UART_RX_FIFO_EMPTY) {
    char c = UARTDR; // 从接收FIFO读取
    tty_insert_flip_char(&port->port, c, TTY_NORMAL); // 推送至TTY缓冲区
    }
  • ​TTY层​​:将数据传递给用户空间程序(如终端模拟器)。

​(5) 显示终端输出​

  • ​终端程序​​(如PuTTY、minicom或嵌入式系统的LCD驱动)将数据渲染到屏幕:
    • ​字符模式​​:直接显示ASCII字符。
    • ​图形模式​​:通过GUI框架(如Qt)渲染文本到显示器。