前言
基于 100ask_imx6ull_pro 开发板
使用 100ASK_IMX6ULL_Flashing_tool 工具进行程序烧写
参考资料为《嵌入式 Linux 应用开发完全手册(第 2 版)》 -> 裸机开发 -> 重定位部分
手册中已经介绍了如何重定位 data 数据段以及全部代码的示例。
因此,本文不再赘述这些基础知识,而是专注于手册中未提到的内容:如何实现函数的重定位。
为什么需要函数重定位
-
由于 MCU 的 SRAM 资源有限,代码通常直接从 norFlash 中读取并运行
然而,在执行 OTA 升级时,需要对 Flash 进行擦写操作,而 Flash 通常不支持 RWW(Read While Write)
为了解决这一冲突,需要将擦写 Flash 的函数搬运到 SRAM 中执行
-
由于 SRAM 速度更快,可以将某些热点函数搬运至 SRAM 中运行,以提高程序的执行效率。
启动分析
在 IMX6ULL 芯片手册中,我们了解到,芯片在上电时,内部固化的 boot ROM 程序会通过外部引脚确定启动方式(如 USB、NAND、eMMC、SD 等)。接着,boot ROM 通过相应的启动接口获取所需的信息,例如如何初始化 DDR、如何搬运程序、以及将程序搬运到哪个内存位置等。随后,boot ROM 会自动将应用的二进制数据(如 app.bin
)从存储介质(如 NAND、eMMC、SD 等)搬运到 DDR 的指定位置,并在该位置开始执行程序。
整个过程由芯片自动完成,但需要提前生成并烧录到存储介质的映像文件。首先,通过编译得到应用的二进制文件 app.bin
(这可以是裸机应用固件或 Linux 固件等)。然后,使用 mkimage
工具(该工具附带在 gcc-arm-linux-gnueabihf-6.2.1
编译器中)根据 imximage.cfg.cfgtmp
文件中的信息生成头部信息,再将头部信息与 app.bin
组合生成 .imx
文件。
如果使用 SD 卡启动,则根据下表所示,.imx
文件的头部还需要添加 1KB 的偏移量,生成 .img
文件,才能将其烧录到 SD 卡中。
由于本次实验采用的是 USB 启动,因此不需要添加偏移量。
代码示例
为了程序简化,便于分析,以下只保留必要的代码。通过对延时函数 delay 的重定位与否来观察 LED 的闪烁频率变化
工程结构
│ ├── tools
│ │ ├── 1k.bin
│ │ ├── imx6ull.lds
│ │ ├── imximage.cfg.cfgtmp
│ ├── main.c
│ ├── main.dis
│ ├── main.imx
│ ├── Makefile
│ ├── start.S
启动文件 start.S
这里只设置了 堆栈指针,为 C 语言提供运行环境,随后跳转到 main 函数
.text
.align 2
.global _start
_start:
ldr sp,=0x80100000
bl main
主程序文件 main.c
delay
函数通过大量循环来实现延时。函数前的 __attribute__((section(".delay")))
修饰符将 delay
函数归入 .delay
段,使其与 .text
、.data
等段并列,供链接脚本使用。
copy_data
函数负责将 delay
函数的代码段从加载地址(存储地址)复制到链接地址(运行地址),其中 _delay_start_addr
等符号是在链接脚本中定义的。
led_init
函数用于初始化 LED,其具体实现可以参考手册的相关章节。在主循环中,通过不断改变 LED 的亮灭状态,可以观察 delay
函数的运行快慢。
#define CCM_CCGR1 (volatile unsigned long *)0x20C406C
#define IOMUXC_SW_MUX_CTL_PAD_GPIO5_IO03 (volatile unsigned long *)0x2290014
#define IOMUXC_SW_PAD_CTL_PAD_GPIO5_IO03 (volatile unsigned long *)0x2290058
#define GPIO5_GDIR (volatile unsigned long *)0x020AC004
#define GPIO5_DR (volatile unsigned long *)0x020AC000
#define uint32_t unsigned int
void copy_data();
void led_init();
void __attribute__((section(".delay"))) delay(uint32_t second);
int main()
{
copy_data();
led_init();
while (1)
{
*(GPIO5_DR) = 0x0;
delay(50);
*(GPIO5_DR) = (1 << 3);
delay(50);
}
return 0;
}
void led_init()
{
*(CCM_CCGR1) = 0xFFFFFFFF; // 开启GPIO1的时钟
*(IOMUXC_SW_MUX_CTL_PAD_GPIO5_IO03) = 0x5; // 设置PAD复用功能为GPIO
*(IOMUXC_SW_PAD_CTL_PAD_GPIO5_IO03) = 0x1F838; // 设置PAD属性
*(GPIO5_GDIR) = (1 << 3); // 设置GPIO为输出模式
*(GPIO5_DR) = 0x0; // 设置输出电平为低电平
}
void __attribute__((section(".delay"))) delay(uint32_t second)
{
uint32_t count = 0xFFFFF * second;
volatile uint32_t i = 0;
for (i = 0; i < count; ++i)
{
__asm("NOP"); /* 调用nop空指令 */
}
}
void copy_data()
{
extern unsigned long _delay_start_addr;
extern unsigned long _delay_end_addr;
extern unsigned long _delay_at_start_addr;
unsigned long *src, *dst;
src = &_delay_at_start_addr;
dst = &_delay_start_addr;
while (dst < &_delay_end_addr)
{
*dst++ = *src++;
}
}
链接脚本 imx6ull.lds
IMX6ULL 具有 128KB 的片内 RAM,在本次实验中,我们将 delay
函数从 DDR 搬运到 OCRAM。
_delay_at_start_addr
保存的是延迟段的加载地址(存储地址),即搬运操作的起始位置。
.delay 0x00900000:
.delay
是一个自定义的段名。在 main
文件中,我们将 delay
函数分配到了 .delay
段。因此,编译后 delay
函数会被放入 .delay
段中。0x00900000
是该段在内存中的运行时地址。程序正常执行时,只有当这段代码或数据放置在此地址处才会运行正确。
AT(_delay_at_start_addr):
AT
是一个关键字,用于指定段的加载地址。加载地址是指程序被加载到内存时该段将被放置的位置。_delay_at_start_addr
是一个变量,表示 .delay
段的加载地址,定义了该段在程序加载到内存时应该被放置的位置。
SECTIONS {
. = 0x80100000; /* 起始地址 */
. = ALIGN(4);
.text :
{
*(.text) /* 主程序代码 */
}
. = ALIGN(4);
.data :
{
*(.data) /* 已初始化的数据 */
}
. = ALIGN(4);
.bss :
{
*(.bss) /* 未初始化的数据 */
}
. = ALIGN(4);
_delay_at_start_addr = .; /* 记录延迟段的装载地址 */
.delay 0x00900000 : AT(_delay_at_start_addr)
{
_delay_start_addr = .; /* 延迟段的起始地址 */
*(.delay) /* 延迟段内容 */
_delay_end_addr = .; /* 延迟段的结束地址 */
}
}
Makefile
mkimage -n ./tools/imximage.cfg.cfgtmp -T imximage -e 0x80100000 -d main.bin main.imx
这一条命令使用 mkimage
工具,将 boot ROM
所需的信息与 main.bin
结合起来生成符合 imx6ull
启动规范的 imx
文件
其中 -e 0x80100000
参数指示 boot ROM
将 main.bin
的内容搬运至 DDR
的 0x80100000
地址处,同时 CPU
从该地址处开始运行用户程序
PREFIX=arm-linux-gnueabihf-
CC=$(PREFIX)gcc
LD=$(PREFIX)ld
AR=$(PREFIX)ar
OBJCOPY=$(PREFIX)objcopy
OBJDUMP=$(PREFIX)objdump
main.imx : main.c start.S ./tools/imx6ull.lds
$(CC) -nostdlib -g -c -o start.o start.S
$(CC) -nostdlib -g -c -o main.o main.c
$(LD) -T ./tools/imx6ull.lds -g start.o main.o -o main.elf
$(OBJCOPY) -O binary -S main.elf main.bin
$(OBJCOPY) -O ihex -S main.elf main.hex
$(OBJDUMP) -D -m arm main.elf > main.dis
mkimage -n ./tools/imximage.cfg.cfgtmp -T imximage -e 0x80100000 -d main.bin main.imx
rm -f *.o main.elf main.bin
clean:
rm -f main.dis main.imx main.hex
调试分析
在工程文件准备完毕后,执行 make
命令生成了 main.dis
、main.imx
和 main.hex
文件。
-
main.imx
是目标文件,通过 USB 启动方式下载到开发板后可进行测试运行。 -
main.hex
文件包含程序的二进制数据及地址信息。使用工具查看该文件,可以发现段与段之间的存储地址是连续的。
-
main.dis
是反汇编文件,以下我们将截取关键部分进行分析:
程序的第一条指令位于 0x80100000
,对应于 start.S
中定义的 _start
函数。设置好堆栈后,程序跳转到 main
函数。
在 main
函数中,首先执行了 copy_data
函数,跳转至 0x8010008c
处。这里需要注意的是,在函数重定位之前只能执行地址无关码(即编译成汇编代码后不依赖绝对地址的指令,因此需要使用地址无关码进行重定位搬运操作)
通过 copy_data
函数,我们可以得知:
-
_delay_start_addr
的地址为0x90000000
-
_delay_end_addr
的地址为0x90000038
-
_delay_at_start_addr
的地址为0x8010e0
也就是说,.delay
段的长度为 0x38
。我们的目标是将 .delay
段的代码搬运至 OCRAM 的 0x90000000-0x90000038
区域,这正是 copy_data
函数所完成的任务。
接下来是 LED 的初始化,通过设置 DR 寄存器来控制输出电平的高低,从而实现 LED 的亮灭。
在 main
函数调用 delay
函数时,程序首先跳转到 __delay_veneer
,再从 __delay_veneer
跳转至 0x00900000
处执行搬运后的 delay
函数。
如果没有进行搬运操作,当程序运行到此处时会跑飞出错。
main.elf: file format elf32-littlearm
Disassembly of section .text:
80100000 <_start>:
80100000: e51fd000 ldr sp, [pc, #-0] ; 80100008 <_start+0x8>
80100004: fa000000 blx 8010000c <main>
80100008: 80100000 andshi r0, r0, r0
8010000c <main>:
8010000c: b580 push {r7, lr}
8010000e: af00 add r7, sp, #0
80100010: f000 f83c bl 8010008c <copy_data>
80100014: f000 f813 bl 8010003e <led_init>
80100018: f44f 4340 mov.w r3, #49152 ; 0xc000
8010001c: f2c0 230a movt r3, #522 ; 0x20a
80100020: 2200 movs r2, #0
80100022: 601a str r2, [r3, #0]
80100024: 2032 movs r0, #50 ; 0x32
80100026: f000 e858 blx 801000d8 <__delay_veneer>
8010002a: f44f 4340 mov.w r3, #49152 ; 0xc000
8010002e: f2c0 230a movt r3, #522 ; 0x20a
80100032: 2208 movs r2, #8
80100034: 601a str r2, [r3, #0]
80100036: 2032 movs r0, #50 ; 0x32
80100038: f000 e84e blx 801000d8 <__delay_veneer>
8010003c: e7ec b.n 80100018 <main+0xc>
8010003e <led_init>:
8010003e: b480 push {r7}
80100040: af00 add r7, sp, #0
80100042: f244 036c movw r3, #16492 ; 0x406c
80100046: f2c0 230c movt r3, #524 ; 0x20c
8010004a: f04f 32ff mov.w r2, #4294967295
8010004e: 601a str r2, [r3, #0]
80100050: 2314 movs r3, #20
80100052: f2c0 2329 movt r3, #553 ; 0x229
80100056: 2205 movs r2, #5
80100058: 601a str r2, [r3, #0]
8010005a: 2358 movs r3, #88 ; 0x58
8010005c: f2c0 2329 movt r3, #553 ; 0x229
80100060: f64f 0238 movw r2, #63544 ; 0xf838
80100064: f2c0 0201 movt r2, #1
80100068: 601a str r2, [r3, #0]
8010006a: f24c 0304 movw r3, #49156 ; 0xc004
8010006e: f2c0 230a movt r3, #522 ; 0x20a
80100072: 2208 movs r2, #8
80100074: 601a str r2, [r3, #0]
80100076: f44f 4340 mov.w r3, #49152 ; 0xc000
8010007a: f2c0 230a movt r3, #522 ; 0x20a
8010007e: 2200 movs r2, #0
80100080: 601a str r2, [r3, #0]
80100082: bf00 nop
80100084: 46bd mov sp, r7
80100086: f85d 7b04 ldr.w r7, [sp], #4
8010008a: 4770 bx lr
8010008c <copy_data>:
8010008c: b480 push {r7}
8010008e: b083 sub sp, #12
80100090: af00 add r7, sp, #0
80100092: f240 03e0 movw r3, #224 ; 0xe0
80100096: f2c8 0310 movt r3, #32784 ; 0x8010
8010009a: 607b str r3, [r7, #4]
8010009c: f240 0300 movw r3, #0
801000a0: f2c0 0390 movt r3, #144 ; 0x90
801000a4: 603b str r3, [r7, #0]
801000a6: e007 b.n 801000b8 <copy_data+0x2c>
801000a8: 683b ldr r3, [r7, #0]
801000aa: 1d1a adds r2, r3, #4
801000ac: 603a str r2, [r7, #0]
801000ae: 687a ldr r2, [r7, #4]
801000b0: 1d11 adds r1, r2, #4
801000b2: 6079 str r1, [r7, #4]
801000b4: 6812 ldr r2, [r2, #0]
801000b6: 601a str r2, [r3, #0]
801000b8: 683a ldr r2, [r7, #0]
801000ba: f240 0338 movw r3, #56 ; 0x38
801000be: f2c0 0390 movt r3, #144 ; 0x90
801000c2: 429a cmp r2, r3
801000c4: d3f0 bcc.n 801000a8 <copy_data+0x1c>
801000c6: bf00 nop
801000c8: 370c adds r7, #12
801000ca: 46bd mov sp, r7
801000cc: f85d 7b04 ldr.w r7, [sp], #4
801000d0: 4770 bx lr
801000d2: 0000 movs r0, r0
801000d4: 0000 movs r0, r0
...
801000d8 <__delay_veneer>:
801000d8: e51ff004 ldr pc, [pc, #-4] ; 801000dc <__delay_veneer+0x4>
801000dc: 00900001 addseq r0, r0, r1
Disassembly of section .delay:
00900000 <delay>:
900000: b480 push {r7}
900002: b085 sub sp, #20
900004: af00 add r7, sp, #0
900006: 6078 str r0, [r7, #4]
900008: 687a ldr r2, [r7, #4]
90000a: 4613 mov r3, r2
90000c: 051b lsls r3, r3, #20
90000e: 1a9b subs r3, r3, r2
900010: 60fb str r3, [r7, #12]
900012: 2300 movs r3, #0
900014: 60bb str r3, [r7, #8]
900016: 2300 movs r3, #0
900018: 60bb str r3, [r7, #8]
90001a: e003 b.n 900024 <delay+0x24>
90001c: bf00 nop
90001e: 68bb ldr r3, [r7, #8]
900020: 3301 adds r3, #1
900022: 60bb str r3, [r7, #8]
900024: 68ba ldr r2, [r7, #8]
900026: 68fb ldr r3, [r7, #12]
900028: 429a cmp r2, r3
90002a: d3f7 bcc.n 90001c <delay+0x1c>
90002c: bf00 nop
90002e: 3714 adds r7, #20
900030: 46bd mov sp, r7
900032: f85d 7b04 ldr.w r7, [sp], #4
900036: 4770 bx lr
实验结果
将生成的 main.imx
文件通过 USB 烧录到开发板后,可以观察到板载 LED 灯大约每 45 秒切换一次亮灭。
如果去除代码搬运部分,直接使用存放在 DDR 中的 delay
函数,并将其烧录到开发板后,板载 LED 灯大约每 50 秒切换一次亮灭。
经过重定位的 delay
函数使 LED 切换速度加快了约 5 秒,由于 DDR 和片内 RAM 的速度差异并不显著,而且延迟函数中读写次数的敏感性较低,因此表现出来的效果并不十分明显。
撰写一篇原创文章不容易,还请大家多多点赞收藏支持一下!有什么问题也可以在评论区讨论,共同进步。