• 单片机程序结构优化

    1、程序的书写结构 虽然书写格式并不会影响生成的代码质量,但是在实际编写程序时还是应该遵循一定的书写规则,一个书写清晰、明了的程序,有利于以后的维护。 在书写程序时,特别是对于While、for、do…while、if…else、switch…case 等语句或这些语句嵌套组合时,应采用“缩格”的书写形式。 2、标识符 程序中使用的用户标识符除要遵循标识符的命名规则以外,一般不要用代数符号(如a、b、x1、y1)作为变量名,应选取具有相关含义的英文单词(或缩写)或汉语拼音作为标识符,以增加程序的可读性,如:count、number1、red、work 等。 3、程序结构 C 语言是一种高级程序设计语言,提供了十分完备的规范化流程控制结构。因此在采用C 语言设计单片机应用系统程序时,首先要注意尽可能采用结构化的程序设计方法,这样可使整个应用系统程序结构清晰,便于调试和维护。 对于一个较大的应用程序,通常将整个程序按功能分成若干个模块,不同模块完成不同的功能。 各个模块可以分别编写,甚至还可以由不同的程序员编写,一般单个模块完成的功能较为简单,设计和调试也相对容易一些。在C 语言中,一个函数就可以认为是一个模块。 所谓程序模块化,不仅是要将整个程序划分成若干个功能模块,更重要的是,还应该注意保持各个模块之间变量的相对独立性,即保持模块的独立性,尽量少使用全局变量等。对于一些常用的功能模块,还可以封装为一个应用程序库,以便需要时可以直接调用。 但是在使用模块化时,如果将模块分成太细太小,又会导致程序的执行效率变低(进入和退出一个函数时保护和恢复寄存器占用了一些时间)。 4、定义常数 在程序化设计过程中,对于经常使用的一些常数,如果将它直接写到程序中去,一旦常数的数值发生变化,就必须逐个找出程序中所有的常数,并逐一进行修改,这样必然会降低程序的可维护性。因此,应尽量当采用预处理命令方式来定义常数,而且还可以避免输入错误。 5、减少判断语句 能够使用条件编译(ifdef)的地方就使用条件编译而不使用if 语句,有利于减少编译生成的代码的长度。 6、表达式 对于一个表达式中各种运算执行的优先顺序不太明确或容易混淆的地方,应当采用圆括号明确指定它们的优先顺序。一个表达式通常不能写得太复杂,如果表达式太复杂,时间久了以后,自己也不容易看得懂,不利于以后的维护。 7、函数 对于程序中的函数,在使用之前,应对函数的类型进行说明,对函数类型的说明必须保证它与原来定义的函数类型一致,对于没有参数和没有返回值类型的函数应加上“void”说明。如果需要缩短代码的长度,可以将程序中一些公共的程序段定义为函数。 如果需要缩短程序的执行时间,在程序调试结束后,将部分函数用宏定义来代替。注意,应该在程序调试结束后再定义宏,因为大多数编译系统在宏展开之后才会报错,这样会增加排错的难度。 8、尽量少用全局变量,多用局部变量 因为全局变量是放在数据存储器中,定义一个全局变量,MCU 就少一个可以利用的数据存储器空间,如果定义了太多的全局变量,会导致编译器无足够的内存可以分配;而局部变量大多定位于MCU 内部的寄存器中,在绝大多数MCU 中,使用寄存器操作速度比数据存储器快,指令也更多更灵活,有利于生成质量更高的代码,而且局部变量所能占用的寄存器和数据存储器在不同的模块中可以重复利用。 9、设定合适的编译程序选项 许多编译程序有几种不同的优化选项,在使用前应理解各优化选项的含义,然后选用最合适的一种优化方式。通常情况下一旦选用最高级优化,编译程序会近乎病态地追求代码优化,可能会影响程序的正确性,导致程序运行出错。 因此应熟悉所使用的编译器,应知道哪些参数在优化时会受到影响,哪些参数不会受到影响。 代码的优化 1、选择合适的算法和数据结构 应熟悉算法语言。将比较慢的顺序查找法用较快的二分查找法或乱序查找法代替,插入排序或冒泡排序法用快速排序、合并排序或根排序代替,这样可以大大提高程序执行的效率。 选择一种合适的数据结构也很重要,比如在一堆随机存放的数据中使用了大量的插入和删除指令,比使用链表要快得多。数组与指针具有十分密切的关系,一般来说指针比较灵活简洁,而数组则比较直观,容易理解。对于大部分的编译器,使用指针比使用数组生成的代码更短,执行效率更高。 但是在Keil 中则相反,使用数组比使用的指针生成的代码更短。 2、使用尽量小的数据类型 能够使用字符型(char)定义的变量,就不要使用整型(int)变量来定义;能够使用整型变量定义的变量就不要用长整型(long int),能不使用浮点型(float)变量就不要使用浮点型变量。 当然,在定义变量后不要超过变量的作用范围,如果超过变量的范围赋值,C 编译器并不报错,但程序运行结果却错了,而且这样的错误很难发现。 3、使用自加、自减指令 通常使用自加、自减指令和复合赋值表达式(如a-=1 及a+=1 等)都能够生成高质量的程序代码,编译器通常都能够生成inc 和dec 之类的指令,而使用a=a+1 或a=a-1之类的指令,有很多C 编译器都会生成2~3个字节的指令。 4、减少运算的强度 可以使用运算量小但功能相同的表达式替换原来复杂的的表达式。如下: (1)求余运算 a=a%8; 可以改为: a=a&7; 说明:位操作只需一个指令周期即可完成,而大部分的C 编译器的“%”运算均是调用子程序来完成,代码长、执行速度慢。通常,只要求是求2n 方的余数,均可使用位操作的方法来代替。 (2)平方运算 a=pow(a,2.0); 可以改为: a=a*a; 说明:在有内置硬件乘法器的单片机中(如51 系列),乘法运算比求平方运算快得多,因为浮点数的求平方是通过调用子程序来实现的,在自带硬件乘法器的AVR 单片机中,如ATMega163 中,乘法运算只需2 个时钟周期就可以完成。 即使是在没有内置硬件乘法器的AVR单片机中,乘法运算的子程序比平方运算的子程序代码短,执行速度快。如果是求3 次方,如: a=pow(a,3.0); 更改为: a=a*a*a; 则效率的改善更明显。 (3)用移位实现乘除法运算 a=a*4; b=b/4; 可以改为: a=a<<2; b=b>>2; 说明:通常如果需要乘以或除以2n,都可以用移位的方法代替。在ICCAVR 中,如果乘以2n,都可以生成左移的代码,而乘以其它的整数或除以任何数,均调用乘除法子程序。 用移位的方法得到代码比调用乘除法子程序生成的代码效率高。实际上,只要是乘以或除以一个整数,均可以用移位的方法得到结果,如: a=a*9 可以改为: a=(a<<3)+a 5、循环 (1)循环语对于一些不需要循环变量参加运算的任务可以把它们放到循环外面,这里的任务包括表达式、函数的调用、指针运算、数组访问等,应该将没有必要执行多次的操作全部集合在一起,放到一个init 的初始化程序中进行。 (2)延时函数 通常使用的延时函数均采用自加的形式: void delay (void){unsigned int i;for (i=0;i<1000;i++); }将其改为自减延时函数:void delay (void){unsigned int i;for (i=1000;i>0;i--); } 两个函数的延时效果相似,但几乎所有的C 编译对后一种函数生成的代码均比前一种代码少1~3 个字节,因为几乎所有的MCU 均有为0转移的指令,采用后一种方式能够生成这类指令。在使用while 循环时也一样,使用自减指令控制循环会比使用自加指令控制循环生成的代码更少1~3 个字母。 但是在循环中有通过循环变量“i”读写数组的指令时,使用预减循环时有可能使数组超界,要引起注意。 (3)while 循环和do…while 循环 用while 循环时有以下两种循环形式: unsigned int i;i=0;while (i<1000){i++; //用户程序}或:unsigned int i;i=1000;do{i--; //用户程序}while (i>0); 在这两种循环中,使用do…while循环编译后生成的代码的长度短于while循环。 6、查表 在程序中一般不进行非常复杂的运算,如浮点数的乘除及开方等,以及一些复杂的数学模型的插补运算,对这些即消耗时间又消费资源的运算,应尽量使用查表的方式,并且将数据表置于程序存储区。 如果直接生成所需的表比较困难,也尽量在启动时先计算,然后在数据存储器中生成所需的表,后面在程序运行直接查表就可以了,减少了程序执行过程中重复计算的工作量。 7、其它 比如使用在线汇编及将字符串和一些常量保存在程序存储器中,均有利于优化。 乘除法优化 目前单片机的市场竞争很激烈,许多应用出于性价比的考虑,选择使用程序存储空间较小(如1K,2K)的小资源8位MCU芯片进行开发。一般情况下,这类MCU没有硬件乘法、除法指令,在程序必须使用乘除法运算时,如果单纯依靠编译器调用内部函数库来实现,常常会有代码量偏大、执行效率偏低的缺点。 上海晟矽微电子推出的MC30、MC32系列MCU,采用了RISC架构,在小资源8位MCU领域有广大的用户群和广泛的应用,本文就以晟矽微电的这两个系列产品的指令集为例,结合汇编与C编译平台,给大家介绍一种既省时又节约资源的乘除法算法。 1、乘法篇 单片机中的乘法是二进制的乘法,也就是把乘数的各个位与被乘数相乘,然后再相加得出,因为乘数和被乘数都是二进制,所以实际编程时每一步的乘法可以用移位实现。 例如:乘数R3=01101101,被乘数R4=11000101,乘积R1R0。步骤如下: 1、清空乘积R1R0; 2、乘数的第0位是1,那被乘数R4需要乘上二进制数1,也就是左移0位,加到R1R0里; 3、乘数的第1位是0,忽略; 4、乘数的第2位是1,那被乘数R4需要乘上二进制数100,也就是左移2位,加到R1R0里; 5、乘数的第3位是1,那被乘数R4需要乘上二进制数1000,也就是左移3位,加到R1R0里; 6、乘数的第4位是0,忽略; 7、乘数的第5位是1,那被乘数R4需要乘上二进制数100000,也就是左移5位,加到R1R0里; 8、乘数的第6位是1,那被乘数R4需要乘上二进制数1000000,也就是左移6位,加到R1R0里; 9、乘数的第7位是0,忽略; 10、这时候R1R0里的值就是最后的乘积,至此算法完成。 以上例子运算结果: R1R0 = R3 * R4= (R4<<6)+(R4<<5)+(R4<<3)+(R4<<2)+R4 = 101001111100001 实际运算流程图见下图: 在实际的程序设计过程中,程序优化有两个目标,提高程序运行效率,和减少代码量。我们来看下本文提供的汇编算法和普通C语言编程的效率和代码量对比。 表1.1是程序运行效率的对比数据(可能会有小的偏差),很明显汇编编译出来的运行时间要比C语言减少很多。 汇编(时钟周期) C语言(时钟周期) 8*8位乘法 79-87 184-190 16*8位乘法 201-210 362-388 16*16位乘法 234-379 396-468 表1.1  乘法运算时钟周期对比表 表1.2是程序代码量的对比数据(可能会有小的偏差),汇编占用的程序空间也要比C语言小很多。 汇编(Byte) C语言(Byte) 8*8位乘法 15 34 16*8位乘法 19 96 16*16位乘法 31 96 表1.2  乘法运算ROM空间使用情况对比表 综上两点,本文介绍的乘法算法各方面使用情况都要比C编译好很多。如果大家在使用过程中,原有的程序不能满足应用需求,例如遇到程序空间不够或者运行时间太久等问题,都可以按照以上方式进行优化。 汇编语言最接近机器语言的。在汇编语言中可以直接操作寄存器,调整指令执行顺序。由于汇编语言直接面对硬件平台,而不同的硬件平台的指令集及指令周期均有较大差异,这样会对程序的移植和维护造成一定的不便,所以我们针对精简指令集做了乘法运算的例程,便于大家的移植和理解。 2、除法篇 单片机中的除法也是二进制的除法,和现实中数学的除法类似,是从被除数的高位开始,按位对除数进行相除取余的运算,得出的余数再和之后的被除数一起再进行新的相除取余的运算,直到除不尽为止,因为单片机中的除法是二进制的,每个步骤除出来的商最大只有1,所以我们实际编程时可以把每一步的除法看作减法运算。 例如:被除数R3R4=1100110001101101,除数R5=11000101,商R1R0,余数R2。步骤如下: 1、清空商R1R0,余数R2;2、被除数放开最高位,第15位,为1,1比除数小,商为0,余数R2为1;3、上一步余数并上被除数次高位,第14位,得11,11仍然比除数小,商为0,余数R2为114、直到放开第8位后,得11001100,比除数大,商得1,余数R2为111;5、上一步余数并上被除数第7位,得1110,没有除数大,商为0,余数R2为1110;6、上一步余数并上被除数第6位,得11101,没有除数大,商为0,余数R2为11101;7、按照以上步骤,直到放开了被除数得第3位,得11101101,比除数大,商为1,余数R2为101000;8、上一步余数并上被除数第2位,得1010001,没有除数大,商为0,余数R2为1010001;9、上一步余数并上被除数第1位,得10100010,没有除数大,商为0,余数R2为10100010;10、上一步余数并上被除数第0位,得101000101,比除数大,商为1,余数R2为10000000;11、然后把以上所有步骤中得商从左至右依次排列就是最后的商100001001,余数为最后算得的余数10000000。 以上例子运算结果:R1R0 = R3R4 / R5 = 100001001 ;R2 = R3R4 % R5 = 10000000 实际运算流程图见下图: 除法运算的效率,代码量见以下表格 表2.1是程序运行效率和代码量的对比数据(可能会有小的偏差),很明显本文提供的汇编算法要优化的很多。 16/8位除法 汇编 C语言 时钟周期 287-321 740-804 使用空间(Byte) 35 142 表2.1  除法运算时钟周期对比表 所以对于除法运算,本文提供的方法也是相对较优的。 以下是针对精简指令集做的除法运算,16/8位的例程,便于大家的移植和理解。

    03-14 137浏览
  • 一文理解单片机BootLoader的前世今生

    Bootloader是一个用于引导嵌入式系统的软件程序,通常存储在芯片的非易失性存储器中,如闪存。Bootloader的主要功能是在系统上电后,负责初始化系统硬件并加载操作系统或应用程序到内存中执行。 下面是一个简要的介绍Bootloader的工作流程和主要功能: 硬件初始化:Bootloader首先负责初始化处理器、时钟、内存和外设等硬件资源,确保系统处于合适的状态以便后续的操作。 引导设备选择:Bootloader可能需要检测可用的引导设备,例如闪存、外部存储器或串口等,并选择其中一个作为加载操作系统或应用程序的来源。 加载操作系统或应用程序:Bootloader根据预定义的引导策略,从选定的引导设备中读取操作系统镜像或应用程序,并将其加载到内存中。 执行加载的程序:一旦程序加载到内存中,Bootloader会跳转到该程序的入口点,启动执行。对于操作系统,这意味着将控制权移交给操作系统内核,而对于应用程序,它将开始执行应用程序的主函数。 错误处理和回滚:Bootloader通常包含错误处理机制,以便在发生错误时执行相应的操作,例如跳转到备用引导设备或者恢复到出厂设置状态。 升级支持:一些Bootloader还提供了固件升级的支持,允许通过特定的接口或协议更新系统固件,而无需物理访问设备。 总的来说,Bootloader在嵌入式系统中扮演着关键的角色,负责启动和管理系统的引导过程,同时提供了灵活性和可靠性,以适应不同的应用需求。 当我面对一个有一定规模、 稍显复杂的嵌入式项目时, 我通常并不会直接专注于主要功能的实现, 而是会做一些磨刀不误砍柴工的工作———设计一个 Bootloader(以下简称 BL) 以及构建一个Shell 框架。可能有人会觉得它们很高深, 实则不难, 正所谓“会者不难, 难者不会”。 本章就针对 BL 进行详细的讲解, 希望让大家可以体会到它的重要性。 1. 烧录方式的更新迭代 1.1 古老的烧录方式 单片机诞生于20 世纪80 年代, 以51 为代表开始广泛应用于工业控制、 家电等很多行业中。起初对于单片机的烧录, 也就是将可执行的程序写入到其内部的 ROM 中, 这不是一件容易的事情, 而且成本不低, 因为需要依赖于专门的烧录设备。而且受到半导体技术与工艺的限制, 对于 ROM 的烧写大多需要高压。这种境况一直持续到2000 年左右( 我上大学的时候还曾用过这种专门的烧录器),: 1.2 ISP 与ICP 烧录方式 随着低压电可擦写 ROM 的成熟, 单片机开始集成可通过数字电平直接读写的存储介质。其最大的优势在于可实现在系统或在电路直接烧录程序, 而无须像以前一样把单片机芯片从电路中拿出来, 放到编程器上, 这种烧录方式就是ISP(In System Programming) 或ICP (In Circuit Programming), 如图7.2 所示。 有人问过这样一个问题:“ISP 和ICP 我都听说过, 都说是可以在电路板上直接烧录程序,而无须拿下芯片, 那ISP 和ICP 有什么区别?”从广义上来说, 两者没有区别, 平时我们把其意义混淆也毫无问题。非要刨根问底的话, 那可以这样来理解:ISP 要求单片机中驻留有专门的程序, 用以与上位机进行通信, 接收固件数据并烧录到自身的 ROM 中, 很显然ISP 的单片机是需要可运行的, 即要具备基本的最小系统电路(时钟和复位); 而ICP 可以理解为 MCU 就是一块可供外部读写的存储电路, 它不需要预置任何程序, 也不需要单片机芯片处于可运行的状态。 支持ISP 或ICP 的芯片, 以 AT89S51 最为经典, 当时从 AT89C51 换成 S51 , 多少人曾因此不再依赖烧录器而大呼爽哉。这种并口下载线非常流行, 如图7.3 所示, 网上还有各种ISP小软件, 可以说它降低了很多人入门单片机的门槛, 让单片机变得喜闻乐见。一台电脑、一个S51 最小系统板、 一条并口ISP 下载线, 齐了! 1.3 更方便的ISP 烧录方式 1. 串口ISP 但是后来我们发现带有并口的电脑越来越少。那是在2005 年前后,STC 单片机开始大量出现, 在功能上其实与S51 相差无几, 甚至比同期的一些高端51 单片机还要逊色。但是它凭借一个优势让人们对它爱不释手, 进一步降低了单片机的学习门槛。这个优势就是———串口ISP, 这是真正意义上的ISP, 如图7.4 和图7.5 所示。再后来,9 针串口都很少见了, 只有 USB。这促使一个烧录和调试神器炙手可热———USB-TTL 串口。这下232 转换芯片省掉了, 直接通过 USB 进行烧录。这种方式造福了无数的单片机学习者和工程师。我本人虽然已经搞了近20 年单片机和嵌入式, USB 串口依然是不可或缺的调试工具。 多年来, 在串口与单片机的交互上, 我动了很多脑筋, 这也是我乐于开发 Bootloader 的一个原因。我希望“USB 串口在手, 一切全有!” STC 并不是第一个使用串口ISP 烧录程序的, 但它是最成功和最深入人心的。与之同期的很多单片机, 包括时至今日仍然应用最广泛的 STM32 全系列也都支持了串口ISP, 它成了一种标配的、 非常普遍的程序烧录手段。 2. 各种 USBISP 串口ISP 固然方便, 但是下载速度是它的硬伤, 当固件体积比较大的时候, 比如一些大型嵌入式项目的固件动辄几百 KB, 甚至几 MB, 再用串口ISP 就未免太慢了。所以一些单片机配有专门的 USBISP 下载器。以下列举几种比较主流的单片机及其 USBISP 下载器。 (1) AVR AVR 单片机曾经盛极一时, 但经历了2016 年的缺芯风波之后, 加之 STM32 的冲击, 开始变得一蹶不振, 鲜有人用了。与之配套的 USBISP 下载器非常多样, 有些是官方发布的, 更多的是爱好者开源项目的成果, 如图7.6 所示。 (2) C8051F( 见图7.7) (3) MSP430( 见图7.8) 我们会发现, 一个具有良好生态的主流单片机, 一定有配套的高效便捷的烧录下载工具。可见一种好的烧录方式, 对单片机开发是多么重要。 不论是串口ISP 还是各种专用的ISP 下载器, 都有一些共同的弊端。 依赖于专门的上位机或下载器硬件, 不能做到统型; 下载器价格仍然比较高, 尤其是原厂的, 这也是为什么有些单片机催生出很多第三方的下载器, 比如 AVR; 下载的时候通常需要附加额外的操作, 比如 STC 要重新上电、STM32 需要设置BOOT 引脚电平等。 这些额外的操作都增加了烧录的复杂性。尤其是在产品形态下要去重新烧录程序, 比如嵌入式升级, 就要打开外壳, 或将附加信号引出到壳外。 这都是非常不高效, 不友好的做法。如果有一种烧录方法, 对于任何一种单片机: 通信方式统一(比如一律都用串口); 提供一个友好的操作界面(比如命令行方式); 高效快速, 没有附加操作, 最好一键自动化烧录; 另外再增加一些嵌入式固件管理的功能(比如固件版本管理)。 这一定会让我们事半功倍。Bootloader 就能实现上述的这一切! 2. 关于Bootloader 2.1 Bootloader 的基本形态 直接看图7.9: 可以看到 BL 就是一段存储在 ROM 中的程序, 它主要实现4 个功能: 通过某种途径获取要烧录的固件数据; 将固件数据写入到 ROM 的 APP 区中; 跳转到 APP 区运行, 将烧录进去的用户程序引导起来; 在此过程中, 提供必要而友好的人机交互界面。这么说可能不好理解, 我们还是通过实例来进行讲解。 2.2 Bootloader 的两个设计实例 下面的两个实例, 用于说明 BL 的实际应用形态, 不涉及具体的实现细节, 旨在让大家了解 BL 实际是如何运行的。 1. 带Shell 命令行的串口BL 基本的操作逻辑如下: 通过超级终端、SecureCRT 或 Xshell 之类的串口终端输入命令program; BL 接收到命令后, 开始等待接收固件文件数据; 串口终端通过某种文件数据传输协议将固件数据传给 BL; BL 将固件数据写入到 ROM 的 APP 区中; BL 将 APP 区中的程序引导运行起来。更具体的示意如图7.10 所示。这里把操作逻辑说得很简单, 实际实现起来却并不容易, 我们放在后面去细究其具体实现。 2. 插SD 卡即烧录的BL 基本的操作逻辑如下: 将待烧录的固件拷贝到SD 卡中; 将SD 卡插入到卡槽中; BL 检测到SD 卡插入, 搜索卡中 BIN 文件; 将 BIN 文件数据读出写入到 ROM 的 APP 区中; BL 将 APP 区中的程序引导运行起来。 如图7.11 所示。通过这两个设计实例, 大家应该已经了解BL 是什么了吧。有没有感受到BL 是比ISP 烧录器更通用、 更灵活、 更友好、 功能更强大的固件烧录和管理手段呢? 有人可能知道 Linux 下的 Uboot, 它就是一个强大的 BL, 它提供非常强大的刷机(烧录操作系统镜像) 的功能以及完备而灵活的Shell 界面, 如图7.12 所示。其实我们电脑的 BIOS 也是一种广义的 BL。 那如何实现一个 BL 呢? 别急, 要实现 BL 是需要满足一些基本要求的。 2.3 BL 实现的要点 首先要说, 并不是任何一个单片机都可以实现 BL 的, 要满足几个要点。 1. 芯片体系架构要支持 来看图7.13 。 我们知道单片机程序的最开头是中断向量表, 包含了程序栈顶地址以及 Reset 程序入口,通过它才能把程序运行起来。很显然在从 BL 向 APP 跳转的时候,APP 程序必须有自己的中断向量表。而且单片机体系架构上要允许中断向量表的重定向。 传统51 单片机的中断向量表只允许放到 ROM 开头, 而不能有偏移量, 所以传统51单片机是不能支持 BL 的。有人要问“你这不是自相矛盾吗? 你前面说 STC 的51 单片机是支持串口ISP 的, 那它应该内置有ISP 程序, 我理解它应该和BL 是一个道理。”没错, 它内置的ISP程序就是一种 BL。STC 之所以可以实现 BL 功能, 是因为宏晶半导体公司对它的硬件架构进行了改进, 请看图7.14 可以看到,STC51 单片机多出了一块专门存放 BL 的 ROM, 称为 BOOTROM。 网上有一位叫 shaoziyang 的网友为 AVR 单片机写了一个 BL, 还配套开发了 一 款 叫AVRUBD 的上位机, 如图7.15 (AVRUBD 是很有用的, 本章后面会介绍, 它可以让我们实现隔空烧录) 所示, 实现了 AVR 单片机的串口烧录, 让很多人摆脱了对 USBISP 之类ISP 下载器的依赖(虽然ISP 下载器已经很方便了, 但它毕竟还需要银子嘛)。 AVR 在硬件架构上与STC51 是一个套路, 如图7.16 所示。 通过配置 AVR 的熔丝位可以控制复位入口地址以及 BOOT 区的大小和开始地址, 如图7.17 所示。 讲到这里, 有人会说:“那有没有一种单片机, 程序放在 ROM 的任何位置都可以运行起来, 也就是中断向量表可以重定位?”当然有, 这种单片机还很多, 其中最典型的就是 STM32。它的程序之所以可以放之各地皆可运行, 是因为在它的 NVIC 控制器中提供了中断向量表偏移量的相关配置, 这个后面我们再详细说。 2. ROM 要支持IAP 这也是需要单片机硬件支持的。很好理解, 在 BL 获取到固件数据之后, 需要将它写入到ROM 的 APP 区中, 所以说单片机需要支持IAP 操作, 所谓IAP 就是 In Application Programming, 即在应用烧录。也就是在程序运行过程中, 可以对自身 ROM 进行擦除和编程操作 。 大家仔细想想是不是这样? 似乎支持串口ISP 的单片机都支持IAP 功能。STC 还把这一功能包装成了它的一大特色, 可以用内部 ROM 来充当 EEPROM 的功能, 可以在运行时记录一些掉电不丢失的参数信息。 STM32 的 ROM 擦写在配套的固件库(标准库或 HAL 库) 中已经有实现, 大家可以参考或直接使用。 3. APP 程序的配套修改 为了让 BL 可以顺利地将 APP 程序引导运行起来, APP 程序在开发的时候需要配合 BL做出相应的修改。最重要的就是 APP 程序的开始地址(即中断向量表的开始地址) 以及对中断控制器的相应配置。 对于51 、AVR 这类单片机 APP 程序不用修改, 具体原因大家应该明白。这里主要对STM32 APP 程序如何修改进行详细讲解。 我们依然是结合实例, 如图7.18 所示。 假设我们所使用的STM32 的 ROM 总大小为128KB,BL 程序的体积是16KB,APP 程序紧邻 BL, 那么 APP 区的开始地址为0X08004000, 也就是 APP 程序的中断向量表偏移地址为0X4000。 如果我们使用 MDK 作为开发环境的话, 需要修改这里, 如图7.19 所示。 而如果我们使用的是gcc 的话, 则需要对link.ld 链接文件进行修改, 如图7.20 所示。 然后我们还需要对 NVIC 的中断向量表相关参数进行配置, 主要是中断向量表的偏移量,如下代码: #define VECT_TAB_OFFSET 0x4000 OK, 经过修改后的程序, 我们把它放到 ROM 的0X08004000 开始地址上, 然后再让 BL跳转到这个地址, 我们的程序就能运行起来了。 有人又会问:“BL 中的跳转代码怎么写?”别急, 这是我们要讲的下一个要点 4. BL 中的跳转代码 跳转代码是 BL 要点中的关键, 直接关系到 APP 程序能否正常运行, 如图7.21 所示。我直接给出STM32 的 jump _app 函数代码。 typedef void ( * iapfun)(void); iapfun jump2app; void MSR_MSP(u32 addr) { __ASM volatile("MSR MSP, r0" ); //set Main Stack value __ASM volatile("BX r14" ); } void load_app(u32 appxaddr) { if((( * (vu32* )appxaddr)&0x2FFE0000) ==0x20000000) //检查栈顶地址合法 { //用户代码区第二个字为程序开始地址(复位地址) jump2app= (iapfun) * (vu32* )(appxaddr+4); //初始化APP 堆栈指针(用户代码区的第一个字用于存放栈顶地址) MSR_MSP( * (vu32* )appxaddr); jump2app(); //跳转到APP. } } 这段代码大家自行研究, 如果展开讲就属于赘述了。到这里 BL 相关的要点就介绍完了, 大家应该有能力去完成一个简单的 BL 了。我基于STM32 设计了一个小实验, 大家有兴趣可以小试牛刀一下, 如图7.22 所示。 我们将 BL 程序用Jlink 烧录到0X08000000 位置, 而把 APP 程序烧录到0X08002000 开始位置, 然后复位, 如果串口打印了hello world 或流水灯亮起来了, 就说明我们的BL 成功了。 3. 把Bootloader 玩出花 上面我所讲的都是 BL 最基础的一些内容, 是我们实现 BL 所必须了解的。BL 真正的亮点在于多种多样的固件数据获取方式。 3.1 BL 的实现与延伸(串口传输固件) 前面我讲到过两个 BL 应用的实例, 一个是串口传输固件文件, 一个是 SD 卡拷贝固件文件。它们是在实际工程中经常被用到的两种 BL 形式。 这里着重对前一个实例的实现细节进行讲解剖析, 因为它非常具有典型意义, 如图7.23 所示。 这个流程图提出了3 个问题: 串口通信协议是如何实现的? 为什么获取到上位机传来的固件数据,不是直接写入到 APP 区,而是先暂存,还要校验? 对固件数据是如何实现校验的? 串口通信协议以及文件传输实现的相关内容略显繁杂, 在本书《 大话文件传输》一章中会专门进行讲解。 第二个问题: 经过串口传输最终由单片机接收到的固件数据是可能出现差错的, 而有错误的固件冒然直接写入到 APP 区, 是一定运行不起来的。所以, 我们要对数据各帧进行暂存, 等全部传输完成后, 对其进行整体校验, 以保证固件数据的绝对正确。 针对第三个问题, 我们要着重探讨一下。 一个文件从发送方传输到接收方, 如何确定它是否存在错误? 通常的做法在文件中加入校验码, 接收方对数据按照相同的校验码计算方法计算得到校验码, 将之与文件中的校验码进行对比, 一致则说明传输无误, 如图7.24 所示。 图7.24 是对固件文件的补齐以及追加校验码的示意。为什么要对文件补齐? 嵌入式程序经过交叉编译生成的可烧录文件, 比如 BIN, 多数情况下都不是128、256 、512 或1024 的整数倍。这就会导致在传输的时候, 最后一帧数据的长度不足整帧, 就会产生一个数据尾巴。取整补齐是解决数据尾巴最直接的方法。这一操作是在上位机上完成的, 通常是编写一个小软件来实现。这个小软件同时会将校验码追加到固件文件末尾。这个校验码可以使用校验和(CheckSum) 或者 CRC, 一般是16 位或32 位, 如图7.25 所示。 又有人会问:“要把整个固件暂存下来, 再作校验, 那得需要额外的存 储 空 间 吧, 外 扩ROM(FlashROM 或 EEPROM)?”是的。如果想节省成本, 我们也可以不暂存, 传输时直接烧写到 APP 区。这是有风险的, 但是一般来说问题不大(STC 和 STM32 的串口ISP 其实也都是实时烧写, 并不暂存)。因为在传输的过程中, 传输协议对数据的正确性是有一定保障的, 它会对每一帧数据进行校验, 失败的话会有重传, 连续失败可能会直接终止传输。所以说, 一般只要传输能够完成, 基本上数据正确性不会有问题。但是仍然建议对固件进行整体校验, 在成本允许的情况下适当扩大 ROM 容量。同时, 固件暂存还有一个另外的好处, 在 APP 区中的固件受到损坏的时候, 比如固件意外丢失或IAP 时不小心擦除了 APP 区, 此时我们还可以从暂存固件恢复回来(完备的 BL 会包含固件恢复的功能)。 其实也不必非要外扩 ROM, 如果固件体积比较小的话, 我们可以把单片机的片上 ROM砍成两半来用, 用后一半来作固件暂存。 如图7.26 所示, 我们将片上 ROM 划分为3 部分, 分别用于存储 BL、APP 固件以及暂存固件。比如 我 们 使 用 STM32F103RBT6 , 它 一 共 有 128 KB 的 ROM, 可 以 划 分 为 16 KB/ 56 KB/56 KB。 有些 产 品 对 成 本 极 为 敏 感。我 就 有 过 这 样 的 开 发 经 历, 当 时 使 用 的 单 片 机 是STM32F103C8T6 , 片上 ROM 总容量为64 KB, 固件大小为48 KB,BL 为12 KB。在通过BL进行固件烧写时根本没有多余的 ROM 进行固件暂存。我使用了一招“狗尾续貂”, 如图7.27所示。 我无意中了解到 STM32F103C8T6 与 RBT6 的晶元是同一个。只 是 因 为 有 些 芯 片 后64 KB 的 ROM 性能不佳或有瑕疵, 而被限制使用了。我实际测试了一下, 确实如此。但是后64 KB ROM 的使用是有前提的, 也就是需要事先对其好坏进行验证。如果是好的, 则暂存校验, 再写入 APP 区; 而如果是坏的, 那么就直接在固件传输时实时写入 APP 区(这个办法我屡试不爽, 还没有发现后64KB 有坏的)。 以上所介绍的是一种“骚操作”, 根本上还是有一定的风险的,ST 官方有声明过, 对后64K ROM 的质量不作保证, 所以还是要慎用。 3.2 10 米之内隔空烧录 OTA (On the Air) 这个“隔空烧录”源于我的一个IoT 项目, 它是对空调的外机进行工况监测。大家知道,空调外机的安装那可不是一般人能干的, 它要不就在楼顶, 要不就在悬窗上。这给硬件升级嵌入式程序带来很大的困难。所以, 我实现了“隔空烧录”的功能, 其实它就是串口 BL 应用的一个延伸, 如图7.28 所示。 “隔空烧录确实牛, 但是总要抱着一个电脑, 这不太方便吧。”确实是! 还记得前面我提过的 AVRUBD 通信协议吗? (详见“大话文件传输”一章) 它的上位机软件是有手机版的。这样我们只要有手机, 就能“隔空烧录”了, 如图7.29 所示。 “哪个 APP? 快告诉我名字”, 别急, 蓝牙串口助手安卓版, 图 7.30 是正在传输固件的界面。 AVRUBD 其实是对 Xmodem 协议的改进, 这个我们放在专门的章节进行详细讲解。 3.3 BL 的分散烧录 我们知道 BL 的核心功能其实就是程序烧录。那你有没有遇到过比较复杂的情况, 如图7.31 所示。 这种情况是有可能遇到的。主 MCU+CPLD+ 通信协处理器+ 采集协处理器就是典型的复杂系统架构。这种产品在批量生产阶段, 烧录程序是非常烦琐的。首先需要维护多个固件, 再就是需要一个个给每一个部件进行烧写, 烧写方式可能还不尽相同。所以我引入了一个机制, 叫“BL 的分散烧录”。 首先我们将所有的固件拼装成一个大固件(依次数据拼接), 并将这个大固件预先批量烧录到外扩 ROM 中, 比如spiFlash; 再将主 MCU 预先烧录好 BL; 然后进行SMT 焊接。PCBA生产出来之后, 只要一上测试工装(首次上电),BL 会去外扩 ROM 中读取大固件, 并从中分离出各个小固件, 分别以相应的接口烧录到各个部件中去。配合工装的测试命令, 直接进行自检。这样做, 批量化生产是非常高效的。当然, 这个 BL 开发起来也会有一定难度, 最大问题可能还是各个部件烧录接口的实现( 有些部件的烧录协 议 是 比 较 复 杂 的, 比 如 STM32 的SWD 或者 ESP8266 的SLIP)。 OK, 上面对一些 BL 实例的实现和应用场景进行了介绍。还有一些实例没有介绍, 比如通过 CAN 总线或SPI 进行文件传输, 这个我们还是放到专门的章节去详细讲解。当然, 各位读者可以在此基础上衍生出更多有特色而又实用的 BL 来。 BL 没有最好的, 只有最适合自己的。通常来说, 我们并不会把 BL 设计得非常复杂, 原则上它应该尽量短小精炼, 以便为 APP 区节省出更多的 ROM 空间。毕竟不能喧宾夺主, APP才是产品的主角。 4. 不走寻常路的BL 4.1 Bootpatcher 我来问大家一个问题:“Bootloader 在 ROM 中的位置一定是在 APP 区前面吗?”很显然不是,AVR 就是最好的例子。那如果我们限定是STM32 呢? 似乎是的。上电复位一定是从0X08000000 位置开始运行的, 而且 BL 一定是先于 APP 运行的。 在某些特殊的情况下, 如果 APP 必须要放在0X08000000 位置上的话, 请问还有办法实现 BL 串口烧录吗? 要知道 APP 在运行的时候, 是不能IAP 自己的程序存储器的(就是自己不能擦除自己来烧录新固件), 如图7.32 所示。 APP 运行时, 想要重新烧录自身, 它可以直接跳转到后面的BL 上,BL 运行起来之后开始接收固件文件, 暂存校验 OK 之后, 将固件写入到前面的 APP 区。然后跳转到0X08000000, 或者直接重启。这样新的 APP 就运行起来了。这个位于 APP 后面的 BL, 我们称之为 Bootpatcher(意为启动补丁)。但是这种做法是有风险的, 一旦 APP 区烧录失败, 那产品就变砖了。所以这种方法一般不用。 4.2 APP 反烧BL 前面我们都是在讲 BL 烧录 APP, 那如果 BL 需要升级怎么办呢? 用JLINK。不错, 不过有更直接的方法, 如图7.33 所示。 这是一种逆向思维, 我们在 APP 程序中也实现接收固件文件, 暂存校验, 然后将其烧录到BL 区。这种做法与 Bootpatcher 同理, 也是有一定风险的, 但一般都没有问题。

    03-12 162浏览
  • 什么是时间片轮转调度?在什么情况下会发生?

    嵌入式单片机:必须会回答的FreeRTOS面试题

    03-06 202浏览
  • 一文读懂单片机的存储器

    单片机内置了非常便于使用的外设功能。但是,如果要有效地运行单片机,程序是不可或缺的,那么程序到底是如何运行的呢?本期我们将向大家介绍单片机与程序的关系。 01 单片机的存储器 首先来了解存储器﹑主存储器和外置存储器的两种作用 记忆(保存)程序和数据的地方即存储器。存储器有以下两种类型。 主存储器CPU能够直接进行存取的存储器,用于保存正在执行中的程序和数据 外置存储器(辅助存储器、二级缓存器)不能从CPU直接进行存取,需通过USB或串行、并行的各种I/O来进行存取,用于保存不在执行当中(处理中)的应用和数据 外置存储器中的程序需传送到主存储器后才能执行。关于单片机的存储器,常会看到ROM(Read Only Memory:只读存储器)和RAM(Random Access Memory:可读写存储器)等词汇,其实ROM和RAM仅是表示存储器性质,而与存储器的作用无关。(请参考单片机入门(1),了解单片机的基本结构和操作) 地址空间(内存空间) CPU能够直接进行读写的所有空间被称为“地址空间(或内存空间)”。这个地址空间的每个字节都标注有号码。这个号码称为“地址(address)”,一般以十六进制来表示。上面所介绍过的主存储器都包含在地址空间内。 根据不同用途,单片机的CPU已开发出了4位、8位、16位和32位。在GR-SAKURA中使用的RX63N单片机搭载了32位的CPU,因此也被称为“32位单片机”。那么,单片机所拥有的地址空间容量到底有多少呢?以RX63N为例,由于是32位的CPU,因此最大能够指定约40亿(2的32次方)个地址。确切地说是4,294,967,296(4x1024x1024x1024)个地址。由于一个地址可以记忆一个字节,这时也可以表示为具有“4GB(千兆字节)的地址空间”。地址空间的容量越大越能搭载大容量的存储器, 也可容纳更大的程序。因此能够实现更高功能的应用。 32位字节的CPU所拥有的4G字节的地址空间示例如图1所示。左边所示的是以十六进制标示的地址。由于一列保存有4个字节(=32位),所以左边所标记的地址就是每4个地址的值。 图1:地址空间及标示例 计算机的单位:位、字节、兆、千兆和兆兆(太) 数据的基本单位是位(b=bit),每个位的值为"0"或"1"。8位为1个字节(B=Byte)。例如,3个字节(3×8位)等同于24位。 电脑存储设备的容量所使用的单位,大家耳熟能详的有KB(千字节)、MB(兆字节)、GB(千兆字节)和TB(太字节)等。一般情况下会说1GB=1000MB或者这样写出来,但在计算机的世界里,此单位并非为1000倍,而是1024倍(2的10次方),因此正确的表示如下: 1KB(千字节)=2的10次方 = 1,024 字节 1MB(兆字节)=1,024KB = 2的20次方 1,048,576 字节 1GB(千兆字节)=1,024MB = 2的30次方 = 1,073,741,824 字节 1TB(太字节)=1,024GB = 2的40次方 = 1,099,511,627,776字节 表示地址的十六进制指的是什么? 地址空间内的地址以16进制来表示。例如,拥有16位(2的16次方)大小的地址空间中,如果以10进制来表示,就是“从地址0到地址65535”,如果以16进制来表示,则是从“地址0h到地址FFFFh”。在10进制中,每一位所取的值都在0到9之间,而在16进制中,则是0到F(相当于10进制的15)。以16进制表示的数,最后都有一个“h”,标明是以16进制来表示的。 程序保存在哪儿呢?(向量表) 那么,程序被保存在地址空间的什么地方,又是怎么样开始工作的呢?单片机复位后便开始执行最优先程序。复位是在接通电源或接收到复位信号时发生。实际上,这种“开始执行最优先程序”处理中,有如下所示的两种方法。 即开始执行程序时,有将执行程序的起始地址设为固定的CPU及将之设为可变地址的CPU。 在将起始地址设为固定的CPU中,大多是从地址0(地址空间中最小的地址)开始执行。这就是程序开始的地点。而且,有时要事先在地址0中实现写入“下一个要执行的是地址○○”的跳转(Jump)指令,并将程序预先放置在“地址○○”中。如果改写“地址○○”,将可获得与将起始地址设为可变地址同样的效果。 将起始地址设为可变地址的CPU将起始地址写入被称为“向量表”的部分中(图2)。向量表是只存放地址空间中各种起始地址的特定区域的名称。一般来说是它放置在地址空间中最大地址的部分。 图2:RX63N系列的向量表以RX63N为例,由于地址是以32位来显示的,为了保存它就需要4个字节。这就意味着图3中的“复位”部分表示从地址FFFFFFFCh到地址FFFFFFFFh的4个字节中保存了程序的起始地址。CPU复位后将读取保存于此的地址,并从作了标记的地址开始执行。被写入向量表的不仅是复位后的起始地址,向量表中还保存发生中断时程序的起始地址和异常处理(Exception Handling)的起始地址。也正因为保存了发生中断及异常处理等因多种事由的起始地址,所以才被称为“表(Table)”。我们来设想一下使用了向量表的程序处理的情况。图3表示出了发生非屏蔽中断(NMI) (*1) 时的处理流程例。 产生NMI, 读取写在向量表的NMI的起始地址(此例中为10000000h), 执行所读取地址(10000000h)中的NMI程序。 图3:使用向量表进行处理的流程(*1)非屏蔽中断(NMI):所谓非屏蔽指的是无法禁止的意思。如有中断请求,CPU将无条件地执行中断处理。可用于通过看门狗定时器进行的中断处理等。关于看门狗定时器,在本连载的第2期--“定时器”中已为大家作了介绍。如上所述,在将程序的起始地址设为可变的CPU中,由于能够通过写入向量表来指定中断处理的起始地址,因此具有在地址空间中自由配置中断处理程序的特征。介绍了CPU的地址空间与外设功能的关系、以及程序是从哪里开始执行的等内容。接下来我们将通过说明执行程序时的处理和内存的关系来进一步理解单片机的有效运行。 02 引导程序的运行―程序计数器 我们已经学习了将程序放在地址空间中,并在向量表中显示保存位置的内容。接下来将介绍在执行程序及产生中断时CPU内会发生什么变化。 一般来说,程序就是计算机将所要进行的处理按顺序排列的指令集。在单片机中,将程序保存在地址空间(存储器 空间)中,并由CPU来执行(处理)指令。假设地址空间中的一个地址保存一条指令,先执行某个地址中的指令(如“将值置位到CPU中”处理),接着执行下一个地址中的指令,接下来再执行下一个地址中的指令……,像这样通过连续执行指令,便可执行程序。 那么,CPU是如何判断执行指令的顺序呢?在单片机中,程序被执行的时候“程序计数器(PC)”的值也同时被更新。存放在CPU内的指令地址中,程序计数器存储有下一条CPU将要执行的指令所在的地址。执行了某个地址的指令后,下一个该执行哪个地址中的指令呢?这个答案由程序计数器来告诉你。一般来说,程序被保存在连续的地址中, 再由CPU按顺序执行存放在各个地址中的指令。图1为程序计数器的示意图。图中,假定(1)执行地址1000h中的指令,(2)执行地址1000h中的指令后,程序计数器的値自动增加一个量并显示出下一个地址1001h,接下来,(3)CPU执行地址1001h中的指令。 图1:程序计数器那么,CPU执行最初的指 令时是一种什么状况呢?单片机在接通电源或是复位时,如上期所说明的,保存在向量表的复位地址中的値(程序的起始地址)将被转移到程序计数器中,该地址中的指令便得到执行。 改变程序的运行路径―转移指令 编写程序时,在执行完某个指令的处理后有时必须先执行保存“(非连续)的下一个地址”中的指令。此时,程序计数器的值将被改写,而所用的指令被称为“转移指令”。图2所示是转移指令的示意图。图2示例中,(1)地址1000h中存放有转移指令,即将(2)程序计数器的值改写为下一个应执行的地址 (1100h)的指令。即CPU执行完1000h地址的指令(转移指令)后,接下来不是执行1001h地址的指令,而是执行(3)1100h地址的指令。 图2:转移指令另外,在转移指令中,能够利用“从当前的程序计数器的值向前(更大的地址)/向后(更小的地址)移动”的方法来设定程序计数器的值 。 信息的暂时存放处―堆栈 执行程序时,在运算过程中仅仅依靠CPU内的数据保存位置(CPU内部寄存器)是不够的,有时需在主存储器中暂时存放信息。这种信息的暂时存放位置被称为“堆栈”,而存放“下一个(暂时)存放的信息地址”的就是“堆栈指针(SP)”。如果一开始就设定好堆栈的地址,那么堆栈指针将自动更新,且总是指示“下一个(暂时)存放的信息地址”。 ⇒CPU内部寄存器等单片机的结构请参照《单片机入门(1)》。如果执行“将该信息存放(有时也用 “堆积”)在堆栈”的指令,那么被指定的信息将会被写入堆栈指针所指定的地址中,且堆栈指针的值也将被更新为新的地址(一般为一个小地址)。该情形如图3所示。如果(1)CPU将信息存放在堆栈指针所指的地址中,则(2)堆栈指针的値将被更新,然后(3)堆栈指针指向下一个存放信息的位置。 图3:将信息存放在堆栈中"将存放在堆栈中的信息返回CPU时,也将用到堆栈指针。图4所示的是将信息返回时的情形。(1)更新堆栈指针的値(更新为 一个大的地址),(2)将暂时存放在堆栈中的信息返送回CPU。此时,(3)堆栈指针指向下一个写入地址(先前将信息返回CPU后空出的地址)。 图4:将信息从堆栈返回但是堆栈中并非可无限制地保存信息。由于堆栈能使用的范围仅限于可改写的被称为RAM的存储器。如果信息存放量过多而导致堆栈超出了RAM的区域,程序将无法正常运行。 理解中断处理 我们将以发生中断时的处理为例来进行思考(图5)。中断处理就是指在执行某个程序的过程中,由于某种原因(产生中断)而导致开始执行完全不同的程序。我们以来自外设功能之一的独立的看门狗计时器(WDT、所谓的Watch Dog即看门狗的意思)的中断为例来进行分析。在程序正常运行时独立的看门狗定时器将什么也不做,但是在程序失去控制,且没有按必要的步骤进行处理时就会产生中断。使失去控制的程序停下并让系统稳定停止的处理是由通过中断开始的程序来执行的。 图5:中断处理的流程 首先,在产生中断时,必须使运行中的程序入栈。 在中断处理 “入栈”时,将信息存放在堆栈指针指向的地址(堆栈)中。进行中断处理时存放在堆栈中的信息就是正 在执行的原先的程序(被中断的程序)时的程序计数器的值,即原先的程序执行到哪一步的信息(地址)。另外,显示CPU内部状态的信息和暂时保存的值也存放 在堆栈中。 如果CPU内部的信息存放在堆栈中且完成“交付”准备(入栈)后,将执行中断程序。中断程序与正在执行的程序不同且所保 存的地址空间也不同,所以程序计数器的值与原先程序也完全不同。中断程序的起始位置将被写入向量表中。起始位置该写在向量表中的哪一项取决于所产生 的中断。 例如,如果存在不可屏蔽中断(NMI,即CPU不能屏蔽的中断),那就从写有NMI项的地址开始进行处理(请参照本文第一节的图 2及图3)。 ⇒使用向量表进行处理的流程在本文第一节中进行解说。 如上所述,向量表的NMI项中的值(地址)将转移到程序计数器中, 并从该处开始执行。此外,如将数值设为0而产生错误时,或者欲存取到无存储器的位置时,CPU本身将产生中断并从向量表中读取开始处理的地址。此例中, 由于在检测到程序失控时是通过独立的看门狗定时器进行中断处理的,所以中断程序将使系统停止下来。 如为一般的周期性中断,那么,中断处理一结束,且在入栈时将存放在堆栈中的“执行原先执行程序时的信息”返回到CPU。最后返回程序计数器的值,并结束从中断返回的处理“出栈”。 开始中断程序时,通过来自外部的信号或从CPU本身发出的指令来开始入栈。出栈时使用“来自中断的出栈指令”,因此编程人员无需考虑“堆栈 中存放有什么信息又是按什么顺序来存放的?”等问题,仅需一条指令便可进行出栈处理。结合本文第一节的内容,从执行程序的观点来分析 ,本期对于CPU中到底产生了什么变化进行了说明。程序存放在地址空间中,且在向量表中保存有起始地址,而且还有将信息暂时存放的被称为堆栈的内容等 等……,在进行嵌入式编程时,必须同时考虑这些内部动作后再进行编程。如果可通过程序对于更细微的部分发出指示,且能发挥出该单片机的能力的话,编程将变得更加容易。

    03-04 226浏览
  • 单片机开发:一文吃透交叉编译

    解锁单片机开发新姿势:一文吃透交叉编译

    02-17 391浏览
  • 51单片机的六路抢答器Protues仿真设计,附演示和源程序

    目录 一、设计背景 二、实现功能 三、仿真演示 四、源程序(部分) 一、设计背景 近年来随着科技的飞速发展,单片机的应用正在不断的走向深入。本文阐述了基于51单片机的六路抢答器设计。本设计中,51单片机充当了核心控制器的角色,通过IO口与各个功能模块相连接。按键模块负责检测参与者的抢答动作,当有人按下抢答按钮时,会通过IO口电平的变化通知单片机,单片机会记录按键的次序,并通过数码管显示当前的抢答结果。 为了保证抢答过程的准确性和公平性,设计中还需要考虑到以下因素。首先,按键模块需要具备快速响应和高可靠性,以确保抢答者的动作能够被准确地捕捉到。其次,显示屏模块需要能够实时更新抢答结果,并显示相应的信息,比如参与者的编号和抢答时间。最后,在电路连接方面,需要注意各个模块之间的线路布局,以避免信号干扰和电气问题。 软件系统采用C语言编写程序,包括显示程序,定时中断服务,延时程序等,并在KEIL5中调试运行,硬件系统利用PROTEUS8.13强大的功能来实现,简单切易于观察,在仿真中就可以观察到实际的工作状态。 二、实现功能 以51单片机为控制核心,设计一种六路抢答器。整个系统包括MCU、晶振电路、时钟电路、蜂鸣器控制电路、指示灯控制电路、译码电路、独立按键电路、矩阵键盘以及数码管显示电路等。可具体实现以下功能: (1)设定矩阵键盘的6个键作为6位选手的抢答按键,键的编号即选手编号,为1~6号;设定1个独立按键作为抢答开始键;选择四位数码管作为倒计时、选手编号显示;选择蜂鸣器作为正常抢答和犯规抢答的提示。 (2)只有当裁判按下开始键时才可以进入正常抢答,否则属于犯规抢答。抢答完毕,或计时时间到,停止抢答。当裁判按下抢答开始键时,开始抢答,计时器开始倒计时,10秒倒计期间,若有抢答,则停止计时,数码管显示选手号;若倒计时结束时无人抢答,则停止抢答。 (3)正常抢答时,有效抢答指示灯亮起,蜂鸣器播放音乐1,低位数码管数码管显示抢答选手的编号,高位数码管开始60s倒计时,60s时间到,数码管显示0-00。违规抢答时,无效抢答指示灯亮起,蜂鸣器播放音乐2,低位数码管显示违规抢答选手编号,高位数码管显示抢答倒计时时间10s。 三、仿真演示 未运行仿真时,数码管不显示。 运行仿真后,进入准备界面,数码管显示0-10。 按下启动按键,进入抢答界面,开始10秒抢答倒计时。 在抢答倒计时范围内,按下序号为1~6的选手抢答按键,抢答有效指示灯亮起,蜂鸣器播放《两只老虎》的旋律,低位数码管上显示抢答选手序号,高位数码管开始60s倒计时。 当裁判未按下开始键时,若有选手抢答视为犯规抢答,抢答无效指示灯亮起,蜂鸣器播放《粉刷匠》的旋律,低位数码管显示犯规选手的编号,高位数码管显示10。 正常抢答还是犯规抢答结束后,按下复位按钮恢复到准备界面,以便进行下一次抢答。 四、源程序(部分) #include "reg52.h" #include "delay.h" #include "smg.h" #include "timer.h" sbit Beep = P1^5; //六位选手 sbit key1 = P1^1; sbit key2 = P1^2; sbit key3 = P1^3; sbit key4 = P1^4; sbit key5 = P1^5; sbit key6 = P1^6; sbit EffectLED = P2^6; //抢答有效指示灯 sbit UeffectLED = P2^7; //抢答无效指示灯 sbit start_stop = P3^1; //抢答按钮 sbit L1 = P1^7; sbit L2 = P1^6; sbit R1 = P1^3; sbit R2 = P1^2; sbit R3 = P1^1; sbit R4 = P1^0; //**《两只老虎》 uint8 code x0[]={1+7,2+7,3+7,1+7,1+7,2+7,3+7,1+7,3+7,4+7,5+7,3+7,4+7,5+7,5+7,6+7,5+7,4+7,3+7,1+7,5+7,6+7,5+7,4+7,3+7,1+7,1+7,5,1+7,1+7,5,1+7}; uint8 code y0[]={4,4,4,4,4,4,4,4,4,4,8,4,4,8,3,1,3,1,4,4,3,1,3,1,4,4,4,4,8,4,4,8}; //**《粉刷匠》 uint8 code x1[]={5+7,3+7,5+7,3+7,5+7,3+7,1+7,2+7,4+7,3+7,2+7,5+7,5+7,3+7,5+7,3+7,5+7,3+7,1+7,2+7,4+7,3+7,2+7,1+7,2+7, 2+7,4+7,4+7,3+7,1+7,5+7,2+7,4+7,3+7,2+7,5+7,5+7,3+7,5+7,3+7,5+7,3+7,1+7,2+7,4+7,3+7,2+7,1+7}; uint8 code y1[]={4,4,4,4,4,4,8,4,4,4,4,16,4,4,4,4,4,4,8,4,4,4,4,16,4,4,4,4,4,4,8,4,4,4,4,16,4,4,4,4,4,4,8,4,4,4,4,16}; //以下定义低中高共21个音阶的定时参数,通过定时器来实现不同音频的输出 uint8 code ti[21][2]={ {0xf8,0x8c},{0xf9,0x5c},{0xfa,0x14},{0xfa,0x67},{0xfb,0x04},{0xfb,0x90},{0xfc,0x0c}, //低音 {0xfc,0x44},{0xfc,0xb6},{0xfd,0x09},{0xfd,0x34},{0xfd,0x82},{0xfd,0xc8},{0xfe,0x06}, //中音 {0xfe,0x22},{0xfe,0x56},{0xfe,0x8c},{0xfe,0x9a},{0xfe,0xc1},{0xfe,0xe4},{0xff,0x03}}; //高音 uint8 th,tl,i; _bool action = 0; _bool key1_flag = 0; _bool key2_flag = 0; _bool key3_flag = 0; _bool key4_flag = 0; _bool key5_flag = 0; _bool key6_flag = 0; _bool start_stop_flag = 0; //抢答标志位 _bool cntflag=0; uint8 second = 10; //时间 uint8 timer0_count = 0; //定时器1计数值 uint8 number = 0; //队号 uint8 number_display = 0; //队号显示 uint8 a = 0xff; //按键值 uint8 key_scan8(void); void start_stop_keyscan(void); void music1(void);//演奏《两只老虎》 void music2(void);//演奏《粉刷匠》 void keycheckdown(void); /* 反转法键盘扫描 */ /*----------------------------------------------------------- 主函数 ------------------------------------------------------------*/ void SMG_delay(uint8 t) { while(t--) { display(number_display,second); } } void main() { ConfigTimer();//定时器初始化 while(1) { start_stop_keyscan();//开始按键 keycheckdown(); if(key_scan8()&&action==0&&cntflag==0) { UeffectLED=0; EffectLED=1; music2(); cntflag=1; } while(action)//按下开始键为1,抢答结束为0 { keycheckdown(); if(cntflag==1) { number_display=0; cntflag=0; } while(!key_scan8()) //无队抢答 { keycheckdown(); display(number_display,second); if(second == 0) { break; } } if(number_display)//有队抢答 { EffectLED=0; UeffectLED=1; second=60; music1(); } while(number_display) { display(number_display,second); TR0 = 1; if(second == 0) { break; } } TR0 = 0;//时间到 display(number_display,second); action = 0;//抢答结束 break; } display(number_display,second); } } void music1(void)//演奏《两只老虎》 { for(i=0;i<14;i++) { th=ti[x0[i]-1][0]; tl=ti[x0[i]-1][1]; TH1=th; TL1=tl; TR1=1; SMG_delay(y0[i]*10); TR1=0; } } void music2(void)//演奏《粉刷匠》 { for(i=0;i<12;i++) { th=ti[x1[i]-1][0]; tl=ti[x1[i]-1][1]; TH1=th; TL1=tl; TR1=1; SMG_delay(y1[i]*9); TR1=0; } } /*----------------------------------------------------------- 中断服务函数 ------------------------------------------------------------*/ void timer0() interrupt 1 { TH0 = (65536-50000)/256; //50ms TL0 = (65536-50000)%256; timer0_count ++; if(timer0_count == 20)//1s { timer0_count = 0; second--; //10s倒计时 if(second == 0)//计时结束 { TR0 = 0; number_display = 0; action = 0; } } } /*----------------------------------------------------------- 开始键扫描函数 ------------------------------------------------------------*/ void start_stop_keyscan(void) { if(start_stop == 0) { SMG_delay(8); if((start_stop == 0)&&(!start_stop_flag)) { start_stop_flag = 1; action = 1; TR0 = 1; } while(start_stop == 0){display(number_display,second);} } else { start_stop_flag = 0; } } void keycheckdown() { L1=0;L2=1; R1=R2=R3=R4=1; if(R1==0) { while(R1==0) { display(number_display,second); } a=1; } else if(R2==0) { while(R2==0) { display(number_display,second); } a=2; } else if(R3==0) { while(R3==0) { display(number_display,second); } a=3; } else if(R4==0) { while(R4==0) { display(number_display,second); } a=0x4; } L2=0;L1=1; R1=R2=R3=R4=1; if(R1==0) { while(R1==0) { display(number_display,second); } a=0x5; } else if(R2==0) { while(R2==0) { display(number_display,second); } a=0x6; } else if(R3==0) { while(R3==0) { display(number_display,second); } a=0x7; } else if(R4==0) { while(R4==0) { display(number_display,second); } a=0x8; } } /*----------------------------------------------------------- 六位抢答键扫描函数 ------------------------------------------------------------*/ uint8 key_scan8(void) { if((a == 1)&&(!key1_flag)) { key1_flag = 1; number = 1; number_display = number; } else { key1_flag = 0; number = 0; } if((a == 2)&&(!key2_flag)) { key2_flag = 1; number = 2; number_display = number; } else { key2_flag = 0; number = 0; } if((a == 3)&&(!key3_flag)) { key3_flag = 1; number = 3; number_display = number; } else { key3_flag = 0; number = 0; } if((a == 0x4)&&(!key4_flag)) { key4_flag = 1; number = 4; number_display = number; } else { key4_flag = 0; number = 0; } if((a == 0x5)&&(!key5_flag)) { key5_flag = 1; number = 5; number_display = number; } else { key5_flag = 0; number = 0; } if((a == 0x6)&&(!key6_flag)) { key6_flag = 1; number = 6; number_display = number; } else { key6_flag = 0; number = 0; } if(number_display != 0) { return 1; } else { return 0; } } void Timer1Service() interrupt 3 /* T0中断服务程序 */ { Beep=~Beep; TH1=th; TL1=tl; }

    02-07 414浏览
  • 深入浅出剖析单片机STM32图形库:U8g2与LVGL

    探索单片机图形库:U8g2 与 LVGL 的深度剖析

    02-07 470浏览
  • Keil编程开发环境(必备)

    1.Keil编程开发环境(必备) 这个是最核心的工具了,用来编写和编译程序,还有一个最重要的功能就是仿真,快速地帮你定位程序BUG,不过要配合ST-Link或者其他仿真器用。 一般51我是用C51V9.0的,STM32我是用Keil4.72...

    01-08 367浏览
  • 单片机到底是不是嵌入式?

    一定有很多人都听说过嵌入式和单片机,但在刚开始接触时,不知道大家有没有听说过嵌入式就是单片机这样的说法,其实嵌入式和单片机还是有区别的。单片机与嵌入式到底有什么关系? 下面我们就来说说嵌入式和单片机之间的联系和区别吧。 01 什么是单片机? 首先,我们来了解一下到底什么是单片机。 嵌入式系统的核心是嵌入式处理器。嵌入式处理器一般可以分为以下几种类型: 嵌入式微控制器MCU(Micro Control Unit) 嵌入式DSP处理器(Digital Signal Processor) 嵌入式微处理器MPU(Micro Processor Unit) 嵌入式片上系统SoC(System on Chip) 可编程片上系统SoPC(System on a Programmable Chip) 我们的单片机属于嵌入式微控制器MCU(Micro Control Unit) MCU内部集成ROM/RAM、总线逻辑、定时/计数器、看门狗、I/O、串口、A/D、D/A、FLASH等。典型代表如8051、8096、C8051F等。 单片机就是在一个芯片(Chip)上集成了CPU、SRAM、Flash及其他需要模块,在一个Chip上实现一个微型计算机系统,所以就叫Single Chip Microcomputer,也就是单片机了。 它其实就是一种集成电路芯片,是通过超大规模集成电路技术,将CPU、RAM、ROM、输入输出和中断系统、定时器/计数器等功能,塞进一块硅片上,变成一个超小型的计算机。 这么说来,单片机不就是一个嵌入式系统? 别急,我们往下看。 “单片机”其实是一种古老的叫法。在那个年代半导体工艺还在起步阶段,集成能力很差,往往是CPU一个芯片,SRAM一个芯片,Flash一个芯片,需要中断的话又得有个专门处理中断的芯片,所以一个完整可用的计算机系统是很多个芯片(Chip)做在一个PCB板上构成的。 不同的功能无法做进一个芯片(Chip),所以会有多片机。现在半导体技术早已非常发达,所以不存在多片机。但是,“单片机”的叫法却一直延用至今。 单片机技术从上世纪70年代末诞生,早期的时候是4位,后来发展为8位,16位,32位。它真正崛起,是在8位时代。8位单片机功能很强,被广泛应用于工业控制、仪器仪表、家电汽车等领域。 我们在研究单片机的时候,经常会听到一个词——51单片机。让我们来了解一下它究竟是什么。 51单片机,其实就是一系列单片机的统称。该系列单片机,兼容Intel 8031指令系统。它们的始祖,是Intel(英特尔)的8004单片机。 注意,51单片机并不全是英特尔公司产品。包括ATMEL(艾德梅尔)、Philips(飞利浦)、华邦Dallas(达拉斯)、Siemens(西门子)、STC(国产宏晶等公司,也有很多产品属于51单片机系列。 ATMEL公司的51单片机,AT89C51这是一个51单片机的开发板,中间那个芯片才是51单片机 51单片机曾经在很长时间里都是市面上最主流、应用最广泛的单片机,占据大量的市场份额。 51单片机其实放在现在毫无技术优势,是一种很老的技术。之所以它的生命力顽强,除了它曾经很流行之外,还有一个原因,就是英特尔公司彻底开放了51内核的版权。 所以,无论任何单位或个人,都可以毫无顾忌地使用51单片机,不用付费,也不用担心版权风险,所以很多学校也都在用这个。 此外,51单片机拥有雄厚的存量基础和群众基础。很多老项目都是用的51单片机,出于成本的考虑,有时候只能继续沿用51单片机的技术进行升级。 而且,很多老一辈的工程师,都精通51单片机开发技术。 所以,51单片机的生命力得以不断延续。 02 什么是嵌入式? 嵌入式系统是一种专用的计算机系统,作为装置或设备的一部分。通常,嵌入式系统是一个控制程序存储在ROM中的嵌入式处理器控制板。 事实上,所有带有数字接口的设备,如手表、微波炉、录像机、汽车等,都使用嵌入式系统,有些嵌入式系统还包含操作系统,但大多数嵌入式系统都是由单个程序实现整个控制逻辑。 从应用对象上加以定义,嵌入式系统是软件和硬件的综合体,还可以涵盖机械等附属装置。国内普遍认同的嵌入式系统定义为: 以应用为中心,以计算机技术为基础,软硬件可裁剪,适应应用系统对功能、可靠性、成本、体积、功耗等严格要求的专用计算机系统。 嵌入式系统具体应用于哪些“专用”方向呢? 举例如下: 办公自动化:打印机,复印机、传真机 军事及航天类产品:无人机、雷达、作战机器人 家电类产品:数字电视、扫地机器人、智能家电 医疗电子类产品:生化分析仪血液分析仪、CT 汽车电子类产品:引擎控制、安全系统、汽车导航与娱乐系统 网络通信类产品:通信类交换设备、网络设备 (交换机、路由器、网络安全) 通信与娱乐:手机、数码相机、音乐播放器、可穿戴电子产品、PSP游戏机 工业控制类产品:工控机交互式终端 (POS、ATM)、安全监控、数据采集与传输、仪器仪表 上述这些领域,都使用了嵌入式系统。这还只是冰山一角。 可以说,嵌入式系统完完全全地融入了我们,时刻影响着我们的工作和生活。 嵌入式系统,既然是一个计算机系统,那么肯定离不开硬件和软件。 一个嵌入式系统的典型架构如下: 这里最重要的就是嵌入式操作系统和嵌入式微处理器。 从硬件角度来看,嵌入式系统就是以处理器(CPU)为核心,依靠总线(Bus)进行连接的多模块系统: 其实大家不难看出和个人PC是一样的方式。 单片机是有清晰定义的,就是单个片(chip)上的计算机系统。而不同的单片机虽然配置不同,性能不同,厂家不同,甚至指令集和开发方式不同,但是都是在一个片上的完整的计算机系统,这个定义不会错。 而嵌入式就是个不清晰的定义了,并没有非常明确的关于“嵌入式”这个词的定义。他也不像单片机一样,是个确定的“物”的名字。 03 单片机是不是嵌入式? 那么单片机到底是不是嵌入式呢? 简单来说:是。 因为很多嵌入式产品中被嵌入的计算机系统就是单片机,譬如空调中嵌入的控制板其实核心就是个单片机。实际上大部分家电产品中嵌入的计算机系统都是单片机。 因为单片机足够简单便宜而且够用,所以使用单片机是最划算最适合的。 而单片机现在出货量最大的领域也就是家电产品了,当然未来IOT类的应用会越来越多,会成为单片机的很大的增量市场。 04 广义和狭义的嵌入式 嵌入式这个概念实际上很泛化,现在讲嵌入式这个词的人,可能想表达的意思并不相同。咱们上面讲的嵌入式的概念是嵌入式本来的定义,也就是所谓广义上的嵌入式。 而狭义的嵌入式,其实是“嵌入式linux系统”的简称。 这种狭义的嵌入式最初指的是运行了linux系统的嵌入式计算机系统。后来也包括运行了和linux同级别的其他嵌入式系统(譬如WinCE、Vxworks、Android等)的计算机。 看过上面的介绍之后你就知道到底单片机是不是嵌入式了,其实这两者之间的联系有很深,总之,不管你是准备学习嵌入式或是单片机,都要自己想好了再做决定。 05 嵌入式和单片机的区别 说到这里,我们来看看,嵌入式和单片机的区别到底是什么。 从前文的介绍来看,嵌入式系统是一个大类,单片机是其中一个重要的子类。嵌式系统像是一个完整的计算机,而单片机更像是一个没有外设的计算机。 以前单片机包括的东西并不算多,两者的硬件区别较为明显。 但是,随着半导体技术的突飞猛进,现在各种硬件功能都能被做进单片机之中。所以,嵌入式系统和单片机之间的硬件区别越来越小,分界线也越来越模糊。 于是,人们倾向于在软件上进行区分。 从软件上,行业里经常把芯片中不带MMU(memory management unit,内存管理单元)从而不支持虚拟地址,只能裸奔或运行RTOS(实时操作系统,例如ucos、华为LiteOS、RT-Thread、freertos等)的system,叫做单片机(如STM32、NXP LPC系列、NXP imxRT1052系列等)。 同时,把芯片自带MMU可以支持虚拟地址,能够跑Linux、Vxworks、WinCE、Android这样的“高级”操作系统的system,叫做嵌入式。 在某些时候,单片机本身已经足够强大,可以作为嵌入式系统使用。它的成本更低,开发和维护的难度相对较小,尤其是针对一些针对性更强的应用。而嵌入式系统理论上性能更强,应用更广泛,但复杂度高,开发难度大。 06 我们为什么要学习嵌入式和单片机 今天我也只是给大家简单地介绍了一下单片机和嵌入式以及他们之间的关系和区别,虽然嵌入式系统已经有30多年的历史,但其实一直隐藏在背后的,自从物联网上升为国家战略后,嵌入式系统也渐渐从后台走到前台。 嵌入式和单片机并不是纯“硬件”类方向。如果你想学好嵌入式和单片机,只懂数字电路和微机接口这样的硬件知识是不够的,你更需要学习的,是汇编、C/C++语言、数据结构和算法知识。拥有软硬结合的能力,远远比单纯掌握某种程序开发语言更有价值。 其次,嵌入式和单片机拥有广泛的应用场景,在各个领域都有项目需求和人才需求。而且我们国家现在正在大力发展芯片产业,也会带动嵌入式人才的就业,提升待遇。 随着5G建设的深入,整个社会正在向“万物互联”的方向变革。 物联网技术也将迎来前所未有的历史机遇。嵌入式和单片机技术是物联网技术的重要组成部分,也将进入快速发展的时代。 技术越难,过程越苦,越有利于构建竞争壁垒。大学里很多同学都热衷于学习各种编程语言,往往忽视了这一块,可以说在嵌入式开发这一块的人才我们国家还是比较欠缺的。因此,我觉得大家非常值得投入时间去学习嵌入式开发的技能。原文:https://www.zhihu.com/question/315310041/answer/2179945564

    01-03 507浏览
正在努力加载更多...
广告