Keil5中使用分散加载文件定制内存布局

AI助手已提取文章相关产品:

Keil5中使用分散加载文件定制内存布局:从实战出发的深度指南 🛠️

你有没有遇到过这样的情况?

  • 固件升级时,不小心把设备的校准参数给擦了,导致所有出厂设备“失明”;
  • 写了个Bootloader,结果跳转到App后一进中断就硬故障(HardFault);
  • 多个团队并行开发,A模块改了个配置,B模块突然跑不起来——查了半天发现是RAM地址冲突。

这些问题背后,往往不是代码逻辑的问题,而是 链接阶段的内存布局失控 。而解决这一切的关键钥匙,就是—— 分散加载文件(Scatter File)

别被这个名字吓到,它听起来高深,其实本质很简单: 告诉链接器,“这段代码放这儿,那个变量放那儿”,就这么直接。

但正是这种“直接”,让它成了嵌入式系统稳定性的最后防线。尤其在Keil MDK环境下, .sct 文件几乎是复杂项目绕不开的一环。

今天我们就抛开教科书式的讲解,用工程师之间聊天的方式,聊聊怎么真正用好这个工具,而不是“照抄模板、出了问题再删库跑路”。


为什么默认链接不够用了?💡

先说个现实:大多数初学者甚至中级开发者都依赖Keil的“Target”设置里那几个简单的Flash/RAM起始地址和大小。这在做点灯、串口打印这类小项目时完全没问题。

但一旦进入真实产品级开发,你会发现:

“咦?我只改了一行代码,为什么程序大小涨了8KB?”
“OTA升级后设备变砖了,是不是跳转地址错了?”
“加密密钥存哪最安全?能不能不让固件更新时被擦除?”

这时候你就得问自己一句: 你的代码,真的知道自己住哪儿吗?

ARM Cortex-M系列MCU虽然架构统一,但每款芯片的存储分布千差万别。STM32F103C8T6有64KB Flash,STM32H743有2MB;有些带DTCM RAM,有些支持双Bank Flash。如果全靠链接器自动分配,那就像让一个陌生人帮你装修房子——东西是放下了,但用起来绝对别扭。

所以,我们需要一种机制,能 精确控制每一个段(section)落在哪个物理地址上 。这就是 .sct 文件存在的意义。


Scatter File 到底是个啥?🧠

你可以把它理解为一份“内存地图+搬家指令”。它不参与编译,但在链接阶段起决定性作用。

它干了哪些事?

  1. 划分区域 :比如前128KB Flash留给Bootloader,后面给Application;
  2. 指定位置 :某个函数必须放在特定地址,用于中断向量表或XIP执行;
  3. 隔离关键数据 :保留最后一块Flash扇区专门存序列号、加密密钥;
  4. 实现Copy-down :将初始化数据从Flash复制到SRAM中运行;
  5. 支持多镜像共存 :比如双备份固件、安全区与普通区分离。

它的核心思想是: 把链接过程从“全自动”变成“半托管”


一张图看懂工作流程 🔍

想象一下你要搬新家:

  • 编译器像是打包工人,把家具按类型分类(沙发→代码段,床→数据段);
  • 链接器是搬家公司司机,原本他只知道“送到XX小区”;
  • .sct 文件是一张详细图纸:“客厅放沙发,主卧放床,次卧暂时空着别堆东西”。

于是整个流程变成了:

源码 → 编译成.o文件 → 链接器读取.sct → 按规则摆放各段 → 输出.bin/.axf

其中最关键的两个概念是:

  • 加载域(Load Region) :烧录时所在的地址,通常是Flash;
  • 运行域(Execution Region) :运行时实际所在地址,可能是RAM。

举个例子:一段代码想在RAM里执行更快(比如滤波算法),但它不能断电丢失,所以要先存在Flash里。上电后由启动代码把它拷贝到RAM中执行。

