mokongking

关于stm32h747双核内存分布

1. 核心专属内存 (TCM - Tightly Coupled Memory)

这类内存与核心通过专用总线连接,延迟极低(接近 0 等待周期),主要用于存放关键代码或频繁操作的数据。

  • ITCM (Instruction TCM): 64 KB。仅限 M7 核心访问,通常存放时间敏感的代码(如中断处理、PID 算法)。

  • DTCM (Data TCM): 128 KB。仅限 M7 核心访问,用于存放关键数据堆栈。

  • 注意: M4 核心无法访问 M7 的 TCM 区域

  • 2. 系统 SRAM 分布 (按域划分)

    STM32H747 的 RAM 被物理分配在不同的总线域中,总容量高达 1MB 左右。

    内存名称 容量 所在域 主要用途
    AXI SRAM 512 KB D1 域 M7 核心的主要工作区,通过 64 位 AXI 总线连接,性能极高。
    SRAM1 128 KB D2 域 主要为 M4 核心或外设(如以太网、USB)DMA 使用。
    SRAM2 128 KB D2 域 同上。
    SRAM3 32 KB D2 域 通常用于以太网、USB 等外设的描述符或缓冲区。
    SRAM4 64 KB D3 域 共享内存的首选。位于低功耗域,常用于双核通信数据交换。
    Backup SRAM 4 KB Backup 低功耗备份域,在掉电(带电池)时仍可保持数据。
  • 3. Flash 存储分布

    STM32H747 通常配备 2MB 的 Flash,采用 Dual Bank(双银行) 结构:

    • Bank 1 & Bank 2: 各 1MB。

    • 分配方式: 两颗核心可以同时读取 Flash。通常的做法是将 M7 的固件放在 Bank 1,将 M4 的固件放在 Bank 2。

  如何实现双核通信

OpenAMP (Open Asymmetric Multi-Processing) 是一个开源软件框架,专门用于解决“不同类型的核心(比如 STM32H747 里的 M7 和 M4)如何互相沟通和管理”的问题。

在 STM32H747 这种双核芯片上,两个核心就像两个独立工作的部门。OpenAMP 就是一套标准化的通信协议栈,让它们能像发邮件或打电话一样交换数据。


1. 为什么需要 OpenAMP?

如果没有 OpenAMP,你只能手动操作硬件:

  • 你要自己配置共享内存地址。

  • 你要自己写硬件信号量(HSEM)来抢占读写权。

  • 你要自己写中断逻辑来通知另一个核心。

OpenAMP 把这些底层脏活儿都包了。 它提供了一套标准的 API,让你可以像在 Linux 里用 Socket 通信一样简单地在两个核心间传数据。


2. OpenAMP 的三大核心组件

理解 OpenAMP,主要就是理解这三个概念:

  • virtio (Virtual I/O): * 作用: 共享内存的管理标准。

    • 直白点说: 它在 RAM 里划出两块“缓冲区”(双向循环队列),规定了数据怎么摆放,谁负责写,谁负责读。

  • rpmsg (Remote Processor Messaging):

    • 作用: 消息传输层(最常用)。

    • 直白点说: 它提供了类似“端口”和“通道”的概念。M7 可以往“通道 A”发一条消息,M4 只要监听这个通道就能收到。它是基于 virtio 构建的。

  • remoteproc (Remote Processor Control):

    • 作用: 生命周期管理。

    • 直白点说: 允许“主核”(通常是 M7)去加载、启动或停止“从核”(M4)。


3. 在 STM32H747 上它是如何工作的?

在 ST 的官方固件库中,OpenAMP 通常这样运作:

  1. 建立连接: M7 核心(主)初始化 OpenAMP 框架,并在共享内存(通常是 SRAM4)中开辟空间。

  2. 创建通道: M7 和 M4 建立一个 rpmsg 虚拟通道。

  3. 发送数据: * M7 想传个字符串给 M4。

    • M7 调用 OPENAMP_send()

    • 底层自动把数据写进共享 RAM,并触发一个 IPCC (Inter-Processor Communication Controller) 中断。

  4. 接收数据: * M4 收到中断,去共享 RAM 里读出数据。

    • M4 触发回调函数处理数据。


4. OpenAMP 的优缺点

