Tasking lsl脚本--内存布局与链接原理

━━━ 引言|为什么你写的代码跑不到你指定的位置?

很多工程师都有过这种经历:写了一个 「__at(0x80001000)」 的函数,想让它烧录到 Flash 的指定地址,结果编译报错,或者运行时根本不在预期位置。

问题往往出在 【LSL 链接脚本(Linker Script Language)】 上。

LSL 是 TASKING 链接器的核心配置文件,决定了:

○ 代码和数据放在芯片的哪块物理内存

○ 多个目标文件的节(section)如何排列

○ 某段代码应该在哪个 CPU 核上运行

○ RAM 和 ROM 的起始地址和大小

理解 LSL,是从"会用编译器"升级到"真正掌握编译器"的关键一步。

━━━ 一、LSL是什么?它能解决什么问题?

LSL = 【Linker Script Language】,是 TASKING 专有的链接脚本语言,文件扩展名为 「.lsl」。

在 TASKING IDE 环境下,LSL 文件【默认自动生成】,并对芯片的 core architecture、on-chip memory、stack/heap 等已做了预定义。也可以通过 IDE 图形界面进行配置:【Processor 选择 → Memory 布局 → Stack/Heap 配置】,所有修改最终都会体现在 LSL 文件中。

LSL 的核心目的有两个:

❶ 【指明目标特性】:设置代码在指定核心上运行,方便为某些代码选择运行核心

❷ 【指明内存布局】:编译生成的 Section 在内存中如何排布

● 什么时候必须改 LSL?

○ 【多核内存分配】:TC3xx 有 2~6 个 TriCore 核,各核有独立的 DSPR/PSPR 地址空间

○ 【绑核绑变量】:要把某个变量/函数强制绑定到某个核的专属内存

○ 【自定义内存区】:外扩 SPI Flash、EMEM、DDR 等

○ 【特定地址存放】:中断向量表、启动代码必须从固定地址开始

○ 【ROM->RAM 拷贝】:需要把代码从 Flash 拷贝到 RAM 中高速执行

━━━ 二、LSL文件结构全解析

一个完整的 LSL 文件通常包含【四层嵌套结构】:

【代码】

architecture   ←  芯片架构定义(TriCore内核、总线结构)

↓

derivative     ←  具体芯片型号(TC27x / TC29x / TC39x...)

↓

memory         ←  物理内存映射(Flash起始地址/大小、RAM起始地址/大小)

↓

section_layout ←  逻辑布局(代码/数据放在哪个memory区域)

【/代码】

● 2.1 architecture / derivative(架构与芯片层)

这两层通常由英飞凌底层代码提供和TASKING 预置在安装目录中【一般不需要修改】。

● 2.2 memory(物理内存层)------定义地址

以 TC27x 为例,其 Core0 的 DSPR(数据暂存器)和 PSPR(程序暂存器)定义如下:

【LSL 代码】

// Core0 数据暂存 RAM(DSPR0)

memory dspr0 (tag="on-chip")

{

    mau = 8;

    type = ram;

    size = 120k;

    // 通过 Core0 FPI 总线访问(高速本地访问)

    map (dest=bus:tc0:fpi_bus, dest_offset=0xd0000000, size=120k, priority=1, exec_priority=0);

    // 通过 SRI 总线访问(其他核跨核访问)

    map (dest=bus:sri, dest_offset=0x70000000, size=120k);

}

    // Core0 程序暂存 RAM(PSPR0)

memory pspr0 (tag="on-chip")

{

    mau = 8;

    type = ram;

    size = 32k;

    map (dest=bus:tc0:fpi_bus, dest_offset=0xc0000000, size=32k, exec_priority=1);

    map (dest=bus:sri, dest_offset=0x70100000, size=32k);

}

// 程序 Flash(pflash0)

memory pflash0 (tag="on-chip")

{

    mau = 8;

    type = rom;

    size = 2M;

    map cached    (dest=bus:sri, dest_offset=0x80000000, size=2M);

    map not_cached(dest=bus:sri, dest_offset=0xa0000000, size=2M, reserved);

}

【/代码】

:light_bulb: 英飞凌 TC 系列内存速查:

┃ - DSPR(Data Scratch Pad RAM):每个核的私有数据暂存区,速度最快

┃ - PSPR(Program Scratch Pad RAM):每个核的私有代码暂存区

┃ - LMU RAM(Local Memory Unit):可供多核共享的本地RAM

┃ - pflash / dflash:程序Flash / 数据Flash(均为 ROM 类型)

【内存 type 属性说明:】

type 含义 常见用途

