• 单片机程序结构优化

    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浏览
  • 单片机开发:一文吃透交叉编译

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

    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浏览
  • 使用VS Code实现编辑,编译,下载,调试

    在刚开始接触STM32的时候,使用的keil作为IDE,由于在这之前,使用过VS, 使用过eclipse,因而在使用keil之后,实在难以忍受keil编辑器简陋的功能,可以说是极其糟糕的写代码体验。 之后,尝试过各种IDE,使用eclipse+keil,结果发现eclipse对C语言的支持也是鸡肋,使用emBits+gcc,需要和其他人协同的话就比较麻烦,之后发现了platformIO,也是使用gcc作为编译器,不过只支持HAL库,而且还有一个重要的原因,同事都是用的keil,如果我使用gcc,就不能协同工作了。 最后,通过使用VS Code + keil的方式,完美解决了写代码的体验问题,以及工程协作问题,其实网上使用VS Code作为编辑器,keil作为编译器的教程很多,不过基本都是需要在VS Code中编辑,然后在keil中编译,下载,调试,本文就要实现编辑,编译,下载,调试,全部使用VS Code。 Part1环境 (1)VS Code; (2)keil;python; (3)GNU Arm Embedded Toolchain(arm gcc工具链); (4)C/C++(VS Code 插件); (5)Cortex-Debug(VS Code 插件); (6)其他VS Code插件(提升体验)。 Part2前提 正式写代码之前,首先需要建立好一个工程,这个需要使用keil完成,包括工程配置,文件添加… Part3编辑 在安装好VS Code插件之后,VS Code编写C代码本身体验就已经很好了, 但是,因为我们使用的是keil环境,所以需要配置头文件包含,宏定义等,在工程路径的.vscode文件夹下打开c_cpp_properties.json文件,没有自己新建一个,内容配置如下: { "configurations": [ { "name": "STM32", "includePath": [ "D:/Program Files/MDK5/ARM/ARMCC/**", "${workspaceFolder}/**", "" ], "browse": { "limitSymbolsToIncludedHeaders": true, "databaseFilename": "${workspaceRoot}/.vscode/.browse.c_cpp.db", "path": [ "D:/Program Files/MDK5/ARM/ARMCC/**", "${workspaceFolder}/**", "" ] }, "defines": [ "_DEBUG", "UNICODE", "_UNICODE", "__CC_ARM", "USE_STDPERIPH_DRIVER", "STM32F10X_MD" ], "intelliSenseMode": "msvc-x64" } ], "version": 4 } 其中,需要在includePath和path中添加头文件路径,${workspaceFolder}/**是工程路径,不用改动,额外需要添加的是keil的头文件路径, 然后在defines中添加宏,也就是在keil的Options for Target的C++选项卡中配置的宏,然后就可以体验VS Code强大的代码提示,函数跳转等功能了(甩keil的编辑器一整个时代)。 Part4编译、烧录 编译和烧录通过VS Code的Task功能实现,通过Task,使用命令行的方式调用keil进行编译和烧录。 keil本身就支持命令行调用,具体可以参考keil的手册,这里就不多说了,但是问题在于,使用命令行调用keil,不管是什么操作,他的输出都不会输出到控制台上!!!(要你这命令行支持有何用) 不过好在,keil支持输出到文件中,那我们就只能利用这个做点骚操作了。一边执行命令,一边读取文件内容并打印到控制台,从而就实现了输出在控制台上,我们就能直接在VS Code中看到编译过程了 为此,我编写了一个Python脚本,实现keil的命令行调用并同时读取文件输出到控制台。 #!/usr/bin/python # -*- coding:UTF-8 -*- import os import threading import sys runing = True def readfile(logfile): with open(logfile, 'w') as f: pass with open(logfile, 'r') as f: while runing: line = f.readline(1000) if line != '': line = line.replace('\\', '/') print(line, end = '') if __name__ == '__main__': modulePath = os.path.abspath(os.curdir) logfile = modulePath + '/build.log' cmd = '\"D:/Program Files/MDK5/UV4/UV4.exe\" ' for i in range(1, len(sys.argv)): cmd += sys.argv[i] + ' ' cmd += '-j0 -o ' + logfile thread = threading.Thread(target=readfile, args=(logfile,)) thread.start() code = os.system(cmd) runing = False thread.join() sys.exit(code) 此脚本需要结合VS Code的Task运行,通过配置Task,我们还需要匹配输出中的错误信息(编译错误),实现在keil中,点击错误直接跳转到错误代码处,具体如何配置请参考VS Code的文档,这里给出我的Task。 { // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "2.0.0", "tasks": [ { "label": "build", "type": "shell", "command": "py", "args": [ "-3", "${workspaceFolder}/scripts/build.py", "-b", "${config:uvprojxPath}" ], "group": { "kind": "build", "isDefault": true }, "problemMatcher": [ { "owner": "c", "fileLocation": [ "relative", "${workspaceFolder}/Project" ], "pattern": { "regexp": "^(.*)\\((\\d+)\\):\\s+(warning|error):\\s+(.*):\\s+(.*)$", "file": 1, "line": 2, "severity": 3, "code": 4, "message": 5 } } ] }, { "label": "rebuild", "type": "shell", "command": "py", "args": [ "-3", "${workspaceFolder}/scripts/build.py", "-r", "${config:uvprojxPath}" ], "group": "build", "problemMatcher": [ { "owner": "c", "fileLocation": [ "relative", "${workspaceFolder}/Project" ], "pattern": { "regexp": "^(.*)\\((\\d+)\\):\\s+(warning|error):\\s+(.*):\\s+(.*)$", "file": 1, "line": 2, "severity": 3, "code": 4, "message": 5 } } ] }, { "label": "download", "type": "shell", "command": "py", "args": [ "-3", "E:\\Work\\Store\\MyWork\\STM32F1\\FreeModbus_M3\\scripts\\build.py", "-f", "${config:uvprojxPath}" ], "group": "test" }, { "label": "open in keil", "type": "process", "command": "${config:uvPath}", "args": [ "${config:uvprojxPath}" ], "group": "test" } ] } 对于使用ARM Compiler 6编译的工程,build和rebuild中的problemMatcher应该配置为: "problemMatcher": [ { "owner": "c", "fileLocation": ["relative", "${workspaceFolder}/MDK-ARM"], "pattern": { "regexp": "^(.*)\\((\\d+)\\):\\s+(warning|error):\\s+(.*)$", "file": 1, "line": 2, "severity": 3, "message": 4, } } ] 文件中的config:uvPath和config:uvprojxPath分别为keil的UV4.exe文件路径和工程路径(.uvprojx),可以直接修改为具体路径,或者在VS Code的setting.json中增加对应的项,至此,我们已经完美实现了在VS Code中编辑,编译,下载了。 编译输出: 有错误时输出: 错误匹配: Part5调试 调试需要使用到Cortex-Debug插件,以及arm gcc工具链,这部分可以参考Cortex-Debug的文档,说的比较详细; 首先安装Cortex-Debug插件和arm gcc工具链,然后配置好环境路径,如果使用Jlink调试,需要下载Jlink套件,安转好之后,找到JLinkGDBServerCL.exe这个程序,在VS Code的设置中添加"cortex-debug.JLinkGDBServerPath": "C:/Program Files (x86)/SEGGER/JLink/JLinkGDBServerCL.exe",后面的路径是你自己的路径。 这里补充一下arm gcc工具链的配置:"cortex-debug.armToolchainPath": "D:\\Program Files (x86)\\GNU Arm Embedded Toolchain\\9 2020-q2-update\\bin",后面的路径是你自己的路径。如果使用STLink调试,需要下载stutil工具,在GitHub上搜索即可找到,同样配置好路径即可。 以上步骤弄好之后,可以直接点击VS Code的调试按钮,此时会新建luanch.json文件,这个文件就是VS Code的调试配置文件,可参考我的文件进行配置。 { // 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。 // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "Cortex Debug(JLINK)", "cwd": "${workspaceRoot}", "executable": "${workspaceRoot}/Project/Objects/Demo.axf", "request": "attach", "type": "cortex-debug", "servertype": "jlink", "device": "STM32F103C8", "svdFile": "D:\\Program Files\\ARM\\Packs\\Keil\\STM32F1xx_DFP\\2.3.0\\SVD\\STM32F103xx.svd", "interface": "swd", "ipAddress": null, "serialNumber": null }, { "name": "Cortex Debug(ST-LINK)", "cwd": "${workspaceRoot}", "executable": "${workspaceRoot}/Project/Objects/Demo.axf", "request": "attach", "type": "cortex-debug", "servertype": "stutil", "svdFile": "D:\\Program Files\\ARM\\Packs\\Keil\\STM32F1xx_DFP\\2.3.0\\SVD\\STM32F103xx.svd", "device": "STM32F103C8", "v1": false } ] } 注意其中几个需要修改的地方,executable修改为你的工程生成的目标文件,也就是工程的.axf文件,svdFile用于对MCU外设的监控,该文件可以在keil的安装路径中找到,可以参考我的路径去找,配置完成后,再次点击调试按钮即可进行调试。 相比keil自己的调试功能,VS Code还支持条件断点,可以设置命中条件,次数等,可以极大的方便调试。 总结 通过以上的配置,我们基本上,除了建立工程和往工程中添加文件,其他完全不需要打开keil,所以也无妨说一句,再见,智障keil! 

    01-09 514浏览
  • Keil编程开发环境(必备)

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

    01-08 367浏览
  • 为什么​嵌入式开发全局变量要越少越好?

    嵌入式开发,特别是单片机os-less的程序,最易范的错误是全局变量满天飞。 这个现象在早期汇编转型过来的程序员以及初学者中常见,这帮家伙几乎把全局变量当作函数形参来用。 在.h文档里面定义许多杂乱的结构体,extern一堆令人头皮发麻的全局变量,然后再这个模块里边赋值123,那个模块里边判断123分支决定做什么。 每当看到这种程序,我总要戚眉变脸而后拍桌怒喝。没错,就是怒喝。 不否认全局变量的重要性,但我认为要十分谨慎地使用它,滥用全局变量会带来其它更为严重的结构性系统问题。 为什么全局变量要越少越好? 它会造成不必要的常量频繁使用,特别当这个常量没有用宏定义“正名”时,代码阅读起来将万分吃力。 它会导致软件分层的不合理,全局变量相当于一条快捷通道,它容易使程序员模糊了“设备层”和“应用层”之间的边界。写出来的底层程序容易自作多情地关注起上层的应用。 这在软件系统的构建初期的确效率很高,功能调试进度一日千里,但到了后期往往bug一堆,处处“补丁”,雷区遍布。说是度日如年举步维艰也不为过。 由于软件的分层不合理,到了后期维护,哪怕仅是增加修改删除小功能,往往要从上到下掘地三尺地修改,涉及大多数模块, 而原有的代码注释却忘了更新修改,这个时候,交给后来维护者的系统会越来越像一个“泥潭”,注释的唯一作用只是使泥潭上方再加一些迷烟瘴气。 全局变量大量使用,少不了有些变量流连忘返于中断与主回圈程序之间。 这个时候如果处理不当,系统的bug就是随机出现的,无规律的,这时候初步显示出病入膏肓的特征来了,没有大牛来力挽狂澜,注定慢性死亡。 无需多言,您已经成功得到一个畸形的系统,它处于一个神秘的稳定状态! 你看着这台机器,机器也看着你,相对无言,心中发毛。你不确定它什么时候会崩溃,也不晓得下一次投诉什么时候道理。 全局变量大量使用有什么后果? “老人”气昂昂,因为系统离不开他,所有“雷区”只有他了然于心。当出现紧急的bug时,只有他能够搞定。你不但不能辞退他,还要给他加薪。 新人见光死,但凡招聘来维护这个系统的,除了改出更多的bug外,基本上一个月内就走人,到了外面还宣扬这个公司的软件质量有够差够烂。 随着产品的后续升级,几个月没有接触这个系统的原创者会发现,很多雷区他本人也忘记了,于是每次的产品升级维护周期越来越长, 因为修改一个功能会冒出很多bug,而按下一个bug,会弹出其他更多的bug。在这期间,又会产生更多的全局变量。 终于有一天他告诉老板,不行啦不行啦,资源不够了,ram或者flash空间太小了,升级升级。 客户投诉不断,售后也快崩溃了,业务员也不敢推荐此产品了,市场份额越来越小,公司形象越来越糟糕。   要问对策,只有两个原则 能不用全局变量尽量不用,我想除了系统状态和控制参数、通信处理和一些需要效率的模块,其他的基本可以靠合理的软件分层和编程技巧来解决。 如果不可避免需要用到,那能藏多深就藏多深。 如果只有某.c文件用,就static到该文件中,顺便把结构体定义也收进来; 如果只有一个函数用,那就static到函数里面去; 如果非要开放出去让人读取,那就用函数return出去,这样就是只读属性了; 如果非要遭人蹂躏赋值,好吧,我开放函数接口让你传参赋值; 实在非要extern侵犯我,我还可以严格控制包含我.h档的对象,而不是放到公共的includes.h中被人围观,丢人现眼。 如此,你可明白我对全局变量的感悟有多深刻,悲催的我,已经把当年那些“老人”交给我维护的那些案子加班全部重新翻写了。 最后补充 全局变量是不可避免要用到的,每一个设备底层几乎都需要它来记录当前状态,控制时序,起承转合。但是尽量不要用来传递参数,这个很忌讳的。 尽量把变量的作用范围控制在使用它的模块里面,如果其他模块要访问,就开个读或写函数接口出来,严格控制访问范围。 这一点,C++的private属性就是这么干的,这对将来程序的调试也很有好处。 C语言之所以有++版本,很大原因就是为了控制它的灵活性,要说面向对象的思想,C语言早已有之,亦可实现。 当一个模块里面的全局变量超过3个(含)时,就用结构体包起来吧,要归0便一起归0,省得丢三落四的。 在函数里面开个静态的全局变量,全局数组,是不占用栈空间的,只是有些编译器对于大块的全局数组,会放到和一般变量不同的地址区。 若是在keil C51,因为是静态编译,栈爆掉了会报警,所以大可以尽情驰骋,注意交通规则就是了。 单片机的os-less系统中,只有栈没有堆的用法,那些默认对堆分配空间的“startup.s”,可以大胆的把堆空间干掉。 程序模型?如何分析抽象出来呢,从哪个角度进行模型构建呢?很愿意聆听网友的意见。 本人一直以来都是从两个角度分析系统,事件--状态机迁移图 和 数据流图,前者分析控制流向,完善UI,后者可知晓系统数据的缘起缘灭。 这些理论,院校的《软件工程》教材都有,大家不妨借鉴下。只不过那些理论,终究是起源于大型系统软件管理的,牛刀杀鸡,还是要裁剪一下的。 

    01-03 376浏览
  • 基于STM32的室内温湿度采集控制系统

    Proteus仿真——《基于STM32的室内温湿度采集控制系统》

    01-03 162浏览
  • AT89C51单片机控制LED实例电路图解析

    AT89C51是40针微控制器,属于8051系列微控制器。它有四个端口,每个端口有8位P0,P1,P2和P3。AT89C51具有4K字节的可编程闪存。端口P0覆盖引脚32至引脚39,端口P1覆盖引脚1至引脚8,端口P2覆盖引脚21至引脚28,端口P...

    2024-12-12 284浏览
  • 搞定pic单片机IO口操作

    对于pic单片机的学习,很多朋友总是能充满激情,不断利用闲余时间研究pic单片机的各类技术。而谈及pic单片机,必须牵扯至51、AVR单片机。因此本文中,将探讨pic单片机以及51、AVR单片机对于IO口的操作。对于本文,希...

    2024-12-12 213浏览
  • 单片机编程软件基础篇,IAR单片机编程软件菜单栏讲解

    单片机编程软件是单片机编程不可或缺的利器,一款好的单片机编程软件更能极大程度提高开发效率。在本文中,主要为大家介绍IAR单片机编程软件的菜单栏,以帮助大家更好了解这款单片机编程软件。 Ⅰ、写在前面 IAR软件...

    2024-12-12 263浏览
正在努力加载更多...
广告