优点 缺点
标准化: 即使你以后换成 Linux + MCU 的架构(如 STM32MP1),代码也很好移植。 开销: 比起纯手写的 HSEM + 内存偏移,它的代码量和 RAM 占用略大。
易用性: 无需关心底层内存对齐和复杂的信号量逻辑。 复杂度: 初始配置比较繁琐(特别是 Linker Script 的配合)。
可靠性: 处理了并发冲突和数据一致性(Cache 刷新)的问题。  

5. 什么时候该用它?

  • 建议使用: 如果你需要频繁、大量、或者结构化地交换数据(比如 M4 采集传感器数据流,发给 M7 绘图或联网)。

  • 不建议使用: 如果你只是想在 M7 启动时告诉 M4 “你可以开始了”,或者偶尔改一个状态位,那直接用硬件信号量(HSEM)更省事。

总结一下: OpenAMP 就是双核之间的“标准化快递系统”,它让 M7 和 M4 之间的跨核通讯变得规范且安全。

问题: MotorStruct MotorObject[MaxMotorNum] NO_CACHE = {0};这样的数组使用双核的话就需要建立两个M7工程一个M4一个,双核访问的主要核心是这个结构体数组的内容的访问这个是不是更加的占用内存

1. 双核工程结构:确实需要“两个工程”

在 STM32CubeIDE 或 Keil 中,双核开发通常表现为:

  • 一个工作空间(Workspace) 下包含两个子工程:CM7 工程和 CM4 工程。

  • 编译结果: 产生两个独立的 .elf/.bin 文件。

  • 运行逻辑: M7 作为主核首先启动,负责初始化系统时钟和外设,然后通过 HAL_RCCEx_EnableBootCore(RCC_BOOT_C2) 指令唤醒 M4 核心。


2. 内存占用:是“共享”还是“复制”?

这是你最担心的点。如果你正确配置了 Linker Script(链接脚本),内存并不会被双倍占用:

  • 单一物理副本: 你在代码中定义的 MotorObject 数组,物理上只存在于 SRAM 的某个特定位置(比如 SRAM4D2_SRAM)。

  • 双核可见: M7 和 M4 只是在各自的程序里通过相同的内存地址去访问这同一块物理空间。

  • 结论: 内存占用只算一份(sizeof(MotorStruct) * MaxMotorNum)。对于 H7 拥有 1MB RAM 来说,这个结构体数组通常只有几 KB,空间占用微乎其微。


3. NO_CACHE 的关键作用

你代码中的 NO_CACHE 宏非常关键。在双核同时访问这个数组时,必须处理 Cache 一致性 问题:

  • 如果不加 NO_CACHE M7 核心读取电机状态时,会把数据存入自己的 L1 Cache。如果此时 M4 核心修改了内存里的状态,M7 读到的可能还是 Cache 里的旧数据,导致控制逻辑出错。

  • 加了 NO_CACHE(通常配合 MPU 设置): 强制 M7 每次都从真正的物理 RAM 中读写。虽然这会降低一点点 M7 访问该数组的速度,但保证了双核数据绝对同步

  原子性问题: 比如 MotorObject[1].Cur_Move_Step = 0; 这一行。如果该变量是 32 位的,而总线操作不是原子的,可能出现 M7 写了一半、M4 读了一半的情况。这个是时刻改变的怎末做,这个变量到sram中运行不占用sram的空间吗我看两个工程的地址都不一样?

1. 关于原子性问题:32位变量真的会“写一半”吗?

在 STM32H7(Cortex-M7 和 M4)这种 32 位架构中,有一个底层的硬件特性:“对齐的 32 位访问是单次总线事务”。

  • 结论: 如果你的 Cur_Move_Stepuint32_tint32_t 类型,并且是 4 字节对齐 的,那么 MotorObject[1].Cur_Move_Step = 0; 这一行赋值代码,在汇编层面是一条 STR 指令。

  • 结果: 硬件保证了 M4 不会看到“前 16 位是新值,后 16 位是旧值”的情况。它看到的要么是修改前的旧值,要么是修改后的 0。

但是,以下两种情况是不安全的:

  1. 非原子操作(RMW): 比如 MotorObject[1].Cur_Move_Step++;。这在底层分为:读取、加1、写回。如果 M7 在加 1 的中间,M4 去读,就会出问题。

  2. 复合更新: 如果你同时修改“目标位置”和“运动速度”,M4 可能读到了新的位置,但还没读到新的速度。

