16.操作外设及重定位基本原理


16.1.操作某个外设的套路
(1)看门狗定时器(watch-dog-timer)现实中因为一些外部因素,电子设备经常会跑飞或者死机(譬如极端炎热、极端寒冷、工业复杂场合),在这种情况下我们希望设备自动复位而不需要人工干预(无人值守),看门狗用来完成这个工作;看门狗是我们SoC内部的1个定时器,定好时间后看门狗定时器会去计时,时间到之前(狗饿了之前)必须去重新置位看门狗定时器(喂狗),如果没有喂狗则系统会被强制复位。
(2)系统在正常工作时,系统软件会自己去喂狗,所以看门狗定时器不会复位;但系统一旦故障跑飞啥的,看门狗就没人喂了,然后下一个周期就会自动复位,达到我们期望的效果。
(3)硬件物理特性->看门狗其实是个定时器(类似闹钟),硬件上就是SoC内部的某个内部外设;原理图->看门狗不用分析原理图,因为看门狗属于内部外设,且没有外部相关的元件与它有关,所以不需要原理图分析,原理图上根本找不到和看门狗有关的地方;数据手册->在数据手册的Section7.3部分,如果直接看不懂数据手册,可以百度看门狗,然后看别人的博客来学习。
(4)找到关键性操作SFR(特殊功能寄存器)->WTCON(0xE2700000),其中bit5是看门狗的开关(0代表关,1代表开);编写汇编代码。
(5)210中看门狗特性(iROM中已经关看门狗)->一般的CPU设计中,在CPU启动后看门狗默认是工作的(防止程序在启动代码前端就死机了或者跑飞了没人管),好处就是没有空当和漏洞,坏处就是在启动代码段我们不方便去喂狗(或者说懒得去喂狗)时看门狗会复位,所以为了偷懒我们就在启动代码前端先去关闭看门狗,然后在后面系统启动起来之后再根据需要决定是否要打开看门狗(一旦打开就必须同时提供喂狗)。
(6)在S5PV210内部的iROM代码(BL0)中,其实已经关过看门狗了,所以我们的启动代码实际上是不用去关也没事的,很多CPU内部是没有BL0的,因此也没人给你关看门狗,都要在启动代码前段自己写代码关看门狗。


16.2.设置栈和调用C语言
(1)C语言运行时需要和栈的意义->C语言运行时(runtime)需要一定的条件,这些条件由汇编来提供;C语言运行时主要是需要栈(C语言中的局部变量都是用栈来实现的);如果我们汇编部分没有给C部分预先设置合理合法的栈地址,则C代码中定义的局部变量就会落空,整个程序就OVER了。
(2)我们平时在编写单片机程序(譬如51单片机)或者编写应用程序时并没有去设置栈,但是C程序还是可以运行的;在单片机中由硬件初始化时提供了1个默认可用的栈;在应用程序中我们编写的C程序其实并不是全部,编译器(gcc)在链接的时候会帮我们自动添加1个头,该头就是1段引导我们的C程序能够执行的1段汇编实现的代码(该代码会设置C程序中需要的栈及其它运行时需要)。
(3)CPU模式和各种模式下的栈->在ARM中37个寄存器中,每种模式下都有自己的独立的SP寄存器(r13);如果各种模式都使用同1个SP,那么就意味着整个程序(操作系统内核程序+用户自己编写的应用程序)都是用1个栈的,你的应用程序一旦出错(譬如栈溢出),则会连累操作系统的栈也损坏,整个操作系统的程序就会崩溃,这种操作系统的设计是非常脆弱的;解决方案就是各种模式下用不同的栈,操作系统内核使用独立的栈,每个应用程序也使用独立的栈,这样就不会株连。
(4)我们现在要设置栈,不可能也懒的而且也没有必要去设置所有的栈,我们先要找到自己的模式,然后设置自己的模式下的栈到合理合法的位置即可(系统在复位后默认是进入SVC模式);正常我们应该先把CPU工作模式设置为SVC,再直接操作SP,但因为我们复位后就已经是SVC模式了,所以直接设置SP即可。
(5)栈必须是当前1段可用的内存(可用->该地方必须有被初始化过可以访问的内存,而且该段内存只会被我们用作栈,不会被其它程序征用),当前CPU刚复位(刚启动),外部的DRRAM尚未初始化,目前可用的内存只有内部的SRAM(它不需初始化即可使用),则我们只能在SRAM中找1段内存来作为SVC的栈。
(6)栈有四种->满减栈(进栈->先移动指针再存,指针向下移动;出栈->先出数据再移动指针,指针向上移动)+满增栈+空减栈+空增栈;在ARM中,ATPCS(ARM关于程序应该怎么实现的1个规范)要求使用满减栈,结合iROM_application_note中的memory-map(图1)可知SVC栈应该设置为0xd0037D80地址处。


