从官方文档TN238中,我们知道了HC9S12X微控制器有直接寻址区这个概念,并且由它的示例得知访问直接寻址区的变量只需要一个字节的地址;嗯,比常用的扩展寻址区省了一个字节,很不错的特性,但是它并没有说清楚定义并访问直接寻址区的整个步骤。
后来,发现自带的代码示例里有DirectData这个示例工程,哇,赶快开始学习。使用示例工程的时候十分成功,但是当想在自己的代码上使用直接寻址区的变量时就出问题了,只要uCOS-II一切换任务,程序就直接跑飞。搞了我好久。好在终于明白怎么回事了。来给大家分享下学习成果。
定义变量到直接寻址区
基础知识
我们知道S12X单片机是16位单片机,正常访问内存地址的时候需要使用16位地址。但是访问直接寻址区(或说,使用直接寻址模式)时却只需要8位地址,那另外8位哪去了呢?其实另外(高)8位是由DIRECT寄存器控制的,比如现在DIRECT寄存器中的值是0x30,然后你访问0x34处的变量其实就是在访问0x3034。
修改prm文件
从另一个角度来看,因为直接寻址只用到了8位地址,所以必须得把所有要直接寻址的变量都定义在某个0xXX00-0xXXFF地址内,只可以少不可以多。为了实现这一步,需要修改prm文件。
下面假设我要把直接寻址区定在0x2000-0x20FF。
打开prm文件,把原来的:
SEGMENTS
...
RAM = READ_WRITE DATA_NEAR 0x2000 TO 0x3FFF;
...
END
PLACEMENT
...
END
修改为:
SEGMENTS
...
RAM_DIRECT = READ_WRITE DATA_NEAR 0x2000 TO 0x20FF;
RAM = READ_WRITE DATA_NEAR 0x2100 TO 0x3FFF;
...
END
PLACEMENT
...
DIRECT_DATA INTO RAM_DIRECT;
...
END
数据定义和声明
对所有要定义在直接寻址区的变量:
像这样进行定义:
#pragma DATA_SEG __SHORT_SEG DIRECT_DATA
int oftenUsed1, oftenUsed2, oftenUsed3; /* these variables are accessed with 8-bit addresses */
int oftenUsed[10] = {
0,1,2,3,4,5,6,7,8,9
};
#pragma DATA_SEG DEFAULT
像这样进行声明:
#pragma DATA_SEG __SHORT_SEG DIRECT_DATA
extern int oftenUsed1, oftenUsed2, oftenUsed3;
extern int oftenUsed[];
#pragma DATA_SEG DEFAULT
这样链接器就知道要把这些变量放到DIRECT_DATA这个segment中。编译器就会考虑用直接寻址的方式访问它们。
-
注意:
- 对于所有要使用这变量的代码,一定要让它见到(比如include包含这个声明的头文件)以上形式(主要是#pragma语句)的定义或者声明,否则它不会知道这个变量在直接寻址区,也不会进行对应优化。
访问直接寻址区的变量
配置编译器
首先要配置编译器,让编译器知道你使用了DIRECT寄存器,以及知道DIRECT寄存器的值,这样编译器才知道怎么优化及产生正确的代码。
菜单栏 Edit->XXXX Settings… 或 Alt + F7 打开配置窗体。
选择Target->Compiler for HC12。添加命令行选项-CpDirect8192或者-CpDirect0x2000。
这句是让编译器知道直接寻址区定义在了0x2000上,同时也定义了宏。
-
注意:
- 如果用到了汇编代码,在Target->Assembler for HC12里最好也添加命令行选项-CpDirect8192或-CpDirect0x2000。
修改DIRECT寄存器的值
尽可能早地,起码在用到直接寻址区之前(比如main函数的一开始),修改DIRECT寄存器的值为你配置的地方对应的值,这个只能手动改,编译器不会帮忙添加这段代码。
DIRECT = __DIRECT_ADR__ >> 8; /* initialize the DIRECT register. This value matches
the value specified on the command line of the Compiler
(e.g. 0x20 for -CpDIRECT0x2000) */
这样产生的代码才能正确的访问直接寻址区。
生成的访问代码
访问直接寻址区的代码没什么特别的,就和访问扩展寻址区的一样;区别是生成的代码。
...
52: oftenUsed1 = oftenUsed1 + 2*oftenUsed2 +3*oftenUsed3;
0003 dc00 [3] LDD oftenUsed2
0005 59 [1] LSLD
0006 d300 [3] ADDD oftenUsed1
0008 b745 [1] TFR D,X
000a dc00 [3] LDD oftenUsed3
000c cd0003 [2] LDY #3
000f 13 [1] EMUL
0010 1ae6 [2] LEAX D,X
0012 5e00 [2] STX oftenUsed1
...
64: normalUsed1 = normalUsed1 + 2*normalUsed2 +3*normalUsed3;
002f fc0000 [3] LDD normalUsed2
0032 59 [1] LSLD
0033 f30000 [3] ADDD normalUsed1
0036 b745 [1] TFR D,X
0038 fc0000 [3] LDD normalUsed3
003b cd0003 [2] LDY #3
003e 13 [1] EMUL
003f 1ae6 [2] LEAX D,X
0041 7e0000 [3] STX normalUsed1
...
可以看出,每次访问直接寻址区都比访问扩展寻址区的变量少产生1字节代码。
另外,直接寻址区的数组如果使用变量系数,则没有任何优化。
...
31: oftenUsed1 = oftenUsed[i]; /* -> thereis no gain to access arrays in the direct page range with variable index */
0009 ee80 [3] LDX 0,SP
000b 1848 [2] LSLX
000d ece20000 [4] LDD oftenUsed,X
0011 5c00 [2] STD oftenUsed1
...
39: nornalUsed1 = nornalUsed[i]; /* above */
000d ee80 [3] LDX 0,SP
000f 1848 [2] LSLX
0011 ece20000 [4] LDD nornalUsed,X
0015 7c0000 [3] STD nornalUsed1
...
但是如果是使用常量系数的话,就能得到优化
...
56: oftenUsed[3] = 10; /* howerver, with a constant index, arrays accesses can be optimized too */
0024 c60a [1] LDAB #10
0026 87 [1] CLRA
0027 5c00 [2] STD oftenUsed:6
...
68: nornalUsed[3] = 10;
0054 c60a [1] LDAB #10
0056 87 [1] CLRA
0057 7c0000 [3] STD nornalUsed:6
...
注意事项
① 可能有聪明的小朋友想到,可以使用DIRECT寄存器和直接访问模式配合的方式来寻址,这样如果需要成片成片访问地址时就可以增加效率了!但是这有个问题,芯片手册中(198页)提到,DIRECT寄存器在特殊模式下可以任意写,但是在其他模式下只能写一次。所以可能你在BDM调试时没有问题,但是正常运行时却出问题。当然,这条我没有进行验证,感兴趣的朋友可以试试。
② DIRECT寄存器必须由用户代码来初始化。自动生成的代码不初始化DIRECT寄存器。
③ 虽然这里是以 RAM存储器\变量 作为例子,但实际上直接寻址区可以定义在整个逻辑地址的任意地方,如寄存器区、EEPROM、RAM、D-flash、P-flash。
④ 前面说到我在uCOS-II中使用直接寻址区时程序会跑飞。
最后发现原因就是因为没有给汇编器添加命令行选项-CpDirect8192。
而uCOS-II有一个汇编文件 os_cpu_a.s,其中有很多负责出栈入栈PPAGE、RPAGE、EPAGE和GPAGE的代码。如:
pula ; Get value of PPAGE register
staa PPAGE ; Store into CPU's PPAGE register
pula ; Get value of RPAGE register
staa RPAGE ; Store into CPU's RPAGE register
pula ; Get value of EPAGE register
staa EPAGE ; Store into CPU's EPAGE register
pula ; Get value of GPAGE register
staa GPAGE ; Store into CPU's GPAGE register
在默认情况下,汇编器会将其优化,生成的代码是(以PPAGE为例,其他类似):
122 122 000016 32 pula ; Get value of PPAGE register
123 123 000017 5A15 staa PPAGE ; Store into CPU's PPAGE register
这是使用了直接寻址的方式来进行访问,因为汇编器认为我们没用到DIRECT寄存器,所以认为其值为默认的0,所以就优化为了使用直接寻址的方式访问PPAGE寄存器(地址为0x0015)。但是由于我已经把DIRECT寄存器设置为了0x20,这样就变成实际访问的是0x2015,结果就出问题跑飞了。
为了生成正确的程序,一种方式是按如上所述添加命令行选项,让汇编器意识到现在DIRECT寄存器的值是0x20,这样就不会进行这种优化。另外一种是把所有XPAGE寄存器前面都加个 ‘>’ ,这样就可以强制使用扩展寻址了:
pula ; Get value of PPAGE register
staa >PPAGE ; Store into CPU's PPAGE register
用任一方法后,生成的代码就变成了:
122 122 000016 32 pula ; Get value of PPAGE register
123 123 000017 7A00 15 staa >PPAGE ; Store into CPU's PPAGE register
这样后就不会跑飞了。 但是因为所有ISR都要写类似的东西,所以还是直接添加命令行选项吧。
这也引出了个尴尬的问题,如果要使用直接寻址区这个特性的话,就得使得uCOS每次任务切换或者进出中断都会多出8个字节的开销(2*4),更进一步想,原来访问0x0000-0x00FF的地址(默认为各种寄存器)的代码都会默认优化为直接寻址,改了DIRECT寄存器后等于对这里的寄存器的访问全部都多了1字节。
所以到底要不要使用直接寻址这个特性呢 -_-|| 可能得权衡下。
参考文献
[1] Freescale semiconductor. MC9S12XEP100 Reference Manual Covers MC9S12XE Family . 2008.
[2] Freescale semiconductor. HCS12X – Data Definition. https://www.nxp.com/docs/en/application-note/TN238.pdf
译文:http://blog.csdn.net/lin_strong/article/details/78110162