解决方法:

  • 使用 volatile 确保编译器不会把变量优化到寄存器里,每次都从 SRAM 读。

  • 硬件信号量 (HSEM): 涉及多个变量同时修改时,必须“上锁”。

  • 内存屏障指令 (__DMB()): 在双核切换访问权时,确保数据已经真正写入了 SRAM。


2. 内存占用:它到底占不占空间?

它当然占用 SRAM 空间,但只占用“一份”物理空间。

你可以把 SRAM 想象成一间共享办公室

  • M7 工程里定义了这个数组,相当于给这块空间贴了个标签叫“电机数据”。

  • M4 工程里也定义(或者 extern)了这个数组,也指在这块空间。

  • 物理上: 芯片里只有那一块 1MB 的 SRAM 晶体管。

  • 空间计算: 只要你在链接脚本(Linker Script)里,把两个工程的这个结构体都强制定位到同一个物理地址(比如 0x38000000 SRAM4 区域),它们就不会重复占用。


3. 为什么两个工程的地址不一样?(核心坑点)

你发现地址不一样,通常是因为你现在的配置是“各自为政”,这在双核开发中是错误的:

  1. Flash 地址(必须不一样): * M7 的代码存放在 0x08000000 开始的地方。

    • M4 的代码通常存放在 0x08100000 (Bank 2) 开始的地方。

    • 因为两颗核运行的代码逻辑不同,必须分开存。

  2. SRAM 地址(共享区必须一样):

    • 如果你在 M7 工程里定义 MotorObject,编译器会自动把它分配到 D1 域的 0x24000000

    • 如果你在 M4 工程里也定义一个,编译器可能会把它分配到 D2 域的 0x30000000

    • 结果: 它们变成了两个完全独立的变量,M7 修改它的,M4 访问它的,根本没法通讯。

4. 正确的做法:如何让双核“对齐”?

为了让这个结构体在双核中地址统一且不冲突,你需要操作 Linker Script (.ld 文件)

第一步:在两个工程的 .ld 文件中定义同一个段

Code snippet
 
/* M7 和 M4 的 ld 文件都要有这一段,指向相同的物理地址 */
MEMORY
{
  ...
  SHARED_RAM (xrw) : ORIGIN = 0x38000000, LENGTH = 64K /* SRAM4 区域 */
}

SECTIONS
{
  .shared_section :
  {
    KEEP(*(.shared_data))
  } > SHARED_RAM
}

第二步:在 C 代码中使用属性指定

C
 
// 在两个工程中都这样定义,或者一个定义一个 extern
MotorStruct MotorObject[MaxMotorNum] __attribute__((section(".shared_data")));

这样做的结果:

  • M7 看到的 MotorObject0x38000000

  • M4 看到的 MotorObject 也在 0x38000000

  • 空间只占用一次,且数据完美同步。