16.3.汇编程序和C程序互相调用
(1)C函数的编写和被汇编调用->在工程中新建并且添加1个C语言源文件(led.c),注意添加后要修改Makefile,在汇编启动代码中设置好栈后,使用bl-xxx的方式来调用C中的函数xxx。
(2)使用C语言来访问寄存器的语法->寄存器的地址类似于内存地址(IO与内存统一编址),则用C语言读写寄存器<=>用C语言来读写内存地址,用C语言来访问内存,则要用到指针(((volatile-unsigned-int-)0x0xE0200240)=0x11111111;)。
(3)神奇的volatile->volatile的作用是让程序在编译时,编译器不对程序做优化,如果程序中某个变量是易变的,不希望编译器帮我们做优化,则在该变量定义时加volatile修饰;加不加有没有差别,取决于编译器,如果编译器做了优化则有差异,如果编译器本身没做优化,那就没有差别(测试编译器arm-2009q3时发现加不加效果相同)。
(4)编译报错(实际上是链接阶段报错)->undefined-reference-to-‘__aeabi_unwind_cpp_pr1’;在编译时添加-nostdlib该编译选项即可解决,nostdlib即不使用标准函数库,标准函数库是编译器中自带的函数库,用-nostdlib可以让编译器链接器优先选择我程序内自己写的函数库。


16.4.开启iCache
(1)cache是1种内存(高速缓存),从容量来说(CPU-小于-寄存器-小于-cache-小于-DDR),从速度来说(CPU-大于-寄存器-大于-cache-大于-DDR),cache的存在是因为寄存器和ddr之间速度差异太大,ddr的速度远不能满足寄存器的需要(更加不能满足cpu的需要,所以没有cache会拉低整个系统的整体速度);整个系统中CPU的供应链=寄存器+cache+DDR+硬盘/flash四阶组成,这是综合考虑了性能+成本后得到的妥协的结果;210内部有32KB的icache和32kb的dcache,icache是用来缓存指令的;dcache是用来缓存数据的。
(2)cache的意义=指令平时是放在硬盘/flash中的,运行时读取到DDR中,再从DDR中读给寄存器,再由寄存器送给cpu;但DDR的速度和寄存器(代表的就是CPU)的速度相差太大,如果CPU运行完1句再去DDR读取下1句,则CPU的速度完全就被DDR给拖慢了,则解决方案就是icache;icache工作时,会把我们CPU正在运行的指令的旁边几句指令事先给读取到icache中(CPU设计有1个基本原理->代码执行时,下一句执行当前1句代码旁边代码的可能性要大很多);当下1句CPU要指令时,cache首先检查自己事先准备的缓存指令中有没这句,如果有就直接拿给CPU,如果没有则需要从DDR中重新去读取拿给CPU,并同时做1系列的动作->清缓存+重新缓存。
(3)S5PV210中iROM中的BL0对cache的操作->icache的一切动作都是自动的,不需人为干预,我们所需要做的就是打开/关闭icache;在210的iROM中BL0已经打开了icache,则之前看到的现象都是icache打开时的现象。
(4)3种情况下的实验现象->直接使用BL0中对icache的操作(irom中确实是打开了icache);关icache(icache关闭确实比icache打开时led闪烁变慢,说明指令执行速度变慢);开icache(现象和”直接使用BL0中对icache的操作”相同)。


