我看过一个故事:在20世纪30年代,英国送奶公司送到订户门口的牛奶,没有盖子也没封口,麻雀和红襟鸟可以很容易的喝到上层的奶皮。后来,牛奶公司把瓶口用锡箔纸封装起来,想防止鸟的偷食。但20年后,英国的麻雀都学会了用嘴把奶瓶的锡箔纸啄开,继续偷吃它们喜欢的奶皮。然而,同样是20年,红襟鸟却一直没学会这种方法。生物学家对这两种鸟进行了研究,从生理角度看它们没多大区别,但在进化上却如此的不同。后来发现这于它们的生活习性有关,麻雀是群居的鸟类,常常一起行动,当某只发现了啄破锡箔纸的方法,就可以教会别的麻雀。而红襟鸟则喜欢独居,它们圈地为主,沟通仅止于求偶和对侵犯者的驱逐,因此,就是有某只发现了那个方法,别的鸟也无法知晓。对于人类也是如此,进步需要交流和行动,这样,任何一个有了新技能才可以真正的发扬光大,使人类生生不息。于是,我写了一下这些学习硬件编程中的感受。如果你已经有硬件开发经验,我的东西也不必看了,里面可能是比较幼稚的想法。如果你准备移植ucOS,最好先去网上查有无移植好的代码,如果有,也不用看了。
一、 初学汇编
我的研究生课程结束时,知道毕设应该是硬件相关的方向,当时我对硬件的认识是一片空白,看着同学们早就忙忙碌碌的投入到自己的课题中,自己很着急。忙着去图书馆借了很多关于硬件的书,和电子、电路、单片机等相关的,五花八门的,只要觉得里面有想知道的,就拿回来啃,饥不择食,又像一只忙碌的没头的苍蝇一样乱撞。对单片机类的,没有"型号"的概念,天书一般,只好理解一些硬件基本知识,很多东西觉得很好,下功夫理解记忆,但没多久就忘了一干二净。现在想想很正常,因为哪些东西是需要实践的。那个学期前后借了几十本书,但看懂的很少。当时做的笔记,只限于对三态门、总线驱动器、锁存器、计数器之类的概念了解,很是低级的东西。后来就看DSP的书,实验室的书不管是写什么系列的,都被我浏览了一番,好像朦胧的明白了什么。脑袋里装了一堆不知前因后果的片断就到了新学期,DSP和操作系统更是没有头绪,好在当时有了确定的目标——TMS320F240。写开题报告前,努力的看了ucOS-II操作系统,仔细的读了有关240的原理的书。但仍然对这两者怎么联系起来的概念很模糊。在开题报告里,写了很多怎样在ucOS 里编程的问题,在当时的理解下,觉得写了很"充分"的东西,想象着以后编程就是那个样子的。但实践以后知道那时的理解有些本末倒置了。
开题后已是四月上旬了,"嵌入式"、"操作系统"、"移植"、"DSP"这些东西一直在脑子了盘旋,看书上网查资料都要朝着这些目标。比较蠢的做法是,像我一样只是努力的找书,企图把这些底层的东西都理解了,甚至想把汇编的指令都记住。直到五月中旬的一天,觉得这样埋头读书不能前进了,就准备动手。当时已经是非典隔离期了,大家都在拼命的运动发泄各种心情,没有学习的气氛。我也很怕,所以没心情学习,但总觉得课题是我不能轻松完成的任务,不敢消沉下去。于是我拿过来板子、仿真器、电源这些很陌生的东西,试着把它们装起来,接着就是装软件、仿真器驱动,因为有安装步骤的说明,我很顺利的完成了。但测试软件时却显示没有成功,仿真器不能用,安装软件的能力我还是比较自信的,但就是找不到问题,请有经验的同学帮忙结果一样。忙了两天还没搞定仿真器,严重的打击了我本来就很迷茫的自信。正当我无所适从时,很幸运的突然发现了电脑CMOS设置里有并口设置的选项,我发现了"EPP"模式,我当时就知道了这次成功了。这个开头很难,但困难有多大,解决困难后就有多兴奋,兴奋之余浑身充满了前进的动力。
接下来就可以编程了,第一步要熟悉软件编程环境,我的第一个疑问就是"Simulator"和"Emulator"的区别。我上网到清华的BBS上发现有很多人在讨论DSP,我在别人的贴子中隐约知道了我用仿真器就是"Emulator"(Simulator是在软件中模拟,开始我还想试试,但有仿真器,最终没去理会)。论坛人气很旺,很多问题我都不知所云,大开眼界,原来问题有这么多!我的第一个程序是最简单的加法。由于我之前还是努力的看了书,所以用到的简单指令不用很费力就可以写出来,但一个完整的程序不止这些,要知道cmd文件怎么写,知道它的作用(当时不能完全理解,按照大家一贯的写法写),还有中断向量表、头文件等。这些文件的作用开始是我不能完全理解的,不太明白为什么那么写。大多书中只是稍微提一下,不能足以帮一个初学者建立一个很明确的概念和编程框架。因为程序很简单,我仿照师姐留下的一个加法程序写了出来。这个加法程序用了三天时间,其中大部分时间花费在一个小问题上:第一次写程序太随意,可能是写高级语言程序的毛病,一个标号的第一个字母我没有写在第一列,而是随意的打了个空格(当时没有意识到后果),这个空格害我找了调试了一天时间!找出错误以后,我又总结了一下,哪些格式可以随意写,哪些要严格遵守,这样再写程序就不那么不自在了。这样简单的"1+1=2",毕竟不能解决我的大部分疑问,只是稍微了解了编程环境,还有很多个疑问不能找到答案,所以就把CC'C2000的帮助文件很仔细的看了一遍,很费劲,当时觉得没有很大收获,但从此对帮助文件的内容有了大致的了解,在以后的编程过程中,很习惯的查看帮助。
完成了第一个程序,以后的就按照这个结构写其他一些简单的程序,逐渐的用到了寄存器、中断等。这样就熟悉了以前看书时想努力记住但没有成功的一些指令和寄存器的配置,还逐步的有了一些调试经验。逐渐的我不像以前那样急切,很多问题是要细心的体会,试验一次不能反映全部问题,经验是在不断更正错误的基础上积累起来的。我的一个体会是:不要试图在最初知道所有问题的答案,即使找到的答案也只是纸上谈兵,还要在实践中深化理解,大部分答案都在实践中自然浮出水面。最初编程很死,不敢越雷池半步,这是因为很多东西不完全理解,只有按部就班的把它做出来,不敢加以随意的变化,变了就不知道对不对。我把这些不理解的东西就当作学习的目标,带着这些问题从练习中逐渐找出答案。这个过程中,我养成了一个习惯:因为问题很多,我随时都有可能明白了一些,也可能又有了一个新的疑问,我就把这些想法随时记下来,等待以后验证或寻找答案。写下来对我加深印象真的很有用。后来,如果有了什么重要的经验,等一个程序成功后,都会把它们总结出来,算是对自己的一种肯定,很有成就感。总结的多了,就了解了很多细节的东西,哪怕是以前看起来很简单的指令,也有运用是很巧妙的地方。举几个例子:
1.对形如:y=a1*x1+a2*x2+a3*x3的多项式编程,240指令的装载临时寄存器的指令有LT、LTD、LTP、LTA、LTS,乘法指令有MPY、MPYS、MPYA、MPYU,这些指令中有很多可以同时执行几步,如果能巧妙的结合利用,程序很简洁、效率很高,但要很好的运用,不是很容易(这些是最能体现DSP特点的指令,还有块移动指令,它们和流水线有关,所以效率很高)。自己写程序不要求很高,但知道它们之间有区别即使不用、记不大清楚,看别人的程序也能充分的领会其中的巧妙。有一条指令BANZ,我的程序中最初肯定不会利用它,偶尔一次看到有人用,仔细的体会了它的用法,发现用在循环中真是个不错的选择。
2.有一次看一个程序,涉及到了定标问题,我几乎是看着程序抄下来的试验的。其中有几条非常常见的指令MPY、MPYA、ADD、OR在编译时提示有错误,程序中有这么两句:
MPY #7FFBH
MPYA #0H
我看不出有问题,而且和书上是一模一样的呀。我就查软件中的帮助,发现原来书上用错了!那个错误实在是非常容易犯的!对MPY #k指令,操作数为立即数时为只能是 "13-bit short immediate value"。对MPYA指令根本就没有立即数寻址方式,只有直接或间接寻址方式。还有ADD和OR的用法都是类似的想当然地用,而不注意它地特殊之处。比较幸运的是,这种错误编译器可以发现的,但有些隐含的错误它可能发现不了,自己又觉得不可能会错,结果出来后错误很难排除。
3.最初面对240众多的寄存器,初始化时总觉得多写了没有用到,不然就是少了一些配置,这些要和240内部结构结合起来记忆理解。开始是对CPU寄存器、系统配置寄存器、时钟模块寄存器熟悉,其次熟悉了定时器、比较模块的寄存器配置,上手后就慢慢熟悉其他的,比较麻烦的就是EV模块。大多寄存器只要在程序的最初配置一次,就可以不用管了,个别的比较特殊。如等待状态发生寄存器WSGR在IO空间中,对它赋值就要用out指令而不是常用的splk。还有如 COMCON,要配置为PWM模式,为保证全比较单元的正确操作,需要对它连续两次写操作。还有时钟控制寄存器CKCR0、CKCR1,编程时,必须先使 CKCR0的CLKMD1=0,禁止PLL,然后根据要求设置CKCR1设置其他位,最后使CLKMD1=1,允许PLL工作(如果使用PLL的话)。还有定时器的控制寄存器TxCON有时也需要写两次,第一次配置,第二次启动。总之,对一些需要比较"特殊"的做法,如果注意总结会对整体有个清晰的把握。
二、 移植系统
练习多了,就有点柳暗花明的感觉。于是跃跃欲试,开始试着做我的重要任务——移植uCOS II,看了绍贝贝的书,明白了我要做的是什么,虽然这时还是雾里看花,理解也很朦胧,但已经有了前所未有的自信。最简单的方法是去www.ucos.com网站下载已经移植的代码,当时我查的时候,对TI的DSP,只有C31和C54的移植代码下载(不知现在有没有更新)。我试着从从这两个例程中学习我需要的东西。首先是要明白这么多文件的组织关系,最初面对一大堆文件,根本不知它们是何种关系,为什么存在?大多文献里的都是对几个移植文件做了详细的说明,而对怎样组织的好像是不言而瑜的事情,对一个资深的程序员确实没有必要教怎么做,可是我没有开发大程序的经验,没有清晰的把握,因为自己做的汇编程序都是十几行的小程序,而且还是对cmd文件、向量表、头文件这些不是程序"核心"的东西没有深刻的认识,我对这些零散的文件研究了很久,才意识到我不止要改几个函数那么简单,还要要写一个有main.c的文件、一个 cmd文件、中断向量表、一些必要的头文件,还要象写其他简单的程序一样做一个框架,操作系统当成普通的用户程序一样和这个框架结合起来,然后再写程序和普通的不同的是我有这么多别人都已经做了的东西,我要实现那样的机制不用自己去写,只需拿来用。
明白这些算是思想上的重大突破,不然连 DSP程序和操作系统的关系都不知道。这样就动手写移植部分代码,代码中的一些是参照一个例子,那个应该是240的移植,也只是移植部分的代码,不是一个完整的代码。最初我对ucos认识不深的时候,借鉴了那个程序中的一些做法,如任务切换函数是用软件中断INT31,当时对中断的认识都很浅,不用说软件中断了。对于什么是硬件中断什么是软件中断的问题也困扰我很长时间,曾经问过一个比我早入手的同学,在他的编程中也没有用过软件中断,可以说没有意识到这两种中断只是汇编中普通的中断。至于为什么用INT31就不知道了,现在知道了可以用任何一个软中断的。还有一个就是调用库函数I$$SAVE和I$$ REST用于移植,最初我在傻傻的想,我是不是可以直接用这两个函数?可是它们在什么地方?我怎样找到它们看看代码?还有很多别的疑问,但我还是建立了一个.mak把那些觉得需要的文件加了进去并按照以前的做法把它们"归位"。然后编译,当然有很多很多错误,除了语法错误,当然有我不懂的错误。我尽可能的改了一些,但不能完全正确。问题是出在用C编程上,所以我还要熟悉用C编程的方法。
(一) 用C编程
最初用C写没有什么可以参考的书,我还是从最简单的加法开始,写一个纯粹的C程序,同学说可以用输出语句输出结果,我的即使运行正确也不能输出,找了人帮忙看,还是于事无补。毕竟用C的人很少,于是我自己开始仔细的找原因。看生成的map文件找结果所在的地址,有结果而且正确,看交*列表,C语句对应的汇编语句,没什么错误,就是不明白为什么有错,和别人的不一样。这当然影响我的自信心,觉得为什么倒霉的总是我?抱怨当然不是办法,还是要继续找错了。于是我拿起了放了很久都没看的TI的C编译器文档,虽是全英文的,还是坚持了三天看完了,好像没有找到答案,但更坚定了我的想法,错误是肯定有的,因为文档上有printf函数,但好像不影响程序的运行结果,于是在以后的编程中只好暂时放弃用输出语句(后来调试基本成功后换了板子就可以输出了),这对调试当然很不方便,要在映射的地址中看结果。看文档的好处是,更清楚的知道了用C和汇编编程的不同,如cmd文件、中断向量表的写法。因为要涉及到混合编程,就要对在C中和汇编中的函数、变量互相调用问题弄明白,这是个难点,那个文档看了很多遍,有的问题还是不能完全明白,最终在老师的帮助下对它有了比较清晰的理解,理解后就用编程来验证,结果是我们的理解是正确的(后来看到有些书上也有对此的讨论,我甚至能判断作者的理解是否完全正确)。下面是几点总结:
- cmd文件写法可以参照CC‘C2000帮助文件中的例子,还可以查阅TI文档spru024D的2.8.3。
- 寄存器映射地址头文件,和汇编中的不同,要重新定义,定义方法如下:
#define CKCR0 (volatile unsigned int *) 0x702B
使用方法: *CKCR0=0x0041; - 中断向量表的第一条语句应该跳转到_c_int0对于这点我最初不是很明白,因为我看到的程序都是以main开始的。后来逐渐明白了,_c_int0是程序真正开始的地方,只是这个开始不是开发者写出来的,而是编译器自动为我们做好的,你要配合它做的是就是在Build Option中对linker的C Initialization的选项选择ROM Autoinitialization Model或RAM Autoinitialization Model,而不是汇编中的No Autoinitialization,开发者的程序要以main函数开始,初始化结束后会跳转到main函数。在反汇编代码中可以看到这些过程。两种初始化的方式详见上面文档的同一节。
- 汇编代码中要用到的C的变量或符号,都要在前面加"_ ",即C中的fun要用在汇编中写为"_fun"。当然互调前要声明为全局变量或外部函数。详细的说明见spru024D的4.2.2。在C中要嵌入汇编的格式为:
asm(" clrc INTM");
这个地方要注意的是引号里面第一个字符为空格或Tab键(还可以是别的记不大清了),不能直接写指令。为什么会有上面的一些规定,看看反汇编的代码就很清楚了,编译后编译器会为C中的符号都加以下划线,所以在汇编中用当然要写成"一样"的了,第二条规则也是和编译以后的程序格式有关,可以在你的程序中故意不正确的写,看看显示的错误就明白了。 - 比较难的是C调用汇编函数,汇编函数的写法。这时要在汇编函数的开始和结束加入一些语句。C中用三个寄存器管理堆栈和局部帧:AR1作为堆栈指针SP,AR0作为帧指针FP,AR2最为局部变量指针LVP。调用函数时当前指针必须为AR1,首先要在软件堆栈中保存函数返回地址、FP,分配局部帧空间,空间的大小是局部变量的个数加1,如果被调函数中可能修改寄存AR6、AR7,也要保存(当编译器优化时它们被用作保存寄存器变量)。函数实现过程中注意调用它的函数传递的参数的存放次序:从右到左按照堆栈增长的方向放置。函数退出时和进入函数时的操作相反。这个规则的原因也可以从生成的交*列表中找到答案,和上面的原因大同小异,C语句编译后的汇编代码可以看出在任何一个函数调用前都会有这样的"保存 "工作,结束时做相应的恢复。详见spru024D的4.2.2,4.2.4和4.3节。
- 用C时需要.lib库函数,这个格式的不能用文本的形式看,在它的同一目录下有rts.scr文件可以以文本形式打开。用一个命令可以提取某个库函数可以对它查看或是修改。我知道这个过程,但没有用过,所以不多说了。详见spru024D的4.1.3。