总结建议

  1. 地址必须强制对齐:通过链接脚本把结构体锁死在同一个物理地址(推荐 SRAM4 或 D3 域)。

  2. 必须加 volatile:防止编译器捣乱。

  3. 读写分离:建议让 M7 负责写配置,M4 负责读并执行,这样能避开大部分原子性冲突。

  4. 在 STM32H7 这种高性能架构下,处理不当会导致电机丢步、数据错乱甚至内核死锁。

    针对你的三个核心疑问(32位访问、中断中的自增、M4 取数策略),我们逐一拆解:


    1. 32位变量修改的原子性与硬件信号量 (HSEM)

    结论: 简单的 32 位对齐变量赋值是硬件级原子操作,但 ++ 操作不是。

    • 赋值原子性: MotorObject[i].Cur_Move_Step = 100; 在汇编层面是一条 STR 指令。只要变量是 4 字节对齐的,总线能保证一次性写完。

    • 自增(++)非原子性: Cur_Move_Step++ 实际上包含三步:读取 -> 加1 -> 写回。

      • 如果 M7 正在执行“读取”后,还没来得及“写回”,M4 此时去读,读到的就是旧值。

      • 如果是跨核竞争修改(两个核都去 ++),必须用 HSEM。

      • 但如果只有一个核负责写(比如 M7 中断负责 ++),另一个核(M4)只负责读,那么你不需要为了这一个变量去上 HSEM 锁。


    2. 中断中自增(++)使用 HSEM 的影响

    结论:强烈建议不要在中断服务函数(ISR)中使用“阻塞式”信号量。

    • 原因: 信号量(HSEM)本质上是一种竞争机制。如果 M7 的主程序拿到了锁,此时触发了 M7 的中断,中断里如果去 while(HAL_HSEM_Take(...)) 死等这个锁,就会发生自死锁(主程序被中断打断无法释放锁,中断在等主程序释放锁)。

    • 性能影响: 中断讲究“快进快出”。在中断里操作 HSEM 会增加指令周期,甚至引入不确定的等待时间,这会导致电机控制的时序发生抖动(Jitter),严重时影响电机高频脉冲的准确性。

    • 优化策略:

      • 如果 Cur_Move_Step 仅由 M7 中断更新,M4 只负责看。不要在中断里用 HSEM。

      • M4 读到的值可能是“刚加完”或“还没加”的,但由于 32 位访问是原子的,它绝对不会读到一个坏掉的中间值(例如 0x0000FFFF 变成 0x00010000 过程中读到 0x00000000)。


    3. M4 在电机运动中如何正确取数?

    当 M4 需要从 M7 的中断数据中获取电机状态时,主要面临两个挑战:数据一致性缓存一致性

    A. 多变量数据一致性(快照法)

    如果 M4 只需要读一个 Cur_Move_Step,直接读即可。但如果 M4 需要同时读取多个相关变量(例如:当前步数、当前速度、当前方向),为了防止读到“一半更新过、一半没更新”的数据,建议采用 双缓冲区逻辑锁

    • 逻辑锁方案(非阻塞):

      1. M7 中断更新完所有电机数据后,设置一个标志位 Update_Flag = 1

      2. M4 读取前检查 Update_Flag,读取完成后将其置 0。

      3. 或者利用 HSEM 的 非阻塞模式

        C
         
        // M4 取数逻辑
        if (HAL_HSEM_Take(HSEM_ID_0, 0) == HAL_OK) { // 立即返回,不等待
            // 快速拷贝整个结构体到 M4 的私有 RAM
            memcpy(&LocalMotorObject, &MotorObject[i], sizeof(MotorStruct));
            HAL_HSEM_Release(HSEM_ID_0, 0);
        }
        

    B. 缓存一致性(最容易忽略的坑)

    你提到 M7 开启了 Cache(通过 NO_CACHE 宏处理)。

    • M7 写数据: M7 中断修改了 Cur_Move_Step。如果该区域没配置好,数据可能只在 M7 的 Cache 里。

    • M4 读数据: M4 直接去 SRAM 读,读到的是旧值。

    • 解决方案:

      1. MPU 配置: 必须通过 MPU 将共享内存区(如 SRAM4)配置为 "Strongly Ordered""Non-cacheable"

      2. 强制刷新: 如果没配置 MPU,M7 中断在更新完数据后,必须调用 SCB_CleanDCache_by_Addr() 强制将数据推送到物理 SRAM。


    4. 总结你的最佳实践建议

    1. 指针变量: 既然你让 M7 控制指针(htim 等),M4 绝对不要去调用 HAL_TIM_xxx 这种带句柄的函数。M4 应该只读 MotorObject 里的数值(步数、状态)。

    2. 中断 ++:

      • 确保变量是 volatile uint32_t

      • 确保 4 字节对齐(__attribute__((aligned(4))))。

      • 不要在中断里 Take 信号量。

    3. M4 取数:

      • 如果只读一个 32 位数,直接读。

      • 如果要读一组数,在 M4 侧使用 HAL_HSEM_Take(id, 0)(非阻塞模式)。如果拿不到锁,就延后一两个周期再读,保证不卡死。

    4. 硬件层确认:

      • 确认 M7 的 MPU 已经把该内存段设为 Non-cacheable

      • 确认双核工程中 MotorObject 指向的是同一个物理地址(如 0x38000000)。

    一句话建议: 让 M7 中断像“发报机”一样只管往 SRAM 写(注意 Cache 刷新),让 M4 像“观察员”一样只管读。只要变量对齐且是 32 位的,你就不用担心读到“错码”。只有当 M4 也要这个结构体时,才必须严格使用 HSEM。

