iot dev technology 2:Boot loader & 驱动程序

2023-10-28

4. 上电之后:Boot Loader

4.1 排查硬件是否正常执行

上电之后,要一个一个排查错误,确保程序可以正确运行

■ 通过ICE在PC上的远程调试工具,在程序的第一行设定断点,确定程序有停下来。
■ 检查CPU的PC(Program Counter)寄存器是否正确。
■ 检查CPU内部RAM的内容是否和我们下载的可执行文件(Binary File)相同。
■ 程序的第一行是设定CPU状态寄存器,执行这一行命令后停下来,观察CPU的状态寄存器是否如预期改变。
■ 继续单步执行,确认PC寄存器是否会跟着改变(通常都是累加,除非执行到Function Call、goto或中断产生),且每行程序的执行结果都是正确的。

4.2 验证boot loader

做完这个验证后,要验证boot loader是否顺利执行

■ CPU寄存器操作测试。
■ Stack Pointer的设定是否正确?Function Call是否可正确运行?
■ 中断矢量表设定是否正确?中断矢量程序是否可正确运行?
■ 存储器初始化及其操作测试,并保证所有的存储器都可正常读或写(如果可以写入的话)。
■ 将data段载入RAM,对BSS段设定初值。如果有程序段必须在ROM以外的存储器执行的话,Boot-Loader也要负责将其载入。在此阶段,我们必须保证当主程序执行起来后,程序中全局变量的初始值都是正确的。假使有需要被载入的程序段,也必须确认其可正确执行。

其中,对于中断矢量表的验证,需要注意以下几点

■ 中断矢量表数组,详细注解每个entry代表的中断源。
■ 有的平台是CPU外接中断控制器,那就必须先完成中断控制器的驱动程序,才可能开始测试中断系统是否正常。
■ 设定CPU的‘中断矢量表地址寄存器’(有些CPU的中断矢量表只能放在规定的地址,通常这种CPU就不会有中断矢量表寄存器),即告知CPU中断矢量表数组所在的地址。
(这里会发生错误:将中断矢量表设定在错误的地址,许多CPU会规定放置中断矢量表的地址必须是某个值的倍数)
■ 设定CPU的中断控制寄存器,一般而言,每个中断都可以设定优先级,而CPU可以设定是否允许中断产生,或仅允许哪个优先级以上的中断才可被触发。
(这里会经常发生错误,中断优先级设置太低,导致中断无法被触发)
■ 确定中断被触发之后,相应的ISR就会被执行。
(这里容易发生错误:中断矢量表中各个entry与中断源的对应关系错误,导致中断产生,却执行到错误的ISR)
■ 提供ISR的写作范例,让写ISR的工程师不用知道中断系统的细节,基本上就是编写一个C的函数即可。

而对于存储器,有以下几点

■  硬件方面来说,如果数据线或地址线连接错误,就会导致读写一直出现错误。
■  软件方面来说,有些存储器(如SRAM、NOR Flash及ROM)不需要额外设定,只要给对地址就可以直接使用,但是其他的存储器种类,以SDRAM为例,则必须通过额外的控制IC(SDRAM Controller)才能操作,程序必须先设定好SDRAM Controller的配置,如SDRAM的size、速度等,才能正确的access到SDRAM。假使某些参数设定错误,如size设定的比较小,则SDRAM或许还是可以动作,只是会access不到高地址的存储器。
■ 系统中的存储器特性各异,所以当CPU在access不同的存储器时,一定要使用不同的时序(Timing),至于外部存储器的时序该如何设定,则每种CPU都大不相同。你现在只要知道Boot-Loader必须负责设定各个外部存储器的时序。时序若设太快,系统会较不稳定;设太慢则系统整体性能变差。CPU的data sheet中通常会提示CPU在什么速度下使用什么存储器,Timing应该如何设定
■ 在把板子与Boot-Loader交付给其他人员之前,一定要把各个存储器的每一个Byte都测过,确定读写(如果能写的话)都没问题。
另外一个问题,假使存储器不能在执行时被写入(如ROM),或者即便有方法可以写入,一旦存储器中的内容是有意义且不容破坏的(如用来存储程序的NOR Flash),在执行时,Boot-Loader该如何验证ROM或NOR Flash中的数据是正确的呢?
——一般来说,有许多方法可以采用,最简单的就是计算checksum,我们会分别在PC为image file算checksum,另外在机器上为ROM或NOR Flash计算checksum,只要这两者值是相同的,我们就可以说机器上的ROM或NOR Flash读取功能正常,而且也可以顺便验证存储器的内容是否正确。

4.3 CPU初始化——总结

对先前的内容做个总结:这里只总结到,迁移data段数据之前
■ 设定Stack Point寄存器。
■ 设定状态寄存器,至少在此阶段要禁止(Disable)中断产生。
■ 设定中断矢量表指针。
■ 设定CPU执行状态(用于嵌入式系统的CPU通常都具有省电功能,可以让CPU以不同的速度执行。
■ 设定存储器控制器(如果有用到像SDRAM的存储器的话)。
■ 设定CPU操作各个存储器的时序。
■ 有些CPU的PIN脚可以有多个功能,可以通过设定寄存器来设定。在Boot-Loader阶段,尽可能把这种多功能的PIN脚设定为我们系统定义的用途。
■ 通常我们会选用已经整合许多外围设备的CPU,如:LCD Controller、USB Controller或SD卡接口等,虽然这已经算是驱动程序的范畴了,但如果有必要的话,也可以在Boot-Loader阶段就先初始化这些外围。

4.4 载入程序段

data与bss段之外,需要加速的程序模块可以看情况Boot-Loader阶段传输
存储器的速度:CPU 寄存器>CPU Cache>CPU 内部存储器(Internal)>外部 SRAM>NOR Flash>SDRAM>Mask ROM>NAND Flash
Nor Flash是可以重复写入的ROM)Nand Flash(P3随身听、随身碟中的存储装置就是NAND Flash,它是目前单位容量最便宜的存储器。)Nand Flash无法直接执行程序

5. 驱动程序

驱动程序是系统的基础,基础不稳定如同房子底层发生小小的摇动,传导到上层就会感觉是大地震,所以驱动程序稳定度的重要性众所周知
一般人总会认为嵌入式系统和PC程序最大的不同,就是前者有一堆驱动程序要自己编写,这种想法当然不完全正确,经过前面几个章节的探讨,我们知道嵌入式系统的开发不仅仅只是开发非PC平台的程序而已,它有着诸多的限制,如:CPU计算能力、存储器大小、成本及进度等。每一位工程师,无论是开发驱动程序还是应用程序,都应该要了解目前正在开发的产品的本质与特性,如果用写PC程序的思想来开发嵌入式系统,无疑就像在系统中埋下一堆不定时炸弹。
举一个经常发生的案例:在PC程序的某个函数中,定义一个有1024个长整数的数组(4096 Byte)不会有任何问题,但在嵌入式系统中就可能造成Stack Overflow(假设系统的Stack size为2048 Bytes )。更麻烦的是,假使测试时没有把数组中所有的元素都设定数值,可能还不会出问题,一旦等到发生较极端的状况,当高地址的元素被写入值了,stack之外的存储器就会被破坏,而且假使被破坏的存储器不会马上被用到,而该工程师还浑然不觉函数已出现问题,要等到被破坏的存储器被使用时,系统才会出状况。此时正在执行的功能(function B)可能和罪魁祸首所在的函数(function A)完全无关,工程师会误判问题的来源,就算把function B从头到尾看一次,也看不出个所以然,因为问题是出在和function B完全不相关的function A身上。

5.1 驱动程序分层