rom 只读存储器 pflash,程序/常量存储
ram 随机存储器 dspr/pspr/lmuram,运行时数据/堆栈
reserved ram 保留RAM Scratchpad,仅手动指定的 group 可用
nvram 非易失RAM EMEM 等特殊存储器

━━━ 三、Section 命名规则------你必须要懂

在写 LSL 的 「select」 语句之前,必须搞清楚【节(Section)的命名规则】,否则选不到目标节,布局也就无从谈起。

● 3.1 Data Section 命名格式

【代码】

.data.<源文件名>.<变量名>

【/代码】

例如,「Demo_Tc27x_Debug.c」 中定义的 「DataTest」 变量,对应节名为:

【代码】

.data.Demo_Tc27x_Debug.DataTest

【/代码】

● 3.2 Program Section 命名格式

【代码】

.text.<源文件名>.<函数名>

【/代码】

例如,「Demo_Tc27x_Debug.c」 中的 「main」 函数,对应节名为:

【代码】

.text.Demo_Tc27x_Debug.main

【/代码】

【默认节前缀速查:】
节前缀 类型 默认位置

.text / .ztext 代码(far/near) Flash(rom)
.rodata / .zrodata 只读常量数据 Flash(rom)
.data / .zdata 已初始化全局变量 RAM
.bss / .zbss 未初始化全局变量 RAM(清零区)

┃ 注意: 带 `z` 前缀(如 `.zbss`、`.zdata`)是近端数据(Near Data),用于 near 寻址模式,地址范围有限但访问速度更快。

━━━ 四、section_layout------决定代码放在哪

section_layout 是 LSL 的灵魂,告诉链接器【每个节放在哪块内存里】。

● 4.1 常用关键属性

【代码】

run_addr   = address   运行时地址(也可以用 mem: 前缀引用 memory 名称)

load_addr  = address   加载地址(即在复制到运行地址前,初始存放地址,通常是 ROM)

ordered                以 group 中定义的排序方式定位 sections

contiguous             在某单个地址范围内随意布局,但会占用一段连续的内存空间

clustered              类似 contiguous,但在内存不足时会被分割开

fill                   避免其他 section 填补,用于填充对齐间隙

overlay                覆盖当前 section 到当前 run address(overlay 用法)

copy                   为 sections 创建一份 ROM 拷贝,启动时从 ROM 复制到 RAM

reserved               保留这些 section,不被链接器自动填充

【/代码】

【Section 属性(Attributes)速查:】

属性符含义

`r`|readable sections(可读)

`w`|writable sections(可写)

`x`|executable sections(可执行)

`i`|initialized sections(已初始化)

`b`|启动时需清零的 sections

`s`|scratch sections(不清零、不初始化)

`p`|protected sections(受保护)

:warning: 重要提示: 地址空间有 `linear`、`abs24`、`abs18`、`csa` 等类型,用户日常最常用的是 linear 地址空间。

● 4.2 基础 section_layout 示例

【LSL 代码】

section_layout :vtc:linear

{
  group CODE (ordered, run_addr = mem:mpe:pflash0)
  {
    select ".text";
    select ".rodata";
  }

  group DATA (ordered, run_addr = mem:mpe:dspr0)
  {
    select ".data";
    select ".bss";
  }
}

【/代码】

━━━ 五、实战案例------多核内存绑定

● 案例1:将变量放到指定 RAM 中

【需求:】 「Demo.c」 中定义了初始化变量 「RAMTestData」,要将其放到 「dspr2」(Core2 本地 RAM)中。

【LSL 代码】

section_layout :vtc:linear
{
  group DSPRDATA (ordered, run_addr = mem:mpe:dspr2) // 也可用绝对地址
  {
    select ".data.Demo_path.RAMTestData";
  }
}

【/代码】

:pushpin: `select` 支持通配符 `*`,例如 `select “.bss.*”` 可选取所有 bss 节。

● 案例2:将常量/函数放到指定 ROM 中

