Zynq UltraScale+ MPSoCs开发笔记



前言

本文记录开发zynqUltra+MPSocs的知识点


提示:以下是本篇文章正文内容,下面案例可供参考

一、windows+sdk

二、petalinux

基本概念

启动文件
1 BOOT.BIN

这是系统最先加载的引导文件
主要组成部分:

  • FSBL (First Stage Boot Loader)
  • PMU固件(如果有)
  • FPGA比特流文件(.bit)
  • U-Boot程序

生成方法:

使用Xilinx SDK/Vitis中的bootgen工具
通过.bif文件配置组成部分

2 boot.scr

U-Boot的脚本文件,以文本形式编写后编译而成
包含U-Boot启动时要执行的命令序列
主要功能:

  • 设置环境变量
  • 配置启动参数
  • 指定加载kernel的方式

生成方法:

# 先创建boot.cmd文本文件,然后转换为boot.scr
mkimage -A arm -O linux -T script -C none -a 0 -e 0 -n "Boot Script" -d boot.cmd boot.scr

3 image.ub

petalinux-build生成,由以下三个部分组成:

  • kernel: 一般就是linux生成的elf文件
  • dtb:设备树二进制文件,和驱动有关
  • rootfs.img:编译完成的文件系统
基本命令
1.petalinux-create

petalinux-create 命令主要用于创建新的 PetaLinux 组件。主要功能包括:

主要用途:

  • 创建新项目
  • 添加应用程序
  • 创建 BSP 包

最常见的工作流程:

  • 使用 --type project 创建新项目
  • 使用 --template 指定目标架构
  • 根据需要使用 --type apps 添加应用程序
# 创建新的 PetaLinux 项目
petalinux-create --type project --name <项目名称> --template <模板类型>

# 可用的模板类型:
# - zynq          : 用于 Zynq-7000 SoC
# - zynqMP        : 用于 Zynq UltraScale+ MPSoC
# - microblaze    : 用于 MicroBlaze 处理器
# - versal        : 用于 Versal ACAP

# 示例: 创建一个 Zynq 项目
petalinux-create --type project --name my_zynq_project --template zynq

# 创建新的应用程序
petalinux-create --type apps --name <应用名称> --enable
# 示例:
petalinux-create --type apps --name my_application --enable

# 从现有项目创建 BSP
petalinux-create --type bsp --project <项目路径> --name <bsp名称> --hwsource <硬件源文件>

# 选项说明:
# --type          : 要创建的组件类型 (project/apps/bsp)
# --template      : 目标架构模板
# --name          : 组件名称
# --enable        : 在 rootfs 中启用应用程序(用于应用程序)
# --project       : 现有项目的路径(用于创建 BSP)
# --hwsource      : 硬件描述文件的路径
# --out          : 输出目录(可选)
# --force        : 强制创建(即使目录已存在)

# 注意: 创建项目前请确保:
# 1. PetaLinux 工具已安装
# 2. PetaLinux 环境已正确配置
# 3. 所需的硬件描述文件(.xsa/.hdf)可用


//从模板创建工程
petalinux-create -t project -n petalinux --template zynqMP
//工程打包到bsp
petalinux-package --bsp -p ./petalinux/ --output chapter1.bsp
//从bsp创建工程
petalinux-create -t project -n petalinux_bsp -s ./chapter1.bsp
2.petalinux-package

petalinux-package 命令主要用于打包 PetaLinux 项目的各种组件。主要功能包括:

基本功能:

  • 创建启动镜像(BOOT.BIN)
  • 生成 BSP 文件
  • 打包预编译镜像
  • 创建 SDK
  • 生成 WIC 镜像

最常见的使用场景:

  • 生成可引导的 SD 卡镜像
  • 创建完整的系统镜像
  • 打包项目为 BSP 以便分享
  • 准备开发环境的 SDK

使用流程:

先完成项目构建(petalinux-build)
根据需要选择打包选项
执行打包命令
验证生成的文件

# 打包启动镜像(BOOT.BIN)
petalinux-package --boot --fsbl <fsbl镜像> --fpga <bit文件> --u-boot --force
# 示例:
petalinux-package --boot --fsbl images/linux/zynq_fsbl.elf --fpga images/linux/system.bit --u-boot --force