Windows或Linux的开发环境都已经有稳定的操作系统,要增加对新硬件设备的支持,驱动程序自然要遵循操作系统的规定,但是嵌入式却是反过来的,当产品的功能确定后,硬件规格的定义会比系统设计早完成,而且为了让硬件板子出来时就有测试程序可供验证,在硬件设计阶段,你们就已经在评估板上开始开发驱动程序了。在这个阶段,系统还在设计呢
然而,需要注意的是,也并不是所有嵌入式系统驱动设计会比系统设计更早完成,不管先有系统还是先有驱动程序,嵌入式系统的驱动程序架构绝对要清晰简单,一般来说最多就是两层。

■ Driver层:真正驱动硬件设备的程序,上层的程序不应该直接调用到这层的函数。
■ API层:根据系统或应用程序的需求,将driver层包装成较简单的接口,这应该是上层程序与硬件沟通的唯一管道。API接口会隐藏所有的硬件细节。

理论上API层的函数应该由使用这些API的人负责定义,只有他们才知道系统或应用程序需要用到哪些硬件的功能,可是他们显然比较不清楚硬件特性的细节,也不知道哪些功能可以实现,哪些功能可能做不到。所以实际上都会由负责固件开发的工程师,先根据硬件的特性与对最终产品的了解,先行定义驱动程序API的初版,然后再提交给将来会使用的单位参考,并根据其反馈做修正

5.2 驱动程序开发前的准备

需要记住一点驱动程序在整个系统开发中是属于没机会发挥个人创意的工作,若CPU或外围IC规定就是要照某些步骤执行它才能正常运行,你就不可能用其他方法让它动。
首先有一些资源需要了解

■ IC原厂的技术人员或代理商的FAE
■ 评估板
■ 范例程序代码(Sample Code)
■ 产品规格书(Data Sheet)
■ 硬件板子设计规格书
■ 原理图与Layout图

另外,使用c语言写驱动程序,就算有必须用汇编进行操纵的步骤,也可以使用内联汇编(inline assembly)来解决

■ volatile变量:操作CPU内Memory Mapping Register的方法就是使用C语言的指针,为避免相关程序被优化
■ Inline Assembly:现代的嵌入式系统开发,为了可移植性、可维护性等原因,除了要操作CPU内部寄存器及对某个模块做性能调整外,用汇编语言写程序的机会不多,通常我们也会用Inline Assembly,免去汇编语言函数与C语言函数间互相调用时的麻烦
■ 中断处理程序(ISR)的写法:虽说ISR其实就是一个C的函数,但ISR的起始和结尾与一般的函数还是有点不同。简单地说,在ISR开始时要将所有寄存器都存储在Stack Memory中,结束前要恢复这些寄存器的值,而且一般函数是用‘return’,的指令返回调用的函数,而ISR必须用‘interrupt return’的指令返回被中断的程序。原本这些细节都要写ISR的工程师自己写,有的编译器会提供额外的语法,如在函数声明时多加一个描述字符串‘__interrupt__’,则编译器碰到这样的函数时,就会自动加上存储/恢复寄存器的指令,并用‘interrup treturn’指令来返回中断发生点,这样就不用写这些细节了。

当产品规格定义完成以及硬件架构大致底定后,Driver API就可以开始进行设计了。我想在设计这套API时,应该要将重心放在系统式与应用程序的需求上;对于硬件设计,我们只需确认其确实能够达成这些API的功能,至于硬件电路的细节应该不会影响Driver API的设计.

实际上我们会把.h文件和包含空函数的.c文件先写好,这样的好处是在驱动程序还没稳定之前,不会影响系统的编译,其他的程序都可以同步开发。如此一来,我们会接收到其他人员对Driver API的意见,有必要的话,也可以尽早修改设计。

此外,我们一般称Driver API为硬件抽象层(Hardware Abstract Level; HAL),它对嵌入式系统开发还有一项重要的意义。因为Driver API上层的程序都是和硬件无关的,所以负责模拟器的人员只要在PC上模拟出这些API的行为,其他上层的程序应该完全不需要任何更动,就可以在机器与模拟器上运行,这样的架构自然会让系统具备较佳的可移植性。

5.3 控制CPU

在Boot-Loader阶段,我们已经完成了CPU的初始化,但是在Boot-Loader阶段,我们只是简单地设定CPU的状态寄存器,以及中断矢量表的指针,而且还禁止了中断产生,所以CPU的功能还没完全开放,我们必须再设定一些寄存器,才能让CPU完全开放所有功能。
下图是一个CPU的架构图(CPU内整合许多外围设备的设计已蔚为风尚)

5.3.1 控制CPU寄存器

写汇编语言时,会遇到的寄存器如下:

■ 可以直接设定值的状态寄存器。例如,我们在系统启动后,要马上设定STACK存储器的地址,否则函数调用以及中断处理程序都会运行不正常。此时,我们会写如下的汇编语言去设定CPU的Stack Point(SP)寄存器。此外,我们可以从之前提过的CPU状态寄存器(PSR),从中知道CPU执行了某个指令之后的状态,也可以设定其中某个bit的值来控制CPU的行为,最常用的就是设定IE(Interrupt Enable)这个bit,可允许或禁止中断发生。
■ 不允许赋值的寄存器。如PC(Program Counter)寄存器,存储CPU接下来要执行指令的地址,当CPU自存储器获取了一个指令来执行时,PC寄存器会自动累加,程序不允许直接设定PC寄存器的值,只能通过jump、call等指令来改变执行顺序时,才会使得PC值跟着改变。

除此之外,还要一种叫做Memory Mapping 寄存器的寄存器
先不管硬件原理的细节,就写驱动程序的人来说,要控制接在CPU外部的芯片势必是通过CPU的PIN脚,可能只是简单的将GPIO脚(General Purpose I/O)设为High或Low来控制外部的chip。同理,外部的设备也可以通过GPIO脚将信号传回给CPU,或者驱动程序可以通过某种通信协议来控制I/C,如SPI或I2C或等。但无论是什么样的通信协议,CPU和外部chip间数据与命令的传递,还是要通过CPU的PIN脚
假使这个chip是整合到CPU内部,姑且不管它与CPU的core是怎么连接的,驱动程序肯定不是通过操作CPU的PIN脚来控制CPU内部的chip。CPU设计者为了让整合进来的chip确实像是CPU的一部分,会把该chip的特性包装起来,所以驱动程序工程师看到的就是一系列的寄存器,通过设定这些寄存器的值,等于通知CPU Core去操作整合进来的chip,而该chip要传递信息给驱动程序时,也是通过这些寄存器。
这种用来控制CPU内部chip的寄存器,与刚刚提到的CPU内部寄存器是完全不同的性质,CPU内部寄存器有自己的名字,而内部外围设备的寄存器只有一个地址,所以通常我们称之为Memory Mapping Register,而这种控制内置chip的方式称为memory mapping I/O。

5.3.2 控制中断处理器

驱动程序在控制中断系统时要注意的事项有

■ Boot之后应立刻禁止中断产生(应该都是设定PSR),因为除了中断系统还没设定好之外,可能产生中断的硬设备也还没初始化完毕。

■ 内置中断处理器的CPU会规定哪些PIN脚是可以产生中断的,而哪些PIN脚会产生什么中断通常也是定义好的。例如,内置USB Controller的CPU,可能就会规定某根PIN脚用来接收USB设备的中断信号,所以在硬件设计时,一定要注意CPU与中断有关的相关规范。举例来说,如果键盘输入接到CPU不会产生中断的PIN脚,则程序只能用polling(查询)的方式,一直去察看有没有key被按下(该PIN脚的电位是否改变),这是很缺乏效率的设计。

