━━━ 引言|为什么你写的代码跑不到你指定的位置?
很多工程师都有过这种经历:写了一个 「__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);
}
【/代码】
┃
英飞凌 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(受保护)
┃
重要提示: 地址空间有 `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";
}
}
【/代码】
┃
`select` 支持通配符 `*`,例如 `select “.bss.*”` 可选取所有 bss 节。
● 案例2:将常量/函数放到指定 ROM 中
【需求:】 使用 「[#pragma](javascript:
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";
}
}
【/代码】
┃
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";
}
}
【/代码】
┃
当 `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」 文件
┃
注意: 导入第三方 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