16.5.重定位和链接脚本引入
(1)大部分指令是位置有关编码;位置无关编码(PIC->position-independent-code)->汇编源文件被编码成二进制可执行程序时编码方式与位置(内存地址)无关;位置有关编码->汇编源码编码成二进制可执行程序时编码方式和内存地址有关;链接地址->链接时指定的地址(Makefile中用-Ttext/链接脚本);运行地址->程序实际运行时地址(由实际运行时被加载到内存的哪个位置说了算);相比而言,位置无关代码要好些,适应性强,放在哪里都能正常运行;位置有关代码就必须运行在链接时指定的地址上,适应性差;位置无关码有一些限制,不能完成所有功能,有时候不得不使用位置有关代码。
(2)链接地址和运行地址这两者可能相同也可能不同;对于位置有关代码来说,最终执行时的运行地址和编译链接时给定的链接地址必须相同,否则一定出错;我们之前的裸机程序中的Makefile中用”-Ttext-0x0”来指定链接地址是0x0,裸机程序实际上的运行时的地址是0xd0020010,这两个地址看似不同,但是实际相同,因为S5PV210内部做了映射,把SRAM映射到了0x0地址上去。
(3)再解S5PV210的启动过程(三星推荐的启动方式和uboot的具体实现方式不同);三星推荐的启动方式->bootloader必须小于96KB并大于16KB,假定bootloader为80KB,先开机上电后BL0运行,BL0会加载外部启动设备中的bootloader的前16KB(BL1)到SRAM中去运行,BL1运行时会加载BL2(bootloader中80-16=64KB)到SRAM中(从SRAM的16KB处开始运行)去运行,BL2运行时会初始化DDR并且将OS搬运到DDR去执行OS,整个初始化启动完成。uboot的具体实现方式->uboot大小随意,假定为200KB,先开机上电后BL0运行,BL0会加载外部启动设备中的uboot的前16KB(BL1)到SRAM中去运行,BL1运行时会初始化DDR,然后将整个uboot搬运到DDR中,然后用1句长跳转(从SRAM跳转到DDR)指令从SRAM中直接跳转到DDR中继续执行uboot直到uboot完全启动,uboot完全启动后在uboot命令行中去启动OS。
(4)重定位的意义->链接地址和运行地址有时候必须不相同,而且还不能全部用位置无关码,这时候就只能通过重定位实现;分散加载->把uboot分成2部分(BL1和整个uboot),两部分分别指定不同的链接地址,启动时将两部分加载到不同的地址(BL1加载到SRAM,整个uboot加载到DDR),这种情况下不用重定位也能启动OS;分散加载其实相当于手工重定位,重定位是用代码来进行重定位,分散加载是手工操作实现重定位的。


16.6.链接脚本详解
(1)运行时的地址是由运行时决定的(编译链接时是无法绝对确定运行时地址的);链接地址是由程序员在编译链接的过程中,通过Makefile中”-Ttext-xxx”或者在链接脚本中指定的,程序员事先会预知自己的程序的执行要求,并且有1个期望的执行地址,并且会用该地址来做链接地址。
(2)linux中的应用程序(gcc_hello.c_-o_hello),此时使用默认的链接地址就是0x0,则应用程序都是链接在0地址的,因为应用程序运行在操作系统的1个进程中,在该进程中该应用程序独享4G的虚拟地址空间,则该应用程序可以链接到0地址,因为每个进程都是从0地址开始的(编译时可以不给定链接地址而都使用0)。
(3)210中的裸机程序,运行地址由我们下载时确定,下载时下载到0xd0020010,所以就从这里开始运行(该下载地址不是我们随意定的,是iROM中的BL0加载BL1时事先指定好的地址,这是由CPU设计决定的),所以理论上我们编译链接时应该将地址指定到0xd0020010,但是实际上我们的裸机程序中都是使用位置无关码PIC,所以链接地址可以是0。
(4)链接脚本其实是个规则文件,其是程序员用来指挥链接器工作的,链接器会参考链接脚本,并且使用其中规定的规则来处理.o文件中那些段,将其链接成1个可执行程序,链接脚本的关键内容=段名+地址(作为链接地址的内存地址)(”SECTIONS{}”->是整个链接脚本;”.”->点号在链接脚本中代表当前位置;”=”->等号代表赋值)。