还有一个问题就是内存位置: 被指向的对象(句柄本身)必须也在共享内存里,否则会发生地址不可达

比如说 MotorObject[0].htim = &htim1;

这句话是双核开发中最容易导致程序硬件错误(HardFault)的深坑。为了让你彻底理解,我们用一个“共享记事本”的例子来说明。

1. 形象理解:记事本与私人抽屉

想象你有两个员工:大经理 (M7)小主管 (M4)。 他们桌子中间有一个公共记事本(共享内存 SRAM4)

  1. 大经理 (M7) 在他的私人抽屉(DTCM 内存)里放了一个印章(TIM_HandleTypeDef 句柄变量)

  2. 大经理公共记事本上写了一句话:“喂,小主管,印章在我的私人抽屉里,地址是 0x20001234。”

  3. 小主管 (M4) 走过来,看了记事本,记下了这个地址。

  4. 小主管试图去开大经理的私人抽屉(访问 0x20001234)。

  5. 结果: 办公室的保安(总线矩阵)冲出来把小主管按住了,并大喊:“你不准进大经理的私人办公室!” —— 这就是 Bus Fault(总线错误/地址不可达)


2. 硬件层面的真相

在 STM32H747 中,内存是分“领地”的:

  • DTCM / ITCM (0x20000000 / 0x00000000): 这是 M7 的绝对私人领地。M4 核心在硬件电路连线上,根本没有通往这里的“路”。

  • AXI SRAM (0x24000000): 这是 M7 的主领地,M4 理论上可以通过总线桥访问,但速度较慢且需要权限配置。

  • SRAM1/2/3 (0x30000000): 这是 M4 的主领地,M7 也可以访问。

  • SRAM4 (0x38000000): 这是公共区域(D3 域),两个核心访问都很方便。


3. 代码里的错误写法 vs 正确写法

❌ 错误写法(会导致 M4 崩溃):

C
 
// 在 M7 的工程中,如果不指定位置,变量默认可能掉进 DTCM(私人领地)
TIM_HandleTypeDef htim1; 

void M7_Init() {
    // MotorObject 在 SRAM4(公共区),没问题
    // 但指针指向的 &htim1 是 M7 的私人地址
    MotorObject[0].htim = &htim1; 
}

// --- 此时 M4 访问 MotorObject[0].htim->State 就会直接 HardFault ---

✅ 正确写法(双核都能访问):

你需要确保这个“印章”(句柄本身)也放在大家都看得到的地方。

方法 1:放在 D2 域 (SRAM1/2/3) 这是最常见的做法,因为 M4 访问这里最快。

C
 
// 在 M7 和 M4 的代码中,都要确保这个句柄变量在 D2 或 D3 域
TIM_HandleTypeDef htim1 __attribute__((section(".sram2_data"))); 

void Init() {
    MotorObject[0].htim = &htim1; // 指向的地址是 0x3000xxxx,两核都能通
}

方法 2:放在共享内存区 (SRAM4) 如果你已经定义了一个 SHARED_RAM 段,那就把句柄也丢进去。


4. 为什么“地址不可达”?

在 STM32H7 的总线矩阵(Bus Matrix)中:

  1. M4 核心是“客人”,它只能通过特定的“总线桥”访问 M7 的 D1 域。

  2. DTCM 是 CPU 的耦合内存,它直接连在 M7 的内核总线上,没有连到总线矩阵上

  3. 物理隔绝: 因为硬件上没有那根线,所以 M4 无论如何也读不到 DTCM 里的数据。


5. 你的结构体里该怎么改?

既然你的 MotorObject 已经用了指针:

  1. 检查 htim1hspi1 等变量的定义位置。.map 文件里看它们的地址。

  2. 如果它们的地址是以 0x2000 开头的,M4 绝对用不了。

  3. 建议: 既然你在做多电机控制,干脆把所有的 TIM_HandleTypeDef 数组也像 MotorObject 一样,通过链接脚本(Linker Script)强制定位到 SRAM1SRAM4