# 创建 BSP 文件
petalinux-package --bsp --project <project路径> --output <输出文件名>.bsp
# 示例:
petalinux-package --bsp --project /path/to/project --output my_custom.bsp

# 打包预编译镜像
petalinux-package --prebuilt --fpga <bit文件> --u-boot --kernel --boot

# 创建 SDK
petalinux-package --sysroot

# 创建 WIC 镜像
petalinux-package --wic

# 主要选项说明:
# 启动镜像(--boot)相关选项:
# --fsbl          : 指定 FSBL (First Stage Boot Loader) 文件
# --fpga          : 指定比特流文件 (.bit)
# --u-boot        : 包含 U-Boot
# --pmufw         : 包含 PMU 固件 (仅 ZynqMP)
# --atf           : 包含 ARM Trusted Firmware (仅 ZynqMP)
# --force         : 强制覆盖已存在的文件

# BSP(--bsp)相关选项:
# --project       : 项目路径
# --output        : 输出的 BSP 文件名
# --hwsource      : 硬件平台规格文件路径

# 预编译(--prebuilt)相关选项:
# --fpga          : 包含比特流
# --u-boot        : 包含 U-Boot
# --kernel        : 包含内核
# --boot          : 生成启动镜像

# 注意事项:
# 1. 打包前确保已完成构建(petalinux-build)
# 2. BOOT.BIN 生成需要正确的 FSBL 和比特流文件
# 3. ZynqMP 设备可能需要额外的启动组件(ATF, PMUFW)
# 4. WIC 镜像需要正确配置存储分区

# 常见错误处理:
# - 如果出现文件缺失错误,检查构建是否完成
# - 权限错误时使用 sudo 或检查文件权限
# - 空间不足时清理项目目录或扩展存储空间
3.petalinux-config
4.petalinux-build

如果不是第一次编译设备树,即使修改了设备树执行petalinux-build -c device-dree也不会生成dtb文件,这时应先执行petalinux-build -c device-tree -x cleansstate 清理编译状态后再编译设备树

1、将 system-user.dtsi复制到petalinux_project\components\plnx_workspace\device-tree\ 目录下(如果将system-user.dtsi放在原来目录,则修改无效,原因未知)

2、执行petalinux-build -c device-tree -x cleansstate清理设备树编译状态

3、执行petalinux-build -c device-dree编译设备树

一些常用命令

petalinux-build -x clean   //清除缓冲

petalinux-build -x distclean //distclean 会删除更彻底的缓存,包括编译器和构建工具的缓存
配套工具安装
1.gdbserver

基本环境

参数1参数2
target boardzynqUltra+Socs,4核a53
gdbgdb8.2版本
gccgcc9.2版本

编译过程

//1.gdb 配置
tar -zxvf gdb-8.2.tar.gz
cd  gdb-8.2
mkdir _install
./configure \
  --build=x86_64-linux-gnu \
  --host=x86_64-linux-gnu \
  --target=aarch64-linux-gnu \
  --prefix=/home/alinx/work/tools/gdb-8.2/_install \
  CC=gcc \
  CXX=g++ \
  --with-python=no \
  --without-guile
make && make install

//2.gdbserver 配置
cd gdb-7.11.1 /gdb/gdbserver
./configure --host=aarch64-linux-gnu
make

3.将 gdb/gdbserver/下的gdbserver执行文件拷贝到根文件系统中的bin目录
同时记得将aarch64-linux-gnu-编译工具下的/lib&/usr/lib拷贝至根文件系统
(此处应为构建根文件系统就搭好)
2.安装过程问题

问题1

aarch64-xilinx-linux-ld.real: ../gdbsupport/libgdbsupport.a(agent.o): Relocations in generic ELF (EM: 62)

解决方案:
此版本下载的gdb10.1,petalinux版本gcc9.2,后将版本降为gdb8.2

问题2 configure指定参数问题

./configure --target=aarch64-linux-gnu --prefix=/home/alinx/work/tools/gdb-8.2/_install
//提示配置异常

解决方案:
增加参数,指定build、host参数

./configure \
  --build=x86_64-linux-gnu \
  --host=x86_64-linux-gnu \
  --target=aarch64-linux-gnu \
  --prefix=/home/alinx/work/tools/gdb-8.2/_install \
  CC=gcc \
  CXX=g++ \
  --with-python=no \
  --without-guile