16.7.源码和bin镜像文件及程序段
(1)从源码到可执行程序的步骤->预编译(预编译器执行->譬如C中的宏定义就是由预编译器处理,注释等也是由预编译器处理)+编译(编译器来执行->把源码.c和.S变成机器码.o文件)+链接(链接器来执行->把.o文件中的各函数(段)按照一定规则(链接脚本来指定)累积在一起形成可执行文件)+strip(strip是把可执行程序中的符号信息给拿掉,以节省空间,分为Debug版本和Release版本)+objcopy(由可执行程序生成可烧录的镜像bin文件)。
(2)程序段=代码段+数据段+bss段(ZI段)+自定义段;段就是程序的一部分,我们把整个程序的所有东西分成了1个1个的段,给每个段起个名字,然后在链接时就可以通过段名来让段分配在合适的位置上;段名=编译器链接器内部定好的先天性的名字+程序员自己指定的自定义的段名。
(3)先天性段名=代码段(.text)(文本段)->函数编译后生成的东西;数据段(.data)->C语言中有显式初始化为非0的全局变量;bss段(.bss)(ZI(zero-initial)段)->零初始化段,对应C语言中初始化为0的全局变量。后天性段名=段名由程序员自己定义,段的属性和特征也由程序员自己定义。
(4)C语言中全局变量如果未显式初始化,值是0->本质就是C语言把这类全局变量放在了bss段,从而保证了为0;C运行时环境如何保证显式初始化为非0的全局变量的值在main之前就被赋值了->因为C语言把这类变量放在了.data段中,而.data段会在main执行之前被处理(初始化)。


16.操作外设及重定位基本原理_第1张图片


16.disable_wdt/readme.txt
xxx_usb.bin是usb启动下载的镜像
xxx_sd.bin是SD卡启动下载的镜像

项目名:disable_wdt
作  者:Rston
博客:http://blog.csdn.net/rston
GitHub:https://github.com/rston
描  述:在代码启动阶段使用汇编关闭看门狗 
16.disable_wdt/Makefile
# 基于210裸机项目的Makefile模板

name:=disable_wdt
$(name).bin:$(name).o 
    arm-linux-ld -Ttext 0x0 -o $(name).elf $^
    arm-linux-objcopy -O binary $(name).elf $(name)_usb.bin
    arm-linux-objdump -D $(name).elf > $(name)_elf.dis
    gcc mkv210_image.c -o mkgec210
    ./mkgec210 $(name)_usb.bin $(name)_sd.bin

%.o : %.S
    arm-linux-gcc -o $@ $< -c

%.o : %.c
    arm-linux-gcc -o $@ $< -c 

clean:
    rm *.o *.elf *.bin *.dis mkgec210 -f
16.disable_wdt/disable_wdt.S
/*
 * 公司:XXXX
 * 作者:Rston
 * 博客:http://blog.csdn.net/rston
 * GitHub:https://github.com/rston
 * 项目:操作外设及重定位基本原理
 * 功能:在代码启动阶段使用汇编关闭看门狗。
 */

#define GPJ2CON 0xE0200280
#define GPJ2DAT 0xE0200284
#define WTCON   0xE2700000

.global _start                      // 把_start链接属性改为外部,则外部其它文件可看见_start 
_start:
    // 关闭看门狗
    ldr r0, =WTCON
    ldr r1, =0x0
    str r1, [r0]

    // 以下为功能代码段,实现4个流水灯功能
    // 第1步:将0x00001111写入0xE0200280位置(GPJ2CON)
    // 即设置GPJ2CON0~GPJ2CON3共4个引脚为输出模式
    ldr r0, =((1<<0) | (1<<4) | (1<<8) | (1<<12))   
    ldr r1, =GPJ2CON                // 通过=号看出使用的是ldr伪指令,因为编译器可判断该立即数
    str r0, [r1]                    // 是合法立即数/非法立即数。
                                    // 寄存器间接寻址,把r0中的数写入到以r1中的数为地址的内存中去
flash:  
    // 第2步:点亮LED0
    ldr r0, =~(1<<0)
    ldr r1, =GPJ2DAT
    str r0, [r1]
    bl delay                        

    // 第3步:点亮LED1
    ldr r0, =~(1<<1)
    ldr r1, =GPJ2DAT
    str r0, [r1]
    bl delay                        

    // 第4步:点亮LED2
    ldr r0, =~(1<<2)
    ldr r1, =GPJ2DAT
    str r0, [r1]
    bl delay                        

    // 第3步:点亮LED3
    ldr r0, =~(1<<3)
    ldr r1, =GPJ2DAT
    str r0, [r1]
    bl delay                        // 使用bl调用延时函数,会将返回地址保存在lr寄存器中

    b flash                         // 继续循环,使用b跳转,不保存返回地址到lr寄存器中

// 延时函数,函数名为delay
delay:
    ldr r2, =9000000
    ldr r3, =0x0