总结一句话: “不但记事本(结构体)要放在公共桌子上,记事本里提到的那些东西(句柄变量)也得放在公共区域,否则小主管(M4)看得到地址却拿不到东西。”

 M7的工程与M4的工程整个结构体都放到共享内存中,然后这两个工程新建两个数组都指向这个共享内存的地址不会造成sram多余:

这个问题具体解释一下,通过这种方式,不仅解决了双核通信的问题,还最大限度地节省了 SRAM 空间

这种方法在嵌入式双核开发中被称为“内存映射对齐”。为了让你彻底放心并确保工程实施不出错,我从物理层面和软件层面为你解释一下为什么这样不会造成“多余”:

1. 物理层面:一套房子的两个门

你可以把这块共享内存(比如 SRAM4)想象成一间房子。

  • M7 工程里的数组名就像是“前门”的门牌号。

  • M4 工程里的数组名就像是“后门”的门牌号。

  • 虽然你在两个工程的代码里都写了 MotorObject[MaxMotorNum],但只要你在链接脚本(Linker Script)里把它们指向了同一个物理地址(例如 0x38000000),那么它们在芯片内部指向的就是同一组晶体管

结论: 物理上只占用了一份 sizeof(MotorStruct) * MaxMotorNum 的空间。


2. 软件层面:如何确保“不多余”?

要实现你说的“不造成 SRAM 多余”,必须满足以下两个条件:

A. 链接脚本的“绝对定位”

如果你只是在两个工程里定义数组,而不去修改 .ld 文件,编译器会根据各自的规则把它们乱放(M7 可能放到 0x24000000,M4 可能放到 0x30000000),这样就真的变成了两份数据,且互不相通。

你必须这样做: 在 M7 和 M4 的 .ld 文件中,定义完全一样的存储区域:

Code snippet
 
/* M7.ld 和 M4.ld 都要有 */
MEMORY
{
  ...
  SHARED_RAM (xrw) : ORIGIN = 0x38000000, LENGTH = 64K
}

SECTIONS
{
  .shared_data_section (NOLOAD) : /* 使用 NOLOAD 防止两个工程重复初始化清零 */
  {
    KEEP(*(.shared_data))
  } > SHARED_RAM
}

B. 结构体定义的“绝对一致”

这是最容易出错的地方。如果 M7 的编译器认为结构体是 8 字节对齐,而 M4 认为是 4 字节对齐,那么 MotorObject[1] 的地址在两个核心看来就不一样了(发生了偏移)。

建议:

  • MotorStruct 的定义放在一个双核共用的头文件中。

  • 强制使用 __packed__attribute__((aligned(4))) 确保成员排列顺序和间隙在两个核心中完全一致。


3. “不多余”带来的额外好处

  1. 零拷贝通信: M7 更新了电机的当前位置,M4 立即就能从同一个地址读到,不需要通过串口、SPI 或 OpenAMP 搬运数据。这种“原地读写”是性能最高的。

  2. 全局状态监控: 你可以轻松地实现:M7 负责高层逻辑(跑算法、画图),M4 负责底层实时脉冲。它们看的是同一个“仪表盘”。


4. 一个非常重要的细节:(NOLOAD)

在链接脚本里,我特意写了 (NOLOAD)

  • 为什么要加: 默认情况下,C 语言会清零全局变量(BSS 段)。如果 M7 启动了,把电机数据初始化好了,结果 M4 紧接着启动,它又去把这块内存清零一遍,那 M7 的工作就白做了。

  • 做法: 使用 (NOLOAD) 属性,或者只在 M7 核心里进行 Task_Motor_Init(),M4 核心直接使用即可,不要再次初始化。

总结

你的方案是标准的最优解。 只要地址对齐,物理上就只有一份内存,不存在多余。

你可以通过以下方式验证:

  1. 编译两个工程。

  2. 查看 M7 的 .map 文件,搜索 MotorObject,记录其地址。

  3. 查看 M4 的 .map 文件,搜索 MotorObject,看看地址是否一模一样

  4. 如果地址一样,说明你成功地让两个内核“共用了一张办公桌”。

你现在准备把这个共享区域放在 SRAM4(0x38000000)还是 D2_SRAM(0x30000000)?(考虑到 M4 运行电机的实时性,放在 D2_SRAM 速度会更快一些)

来自于AI生成

posted on 2026-05-07 14:53  虎啸岳林  阅读(107)  评论(0)    收藏  举报

导航