为什么我的两个 Cortex-A9 核心没有缓存一致性?
摘要
一篇个人博客文章,详细描述了作者在 Terasic DE0-Nano-SOC 开发板上遇到的缓存一致性问题,该开发板搭载 Cortex-A9 处理器,包括硬件设置和调试步骤。
<p><a href="https://lobste.rs/s/vikit1/why_aren_t_my_two_cortex_a9_cores_cache">评论</a></p>
查看缓存全文
缓存时间: 2026/08/23 05:15
# JP的网站来源:https://thejpster.org.uk/blog/blog-2026-08-22/
### JP的网站(https://thejpster.org.uk/)
---
## 为什么我的两个Cortex-A9核心没有缓存一致性?
发布于**2026-08-22**
## 目录
- [硬件](https://thejpster.org.uk/blog/blog-2026-08-22/#the-hardware)
- [加载代码](https://thejpster.org.uk/blog/blog-2026-08-22/#loading-code)
- [内存管理单元](https://thejpster.org.uk/blog/blog-2026-08-22/#the-memory-management-unit)
- [内存种类](https://thejpster.org.uk/blog/blog-2026-08-22/#kinds-of-memory)
- [实际问题](https://thejpster.org.uk/blog/blog-2026-08-22/#the-actual-issue)
空闲时我最喜欢做的事情之一,就是从一堆随机开发板中随手拿一块,尝试为它编写一些能运行的代码。我通常尽量避免使用现有的SDK或安装新工具。我只是想看看最少需要编写多少代码就能让板子运行起来并产生可见的输出。如果进展顺利,我可能会大干一场,开始为各种外设编写一些小驱动程序。如果真的[失控了](https://thejpster.org.uk/blog/blog-2018-02-20/),我最终可能会写完一个全新的操作系统,带有VGA输出和SD卡支持。但通常我满足于在UART上显示`Hello, World`或让LED闪烁。
过去几周我休了几天假,为了换换口味,不想再[摆弄ARM7](https://git.thejpster.org.uk/thejpster/mcb2130-rs)了,我拿起了我的[Terasic DE0-Nano-SOC](https://soc.terasic.com/)。这块板子我买了大约7年,但一直没怎么用过。它和MiSTER复古模拟项目[使用的DE10 Nano板](https://misterfpga.org/)类似,但并不兼容。
## 硬件
Terasic DE0-Nano-SOC是我目前所知约2015年发布的一款小型开发板。它配备:
- 一个Altera Cyclone-V片上系统
- 1 GiB DDR3 SDRAM
- 一个RJ45以太网端口
- 一个5V电源输入(圆孔插座)
- 一个mini-USB编程端口
- 一个micro-USB端口,连接板载UART转USB串行转换器
- 一个SD卡插槽
- Arduino Uno风格的排针
可惜的是,它没有主动散热器,因为运行时它会*变得非常热*。热到碰都碰不了。我接了一个备用的80mm风扇,这似乎足以让它保持凉爽。
Altera Cyclone-V SoC是标准Arm SoC(*硬处理器系统*)和Altera FPGA(含40K逻辑单元)的组合。我特别感兴趣的是其中的两个Arm Cortex-A9处理器。SoC内置Boot ROM,可以从SD卡的特定扇区加载*预加载器*。该*预加载器*从片上RAM(OCRAM)运行,初始化外部DDR3 SDRAM,然后从SD卡加载完整的U-Boot副本到SDRAM中,并从那里执行它。我不想折腾这些,所以直接从https://soc.terasic.com/下载了磁盘镜像,并将其写入了一张备用卡。一切似乎都启动正常。默认情况下,它启动到一个相当旧的Linux内核(根分区存储在SD卡上),但我删除了内核,导致它停在U-Boot提示符处。
板载USB转串行适配器意味着我只需将PC连接到micro-USB端口,并在`/dev/tty.usbserial-`上使用`minicom`与板子交互。*预加载器*打印如下信息:
```
U-Boot SPL 2013.01.01 (Dec 29 2014 - 15:29:15)
BOARD : Terasic DE0_Nano_SoC Version-A Board
################################################################################ ##################################= =# ########=-=#######################= -- =# ######- =#######################= ####- =###### =# ######- ###############- -=##=##= ####- ######## =# ####= =##- -### ##= -####### ###### ####- #####- - =# #### ## ### ## -##= -=--=###= ####==# ####- #### =# ######- ###- ##- -### =####= =====###= ####- #### #### =# ######- ### =##### #####= ####-=###= -=###- ####- ##### =# ######- -#= - =## #####= #### -###= #=-=#### ####- ########- ## ####### =##= ## #####= -########= ####### ####- =###### =# #########==######====####==#######= - --- -- ---- =# ##################################= =# ################################################################################ BOARD : Terasic DE0_Nano_SoC Version-A Board
CLOCK: EOSC1 clock 25000 KHz
CLOCK: EOSC2 clock 25000 KHz
CLOCK: F2S_SDR_REF clock 0 KHz
CLOCK: F2S_PER_REF clock 0 KHz
CLOCK: MPU clock 925 MHz
CLOCK: DDR clock 400 MHz
CLOCK: UART clock 100000 KHz
CLOCK: MMC clock 50000 KHz
CLOCK: QSPI clock 3613 KHz
SDRAM: Initializing MMR registers
SDRAM: Calibrating PHY
SEQ.C: Preparing to start memory calibration
SEQ.C: CALIBRATION PASSED
SDRAM: 1024 MiB
ALTERA DWMMC: 0
```
它只是一个运行在少量片上RAM(OCRAM)上的U-Boot小副本。完整的U-Boot则打印如下信息:
```
U-Boot 2013.01.01 (Dec 30 2014 - 12:07:34)
CPU : Altera SOCFPGA Platform
BOARD : Terasic DE0_Nano_SoC Version-A Board
################################################################################ ##################################= =# ########=-=#######################= -- =# ######- =#######################= ####- =###### =# ######- ###############- -=##=##= ####- ######## =# ####= =##- -### ##= -####### ###### ####- #####- - =# #### ## ### ## -##= -=--=###= ####==# ####- #### =# ######- ###- ##- -### =####= =====###= ####- #### #### =# ######- ### =##### #####= ####-=###= -=###- ####- ##### =# ######- -#= - =## #####= #### -###= #=-=#### ####- ########- ## ####### =##= ## #####= -########= ####### ####- =###### =# #########==######====####==#######= - --- -- ---- =# ##################################= =# ################################################################################ BOARD : Terasic DE0_Nano_SoC Version-A Board
I2C: ready
DRAM: 1 GiB
MMC: ALTERA DWMMC: 0
In: serial
Out: serial
Err: serial
Skipped ethaddr assignment due to invalid EMAC address in EEPROM
Net: mii0
Warning: failed to set MAC address
Hit any key to stop autoboot: 0
SOCFPGA_CYCLONE5 #
```
## 加载代码
我已经写了足够多的[AArch32](https://github.com/rust-embedded/aarch32/tree/main/examples) [Rust](https://github.com/ferrous-systems/rust-training/tree/main/example-code/qemu-aarch32v8r)[示例](https://github.com/ferrous-systems/s32z2-rust-demo)[代码](https://github.com/ferrous-systems/rcar-v4h-experiments)(此处多次引用不同示例),所以再写一个相对容易。
- 从一个空项目开始
- 引入`aarch32-rt`(https://crates.io/crates/aarch32-rt)
- 编写一个`memory.x`链接脚本片段,说明内存位置
- 编写一个非常基础的16550 UART驱动,并将其指向UART0的基地址,假设U-Boot已启用它并配置了合适的波特率
链接脚本片段如下:
```
MEMORY {
RAM : ORIGIN = 0x00100000, LENGTH = 1M
}
REGION_ALIAS("VECTORS", RAM);
REGION_ALIAS("CODE", RAM);
REGION_ALIAS("DATA", RAM);
REGION_ALIAS("STACKS", RAM);
PROVIDE(_vector_start = ORIGIN(VECTORS));
PROVIDE(_hyp_stack_size = 16K);
PROVIDE(_und_stack_size = 16K);
PROVIDE(_svc_stack_size = 16K);
PROVIDE(_abt_stack_size = 16K);
PROVIDE(_irq_stack_size = 64);
PROVIDE(_fiq_stack_size = 64);
PROVIDE(_sys_stack_size = 16K);
```
我们有*大量*内存可用,但1 MiB对我们所需已绰绰有余。重要的是起始地址不是`0x0`。相反,我选择将起始地址设为`0x0010_0000`,跳过前1 MiB。这是为了避免Boot ROM(可以在地址`0x0`处映射或取消映射),以及避免U-Boot使用的任何RAM。
我们简陋的UART驱动程序简单如下:
```
/// 这与U-Boot在DE0-Nano-SOC上使用的控制台相同
pub static CONSOLE: Console = Console::new();
/// 表示我们的标准输出控制台(在UART0上)
pub struct Console {
_inner: (),
}
impl Console {
const UART0_BASE_THR: *mut u32 = 0xFFC0_2000 as *mut u32;
const UART0_BASE_LSR: *mut u32 = 0xFFC0_2014 as *mut u32;
const LSR_TX_EMPTY: u32 = 1 << 6;
const fn new() -> Console {
Console { _inner: () }
}
/// 等待UART空闲
fn waitbusy(&self) {
loop {
let lsr = unsafe { Self::UART0_BASE_LSR.read_volatile() };
if (lsr & Self::LSR_TX_EMPTY) != 0 {
break;
}
}
}
/// 向UART写入一个字节
fn putc(&self, byte: u8) {
// 安全性:这是我们的UART,且缓冲区溢出不是未定义行为(UB)
unsafe {
Self::UART0_BASE_THR.write_volatile(byte as u32);
}
}
}
impl core::fmt::Write for &Console {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
for b in s.as_bytes() {
self.waitbusy();
if cfg!(feature = "console-crlf") {
if *b == b'\n' {
self.putc(b'\r');
self.waitbusy();
}
}
self.putc(*b);
}
Ok(())
}
}
```
这基本上是从我早前在*Pandaboard*(我提过这不是我第一个Arm开发板吗?)上的一个项目中抄来的,只是改了基地址。有一堆库crate可以引入以实现更好的驱动,但我乐于复制粘贴这几行代码,因为它放在代码树里与其他代码一起更方便修改。
主函数很简单:
```
#![no_std]
#![no_main]
use core::fmt::Write;
use hello_de0_nano_soc::CONSOLE;
#[aarch32_rt::entry]
fn main() -> ! {
_ = writeln!(&CONSOLE, "Hello, this is a DE0-Nano-SOC!");
panic!("I am a sample panic!");
}
```
现在,要将代码加载到板子上,我们需要U-Boot喜欢的文件格式。我不认为你可以直接给它ELF文件(遗憾,我写了一个很棒的[裸机ELF解析器](https://crates.io/crates/neotron-loader),所以我知道这并不难),但你可以给它[Motorola S-Record](https://en.wikipedia.org/wiki/Motorola_S-record)文件。我知道,多古雅啊。幸运的是,LLVM的binutils可以做到,我喜欢通过`cargo`的`cargo-binutils`(https://crates.io/crates/cargo-binutils)插件来驱动它:
```
cargo objcopy --release -- -O srec
```
烦人的是,这会用十六进制文件覆盖ELF文件——如果你知道如何让`cargo-binutils`的`objcopy`子命令不这样做,请告诉我。不过没关系,反正我也不需要ELF文件。
我们通过运行`loads`命令让U-Boot加载文件,并通过我的Mac上的串行终端以ASCII方式发送文件。出于我自己也不完全理解的原因,我选择了使用`minicom`,当我弄清楚“Esc”然后“S”打开发送菜单,“双击空格”在`minicom`内的文件浏览器中进入目录后,我们就出发了。
## 内存管理单元
程序运行正常,但我们没有:
- 第二个核心运行
- L1指令缓存启用
- L1数据缓存启用
- L2缓存启用
- MMU启用
MMU是真正重要的,因为没有它,处理器会将所有内存视为强顺序类型,且未对齐的加载或原子访问在这种内存上无法工作。我认为从技术上讲,在那种状态下执行Rust代码是*未定义行为*,但不管了,让我们先让MMU运行起来。
为此,我们需要一个包含4,096个一级页表条目的数组,每个条目32位长,每个代表虚拟地址空间的1 MiB部分。我们可以使用Rust的`const fn`在编译时生成它。
```
/// 持有适当对齐的一级页表
///
/// 你应创建此类型的静态变量来表示你的页表。
#[repr(C)]
#[derive(Debug)]
pub struct L1Table {
/// 我们可变的MMU表项列表
///
/// 此表由硬件读取。
pub entries: core::cell::UnsafeCell<[L1Section; NUM_L1_PAGE_TABLE_ENTRIES]>,
}
unsafe impl Sync for L1Table {}
/// 我们的MMU页表
#[unsafe(no_mangle)]
#[unsafe(link_section = ".pagetable")]
pub static MMU_L1_PAGE_TABLE: L1Table = make_mmu_table();
const DDR_ATTRS: SectionAttributes = SectionAttributes {
non_global: false,
p_bit: false,
shareable: true,
access: AccessPermissions::FullAccess,
memory_attrs: MemoryRegionAttributes::CacheableMemory {
inner: CachePolicy::WriteBackWriteAlloc,
outer: CachePolicy::NonCacheable,
}
.as_raw(),
domain: u4::new(0b0),
execute_never: false,
};
const DEVICE_ATTRS: SectionAttributes = SectionAttributes {
non_global: false,
p_bit: false,
shareable: true,
access: AccessPermissions::FullAccess,
memory_attrs: MemoryRegionAttributes::ShareableDevice.as_raw(),
domain: u4::new(0b0),
execute_never: false,
};
/// 1 MiB中的字节数
const ONE_MB: u32 = 1024 * 1024;
const fn make_mmu_table() -> L1Table {
let mut temp: [L1Section; NUM_L1_PAGE_TABLE_ENTRIES] = [L1Section::ZERO; NUM_L1_PAGE_TABLE_ENTRIES];
// 映射位于@ 0x0000_0000的1024 MiB DDR SDRAM
let mut page = 0;
while page < 1024 {
let section = L1Section::new_with_addr_and_attrs(0x0000_0000 + (page * ONE_MB), DDR_ATTRS);
temp[0x000 + (page as usize)] = section;
page += 1;
}
// 映射位于@ 0xF000_0000的256 MiB系统/MPCore外设
page = 0;
while page < 256 {
let section = L1Section::new_with_addr_and_attrs(0xF000_0000 + (page * ONE_MB), DEVICE_ATTRS);
temp[0xF00 + (page as usize)] = section;
page += 1;
}
L1Table {
entries: core::cell::UnsafeCell::new(temp),
}
}
```
最初我使用了`aarch32_cpu::mmu::L1Table`(https://docs.rs/aarch32-cpu/latest/armv7a-none-eabi/aarch32_cpu/mmu/struct.L1Table.html)类型,但它被标记为需要1 MiB边界对齐。这没问题,除了对象的大小必须是其对齐方式的倍数,所以Rust将页表从16 KiB填充到了1 MiB。当我要通过115,200波特率的UART加载它时,这就成问题了。因此,我创建了自己的没有对齐要求的类型,并将其放入特殊段以确保适当对齐。Motorola S-Record格式可以轻松跳过间隙,所以加载并没有花太长时间(大约5秒左右)。
MMU映射非常简单——从虚拟地址到物理地址的扁平1:1映射,底部1 GiB是*内部可缓存*的,顶部256 MiB是*设备内存*。等等,什么?
## 内存种类
Arm处理器理解存在不同种类的内存,这是为了性能。某些内存类型是这样的:如果代码向特定地址写入32位值,硬件需要实际执行该写入,且仅一次,既不能早于也不能晚于可能发生的任何其他对该地址(或类似地址)的写入。当地址是UART发送FIFO寄存器时,这点很重要。Arm称此为[设备内存](https://support.arm.com/documentation/ddi0406/c/Application-Level-Architecture/Application-Level-Memory-Model/Memory-types-and-attributes-and-the-memory-order-model/Device-and-Strongly-ordered-memory?lang=en),它是不可缓存且强顺序的。
如果所有RAM都被这样对待,它会扼杀你的性能。你的RAM比你的处理器慢得多得多,所以我们需要缓存(实际上有多个级别的缓存)来尽可能为处理器提供指令和数据。自1990年代初以来,桌面PC上就是这样(例如,Intel 486具有片上8 KiB的一级缓存),并且自ARM3大约同时推出以来,Arm处理器也是如此。
我们的Arm Cortex-A9处理器有两个内存接口——一个用于指令,一个用于数据(所谓的*[改进型哈佛架构](https://en.wikipedia.org/wiki/Modified_Harvard_architecture)*设计)——因此它有两个内置在处理器中的一级缓存。它们通常简称为I缓存和D缓存。我们希望处理器使用它们,所以我们告诉MMU大部分内存空间是*[正常内存](https://support.arm.com/documentation/ddi0406/c/Application-Level-Architecture/Application-Level-Memory-Model/Memory-types-and-attributes-and-the-memory-order-model/Normal-memory)*。这允许它缓存读写、缓冲写操作(以便它们可能在缓存中乱序出现或合并为单个更大的写操作),并且通常做一些让CPU运行更快的事情,但这些通常只有在地址空间连续时我们才能侥幸成功。
相似文章
NX位不仅仅关乎安全
一位开发者描述了在postmarketOS的ARM64裸机虚拟机管理程序中调试一个复杂bug的过程,涉及指令缓存一致性问题和与NX位相关的硬件特定行为。
Go语言中的L1指令缓存集冲突、关联性与代码对齐
深入探讨L1指令缓存集冲突和代码对齐如何导致Go语言中出现意外的性能回退,以及调查过程。
TrustZone 插曲:i.MX 8M 上 OP-TEE 内存隔离缺陷
文章揭示,由于缺少 TZASC 配置,NXP i.MX 8M SoC 上的 OP-TEE 内存隔离可以从普通世界被绕过,从而允许拥有内核访问权限的攻击者读取安全世界内存。修复方案已在上游 OP-TEE v4.10.0+ 中提供,需配置 CFG_TZASC_REGION0_SECURE=y,或使用下游版本 lf-6.12.49_2.2.0。
Intel 9 285K 在 ASUS Z890 上:不稳定!
关于 Intel Core Ultra 9 285K CPU 在 ASUS Z890 主板上稳定性问题的详细报告,包括崩溃和调试步骤,提醒潜在买家。
使Xen的dom0 I/O路径具备NUMA感知能力
本文详细介绍了使Xen的dom0 I/O路径具备NUMA感知能力的过程,包括内存放置和拓扑合成的修复,这是关于Xen虚拟机监控器NUMA优化系列的一部分。