这时:
- 加载域 = Flash 地址(如 0x0800_8000
- 运行域 = SRAM 地址(如 0x2000_0000

链接器会自动生成一段“搬运工代码”(通常叫 __main 或 scatter-loading routine),完成这个拷贝动作。

⚠️ 注意:如果你看到程序启动时卡住没进 main() ,很可能就是 scatter loading 出了问题,比如目标RAM区域已被占用或未使能。


语法结构拆解:别怕,都是纸老虎 🧩

我们来看一个典型的 .sct 文件:

LR_IROM1 0x08000000 0x00080000 {
    ER_IROM1 0x08000000 0x0007C000 {
        *.o (RESET, +First)
        *(InRoot$$Sections)
        .ANY (+RO)
    }

    RW_IRAM1 0x20000000 0x00010000 {
        .ANY (+RW +ZI)
    }
}

一行行拆开看:

第一行:定义加载域

LR_IROM1 0x08000000 0x00080000
  • LR_IROM1 :名字随便起,一般表示“Load Region for Internal ROM”;
  • 0x08000000 :起始地址,也就是Flash基址;
  • 0x00080000 :长度,512KB。

这一行相当于说:“我打算在这块Flash里放点东西。”

第二层:运行域(代码部分)

ER_IROM1 0x08000000 0x0007C000 {
    *.o (RESET, +First)
    *(InRoot$$Sections)
    .ANY (+RO)
}
  • ER_IROM1 :Execution Region,运行在这里的代码直接从Flash执行(XIP);
  • +First :强制把这个目标文件放在最前面——为什么重要?因为复位向量必须位于地址起点!

✅ 关键知识点:CM内核上电后自动从 0x08000000 取MSP初值,第二个字是复位向量地址。如果这里不是你的启动代码,CPU就会“迷路”。

.ANY (+RO) 是个通配符,意思是“剩下的所有只读段都塞进来”,包括:
- 函数代码(code)
- 字符串常量(const char[])
- 全局 const 变量

数据段区域

RW_IRAM1 0x20000000 0x00010000 {
    .ANY (+RW +ZI)
}
  • 所有已初始化变量(如 int x = 5; )归为 RW
  • 未初始化变量(如 int buffer[1024]; )属于 ZI
  • 它们都放进SRAM运行。

注意: ZI 段不会占用Flash空间!它是零初始化段,链接器只会记录大小,运行时由启动代码清零。


实战案例:构建可升级系统的内存布局 💥

假设你正在做一个支持 OTA 升级的产品,MCU 是 STM32F407,有 1MB Flash。

你想做到:
- 前 128KB 是 Bootloader,负责验证和跳转;
- 后面 896KB 是 App,可以随时更新;
- 最后 4KB 不参与任何操作,用来存设备唯一标识和校准数据;
- App 使用独立的中断向量表。

这就需要两个 .sct 文件:一个给 Bootloader,一个给 App。

Bootloader 的 .sct

LR_BOOT 0x08000000 0x00020000 {              ; 128KB 区域
    ER_BOOT 0x08000000 0x00020000 {
        *.o (RESET, +First)                  ; 启动文件放最前
        *bootloader_core.o (+RO)             ; 核心功能代码
        *usart_driver.o (+RO)                ; 通信驱动
        .ANY (+RO)                           ; 其他只读段
    }

    RW_RAM 0x20000000 0x00010000 {
        .ANY (+RW +ZI)                       ; 所有变量放SRAM
    }
}

Application 的 .sct

LR_APP 0x08020000 0x000E0000 {               ; 从0x08020000开始(128KB偏移)
    ER_APP 0x08020000 0x000E0000 {
        *.o (RESET, +First)                  ; App自己的向量表
        *app_main.o (+RO)
        .ANY (+RO)
    }

    RW_APP_RAM 0x20000000 0x00010000 {
        .ANY (+RW +ZI)
    }
}

📌 小技巧:可以用宏定义统一管理这些地址,在 C 代码中也保持一致:

```c

define APP_START_ADDR 0x08020000UL

define APP_VECTOR_TABLE APP_START_ADDR

define BOOTLOADER_SIZE 0x20000UL

```

这样哪怕后期换芯片,只要改一处就行。


怎么确保跳转成功?别让HardFault背锅 ❌

很多人写完Bootloader后,信心满满地跳过去,结果一进中断就崩。

原因往往是: 中断向量表没切过去!

CM内核有个寄存器叫 VTOR (Vector Table Offset Register),它决定了CPU去哪里找中断入口。

默认是 0x00000000 ,映射到 0x08000000 。但App的向量表在 0x08020000 ,你不改 VTOR,CPU 还是回去老地方找,当然找不到。

正确的跳转流程应该是:

void jump_to_application(void) {
    // 1. 禁能所有中断
    __disable_irq();

    // 2. 设置新的栈顶(MSP)
    uint32_t app_msp = *(volatile uint32_t*)APP_START_ADDR;
    __set_MSP(app_msp);

    // 3. 更新向量表偏移
    SCB->VTOR = APP_VECTOR_TABLE;

    // 4. 获取复位处理函数地址
    pFunction app_reset_handler = (pFunction)(*(uint32_t*)(APP_START_ADDR + 4));

    // 5. 开启中断(可选,取决于App是否需要)
    __enable_irq();

    // 6. 跳!
    app_reset_handler();
}

⚠️ 注意事项:
- 必须先设 MSP,否则后续函数调用会出问题;
- VTOR 修改后,所有中断都会从新表响应;
- 如果你在 Bootloader 中用了 RTOS 或大量中断,记得关闭相关外设。


如何把某个函数/变量放到指定地址?🎯

有时候你不需要整个分区,只想“钉住”某一小块内容。

比如:

  • 把加密密钥放在最后一个扇区,防止误擦;
  • 让某个高频ISR紧挨着向量表,减少跳转延迟;
  • 创建共享内存供双核通信(如CM4+CM0架构)。

方法一:用 __attribute__((section)) 自定义段名

这是最常用也最灵活的方法。

// calibration_data.c
__attribute__((section(".calib_data"))) 
const CalibrationData_t g_calib = {
    .temp_offset = 0.5f,
    .vref = 3.298f,
    .sn = {0xDE, 0xAD, 0xBE, 0xEF}
};

然后在 .sct 中添加:

LR_CONFIG 0x080FF000 0x00001000 {           ; 最后4KB
    ER_CALIB 0x080FF000 0x00000100 {
        *.o (.calib_data)
    }
    ; 其余空间可用于日志或其他用途
}

这样一来,无论你怎么更新App,只要不主动擦这块区域,数据就一直安全。

🔒 安全建议:还可以配合Flash保护机制(如RDP级别或PCROP)进一步锁定该区域。

方法二:固定地址 + FIXED 关键字(慎用)

如果你想让某个函数永远待在一个绝对地址,可以用 FIXED

LR_SPECIAL_FUNC 0x0807F000 0x00001000 {
    ER_HANDLER 0x0807F000 FIXED {
        mymodule.o (MY_ISR_SECTION)
    }
}

对应C代码:

void __attribute__((section("MY_ISR_SECTION"), noinline)) fast_isr(void) {
    // 极快响应的中断服务程序
}

⚠️ 警告:
- FIXED 会让链接器失去弹性,一旦地址冲突会直接报错;
- 不要轻易用于大型函数,容易引发维护灾难;
- 建议仅用于极少数关键场景,比如硬件调试桩函数。


常见陷阱与避坑指南 🕳️

❌ 陷阱1:段重叠但链接器不报错

你以为写了 .sct 就万事大吉?错!

链接器不会主动检查地址冲突。比如你同时写了:

ER_A 0x08008000 0x1000 { .ANY(+RO) }
ER_B 0x08008000 0x1000 { *special.o(+RO) }

这两个运行域起始地址相同,链接器可能会静默合并或覆盖,导致行为不可预测。

解决方案
- 使用不同名称明确区分;
- 在 .sct 中合理排序,优先级高的放前面;
- 用 *module.o(section) 精确匹配,避免 .ANY 泛滥;
- 编译后查看 .map 文件确认实际布局。

❌ 陷阱2:忘记对齐要求

某些硬件模块(如DMA、Ethernet MAC)要求缓冲区地址对齐到 4-byte、8-byte 甚至 32-byte 边界。

如果你把一个缓冲区随便扔进 .ANY (+RW) ,可能恰好不对齐,导致DMA传输失败。

解决方案

__attribute__((aligned(32), section(".dma_buf")))
uint8_t dma_rx_buffer[256];

并在 .sct 中确保该段所在区域自然满足对齐。

或者更稳妥的做法是在 .sct 中显式控制:

ER_DMA_BUF 0x20001000 UNINIT 256 {
    *.o (.dma_buf)
}

UNINIT 表示不进行初始化(适合DMA缓冲), 256 指定大小。


如何启用 .sct 文件?📌

很多新手卡在这一步。

在 Keil5 中:

  1. .sct 文件拖进工程目录;
  2. 右键 “Options for Target” → “Linker” 标签页;
  3. 取消勾选 “Use Memory Layout from Target Dialog”;
  4. 勾选 “Use Scatter File”;
  5. 点击“…”选择你的 .sct 文件路径;
  6. 编译。

✅ 提示:可以在 Project → Manage → Project Items 中设置不同 build target 使用不同的 .sct ,方便管理 Bootloader/App/Debug 等多种配置。


Map 文件怎么看?🔍

每次编译完生成的 .map 文件其实是宝藏。

打开它,你能看到:

  • 每个段的实际地址;
  • 是否发生重叠;
  • 内存利用率;
  • 特殊段是否落到了预期位置。

重点关注这几部分:

Memory Map of the image

    Execution Region Load Addr   Type     Attr     Addr         Size       ΜΙ
    LR_BOOT        0x08000000   Load     RO       0x08000000   0x0001a3e0  
    ER_BOOT        0x08000000   Exec     RO       0x08000000   0x0001a3e0  
    RW_RAM         0x20000000   Exec     RW-ZI    0x20000000   0x00002100  

再往下看具体段分布:

Home Section        Address    Size     Type   Attr      Idx    E Section Name Object
----------------------------------------------------------------------
RESET               0x08000000 0x01ac   Code  RO          1     RESET startup_stm32f407xx.o
.text               0x080001ac 0xabc0   Code  RO          2     .text main.o
.calib_data         0x080ff000 0x0010   Data  RO          3     .calib_data calibration.o

一眼就能看出 calib_data 是否在正确位置。


更进一步:自动化与工程化建议 🛠️

当你项目越来越大,手写 .sct 容易出错。怎么办?

✅ 推荐做法1:用头文件统一地址定义

// memory_layout.h
#ifndef MEMORY_LAYOUT_H
#define MEMORY_LAYOUT_H

#define FLASH_BASE          0x08000000UL
#define BOOT_SIZE           (128 * 1024)
#define APP_START_ADDR      (FLASH_BASE + BOOT_SIZE)
#define CALIB_AREA_ADDR     (FLASH_BASE + (1024 * 1024) - 0x1000)  // 最后4KB

#endif

然后在 .sct 中尽量使用注释标明来源:

; 来源于 memory_layout.h 中定义的 APP_START_ADDR
LR_APP 0x08020000 0x000E0000 {
    ...
}

✅ 推荐做法2:为不同模式创建多个 .sct

例如:
- bootloader_debug.sct
- bootloader_release.sct
- app_normal.sct
- app_recovery.sct

通过 Keil 的 “Manage Configurations” 功能切换,避免混淆。

✅ 推荐做法3:加入CI脚本做静态检查

写个 Python 脚本解析 .map 文件,检查关键段地址是否合规,比如:

  • calib_data 必须在 [0x080FF000, 0x080FFFFF] 范围内;
  • App 向量表必须在 0x08020000
  • 总代码大小不超过预留空间的 90%;

提前发现问题,比现场调试省一百倍力气。


结语:掌控底层,才有自由 🌟

分散加载文件从来不是一个“炫技工具”,它是 系统健壮性的基础设施

当你开始思考“我的代码住哪儿”、“它会不会被意外动掉”、“别人改了我的模块会不会炸”,你就已经迈入了专业嵌入式开发的大门。

.sct 文件,就是你手中的画笔。

别再盲目相信“默认配置能搞定一切”。真正的高手,都是从学会控制链接开始的。

下次你遇到内存布局问题时,不妨停下来问问自己:

“我是想让它随机住,还是我想让它住得好?”

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

您可能感兴趣的与本文相关内容

<think>好的,我现在需要帮助用户解决在Keil中如何让分散加载文件使用全局宏预编译的问题。首先,我得回顾一下之前的对话历史,确保理解上下文。用户之前询问过关于全局宏预编译分散加载文件的概念,现在具体到Keil环境下如何实现。 首先,用户的问题可能源于他们在Keil项目中遇到了需要根据不同的宏定义来调整内存布局或模块加载的情况。例如,他们可能在开发跨不同硬件平台的项目,需要根据不同的宏定义选择不同的链接脚本。或者他们可能在处理不同配置(如调试版和发布版)时需要不同的内存分配策略。 接下来,我需要考虑Keil MDK使用分散加载文件scatter file)的结构和语法。分散加载文件通常用于定义代码和数据在内存中的分布,但默认情况下可能不支持宏预编译。因此,用户的问题核心在于如何在分散加载文件使用全局宏,并通过预编译条件来控制不同的内存布局。 根据Keil的文档,分散加载文件本身不支持预处理器指令,如#ifdef或#define。因此,直接在里面写条件编译可能不可行。但用户可能需要通过其他方法间接实现类似的效果,比如在项目配置中定义全局宏,然后在分散加载文件中引用这些宏,或者在构建过程中生成不同的分散加载文件。 可能的解决步骤包括: 1. **在项目选项中定义全局宏**:在Keil的Target Options里定义宏,如USE_EXTERNAL_RAM=1。这样在编译时,这些宏会被传递给编译器,但分散加载文件是否能访问这些宏呢?需要进一步确认。 2. **使用预处理生成分散加载文件**:可能需要通过外部脚本或构建系统,在编译前根据宏定义生成不同的分散加载文件。例如,使用批处理或Python脚本根据宏的值选择不同的.scat文件。 3. **在代码中使用条件编译影响内存分配**:虽然分散加载文件本身不支持宏,但可以通过代码中的宏控制特定段(section)的放置,然后在分散加载文件中引用这些段。例如,使用__attribute__((section("name")))将特定变量或函数放入指定段,再在分散加载文件中处理这些段的位置。 需要验证这些方法的可行性。例如,检查Keil是否允许在分散加载文件使用环境变量或通过命令行传递宏定义。此外,用户可能需要了解分散加载文件是否支持某种形式的变量替换,或者是否需要在链接阶段进行条件处理。 另一个考虑是用户可能对Keil工具链的具体功能不熟悉,需要指导他们如何在项目设置中正确配置宏,并确保这些宏在链接阶段被正确应用。例如,在Keil中,预定义的宏可以通过“Options for Target” -> “C/C++” -> “Define”来设置,但分散加载文件是否能够识别这些宏呢? 根据我的现有知识,分散加载文件Keil中通常是在链接阶段处理的,而预处理器宏是在编译阶段处理的。因此,直接的条件编译可能无法在分散加载文件中实现。因此,替代方法可能更为实际,例如生成不同的分散加载文件使用链接器符号来动态调整内存布局。 总结可能的解决方案步骤: 1. **定义全局宏**:在Keil的项目选项中设置需要的宏,例如USE_EXTERNAL_RAM。 2. **预处理生成分散加载文件**:编写脚本,根据宏定义生成对应的.scat文件,替换相应的内存区域定义。 3. **在代码中使用条件编译控制段分配**:结合代码中的宏,将特定数据或代码分配到不同的段,然后在分散加载文件中处理这些段的位置。 4. **使用链接器符号**:在代码中定义符号,根据宏的值在分散加载文件中引用这些符号来调整内存布局。 需要详细说明每一步的具体操作,确保用户能够按照步骤实施。同时,需要注意可能的陷阱,例如分散加载文件的语法是否正确,路径是否正确,以及构建过程中生成文件的时机是否合适。 最后,用户的相关问题可能涉及如何调试宏定义是否生效,或者如何处理多个宏组合的情况,这些也需要在回答中适当提及,以帮助用户全面理解和解决问题。</think>在 **Keil MDK** 中,分散加载文件Scatter File,`.scat`)用于定义代码和数据在内存中的分布。默认情况下,分散加载文件不支持直接使用全局宏预编译(如 `#ifdef`、`#define`),但可以通过以下方法间接实现类似功能: --- ### 方法 1:通过 **项目选项预定义宏** 控制分散加载内容 **原理**:在项目选项中预定义全局宏,并在分散加载文件中通过 `__attribute__` 或代码中的条件编译间接关联这些宏。 #### 步骤: 1. **定义全局宏** 在 Keil 的项目选项中预定义宏(例如 `USE_EXTERNAL_RAM=1`): - 右键点击项目 -> `Options for Target` -> `C/C++` -> `Define` 输入宏,如 `USE_EXTERNAL_RAM=1`。 ![Keil Define Macro](https://example.com/keil-define-macro.png) 2. **在代码中通过条件编译控制段(Section)分配** 在代码中使用宏控制数据/代码的存放位置: ```c #ifdef USE_EXTERNAL_RAM // 将变量分配到外部 RAM 的段 __attribute__((section(".external_ram"))) uint8_t buffer[1024]; #else // 默认分配到内部 RAM uint8_t buffer[1024]; #endif ``` 3. **在分散加载文件中定义对应段的位置** 在 `.scat` 文件中根据宏的逻辑定义段的位置: ```plaintext LR_IROM1 0x08000000 0x00080000 { ; 内部 Flash ER_IROM1 0x08000000 0x00080000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00010000 { ; 内部 RAM .ANY (+RW +ZI) } #if defined(USE_EXTERNAL_RAM) RW_ERAM 0x60000000 0x00020000 { ; 外部 RAM(通过宏控制) *.o (.external_ram) } #endif } ``` > **注意**:分散加载文件本身不支持 `#if` 指令,上述写法仅为逻辑示意,实际需通过代码中的段分配间接实现。 --- ### 方法 2:使用 **多套分散加载文件 + 构建脚本控制** **原理**:为不同配置生成不同的分散加载文件,通过构建脚本(如批处理、Python)或 Keil 的 `Target` 配置动态选择文件。 #### 步骤: 1. **编写多套分散加载文件** 例如: - `scatter_external_ram.scat`(含外部 RAM 配置) - `scatter_internal_ram.scat`(仅内部 RAM) 2. **在 Keil 中通过 Target 配置切换文件** - 右键项目 -> `Manage Project Items` -> 创建多个 Target(如 `Debug_ExternalRAM`、`Debug_InternalRAM`)。 - 每个 Target 的 `Linker` 选项中指定对应的 `.scat` 文件。 3. **通过全局宏关联不同 Target** 在每个 Target 的 `C/C++` -> `Define` 中定义对应宏(如 `USE_EXTERNAL_RAM`)。 --- ### 方法 3:通过 **预处理生成分散加载文件** **原理**:使用外部工具(如 Python 脚本)根据宏动态生成 `.scat` 文件。 #### 步骤: 1. 创建模板文件 `scatter_template.scat`,包含占位符: ```plaintext LR_IROM1 0x08000000 0x00080000 { ER_IROM1 0x08000000 0x00080000 { ... } RW_IRAM1 0x20000000 0x00010000 { ... } {{EXTERNAL_RAM_SECTION}} } ``` 2. 编写脚本根据宏生成最终 `.scat` 文件: ```python import sys use_external_ram = "USE_EXTERNAL_RAM" in sys.argv with open("scatter_template.scat", "r") as f: content = f.read() if use_external_ram: content = content.replace("{{EXTERNAL_RAM_SECTION}}", "RW_ERAM 0x60000000 0x00020000 { *.o (.external_ram) }") else: content = content.replace("{{EXTERNAL_RAM_SECTION}}", "") with open("scatter.scat", "w") as f: f.write(content) ``` 3. 在 Keil 的 **Pre-Build 步骤** 中调用脚本: - 项目选项 -> `User` -> `Before Build` -> 添加命令: `python generate_scatter.py USE_EXTERNAL_RAM` --- ### 验证方法 1. **查看宏是否生效** - 在代码中添加 `#ifdef USE_EXTERNAL_RAM` 调试输出,或直接检查编译日志中的宏定义。 - 使用 `--list` 链接器选项生成 `.map` 文件,确认段分配是否符合预期。 2. **检查分散加载文件** 确保生成的 `.scat` 文件内容与宏逻辑一致。 --- ### 常见问题 1. **为什么分散加载文件不支持 `#ifdef`?** Keil分散加载文件由链接器直接解析,而非预处理器,因此不支持宏指令。 2. **如何避免滥用宏?** - 仅对硬件相关或配置差异较大的部分使用宏控制。 - 尽量通过代码结构(如模块化)而非宏管理功能。 --- ### 相关扩展 - **Keil 文档参考**: [ARM Scatter File Syntax](https://developer.arm.com/documentation/101754/0622/armlink-Reference/Scatter-File-Syntax) - **进阶技巧**:使用 `__attribute__((at(address)))` 直接指定变量地址,结合宏控制。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值