■ 固件工程师必须知道什么设备接到CPU的哪一根PIN脚,是否会引发中断,如果会的话,又是第几号中断,一定要弄清楚相关的硬件设计,否则怎么有办法写该设备的驱动程序?

■ 设定中断矢量表的起始地址(通常都是通过寄存器来设定),中断矢量表可以写成一个C的数组,数组内的元素就是各个ISR的地址。

■ 我们的产品不见得会用到所有CPU支持的中断来源,也就是说,并非每一个中断矢量表的entry都有对应的ISR,但中断矢量表的每一个entry都必须有值,此时就必须为这些没有用到的中断来源写一个空的ISR。当这个空的ISR被执行时,就表示有不该产生的中断产生了,这可能是硬件设计错误或静电产生的误动作,必须通知硬件人员处理。

■ 驱动程序可以通过设定寄存器为每一个中断来源设定优先级(一般CPU都是8个等级),PSR寄存器中除了可以设定中断是否可以产生之外,也可以设定哪一个优先级以下的中断不会被CPU处理。

■ 一般情况下,除非产品有hard real time的应用,否则我们不允许巢状中断(ISR在执行时允许发生中断,于是ISR可能被中断,CPU去执行另一个ISR),即ISR执行时期禁止中断产生,这是因为巢状中断会出现很复杂的状况。此时,可以通过设定CPU允许产生中断的优先级。例如,一般中断的优先级都是6,而不允许任何等待或丢失的中断事件则设为7,平常ISR执行时设定CPU仅允许优先级7以上的中断产生,则所有优先级6的中断不会产生巢状中断,但优先级7的中断在任何时候都保证会被处理。

■ 有一个比较特殊的中断叫做NMI(Non-Maskable Interrupt;不可屏蔽式中断),顾名思义,就是无论如何都不会被禁止的中断,硬件设计时可以把hard real time的中断来源接到NMI上。

■ ISR也是一个函数,但有很多注意事项要遵守,尤其是critical section的保护,稍后我们会再提到。

此外,ISR不像一般程序是循序执行的,它会在任何程序在执行时被调用,可能发生的状况很多,所以我会要求对ISR进行更严格的测试。

5.3.4 Clock

所谓的Clock是维持CPU正常运行所需的基本时间单位
系统中的CPU和存储器应该要有一个统一的时间基准,我们称之为时序。例如,我们说某颗Pentium 4 CPU是2GHz的,则表示这颗CPU运行的基本时间单位为1/(2×1024×1024)秒,这里所说的基本时间单位可以简称为一个Clock。如果CPU一个Clock可以执行一个指令,那么一个Clock越短的CPU,在每1秒内就可以执行更多的指令,对使用者而言,这颗CPU的性能也就越好。
这就是所谓CPU‘超频’的原理!同一颗CPU使用不同的时序就会有运算速度不同的效果。
CPU的Clock是由外部的Clock Generator提供的,例如振荡器IC(之所以用振荡器IC是为了获取稳定的时序)

5.3.5 Bus & Chip Select

所谓的Bus应该包含Address Bus与Data Bus两种,CPU以及可用地址操作的芯片(以存储器居多)都会有类似称为A0、A1……A15……或D0、D1……D15……的PIN脚,前者就是Address Bus,后者则是Data Bus。CPU与所有的外部存储器主要就是用这些PIN脚串接起来
下图可以清晰的描述这个过程
刚刚我们看的那张时序图里就包含Data Bus与Address Bus的状态变化。首先,CPU通过Address Bus送出要操作的地址,如果是写入存储器的动作,CPU接着会送出要写入的数据到Data Bus上,存储器可从Bus上分别取得地址与要写入的值,并执行CPU要求的动作。反之,存储器从Address Bus上取得地址后,会把地址上的值放到Data Bus上,CPU则会自Data Bus上把值读入,从而完成从存储器上取值的操作。简单地说,CPU必须通过Data Bus、Address Bus与存储器连接,才能用指定地址的方式来存取存储器

问题来了,CPU和多个存储器连在一起,存储器怎么知道CPU要获取的地址是哪一个存储器里的呢?
CPU要使用某个芯片之前,通常要明确的指定或‘告知’。所谓‘告知’的方法就是改变存储器芯片上CS PIN(Chip Select)的状态,一般都是由High拉到Low,当芯片的CS PIN状态被改变时,即表示通知其准备开始工作。当CPU不再使用该芯片时,就把该存储器芯片的CS PIN状态设为High。这就是为什么即使Bus上串了许多存储器芯片,但同时只有一个芯片会动作的原因。

在系统有多个存储器芯片的状况下,CPU与所有存储器芯片当然是通过同一组Address与Data Bus串接,但是CPU会用不同的PIN脚去控制不同存储器芯片的CS PIN。例如,用名为CS#1的PIN脚控制SRAM,用CS #2控制ROM。当然,是由CPU负责控制存储器芯片的CS PIN,否则,编译器怎么可能预先知道产品的硬件设计会选用哪一根PIN脚来控制某个存储器,怎么有办法为诸如‘操作变量’的C语言程序代码产生汇编语言代码?你可以想象CPU内有一个对应关系的表格(如下图所示) CPU会根据要操作的地址,换算接在这个范围内的存储器芯片,是用哪个PIN脚控制其CS PIN。在送出地址之前或同时,CPU就会把该PIN脚拉Low(设为低电位)。如上图所示:一个area对应一段地址空间,你可以想象每个Area对应一个CPU的chip select PIN,一个chip select PIN可以接一颗存储器IC,所以,当你的程序要存取某个地址时,CPU会根据地址判断是哪个Area,再把相应的chip select PIN拉Low,就可以存取到正确的存储器IC了

因为系统中的每一个存储器芯片会有不同的特性,例如,存取速度、基本的数据宽度(8bit、16bit或32bit)等,所以CPU在操作不同存储器芯片时应该会有不同的行为模式。如ROM和CPU比起来算是较为慢速的设备,假设CPU要到ROM读一笔数据,当CPU将地址通过Address Bus送出后,可能要多等待几个Clock,该存储器才来得及将数据放上Data Bus。其中等待的Clock数就称为waiting cycle,这也就是上面所说得行为模式之一。
理论上,CPU操作某存储器要插入几个waiting cycle是可以计算的,CPU的data sheet内都会有此信息,通常是一个公式,输入CPU的时序(如24 MHz)以及存储器芯片的执行速度(如90 ns),就可以得出理论的waiting cycle数。实际上,我们开始会把这个值设大一点,即CPU存取该存储器的速度会慢一点,等系统可以稳定的执行之后,再来慢慢调整系统各部门的timing。

最后,值得注意的是,并非串在Bus上的存储器芯片才有Chip-Select PIN,许多非存储器的芯片在控制之前都必须改变其Chip-Select PIN的状态。

5.3.6 GPIO Port

CPU是整个计算机系统的大脑,但它总是要控制外部设备,最简单的像是点亮LED、发出一段beep(哔~~)的声音,或是将计算结果输出到LCD上。除了输出,CPU也可能要接受并处理各种形式的输入,如按键、温度变化、网络来的封包等。当然有的CPU就是为了处理特殊的输出或输入而设计,但基本上大部分的CPU在设计时,不可能知道客户会拿它来设计什么样的产品,所以一定会设计许多用来做通用输出/输入的PIN脚,我们称为GPIO Port(Gen eral Purpose I/0)。

假使CPU是大脑,那么,这些通用GPIO Port就如同神经线,用以与外部的设备沟通(数据总线(Data Bus)和通用输入输出(GPIO)是电子和计算系统中的两种不同的通信接口。数据总线用于在微处理器、内存和其他系统组件之间传输数据,而GPIO是一种更为通用和简单的接口,用于读取和控制单个的数字信号。GPIO可以用于简单的设备通信,但它的速度和数据宽度通常比数据总线要小。在某些情况下,GPIO可以用于实现简单的数据总线,但通常会有性能的牺牲)