delay_loop:
    sub r2, r2, #1                  // r2 = r2 - 1
    cmp r2, r3                      // cmp会影响CPSR中的Z标志位,若r2等于r3,则Z标志位置1,下句中的bne不成立
    bne delay_loop
    mov pc, lr                      // 函数调用返回
16.disable_wdt/write2sd.sh
#!/bin/sh
sudo dd iflag=dsync oflag=dsync if=disable_wdt_sd.bin of=/dev/sdb seek=1

16.set_svc_stack/readme.txt
xxx_usb.bin是usb启动下载的镜像
xxx_sd.bin是SD卡启动下载的镜像

项目名:set_svc_stack
作  者:Rston
博客:http://blog.csdn.net/rston
GitHub:https://github.com/rston
描  述:通过汇编设置SVC栈     
16.set_svc_stack/set_svc_stack.S
/*
 * 公司:XXXX
 * 作者:Rston
 * 博客:http://blog.csdn.net/rston
 * GitHub:https://github.com/rston
 * 项目:操作外设及重定位基本原理
 * 功能:通过汇编设置SVC栈。
 */

#define GPJ2CON     0xE0200280
#define GPJ2DAT     0xE0200284
#define WTCON       0xE2700000
#define SVC_STACK   0xD0037D80

.global _start                      // 把_start链接属性改为外部,则外部其它文件可看见_start 
_start:
    // 关闭看门狗
    ldr r0, =WTCON
    ldr r1, =0x0
    str r1, [r0]

    // 设置SVC_STACK栈
    ldr sp, =SVC_STACK

    // 以下为功能代码段,实现4个流水灯功能
    // 第1步:将0x00001111写入0xE0200280位置(GPJ2CON)
    // 即设置GPJ2CON0~GPJ2CON3共4个引脚为输出模式
    ldr r0, =((1<<0) | (1<<4) | (1<<8) | (1<<12))   
    ldr r1, =GPJ2CON                // 通过=号看出使用的是ldr伪指令,因为编译器可判断该立即数
    str r0, [r1]                    // 是合法立即数/非法立即数。
                                    // 寄存器间接寻址,把r0中的数写入到以r1中的数为地址的内存中去
flash:  
    // 第2步:点亮LED0
    ldr r0, =~(1<<0)
    ldr r1, =GPJ2DAT
    str r0, [r1]
    bl delay                        

    // 第3步:点亮LED1
    ldr r0, =~(1<<1)
    ldr r1, =GPJ2DAT
    str r0, [r1]
    bl delay                        

    // 第4步:点亮LED2
    ldr r0, =~(1<<2)
    ldr r1, =GPJ2DAT
    str r0, [r1]
    bl delay                        

    // 第3步:点亮LED3
    ldr r0, =~(1<<3)
    ldr r1, =GPJ2DAT
    str r0, [r1]
    bl delay                        // 使用bl调用延时函数,会将返回地址保存在lr寄存器中

    b flash                         // 继续循环,使用b跳转,不保存返回地址到lr寄存器中

// 延时函数,函数名为delay
delay:
    ldr r2, =9000000
    ldr r3, =0x0
delay_loop:
    sub r2, r2, #1                  // r2 = r2 - 1
    cmp r2, r3                      // cmp会影响CPSR中的Z标志位,若r2等于r3,则Z标志位置1,下句中的bne不成立
    bne delay_loop
    mov pc, lr                      // 函数调用返回

16.asm_call_c/readme.txt
xxx_usb.bin是usb启动下载的镜像
xxx_sd.bin是SD卡启动下载的镜像

项目名:asm_call_c
作  者:Rston
博客:http://blog.csdn.net/rston
GitHub:https://github.com/rston
描  述:由汇编语言调用C语言
16.asm_call_c/Makefile
# 基于210裸机项目的Makefile模板

name:=asm_call_c
$(name).bin:start.o water_lights.o  
    arm-linux-ld -Ttext 0x0 -o $(name).elf $^
    arm-linux-objcopy -O binary $(name).elf $(name)_usb.bin
    arm-linux-objdump -D $(name).elf > $(name)_elf.dis
    gcc mkv210_image.c -o mkgec210
    ./mkgec210 $(name)_usb.bin $(name)_sd.bin

%.o : %.S
    arm-linux-gcc -o $@ $< -c -nostdlib

%.o : %.c
    arm-linux-gcc -o $@ $< -c -nostdlib