进程间通信
1.有名管道
2.共享内存

共享内存(Shared Memory)是进程间通信(IPC)的一种方式,允许多个进程访问同一块内存区域。它通过在内存中创建一个共享的区域,使得数据可以直接通过读写内存来交换,而不需要进行拷贝操作。这种方式效率很高,适合需要频繁和大量数据交换的场景。
共享内存的关键特性

  • 高效性:

    共享内存是所有 IPC 机制中效率最高的,因为它直接使用内存,不需要内核参与数据的搬运

  • 持久性:

    共享内存段在显式销毁之前一直存在,即使没有进程使用它,直到调用 shmctl 删除为止。

  • 同步问题:

    共享内存本身提供同步机制。如果多个进程同时读写,需要额外使用信号量或其他同步机制避免冲突。

  • 实现方式

    • System V 共享内存(shmget)

      主要特点:

      • 使用 key 标识共享内存段
      • 需要显式管理共享内存的创建和删除
      • 系统范围内可见
      • 有最大尺寸限制
      • 权限控制相对简单
    • POSIX 共享内存(shm_open) 建议使用
      主要特点:

      • 使用文件系统路径命名
      • 可以像文件一样操作
      • 支持更细粒度的权限控制
      • 接口更现代,与其他 POSIX API 一致
      • 可以使用 mmap 的相关特性
// 创建共享内存对象
int shm_open(const char *name, int oflag, mode_t mode);
// 设置大小
int ftruncate(int fd, off_t length);
// 映射到进程地址空间
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
// 解除映射
int munmap(void *addr, size_t length);
// 关闭和删除
int close(int fd);
int shm_unlink(const char *name);
3.消息队列
4.信号量
5.socket&文件
中断机制
1.上半部

上半部(Top Half):

是中断处理的第一阶段,直接响应硬件中断
运行在中断上下文中,不可被中断
执行时间必须很短
(ps:感觉是为了快速处理+不漏中断,设计出上半部和下半部的概念)
主要完成以下工作:

  • 保存中断现场
  • 标记中断已处理
  • 获取必要的硬件状态
  • 唤醒下半部执行
  • 恢复中断现场

实现方式:

// 编程接口,硬中断
// 注册中断处理函数
request_irq(irq_num, handler, flags, name, dev);

// 中断处理函数原型
irqreturn_t interrupt_handler(int irq, void *dev_id)
2.下半部

下半部(Bottom Half):

是中断处理的第二阶段,可以被其他中断打断
在进程上下文中运行
可以执行较长时间的任务
主要完成以下工作:

  • 处理上半部保存的数据
  • 执行耗时的数据处理
  • 触发其他内核机制

举个例子:
当网卡收到数据包时:

  • 上半部快速保存数据包到缓冲区
  • 下半部再进行协议解析等耗时操作

这种分层设计的优点是:

  • 提高系统响应性 - 上半部快速执行完毕
  • 降低中断延迟 - 耗时操作放在可中断的下半部
  • 更好的并发处理 - 下半部可以被调度

实现方式:
主要有三种机制:

  • 软中断(softirq):

    最底层的下半部机制
    优先级固定
    多个CPU可以同时执行

// 定义软中断
struct softirq_action {
    void (*action)(struct softirq_action *);
};

// 注册软中断
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
  • tasklet

    基于软中断实现
    动态创建
    同一类型的tasklet串行执行

// 定义tasklet
DECLARE_TASKLET(name, func, data);

// 调度tasklet执行
tasklet_schedule(&my_tasklet);
  • 工作队列(workqueue):

    运行在进程上下文
    可以睡眠
    适合耗时较长的任务

// 创建工作
DECLARE_WORK(name, func);

// 调度工作执行
schedule_work(&my_work);
应用函数
1. mmap
// 设备文件,代表整个物理内存
int fd = open("/dev/mem", O_RDWR);

// 通过mmap将物理内存映射到用户空间,
void *virt_addr = mmap(NULL,                  // 建议的映射地址,NULL让系统自动选择
                      length,                  // 映射长度
                      PROT_READ | PROT_WRITE, // 读写权限
                      MAP_SHARED,             // 共享映射
                      fd,                     // /dev/mem的文件描述符
                      phys_addr);             // 物理地址
                      
