第一篇:嵌入式系统c语言编程该怎么学?
雪中悍刀行 http://bmdqw.com/
嵌入式系统c语言编程该怎么学?
C语言博大精深,玩了很长时间了,一直徘徊在入门处。看了很多别人的编程经验,加上项目程序越做越大,直到这半年来突然有很多体会,明天就要回家了,下午闲来无事也试着总结一些心得体会,喜欢对师弟妹们的学习有所帮助。
首先要说说编程的几个重要原则,看了很多别人的编程经验,更多的是说技巧。技巧能显著提高程序的效率,固然重要但是技巧的掌握靠了还是大量的工程实践,只有在有一定功底后才可以去追求这些编程技巧。但是编程的原则却是要在学习一开始就要认真贯彻,才能养成良好的编程习惯,苦练内功后练上层功夫才不会走火入魔。
言归正传,嵌入式系统C语言编程需要遵守什么样的原则呢?随着时代和技术的不断发展,这个问题也许仁者见仁智者见智了,但是总结起来大家还是有很多共识。根据目前提倡的软件工程的做法,和我们教研室的做法,列举最重要原则:
一、模块划分.C语言作为一种结构化的程序设计语言,在模块的划分上主要依据功能:
(1)一个功能模块即是一个.c文件和一个.h文件的结合,.h文件中是对于该模块功能函数和使变 量的声明
(2)该模块提供给其它模块调用的外部函数及数据都需要在.h中文件中以extern关键字声明
(3)模块内的函数和全局变量只能在.c文件定义
(4)不允许在.h文件中定义变量(定义变量和声明变量的区别在于定义会产生内存分配的操作,而声明则只是告诉包含该声明的模块在连接阶段从其它模块寻找外部函数和变量)。
二、一个嵌入式系统程序包括两类模块分三层编写:
(1)硬件驱动模块,一种特定硬件对应一个模块,包括了片内集成的硬件模块和外部扩展的(2)软件功能模块,软件功能模块是建立在硬件驱动模块上的与硬件无关的逻辑功能。
(3)三层编写即HAL(硬件应用层),API(应用函数包),APP(逻辑应用层)注:名称是借用的,表大个意思而已。HAL就是硬件驱动模块和系统硬件密切相关,API可以是建立在HAL上的硬件应用服务程序也可以是通用的函数模块,APP则是最终构成嵌入式系统应用的功能逻辑关系。HAL和API是为了方便技术积累和提高开发效率而分开了,APP则是针对特殊应用而定制的。
三、中断服务程序的要求:
(1)不能返回值
(2)不能向ISR传递参数
(3)ISR应该尽可能的短小精悍,不允许有等待信号的操作
四、编程风格问题
五、需要学会熟练应用的C语言的基本手法
(1)数据指针,不能仅仅只会使用数组
(2)宏定义,定义寄存器地址,定义宏函数等
(3)函数指针的应用
(4)条件编译,在带操作系统的应用时经常要用
六、不要偷懒,写好必要的注释
七、做到以上几点时就可以吸收高超的编程技巧了。转载请保留连接
本文由www.xiexiebang.com整理
第二篇:C语言嵌入式系统编程修炼之道
C语言嵌入式系统编程修炼之道收藏
C语言嵌入式系统编程修炼之道——背景篇...1 C语言嵌入式系统编程修炼之道——软件架构篇...4 1.模块划分...4 2.多任务还是单任务...5 3.单任务程序典型架构...6 4.中断服务程序...7 5.硬件驱动模块...9 6.C的面向对象化...10 总结...10 C语言嵌入式系统编程修炼之道——内存操作篇...12 1.数据指针...12 2.函数指针...13 3.数组vs.动态申请...14 4.关键字const 15 5.关键字volatile.16 6.CPU字长与存储器位宽不一致处理...17 总结...18 C语言嵌入式系统编程修炼之道——屏幕操作篇...19 1.汉字处理...19 2.系统时间显示...20 3.动画显示...21 4.菜单操作...22 5.模拟MessageBox函数...24 总结...26 C语言嵌入式系统编程修炼之道——键盘操作篇...27 1.处理功能键...27 2.处理数字键...28 3.整理用户输入...29 总结...30 C语言嵌入式系统编程修炼之道——性能优化篇...31 1.使用宏定义...31 2.使用寄存器变量...31 3.内嵌汇编...32 4.利用硬件特性...32 5.活用位操作...33 总结
C语言嵌入式系统编程修炼之道——背景篇 不同于一般形式的软件编程,嵌入式系统编程建立在特定的硬件平台上,势必要求其编程语言具备较强的硬件直接操作能力。无疑,汇编语言具备这样的特质。但是,归因于汇编语言开发过程的复杂性,它并不是嵌入式系统开发的一般选择。而与之相比,C语言——一种“高级的低级”语言,则成为嵌入式系统开发的最佳选择。笔者在嵌入式系统项目的开发过程中,一次又一次感受到C语言的精妙,沉醉于C语言给嵌入式开发带来的便利。本文的目的在于进行“C语言嵌入式系统开发的内功心法”秀,一共包括25招。
图1给出了本文的讨论所基于的硬件平台,实际上,这也是大多数嵌入式系统的硬件平台。它包括两部分:
(1)
以通用处理器为中心的协议处理模块,用于网络控制协议的处理;(2)
以数字信号处理器(DSP)为中心的信号处理模块,用于调制、解调和数/模信号转换。
本文的讨论主要围绕以通用处理器为中心的协议处理模块进行,因为它更多地牵涉到具体的C语言编程技巧。而DSP编程则重点关注具体的数字信号处理算法,主要涉及通信领域的知识,不是本文的讨论重点。
着眼于讨论普遍的嵌入式系统C编程技巧,系统的协议处理模块没有选择特别的CPU,而是选择了众所周知的CPU芯片——80186,每一位学习过《微机原理》的读者都应该对此芯片有一个基本的认识,且对其指令集比较熟悉。80186的字长是16位,可以寻址到的内存空间为1MB,只有实地址模式。C语言编译生成的指针为32位(双字),高16位为段地址,低16位为段内编译,一段最多64KB。
图1 系统硬件架构
协议处理模块中的FLASH和RAM几乎是每个嵌入式系统的必备设备,前者用于存储程序,后者则是程序运行时指令及数据的存放位置。系统所选择的FLASH和RAM的位宽都为16位,与CPU一致。
实时钟芯片可以为系统定时,给出当前的年、月、日及具体时间(小时、分、秒及毫秒),可以设定其经过一段时间即向CPU提出中断或设定报警时间到来时向CPU提出中断(类似闹钟功能)。
NVRAM(非易失去性RAM)具有掉电不丢失数据的特性,可以用于保存系统的设置信息,譬如网络协议参数等。在系统掉电或重新启动后,仍然可以读取先前的设置信息。其位宽为8位,比CPU字长小。文章特意选择一个与CPU字长不一致的存储芯片,为后文中一节的讨论创造条件。
UART则完成CPU并行数据传输与RS-232串行数据传输的转换,它可以在接收到[1~MAX_BUFFER]字节后向CPU提出中断,MAX_BUFFER为UART芯片存储接收到字节的最大缓冲区。
键盘控制器和显示控制器则完成系统人机界面的控制。以上提供的是一个较完备的嵌入式系统硬件架构,实际的系统可能包含更少的外设。之所以选择一个完备的系统,是为了后文更全面的讨论嵌入式系统C语言编程技巧的方方面面,所有设备都会成为后文的分析目标。
嵌入式系统需要良好的软件开发环境的支持,由于嵌入式系统的目标机资源受限,不可能在其上建立庞大、复杂的开发环境,因而其开发环境和目标运行环境相互分离。因此,嵌入式应用软件的开发方式一般是,在宿主机(Host)上建立开发环境,进行应用程序编码和交叉编译,然后宿主机同目标机(Target)建立连接,将应用程序下载到目标机上进行交叉调试,经过调试和优化,最后将应用程序固化到目标机中实际运行。
CAD-UL是适用于x86处理器的嵌入式应用软件开发环境,它运行在Windows操作系统之上,可生成x86处理器的目标代码并通过PC机的COM口(RS-232串口)或以太网口下载到目标机上运行,如图2。其驻留于目标机FLASH存储器中的monitor程序可以监控宿主机Windows调试平台上的用户调试指令,获取CPU寄存器的值及目标机存储空间、I/O空间的内容。图2 交叉开发环境
后续章节将从软件架构、内存操作、屏幕操作、键盘操作、性能优化等多方面阐述C语言嵌入式系统的编程技巧。软件架构是一个宏观概念,与具体硬件的联系不大;内存操作主要涉及系统中的FLASH、RAM和NVRAM芯片;屏幕操作则涉及显示控制器和实时钟;键盘操作主要涉及键盘控制器;性能优化则给出一些具体的减小程序时间、空间消耗的技巧。
本文即将讲述的25个主题可分为两类,一类是编程技巧,有很强的适用性;一类则介绍嵌入式系统编程的一般常识,具有一定的理论意义。So, let’s go.C语言嵌入式系统编程修炼之道——软件架构篇 1.模块划分
模块划分的“划”是规划的意思,意指怎样合理的将一个很大的软件划分为一系列功能独立的部分合作完成系统的需求。C语言作为一种结构化的程序设计语言,在模块的划分上主要依据功能(依功能进行划分在面向对象设计中成为一个错误,牛顿定律遇到了相对论),C语言模块化程序设计需理解如下概念:(1)
模块即是一个.c文件和一个.h文件的结合,头文件(.h)中是对于该模块接口的声明;
(2)
某模块提供给其它模块调用的外部函数及数据需在.h中文件中冠以extern关键字声明;
(3)
模块内的函数和全局变量需在.c文件开头冠以static关键字声明;(4)
永远不要在.h文件中定义变量!定义变量和声明变量的区别在于定义会产生内存分配的操作,是汇编阶段的概念;而声明则只是告诉包含该声明的模块在连接阶段从其它模块寻找外部函数和变量。如: /*module1.h*/ int a = 5;
/* 在模块1的.h文件中定义int a */
/*module1.c*/ #include “module1.h”
/* 在模块1中包含模块1的.h文件 */ /*module2.c*/ #include “module1.h”
/* 在模块2中包含模块1的.h文件 */ /*module3.c*/ #include “module1.h”
/* 在模块3中包含模块1的.h文件 */ 以上程序的结果是在模块1、2、3中都定义了整型变量a,a在不同的模块中对应不同的地址单元,这个世界上从来不需要这样的程序。正确的做法是: /*module1.h*/ extern int a;
/* 在模块1的.h文件中声明int a */ /*module1.c*/ #include “module1.h”
/* 在模块1中包含模块1的.h文件 */ int a = 5;
/* 在模块1的.c文件中定义int a */ /*module2.c*/ #include “module1.h”
/* 在模块2中包含模块1的.h文件 */
/*module3.c*/ #include “module1.h”
/* 在模块3中包含模块1的.h文件 */ 这样如果模块1、2、3操作a的话,对应的是同一片内存单元。一个嵌入式系统通常包括两类模块:
(1)硬件驱动模块,一种特定硬件对应一个模块;
(2)软件功能模块,其模块的划分应满足低偶合、高内聚的要求。2.多任务还是单任务
所谓“单任务系统”是指该系统不能支持多任务并发操作,宏观串行地执行一个任务。而多任务系统则可以宏观并行(微观上可能串行)地“同时”执行多个任务。
多任务的并发执行通常依赖于一个多任务操作系统(OS),多任务OS的核心是系统调度器,它使用任务控制块(TCB)来管理任务调度功能。TCB包括任务的当前状态、优先级、要等待的事件或资源、任务程序码的起始地址、初始堆栈指针等信息。调度器在任务被激活时,要用到这些信息。此外,TCB还被用来存放任务的“上下文”(context)。任务的上下文就是当一个执行中的任务被停止时,所要保存的所有信息。通常,上下文就是计算机当前的状态,也即各个寄存器的内容。当发生任务切换时,当前运行的任务的上下文被存入TCB,并将要被执行的任务的上下文从它的TCB中取出,放入各个寄存器中。嵌入式多任务OS的典型例子有Vxworks、ucLinux等。嵌入式OS并非遥不可及的神坛之物,我们可以用不到1000行代码实现一个针对80186处理器的功能最简单的OS内核,作者正准备进行此项工作,希望能将心得贡献给大家。
究竟选择多任务还是单任务方式,依赖于软件的体系是否庞大。例如,绝大多数手机程序都是多任务的,但也有一些小灵通的协议栈是单任务的,没有操作系统,它们的主程序轮流调用各个软件模块的处理程序,模拟多任务环境。3.单任务程序典型架构
(1)从CPU复位时的指定地址开始执行;(2)跳转至汇编代码startup处执行;
(3)跳转至用户主程序main执行,在main中完成: a.初试化各硬件设备;
b.初始化各软件模块; c.进入死循环(无限循环),调用各模块的处理函数
用户主程序和各模块的处理函数都以C语言完成。用户主程序最后都进入了一个死循环,其首选方案是: while(1){ } 有的程序员这样写: for(;;){ } 这个语法没有确切表达代码的含义,我们从for(;;)看不出什么,只有弄明白for(;;)在C语言中意味着无条件循环才明白其意。下面是几个“著名”的死循环:(1)操作系统是死循环;(2)WIN32程序是死循环;(3)嵌入式系统软件是死循环;
(4)多线程程序的线程处理函数是死循环。你可能会辩驳,大声说:“凡事都不是绝对的,2、3、4都可以不是死循环”。Yes,you are right,但是你得不到鲜花和掌声。实际上,这是一个没有太大意义的牛角尖,因为这个世界从来不需要一个处理完几个消息就喊着要OS杀死它的WIN32程序,不需要一个刚开始RUN就自行了断的嵌入式系统,不需要莫名其妙启动一个做一点事就干掉自己的线程。有时候,过于严谨制造的不是便利而是麻烦。君不见,五层的TCP/IP协议栈超越严谨的ISO/OSI七层协议栈大行其道成为事实上的标准? 经常有网友讨论:
printf(“%d,%d”,++i,i++);
/* 输出是什么?*/ c = a+++b;
/* c=? */ 等类似问题。面对这些问题,我们只能发出由衷的感慨:世界上还有很多有意义的事情等着我们去消化摄入的食物。实际上,嵌入式系统要运行到世界末日。4.中断服务程序
中断是嵌入式系统中重要的组成部分,但是在标准C中不包含中断。许多编译开发商在标准C上增加了对中断的支持,提供新的关键字用于标示中断服务程序(ISR),类似于__interrupt、#program interrupt等。当一个函数被定义为ISR的时候,编译器会自动为该函数增加中断服务程序所需要的中断现场入栈和出栈代码。
中断服务程序需要满足如下要求:(1)不能返回值;
(2)不能向ISR传递参数;
(3)ISR应该尽可能的短小精悍;
(4)printf(char * lpFormatString,„)函数会带来重入和性能问题,不能在ISR中采用。
在某项目的开发中,我们设计了一个队列,在中断服务程序中,只是将中断类型添加入该队列中,在主程序的死循环中不断扫描中断队列是否有中断,有则取出队列中的第一个中断类型,进行相应处理。/* 存放中断的队列 */ typedef struct tagIntQueue { int intType;
/* 中断类型 */ struct tagIntQueue *next;}IntQueue;
IntQueue lpIntQueueHead;
__interrupt ISRexample(){
int intType;
intType = GetSystemType();QueueAddTail(lpIntQueueHead, intType);/* 在队列尾加入新的中断 */ } 在主程序循环中判断是否有中断: While(1){ If(!IsIntQueueEmpty())
{
intType = GetFirstInt();
switch(intType)
/* 是不是很象WIN32程序的消息解析函数? */
{
/* 对,我们的中断类型解析很类似于消息驱动 */
case xxx:
/* 我们称其为“中断驱动”吧? */
…
break;
case xxx:
…
break;
…
} }
} 按上述方法设计的中断服务程序很小,实际的工作都交由主程序执行了。5.硬件驱动模块
一个硬件驱动模块通常应包括如下函数:(1)中断服务程序ISR(2)硬件初始化
a.修改寄存器,设置硬件参数(如UART应设置其波特率,AD/DA设备应设置其采样速率等);
b.将中断服务程序入口地址写入中断向量表: /* 设置中断向量表 */
m_myPtr = make_far_pointer(0l);/* 返回void far型指针void far * */
m_myPtr += ITYPE_UART;/* ITYPE_UART: uart中断服务程序 */ /* 相对于中断向量表首地址的偏移 */
*m_myPtr = &UART _Isr;
/* UART _Isr:UART的中断服务程序 */(3)设置CPU针对该硬件的控制线
a.如果控制线可作PIO(可编程I/O)和控制信号用,则设置CPU内部对应寄存器使其作为控制信号;
b.设置CPU内部的针对该设备的中断屏蔽位,设置中断方式(电平触发还是边缘触发)。
(4)提供一系列针对该设备的操作接口函数。例如,对于LCD,其驱动模块应提供绘制像素、画线、绘制矩阵、显示字符点阵等函数;而对于实时钟,其驱动模块则需提供获取时间、设置时间等函数。6.C的面向对象化
在面向对象的语言里面,出现了类的概念。类是对特定数据的特定操作的集合体。类包含了两个范畴:数据和操作。而C语言中的struct仅仅是数据的集合,我们可以利用函数指针将struct模拟为一个包含数据和操作的“类”。下面的C程序模拟了一个最简单的“类”: #ifndef C_Class
#define C_Class struct #endif C_Class A {
C_Class A *A_this;
/* this指针 */
void(*Foo)(C_Class A *A_this);/* 行为:函数指针 */
int a;
/* 数据 */
int b;};我们可以利用C语言模拟出面向对象的三个特性:封装、继承和多态,但是更多的时候,我们只是需要将数据与行为封装以解决软件结构混乱的问题。C模拟面向对象思想的目的不在于模拟行为本身,而在于解决某些情况下使用C语言编程时程序整体框架结构分散、数据和函数脱节的问题。我们在后续章节会看到这样的例子。总结
本篇介绍了嵌入式系统编程软件架构方面的知识,主要包括模块划分、多任务还是单任务选取、单任务程序典型架构、中断服务程序、硬件驱动模块设计等,从宏观上给出了一个嵌入式系统软件所包含的主要元素。
请记住:软件结构是软件的灵魂!结构混乱的程序面目可憎,调试、测试、维护、升级都极度困难。
一个高尚的程序员应该是写出如艺术作品般程序的程序员。
C语言嵌入式系统编程修炼之道——内存操作篇 1.数据指针
在嵌入式系统的编程中,常常要求在特定的内存单元读写内容,汇编有对应的MOV指令,而除C/C++以外的其它编程语言基本没有直接访问绝对地址的能力。在嵌入式系统的实际调试中,多借助C语言指针所具有的对绝对地址单元内容的读写能力。以指针直接操作内存多发生在如下几种情况:
(1)
某I/O芯片被定位在CPU的存储空间而非I/O空间,而且寄存器对应于某特定地址;
(2)
两个CPU之间以双端口RAM通信,CPU需要在双端口RAM的特定单元(称为mail box)书写内容以在对方CPU产生中断;
(3)
读取在ROM或FLASH的特定单元所烧录的汉字和英文字模。譬如:
unsigned char *p =(unsigned char *)0xF000FF00;*p=11;以上程序的意义为在绝对地址0xF0000+0xFF00(80186使用16位段地址和16位偏移地址)写入11。在使用绝对地址指针时,要注意指针自增自减操作的结果取决于指针指向的数据类别。上例中p++后的结果是p= 0xF000FF01,若p指向int,即: int *p =(int *)0xF000FF00;p++(或++p)的结果等同于:p = p+sizeof(int),而p—(或—p)的结果是p = p-sizeof(int)。同理,若执行:
long int *p =(long int *)0xF000FF00;则p++(或++p)的结果等同于:p = p+sizeof(long int),而p—(或—p)的结果是p = p-sizeof(long int)。
记住:CPU以字节为单位编址,而C语言指针以指向的数据类型长度作自增和自减。理解这一点对于以指针直接操作内存是相当重要的。2.函数指针
首先要理解以下三个问题:
(1)C语言中函数名直接对应于函数生成的指令代码在内存中的地址,因此函数名可以直接赋给指向函数的指针;
(2)调用函数实际上等同于“调转指令+参数传递处理+回归位置入栈”,本质上最核心的操作是将函数生成的目标代码的首地址赋给CPU的PC寄存器;(3)因为函数调用的本质是跳转到某一个地址单元的code去执行,所以可以“调用”一个根本就不存在的函数实体,晕?请往下看: 请拿出你可以获得的任何一本大学《微型计算机原理》教材,书中讲到,186 CPU启动后跳转至绝对地址0xFFFF0(对应C语言指针是0xF000FFF0,0xF000为段地址,0xFFF0为段内偏移)执行,请看下面的代码:
typedef void(*lpFunction)();
/* 定义一个无参数、无返回类型的 */ /* 函数指针类型 */ lpFunction lpReset =(lpFunction)0xF000FFF0;
/* 定义一个函数指针,指向*/ /* CPU启动后所执行第一条指令的位置 */ lpReset();
/* 调用函数 */ 在以上的程序中,我们根本没有看到任何一个函数实体,但是我们却执行了这样的函数调用:lpReset(),它实际上起到了“软重启”的作用,跳转到CPU启动后第一条要执行的指令的位置。
记住:函数无它,唯指令集合耳;你可以调用一个没有函数体的函数,本质上只是换一个地址开始执行指令!3.数组vs.动态申请
在嵌入式系统中动态内存申请存在比一般系统编程时更严格的要求,这是因为嵌入式系统的内存空间往往是十分有限的,不经意的内存泄露会很快导致系统的崩溃。
所以一定要保证你的malloc和free成对出现,如果你写出这样的一段程序: char * function(void){
char *p;
p =(char *)malloc(…);
if(p==NULL)„;
„
/* 一系列针对p的操作 */ return p;} 在某处调用function(),用完function中动态申请的内存后将其free,如下: char *q = function();„ free(q);上述代码明显是不合理的,因为违反了malloc和free成对出现的原则,即“谁申请,就由谁释放”原则。不满足这个原则,会导致代码的耦合度增大,因为用户在调用function函数时需要知道其内部细节!
正确的做法是在调用处申请内存,并传入function函数,如下: char *p=malloc(…);if(p==NULL)„;function(p);„ free(p);p=NULL;而函数function则接收参数p,如下: void function(char *p){ „
/* 一系列针对p的操作 */ } 基本上,动态申请内存方式可以用较大的数组替换。对于编程新手,笔者推荐你尽量采用数组!嵌入式系统可以以博大的胸襟接收瑕疵,而无法“海纳”错误。毕竟,以最笨的方式苦练神功的郭靖胜过机智聪明却范政治错误走反革命道路的杨康。
给出原则:
(1)尽可能的选用数组,数组不能越界访问(真理越过一步就是谬误,数组越过界限就光荣地成全了一个混乱的嵌入式系统);
(2)如果使用动态申请,则申请后一定要判断是否申请成功了,并且malloc和free应成对出现!4.关键字const const意味着“只读”。区别如下代码的功能非常重要,也是老生长叹,如果你还不知道它们的区别,而且已经在程序界摸爬滚打多年,那只能说这是一个悲哀: const int a;int const a;const int *a;int * const a;int const * a const;(1)关键字const的作用是为给读你代码的人传达非常有用的信息。例如,在函数的形参前添加const关键字意味着这个参数在函数体内不会被修改,属于“输入参数”。在有多个形参的时候,函数的调用者可以凭借参数前是否有const关键字,清晰的辨别哪些是输入参数,哪些是可能的输出参数。
(2)合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改,这样可以减少bug的出现。const在C++语言中则包含了更丰富的含义,而在C语言中仅意味着:“只能读的普通变量”,可以称其为“不能改变的变量”(这个说法似乎很拗口,但却最准确的表达了C语言中const的本质),在编译阶段需要的常数仍然只能以#define宏定义!故在C语言中如下程序是非法的: const int SIZE = 10;char a[SIZE];/* 非法:编译阶段不能用到变量 */ 5.关键字volatile C语言编译器会对用户书写的代码进行优化,譬如如下代码: int a,b,c;a = inWord(0x100);/*读取I/O空间0x100端口的内容存入a变量*/ b = a;a = inWord(0x100);/*再次读取I/O空间0x100端口的内容存入a变量*/ c = a;很可能被编译器优化为: int a,b,c;a = inWord(0x100);/*读取I/O空间0x100端口的内容存入a变量*/ b = a;c = a;但是这样的优化结果可能导致错误,如果I/O空间0x100端口的内容在执行第一次读操作后被其它程序写入新值,则其实第2次读操作读出的内容与第一次不同,b和c的值应该不同。在变量a的定义前加上volatile关键字可以防止编译器的类似优化,正确的做法是: volatile int a;
volatile变量可能用于如下几种情况:
(1)并行设备的硬件寄存器(如:状态寄存器,例中的代码属于此类);(2)一个中断服务子程序中会访问到的非自动变量(也就是全局变量);(3)多线程应用中被几个任务共享的变量。6.CPU字长与存储器位宽不一致处理
在背景篇中提到,本文特意选择了一个与CPU字长不一致的存储芯片,就是为了进行本节的讨论,解决CPU字长与存储器位宽不一致的情况。80186的字长为16,而NVRAM的位宽为8,在这种情况下,我们需要为NVRAM提供读写字节、字的接口,如下: typedef unsigned char BYTE;typedef unsigned int WORD;
/* 函数功能:读NVRAM中字节
* 参数:wOffset,读取位置相对NVRAM基地址的偏移
* 返回:读取到的字节值 */ extern BYTE ReadByteNVRAM(WORD wOffset){
LPBYTE lpAddr =(BYTE*)(NVRAM + wOffset * 2);/* 为什么偏移要×2? */
return *lpAddr;}
/* 函数功能:读NVRAM中字
* 参数:wOffset,读取位置相对NVRAM基地址的偏移
* 返回:读取到的字 */ extern WORD ReadWordNVRAM(WORD wOffset){
WORD wTmp = 0;
LPBYTE lpAddr;
/* 读取高位字节 */
lpAddr =(BYTE*)(NVRAM + wOffset * 2);
/* 为什么偏移要×2? */
wTmp +=(*lpAddr)*256;
/* 读取低位字节 */
lpAddr =(BYTE*)(NVRAM +(wOffset +1)* 2);
/* 为什么偏移要×2? */
wTmp += *lpAddr;
return wTmp;}
/* 函数功能:向NVRAM中写一个字节
*参数:wOffset,写入位置相对NVRAM基地址的偏移 *
byData,欲写入的字节 */ extern void WriteByteNVRAM(WORD wOffset, BYTE byData){
… }
/* 函数功能:向NVRAM中写一个字 */ *参数:wOffset,写入位置相对NVRAM基地址的偏移 *
wData,欲写入的字 */ extern void WriteWordNVRAM(WORD wOffset, WORD wData){
… } 子贡问曰:Why偏移要乘以2? 子曰:请看图1,16位80186与8位NVRAM之间互连只能以地址线A1对其A0,CPU本身的A0与NVRAM不连接。因此,NVRAM的地址只能是偶数地址,故每次以2为单位前进!
图1 CPU与NVRAM地址线连接
子贡再问:So why 80186的地址线A0不与NVRAM的A0连接? 子曰:请看《IT论语》之《微机原理篇》,那里面讲述了关于计算机组成的圣人之道。总结
本篇主要讲述了嵌入式系统C编程中内存操作的相关技巧。掌握并深入理解关于数据指针、函数指针、动态申请内存、const及volatile关键字等的相关知识,是一个优秀的C语言程序设计师的基本要求。当我们已经牢固掌握了上述技巧后,我们就已经学会了C语言的99%,因为C语言最精华的内涵皆在内存操作中体现。
我们之所以在嵌入式系统中使用C语言进行程序设计,99%是因为其强大的内存操作能力!
如果你爱编程,请你爱C语言; 如果你爱C语言,请你爱指针; 如果你爱指针,请你爱指针的指针!
C语言嵌入式系统编程修炼之道——屏幕操作篇 1.汉字处理
现在要解决的问题是,嵌入式系统中经常要使用的并非是完整的汉字库,往往只是需要提供数量有限的汉字供必要的显示功能。例如,一个微波炉的LCD上没有必要提供显示“电子邮件”的功能;一个提供汉字显示功能的空调的LCD上不需要显示一条“短消息”,诸如此类。但是一部手机、小灵通则通常需要包括较完整的汉字库。
如果包括的汉字库较完整,那么,由内码计算出汉字字模在库中的偏移是十分简单的:汉字库是按照区位的顺序排列的,前一个字节为该汉字的区号,后一个字节为该字的位号。每一个区记录94个汉字,位号则为该字在该区中的位置。因此,汉字在汉字库中的具体位置计算公式为:94*(区号-1)+位号-1。减1是因为数组是以0为开始而区号位号是以1为开始的。只需乘上一个汉字字模占用的字节数即可,即:(94*(区号-1)+位号-1)*一个汉字字模占用字节数,以16*16点阵字库为例,计算公式则为:(94*(区号-1)+(位号-1))*32。汉字库中从该位置起的32字节信息记录了该字的字模信息。
对于包含较完整汉字库的系统而言,我们可以以上述规则计算字模的位置。但是如果仅仅是提供少量汉字呢?譬如几十至几百个?最好的做法是: 定义宏:
# define EX_FONT_CHAR(value)
# define EX_FONT_UNICODE_VAL(value)(value), # define EX_FONT_ANSI_VAL(value)(value), 定义结构体:
typedef struct _wide_unicode_font16x16 { WORD value;
/* 内码 */ BYTE data[32];/* 字模点阵 */ }Unicode;#define CHINESE_CHAR_NUM „
/* 汉字数量 */ 字模的存储用数组:
Unicode chinese[CHINESE_CHAR_NUM] = { {
EX_FONT_CHAR(“业”)
EX_FONT_UNICODE_VAL(0x4e1a)
{0x04, 0x40, 0x04, 0x40, 0x04, 0x40, 0x04, 0x44, 0x44, 0x46, 0x24, 0x4c, 0x24, 0x48, 0x14, 0x50, 0x1c, 0x50,0x14, 0x60, 0x04, 0x40, 0x04, 0x40, 0x04, 0x44, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00}
},{
EX_FONT_CHAR(“中”)
EX_FONT_UNICODE_VAL(0x4e2d)
{0x01, 0x00, 0x01, 0x00, 0x21, 0x08, 0x3f, 0xfc, 0x21, 0x08, 0x21, 0x08, 0x21, 0x08, 0x21, 0x08, 0x21, 0x08,0x3f, 0xf8, 0x21, 0x08, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00}
},{
EX_FONT_CHAR(“云”)
EX_FONT_UNICODE_VAL(0x4e91)
{0x00, 0x00, 0x00, 0x30, 0x3f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0xff, 0xfe, 0x03, 0x00, 0x07, 0x00,0x06, 0x40, 0x0c, 0x20, 0x18, 0x10, 0x31, 0xf8, 0x7f, 0x0c, 0x20, 0x08, 0x00, 0x00}
},{
EX_FONT_CHAR(“件”)
EX_FONT_UNICODE_VAL(0x4ef6)
{0x10, 0x40, 0x1a, 0x40, 0x13, 0x40, 0x32, 0x40, 0x23, 0xfc, 0x64, 0x40, 0xa4, 0x40, 0x28, 0x40, 0x2f, 0xfe,0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40}
} } 要显示特定汉字的时候,只需要从数组中查找内码与要求汉字内码相同的即可获得字模。如果前面的汉字在数组中以内码大小顺序排列,那么可以以二分查找法更高效的查找到汉字的字模。
这是一种很有效的组织小汉字库的方法,它可以保证程序有很好的结构。2.系统时间显示
从NVRAM中可以读取系统的时间,系统一般借助NVRAM产生的秒中断每秒读取一次当前时间并在LCD上显示。关于时间的显示,有一个效率问题。因为时间有其特殊性,那就是60秒才有一次分钟的变化,60分钟才有一次小时变化,如果我们每次都将读取的时间在屏幕上完全重新刷新一次,则浪费了大量的系统时间。
一个较好的办法是我们在时间显示函数中以静态变量分别存储小时、分钟、秒,只有在其内容发生变化的时候才更新其显示。extern void DisplayTime(…){
static BYTE byHour,byMinute,bySecond;
BYTE byNewHour, byNewMinute, byNewSecond;
byNewHour = GetSysHour();
byNewMinute = GetSysMinute();
byNewSecond = GetSysSecond();
if(byNewHour!= byHour)
{ „
/* 显示小时 */ byHour = byNewHour;}
if(byNewMinute!= byMinute)
{ „
/* 显示分钟 */ byMinute = byNewMinute;}
if(byNewSecond!= bySecond)
{ „
/* 显示秒钟 */ bySecond = byNewSecond;} } 这个例子也可以顺便作为C语言中static关键字强大威力的证明。当然,在C++语言里,static具有了更加强大的威力,它使得某些数据和函数脱离“对象”而成为“类”的一部分,正是它的这一特点,成就了软件的无数优秀设计。3.动画显示
动画是无所谓有,无所谓无的,静止的画面走的路多了,也就成了动画。随着时间的变更,在屏幕上显示不同的静止画面,即是动画之本质。所以,在一个嵌入式系统的LCD上欲显示动画,必须借助定时器。没有硬件或软件定时器的世界是无法想像的:
(1)
没有定时器,一个操作系统将无法进行时间片的轮转,于是无法进行多任务的调度,于是便不再成其为一个多任务操作系统;
(2)
没有定时器,一个多媒体播放软件将无法运作,因为它不知道何时应该切换到下一帧画面;
(3)
没有定时器,一个网络协议将无法运转,因为其无法获知何时包传输超时并重传之,无法在特定的时间完成特定的任务。
因此,没有定时器将意味着没有操作系统、没有网络、没有多媒体,这将是怎样的黑暗?所以,合理并灵活地使用各种定时器,是对一个软件人的最基本需求!在80186为主芯片的嵌入式系统中,我们需要借助硬件定时器的中断来作为软件定时器,在中断发生后变更画面的显示内容。在时间显示“xx:xx”中让冒号交替有无,每次秒中断发生后,需调用ShowDot: void ShowDot(){ static BOOL bShowDot = TRUE;
/* 再一次领略static关键字的威力 */ if(bShowDot)
{ showChar(‘:’,xPos,yPos);} else
{ showChar(‘ ’,xPos,yPos);
} bShowDot =!bShowDot;} 4.菜单操作
无数人为之绞尽脑汁的问题终于出现了,在这一节里,我们将看到,在C语言中哪怕用到一丁点的面向对象思想,软件结构将会有何等的改观!笔者曾经是个笨蛋,被菜单搞晕了,给出这样的一个系统: 图1 菜单范例
要求以键盘上的“←→”键切换菜单焦点,当用户在焦点处于某菜单时,若敲击键盘上的OK、CANCEL键则调用该焦点菜单对应之处理函数。我曾经傻傻地这样做着:
/* 按下OK键 */ void onOkKey(){ /* 判断在什么焦点菜单上按下Ok键,调用相应处理函数 */ Switch(currentFocus){ case MENU1:
menu1OnOk();
break;case MENU2:
menu2OnOk();
break;„ } } /* 按下Cancel键 */ void onCancelKey(){ /* 判断在什么焦点菜单上按下Cancel键,调用相应处理函数 */ Switch(currentFocus){ case MENU1:
menu1OnCancel();
break;case MENU2:
menu2OnCancel();
break;„ } } 终于有一天,我这样做了:
/* 将菜单的属性和操作“封装”在一起 */ typedef struct tagSysMenu
{
char *text;
/* 菜单的文本 */
BYTE xPos;/* 菜单在LCD上的x坐标 */
BYTE yPos;/* 菜单在LCD上的y坐标 */
void(*onOkFun)();
/* 在该菜单上按下ok键的处理函数指针 */
void(*onCancelFun)();/* 在该菜单上按下cancel键的处理函数指针 */ }SysMenu, *LPSysMenu;当我定义菜单时,只需要这样: static SysMenu menu[MENU_NUM] = {
{
“menu1”, 0, 48, menu1OnOk, menu1OnCancel
} ,{
“ menu2”, 7, 48, menu2OnOk, menu2OnCancel
} ,{
“ menu3”, 7, 48, menu3OnOk, menu3OnCancel
} ,{
“ menu4”, 7, 48, menu4OnOk, menu4OnCancel
}
… };OK键和CANCEL键的处理变成: /* 按下OK键 */ void onOkKey(){
menu[currentFocusMenu].onOkFun();
} /* 按下Cancel键 */ void onCancelKey(){ menu[currentFocusMenu].onCancelFun();
} 程序被大大简化了,也开始具有很好的可扩展性!我们仅仅利用了面向对象中的封装思想,就让程序结构清晰,其结果是几乎可以在无需修改程序的情况下在系统中添加更多的菜单,而系统的按键处理函数保持不变。面向对象,真神了!5.模拟MessageBox函数
MessageBox函数,这个Windows编程中的超级猛料,不知道是多少入门者第一次用到的函数。还记得我们第一次在Windows中利用MessageBox输出“Hello,World!”对话框时新奇的感觉吗?无法统计,这个世界上究竟有多少程序员学习Windows编程是从MessageBox(“Hello,World!”,„)开始的。在我本科的学校,广泛流传着一个词汇,叫做“‘Hello,World’级程序员”,意指入门级程序员,但似乎“‘Hello,World’级”这个说法更搞笑而形象。
图2 经典的Hello,World!图2给出了两种永恒经典的Hello,World对话框,一种只具有“确定”,一种则包含“确定”、“取消”。是的,MessageBox的确有,而且也应该有两类!这完全是由特定的应用需求决定的。
嵌入式系统中没有给我们提供MessageBox,但是鉴于其功能强大,我们需要模拟之,一个模拟的MessageBox函数为:
/****************************************** /*
函数名称:
MessageBox /*
功能说明:
弹出式对话框,显示提醒用户的信息 /*
参数说明:
lpStr---提醒用户的字符串输出信息
/*
TYPE---输出格式(ID_OK = 0, ID_OKCANCEL = 1)/*
返回值:
返回对话框接收的键值,只有两种 KEY_OK, KEY_CANCEL /****************************************** typedef enum TYPE
{ ID_OK,ID_OKCANCEL
}MSG_TYPE;extern
BYTE MessageBox(LPBYTE lpStr, BYTE TYPE){
BYTE keyValue =-1;
ClearScreen();
/* 清除屏幕 */
DisplayString(xPos,yPos,lpStr,TRUE);/* 显示字符串 */
/* 根据对话框类型决定是否显示确定、取消 */
switch(TYPE)
{
case
ID_OK:
DisplayString(13,yPos+High+1, “ 确定 ”, 0);
break;
case
ID_OKCANCEL:
DisplayString(8, yPos+High+1, “ 确定 ”, 0);
DisplayString(17,yPos+High+1, “ 取消 ”, 0);
break;
default:
break;
}
DrawRect(0, 0, 239, yPos+High+16+4);/* 绘制外框 */
/* MessageBox是模式对话框,阻塞运行,等待按键 */
while((keyValue!= KEY_OK)||(keyValue!= KEY_CANCEL))
{ keyValue = getSysKey();} /* 返回按键类型 */ if(keyValue== KEY_OK){ return ID_OK;} else { return ID_CANCEL;} } 上述函数与我们平素在VC++等中使用的MessageBox是何等的神似啊?实现这个函数,你会看到它在嵌入式系统中的妙用是无穷的。总结
本篇是本系列文章中技巧性最深的一篇,它提供了嵌入式系统屏幕显示方面一些很巧妙的处理方法,灵活使用它们,我们将不再被LCD上凌乱不堪的显示内容所困扰。
屏幕乃嵌入式系统生存之重要辅助,面目可憎之显示将另用户逃之夭夭。屏幕编程若处理不好,将是软件中最不系统、最混乱的部分,笔者曾深受其害。
C语言嵌入式系统编程修炼之道——键盘操作篇 1.处理功能键
功能键的问题在于,用户界面并非固定的,用户功能键的选择将使屏幕画面处于不同的显示状态下。例如,主画面如图1: 图1 主画面
当用户在设置XX上按下Enter键之后,画面就切换到了设置XX的界面,如图2:
图2 切换到设置XX画面
程序如何判断用户处于哪一画面,并在该画面的程序状态下调用对应的功能键处理函数,而且保证良好的结构,是一个值得思考的问题。
让我们来看看WIN32编程中用到的“窗口”概念,当消息(message)被发送给不同窗口的时候,该窗口的消息处理函数(是一个callback函数)最终被调用,而在该窗口的消息处理函数中,又根据消息的类型调用了该窗口中的对应处理函数。通过这种方式,WIN32有效的组织了不同的窗口,并处理不同窗口情况下的消息。
我们从中学习到的就是:
(1)将不同的画面类比为WIN32中不同的窗口,将窗口中的各种元素(菜单、按钮等)包含在窗口之中;
(2)给各个画面提供一个功能键“消息”处理函数,该函数接收按键信息为参数;
(3)在各画面的功能键“消息”处理函数中,判断按键类型和当前焦点元素,并调用对应元素的按键处理函数。
/* 将窗口元素、消息处理函数封装在窗口中 */ struct windows {
BYTE currentFocus;
ELEMENT element[ELEMENT_NUM];
void(*messageFun)(BYTE keyValue);
… };/* 消息处理函数 */ void messageFunction(BYTE keyValue){
BYTE i = 0;
/* 获得焦点元素 */
while((element [i].ID!= currentFocus)&&(i < ELEMENT_NUM))
{
i++;
}
/* “消息映射” */
if(i < ELEMENT_NUM)
{
switch(keyValue)
{
case OK:
element[i].OnOk();
break;
…
}
} } 在窗口的消息处理函数中调用相应元素按键函数的过程类似于“消息映射”,这是我们从WIN32编程中学习到的。编程到了一个境界,很多东西都是相通的了。其它地方的思想可以拿过来为我所用,是为编程中的“拿来主义”。
在这个例子中,如果我们还想玩得更大一点,我们可以借鉴MFC中处理MESSAGE_MAP的方法,我们也可以学习MFC定义几个精妙的宏来实现“消息映射”。2.处理数字键
用户输入数字时是一位一位输入的,每一位的输入都对应着屏幕上的一个显示位置(x坐标,y坐标)。此外,程序还需要记录该位置输入的值,所以有效组织用户数字输入的最佳方式是定义一个结构体,将坐标和数值捆绑在一起: /* 用户数字输入结构体 */ typedef struct tagInputNum
{
BYTE byNum;/* 接收用户输入赋值 */
BYTE xPos;
/* 数字输入在屏幕上的显示位置x坐标 */
BYTE yPos;
/* 数字输入在屏幕上的显示位置y坐标 */
}InputNum, *LPInputNum;那么接收用户输入就可以定义一个结构体数组,用数组中的各位组成一个完整的数字:
InputNum inputElement[NUM_LENGTH];/* 接收用户数字输入的数组 */ /* 数字按键处理函数 */ extern void onNumKey(BYTE num){
if(num==0|| num==1)/* 只接收二进制输入 */
{ /* 在屏幕上显示用户输入 */ DrawText(inputElement[currentElementInputPlace].xPos, inputElement[currentElementInputPlace].yPos, “%1d”, num);
/* 将输入赋值给数组元素 */
inputElement[currentElementInputPlace].byNum = num;
/* 焦点及光标右移 */
moveToRight();
} } 将数字每一位输入的坐标和输入值捆绑后,在数字键处理函数中就可以较有结构的组织程序,使程序显得很紧凑。3.整理用户输入
继续第2节的例子,在第2节的onNumKey函数中,只是获取了数字的每一位,因而我们需要将其转化为有效数据,譬如要转化为有效的XXX数据,其方法是:
/* 从2进制数据位转化为有效数据:XXX */ void convertToXXX(){
BYTE i;
XXX = 0;
for(i = 0;i < NUM_LENGTH;i++)
{
XXX += inputElement[i].byNum*power(2, NUM_LENGTH1);
}
} 反之,我们也可能需要在屏幕上显示那些有效的数据位,因为我们也需要能够反向转化:
/* 从有效数据转化为2进制数据位:XXX */ void convertFromXXX(){
BYTE i;
XXX = 0;
for(i = 0;i < NUM_LENGTH;i++)
{
inputElement[i].byNum = XXX / power(2, NUM_LENGTH1)% 2;
}
} 当然在上面的例子中,因为数据是2进制的,用power函数不是很好的选择,直接用“<< >>”移位操作效率更高,我们仅是为了说明问题的方便。试想,如果用户输入是十进制的,power函数或许是唯一的选择了。总结
本篇给出了键盘操作所涉及的各个方面:功能键处理、数字键处理及用户输入整理,基本上提供了一个全套的按键处理方案。对于功能键处理方法,将LCD屏幕与Windows窗口进行类比,提出了较新颖地解决屏幕、键盘繁杂交互问题的方案。
计算机学的许多知识都具有相通性,因而,不断追赶时髦技术而忽略基本功的做法是徒劳无意的。我们最多需要“精通”三种语言(精通,一个在如今的求职简历里泛滥成灾的词语),最佳拍档是汇编、C、C++(或JAVA),很显然,如果你“精通”了这三种语言,其它语言你应该是可以很快“熟悉”的,否则你就没有“精通”它们。
C语言嵌入式系统编程修炼之道——性能优化篇 1.使用宏定义
在C语言中,宏是产生内嵌代码的唯一方法。对于嵌入式系统而言,为了能达到性能要求,宏是一种很好的代替函数的方法。
写一个“标准”宏MIN,这个宏输入两个参数并返回较小的一个:
错误做法:
#define MIN(A,B)(A <= B ? A : B)正确做法:
#define MIN(A,B)((A)<=(B)?(A):(B))对于宏,我们需要知道三点:(1)宏定义“像”函数;
(2)宏定义不是函数,因而需要括上所有“参数”;(3)宏定义可能产生副作用。下面的代码:
least = MIN(*p++, b);将被替换为:
((*p++)<=(b)?(*p++):(b))发生的事情无法预料。
因而不要给宏定义传入有副作用的“参数”。2.使用寄存器变量
当对一个变量频繁被读写时,需要反复访问内存,从而花费大量的存取时间。为此,C语言提供了一种变量,即寄存器变量。这种变量存放在CPU的寄存器中,使用时,不需要访问内存,而直接从寄存器中读写,从而提高效率。寄存器变量的说明符是register。对于循环次数较多的循环控制变量及循环体内反复使用的变量均可定义为寄存器变量,而循环计数是应用寄存器变量的最好候选者。(1)
只有局部自动变量和形参才可以定义为寄存器变量。因为寄存器变量属于动态存储方式,凡需要采用静态存储方式的量都不能定义为寄存器变量,包括:模块间全局变量、模块内全局变量、局部static变量;
(2)
register是一个“建议”型关键字,意指程序建议该变量放在寄存器中,但最终该变量可能因为条件不满足并未成为寄存器变量,而是被放在了存储器中,但编译器中并不报错(在C++语言中有另一个“建议”型关键字:inline)。
下面是一个采用寄存器变量的例子: /* 求1+2+3+„.+n的值 */ WORD Addition(BYTE n){ register i,s=0;for(i=1;i<=n;i++){ s=s+i;} return s;} 本程序循环n次,i和s都被频繁使用,因此可定义为寄存器变量。3.内嵌汇编
程序中对时间要求苛刻的部分可以用内嵌汇编来重写,以带来速度上的显著提高。但是,开发和测试汇编代码是一件辛苦的工作,它将花费更长的时间,因而要慎重选择要用汇编的部分。
在程序中,存在一个80-20原则,即20%的程序消耗了80%的运行时间,因而我们要改进效率,最主要是考虑改进那20%的代码。
嵌入式C程序中主要使用在线汇编,即在C程序中直接插入_asm{ }内嵌汇编语句:
/* 把两个输入参数的值相加,结果存放到另外一个全局变量中 */ int result;
void Add(long a, long *b)
{
_asm
{
MOV
AX, a
MOV
BX, b
ADD
AX, [BX]
MOV
result, AX
}
}
4.利用硬件特性
首先要明白CPU对各种存储器的访问速度,基本上是:
CPU内部RAM > 外部同步RAM > 外部异步RAM > FLASH/ROM 对于程序代码,已经被烧录在FLASH或ROM中,我们可以让CPU直接从其中读取代码执行,但通常这不是一个好办法,我们最好在系统启动后将FLASH或ROM中的目标代码拷贝入RAM中后再执行以提高取指令速度; 对于UART等设备,其内部有一定容量的接收BUFFER,我们应尽量在BUFFER被占满后再向CPU提出中断。例如计算机终端在向目标机通过RS-232传递数据时,不宜设置UART只接收到一个BYTE就向CPU提中断,从而无谓浪费中断处理时间;
如果对某设备能采取DMA方式读取,就采用DMA读取,DMA读取方式在读取目标中包含的存储信息较大时效率较高,其数据传输的基本单位是块,而所传输的数据是从设备直接送入内存的(或者相反)。DMA方式较之中断驱动方式,减少了CPU 对外设的干预,进一步提高了CPU与外设的并行操作程度。5.活用位操作
使用C语言的位操作可以减少除法和取模的运算。在计算机程序中数据的位是可以操作的最小数据单位,理论上可以用“位运算”来完成所有的运算和操作,因而,灵活的位操作可以有效地提高程序运行的效率。举例如下: /* 方法1 */ int i,j;i = 879 / 16;j = 562 % 32;
/* 方法2 */ int i,j;i = 879 >> 4;j = 562-(562 >> 5 << 5);对于以2的指数次方为“*”、“/”或“%”因子的数学运算,转化为移位运算“<< >>”通常可以提高算法效率。因为乘除运算指令周期通常比移位运算大。
C语言位运算除了可以提高运算效率外,在嵌入式系统的编程中,它的另一个最典型的应用,而且十分广泛地正在被使用着的是位间的与(&)、或(|)、非(~)操作,这跟嵌入式系统的编程特点有很大关系。我们通常要对硬件寄存器进行位设置,譬如,我们通过将AM186ER型80186处理器的中断屏蔽控制寄存器的第低6位设置为0(开中断2),最通用的做法是: #define INT_I2_MASK
0x0040
wTemp = inword(INT_MASK);outword(INT_MASK, wTemp &~INT_I2_MASK);而将该位设置为1的做法是:
#define INT_I2_MASK
0x0040
wTemp = inword(INT_MASK);outword(INT_MASK, wTemp | INT_I2_MASK);判断该位是否为1的做法是:
#define INT_I2_MASK
0x0040
wTemp = inword(INT_MASK);if(wTemp & INT_I2_MASK){
„
/* 该位为1 */ } 上述方法在嵌入式系统的编程中是非常常见的,我们需要牢固掌握。总结
在性能优化方面永远注意80-20准备,不要优化程序中开销不大的那80%,这是劳而无功的。
宏定义是C语言中实现类似函数功能而又不具函数调用和返回开销的较好方法,但宏在本质上不是函数,因而要防止宏展开后出现不可预料的结果,对宏的定义和使用要慎而处之。很遗憾,标准C至今没有包括C++中inline函数的功能,inline函数兼具无调用开销和安全的优点。
使用寄存器变量、内嵌汇编和活用位操作也是提高程序效率的有效方法。除了编程上的技巧外,为提高系统的运行效率,我们通常也需要最大可能地利用各种硬件设备自身的特点来减小其运转开销,例如减小中断次数、利用DMA传输方式等。
第三篇:嵌入式C语言编程心得
一、.H文件与.C文件的关系:
迄今为止,写过的程序都是一些很简单的程序,从来没有想到要自己写.H文件,也不知道.H文件到底什么用,与.C文件什么关系。只是最近写键盘程序,参考别人的程序时,发现别人写的严格的程序都带有一个“KEY.H”,里面定义了.C文件里用到的自己写的函数,如Keyhit()、Keyscan()等。
经过查找资料得知,.H文件就是头文件,估计就是Head的意思吧,这是规范程序结构化设计的需要,既可以实现大型程序的模块化,又可以实现根各模块的连接调试。
1、.H文件介绍:
在单片机C程序设计中,项目一般按功能模块化进行结构化设计。将一个项目划分为多个功能,每个功能的相关程序放在一个C程序文档中,称之为一个模块,对应的文件名即为模块名。一个模块通常由两个文档组成,一个为头文件*.h,对模块中的数据结构和函数原型进行描述;另一个则为C文件*.c,对数据实例或对象定义,以及函数算法具体实现。
2、.H文件的作用
作为项目设计,除了对项目总体功能进行详细描述外,就是对每个模块进行详细定义,也就是给出所有模块的头文件。通常H头文件要定义模块中各函数的功能,以及输入和输出参数的要求。模块的具体实现,由项目组成根据H文件进行设计、编程、调试完成。为了保密和安全,模块实现后以可连接文件OBJ、或库文件LIB的方式提供给项目其他成员使用。由于不用提供源程序文档,一方面可以公开发行,保证开发人员的所有权;另一方面可以防止别人有意或无意修改产生非一致性,造成版本混乱。所以H头文件是项目的详细设计和团队工作划分的依据,也是对模块进行测试的功能说明。要引用模块内的数据或算法,只要用包含include指定模块H头文件即可。
3、.H文件的基本组成 /*如下为键盘驱动的头文档*/ #ifndef _KEY_H_ //防重复引用,如果没有定义过_KEY_H_,则编译下句 #define _KEY_H_ //此符号唯一,表示只要引用过一次,即#i nclude,则定义符号_KEY_H_ ///////////////////////////////////////////////////////////////// char keyhit(void);//击键否
unsigned char Keyscan(void);//取键值
///////////////////////////////////////////////////////////////// #endif.c文件是整个程序中的一个或几个函数组成,在别的.C文件里可以调用它,不只是在主函数中。这样做可以增强程序的模块化,提高程序的可读性。当编制好一个模块时你可以保存在一个工程下,文件名改为**.C。这样在另一个.C文件中的文件头处只要你对它进行说明就可以调用它。比如#include string.c 那么你就可以在你说明了的.C文件中调用它,这样一个模块可以在很多处调用,使得编程复杂度降低。程序编译时把你所说明的文件复制到你调用处程序就可以运行了。
.H文件是对单片机一些端口及一些常用的程序的库说明,比如我们在程序中用到端口一即P1,我们能够用它是因为在库中已对它进行了说明,对P1赋予了一个端口地址,这样程序编译是才不会出错,不然会提示你UNDIFINED。
分开多个文件写就有很多的.c和.h文件了,当然要加上#include语句把其它的包含进来,分开多个文件写有利于管理,其实我觉得.c和.h区别不是很大,只是.h一般都是写定义、声明的东西,.c文件一般都写函数的具体实现
h文件就是头文件,一般进行声明、宏定义等。比如要编写流水灯的程序,h文件这样写: #ifndef _LED_H #define _LED_H define LED_ON P1.1=1;define LED_OFF P1.1=0;//还可以进行函数的声明 //void LED_twikle(void);#endif
在你的c文件中要加入#include“LED.h”
二、尽量使用宏定义#define
开始看别人的程序时,发现程序开头,在文件包含后面有很多#define语句,当时就想,搞这么多标示符替换来替换去的,麻不麻烦啊,完全没有理解这种写法的好处。原来,用一个标示符表示常数,有利于以后的修改和维护,修改时只要在程序开头改一下,程序中所有用到的地方就全部修改,节省时间。
#define KEYNUM 65//按键数量,用于Keycode[KEYNUM] #define LINENUM 8//键盘行数 #define ROWNUM 8//键盘列数 注意的地方:
1、宏名一般用大写
2、宏定义不是C语句,结尾不加分号
三、不要乱定义变量类型
以前写程序,当需要一个新的变量时,不管函数内还是函数外的,直接在程序开头定义,虽然不是原则上的错误,但是很不可取的作法。
下面说一下,C语言中变量类型的有关概念: 从变量的作用范围来分,分为局部变量和全局变量:
1、全局变量:是在函数外定义的变量,像我以前定义在程序开头的变量都是全局变量,这里我就犯了一个大忌,使用了过多的全局变量。
带来的问题有两个:一是,全局变量在程序全部执行过程中都占用资源;二是,全局变量过多使程序的通用性变差,因为全局变量是模块间耦合的原因之一。
2、局部变量:在函数内部定义的变量,只在函数内部有效。从变量的变量值存在的时间分为两种:
1、静态存储变量:程序运行期间分配固定的存储空间。
2、动态存储变量:程序运行期间根据需要动态地分配存储空间。具体又包括四种存储方式:auto, static ,register, extern
1、局部变量,不加说明默认为auto型,即动态存储,如果不赋初值,将是一个不确定的值。而将局部变量定义为static型的话,则它的值在函数内是不变的,且初值默认为0。
static unsigned char sts;//按键状态变量 static unsigned char Nowkeycode;//此时的键码 static unsigned char Prekeycode;//上一次的键码
static unsigned char Keydowntime;//矩形键盘按下去抖时间变量 static unsigned char Keyuptime;//矩形键盘释放去抖时间变量 static unsigned char Onoffdowntime;//关机键按下去抖时间变量 static unsigned char Onoffuptime;//关机键释放去抖时间变量
static unsigned char onoff_10ms;//判断关机键中断次数变量,累计150次大约为3S,因为前后进了两个10ms中断
2、全局变量,编译时分配为静态存储区,可以被本文件中的各个函数引用。如果是多个文件的话,如果在一个文件中引用另外文件中的变量,在此文件中要用extern声明。不过如果一个全局变量定义为static的话,就只能在此一个文件中使用。
static 函数内部函数和外部函数
当一个源程序由多个源文件组成时,C语言根据函数能否被其它源文件中的函数调用,将函数分为内部函数和外部函数。
内部函数(又称静态函数)
如果在一个源文件中定义的函数,只能被本文件中的函数调用,而不能被同一程序其它文件中的函数调用,这种函数称为内部函数。
定义一个内部函数,只需在函数类型前再加一个“static”关键字即可,如下所示:
static 函数类型 函数名(函数参数表){„„}
关键字“static”,译成中文就是“静态的”,所以内部函数又称静态函数。但此处“static”的含义不是指存储方式,而是指对函数的作用域仅局限于本文件。
使用内部函数的好处是:不同的人编写不同的函数时,不用担心自己定义的函数,是否会与其它文件中的函数同名,因为同名也没有关系。外部函数
外部函数的定义:在定义函数时,如果没有加关键字“static”,或冠以关键字“extern”,表示此函数是外部函数:
[extern] 函数类型 函数名(函数参数表){„„} 调用外部函数时,需要对其进行说明:
[extern] 函数类型 函数名(参数类型表)[,函数名2(参数类型表2)„„];
四、特殊关键字const volatile的使用
1、const const用于声明一个只读的变量
const unsigned char a=1;//定义a=1,编译器不允许修改a的值 作用:保护不希望被修改的参数 const unsigned char Key_code[KEYNUM]={0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08, 0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10, 0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18, 0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F,0x20, 0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28, 0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0x30, 0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38, 0x39,0x3A,0x3B,0x3C,0x3D,0x3E,0x3F,0x40, 0x41 };//键码
const unsigned char Line_out[LINENUM]={0xFE,0xFD,0xFB,0xf7,0xEF,0xDF,0xBF,0x7F};//行输出编码 const unsigned char Row_in[ROWNUM]={0xFE,0xFD,0xFB,0xf7,0xEF,0xDF,0xBF,0x7F};//列输入编码
2、volatile 一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
static int i=0;int main(void){...while(1){ if(i)dosomething();} } /* Interrupt service routine.*/ void ISR_2(void){ i=1;} 程序的本意是希望ISR_2中断产生时,在main当中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远也不会被调用。如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。一般说来,volatile用在如下的几个地方:
1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
2、多任务环境下各任务间共享的标志应该加volatile;
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
五、C语言中extern的用法
extern可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。
另外,extern也可用来进行链接指定。
extern 变量
在一个源文件里定义了一个数组:char a[6];在另外一个文件里用下列语句进行了声明:extern char *a; 请问,这样可以吗? 答案与分析: 1)、不可以,程序运行时会告诉你非法访问。原因在于,指向类型T的指针并不等价于类型T的数组。extern char *a声明的是一个指针变量而不是字符数组,因此与实际的定义不同,从而造成运行时非法访问。应该将声明改为extern char a[ ]。2)、例子分析如下,如果a[] = “abcd”,则外部变量a=0x12345678(数组的起始地址),而*a是重新定义了一个指针变量a的地址可能是0x87654321,直接使用*a是错误的.3)、这提示我们,在使用extern时候要严格对应声明时的格式,在实际编程中,这样的错误屡见不鲜。4)、extern用在变量声明中常常有这样一个作用,你在*.c文件中声明了一个全局的变量,这个全局的变量如果要被引用,就放在*.h中并用extern来声明。
函数
extern 函数1 常常见extern放在函数的前面成为函数声明的一部分,那么,C语言的关键字extern在函数的声明中起什么作用? 答案与分析: 如果函数的声明中带有关键字extern,仅仅是暗示这个函数可能在别的源文件里定义,没有其它作用。即下述两个函数声明没有明显的区别: extern int f();和int f();当然,这样的用处还是有的,就是在程序中取代include “*.h”来声明函数,在一些复杂的项目中,我比较习惯在所有的函数声明前添加extern修饰。extern 函数2 当函数提供方单方面修改函数原型时,如果使用方不知情继续沿用原来的extern申明,这样编译时编译器不会报错。但是在运行过程中,因为少了或者多了输入参数,往往会照成系统错误,这种情况应该如何解决? 答案与分析: 目前业界针对这种情况的处理没有一个很完美的方案,通常的做法是提供方在自己的xxx_pub.h中提供对外部接口的声明,然后调用方include该头文件,从而省去extern这一步。以避免这种错误。宝剑有双锋,对extern的应用,不同的场合应该选择不同的做法。extern “C” 在C++环境下使用C函数的时候,常常会出现编译器无法找到obj模块中的C函数定义,从而导致链接失败的情况,应该如何解决这种情况呢? 答案与分析: C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的情况,此时C函数就需要用extern “C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名。下面是一个标准的写法: //在.h文件的头上 #ifdef __cplusplus #if __cplusplus extern “C”{ #endif #endif /* __cplusplus */ „ „ //.h文件结束的地方 #ifdef __cplusplus #if __cplusplus } #endif #endif /* __cplusplus */
六、c++中-> 是什么意思,如何使用
->是指针的指向运算符,通常与结构体一起使用。
具体使用方法可以参考如下程序: #include
七、warning: last line of file ends without a newline 怎么解决?
原因:编译器要求有空行作为程序的结束
解决办法:将光标移到提示告警的代码最后一行(有代码的那一行)然后按住del键,直到确定下面没有回车行,最后回车一下或多下即可。
八、__I、__O、__IO volatile是什么?怎么用?
__I、__O、__IO是什么意思?
这是ST库里面的宏定义,定义如下:
#define __I volatile const /*!< defines 'read only' permissions */ #define __O volatile /*!< defines 'write only' permissions */ #define __IO volatile /*!< defines 'read / write' permissions */
显然,这三个宏定义都是用来替换成 volatile 和 const 的,所以我们先要了解 这两个关键字的作用:
volatile
简单的说,就是不让编译器进行优化,即每次读取或者修改值的时候,都必须重新从内存或者寄存器中读取或者修改。
一般说来,volatile用在如下的几个地方:
1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
2、多任务环境下各任务间共享的标志应该加volatile;
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,所有这些都要求用到 volatile变量。不懂得volatile的内容将会带来灾难。假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。
1)一个参数既可以是const还可以是volatile吗?解释为什么。
2);一个指针可以是volatile 吗?解释为什么。
3);下面的函数有什么错误:
int square(volatile int *ptr){
return *ptr * *ptr;}
1)是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
2);是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。
3)这段代码有点变态。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:
int square(volatile int *ptr){ int a,b;a = *ptr;b = *ptr;return a * b;}
由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
long square(volatile int *ptr){ int a;a = *ptr;return a * a;}
const 只读变量,即变量保存在只读静态存储区。编译时,如何尝试修改只读变量,则编译器提示出错,就能防止误修改。
const与define
两者都可以用来定义常量,但是const定义时,定义了常量的类型,所以更精确一些(其实const定义的是只读变量,而不是常量)。#define只是简单的文本替换,除了可以定义常量外,还可以用来定义一些简单的函数,有点类似内置函数。const和define定义的常量可以放在头文件里面。(小注:可以多次声明,但只能定义一次)
const与指针
int me;
const int * p1=&me;//p1可变,*p1不可变 const 修饰的是 *p1,即*p1不可变
int * const p2=&me;//p2不可变,*p2可变 const 修饰的是 p2,即p2不可变
const int *const p3=&me;//p3不可变,*p3也不可变 前一个const 修饰的是 *p3,后一个const 修饰的是p3,两者都不可变
前面介绍了 volatile 和 const 的用法,不知道大家了解了没?了解了后,下面的讲解就更加容易了:
__I :输入口。既然是输入,那么寄存器的值就随时会外部修改,那就不能进行优化,每次都要重新从寄存器中读取。也不能写,即只读,不然就不是输入而是输出了。
__O :输出口,也不能进行优化,不然你连续两次输出相同值,编译器认为没改变,就忽略了后面那一次输出,假如外部在两次输出中间修改了值,那就影响输出
__IO:输入输出口,同上
为什么加下划线?
原因是:避免命名冲突
一般宏定义都是大写,但因为这里的字母比较少,所以再添加下划线来区分。这样一般都可以避免命名冲突问题,因为很少人这样命名,这样命名的人肯定知道这些是有什么用的。
经常写大工程时,都会发现老是命名冲突,要不是全局变量冲突,要不就是宏定义冲突,所以我们要尽量避免这些问题,不然出问题了都不知道问题在哪里。
九、C语言中关于枚举类型
1.enum 枚举的定义
枚举类型定义的一般形式为: enum 枚举名{ 枚举值表 };在枚举值表中应罗列出所有可用值。这些值也称为枚举元素。例如:
该枚举名为weekday,枚举值共有7个,即一周中的七天。凡被说明为weekday类型变量的取值只能是七天中的某一天。
2.枚举变量的说明
如同结构和联合一样,枚举变量也可用不同的方式说明,即先定义后说明,同时定义说明或直接说明。
设有变量a,b,c被说明为上述的weekday,可采用下述任一种方式: enum weekday{ sun,mou,tue,wed,thu,fri,sat };enum weekday a,b,c;或者为:
enum weekday{ sun,mou,tue,wed,thu,fri,sat }a,b,c;或者为:
enum { sun,mou,tue,wed,thu,fri,sat }a,b,c;
3、枚举类型变量的赋值和使用 枚举类型在使用中有以下规定:
a.枚举值是常量,不是变量。不能在程序中用赋值语句再对它赋值。例如对枚举weekday的元素再作以下赋值: sun=5;mon=2;sun=mon;都是错误的。
b.枚举元素本身由系统定义了一个表示序号的数值,从0开始顺序定义为0,1,2…。如在weekday中,sun值为0,mon值为1,…,sat值为6。int main(){ enum weekday { sun,mon,tue,wed,thu,fri,sat } a,b,c;a=sun;b=mon;c=tue;printf(“%d,%d,%d”,a,b,c);return 0;} 说明:
只能把枚举值赋予枚举变量,不能把元素的数值直接赋予枚举变量。如: a=sum;b=mon;是正确的。而: a=0;b=1;是错误的。如一定要把数值赋予枚举变量,则必须用强制类型转换。如:a=(enum weekday)2;其意义是将顺序号为2的枚举元素赋予枚举变量a,相当于: a=tue;还应该说明的是枚举元素不是字符常量也不是字符串常量,使用时不要加单、双引号。int main(){ enum body { a,b,c,d } month[31],j;int i;j=a;for(i=1;i<=30;i++){ month[i]=j;j++;if(j>d)j=a;} for(i=1;i<=30;i++){ switch(month[i]){ case a:printf(“ %2d %ct”,i,'a');break;case b:printf(“ %2d %ct”,i,'b');break;case c:printf(“ %2d %ct”,i,'c');break;case d:printf(“ %2d %ct”,i,'d');break;default:break;} } printf(“n”);return 0;}
第四篇:7年经验总结,C语言嵌入式系统_编程规范_编程思想
嵌入式系统编程规范
李红志
程序的可读性、可扩展性、可复用性、易维护性、语法是代码的入门,算法是代码的灵魂。
第1章 编程常见错误
1.1、语法错误
1、错用函数数据类型,比如abs(x),x可能为16bit的值,如果为16bit的值,给出32bit的值就会出错。
2、内存越界访问
内存越界访问有两种:一种是读越界,即读了不属于自己的数据,如果所读的内存地址是无效的,程度立刻就崩溃了。如果所读内存地址是有效的,在读的时 候不会出问题,但由于读到的数据是随机的,它会产生不可预料的后果。另外一种是写越界,又叫缓冲区溢出,所写入的数据对别人来说是随机的,它也会产生不可 预料的后果。
3、结构的成员顺序变化引发的错误
在初始化一个结构时,老手可能很少像新手那样老老实实的,一个成员一个成员的为结构初始化,而是采用快捷方式,如:
Struct s { int l;char* p;};int main(int argc, char* argv[]){ struct s s1 = {4, “abcd”};return 0;} 以上这种方式是非常危险的,原因在于你对结构的内存布局作了假设。如果这个结构是第三方提供的,他很可能调整结构中成员的相对位置。而这样的调整往 往不会在文档中说明,你自然很少去关注。如果调整的两个成员具有相同数据类型,编译时不会有任何警告,而程序的逻辑可能相距十万八千里了。
4、栈溢出。
我们在前面关于堆栈的一节讲过,在PC上,普通线程的栈空间也有十几M,通常够用了,定义大一点的临时变量不会有什么问题。
而在一些嵌入式中,线程的栈空间可能只5K大小,甚至小到只有256个字节。在这样的平台中,栈溢出是最常用的错误之一。
1.2、编译错误 1.3、链接错误
第2章 编程知识
关键字valotile的作用是告诉编译器,不要把变量优化到寄存器里。
第3章 编程规范
1.1 整体结构
1、必须包含的两个文件:
“#include “std_inc.h”” “#include “std_defs.h””
2、一个完整的project需要有程序说明文档
3、需要有变量宏定义函数说明文档,包含变量规则命名。
4、需要有程序流程图
5、需要有硬件测试报告
6、需要有程序修改记录
7、要有软件时间控制分析
1.2 编程规范
1、定义宏定义按照功能模块来区分;
2、枚举型定义当宏定义来处理;
3、程序和数据要分开;
4、格式上要对齐;
5、空行要规范;
6、中断中调用的变量,一定要分析在计算过程中别的地方赋值是不是有非本身意义的赋值;
7、变量的意义要清晰;
8、程序要分层设计; 9、1.3 注释规范
1、变量和宏定义都要在定义的时候注释一下,作用是什么,单位,放大倍数。
2、用“#”标记需要问别人、需要改进的地方。
3、用“$”标记如果硬件改变需要进行变化的地方。
4、用“// XX”。
5、每个函数上面都要写注释;
6、程序段内不要太多的注释,多的话影响程序的可读性;
1.4 变量命名
1、变量名用小写
2、宏定义用大写
1.5 不建议使用全局变量的原因:
(1)全局变量在程序的全部执行过程中都占用存储单元,而不是仅在需要时才开辟单元;
(2)它使函数的通用性降低了,因为函数在执行时依赖于其所在的外部变量。如果将一个函数移到另一个文件中,还要将有关的外部变量及其值一起移过去。但若该外部变量与其他文件 中的变量同名时,就会出现问题,降低了程序的可靠性和通用性。在程序设计中,在划分模块时就要求模块的“内聚性”强、与其他模块的“耦合性”弱。即模块的功能要单一(不要把许多互不相干的功能放到一个模块中),与其他模块的相互影响要尽量少,而使用全局变量是不符合这个原则的。一般要求把C程序中的函数做成一个封闭体,除了可以通过“实参——形参”的渠道与外界发生联系外,没有其他渠道。这样的程序移植性好,可读性强。
(3)使用全局变量过多,会降低程序的清晰性,人们往往难以清楚地判断出每个瞬时各个外部变量的值。在各个函数执行时都可能改变外部变量的值,程序容易出错。因此要限制使用全局变量。
(4)如果在同一个源文件中,外部变量与局部变量同名,则在局部变量的作用范围内,外部变量被“屏蔽”,即它不起作用。
说明:使用全局变量的作用是增加了函数间的数据联系的渠道。由于函数的调用只能带回一个返回值,因此有时可以利用全局变量增加与函数联系的渠道,从而到到一个以上的返回值。
第4章 2011-4-22:程序的矩阵化设计思想
适用于嵌入式软件设计,无操作系统,软件结构较复杂的情况。
1.6 定义
最小时延原则:软件设计过程中,在不影响其他性能的情况下,应该让数据的产生过程和使用过程之间的时延最小。
需控变量:软件运行过程中,需要控制计算顺序的全局变量。
非需控变量:软件运行过程中,不需要控制计算顺序的全局变量,比如从总线引发的中断中获取的信号。
优化矩阵:在设计函数执行顺序时,用于记录函数和函数输出变量的矩阵。1.7 软件结构的矩阵化
程序上的分层设计。层与层之间有接口。矩阵化设计。全局变量的作用范围要有设定,不能从上到下都是一种变量。分层设计后,才比较容易处理指令冲突的问题,因为指令被执行之前会有对几种指令进行判断的操作。模块化是矩阵化设计的基础,模块作为矩阵的cell,同一个层面的作为一个row,程序执行一次,就是从顶层到底层一次,只不过每次可能走的trace不同。这个trace就是程序真正运行了哪些模块。
程序应该是从模块化、发展到分层、再到矩阵。
环境识别、驾驶意图识别、干预退出预估、1.8 函数执行顺序的矩阵化
适用情况:系统信号较多,全局变量较多,在一个控制周期内,相互之间有计算先后要求。同一层次的函数较多。全局变量只在一个函数中被赋值,在多处被调用。
全局变量分为需控变量和非需控变量。
该方法是对某一层的函数进行执行顺序的设计 1.对每一个函数进行编号;
2.建立需控变量集:将每一个函数的输入全局变量和输出全局变量中的需控变量放进需控变量集。3.对需控变量集中的每个元素编号;
4.建立每个函数的输入需控变量集和输出需控变量集,变量集用需控变量集中元素编号表示;
5.逐个将函数添加至优化矩阵,每添加一个函数,调整优化矩阵,直到所有函数添加完成。得到每个函数的可存放域。
6.根据最小时延原则,调整每个函数到最优位置。7.输出可行的函数序列。第5章 编程经验
1.9 程序设计思想 控制时序的设计思想: 程序分层的设计思想: 程序的矩阵式设计思想; 变量的集中处理思想; 变量自衰减的处理思想;
估算变量的自衰减。估算变量时,由于只有满足估算条件才能进行估算,而估算条件不是持续成立的,所以只能在某些点进行估算。没有进行计算的地方,该变量就要随时间衰减,并且要给出一个health指标,表明这个被估算值的可信度。
干预退出预估的处理思想;
1.10 【2010-12-5】
1、能从CAN上获取的信号要从CAN上获取,比如发动机转速、发动机输出转矩、加速踏板位置(不知道还有用没了)、节气门开度(不知道还有用没了)、传动比(或者说档位)。
2、我觉得用115200,10ms传出100个byte没什么问题。这样的话,B+S的采集主要作用就在于同步一下压力,方向盘、横摆等信号用更高的频率采集,确认一下单片机的处理是否达到精度。
3、变量命名的规范化。全局变量中的temp,写成s32temp1,局部变量的temp,写成temp1s32.感觉不怎么好,但是总要区别一下的。
4、abs(x);的函数原型是int abs(int x)
5、尽量避免一个变量在不同程序段被幅值;
6、中断中用到的量,要小心在程序外会不会有非本身含义的短时间赋值;
7、一段代码尽量不要超过100行;
8、全局附着比局部复杂要复杂;
9、任何一段代码,要做到能用1句话描述;
10、【2011-7-4】
代码变成各种意义明确的节的优点:
1、高可靠性:每段很小,就更容易做到确认代码不会出问题,更能确认该段指令是千锤百炼的,绝对可靠的;
2、可维护性强:由于每段很小,很容易看懂,调试及修改都更方便;
3、可扩展性强:如果某一种计算有问题了,只用替换或修改某一小段,而不用到处找需要改哪些地方;
4、可复用性强:对某些小段,可能多个地方都可以用到,不用在不同的地方写很多次;
5、结构清晰:每一小段意义明确,程序结构、层次、调用关系、数据流等都更清晰;
6、可读性强:由于结构清晰,增加了可读性,另外结构清晰了,就容易写清楚注释,也增加了可读性;
7、函数无条件执行的优点:
1、结构更清晰:不用考虑是否执行了;
2、数据流更清晰:一个新的计算量由哪些变量得到,传递关系更清晰;
3、函数的模块化更好:能让函数无条件执行,表示该函数具有更高的独立性,也就是模块更完整,与其他代码的耦合程度更低,迁移更方便;
4、程序清晰之后,程序结构简单,从而增加可靠性;
5、集中处理的优点:(集中处理是只,如果一个变量的计算,在不同条件下,算法不同,那么把条件汇总到一起,在一段代码处处理这个变量)
1、结构清晰:一个变量的计算只出现在一个位置;
2、分散处理会造成同一个值在一次运算周期重复计算,后面的计算结果覆盖前面的计算结果,从而难以控制数据流;
3、【2011-7-5】
1、ESP方式的减压和泄压:开关控制方式下,两者没什么区别;
2、ABS方式的减压:减压速率和ESP减压方式差别很大;远小于ESP泄压速率,近似1/2关系;
3、Bosch 8.1的HCU,保压时有个大约90Hz的噪声,和王伟玮讨论了一下,基本上可以理解为那就是当时电机转动频率;
【2011-7-6】
1、在举升机上,两前轮确实会有转速差,并且转速差可能越来越大;
2、阀的端口,烧程序时的状态确认;没有高电平的;
3、粗略来讲,泄压速率正比于轮缸压力?
4、轮缸制动液净进入量和轮缸压力什么关系?在常用的范围内,大概是正比关系;
5、制动轮缸制动液液量增大了,谁变形了?制动盘、摩擦块、制动钳,还有谁?那个变形大?是不是制动钳>摩擦块>制动盘(最大也就0.1mm),6、主动增压时,进油速率基本恒定。HCU中的柱塞泵效率大概50%,好点儿能到60%;轮缸压力大,效率可能稍微高一点儿;轮缸压力大,电机转速稍微低一点儿。
7、【2011-7-8】
1、大型的复杂的程序,用面向对象编程效率要高得多;单就变量的private和public设定而言,就会减少很多变量赋值的误操作,相当于自动添加了一种自检验机制;其他还有很多优点。
2、应该把每个函数都做一个更新记录。这种更新记录怎么能做到比较容易看到上次的状态呢?难道只能把不同状 态都记录下来看的时候再对比?
3、【2011-7-9】
1、轴距和FMVSS126的A值之间的关系,基本上只轴距减小10%,A值减小3%(尊驰在CarSim中的仿真结果)。2DOF理论上应该成比例改变的,实际上可能悬架、轮胎的柔性有影响。
2、尊驰和C118在CarSim中仿真的A值大概都是31deg。
3、.c中的函数,必须在.h中声明,不是为了能被调用,还是为了比较直观地看到在这个.c文件中定义了哪些函数;
4、单片机上,除0,会怎样?
经测试,初步结论:正值/0 = 0xF*F,负值/0 = 1
5、坡道TCS之所以困难,就在于压力估算的偏差影响对坡度的识别,压力的持续控制也比较困难;
6、坡道对前轴载荷几乎没影响。cos(15deg)= 0.985
【2011-7-13】
1、任何一个条件执行的函数,都涉及到不满足条件时不执行了,它计算的变量清零,还是怎么处理。条件选择,都是选取一种处理方法,那么必须对任何一种处理都有对应的方法。
2、条件运算的函数改变的变量,函数不运行时也需要对变量进行赋值;函数在程序中的作用,就是为了给那几个变量赋值,不运行了,就要用其他方式赋值;
3、先分好层次,定义清楚变量和函数,画好框图再写程序;
4、定义文件之间的接口,函数之间的接口;
【2011-7-29】
1、数据后处理中用数据组合成控制信号,是不靠谱的。组合生成一些信号比较靠谱。
【2011-8-12】
1、程序要写到什么样才算好?多一个字则太多,少一个字则太少,天衣无缝,完美无暇,千锤百炼,炉火纯青。
2、一群人,怎么才能做好一个程序?首先定义清晰,规则明确,不能越俎代庖,每个人有不同的权限去维护不同的代码。每个人都要遵守其中的规则,不守规则的那个人就是系统的bug。
3、虽然说程序是调出来的,但是还是应该尽量写的时候就写完善,不能写得一塌糊涂而等调试去解决问题。写的时候,关注的只是那么几十行,而找bug的时候,关注的可是几千行,这效率能一样么?
4、大程序设计首要原则,降低各部分耦合度。
5、如果每个人负责几个文件的话,就不要轻易定义全局变量了,要尽量使用静态全局变量(作用域为本文件)。定义全局变量要先看全局变量库,并建立并进行登记。
6、以前程序很大的一个问题就是层次没分清,从而耦合度高,混乱。
7、格式上,所有的一块儿逻辑前面要有空行,比如if之类的。
8、定义一个新变量前,先检查一下是否已存在这个变量名。
9、函数中间尽量不用return,结构混乱。
10、减少调用层次;
11、程序的本质是什么?数据流;
12、函数的作用是什么?计算数据;
13、写程序的本质是什么?用矩阵化方式决定数据流的运行;
14、为什么要分层?降低耦合度;
【2012-2-5】
1、把OBJ中的一段,ID中的一段儿留给调试。也是分层的概念。
2、
第五篇:C语言编程
#include(stdio.h)
main()
{ int question[4]={-1,-1,-1,-1},i=0,j=0,k=0,A=0,B=0,answer[4]={0};
char again='y';
while(again=='y'){ srand((int)time(0));
while(i4){ k=(int)rand()%10;
for(j=0;ji;j++)if(k==question[j]){ k=-1;break;}
if(k==-1)continue;question[i]=k;i++;}/*while i*/
for(i=8;i0;i--)/*还有8次机会*/
{ A=0;B=0;printf(“n你还剩下%d次机会。”,i);
printf(“n请输入四个0-9之间的数字,中间用空格隔开n”);for(j=0;j4;j++)scanf(“%d”,&answer[j]);
for(j=0;j4;j++)
for(k=0;k4;k++)
{ if(answer[j]==question[k]){ if(j==k)A++;else B++;} }/*for*/
if(A==4){ again='0';
printf(“n你赢了,还继续猜吗?(y/n)”);
while(again!='y'&&again!='n')
scanf(“...%c”,&again);break;}/*if*/
printf(“n%dA%dB”,A,B);if(i==1){ again='0';
printf(“n你输了,正确答案是”);
for(j=0;j4;j++)
printf(“%dt”,question[j]);
printf(“n还继续吗?(y/n)”);
while(again!='y'&&again!='n')scanf(“%c”,&again);
printf(“%c”,again);break;}/*if*/ }/*for changce*/ }/*while again*/ printf(“感谢您玩这个游戏。”);}