本系列文章链接


本文主要介绍C6455的二级boot的启动程序,参考文档为:

  之前没有注意一个问题,就是CCS的版本还有一个限制,我们用的仿真器是seed-xds560v2,它的文档里说明了,最高只支持CCS v6,所以我现在又换回了CCS v6.2.0的版本。

  上面是一个小插曲,下面进入正题。

  SPRUEC6G能够获取的有用信息并不多。启动模式由BOOTMODE[3:0]这几个引脚的复位时的状态决定。C6455内部有一块ROM存放了bootloader程序,会根据所配置的启动模式开启所需要的外设。这些外设在bootloader中的启用后,即使退出bootloader程序,它们也还是会保持启用的状态。然后它关于从EMIF启动的介绍也很少,只有说直接运行外部存储器中的代码。这个实际上的意思是,这个就要求接在CE3上的Flash要能够支持片上执行,只有NOR Flash可以,而NAND Flash做不到这一点。然后第6章,创建启动镜像也没有介绍从EMIF启动相关的内容,从I2C或者EMAC启动的话没有直接可以执行代码的条件,所以需要用到Boot Parameter Table、Boot Config Table。但如果直接从EMIF启动,就可以直接用汇编指令写启动代码,所以这部分内容暂时也没有参考价值。
  EMIF的二级Bootloader主要参考SPRA999A1这个文档。但这个文档也只能是参考,它并没有介绍C64x+架构的DSP的启动,而只是有C64x架构的相关介绍,所以文档中的和C6455的实际情况也有点出入。而且文档中大部分介绍的是带BIOS系统的二级启动,与我们现在刚开始的裸核程序也有一点区别。
  因为没有找到更多的和C6455的Boot相关的文档,而C64x架构的DSP内部ROM的第一级bootloader会先把外部存储器中的前1k地址空间的代码搬运到内部的address 0处开始运行,所以我们也可以认为C6455是先运行外部存储器中前1k地址空间内的代码,我们要做的就是把一段启动代码放到这1k地址空间中。这一段启动代码只能靠汇编编写,因为这时候C语言环境还没有建立。利用这段代码,我们将代码的主体部分搬运到L2空间,这样就让那些代码段的实际位置和run address吻合了。在退出bootloaer之后,程序进入“_c_int00”的程序入口,建立C语言环境,再进入main函数,开始执行主程序。
  SPRA999A1中的2.3节介绍了“Writing the Secondary Bootloader”,这里给的示例代码先对EMIF进行初始化,再根据boot table的格式搬运代码。因为C6455在配置为从EMIF启动后,EMIF已经由内部ROM初始化,所以我们不再需要对EMIF进行初始化。所以只需要后面那些“copy section”的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
       .list
.title "Flash bootup utility for 6455 dsk"
.option D,T
.length 102
.width 140

COPY_TABLE .equ 0xb0000400

.sect ".boot_load"
.global _boot
_boot:
;************************************************************************
;* Debug Loop - Comment out B for Normal Operation
;************************************************************************
zero B1
_myloop: ; [!B1] B _myloop
nop 5
_myloopend: nop

;****************************************************************************
;* Copy code sections
;****************************************************************************
mvkl COPY_TABLE, a3 ; load table pointer
mvkh COPY_TABLE, a3
ldw *a3++, b1 ; Load entry point

copy_section_top:
ldw *a3++, b0 ; byte count
ldw *a3++, a4 ; ram start address
nop 3

[!b0] b copy_done ; have we copied all sections?
nop 5

copy_loop:
ldb *a3++,b5
sub b0,1,b0 ; decrement counter
[ b0] b copy_loop ; setup branch if not done
[!b0] b copy_section_top
zero a1
[!b0] and 3,a3,a1
stb b5,*a4++
[!b0] and -4,a3,a5 ; round address up to next multiple of 4
[ a1] add 4,a5,a3 ; round address up to next multiple of 4

;****************************************************************************
;* Jump to entry point
;****************************************************************************
copy_done:
b .S2 b1
nop 5

  整个boot.asm的代码如上所示,代码不长,功能也很简单。
  要看懂这部分代码可以查阅SPRU732J的第三章,这一章专门介绍了指令集,3.11节有对所有指令的介绍。如果搜索不方便,可以在附录A,Instruction Compatibility中通过首字母索引的方式找到某个指令,并通过超链接跳转到详细介绍的页面。这些指令除了注意它的功能以外,还需要注意它的delay slot,可以通过它提供的例子比较好地理解。
  还有一些指令是汇编器的指令(Assembler Directives),就是以一个“.”开始的指令,比如上面的“.title”、“.option”之类的指令,可以在SPRU186W的第四章找到相关的说明。
  有一个非常建议的设置,就是可以打开“–asm_listing”的选项,这样在编译之后就会得到一个和源文件名称相同,后缀为“.lst”的list文件,好像是一个源文件对应一个lst文件。比如这里的这段启动代码,它的文件名是“boot.asm”,文件中加入了“.list”指令,然后如果也按照下面的流程进行了设置,在编译后就可以在Debug目录下得到一个“boot.lst”文件,里面是汇编器汇编后得到的机器码。这个文件在后面比对我们生成的boot table时非常有用。

  代码中的“.title”、“.option”、“.width”、“.length”都是和那个list文件相关的一些设置,没有很大作用,所以可以不用关心。