clean:
    rm *.o *.elf *.bin *.dis mkgec210 -f
16.asm_call_c/write2sd.sh
#!/bin/sh
sudo dd iflag=dsync oflag=dsync if=asm_call_c_sd.bin of=/dev/sdb seek=1
16.asm_call_c/start.S
/*
 * 公司:XXXX
 * 作者:Rston
 * 博客:http://blog.csdn.net/rston
 * GitHub:https://github.com/rston
 * 项目:操作外设及重定位基本原理
 * 功能:由汇编语言调用C语言。
 */

#define WTCON       0xE2700000
#define SVC_STACK   0xD0037D80

.global _start                      // 把_start链接属性改为外部,则外部其它文件可看见_start 
_start:
    // 关闭看门狗
    ldr r0, =WTCON
    ldr r1, =0x0
    str r1, [r0]

    // 设置SVC_STACK栈
    ldr sp, =SVC_STACK

    // 调用C程序
    bl water_lights

    // 汇编最后的这个死循环绝对不能丢
    b .
16.asm_call_c/water_lights.h
/*
 * 公司:XXXX
 * 作者:Rston
 * 博客:http://blog.csdn.net/rston
 * GitHub:https://github.com/rston
 * 项目:操作外设及重定位基本原理
 * 功能:C语言实现4个LED流水灯效果。  
 */

// 宏定义寄存器地址空间
#define rGPJ2CON    (*((volatile unsigned int *)0xE0200280))
#define rGPJ2DAT    (*((volatile unsigned int *)0xE0200284))

// 相关函数声明
void delay(void);
void water_lights(void);
16.asm_call_c/water_lights.c
/*
 * 公司:XXXX
 * 作者:Rston
 * 博客:http://blog.csdn.net/rston
 * GitHub:https://github.com/rston
 * 项目:操作外设及重定位基本原理
 * 功能:C语言实现4个LED流水灯效果。  
 */
#include "water_lights.h"

void delay(void)
{
    volatile unsigned int i = 1000000;      // volatile让编译器不要优化,才能真正的减
    while (i--);                            // 才能确实的消耗时间,实现delay
}

void water_lights(void)
{
    // 设置GPJ2CON0~GPJ2CON3共4个引脚为输出模式
    rGPJ2CON = ((1<<0) | (1<<4) | (1<<8) | (1<<12));

    // 实现LED流水灯功能
    while (1)
    {
        rGPJ2DAT = ~(1<<0);
        delay();

        rGPJ2DAT = ~(1<<1);
        delay();

        rGPJ2DAT = ~(1<<2);
        delay();

        rGPJ2DAT = ~(1<<3);
        delay();
    }
}

16.icache/readme.txt
xxx_usb.bin是usb启动下载的镜像
xxx_sd.bin是SD卡启动下载的镜像

项目名:asm_call_c
作  者:Rston
博客:http://blog.csdn.net/rston
GitHub:https://github.com/rston
描  述:由汇编实现关闭或打开icache
16.icache/start.S
/*
 * 公司:XXXX
 * 作者:Rston
 * 博客:http://blog.csdn.net/rston
 * GitHub:https://github.com/rston
 * 项目:操作外设及重定位基本原理
 * 功能:由汇编实现关闭或打开icache。
 */

#define WTCON       0xE2700000
#define SVC_STACK   0xD0037D80

.global _start                      // 把_start链接属性改为外部,则外部其它文件可看见_start 
_start:
    // 关闭看门狗
    ldr r0, =WTCON
    ldr r1, =0x0
    str r1, [r0]

    // 设置SVC_STACK栈
    ldr sp, =SVC_STACK

    // 关闭或开的icache
    mrc p15,0,r0,c1,c0,0;   // 读出协处理器C1 
    bic r0, r0, #(1<<12)    // 第12位置0,关闭icache
    //orr r0, r0, #(1<<12)  // 第12位置1,打开icache
    mcr p15,0,r0,c1,c0,0;   // 给C1赋值

    // 调用C程序
    bl water_lights

    // 汇编最后的这个死循环绝对不能丢
    b .

16.link.lds
SECTIONS
{
    . = 0xd0024000;

    .text : {
        start.o
        * (.text)
    }

    .data : {
        * (.data)
    }

    bss_start = .;

    .bss : {
        * (.bss)
    }

    bss_end = .;
}

你可能感兴趣的:(arm裸机)