由CPU输出到设备比较简单,只要在合适的时间设定Output PIN的电位即可。例如,有些非存储器的IC在操作前也需要先控制其CS(Chip Select)PIN,硬件设计时,就要用到一根CPU的Output PIN连接CPU与该IC的CS PIN。
CPU的Input PIN可分为会引发中断与不会引发中断两种。硬件中断的基本原理之前已陆陆续续提到,视外部设备的特性来选择要使用哪种Input PIN,因为中断会打断目前正在执行的程序,也可以说ISR是优先级最高的程序,这样的输入很适合不定时发生的事件,例如,使用者压下按键、网络送来一个封包、DMA完成、负责传输数据的buffer空了(通知CPU尽快送数据来)或者满了(通知CPU尽快把数据取走)等。当外部设备通过CPU的Input PIN产生中断,CPU得知有‘紧急’事件发生后,就可以通过其他的I/0 PIN与该设备沟通,如自外部设备中接收数据,此时使用的Input PIN就无需具备中断的功能。

除此之外,CPU也会提供寄存器,让固件工程师可以设定每一个中断的属性,中断的属性分为两大类。

■ 高电位变为低电位时产生中断,或者低电位变成高电位时产生中断。
■ Edge trigger或Level trigger。

一根GPIO PIN就只有High、Low两种状态,只能做开关式的控制。多根PIN脚再搭配时间,就可以组合出各种复杂的通信协议

5.3.7 NOP与实作Busy Waiting的时间区段

控制信号彼此之间都有时间的关系,我想知道我们的程序是如何精准地控制时间?
我知道有些操作要求的Timing非常严格,举例来说,假设CPU通过两根PIN脚与某个IC连接,一根是CS(Chip Select),另一根是输入PIN—P#1,data sheet里说明如何从IC取得数据的时序图如下图所示。其中规定,当CPU把CS由High被拉到Low时,该IC会开始动作,经过T1的时间后,IC会把数据放在P#1上(这里所谓的‘数据’不是High就是Low,或者说不是0就是1),维持的时间为T2。在这种状况下,程序如何在正确的时间里到P#1上获取正确的数据?” 刚刚已经讲解过,要把某根输出PIN做High/Low变化,以及要从某根输入PIN读出状态都可以通过CPU的寄存器,基本的控制应该没问题吧!这个例子还算简单,从时序图中,我们可以看出程序该做的事情有

Step01:把CS PIN设定为Low。
Step02:等待T1的时间。
Step03:立即从P#1读出状态。
通常执行Step03的时间不会大于T2,所以上述的步骤中省略了T2。假设T1 = 10ms,先不要管操作CPU PIN脚的细节,我们可以将以上的步骤写成如程序代码9-13所示的程序。

个程序很简单,但是drv_wait_1ms()要如何implement?难道是用Timer吗?我们在开发驱动程序时,最常用,也是最简单的方法,就是写一个什么事情都不做的循环
另外,先前提过,我们可以计算出cpu的clock是多少,cpu产商在出货时,也会跟着出一个图表,里面记载着每条汇编指令执行所需的clock数量,这也就能计算那个循环要执行多少次才能达到我们想要的时间。

5.3.8 Power Management & Low Power

Power Management就是所谓的电源管理啦。
些电子产品的确是不需要太严谨的电源管理功能,如家电或车用电子,但如果换成产品吃的是电池的电源,如手机、电子字典、电子书、玩具等,要使用者忍受每天充电、没几天就要换电池,或者在外面用到一半突然没电等状况,这样的产品就算功能再强大,也不会是成功或热销的产品吧
当然,硬件设计和系统耗电有绝对的关系,但当系统闲置时,是否需要关闭外部设备的电源、调整PIN脚的状态、让CPU降频或切换CPU的模式等则是软件的工作
我们在此先不讨论硬件如何设计会比较省电,这是硬件工程师的工作。我们的重点是:系统在不同的执行状态应该要对应不同的耗电状态。简单地说,在不同状态下,系统要关掉用不到的设备,不能关掉的设备就要让其尽量处于最省电的状态
通常情况下,一个手机的应用耗电量如下图所示(从上到下依次耗电下降)

■ 通话或上网
■ 拍照、玩游戏、播放MP3或动画
■ 使用手机其他的功能
■ 待机(背光关闭、屏幕亮着)
■ 待机(屏幕关闭)
■ 关机

根据产品特性不同可以区分出不同的耗电等级,一般来说,电源管理程序会把执行状态至少分为以下3种等级。

■ Full Run Mode——系统中所有设备全速执行。
■ Idle Mode——待机状态,系统在等待下一个命令,目前没事可做。以手机的例子来说,就是屏幕还亮着的待机状态。
■ Sleep Mode——睡眠状态,使用者看起来会觉得机器象是关机,但仍可通过某种输入设备立刻将其唤醒。

要做到省电,可以从以下几个方向着手:

■ CPU:这是整个系统的心脏,不能轻易停掉,但‘跳慢一点’当然会比较省电。一般用于嵌入是系统的CPU,都会提供以下3种方式来控制CPU的用电。 
  □ Clock调整。例如,输入CPU的Clock是48 Mhz,CPU内部可以设定将其升频(如x2,变成96 MHz),或降频(如除以2、除以4、除以8等)后的频率当作CPU的工作频率。
  □ 有的CPU设计为可执行于不同工作电压下,当然,比较低的电压会比较省电,但执行效率通常较差。
  □ 执行状态调整。CPU通常会提供诸如‘halt’或‘sleep’的指令,执行后可以让CPU停止一些CPU内部的功能,并暂时停止执行指令,如图9-22所示。顾名思义,halt是暂停的意思,sleep则是睡眠的意思,两者同样会让CPU暂停执行指令,但后者显然会关掉比较多的CPU功能,当然比较省电。当产生特定的中断(一般会使用Timer、key、touch panel等中断来源来唤醒CPU)时,就可以把CPU从halt或sleep mode中唤醒,继续获取指令执行(CPU醒来后执行的第一行指令一定是中断处理程序)。

■ 关闭暂时不使用的设备(包含外部设备以及整合进CPU内部的设备)的电源:这个当然必须硬件设计配合,首先,该设备及相关组件的电源供给必须独立,接着硬件设计必须提供可以控制该设备电源切换的开关,这样驱动程序才可以通过Port I/O控制开关,达到真正切断该设备电源的目的。基本上,设备的电源切断后就不应该再耗电。

■ 切换内/外部设备的执行模式或工作电压:和CPU一样,有些设备不能任意切断电源,所以就会提供不同的执行模式,但基本上不会像CPU那么复杂,通常就是Full Run Mode与Standby Mode两种模式。驱动程序必须根据data sheet内的规定,在暂时用不到该设备的时候,让其进入较省电的Standby Mode。

■ 将CPU的各个PIN脚逐一切换为较省电的模式:用于嵌入式系统的CPU为了节省PIN脚、降低成本等因素,它的某些PIN脚可能是多用途的,当这些PIN脚被设定成不同的用途,就可能有不同的耗电状态,所以当某根PIN脚暂时不使用时,必须将其设定为最省电的用途。这是项挺麻烦的工作,每个CPU的设计逻辑都不一样,有时候甚至要用trial-and-error,实际测量在不同设定下的耗电流。虽然如此,我们还是有几个简单的基本原则。
  □ 通常GPIO会比其他的设定(如AD Port、Data Bus等)省电。
  □ 通常Output PIN会比Input PIN省电。
  □ 通常将Output PIN保持在低电位,会比保持在高电位省电。
  □ 如果GPIO有被电路拉高(Pull High)或拉低(Pull Low),逆势而为通常会比较耗电。
  □ 以上原则不见得适用于所有CPU, IC从设计到制造有太多的状况会发生,有可能是IC设计上的特殊考虑或bug,也有可能是工艺上的瑕疵,造成某些IC的耗电特性与一般原则不同。这种状况并不罕见,通常原厂提供的data sheet或sample code中内会特别标明。