1
COPY_TABLE    .equ    0xb0000400

  指定了copy table 的首地址。具体的copy table是靠hex6x.exe这个工具生成的,主要在下一篇文章进行介绍。boot table 的格式在SPRUEC6G的6.2.2中介绍得比较详细。主要依次包括以下三个部分

  • 程序入口地址,在boot程序完成后,最后需要跳转的目的
  • 对于每个要COFF格式的section
    • section的字节数
    • 搬运的目的地址
    • 主体部分(代码或者数据)
  • 结束标识(0x00000000)

  “.sect”指定了这里的代码都是放在“.boot_load”这个段里。紧接着的“Debug Loop”明显是为了调试用的,对实际的功能没有影响。最后的“Jump to entry point”也很好理解,就是跳转到程序入口。所以主要就是“Copy code sections”中的内容。

1
2
3
4
ldw   *a3++, b1        ; Load entry point
ldw *a3++, b0 ; byte count
ldw *a3++, a4 ; ram start address
nop 3

  mvkh、mvkl指令将copy table的首地址赋给A3寄存器,A3相当于存储器访问的指针。B1寄存器存放了程序入口地址。B0寄存器存放的是某个section 的字节数,A4存放的是搬运某个section的目的首地址。代码中的nop指令都是为了满足ldw指令和跳转指令的delay slot需求才插入的。ldw指令的delay slot是4,跳转指令的delay slot是5。
  紧接着的是判断B0是否为0,若为0则跳转到copy done。也就是检测到copy table 的结束标识时就结束这里的循环。

1
2
3
4
5
6
7
8
9
10
copy_loop:
ldb *a3++,b5
sub b0,1,b0 ; decrement counter
[ b0] b copy_loop ; setup branch if not done
[!b0] b copy_section_top
zero a1
[!b0] and 3,a3,a1
stb b5,*a4++
[!b0] and -4,a3,a5 ; round address up to next multiple of 4
[ a1] add 4,a5,a3 ; round address up to next multiple of 4

  这里总共有内外两层循环,外层的是copy_section的循环,因为不只一个section需要copy;内层的是copy loop,是对每个字节的复制。
  我们可以分几种情况来讨论一下一上这段代码。ldb指令从外部存储器获取一个字节的数据存入B5寄存器中,这个需要再另外执行4条指令才能在B5寄存器中得到结果。sub指令是没有delay slot的,B0立即减一,然后对B0进行判断,如果B0不为零,那么接着继续在copy loop的小循环中;如果B0为零,就跳转到下一个section的copy。
  对于B0不为零的情况,从第4行依次向下数5条指令,可以发现程序会运行到第9行,但因为B0不为零,前面的带[!b0]的条件都不满足,所以对应的指令也就相当于是“nop”,这时只是用“stb”指令,完成复制一个字节的操作。
  对于B0为零的情况,从第5行依次向下数5条指令,可以发现程序会运行到第10行,而且前面的[!b0]条件都满足,对应指令也都会执行。这里的and指令的第一个操作数是有符号立即数,会进行符号位扩展,相当于检测每个section的大小是否是4字节的整数倍。这个在SPRUEC6G的6.2.2.3中的第8点也有体现。原文是这样的:

Correct any sections that are not multiples of 32 bits. The C compiler always generates sections whose lengths are multiples of 32 bits. This may not be the case for any sections declared in assembly. For little endian systems, the byte order must be swapped for these remaining bytes.

  所以这部分程序就是依次将boot table中的代码和数据逐字节地依次复制到了指定的位置,也就完成了boot loader的基本功能。
  今天写这个的时候又想到了一个问题,就是这个我们写的这部分加入到原本的整个工程中之后。其实这部分代码只运行一次,而且它是直接在外部存储器中运行的,所以它并不会在L2上。虽然我们在链接的cmd文件中将它分配到L2中,但实际运行起来的时候,L2的那部分空间并没有这些代码,所以是有这么1k的地址空间被浪费了。包括在仿真调试的时候,我们用仿真器将代码放到L2上,虽然这时候L2上的那段空间是有这部分启动代码的,但这部分代码永远也不会被执行,所以这1k的地址空间也是浪费了的。那么有没有什么办法再把这部分空间利用起来呢?好像在链接cmd文件的SECTION那部分有介绍用UNION声明来讲不同的段放到同一地址空间,不过现在还没到纠结那1k空间的地步,先就只在这里挖个坑。