【需求:】 使用 「[#pragma](javascript::wink: section」 给常量改名,然后在 LSL 中将其定位到指定 Flash 地址。

【C 文件:】

【代码】

\[#pragma\](javascript:;) section all "sharedata"   // 将中间的 section 改名为 sharedata

const int con_near_data = 100;

\[#pragma\](javascript:;) section all restore

【/代码】

【LSL 代码】

section_layout :vtc:linear
{
  group ROMDATA (ordered, run_addr = 0x80000300)
  {
    select ".rodata.sharedata";
  }
}

【/代码】

● 案例3:Clone 变量------自动复制到所有核的 RAM

当一个变量需要在所有核中都有一份副本时,有两种写法:

【方法一:C 代码关键字】

【代码】

\__clone \__far \_*protect*\_ unsigned int Ramtest; // \__clone 关键字

【/代码】

【方法二:LSL 中显式克隆】

【LSL 代码】

section_setup mpe:vtc:linear
{
  modify input (space = mpe:vtc:tc0_linear|tc1_linear|tc2_linear, copy)
  {
    select ".bss.Demo_Tc27x_Debug.Ramtest";
  }
}

【/代码】

:pushpin: clone 变量常用于多核共享的状态标志、配置参数等场景。

● 案例4:Data Copy------ROM 中保存,RAM 中运行

【需求:】 变量 「Ramtest」 初始值存在 pflash0 的 0x100 偏移处,运行时复制到 dspr0 的 0x200 偏移处。

【代码】

\__far \_*protect*\_ unsigned int Ramtest = 0xf;

【/代码】

【LSL 代码】

section_layout :vtc:linear
{
  // 指定加载地址(ROM 中的位置)
  group MY_ROM_COPY_SECTIONS (ordered, load_addr=mem:mpe:pflash0\[0x100\])
  {
    select ".data.Demo_Tc27x_Debug.Ramtest";
  }

  // 指定运行地址(RAM 中的位置)
  group MY_INITIALIZED_SECTIONS (ordered, run_addr = mem:mpe:dspr0\[0x200\])
  {
    select ".data.Demo_Tc27x_Debug.Ramtest";
  }
}

【/代码】

:warning: 当 `load_addr ≠ run_addr` 时,TASKING 链接器会自动生成 copy table,启动代码在系统初始化时自动将内容从 ROM 复制到 RAM,不需要手动写拷贝代码。

━━━ 六、TASKING IDE 图形界面操作

不熟悉 LSL 语法时,可以先通过 IDE 图形界面操作,再查看生成的 LSL 文件来学习:

❶ 打开 【TASKING IDE → Project Properties → Processor】

❷ 在 【Memory 布局】 标签页:

○ 点击 【“Add”】 手动添加新的 Memory 段

○ 点击 【“Edit”】 修改已有 Memory 属性

○ 右侧按钮可一键选择 【Cached 】 或 【Not Cached】

❸ 所有修改自动写入项目的 「.lsl」 文件

:warning: 注意: 导入第三方 LSL 文件后,IDE 界面可以读取展示,但不能通过 IDE 界面修改------需直接编辑 LSL 文件。

━━━ 七、TASKING CCTC 中使用 LSL 的方法

● 方法1:命令行指定

【代码】

\# 单个 LSL 文件

cctc test.c -o test.elf --lsl-file=myboard.lsl

\# 多个 LSL 文件

cctc test.c -o test.elf \\

\--lsl-file=tc27x.lsl \\

\--lsl-file=extmem.lsl      # 扩展内存配置

【/代码】

● 方法2:Eclipse IDE 中配置

Project → Properties → C/C++ Build → Setting → Linker → LSL File

● 查看当前芯片的默认 LSL

TASKING 安装目录中预置了大量芯片 LSL 文件:

【代码】

<TASKING安装目录>/lib/tc<型号>.lsl

\# 例如:TC27x -> tc27x.lsl

【/代码】

━━━ 八、常见错误排查

● 错误1:Section 选不到

配置了 「select “.data.Demo.Ramtest”」 但链接器说找不到 section?

【原因:】 通常是section名写错了。要用如下方法确认节名:

【代码】

cctc --print-section-layout test.c > layout.txt

【/代码】

在 layout.txt 中查找实际的section名,以 「<源文件名>.<变量名>」 的格式核对。

● 错误2:E120 地址越界

【代码】

Error\[E120\]: section .text placed after section with required order

【/代码】

【原因:】 load_addr 超过了 memory 大小,或多个 group 地址重叠。

【解法:】 检查 LSL 中各 memory 的 「dest_offset + size」 是否越界。

● 错误3:E121 地址冲突

【代码】

Error\[E121\]: address conflict in group 'xxx'

【/代码】

【原因:】 多个 group 被分配到了同一个内存地址。

【解法:】 使用 「ordered」 属性确保排布顺序,或调整 「run_addr」 偏移。

━━━ 结语|LSL是链接器的"地图",会写才算真懂编译器 ━━━

LSL 脚本表面上是一堆配置,但背后反映的是工程师对芯片内存架构的深度理解。能写出正确的 LSL,意味着你知道:

○ Section 的命名规则,以及如何精准 「select」 到目标节

○ DSPR / PSPR / LMU RAM / pflash 各自的物理地址和用途

○ 如何把变量或函数绑定到指定核的专属内存

○ load_addr ≠ run_addr 时,copy table 是怎么工作的

【理解 LSL,是从"调用 API 的使用者"进化到"掌控系统的开发者"的标志。】

原创:ChanTech