我们的硬件设计很难考虑到这些,如果硬件设计没对应好,会使得原有的固件也很难做这就是为什么固件和系统人员必须参与硬件设计的原因了。就算再没空,至少都要参加电路图的review会议,并对硬件设计人员讲解电源管理程序的基本架构,例如,系统有几种耗电状态、idle mode和sleep mode有哪些设备必须关掉、哪些不用等。固件人员必须确认电源管理的相关硬件设计是什么,包含:哪些设备具有独立电源、该设备的耗电流是否可以单独测量、用哪根PIN脚切换设备的电源开关、CPU各个PIN脚在各个执行状态应如何设定较省电等。如果事前不做这些沟通,等板子回来后,你就会发现有些设备的电源无法控制,或者使用了不合适的PIN脚导致耗电升高

举个常会犯错的例子。硬件工程师经常忽略有些CPU PIN脚的状态(High或Low)会影响耗电流,假设硬件工程师选了一根CPU的GPIO Port当做切换某个设备的电源开关,并定义低电位是切掉电源,高电位是开启电源,这样的设计乍看没有问题,一旦用来控制电源开关的这根PIN脚的特性是维持低电位时较耗电(例如,这根PIN在CPU内部有pull-high电阻,要把被拉高的电位维持在低电位,当然需要更多的电)。为了关掉这个外部设备的电源,结果却使CPU耗了较多的电

最后对电源管理做个结论。一般人以为电源管理就是把机器的总体用电量尽量的调低,但这只说对了一半。基本上,只要关闭当下不需使用的‘设备’(例如,不播音乐时,就可以把译码器与放大器关掉),并让每个设备运行在可正常运行的最低频率或最低电压,就可以做到低耗电。但整个系统中包含了许多设备,尤其是大部分设备都整合到一颗SOC里的状况,这些设备往往会互相影响。例如,设备A与设备B共享Clock的来源,在某个电源配置不会用到设备B,却需要设备A全速运转,这样的程序就会出问题。

正确的做法是:根据软件的需求与硬件的限制,系统设计者可以归纳出这个产品应该包含的所有电源模式,并在系统中设计一个模块——Power Manager,由其统一管控。

■ 系统在什么状况,应该要切换到哪个Power Mode。
■ 在某个Power Mode下,系统中的CPU与各个设备的耗电设定。

实际在implement电源管理系统时,还必须注意以下事项

■ 同一个系统常会应用在不同的硬件设置上(或说电子产品可能因成本的需求,取消某硬件功能,或替换功能相近的硬件设备),所以必须要求所有驱动程序中跟电源管理有关的行为,都保持一致的style与interface,并且符合上层Power Manager的规范。通常我们会将所有驱动程序应该遵守的规范定义在HAL (Hardware Abstraction Layer)API中。

■ 驱动程序只提供电源管理的机制(Mechanism),由Power Manager决定电源管理的策略(Policy)。也就是说,Power Manager需明确定义各Power Mode的配置,并根据该配置去调用HAL中相关functions。
例如,假设有一个Power Mode ‘IDLE’,它的配置定义为‘1.CPU Clock降至最低;2.除了LCD Controller之外,关闭所有的设备’。当Power Manager切换到此Power Mode时,要做的事情就是根据Power Mode配置定义,调用驱动程序公开的电源管理API。

■ 若说驱动程序提供mechanism, Power Manager制定policy,则启动或切换Power Mode的时机则由系统模块或应用程序来决定。因此,Power Manager必须提供足够的Power Mode与API让其他程序调用。

■ 无论是否允许应用程序主动改变Power Mode,系统都必须根据系统架构主动切换Power Mode。例如,当系统中的idle task被唤醒执行时,这表示系统暂时无事可做,所以idle task必须主动将Power Mode切换到低耗电量的等级,从而满足系统架构中对电源管理的设计要求。

■ 除了 Power Mode切换外,电源管理还有一个重要的topic——低电压与突然断电的处理。系统必须在这些‘紧急’状况发生时,抓紧时间做一些必要的处理,以避免重要系统信息的丢失,造成下次上电开机时发生不正常的现象。

5.3.9 断电前处理

电池的放电特性如下图所示:

所谓的低电压事件,指的是电池到达电压陡降的这个时间点
这个事情麻烦的地方在于‘要设定哪个电压值当作低电压的临界点?’。若设太高,虽然争取到充裕的时间,但难免浪费了电池可用电量,也减少了机器实际可操作的时间;若设太低,才检测到低电压,还来不及处理任何事情,CPU或其他设备就已经没电罢工了。所以实际上我们会至少设两个低电压等级,一个是wamning level,另一个是fatal level。前者系统仍有充裕时间备份数据与警告使用者,而后者通常就必须强制关机了。

另一个是突然断电的状况,可能是使用者不小心拔掉插头,或机器经撞击将电池弹出。一般人会认为马上就应该断电了,但对CPU的运算速度来说,能争取到几个mini-second就能执行不少指令了,而且电路板上或多或少都会有些电容,这些电容平常的主要作用可能是用来稳压。但在这种状况下,电容里的电可以使系统再撑一小段时间。所以这种突然断电的状况是有办法处理的,但重点是时间有限,系统必须把握时间做真正必须做的事情。

无论是低电压或突然断电,系统能判断到的硬件事件都是电压已经下降到某个临界值。

值得注意的是,像这种不正常的突然断电可能会使CPU运行不正常,尤其是瞬间断电又上电的状况,这会使CPU没有正常的reset。换句话说,通常低电压处理的最后一件事情,就是一定要保留时间让CPU来得及reset(主控IC通常都会有RESET PIN,控制这根脚就能让CPU RESET,或者CPU会提供reset的instruction,只要用Inline Assembly执行这个指令即可)

5.3.10 充电(Charger)控制

有的产品会使用充电电池(如手机等手持式设备),既然已经说到了电源管理,我们就顺便来谈谈充电(charger)控制。
因为锂离子电池有高的能量密度、高的工作电压、无记忆效应、寿命长、无污染、重量轻、自放电小等优势,所以目前的电子产品多采用锂离子电池。但对锂离子电池充电仍必须考虑一些事情,否则轻则充不满或减损电池寿命,重则造成电池变形、漏液,甚至发烫爆炸等现象。偶尔还是会看到手机电池自燃或爆炸的新闻,主要原因可能是电池质量不好或充电控制出了问题。
所有你在机器或板子上看到的东西都是要控制才会动的,只是到底是由工程师通过编写驱动程序进行控制,还是利用其他硬件或IC来帮忙控制罢了。 充电电池可以外加一个Charger IC来做充电控制,也能通过系统中的驱动程序来控制,当然也可以如你所说的,完全不加控制。
其实不做控制,这种充电方式就叫做’直充‘,这种方式就需要依赖硬件电路中的过充保护电路。这指的是电池充满后,会自动停止充电。然而,很多厂商比较黑心,可能保护电路压根就没有,这时候你不做控制,就坏了
电池会出问题的原因不少,但还是由电池或电源供应器质量不良所引起的居多,锂离子电池充电的算法其实并没有那么复杂,主要目的就是要尽量避免过充与过放的状况发生。我们先来谈谈何谓过充与过放。

