前言
你正用这个16位单片机开发着,你对自己写的程序非常有信心,从来没有出过bug(那你真是神),你刚刚分配了一个很大的全局缓冲区,所有使用都如同英文经典教科书般的完美。“这么简单的一个程序,不可能会出错的”,你边想着边点下了编译链接按钮。整个编译链接过程一切顺利,编译完美通过,链接眼看着就最后一点点了,突然:
纳尼?这是什么鬼?
这是单片机的RAM不够用了呀老铁!
MC9S12XEP100的存储器映射
可能你觉得我也就用了这么点RAM,充其量也就几KB而已,怎么就RAM不够用了?老子在Linux、STM32啥的上面都是动不动给程序分个几MB的变量的。才用这么点就放不下了,这B单片机也太不靠谱了。
emmmmm 是蛮不靠谱的。但这是16位单片机。不要拿你在32位单片机上被惯出来的乱用内存的垃圾习惯来抱怨。换个角度想,8位、16位这些单片机有限的资源正考验了我们这些程序员的榨取硬件资源的能力,真正考验了我们(一方面)的编程基本功。
我们先来看看RAM咋就不够用了。
上图的左边就是9S12XEP100的本地地址映射,CPU一般情况下能直接或说默认访问的就是这块0x0000-0xFFFF的地址区域。我们可以看到,这个地址区域被分别映射为好几种资源。最开头的2K映射到硬件寄存器资源,各种硬件模块、IO控制什么的;然后是EEPROM、RAM和FLASH。
EEPROM、RAM和FLASH区域中都有一个叫window的区域,这是什么东西呢?
简单解释:
如果只有这16位地址的资源的话,我们的总体资源实在太有限了,所以为了能够给我们更多可用的资源,就想了这么一个方法。实际上芯片上的资源是远多于我们可以直接16位寻址的资源的,这些资源被分布在叫做全局地址的存储器映射地址上,可以看到右图给出了全局地址的资源划分,从0x00_0000到0x7F_FFFF。
各存储器资源的实际大小远大于我们本地这点访问的大小。那为了CPU能在本地地址访问到全局地址的资源,飞思卡尔的解决方案是(其实也可以直接全局地址访问,但这里我们介绍分页访问方法),为每个资源提供一个“窗口”,就是那个window。每个窗口对应一个页寄存器,就是那个RPAGE、EPAGE、PPAGE,然后把全局地址的资源按照窗口的大小,平均分配,然后连续对应到不同的页编号去,通过设置页寄存器为不同的页编号,这段窗口就会联系到对应的全局地址去,而那段没有window这名字的资源其实也对应映射到了某几页上面,一般是资源的最后几个页,只是它是固定映射的。
随便举个例子,比如全局有20K的RAM资源,按照这个方案,因为RAM窗口大小为4K,所以我们会有5个页,我给它们编号为0xFB到0xFF。然后因为固定的RAM部分有8K,所以实际上这8K映射的是FE和FF的页,而window中,默认的,也就是RPAGE寄存器中默认的会对应FD那一页。
然后,叫法上,那些固定映射的RAM,我们叫他非分页(non-paged或non-banked)RAM,要通过window访问的自然就是分页RAM了。其他资源对应。注意,window是可以映射到非分页RAM的那几个页的,但是,好像没有什么需求要这么做吧。
我们打开工程的prm文件就可以很清晰的看到这一点:
SEGMENTS
……
/* non-paged RAM */
RAM = READ_WRITE DATA_NEAR 0x2000 TO 0x3FFF;
……
/* paged RAM: 0x1000 TO 0x1FFF; addressed through RPAGE */
RAM_F0 = READ_WRITE DATA_FAR 0xF01000 TO 0xF01FFF;
RAM_F1 = READ_WRITE DATA_FAR 0xF11000 TO 0xF11FFF;
RAM_F2 = READ_WRITE DATA_FAR 0xF21000 TO 0xF21FFF;
RAM_F3 = READ_WRITE DATA_FAR 0xF31000 TO 0xF31FFF;
RAM_F4 = READ_WRITE DATA_FAR 0xF41000 TO 0xF41FFF;
RAM_F5 = READ_WRITE DATA_FAR 0xF51000 TO 0xF51FFF;
RAM_F6 = READ_WRITE DATA_FAR 0xF61000 TO 0xF61FFF;
RAM_F7 = READ_WRITE DATA_FAR 0xF71000 TO 0xF71FFF;
RAM_F8 = READ_WRITE DATA_FAR 0xF81000 TO 0xF81FFF;
RAM_F9 = READ_WRITE DATA_FAR 0xF91000 TO 0xF91FFF;
RAM_FA = READ_WRITE DATA_FAR 0xFA1000 TO 0xFA1FFF;
RAM_FB = READ_WRITE DATA_FAR 0xFB1000 TO 0xFB1FFF;
RAM_FC = READ_WRITE DATA_FAR 0xFC1000 TO 0xFC1FFF;
RAM_FD = READ_WRITE DATA_FAR 0xFD1000 TO 0xFD1FFF;
/* RAM_FE = READ_WRITE 0xFE1000 TO 0xFE1FFF; intentionally not defined: equivalent to RAM: 0x2000..0x2FFF */
/* RAM_FF = READ_WRITE 0xFF1000 TO 0xFF1FFF; intentionally not defined: equivalent to RAM: 0x3000..0x3FFF */
……
END
PLACEMENT
DEFAULT_RAM /* all variables, the default RAM location */
INTO RAM;
……
PAGED_RAM INTO /* when using banked addressing for variable data, make sure to specify
the option -D__FAR_DATA on the compiler command line */
RAM_F0, RAM_F1, RAM_F2, RAM_F3, RAM_F4, RAM_F5, RAM_F6, RAM_F7,
RAM_F8, RAM_F9, RAM_FA, RAM_FB, RAM_FC, RAM_FD;
……
END
经过这样的解释,这个prm文件大概能看懂了吧。我们可以看到分页RAM的最后两页被注释掉了,后面的注释就说了其直接等于那两段本地地址。
然后PLACEMENT中,DEFAULT_RAM INTO RAM的意思就是默认的,所有RAM变量会分配到前面定义的RAM这个SEGMENT即段中,这个段的大小只有8K。一些小应用可能够用,但仔细想想,上一个RTOS,除了RTOS自己管理要分配的RAM,一个Task的栈得给他个4、5百K吧。然后堆区域只要你用到了malloc就会分配,按照CW的默认值是2K。如果你有一些通讯的端口,一个通信端口给个几百、1K的缓冲区也不过分吧。算了算,其实真正留给你app用的非分页RAM其实没多少。
解决方案
那么我们来看看遇到RAM不够用的情况能够做些什么。
优美的代码
首先,也是最重要的,就是你身为程序员最重要的,就是写出优美的代码,用尽量少的资源完成尽量多的事,这是很考验基本功的。基本要考虑到的就是
- 优化数据结构与算法:选择正确的数据结构与算法,降低解决任务的时间和空间复杂度,最本质的基本功,这没什么好解释的吧。完成同样一个功能,使用不同的数据结构和算法带来的性能和资源占用可能天差地别。哪怕算法类似,也许只是不同的编写代码方式,最终编译器生成的代码也可能会差异很大,这很考验基本功,要求对计算机系统特别熟悉,没什么好说的,CSAPP、APUE啥的一本本往上怼吧。
- 严格评估全局变量大小:比如,缓冲区真的需要1K么,可能1百就够了,不要为了自己方便就乱分配一个特别大的值;每个任务的栈到底需要多少,严格评估下,然后给个安全够用的值,比如uCOS-ii就有提供功能查看栈的使用量,正常运行一段时间后在最大使用量上再加个20%应该就足够安全了,还有很多预分配的资源可能编译时就能确定最多用到几个,那就不要预分配更多了。
- 提升复用性:这里当然是着重讲RAM方面的,实际上代码也有很多复用性技巧。
RAM方面的复用性,我能想到的,比如:
使用局部变量本身就是一个很好的复用,因为局部变量是直接分配在栈上的,对于平级调用的函数来说,它们相互之间的局部变量空间是复用的,但是得注意不要在局部变量上分配大数组,这样会导致任务栈的使用量暴涨。一般还是把大数组作为全局变量的。
大数组没法用栈的复用,那怎么办呢,比如,任务A和任务B不会同时工作,那严格评估后可以让A和B使用同一个缓冲区,那缓冲区的占用量直接少了一半;就算有很小的几率A、B同时使用缓冲区,那也可以通过加锁等方法互斥地使用。
另外,其实动态分配的内存也是很有利于RAM复用的,之前说了,堆默认占了2K的大小,这2K你可得好好利用起来呀,比如临时需要的,或者大小可变的一些实例,就可以多使用动态分配的方式来获取,相当于所有任务都在复用这2KB的RAM。当然,记得好好写代码,别内存泄露了。另,malloc不是线程安全的,记得加锁(我的娘呀,突然想到我之前写的程序完全忘了这一点,赶紧去检查下)。 - 暂时想到这些,欢迎补充。
利用分页区/全局地址资源
上面是程序员的自我修养,但毕竟16位地址的资源实在是太少了,大点的app还是要用到更多资源的。那一种办法就是用到上面介绍的分页区资源。
关于怎么把变量定义到分页区以及访问的方法,我之前已经写了很多篇博文了。这里就不赘述了,直接给出几个链接:
理解S12(X)架构中的地址映射方案
HCS12X–数据访问(如何在CodeWarrior中转换逻辑地址与全局地址)
HCS12X–数据定义(如何在CodeWarrior中将数据定义到分页区)
CodeWarrior的map文件详解
相信这几篇读完基本你就知道怎么做了。
简而言之就是
- 通过#pragma把变量 声明/定义 到自己想要的位置。
- 在声明可见的地方可以直接使用分页区变量,这样编译器会帮你生成正确的访问分页区资源的代码
- 在使用指针访问分页区变量时一定要使用扩展的指针。注意函数调用时传参导致的指针的页信息丢失问题,即,如果需要传扩展的指针给函数的话,函数的指针参数也应该是扩展的。
像将环形缓冲区放到PAGED RAM(、同时也可以放到非分页RAM甚至自己加一点实现还可以用全局地址访问)的需求,我已经直接给出了实现的模块了,感兴趣的可以去看看: https://blog.csdn.net/lin_strong/article/details/88236566。
无脑使用large/custom地址模型
之前所说的那些都是当我们使用small或者默认的banked地址模型时,默认访问RAM时使用本地寻址,分配也默认分配到本地地址。而如果你实在搞不过来的,接手了个垃圾程序,就想立即把程序链接通过了,那你其实可以选择使用large或者custom地址模型,好像现在是不让创建工程时使用large,只让选custom,但也差不多。
在large地址模型下,默认对所有资源的访问,不光是RAM,都会通过全局访问的方式,资源也直接在全局地址上分配,这样就相当于你直接可以使用所有的资源,也不需要像上一条那样仔细分配每一个变量的位置。(实际上我没有使用过large模型,所以可能这段话有误。)
但是这样生成的程序会大很多,而且运行效率也会慢。因为全局寻址比本地寻址慢,生成的代码也大。
可能这样弄会方便很多,那你自己看看怎么搞吧,我教不了。
但是如果第一条做不到的话,就算给再多的资源,也照样会被这糟糕的程序员浪费完。
在本地地址多变出16K RAM
各种精打细算的安排怎么分页真是痛苦呀,8K非分页RAM是在是太少了。那再给你16K何如。
基本原理就是,其实0x4000-0x7FFF这段地址是可以配置映射哪个资源的,通过配置MMCTL1寄存器实现,可以映射的资源如下:
默认情况下的配置是映射那16K的FLASH,但是其实可以配置成映射RAM。而且你仔细看上图右边,这16K的RAM不属于分页区RAM!等于是把隐藏的16K RAM给调了出来。
好激动呀!那怎么让这段地址映射RAM呢?最简单的方法,创建新工程时,按如下改一个选项就好:
然后在引导代码中就会自动帮你配置寄存器,将这段地址映射到16K的RAM去了。
这个选项做了什么?
首先,它改了你的编译器、链接器、汇编器选项,如将编译器选项中的
-MapFlash -D__MAP_FLASH__
改成了
-MapRAM -D__MAP_RAM__
其他的类似,自己可以建个工程看看区别。
有兴趣的可以到start12.c里看看,后面那个宏定义决定了自动生成的代码怎么设置MMCTL1寄存器。
然后还在cmd文件夹中的XXXX_Postload.cmd里头加了一句
HCS12X_MAP4000 RAM
这样,调试器就知道4000这个地址映射为了RAM。
然后,它还改了你的prm文件。
将原本的
ROM_4000 = READ_ONLY DATA_NEAR IBCC_NEAR 0x4000 TO 0x7FFF;
改为了
RAM_4000 = READ_WRITE DATA_NEAR 0x4000 TO 0x7FFF;
当然后面的PLACEMENT也对应进行了修改,这样就实现了本地多出来了16K的RAM。
怎样。这样够用了吧!
结束语
这篇文章讲了这么多,其实最本质的还是咱程序员要提高自己的基本功,千万别做那种滥用资源的乐色程序员。
打铁还需自身硬,共勉。