物理地址(fd + phys_addr)映射到虚拟地址
2.__sync_synchronize

__sync_synchronize 是 GCC 提供的一个内置函数,用于实现内存屏障(memory barrier)。它是一个 全局的内存同步操作,确保当前线程对共享内存的所有读写操作在屏障前已经完成,并且这些操作的结果对其他线程是可见的。

  • 防止编译器优化:

    编译器可能会对代码重新排序,以优化性能,但这可能打乱多线程环境下的预期行为。__sync_synchronize 可以禁止编译器重新排序操作。

  • 防止 CPU 指令乱序:

    现代 CPU 为了提高性能,会对指令进行乱序执行(out-of-order execution)。__sync_synchronize 确保内存操作的顺序严格按照代码逻辑顺序执行。

  • 多线程同步:

    在多线程编程中,确保不同线程之间对共享变量的访问是按照某种特定的顺序执行。

开发过中遇到的问题

1. nfs 挂载失败问题

命令
mount -t nfs 192.168.0.110:/home/alinx/work /mnt/nfsdir
日志

//LOG
mount -t nfs 192.168.0.110:/home/alinx/work /mnt/nfsdir
mount: /mnt/nfsdir: bad option; for several filesystems (e.g. nfs, cifs) you might need a /sbin/mount.<type> helper program.

解决方案
mount -t nfs 192.168.0.110:/home/alinx/work /mnt/nfsdir -o rw,nolock,addr=192.168.0.110
在原命令基础上增加参数 -o rw,nolock,addr=192.168.0.110

2.启动pl_dma导致类似无法处理虚拟地址问题

日志

`Unable to handle kernel paging request at virtual address ffffff8000f91000 [ 656.654134]`

解决方案
在设备树中写入了对plmemory地址的管理(0x8000 0000 ~0xc000 0000)
取消掉就正常了

3.考虑reserved-memory数据一致性问题

情景

设备树代码
memory {
        device_type = "memory";
        reg = <0x00 0x00 0x00 0x60000000>;
    };

    reserved-memory {
        #address-cells = <0x02>;
        #size-cells = <0x02>;
        ranges;

        reserved {
            reg = <0x00 0x60000000 0x00 0x1f000000>;
            no-map;
        };
    };
    
硬件架构
CPU <--> Cache <--> DDR
DMA <-----------> DDR
- DMA控制器直接通过系统总线访问DDR
- DMA没有cache层,不能访问CPU的cache
- 这就是为什么需要cache一致性维护的根本原因

解决方案
方案1:使用内存屏障

// CPU写入后确保数据写回内存
wmb();  // 写内存屏障
dsb();  // ARM架构的数据同步屏障

// CPU读取前确保读到最新数据
rmb();  // 读内存屏障

方案2:手动管理cache

CPU写->DMA读的场景
// CPU写入后刷新cache
//调用dma_sync_single_for_device: Cache中的脏数据写回DDR
//DMA读数据: 直接从DDR读取(此时DDR数据是最新的)
dma_sync_single_for_device(dev, addr, size, DMA_TO_DEVICE);
DMA写数据: DMA -> DDR(写入新数据)
//调用dma_sync_single_for_cpu: 使Cache中对应的行失效
//CPU读数据: 因为Cache已失效,会从DDR重新加载最新数据到Cache
// DMA操作完成后使cache失效
dma_sync_single_for_cpu(dev, addr, size, DMA_FROM_DEVICE);

具体代码

void dma_sync_single_for_device(struct device *dev, dma_addr_t addr,
                               size_t size, enum dma_data_direction dir)
{
    // 对于ARM架构,最终会调用到:
    __dma_map_area(phys_to_virt(addr), size, DMA_TO_DEVICE);
}

static inline void __dma_map_area(const void *addr, size_t size,
                                 enum dma_data_direction dir)
{
    if (dir == DMA_TO_DEVICE) {
        // 1. Clean - 将cache中的脏数据写回内存
        dmac_clean_range(addr, addr + size);
        // 或者使用
        __clean_dcache_area(addr, size);
        
        // 2. 确保清理完成
        dsb(ishst);
    }
}