■ 过充:过充电是指电池经一定充电过程充满电后,再继续充电的行为。锂离子电池在设计时,负极容量比正极容量要高,因此,正极产生的气体通过隔膜纸与负极产生的镉复合;故一般充电情况下,电池的内压不会有明显升高。但如果充电电流过大,或充电时间过长,产生的氧气来不及被负极消耗,就可能造成内压升高,电池变形、漏液等不良现象。同时,其电性能也会显著降低。
我们可将锂离子电池的充放电原理,用生活中常见的泡沫现象来比喻。锂离子电池如同一堆肥皂泡沫,泡泡内存储的就是电能。当充电时,汽泡会随着充电时间加长而不断增大,当超过其极限值时,汽泡就会破裂,此时即损坏了锂电晶型,造成永久性损坏。当过度放电则会造成汽泡塌陷、消失,这样下次充电时汽泡也充不起来,而造成锂电失效。如何控制汽泡不充爆和汽泡不过度塌陷?就必须要用保护电路加以严格控制。当然,优质的电池芯和精确的控制算法可大大地延长电池的使用寿命。
■ 过放:电池放完内部存储的电量,电压达到一定值后,再继续放电就会造成过放电。过放电会使电池内压升高,正负极活性物质可逆性受到破坏,即使充电也只能部分恢复,容量也会有明显衰减。
锂离子电池过放之后,电压会降低到2.5 V以下,如果过放不是很严重,通过小电流涓流充电,还能够挽回一些容量,使电池不至于废弃。但是如果对已经过放的.电池直接充太大电流,则可能会更彻底的摧毁它。

所以Charger控制的重点是:为了避免过充,当快充满的时候,必须放慢充电的速度,当电池电压充到某个临界值时就必须停止充电。而要避免过放就如同我们前面说的低电压控制,在电池快没电时,电池电压会突然陡降,此时必须停止系统运行或自动关机,以避免电池电量被过度消耗,造成永久性的破坏

接下来介绍一下充电的算法,请先看一张图。
纵轴是用来充电的电流大小,横轴是时间,我们可以看到电池充电分为3个阶段。

■ 小电流启动:charger会先用小电流来充电,为了就是避免当时电池电压过低。若是马上使用大电流可能会对电池有影响。

■ 定电流充电(CC):使用固定电流,将电池快速充到一个特定电压(接近充饱的电压)后,此时就应该放慢充电的速度,以避免过充。

■ 定电压充电(CV):当电池使用定电流充到某个电压值后,必须要使用小电流来慢慢充电,否则会因为电池的内阻效应,在大电流时会造成一个电压差,进而影响电池的饱和度。通常需要控制充电电压为电池的额定电压±50mV以内,以4.2V锂电来说,就是4.15V〜4.25V。

一般我们就是采用这种先定电流充电后,再定电压充电的充电方式,它是在充电开始时先用定电流充电,当充电到某电压之后,再改用定电压的充电方式,直至充满为止。CC/CV充电方式的优点是它可以缩短电池的充电时间,又能防止电池过充电。
下图是一张电压和电流在一个充电周期内的变化图

5.3.11 ADC & DAC

在实现charger算法时,必须随时可以判断电池的电压值,才知道什么时候该停止充电。但电压值是连续的,或是模拟的值,而CPU只能处理0、1组成的数字数据,我们的驱动程序需要通过模拟/数字转换器——ADC(Analog To Digital Converter),做模拟/数字信号的转换。
同样的,CPU想要把信息输出到外界,也需要通过DAC,将计算的数字结果转换为模拟信号。
先说明ADC。ADC的运行原理相当简单,就以测量电池电压为例,输入AD Port的电压范围是0V~3V,如果该AD Port的精度是10bit,即模拟转为数字后的值为0〜1023之间(210=1024)。举例来说,当电压值为0时,转换后的AD值为0,当电压值为最大值3V时,则转换后的AD值是1023。至于在0V〜3V之间的输入,则会以线性的方式作转换。例如,输入电压为2.2V,则取得的AD值为:(2.2/3.0)×1024 =751

对驱动程序而言,自AD port取得的值(通常是通过Memory Mapping Register取得),反算可得真正的电压值。同上例的状况(电压值范围0V〜3V、AD port精度10bit),假设得到的AD值是620,则反算可得输入的电压值为:(620/1024)×3V=1.82V

外部芯片只要能将模拟的输入(如温度、音量等级、震动程度等)转换为不同的电压输出,再连接到CPU的AD Port,则程序就可以在控制的精度之下算得外界模拟的输入。
其实一般DAC IC的功能比你想象得高端,根据其定义的应用领域,CPU只要将数字数据传给DAC IC,它就会帮忙做完所有的事。举例来说,有一颗称为“Ultralow Noise20-Bit Audio DAC”的IC,系统只要把PCM音频数据传给它,它会执行译码,并将结果转换为模拟数据输出,有的Audio DACIC甚至可以直接处理MP3的数据。再例如,一颗名为“Voltage Output 10-Bit DAC”的IC,系统甚至只要传给它诸如1.8、3.3等数字,就会转出1.8V或3.3V的电压。

所以系统在使用DAC IC时,只需要注意DAC IC与CPU之间的接口(例如Audio DAC都是用I2C或I2S与CPU沟通),以及数据传递的protocol即可。 但有些比较简单的应用,例如,控制马达转速、调整LED亮度、输出较低质量的声音(如语音或特效音),若还要通过另一颗DAC IC来做,难免有点杀鸡用牛刀之嫌,而且还增加成本与系统复杂度。我们还是需要让CPU模拟信号的方法,最常用的就是PWM(Pulse Width Modulation),原理如下图所示:
PWM的原理很简单,固定时间段内高电压所占的比例称为Duty Cycle,而不同的Duty Cycle代表不同模拟输出的值。简单地说,就是对欲输出的模拟信号进行编码。许多SoC本身就提供PWM输出的功能,就算主控IC没提供PWM输出,驱动程序也能够通过一个Timer计时,控制某根IO脚的Duty Cycle。

5.3.12 Watch-Dog

电子产品在运行时常常会受到来自外界电磁场的干扰,造成程序‘乱跑’或陷入死循环,使得系统无法继续工作,并造成整个系统的陷入停滞状态.Watchdog Timer的主要功能就是当程序发生不可预期的错误(程序本身的漏洞、程序跑进无穷循环,或外部信号干扰,导致产生错误动作)时,会自动重置系统,以确保系统稳定。
顾名思义,Watchdog的意思是看门狗,而Watchdog Timer当然也是一种Timer。当系统enable了Watchdog Timer后,它就会从某个已设定好的值开始倒数,数到0时,就会做产生一个最高优先级的中断或直接让CPU reset。
当Watchdog Timer开始倒数时,为避免timeout后Watchdog去reset CPU,系统必须及时重新设定Watchdog的counter,我们称之为‘喂狗’,以此表示系统还活着。若某个程序(任何程序,可能是驱动程序、系统程序或应用成程序)在循环内运行了太久的时间,又没有及时重新设定Watchdog的counter,则Watchdog会以为系统死机了,自然会去reset CPU。

5.3.13 CPU初始化

CPU也是系统的设备之一,当然也需要驱动程序,它必须在其他程序开始执行前就做好CPU的初始化工作,包含:

■ 设定CPU内部寄存器,包含PSR与SP等。

■ 设定中断矢量表。

■ 设定CPU内部各单元(CPU core以及内置设备)的Clock。

■ BUS设定——设定各个外部存储器的特性,包含要插入几个waiting cycle、操作该存储器的基本单位宽度(是16bit或32bit)等。

■ 设定CPU各个PIN脚的用途使其符合系统或应用程序的需求。