void dma_sync_single_for_cpu(struct device *dev, dma_addr_t addr,
                            size_t size, enum dma_data_direction dir)
{
    // 对于ARM架构,最终会调用到:
    __dma_map_area(phys_to_virt(addr), size, DMA_FROM_DEVICE);
}

static inline void __dma_map_area(const void *addr, size_t size,
                                 enum dma_data_direction dir)
{
    if (dir == DMA_FROM_DEVICE) {
        // 1. Invalidate - 使cache line失效
        dmac_inv_range(addr, addr + size);
        // 或者使用
        __inval_dcache_area(addr, size);
        
        // 2. 确保失效操作完成
        dsb(ishst);
    }
}

// Clean操作的底层实现(ARM64为例)
void __clean_dcache_area(void *addr, size_t size)
{
    uint64_t line, end;
    
    // 计算需要clean的cache line范围
    line = (uint64_t)addr & ~(CACHE_LINE_SIZE - 1);
    end = (uint64_t)(addr + size - 1) & ~(CACHE_LINE_SIZE - 1);
    
    // 逐个clean cache line
    while (line <= end) {
        // 使用dc cvac指令clean cache line到point of coherency
        asm volatile("dc cvac, %0" : : "r" (line));
        line += CACHE_LINE_SIZE;
    }
}

// Invalidate操作的底层实现
void __inval_dcache_area(void *addr, size_t size)
{
    uint64_t line, end;
    
    // 计算需要invalidate的cache line范围
    line = (uint64_t)addr & ~(CACHE_LINE_SIZE - 1);
    end = (uint64_t)(addr + size - 1) & ~(CACHE_LINE_SIZE - 1);
    
    // 逐个invalidate cache line
    while (line <= end) {
        // 使用dc ivac指令invalidate cache line
        asm volatile("dc ivac, %0" : : "r" (line));
        line += CACHE_LINE_SIZE;
    }
}

内联汇编,无效指定缓存行(cache line)


DC IVAC 是一条 数据缓存失效 指令,它通过指定一个内存地址,来使该地址所在的缓存行失效。
缓存行(cache line)通常是 64 字节,包含多个连续的内存地址。

#include <stdio.h>

int main() {
    int x = 42;  // 定义一个变量
    uintptr_t address = (uintptr_t)&x;  // 获取变量地址

    // ARM 汇编中使用 DC IVAC
    __asm__ volatile(
        "MOV x0, %0\n"    // 将 address 加载到寄存器 x0
        "DC IVAC, x0\n"   // 使缓存行失效
        :
        : "r"(address)    // 将变量地址传递给汇编
        : "x0"            // 声明汇编会使用 x0
    );

    return 0;
}

方案3: 将该区域设置为non-cacheable
可以通过修改设备树添加 “memory-region-no-cache” 属性,或通过MMU页表设置来实现

4.编译器优化掉预留区域变量

情景
定义了全局变量,并设置在指定section,预留给DMA或者其他IP使用,被编译器优化掉
解决方案
在link.lds使用KEEP关键词

SECTIONS {
    .my_section : {
        KEEP(*(.my_section))  /* 强制保留 .my_section 段 */
    } >RAM
}

int my_variable __attribute__((section(".my_section"))) = 42;

5.串口设备ttyPS1乱码

情景:
在windows使用串口调试助手,接收串口数据,出现乱码
解决方案:

  • 检查波特率
//查看设备信息命令
stty -F /dev/ttyPS1	
//日志输出如下
stty -F /dev/ttyPS1
speed 9600 baud; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>;
start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O; min = 1; time = 0;
-brkint -imaxbel

  • 检查物理连线
6.linux系统初始化完成后再执行脚本

情景:
想在系统完全初始化后,再执行脚本,包括应用函数、IP配置等

解决方案:
/etc/ini.d/rc中末尾增加命令,如 source /home/root/user.h
/etc/ini.d/rcS末尾添加的命令的话,系统还未初始化完全

7.linux下串口设备号