■ 设定CPU的执行模式,刚开始应该是在full run mode。

■ 其他内部设备的初始化。

5.3.14 CPU内部还有什么?

随着半导体技术的进步与IC设计产业的蓬勃发展,越来越多的CPU整合了各式各样的功能。有些CPU就是一个具体而微的系统,CPU里面甚至有ROM以及RAM,几乎只要供给电源就可以独立运行。这种高度整合的CPU简化了硬件设计的复杂度,同样让固件工程师较少有机会碰到在完全不稳定的硬件平台上开发的困境。
所以CPU内部还有很多其他的东西,例如:

■ Timer:前面说过,CPU必须有稳定的Clock输入才可运行,CPU将输入的Clock升频或降频后,提供给CPU内部的各单元,或者输出到其他的外部设备,使其时序可以与CPU同步。应用这个Clock,CPU可以轻易地提供计时或闹铃的功能,我们称之为Timer。CPU内可以提供多个可独立运行的Timer,每一个Timer都会有其相应的中断号码,通过许多Memory Mapping Register来控制Timer。
驱动程序要先设定Timer的属性,例如,该Timer启动后多久到时;Timer的基本时间单位为Clock,假如Timer的Clock来源是24Mhz,表示(24× 1024 × 1024)个Clock就是1秒钟,或者可换算基本的时间单位为:
1秒/(24×1024×1024)=40×10-9秒=40ns(纳秒)
接着写作相应的timeout中断处理程序,则该ISR会从Timer启动起的指定时间后被执行。电子产品的应用或多或少都有计时的需求,以驱动程序来说,要驱动某些设备或实现某些protocol要有很精准的Timing。以系统程序来说,Timer几乎是必备的功能,并且基于驱动程序提供的基本计时功能,扩展出更复杂、以手表时间(秒、分、时)为主的Timer功能。例如,每N个mini-second(千分之一秒)产生一次timeout,连续产生M次等。
应用程序使用系统提供的Timer功能时,只要指定callback function即可,当然不用自己写ISR。


■ RTC(Real-Time-Clock):上述的Timer是用来做较精准的计时,如mini-second(10-3秒)或micro-second(10-6秒)等级,如果是应用在计算以秒或分为单位的计时反而不适用。以16 bit Timer为例,它最多仅能计时到216(65536)个时间单位,通常还远少于1秒钟,要累积很多次timeout才能计时到1秒钟,则为了达成简单的时钟功能,系统将一直处于中断产生的状态。
RTC也是一种Timer,但是它的精度比较粗,通常是百分之一秒的等级,程序可以将其调整到每隔1秒甚至1分钟才产生中断,如此一来,可以轻易达成时钟或日历的功能。因为这种每分钟才产生一次的中断并不会耗掉太多的电,系统被中断唤醒后,只需要做简单的累加计算即可,即便系统处于sleeping mode,仍然可以维持长时间计时。


■ DMA(Direct Memory Access;直接存储器存取):DMA允许某些外部设备可以独立地对系统存储器进行读取或写入,而不需牵涉到CPU。在PC中,许多硬件的系统会使用DMA,包含硬盘控制器、绘图显示卡、网络卡,以及声卡等。当CPU要从外部设备(如硬盘、NAND Flash或网络封包)读取大量的数据到主存储器时,如果不使用DMA的话,CPU必须自设备逐一读出数据,通常是将数据先读到一般寄存器,然后才写入主存储器。
在做这种外部设备到主存储器的数据传输时,CPU除了I/O之外什么事也不能做。如果传输的数据量很大,或者传输的一方是像硬盘般的慢速设备(不管硬盘技术再怎么进步,性能还是不会比存储器快),系统在一段长时间内只能执行数据传输的动作,无法做计算的工作,这对嵌入式系统中最宝贵的资源——computing power是一种严重的浪费,简单地说,就是杀鸡用牛刀。应该要把简单的I/O工作交给DMA控制器去做即可,同时间CPU可以执行更重要的程序。
用于嵌入式系统的CPU一般都会内置DMA控制器,所以都可通过寄存器去控制它。在进行数据传输之前,驱动程序要先设定好来源地址、目标地址以及要传输的数据量,上述的来源与目标地址可能是存储器的地址,也可能是Memory Mapping Register。例如,写入某寄存器即代表输出到某设备,或者某设备会将数据逐一存入某寄存器之中。当驱动程序设定好传输数据的信息后,就可以下指令启动(Trigger)DMA控制器。接着,系统可以去做其他的事情或切换到其他的task,当数据传输完毕后,DMA控制器会产生中断通知CPU。


■ 其他内置IP:目前业界CPU内部功能整合度之高已超乎想象,有些几乎是为特定产品量身定做,例如,手持式设备、手机、MP3随身听等都有整合型的CPU可选用。图9-29所示是一个用于MP3 Player整合型CPU的实例,图中大方框内的方块就是整合在CPU内部的芯片,你能想到MP3 Player该有的功能都包含在里面了,包含MP3译码器、LCDController、NAND Flash Controller、USB Controller等。产品开发商用了这种CPU,软、硬件都不需要太大的effort,就能整合出完整的产品。如此一来,不但缩短研发时间与经费,还可加速产品上市的进度。

■ MMU(Memory Management Unit):MMU系统可用来实现虚拟存储器、虚拟地址空间、page on demand等大型操作系统的存储器管理功能,一般只在WinCE或Embedded Linux专用

5.4 存储器

一般情况下,iot的存储器分为以下几种类型:

■ 可用地址读取的只读存储器:如EEPROM与Mask ROM等,通常用来存放程序与执行时期不会更动的数据,程序可以直接在ROM里面执行,除非有性能的需求,否则没有载入到RAM的必要。CPU或驱动程序可以通过地址或指针直接读取ROM里面的数据,但执行时期无法将数据写入ROM里。CPU是通过Address Bus与Chip select寻址,利用Data Bus取得ROM里的数据。

■ 可用地址读取写入的RAM:和ROM一样,CPU通过Address Bus与Chip Select寻址,利用Data Bus存取(读写)RAM里的数据,但是RAM的内容是可以在执行时被改变的。
  □ Static RAM:顾名思义,只要持续供应电源,SRAM里的数据就得以保存;SRAM耗电流比DRAM低,价格比DRAM贵。对固件工程师而言,SRAM的控制很简单(只要设定CPU与SRAM之间的timing即可,几乎不需要其他的控制就可以存取SRAM了)。对硬件工程师来说,SRAM的线路设计非常简单。虽然SRAM的好处多多,但为了成本考虑,通常在系统中,不会使用size太大的SRAM。
  
  □ Dynamic RAM: DRAM有个要命的特性,是必须持续refresh才能维持数据的正确性,所以需要一个额外的DRAM控制器。驱动程序必须通过设定DRAM控制器,需要设定的数据包含DRAM的size、存取的时序、refresh的特性等,DRAM才可以正常的运行。在嵌入式系统中,我们通常使用的是SDRAM(Synchronous DRAM),详细原理在此就不详述了,最大的不同点是CPU必须提供持续而稳定的Clock给SDRAM。SDRAM的好处是较SRAM便宜很多,如果系统需要较大的缓冲区,通常都会选择使用SDRAM。
  