在 Linux 中,串口一般在操作系统的/dev/,并以 tty* 开头。 常见名称有:

  • /dev/ttyACM0 表示 USB 总线上的 ACM 调制解调器。Arduino UNO 一般是这个名字。
  • /dev/ttyPS0 使用 Yocto 移植 Linux 版本的 Xilinx Zynq FPGA 一般使用此名称作为 Getty 连接的默认串行端口。
  • /dev/ttyS0标准 COM 端口将使用此名称。如今,这些端口已不常见。
  • /dev/ttyUSB0大多数 USB 转串行线将使用类似这样的文件显示。
  • /dev/pts/0伪终端。
8.常量定义
无后缀int(通常为 32 位)
Uunsigned int(无符号 32 位)
Llong(通常为 32 位或 64 位,视平台而定)
ULunsigned long(无符号长整型)
LLlong long(通常为 64 位)
ULLunsigned long long(无符号 64 位)
9.丢弃串口设备缓冲区数据

情景:
使用FILE* fopen打开的文件,而不是open
而函数 int tcflush(int fd, int queue_selector) 使用的是int型的文件描述符
解决方案:
使用int fd = fileno(fp)从文件FILE *fp中获取fd

#include <stdio.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>

int main() {
    const char *serial_port = "/dev/ttyS0"; // 替换为实际串口设备路径

    // 使用 fopen 打开串口设备
    FILE *fp = fopen(serial_port, "r+");
    if (!fp) {
        perror("Failed to open serial port");
        return 1;
    }

    // 从 FILE * 中获取文件描述符
    int fd = fileno(fp);
    if (fd < 0) {
        perror("Failed to get file descriptor");
        fclose(fp);
        return 1;
    }

    // 清空输入缓冲区
    if (tcflush(fd, TCIFLUSH) < 0) {
        perror("Failed to flush input buffer");
    } else {
        printf("Input buffer flushed successfully.\n");
    }

    // 清空输出缓冲区
    if (tcflush(fd, TCOFLUSH) < 0) {
        perror("Failed to flush output buffer");
    } else {
        printf("Output buffer flushed successfully.\n");
    }

    // 同时清空输入和输出缓冲区
    if (tcflush(fd, TCIOFLUSH) < 0) {
        perror("Failed to flush both input and output buffers");
    } else {
        printf("Both input and output buffers flushed successfully.\n");
    }

    fclose(fp);
    return 0;
}

10.判断文件类型
S_ISLNK(st_mode):是否是一个连接.
S_ISREG(st_mode) :是否是一个常规文件.
S_ISDIR(st_mode) :是否是一个目录
S_ISCHR(st_mode):是否是一个字符设备.
S_ISBLK(st_mode):是否是一个块设备
S_ISFIFO(st_mode):是否 是一个FIFO文件.
S_ISSOCK(st_mode):是否是一个SOCKET文件 

void list_files(const char *path) {
    struct dirent *entry;
    struct stat file_stat;
    char filepath[1024];
    T_Sample_Param sampleCfg;
    DIR *dir = opendir(path);

    if (!dir) {
        perror("opendir");
        return;
    }

    while ((entry = readdir(dir)) != NULL) {
        // 忽略 "." 和 ".." 目录
        if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
            continue;

        // 构建文件完整路径
        snprintf(filepath, sizeof(filepath), "%s/%s", path, entry->d_name);

        // 获取文件状态信息
        if (stat(filepath, &file_stat) == -1) {
            perror("stat");
            continue;
        }

        // 判断是否为文件
        if (S_ISREG(file_stat.st_mode)) {
            printf("File: %s, Size: %ld bytes\n", entry->d_name, file_stat.st_size);
        }
    }

    closedir(dir);
}
11.DSB、DMB、ISB、CACHE数据一致性

常规代码(默认数据为cacheable)

// CPU0 写数据
void write_shared_data(int new_value) {
    shared_data = new_value;     // 写入数据,到cacheable区域
    DMB();                       // 确保数据写入完成
    data_ready = 1;             // 设置标志
    DSB();                      // 确保标志写入完成并对其他核心可见
}

// CPU1 读数据
int read_shared_data(void) {
    while (data_ready != 1) {
        DMB();                  // 确保每次都读取最新的标志值
    }
    ISB();                      // 刷新流水线
    DMB();                      // 确保读取最新数据
    return shared_data;
}

代码补充说明:
让我详细解释 CPU0 写入数据后的状态和 CPU1 观察数据的过程:
在 CPU0 执行完这段代码后:

shared_data = new_value; //假设shared_datacacheable内存区域

  • 数据首先写入 CPU0L1 cache(写策略是Write-Back则写入cache,写策略是Write-Through则同步写入RAMcache)
  • 缓存一致性协议会将这个 cache line标记为 Modified 状态

DMB():

  • 确保 data 的写入动作在CPU0中完成(根据写策略决定数据写入位置)

  • 其他 CPU 此时可能还看不到更新

flag = 1;

  • 同步标志也是先写入 CPU0 的 cache
  • 缓存一致性协议会将这个 cache line 标记为 Modified

DSB();

  • 确保所有缓存操作完成
  • 数据依然可能只存在于 cache中,不一定写回 DDR

当 CPU1 要读取数据时:

  • 通过缓存一致性协议(如 MESI),CPU1 会直接从 CPU0cache 中获取数据
  • 不需要等待数据写回 DDR(写策略Write-Back时)
  • 这个过程是由硬件自动完成的

简单来说

数据更新后主要存在于cache
写回 DDR 的时机由缓存策略决定(如 write-backwrite-through
CPU1 读取时是通过缓存一致性协议直接从其他CPU cache 获取,而不是从 DDR 读取
DMB/DSB 确保内存操作顺序,但不会强制写回 DDR

特性DMB(Data Memory Barrier)DSB(Data Synchronation Barrier)ISB(Instruction Synchronation Barrier)
类型数据内存屏障数据同步屏障指令同步屏障
主要用途确保内存访问顺序比DMB严格,确保DSB之前的所有指令(不仅是存储器访问,还包括cache维护、TLB维护等)都完成确保指令流水线同步
影响范围数据内存访问所有指令和内存访问仅指令流水线
典型场景多核内存同步缓存失效、TLB 刷新修改处理器状态寄存器

应用建议:

  • DMB:用于数据访问的内存同步,适合在共享内存或多线程环境下。
  • DSB:用于确保所有系统级操作完成,常见于系统初始化和设备交互。
  • ISB:用于确保指令序列重新获取,适合状态切换或上下文切换的场景。

补充2 write-back或 write-through

Write-through(写通策略):

  • 工作原理

    • 当CPU写数据时,同时写入cache和主存(DDR)
    • 每次写操作都会立即更新主存
    • cache和主存的数据始终保持一致
  • 优点:

    • 数据一致性好,主存总是包含最新数据
    • 系统掉电时数据不会丢失
    • 缓存一致性协议实现相对简单
  • 缺点:

    • 写操作延迟较高(需要等待写入主存)
    • 占用更多内存带宽
    • 功耗较高(频繁访问主存)

Write-back(写回策略):

  • 工作原理
    CPU写数据时只写入cache
    将cache line标记为"脏"(dirty)状态

    只在必要时才写回主存:

    • cache line被替换时
    • 其他CPU请求该数据时
    • 显式的cache flush操作
    • 同步指令(如DSB)要求时
  • 优点:

    • 写操作延迟低(不用等待主存)
    • 减少内存带宽使用
    • 多次写入同一地址只需最后写回一次
    • 功耗较低
  • 缺点:

    • 掉电可能丢失数据(dirty数据未写回)
    • cache一致性协议实现较复杂
    • 需要额外的dirty标志位

补充3 DSB ISB区分

DSB 和 ISB 关注的"数据"是不同的:

DSB 关注的"数据":

内存中的数据(Memory data)
缓存中的数据(Cache data)
外设寄存器的数据(Peripheral register data)
这些数据的特点是:它们是程序操作的对象,是存储在内存系统中的内容

ISB 关注的"系统状态":

系统寄存器的值(比如 SCTLR_EL1, TCR_EL1 等)
MMU 配置
特权级别
这些更像是处理器的"配置"或"状态",决定了处理器如何执行指令

场景1:修改内存数据

STR  X1, [X0]    // 写入内存数据
DSB  ISH         // 需要DSB确保数据写入完成

场景2:修改系统配置

MSR  SCTLR_EL1, X0  // 修改系统控制寄存器
ISB                 // 需要ISB确保新的系统配置生效

四、链接脚本

https://sourceware.org/binutils/docs/ld/Scripts.html#Scripts

相关参考资料:
https://www.jianshu.com/p/1782e14a0766

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值