■ 可用地址读取,但必须下命令写入的Flash(快闪存储器):这就是我们之前提到过的NOR Flash,驱动程序可以把它当作ROM使用,CPU通过Address Bus与Chip select寻址,利用Data Bus取得ROM里的数据。NOR Flash的内容是可以更改的,但不像写入RAM那么容易,即不是通过address与Data Bus,必须对NOR Flash IC下命令才行。此外,NOR Flash有个特性,写入时必须以block为单位(一个block可能是16K或8K Byte),而且写入前必须先做擦除(Erase)的动作。这个特性使得NOR Flash的写入很麻烦,且性能很差
所以一般NOR Flash的用途为:一个是当作ROM的代替品,如果程序有问题可以重复烧录,免除开Mask以及ROM的内容无法修改的风险;因为NOR Flash有断电后内容不会丢失的特性,所以NOR Flash的第二个用途就是用来存储或备份执行时期的数据,例如,使用者私人的设定或录音数据等(除了NAND Flash外,使用SPI或I2C控制的NOR Flash、EEPROM也可算入此种存储器分类,但其单位价格比NAND Flash贵上许多)。

■ 只能用命令读取和写入的Flash:MP3 Player与随身碟就是用这种NAND Flash当作大量存储介质。对固件工程师而言,它不能直接用地址存取,不论读写都要下命令,读取是以page为单位(512 Byte或2048 Byte),写入和擦除则是以block为单位(block是多个page的集合)。此外,NAND Flash有个比较麻烦的特性,它和硬盘一样会产生坏道(Bad Block),所以通常我们可以把NAND Flash当做比较小的硬盘使用,例如,MP3 Player或随身碟就会把NAND Flash格式化成FAT的格式。

5.5 控制其他芯片

驱动程序在操控CPU内置芯片与外部设备的不同点如下

■ CPU内部的设备性能通常比较好,而且绝对不会有硬件线路设计错误的问题,所以固件工程师直接写驱动程序即可。至于外部设备则可能发生大大小小的设计疏失,驱动程序编写时往往还肩负帮忙硬件调试的任务。

■ CPU内部的设备是用寄存器(Memory Mapping Register)控制,而外部设备则只能利用CPU的PIN脚与其连接,所以必须通过控制这些PIN脚来控制外部设备。

虽然外部设备是通过CPU的PIN脚来控制,但程序要设定CPU PIN脚的状态、从CPU PIN脚中取得设备的状态,或者外部设备可以通过CPU PIN脚产生中断,还是得通过CPU的寄存器。

驱动程序不会动时,通常我们会做两件事:

■ 把我们的程序和CPU的data sheet一起寄给IC厂商的FAE。

■ 把我们的程序和IC的data sheet一起寄给CPU厂商的FAE。

一般而言,都会得到有用的信息或线索,再不行的话就得请厂商的技术人员过来看看,或者我们带着板子、开发环境和notebook去拜访人家。

5.5.1 CPU与外围IC的连接与控制方式

■ GPIO

■ BUS

■ I2C

■ SPI(Serial Peripheral Interface Bus):SPI是一种高速串行传输总线的全双工通信协议,主要应用在CPU与转换器(ADC、DAC)、存储器(EEPROM、FLASH)、实时时钟(RTC)、传感器(温度、压力)、存储卡(SD card)等IC的串接。SPI是一种四线制串行总线接口,如图9-32所示。其为主/从结构(Master/Slave),主控IC只要4根PIN就可以串接多个slave组件,4条导线分别为
  □ SCLK—Serial Clock(自master输出)
  □ MOSI/SIMO—Master Output;Slave Input(自master输出)
  □ MISO/SOMI—Master Input;Slave Output(自slave输出)
  □ SS——Slave Selectactive low(自master输出)

Master为频率提供者(通常就是CPU或主控IC),可发起读取或写入slave组件的作业。和I2C协议不同(I2C的每个组件都有独自的ID),SPI的master必须拉低目前欲控制之slave组件的SS PIN,被指定的slave收到命令后才会开始动作(SS的作用如同之前说过的Chip Select PIN)。 除此之外,还有其他的控制方法:

■ I2S(Inter-IC Sound或Integrated Interchip Sound):是IC间传输数字音频数据的一种接口标准,采用序列的方式传输2组(左右声道)数据。I2S常被使用在传送音频数据到DAC中。由于I2S将数据信号和频率信号分开传送,其失真率非常小。

■ CAN(Controller Area Network):CAN为一序列总线,它提供高安全等级及有效率的实时控制,更具备了侦错和优先权判别的机制。在这样的机制下,网络信息的传输变得更为可靠而有效率;CAN在网络上支持多主机,在整个分布式系统中,每个主机负责各自局部的监听与控制,如图9-34所示。CAN被广泛的应用在各种车辆、医疗仪器与设备、航空/航海电子仪器、工厂自动化、工业机械控制等领域上。

CAN的优点可太多了 □ 采用独特的非破坏性Bus仲裁技术,优先级高的节点优先传送数据,能满足实时性要求。
□ 具有点对点、一点对多点及全局广播传送数据的功能。
□ 对传输数据进行CRC及其他校验措施,数据出错率极低,万一某节点出现严重错误,可自动脱离Bus,Bus上的其他操作不受影响
□ Bus只有两根导线,系统扩展时,可直接将新节点挂在bus上即可,因此,走线较少,系统扩展越容易,改型也相对灵活。
□ 传输速度快,在传输距离小于40 m时,最大传输速率可达1 Mbit/s。

■ 其他标准:UART(RS232、RS422)、Irda(红外线)、RS485、Ethernet、USB等。

5.6 ISR写作注意事项

1. 尽量不要在ISR内使用全局变量,如果一定要使用的话一定要做好保护,我们称这种应该受保护的程序区段为Critical Section。
保护的方法很多,最简单的是在进入critical section之前,先禁止中断的产生,所以在critical section中全局变量就不会被ISR破坏,从而达到保护的效果

2. ISR的第一个动作一定是将所有的CPU内部寄存器存储起来,一般都是存在Stack Memory内,而ISR的最后一个动作就是回复所有寄存器的值。

3. 调用外部函数时要确认其是否‘可重进入’(Reentry),如果一定要在ISR内调用非reentry的函数,则一般程序在使用同一个模块的函数时,一定要加以保护。
Reentry:可重入的程序代码指的是一段可以被多个任务(task或ISR)同时调用,而不必担心数据会被破坏的程序代码(如一个函数)。

4. ISR的程序或执行时间尽量不要太长,一来不易掌握全局变量以及外部函数的使用,二来ISR执行时通常不允许其他中断产生。如果ISR执行时间太长,也就是上层程序被暂停的时间过长,使用者会觉得系统的性能很不稳定

5. SR中调用系统功能时必须谨慎,尤其是与Task/Thread/Process切换有关的功能
有些应用需要在ISR内唤醒某个Task,使其立即去执行某件重要工作,此时,必须在ISR内调用诸如wakeup-task、send-message等这类的系统功能。这些功能无可避免的必须修改系统表格,如果发生中断的当下也正在操作这些重要的表格,则势必引起系统死机。要避免这种状况,一是做好critical section的保护,二是调用系统提供给ISR专用的系统功能

6. ISR尽量不要牵涉到复杂的算法,它的任务只是负责弄清楚发生了什么硬件事件(举例来说,哪个键被按下、转换完的AD值是什么、哪个Timer已经到时等),然后将硬件事件送给上层的程序,交由系统或应用程序决定要如何处理这个硬件事件

5.7 驱动程序调试

驱动程序调试较为麻烦,以5.3.7节的内容来看,因为调试时中止了系统运行,所以很容易出现在执行指令时CPU无法读取到pin脚上的正确值的情况
缓解办法如下:

1. code review: 先自己检查一遍;检查不出来就拉其他员工和主管一起帮忙看

2. 示波器: 要证明驱动程序正常运行最直觉的方法就是能在正确的PIN脚上量到和时序图一样的信号。

3. 逻辑分析仪(简称LA),它能把一段时间中多个PIN脚的信号都记录下来,研发人员可以事后慢慢分析。