• 关于STM32的几种输入模式以及外部中断的配置

    最近做毕业设计,需要用按键来触发外部中断。实验的时候是正常的,但是换了个核心板以及用上自己做的PCB电路板后,出现了一些问题。问题如下: 要求:将连接按键的IO口配置为上拉输入,按键一端接IO口,一端接地,即当按键按下后,该IO口会产生一个下降沿,触发下降沿中断。 问题:将相应的IO口配置好后,测了引脚的的电压,并不是3.3V左右,而是0.1V左右。于是猜想: 1. 外围电路对IO口产生了影响。 2. STM32内部上拉能力较弱,一次只能上拉一个IO口。 于是开始从这两个想法着手解决。首先第一个,很容易就排除了。将外围电路撤掉,我用的是杜邦线,直接拔掉测量引脚上的电压,依然是0.1V左右,于是第1个猜想排除。 第二个,查看万能的参考手册,发现每一个IO口都是有独立的驱动电路,这样第2个也排除了。 最后各种纠结,然后发现:在主函数的开头部分初始化的时候,我把初始化的那个函数给注释掉了。低级错误,见笑了。 现在总结一下: STM32的输入有4种输入模式: 模拟输入 GPIO_AIN 用于AD转换 浮空输入 GPIO_IN_FLOATING 引脚处于浮空模式,电平状态是不确定的。外部信号输入什么,IO口就是什么状态。 上拉输入 GPIO_IPU 防止IO口出现不确定的状态,比如,当IO口悬空时,就会通过内部的上拉电阻将该点钳位在高电平。 下拉输入 GPIO_IPD 功能与上拉电阻类似,防止IO口出现不确定的状态,比如,当IO口悬空时,就会通过内部的下拉电阻将该点钳位在低电平。 STM32中空的I/O管脚是高电平还是低电平取决于具体情况。 1、IO端口复位后处于浮空状态,也就是其电平状态由外围电路决定。 2、STM32上电复位瞬间I/O口的电平状态默认是浮空输入,因此是高阻。做到低功耗。 3、STM32的IO管脚配置口默认为浮空输入,把选择权留给用户,这是一个很大的优势:一方面浮空输入确保不会出现用户不希望的默认电平(此时电平取决于用户的外围电路);另一方面降低了功耗,因为不管是上拉还是下拉都会有电流消耗。从另一个角度来看,不管I/O管脚的默认配置如何,还是需要在输出的管脚外加上拉或下拉,这是为了保证芯片上电期间和复位时输出的管脚始终处于已知的电平。 4、在没有任何操作的情况下,STM32通用推挽输出模式的引脚默认低电平,也就是有电的状态。所以在配置的时候通常会先把引脚的电平设置拉高,让电路不产生电流。有电到没电这一过程也就是引脚电平从低到高的过程。 5、STM32的I/O管脚有两种:TTL和CMOS,所有管脚都兼容TTL和CMOS电平。也就是说从输入识别电压上看,所有管脚不管是TTL管脚还是CMOS管脚都可以识别TTL或CMOS电平。

    时间:2018-07-17 关键词: STM32

  • 电子技术的发展简史

     电子技术和电子学,是与电子有关的理论与技术。现在,人们已经掌握了大量的电子技术方面的知识,而且电子技术还在不断地发展着。这些知识是人们长期劳动的结晶。 我国很早就已经发现电和磁的现象,在古籍中曾有“磁石召铁”和“琥珀拾芥”的记载。磁石首先应用于指示方向和校正时间,在《韩非子》和东汉王充著《论衡》两书中提到的“司南”就是指此。以后由于航海事业发展的需要,我国在十一世纪就发明了指南针。在宋代沈括所著的《梦溪笔谈》中有“方家以磁石磨针锋,则能指南,然常微偏东,不全南也”的记载。这不仅说明了指南针的制造,而且已经发现了磁偏角。直到十二世纪,指南针才由阿拉伯人传入欧洲。 在十八世纪末和十九世纪初的这个时期,由于生产的需要,在电磁现象方面的研究工作发展得很快。库仑在1785年首先从实验室确定了电荷间的相互作用力,电荷的概念开始有了定量的意义。1820年,奥斯特从在实验时发现了电流对磁针有力的作用,揭开了电学理论的新的一页。同年,安培确定了通有电流的线圈的作用与磁铁相似,这就指出了此现象的本质问题。有名的欧姆定律是欧姆于1826年通过实验得出的。法拉第对电磁现象的研究有特殊的贡献,他在1831年发现的电磁感应现象是以后电子技术的重要理论基础。在电磁现象的理论与使用问题的研究上,楞次发挥了巨大的作用。他在1833年建立确定感应电流方向的定则(楞次定则)。其后,他致力于电机理论的研究,并阐明了电机可逆性的原理。他在1844年还与英国物理学家焦耳分别独立地确定了电流热效应定律(焦耳—楞次定律)。与楞次一道从事电磁现象研究工作的雅可比在1834年制造出世界上第一台电动机,从而证明了实际应用电能的可能性。电机工程得以飞速的发展是与多里沃-多勃罗沃尔斯基的工作分不开的。这位杰出的俄罗斯工程师是三相系统的创始者,他发明和制造出三相异步电机和三相变压器,并首先采用了三相输电线。在法拉第的研究工作上,麦克斯韦在1864年至1873年提出了电磁场理论。他从理论上推测到电磁波的存在,为无线电技术的发展奠定了基础。1888年,赫兹通过实验获得电磁波,证实了麦克斯韦的理论。但实际利用电磁波为人类服务的还应归功于马克尼和波波夫。大约在赫兹实验成功七年后,他们彼此独立地分别在意大利和俄国进行通信实验,为无线电技术的发展开辟了道路。 人类在与自然界斗争的过程中,不断总结和丰富着自己的知识。电子技术就是在生产斗争和科学实验中发展起来的。1883年美国发明家爱迪生发现了热电子效应,随后在1904年弗莱明利用这个效应制成了电子二极管,并证实了电子管具有“阀门”作用,它首先被用于无线电检波。1906年美国的德弗雷斯在弗莱明的二极管中放进了第三个电极—栅极,而发明了电子三极管,从而建树了早期电子技术上最重要的里程碑。又经过五年研究改进,从1911年开始了使用电子技术的时代。所以,电子技术作为一门新兴科学,其发展至今不过七八十年。 半个多世纪以来,电子管在电子技术中立下了很大功劳;但是电子技术的成本高,制造繁,体积大,耗电多,从1948年美国贝尔实验室的几位研究人员发明晶体管以来,在大多数领域中已逐渐用晶体管来取代电子管。但是,由于电子管的独特优点,在有些装置中,不论从稳定性,经济性或功率上考虑,还需采用电子管。 1948年用半导体材料做成的第一只晶体管,叫“半导体器件”或“固体器件”,1951年有了商品,这是出现分立元件的又一个里程碑。20世纪50年代末提出“集成”的观点。1950年Kilby在IRE的一次会议上宣布“固体电路”的出现,以后叫“集成电路”。集成电路的出现和应用,标志着电子技术发展到了一个新的阶段。它实现了材料,元件,电路三者之间的统一;同传统的电子元件的设计与生产方式,电路的结构形式有着本质的不同。1960年集成电路处于“小规模集成”阶段,每个半导体芯片上有不到100个元器件。1966年进入“中规模集成”阶段,每个芯片上有100到1000个元器件。1969年进入“大规模集成”阶段,每个芯片上的元器件达到10000左右。1975年更进一步跨入“超大规模集成”阶段,每个芯片上的元器件多达10000个以上。从1960年至1980年的二十年间,芯片上元器件的“集成度”增加了1000000倍,每年递增率约为2倍。目前的超大规模集成,在几十平方毫米的芯片上有上百万个元器件,已经进入“微电子”时代,大大促进了先进科学技术的发展。 随着半导体技术的发展和科学研究,生产和管理等的需要,电子计算机应时而兴起,并且日臻完善。从1946年诞生第一台电子计算机以来,它们已经经历了电子管,晶体管,集成电路及超大规模集成电路四代,每秒运算速度已达10亿次。现在正在研究开发第五代计算机(人工智能计算机)和第六代计算机(生物计算机),它们不依靠程序工作,而凭借人工智能工作。特别是七十年代卫星计算机问世以来,由于它价廉、方便、可靠、小巧,大大加快了电子计算机的普及速度。 数字控制和数字测量也在不断发展和日益广泛地应用。数字控制机床和“自适应”数字控制机床相继出现。目前利用电子计算机对几十台乃至百台数字控制机床进行集中控制(所谓“群控”)也已经实现。 在工业上晶体闸流管也获得了广泛应用,使半导体技术进入了强电领域。 随着生产和科学技术发展的需要,现在电子技术的应用已经渗透到了人类生活和生产的各个方面。西方学者将之归纳为四个方面,或者叫做四个“C”。有两种说法:一种是元器件制造工业,通讯,控制和计算机;另一种说法是通讯,控制,计算机和文化生活,如广播、电视、录音、电化教学、电子文体用具、电子表等。 由于电子技术得到高度发展和广泛应用(如空间电子技术、生物医学电子技术、信息处理和遥感技术、微波应用等),它对于社会生产力的发展,也起了变革性的推动作用。电子水准是现代化的一个重要标志,电子工业是实现现代化的重要物质技术基础。电子工业的发展速度和技术水平,特别是电子计算机的高度发展及其在生产领域中的广泛应用,直接影响到工业、农业、科学技术和国防建设,关系着社会主义建设的发展速度和国家的安危;也直接影响到亿万人民的物质、文化生活,关系着广大群众的切身利益。

    时间:2018-07-17 关键词: 电子技术

  • 补补嵌入式系统基础知识

     在物联网技术如火如荼的今天,嵌入式开发重新得到了广大IT从业人员的关注。那么,什么是嵌入式系统?嵌入式系统由哪几部分组成?嵌入式系统的特点都有哪些?嵌入式系统的开发流程是怎样的?本文将带领大家重温这些基础知识。 嵌入式系统简介 嵌入式系统是指以应用为中心,以计算机技术为基础,软硬件可裁剪,适应应用系统对功能、可靠性、成本、体积、功耗等严格要求的专用计算机系统。嵌入式系统一般应用于消费电子、智能家电、网络设备、医疗仪器、航天设备、工业控制等行业中。 嵌入式系统组成 一个嵌入式系统一般都由嵌入式计算机系统和执行装置组成,嵌入式计算机系统是整个嵌入式系统的核心,由硬件层、中间层、系统软件层和应用软件层组成。执行装置也称为被控对象,它可以接受嵌入式计算机系统发出的控制命令,执行所规定的操作或任务。 下面,我们来了解一下嵌入式计算机系统的各个部分。 1.硬件层 硬件层中包含嵌入式微处理器、存储器(RAM、ROM、Flash等)、通用设备接口和I/O接口(A/D、D/A、I/O等)。在一块嵌入式处理器基础上添加电源电路、时钟电路和存储器电路,就构成了一个嵌入式核心控制模块。其中操作系统和应用程序都可以固化在ROM中。 嵌入式系统硬件层的核心是嵌入式微处理器,它与通用CPU最大的不同在于它大多工作在为特定用户群所专用设计的系统中,它将通用CPU许多由板卡完成的任务集成在芯片内部,从而有利于嵌入式系统在设计时趋于小型化,同时还具有很高的效率和可靠性。 嵌入式微处理器的体系结构可以采用冯·诺依曼体系或哈佛体系结构,指令系统可以选用精简指令系统(Reduced Instruction Set Computer,RISC)和复杂指令系统CISC(Complex Instruction Set Computer,CISC)。嵌入式微处理器有各种不同的体系,即使在同一体系中也可能具有不同的时钟频率和数据总线宽度,或集成了不同的外设和接口。 嵌入式系统需要存储器来存放和执行代码。嵌入式系统的存储器包含Cache、主存和辅助存储器。 Cache是一种容量小、速度快的存储器阵列,它位于主存和嵌入式微处理器内核之间,存放的是一段时间微处理器使用最多的程序代码和数据;在需要进行数据读取操作时,微处理器尽可能的从Cache中读取数据,而不是从主存中读取,这样就大大改善了系统的性能,提高了微处理器和主存之间的数据传输速率;Cache的主要目标就是:减小存储器(如主存和辅助存储器)给微处理器内核造成的存储器访问瓶颈,使处理速度更快,实时性更强;在嵌入式系统中Cache全部集成在嵌入式微处理器内,可分为数据Cache、指令Cache或混合Cache,Cache的大小依不同处理器而定,一般中高档的嵌入式微处理器才会把Cache集成进去。 主存是嵌入式微处理器能直接访问的寄存器,用来存放系统和用户的程序及数据;它可以位于微处理器的内部或外部,其容量为256KB~1GB,根据具体的应用而定,一般片内存储器容量小,速度快,片外存储器容量大。 辅助存储器用来存放大数据量的程序代码或信息,它的容量大、但读取速度与主存相比就慢的很多,用来长期保存用户的信息;嵌入式系统中常用的外存有:硬盘、NAND Flash、CF卡、MMC和SD卡等。 嵌入式系统和外界交互需要一定形式的通用设备接口,外设通过和片外其他设备的或传感器的连接来实现微处理器的输入/输出功能。每个外设通常都只有单一的功能,它可以在芯片外也可以内置芯片中。外设的种类很多,可从一个简单的串行通信设备到非常复杂的无线设备。嵌入式系统中常用的通用设备接口有A/D(模/数转换接口)、D/A(数/模转换接口),I/O接口有RS-232接口(串行通信接口)、Ethernet(以太网接口)、USB(通用串行总线接口)、音频接口、VGA视频输出接口、I2C(现场总线)、SPI(串行外围设备接口)和IrDA(红外线接口)等。 2.中间层 硬件层与软件层之间为中间层,也称为硬件抽象层(Hardware Abstract Layer,HAL)或板级支持包(Board Support Package,BSP),它将系统上层软件与底层硬件分离开来,使系统的底层驱动程序与硬件无关,上层软件开发人员无需关心底层硬件的具体情况,根据BSP层提供的接口即可进行开发。该层一般包含相关底层硬件的初始化、数据的输入/输出操作和硬件设备的配置功能。 BSP具有以下两个特点: 第一,硬件相关性。因为嵌入式实时系统的硬件环境具有应用相关性,而作为上层软件与硬件平台之间的接口,BSP需要为操作系统提供操作和控制具体硬件的方法。 第二,操作系统相关性。不同的操作系统具有各自的软件层次结构,因此,不同的操作系统具有特定的硬件接口形式。 实际上,BSP是一个介于操作系统和底层硬件之间的软件层次,包括了系统中大部分与硬件联系紧密的软件模块,它的一个主要功能是硬件相关的设备驱动。设计一个完整的BSP需要完成两部分工作:嵌入式系统的硬件初始化以及BSP功能,设计硬件相关的设备驱动。 3.系统软件和应用软件 系统软件层由实时多任务操作系统(Real-time Operation System,RTOS)、文件系统、图形用户接口(Graphic User Interface,GUI)、网络系统及通用组件模块组成。RTOS是嵌入式应用软件的基础和开发平台。 嵌入式操作系统(Embedded Operation System,EOS)是一种用途广泛的系统软件,过去它主要应用于工业控制和国防系统领域。EOS负责嵌入系统的全部软、硬件资源的分配、任务调度,控制、协调并发活动。它必须体现其所在系统的特征,能够通过装卸某些模块来达到系统所要求的功能。随着Internet技术的发展、信息家电的普及应用及EOS的微型化和专业化,EOS开始从单一的弱功能向高专业化的强功能方向发展。嵌入式操作系统在系统实时高效性、硬件的相关依赖性、软件固化以及应用的专用性等方面具有较为突出的特点。EOS是相对于一般操作系统而言的,它除具有了一般操作系统最基本的功能,还有以下功能:如任务调度、同步机制、中断处理、文件处理等。 嵌入式文件系统比较简单,主要提供文件存储、检索和更新等功能,一般不提供保护和加密等安全机制。它以系统调用和命令方式提供文件的各种操作,主要有:1)设置、修改对文件和目录的存取权限;2)提供建立、修改、改变和删除目录等服务;3)提供创建、打开、读写、关闭和撤销文件等服务。文件系统的特点:1)兼容性;2)实时文件系统;3)可裁剪、可配置;4)支持多种存储设备。 嵌入式GUI具有下面几个方面的基本要求:轻型、占用资源少、高性能、高可靠性、便于移植、可配置等特点。嵌入式系统中的图形界面,一般采用下面的几种方法实现:1)针对特定的图形设备输出接口,自行开发相关的功能函数;2)购买针对特定嵌入式系统的图形中间软件包;3)采用源码开放的嵌入式GUI系统;4)使用独立软件开发商提供的嵌入式GUI产品。 应用软件层是由基于实时系统开发的应用程序组成,用来实现对被控对象的控制功能。功能层是要面对被控对象和用户,为方便用户操作,往往需要提供一个友好的人机界面。对于一些复杂的系统,在系统设计的初期阶段就要对系统的需求进行分析,确定系统的功能,然后将系统的功能映射到整个系统的硬件、软件和执行装置的设计过程中,称为系统的功能实现。 嵌入式系统特点 嵌入式系统的特点包括:系统内核小、专用性强、系统精简、实时性等。具体而言,它主要有如下八个特点: 1)可裁剪性。支持开放性和可伸缩性的体系结构。 2)强实时性。EOS实时性一般较强,可用于各种设备控制中。 3)统一的接口。提供设备统一的驱动接口。 4)操作方便、简单、提供友好的图形GUI和图形界面,追求易学易用;提供强大的网络功能,支持TCP/IP协议及其他协议,提供TCP/UDP/IP/PPP协议支持及统一的MAC访问层接口,为各种移动计算设备预留接口。 5)强稳定性,弱交互性。嵌入式系统一旦开始运行就不需要用户过多的干预、这就要负责系统管理的EOS具有较强的稳定性。嵌入式操作系统的用户接口一般不提供操作命令,它通过系统的调用命令向用户程序提供服务。 6)固化代码。在嵌入式系统中,嵌入式操作系统和应用软件被固化在嵌入式系统计算机的ROM中。 7)更好的硬件适应性。也就是良好的移植性。 8)嵌入式系统和具体应用有机地结合在一起,它的升级换代也是和具体产品同步进行,因此嵌入式系统产品一旦进入市场,具有较长的生命周期。 嵌入式系统开发流程 嵌入式系统的开发分为不带操作系统(裸机开发)和带操作系统的开发两种。裸机开发是指对于功能简单仅包括应用程序的嵌入式系统一般不使用操作系统,仅有应用程序和设备驱动程序。带操作系统的开发是由一个操作系统(OS)来管理控制内存、多任务、周边资源等。 通常基于Linux系统的嵌入式开发步骤有如下七步: 1)开发目标硬件系统:如选择微处理器、Flash及其它外设等。 2)建立交叉开发环境:安装交叉编译工具链、安装开发调试工具。 3)开发Bootloader:移植uboot。 4)移植Linux内核:如Linux 3.0.25内核。 5)开发根文件系统:如CRAMFS,YAFFS等。 6)开发相关硬件的驱动程序:如led,adc等驱动。 7)开发上层的应用程序:如QT GUI开发。 嵌入式系统初始化 嵌入式系统初始化过程可以分为3个主要环节,按照自底向上、从硬件到软件的次序依次为:片级初始化、板级初始化和系统级初始化。 片级初始化完成嵌入式微处理器的初始化,包括设置嵌入式微处理器的核心寄存器和控制寄存器、嵌入式微处理器核心工作模式和嵌入式微处理器的局部总线模式等。片级初始化把嵌入式微处理器从上电时的默认状态逐步设置成系统所要求的工作状态。这是一个纯硬件的初始化过程。 板级初始化完成嵌入式微处理器以外的其他硬件设备的初始化。另外,还需设置某些软件的数据结构和参数,为随后的系统级初始化和应用程序的运行建立硬件和软件环境。这是一个同时包含软硬件两部分在内的初始化过程。 系统级初始化以软件初始化为主,主要进行操作系统的初始化。BSP(中间层)将对嵌入式微处理器的控制权转交给嵌入式操作系统,由操作系统完成余下的初始化操作,包含加载和初始化与硬件无关的设备驱动程序,建立系统内存区,加载并初始化其他系统软件模块,如网络系统、文件系统等。最后,操作系统创建应用程序环境,并将控制权交给应用程序的入口。

    时间:2018-07-17 关键词: 嵌入式 基础知识

  • 嵌入式初学者注意事项

     1 嵌入式系统的知识体系 嵌入式系统的应用范围可以粗略分为两大类:(1)电子系统的智能化(工业控制,现代农业、家用电器、汽车电子、测控系统、数据采集等等);(2)计算机应用的延伸(MP3、手机、通信、网络、计算机外围设备等)。从这些应用可以看出,要完成一个以MCU为核心的嵌入式系统应用产品设计,需要有硬件、软件及行业领域相关知识。硬件主要有MCU的硬件最小系统、输入/输出外围电路、人机接口设计。软件设计有固化软件的设计,也可能含PC机软件的设计。行业知识需要通过协作、交流与总结获得。 概括地说,学习以MCU为核心的嵌入式系统,需要以下软件硬件基础知识与实践训练: 1:硬件最小系统(包括电源、晶振、复位、写入调试接口); 2:通用I/O(开关量输入/输出,涉及各种二值量检测与控制); 3:模数转换A/D(各种传感器信号的采集与处理,如红外、温度、光敏、超声波、方向等等); 4:数模转换D/A(对模拟量设备利用数字进行控制); 5:通信(串行通信接口SCI、串行外设接口SPI、集成电路互联总线I2C,CAN、USB、嵌入式以太网、ZigBee技术等); 6:显示(LED、LCD等等); 7:控制(控制各种设备,包含PWM等控制技术); 8:数据处理(图形、图像、语音、视频等处理或识别); 9:各种具体应用。 事实上,万变不离其宗,任何应用都可以归入这几类。而应用中的硬件设计、软件设计、测试等都必须遵循嵌入式软件工程的方法、原理与基本原则。所以,嵌入式软件工程也是嵌入式系统知识体系的有机组成部分,只不过,它融于具体项目的开发过程之中。 以上实践训练涉及硬件基础、软件基础及相关领域知识。计算机语言、操作系统、开发环境等均是完成这些目的的工具。有些初学者,容易把工具的使用与所要达到的真正目的相混淆。例如,有的学习者,学了很长时间的嵌入式操作系统移植,而不进行实际嵌入式系统产品的开发,到了最后,做不好一个嵌入式系统小产品,偏离了学习目标,甚至放弃了嵌入式系统领域。这就是进入了嵌入式系统学习误区的情况,下面对此做一些分析。 2 嵌入式系统的学习误区 关于嵌入式系统的学习方法,因学习经历、学习环境、学习目的、已有的知识基础等不同,可能在学习顺序、内容选择、实践方式等方面有所不同。但是,应该明确哪些是必备的基础知识,哪些应该先学,哪些应该后学,哪些必须通过实践才能获得的,哪些是与具体芯片无关的通用知识,哪些是与具体芯片或开发环境相关的知识。 由于微处理器与微控制器种类繁多,也可能由于不同公司、不同机构出于自身的利益,给出一些误导性宣传,特别是我国嵌入式微控制器制造技术的落后及其他相关情况,使得人们对微控制器的发展,在认识与理解上存在差异。导致一些初学者,进入了嵌入式系统的学习误区,浪费了宝贵的学习时间。下面分析初学者可能存在的几个误区。 如果说,学习嵌入式系统不是为了开发其应用产品,那就没有具体目标了,许多诸如学习方法问题也就不必谈了。实际上,这正是许多人想学,又不知从何开始学习的关键问题所在,不知道自己学习的具体目标。于是,看了一些培训广告,看了书店中书架上种类繁多的嵌入式系统的书籍,或上网以“嵌入式系统”为关键词进行查询,然后参加培训或看书,开始“学习起来”。对于有计算机阅历的人,往往选择一个嵌入式操作系统就开始学习了。不好听的比喻,有点象“瞎子摸大象”,只了解其一个侧面。这样如何能对嵌入式产品的开发过程有个全面了解呢?针对许多初学者选择“xxx嵌入式操作系统+xxx处理器”的嵌入式系统入门学习模式,我认为是不合适的。我的建议是:首先把嵌入式系统软件与硬件基础打好了,再根据实际需要,选择一种实时操作系统(RTOS)进行学习实践。要记住:RTOS是开发某些类嵌入式产品的辅助工具,是手段,不是目的。许多类嵌入式产品,并不需要RTOS。所以,一开始就学习RTOS,并不符合学习“由浅入深、循序渐进”的学习规律。 RTOS本身由于种类繁多,实际使用何种RTOS,一般需要工作单位确定。基础阶段主要学习RTOS的基本原理与在RTOS之上的软件开发方法,而不是学习如何设计RTOS。以开发实际嵌入式产品为目标的学习者,不要把过多的精力花在设计或移植RTOS上面。正如很多人使用Windows操作系统,而设计Windows操作系统只有Microsoft。许多人“研究”Linux,但不使用它,浪费时间了,人的精力是有限的,学习必须有所选择。 2.2 嵌入式系统学习误区2-硬件与软件的困惑 以MCU为核心的嵌入式技术的知识体系必须通过具体的MCU来体现、实践与训练。但是,选择任何型号的MCU,其芯片相关的知识只占知识体系的20%,80%是通用知识。但是80%的通用知识,必须通过具体实践才能进行,所以学习嵌入式技术要选择一个系列的MCU。但不论如何,系统含有硬件与软件两大部分,它们之间的关系如何? 有些学者,仅从电子角度认识嵌入式系统。认为“嵌入式系统=MCU硬件系统+小程序”。这些学者,大多学习背景是具有良好的电子技术基础知识。实际情况是,早期MCU内部RAM小、程序存储器外接,需要外扩各种I/O,没有象现在这样USB、嵌入式以太网等较复杂的接口,因此,程序占总设计量小于50%,使人们认为嵌入式系统(单片机)是“电子系统”,以硬件为主、程序为辅。但是,随着MCU制造技术的发展,不仅MCU内部RAM越来越大,Flash进入MCU内部改变了传统的嵌入式系统开发与调试方式,固件程序可以被更方便地调试与在线升级,许多情况与开发PC机程序方便程度相差无几,只不过开发环境与运行环境不是同一载体而已。这些情况使得嵌入式系统的软件硬件设计方法发生了根本变化。 有些学者,仅从软件开发角度认识嵌入式系统,甚至有的仅从嵌入式操作系统认识嵌入式系统。这些学者,大多具有良好的计算机软件开发基础知识,认为硬件是生产厂商的事,没有认识到,嵌入式系统产品的软件与硬件均是需要开发者设计的。我常常接到一些关于嵌入式产品稳定性的咨询电话,发现大多数是由于软件开发者对底层硬件的基本原理不理解造成的。特别是,有些功能软件开发者,过分依赖于底层硬件的驱动软件设计完美,自己对底层驱动原理知之甚少。实际上,一些功能软件开发者,名义上再做嵌入式软件,实际上,仅仅使用嵌入式编辑、编译环境而已,本质与开发通用PC机软件没有两样。而底层硬件驱动软件的开发,若不全面考虑高层功能软件对底层硬件的可能调用,也会使得封装或参数设计得不合理或不完备,导致高层功能软件的调用困难。从这段描述可以看出,若把一个嵌入式系统的开发孤立地分为硬件设计、底层硬件驱动软件设计、高层功能软件设计,一旦出现了问题,就可能难以定位。实际上,嵌入式系统设计是一个软件、硬件协同设计工程,不能象通用计算机那样,软件、硬件完全分开来看,要在一个大的框架内协调工作。 面对学习嵌入式系统以软件为主还是以硬件为主,或是如何选择切入点,如何在软件与硬件之间取得一些平衡。对于这个困惑的建议是:要想成为一名真正的嵌入式系统设计师,在初学阶段,必须重视打好嵌入式系统的硬件与软件基础。以下是从事嵌入式系统设计二十多年的一个美国学者John Catsoulis 在《Designing Embedded Hardware》一书中关于这个问题的总结:嵌入式系统与硬件紧密相关,是软件与硬件的综合体,没有对硬件的理解就不可能写好嵌入式软件,同样没有对软件的理解也不可能设计好嵌入式硬件。 嵌入式系统产品种类繁多,应用领域各异。在2.1小节中,我们把嵌入式系统的应用范围粗略分为电子系统的智能化与计算机应用的延伸两大类,从初学者角度,可能有分别从这两个角度片面认识嵌入式系统的问题。因此,一些从电子系统智能化角度认识嵌入式系统的学习者,可能会忽视编程结构、编程规范、软件工程的要求、操作系统等知识的积累。另一些从计算机应用的延伸角度认识嵌入式系统的学习者,可能会把通用计算机学习过程中的概念与方法生搬硬套到嵌入式系统的实践中,忽视嵌入式系统与通用计算机的差异。 实际上,在嵌入式系统学习与实践的初始阶段,应该充分了解嵌入式系统的特点,根据自身的已有知识结构,制定适合自身情况的学习计划。目标应该是打好嵌入式系统的硬件与软件基础,通过实践,为成为良好的嵌入式系统设计师建立起基本知识结构。学习过程,可以通过具体应用系统为实践载体,但不能拘泥于具体系统,应该有一定的抽象与归纳。例如,有的初学者开发一个实际控制系统,没有使用实时操作系统,但不要认为实时操作系统不需要学习。又例如,有的初学者以一个带有实时操作系统的样例为蓝本进行学习,但不要认为,任何嵌入式系统都需要使用实时操作系统,甚至把一个十分简明的实际系统加上一个不必要的实时操作系统。因此,片面认识嵌入式系统,可能导致学习困惑。应该根据实际项目需要,锻炼自己分析实际问题、解决问题的能力。这是一个比较长期的学习与实践过程,不能期望通过短期培训完成整体知识体系的建立,应该重视自身实践,全面地理解嵌入式系统的知识体系。 嵌入式系统的大部分初学者需要选择一个微控制器(MCU)进行入门级学习,面对众多厂家生产的微控制器系列,不知如何是好。 首先是关于位数问题,目前主要有8位、16位、32位,面对嵌入式系统应用的多样性,不同位数的MCU各有应用领域,这一点与通用微机有很大不同。你做一个遥控器,不需要使用一个32位MCU,可能一个MCU芯片价格已经超过遥控器价格需求。对于首次接触嵌入式系统的学习者,可以根据自己的知识基础选择入门芯片的位数。建议大多数初学者,可以选择一个8位MCU作为快速入门芯片,了解一些汇编与底层硬件知识,之后再选一个16位或32位芯片进行学习实践。 关于芯片选择的另一个误区,认为有“主流芯片”存在,嵌入式系统也可以形成芯片垄断。这完全是一种误解,是套用通用计算机系统的思维模式,而忽视了嵌入式系统应用的多样性。 关于学习芯片选择还有一个误区,是系统的工作频率。误认为选择工作频率高的芯片进行入门学习,表示更先进。实际上,工作频率高可能给初学者带来学习过程中的不少困难。 实际嵌入式系统设计不是追求芯片位数、工作频率、操作系统等因素,而是追求稳定可靠、维护、升级、功耗、价格等指标。而初学者选择入门芯片,是通过某一MCU作为蓝本获得嵌入式系统知识体系的通用基础,其基本原则是:入门时间较快、硬件成本较少,知识要素较多,学习难度较低。 3 基础阶段的学习建议 基于以上讨论,下面对广大渴望学习嵌入式系统的学子提出几点基础阶段的学习建议: (1)嵌入式系统软件硬件密切相关,一定要打好软件硬件基础。其实,只要找到正确的方法,加上努力,任何理工科学生,甚至非理工科学生,都能学好嵌入式系统。 (2)选择一个芯片及硬件评估板(入门芯片最好是简单一点,例如8位MCU)、选择一本好书(最好有规范的例子)、找一位好老师(最好是有经验且热心的)。硬件评估板的价格一定要在1000元以下,不要太贵,最好能有自己动手的空间。不花一分硬件钱,要想学好嵌入式系统不实际。因为,这是实践性很强的学科。好书,可以使你少走弯路,不会被误导,要知道有的书是会使人进入学习误区的。好老师也可以是做过一些实际项目的学长(一定要找做过几个成功项目的学长或老师做指导,否则,经验不足也可能误导),有教师指导,学习进程会加快(人工智能学科里有个术语叫无教师指导学习模式与有教师指导学习模式,无教师指导学习模式比有教师指导学习模式复杂许多)。 (3)许多人怕硬件,其实嵌入式系统硬件比电子线路好学多了。只要深入理解MCU的硬件最小系统,对I/O口、串行通信、键盘、LED、LCD、SPI、I2C、PWM、A/D(包括一些传感器)、D/A等逐个实验理解,逐步实践。再通过自己做一个实际的小系统,底层硬件基础就有了。各个硬件模块驱动程序的编写是嵌入式系统的必备基础。学习嵌入式系统的初期,这个过程是必须的。 (4)至于嵌入式实时操作系统RTOS,一定不要一开始就学,这样会走很多弯路,也会使你对嵌入式系统感到畏惧。等你软件硬件基础打好了,再学习就感到容易理解。实际上,众多嵌入式应用,并不一定需要操作系统。也可以根据实际项目需要再学习特定的RTOS。一定不要被一些嵌入式实时操作系统培训班宣传所误导,而忽视实际嵌入式系统软件硬件基础知识的学习。 (5)要避免片面地单纯从“电子”或“计算机软件”角度认识嵌入式系统。前面说过,嵌入式系统是软件与硬件的综合体。因此,要逐步从MCU的最小系统开始,一点一点理解硬件原理及底层硬件驱动编程方法。要通过规范的例子,理解软件工程封装、可复用等思想。通过规范编程,积累底层构件(Component),也就是一个一个模块,但是要封装得比较好,可复用。 (6)注重实验与实践。这里说的实验主要指通过重复或验证他人的工作,目的是学习基础知识,这个过程一定要经历。实践是自己设计,有具体的“产品”目标。如果你能花500元左右自己做一个具有一定功能的小产品,且能稳定运行1年以上,就可以说接近入门了。 (7)关于入门芯片的选择。不要选太复杂的微控制器作为入门芯片,不能超越学习过程。不要一下子学习几种芯片,可以通过一个芯片入门并具有一个实践经验后,根据实际需要选择芯片开发实际产品。注意,不要把微处理器(MPU)与微控制器(MCU)概念相混淆,微处理器只是微控制器的内核。 (8)关于操作系统的选择。可以等到你具有一定实践后,选择一个简单容易理解原理的进行学习,不要一开始就学习几种操作系统,理解了基本原理,实践中确有实际需要再学习也不迟。人总是要不断学习的。 (9)关于汇编与C语言的取舍。随着MCU对C编译的优化支持,对于汇编可以只了解几个必须的语句,直接使用C语言编程。但必须通过第一个程序理解芯片初始化过程、中断机制、程序存储情况等区别于PC机程序的内容。另外,为了测试的需要,最好掌握一门PC机编程语言。 (10)要明确自己的学习目的,并注意学习方法。关于学习目的要明确是打基础,还是为了适应某些工作而进行的短训;而学习方法方面,要根据学习目的选择合适的学习途径,注意理论学习与实践、通用知识与芯片相关知识、硬件知识与软件知识的平衡,要在理解软件工程基本原理基础上理解硬件构件与软件构件等基本概念。 以上建议,仅供参考。 当然,以上只是基础阶段的学习建议,要成为良好的嵌入式系统设计师,还需要在实际项目中锻炼,并不断学习与积累经验。

    时间:2018-07-17 关键词: 嵌入式

  • 在单片机开发中NOR_FLASH的应用

     在单片机开发中,NOR_FLASH常用的有4M和8M的大小: 4M的FLASH在程序中可以这样表示:Ptr < 0x220000 8M的FLASH在程序中可以这样表示:Ptr < 0x400000(最大只能读到0x3fffff) 有了这个关系,在判断NOR_FLASH好坏的方法上我们可以采用计算checksum的方式来校验。 在程序中,我们一般把地址的指针定义为unsigned short *ptr 类型。然后通过指针不断的往后移动可以简引用获取到FLASH中的每一个byte,如果此时定义成unsigned short类型,那么用一个temp变量来接收的话,一次是可以接收到2个byte的数据,如果此时定义成unsigned long类型,同样用temp变量来接收这时一次性可以读到4个byte的数据,所以在处理数据方面可以采用位运算来进行处理。 以下这段程序是每1024byte获取前16个byte的数据依次累加返回,注意,这里累加是以word的形式累加,1 word = 2byte,所以读512相当于读了1024byte 注意,这里的单片机是16位的,其它的可能不同,不要类比,需要重新计算相应数据类型的值。

    时间:2018-07-06 关键词: 单片机 单片机开发

  • 单片机实战开发细节:如何为单片机的按键加一个锁防止多次触发

     最近一直在做凌阳的GPL32001的单片机开发,主打产品是一架钢琴。 在这架钢琴上,我们可以看到遍布着很多按键,有琴键,也有功能选择的按键,面对如此多的按键,对于一个刚出来工作的小伙伴肯定压力比较大,琴键的特征和普通按键不太一样,琴键的一个按键由两个按键组成,一个按键储存着两样信息,力度和键值。 那么在我写的程序的项目要求是这样的,要求每个按键一次只能触发一次,并且触发的时候要发出不同的键码,通过音频解码盒将该键码值读出来,比如第一个白色琴键是key01--->对应的键值就是0000 0001 也就是0x01,而功能按键的编排和琴键有所不同,功能按键的编排从序号key55开始,键值也和琴键的不一样。鉴于这样的特征,即可以鉴别机器是否出现短路,断路等硬件是否损坏的情况。 那么,今天我提出的一个问题也是在单片机开发中常见的,也就是按键,学过单片机的同学都玩过按键,一开始都是这样的代码: if(key == 0) bell = 0 ; else bell = 1 ; 但是如果这样的话,假设是在一个死循环里面,按键如果检测到低电平为按下,按键就会一直触发,bell=0的分支就会被不断的执行。 于是我想到一个好的办法,我项目里是这么写的。 定义一个 static int lock ;然后做以下的操作,当然这个操作是在一个死循环内操作的: //获取按键状态 data = *P_IOE_Data; if((data&0x0080)) { IOE_lock = 0 ; } if((data&0x0080) == 0) { if(IOE_lock == 0) { play_sound_hightolow(0x33,Vol_value); } IOE_lock = 1 ; } if((data & 0x0080))表示按键没有被按下,此时按键锁标志为0,staic类型将记录这个标志变量的值,当if((data & 0x0080) == 0)时,按键此时被按下了,我要判断按键锁标志是否为0,如果为1,那么程序肯定不会运行play_sound_hightolow();这个函数,所以当按下按键的时候,锁的初始化值为0,喇叭发出声音码,音频解码器读出对应的键值为0x33。读完之后立马的将锁标志置1,如果此时一直按住按键不放,因为锁标志等于1,所以无效,程序不进入发码的状态。当松开后,按键的状态由1变成0,此时再按下按键,又有效,然后锁住。 这样做的好处就是使按键按下的时候,发码的状态只触发一次,就不会连着发出0x33的声音码了,只发了一次。在合适的开发利用好标志锁,可以很方便的高效解决很多问题。

    时间:2018-07-06 关键词: 单片机 单片机开发

  • 谈谈单片机裸奔的程序框架

     初学单片机时,都会纠结于其各个模块功能的应用,如串口(232,485)对各种功能IC的控制,电机控制PWM,中断应用,定时器应用,人机界面应用,CAN总线等. 这是一个学习过程中必需的阶段,是基本功。很庆幸,在参加电子设计大赛赛前培训时,MCU周围的控制都训练的很扎实。经过这个阶段后,后来接触不同的MCU就会发现,都大同小异,各有各的优势而已,学任何一种新的MCU都很容易入手包括一些复杂的处理器。而且对MCU的编程控制会提升一个高度概况——就是对各种外围进行控制(如果是对复杂算法的运算就会 用DSP了),而外围与MCU的通信方式一般也就几种时序:IIC,SPI,intel8080,M6800。这样看来MCU周围的编程就是一个很简单的东西了。 然而这只是嵌入式开发中的一点皮毛而已,在接触过多种MCU,接触过复杂设计要求,跑过操作系统等等后,我们在回到单片机的裸机开发时,就不知不觉的就会考虑到整个程序设计的架构问题;一个好的程序架构,是一个有经验的工程师和一个初学者的分水岭。 以下是我对单片机程序框架以及开发中一些常用部分的认识总结: 任何对时间要求苛刻的需求都是我们的敌人,在必要的时候我们只有增加硬件成本来消灭它;比如你要8个数码管来显示,我们在没有相关的硬件支持的时候必须用MCU以动态扫描的方式来使其工作良好;而动态扫描将或多或少的阻止了MCU处理其他的事情。在MCU负担很重的场合,我会选择选用一个类似max8279外围ic来解决这个困扰; 然而庆幸的是,有着许多不是对时间要求苛刻的事情: 例如键盘的扫描,人们敲击键盘的速率是有限的,我们无需实时扫描着键盘,甚至可以每隔几十ms才去扫描一下;然而这个几十ms的间隔,我们的MCU还可以完成许多的事情; 单片机虽然是裸机奔跑,但是往往现实的需要决定了我们必须跑出操作系统的姿态——多任务程序; 比如一个常用的情况有4个任务: 1 键盘扫描; 2 led数码管显示; 3 串口数据需要接受和处理; 4 串口需要发送数据; 如何来构架这个单片机的程序将是我们的重点; 读书时代的我会把键盘扫描用查询的方式放在主循环中,而串口接收数据用中断,在中断服务函数中组成相应的帧格式后置位相应的标志位,在主函数的循环中进行数据的处理,串口发送数据以及led的显示也放在主循环中; 这样整个程序就以标志变量的通信方式,相互配合的在主循环和后台中断中执行; 然而必须指出其不妥之处: 每个任务的时间片可能过长,这将导致程序的实时性能差。如果以这样的方式在多加几个任务,使得一个循环的时间过长,可能键盘扫描将很不灵敏。所以若要建立一个良好的通用编程模型,我们必须想办法,消去每个任务中费时间的部分以及把每个任务再次分解;下面来细谈每个任务的具体措施: 1 键盘扫描 键盘扫描是单片机的常用函数,以下指出常用的键盘扫描程序中,严重阻碍系统实时性能的地方; 众所周知,一个键按下之后的波形是这样的(假定低有效): 在有键按下后,数据线上的信号出现一段时间的抖动,然后为低,然后当按键释放时,信号抖动一段时间后变高。当然,在数据线为低或者为高的过程中,都有可能出现一些很窄的干扰信号。 unsigned char kbscan(void) { unsigned char sccode,recode; P2=0xf8; if ((P2&0xf8)!=0xf8) { delay(100); //延时20ms去抖--------这里太费时了,很糟糕 if((P2&0xf8)!=0xf8) { sccode=0xfe; while((sccode&0x08)!=0) { P2=sccode; if ((P2&0xf8)!=0xf8) break; sccode=(sccode<<1)|0x01; } recode=(P2&0xf8)|0x0f; return(sccode&recode); } } return (KEY_NONE); } 键盘扫描是需要软件去抖的,这没有争议,然而该函数中用软件延时来去抖(ms级别的延时),这是一个维持系统实时性能的一个大忌讳; 一般还有一个判断按键释放的代码: While( kbscan() != KEY_NONE) ; //死循环等待 这样很糟糕,如果把键盘按下一直不放,这将导致整个系统其它的任务也不能执行,这将是个很严重的bug。 有人会这样进行处理: While(kbsan() != KEY_NONE ) { Delay(10); If(Num++ > 10) Break; } 即在一定得时间内,如果键盘一直按下,将作为有效键处理。这样虽然不导致整个系统其它任务不能运行,但也很大程度上,削弱了系统的实时性能,因为他用了延时函数; 我们用两种有效的方法来解决此问题: 1 在按键功能比较简单的情况下,我们仍然用上面的kbscan()函数进行扫描,只是把其中去抖用的软件延时去了,把去抖以及判断按键的释放用一个函数来处理,它不用软件延时,而是用定时器的计时(用一般的计时也行)来完成;代码如下 void ClearKeyFlag(void) { KeyDebounceFlg = 0; KeyReleaseFlg = 0; } void ScanKey(void) { ++KeyDebounceCnt;//去抖计时(这个计时也可以放在后台定时器计时函数中处理) KeyCode = kbscan(); if (KeyCode != KEY_NONE) { if (KeyDebounceFlg)//进入去抖状态的标志位 { if (KeyDebounceCnt > DEBOUNCE_TIME)//大于了去抖规定的时间 { if (KeyCode == KeyOldCode)//按键依然存在,则返回键值 { KeyDebounceFlg = 0; KeyReleaseFlg = 1;//释放标志 return; //Here exit with keycode } ClearKeyFlag(); //KeyCode != KeyOldCode,只是抖动而已 } }else{ if (KeyReleaseFlg == 0) { KeyOldCode = KeyCode; KeyDebounceFlg = 1; KeyDebounceCnt = 0; }else{ if (KeyCode != KeyOldCode) ClearKeyFlag(); } } }else{ ClearKeyFlag();//没有按键则清零标志 } KeyCode = KEY_NONE; } 在按键情况较复杂的情况,如有长按键,组合键,连键等一些复杂功能的按键时候,我们跟倾向于用状态机来实现键盘的扫描; //avr 单片机 中4*3扫描状态机实现 char read_keyboard_FUN2() { static char key_state = 0, key_value, key_line,key_time; char key_return = No_key,i; switch (key_state) { case 0: //最初的状态,进行3*4的键盘扫描 key_line = 0b00001000; for (i=1; i<=4; i++) // 扫描键盘 { PORTD = ~key_line; // 输出行线电平 PORTD = ~key_line; // 必须送2次!!!(注1) key_value = Key_mask & PIND; // 读列电平 if (key_value == Key_mask) key_line <<= 1; // 没有按键,继续扫描 else { key_state++; // 有按键,停止扫描 break; // 转消抖确认状态 } } break; case 1: //此状态来判断按键是不是抖动引起的 if (key_value == (Key_mask & PIND)) // 再次读列电平, { key_state++; // 转入等待按键释放状态 key_time=0; } else key_state--; // 两次列电平不同返回状态0,(消抖处理) break; case 2: // 等待按键释放状态 PORTD = 0b00000111; // 行线全部输出低电平 PORTD = 0b00000111; // 重复送一次 if ( (Key_mask & PIND) == Key_mask) { key_state=0; // 列线全部为高电平返回状态0 key_return= (key_line | key_value);//获得了键值 } else if(++key_time>=100)//如果长时间没有释放 { key_time=0; key_state=3;//进入连键状态 key_return= (key_line | key_value); } break; case 3://对于连键,每隔50ms就得到一次键值,windows xp 系统就是这样做的 PORTD = 0b00000111; // 行线全部输出低电平 PORTD = 0b00000111; // 重复送一次 if ( (Key_mask & PIND) == Key_mask) key_state=0; // 列线全部为高电平返回状态0 else if(++key_time>=5) //每隔50MS为一次连击的按键 { key_time=0; key_return= (key_line | key_value); } break; } return key_return; } 以上用了4个状态,一般的键盘扫描只用前面3个状态就可以了,后面一个状态是为增加“连键”功能设计的。连键——即如果按下某个键不放,则迅速的多次响应该键值,直到其释放。在主循环中每隔10ms让该键盘扫描函数执行一次即可;我们定其时限为10ms,当然要求并不严格。 2 数码管的显示 一般情况下我们用的八位一体的数码管,采用动态扫描的方法来完成显示;非常庆幸人眼在高于50hz以上的闪烁时发现不了的。所以我们在动态扫描数码管的间隔时间是充裕的。这里我们定其时限为4ms(250HZ) ,用定时器定时为2ms,在定时中断程序中进行扫描的显示,每次只显示其中的一位;当然时限也可以弄长一些,更推荐的方法是把显示函数放入主循环中,而定时中断中置位相应的标志位即可; // Timer 0 比较匹配中断服务,4ms定时 interrupt [TIM0_COMP] void timer0_comp_isr(void) { display(); // 调用LED扫描显示 …………………… } void display(void) // 8位LED数码管动态扫描函数 { PORTC = 0xff; // 这里把段选都关闭是很必要的,否则数码管会产生拖影 PORTA = led_7[dis_buff[posit]]; PORTC = position[posit]; if (++posit >=8 ) posit = 0; } 3 串口接收数据帧 串口接收时用中断方式的,这无可厚非。但如果你试图在中断服务程序中完成一帧数据的接收就麻烦大了。永远记住,中断服务函数越短越好,否则影响这个程序的实时性能。一个数据帧一般包括若干个字节,我们需要判断一帧是否完成,校验是否正确。在这个过程中我们不能用软件延时,更不能用死循环等待等方式; 所以我们在串口接收中断函数中,只是把数据放置于一个缓冲队列中。 至于组成帧,以及检查帧的工作我们在主循环中解决,并且每次循环中我们只处理一个数据,每个字节数据的处理间隔的弹性比较大,因为我们已经缓存在了队列里面。 /*========================================== 功能:串口发送接收的时间事件 说明:放在大循环中每10ms一次 输出:none 输入:none ==========================================*/ void UARTimeEvent(void) { if (TxTimer != 0)//发送需要等待的时间递减 --TxTimer; if (++RxTimer > RX_FRAME_RESET) // RxCnt = 0; //如果接受超时(即不完整的帧或者接收一帧完成),把接收的不完整帧覆盖 } /*========================================== 功能:串口接收中断 说明:接收一个数据,存入缓存 输出:none 输入:none ==========================================*/ interrupt [USART_RXC] void uart_rx_isr(void) { INT8U status,data; status = UCSRA; data = UDR; if ((status & (FRAMING_ERROR | PARITY_ERROR | DATA_OVERRUN))==0){ RxBuf[RxBufWrIdx] = data; if (++RxBufWrIdx == RX_BUFFER_SIZE) //接收数据于缓冲中 RxBufWrIdx = 0; if (++RxBufCnt == RX_BUFFER_SIZE){ RxBufCnt = 0; //RxBufferOvf=1; } } } /*========================================== 功能:串口接收数据帧 说明:当非0输出时,收到一帧数据 放在大循环中执行 输出:==0:没有数据帧 !=0:数据帧命令字 输入:none ==========================================*/ INT8U ChkRxFrame(void) { INT8U dat; INT8U cnt; INT8U sum; INT8U ret; ret = RX_NULL; if (RxBufCnt != 0){ RxTimer = 0; //清接收计数时间,UARTimeEvent()中对于接收超时做了放弃整帧数据的处理 //Display(); cnt = RxCnt; dat = RxBuf[RxBufRdIdx]; // Get Char if (++RxBufRdIdx == RX_BUFFER_SIZE) RxBufRdIdx = 0; Cli(); --RxBufCnt; Sei(); FrameBuf[cnt++] = dat; if (cnt >= FRAME_LEN)// 组成一帧 { sum = 0; for (cnt = 0;cnt < (FRAME_LEN - 1);cnt++) sum+= FrameBuf[cnt]; if (sum == dat) ret = FrameBuf[0]; cnt = 0; } RxCnt = cnt; } return ret; } 以上的代码ChkRxFrame()可以放于串口接收数据处理函数RxProcess() 中,然后放入主循环中执行即可。以上用一个计时变量RxTimer,很微妙的解决了接收帧超时的放弃帧处理,它没有用任何等待,而且主循环中每次只是接收一个字节数据,时间很短。 我们开始架构整个系统的框架: 我们选用一个系统不常用的TIMER来产生系统所需的系统基准节拍,这里我们选用4ms; 在meg8中我们代码如下: // Timer 0 overflow interrupt service routine interrupt [TIM0_OVF] void timer0_ovf_isr(void) { // Reinitialize Timer 0 value TCNT0=0x83; // Place your code here if ((++Time1ms & 0x03) == 0) TimeIntFlg = 1; } 然后我们设计一个TimeEvent()函数,来调用一些在以指定的频率需要循环调用的函数, 比如每个4ms我们就进行喂狗以及数码管动态扫描显示,每隔1s我们就调用led闪烁程序,每隔20ms我们进行键盘扫描程序; void TimeEvent (void) { if (TimeIntFlg){ TimeIntFlg = 0; ClearWatchDog(); display(); // 在4ms事件中,调用LED扫描显示,以及喂狗 if (++Time4ms > 5){ Time4ms = 0; TimeEvent20ms();//在20ms事件中,我们处理键盘扫描read_keyboard_FUN2() if (++Time100ms > 10){ Time100ms = 0; TimeEvent1Hz();// 在1s事件中,我们使工作指示灯闪烁 } } UARTimeEvent();//串口的数据接收事件,在4ms事件中处理 } } 显然整个思路已经很清晰了,cpu需要处理的循环事件都可以根据其对于时间的要求很方便的加入该函数中。但是我们对这事件有要求: 执行速度快,简短,不能有太长的延时等待,其所有事件一次执行时间和必须小于系统的基准时间片4ms(根据需要可以加大系统基准节拍)。所以我们的键盘扫描程序,数码管显示程序,串口接收程序都如我先前所示。如果逼不得已需要用到较长的延时(如模拟IIc时序中用到的延时) 我们设计了这样的延时函数: void RunTime250Hz (INT8U delay)//此延时函数的单位为4ms(系统基准节拍) { while (delay){ if (TimeIntFlg){ --delay; TimeEvent(); } TxProcess(); RxProcess(); } } 我们需要延时的时间=delay*系统记住节拍4ms,此函数就确保了在延时的同时,我们其它事件(键盘扫描,led显示等)也并没有被耽误; 好了这样我们的主函数main()将很简短: Void main (voie) { Init_all(); while (1) { TimeEvent(); //对于循环事件的处理 RxProcess(); //串口对接收的数据处理 TxProcess();// 串口发送数据处理 } } 整体看来我们的系统就成了将近一个万能的模版了,根据自己所选的cpu,选个定时器,在添加自己的事件函数即可,非常灵活方便实用,一般的单片机能胜任的场合,该模版都能搞定。 整个系统以全局标志作为主线,形散神不散;系统耗费比较小,只是牺牲了一个Timer而已,在资源缺乏的单片机中,非常适;曾经看过一个网友的模版“单片机实用系统”,其以51为例子写的,整体思路和这个差不多,不过他写得更为规范紧凑,非常欣赏;但个人觉得代码开销量要大些,用惯了都一样哦。但是由于本系统以全局标志为驱动事件,所以比较感觉比较凌乱,全局最好都做好注释,而其要注意一些隐形的函数递归情况,千万不要递归的太深哦(有的单片机不支持)。 http://bbs.21ic.com/icview-237577-1-1.html

    时间:2018-07-06 关键词: 单片机

  • 单片机与ARM的区别和选择?

     1 软件方面 这应该是最大的区别了。引入了操作系统。为什么引入操作系统?有什么好处? 1、方便。主要体现在后期的开发,即在操作系统上直接开发应用程序。不像单片机一样一切都要重新写。前期的操作系统移植工作,还是要专业人士来做。 2、安全。这是LINUX的一个特点。LINUX的内核与用户空间的内存管理分开,不会因为用户的单个程序错误而引起系统死掉。这在单片机的软件开发中没见到过。 3、高效。引入进程的管理调度系统,使系统运行更加高效。在传统的单片机开发中大多是基于中断的前后台技术,对多任务的管理有局限性。 2 硬件方面 现在的8位单片机技术硬件发展的也非常得快,也出现了许多功能非常强大的单片机。但是与32arm相比还是有些差距吧。 1、arm芯片大多把SDRAM、LCD等控制器集成到片子当中。在8位机,大多要进行外扩。 2、单片机是个微控制器,arm显然已经是个微处理器了。 3、引入嵌入式操作系统之后,可以实现许多单片机系统不能完成的功能。比如:嵌入式web服务器,java虚拟机等。也就是说,有很多免费的资源可以利用,上述两种服务就是例子。如果在单片机上开发这些功能可以想象其中的难度。 3 初学者如何选择ARM开发硬件? 1、 如果你有做硬件和单片机的经验,建议自己做个最小系统板:假如你从没有做过ARM的开发,建议你一开始不要贪大求全,把所有的应用都做好,因为ARM的启动方式和dsp或单片机有所不同,往往会碰到各种问题,所以建议先布一个仅有Flash、SRAM或SDRAM、CPU、JTAG、和复位信号的小系统板,留出扩展接口。使最小系统能够正常运行,你的任务就完成了一半,好在arm的外围接口基本都是标准接口,假如你已有这些硬件的布线经验,这对你来讲是一件很轻易的事情。 2、动手写启动代码,根据硬件地址先写一个能够启动的小代码,包括以下部分: 初始化端口,屏蔽中断,把程序拷贝到SRAM中 完成代码的重映射 配置中断句柄,连接到C语言入口。 也许你看到给你的一些示例程序当中,bootloader会有很多东西,但是不要被这些复杂的程序所困扰,因为你不是做开发板的,你的任务就是做段小程序,让你的应用程序能够运行下去。 3、假如你是作硬件,每个厂家基本上都有针对该芯片的DEMO板原理图。先将原理图消化。这样你以后做设计时,对资源的分配心中有数。器件的DATSHEET一定要好好消化。 4、 仔细研究你所用的芯片的资料,尽管arm在内核上兼容,但每家芯片都有自己的特色,编写程序时必须考虑这些问题。尤其是女孩子,在这儿千万别有依靠心理,总想拿别人的示例程序修改,却越改越乱。 5、多看一些操作系统程序,在arm的应用开放源代码的程序很多,要想提高自己,就要多看别人的程序,Linux、uCos-II等等这些都是很好的原码。 6、假如做软件最好对操作系统的机理要有所了解。当然这对软件工程师来说是小菜一碟。但假如是硬件出身的就有点费劲。 7、最好买一块现成的ARM板,学嵌入式系统的话,选择ARM9、Cortex-A8等板子,这样学习效率会大大提高,也会比自己做板子省成本。

    时间:2018-07-06 关键词: ARM 单片机

  • 关于程序效率的问题,你有思考过吗?

     for(;;) { void* buffer = malloc(SIZE); memset(buffer,SIZE); process(buffer) free buffer; } 这是一位实习生(我曾带过10+位实习生,因此见多识广)的伪代码,原本这个SIZE很小,估计是存放URL用的,定义为512字节,后来由于某种原因,扩大到了1M,从512字节扩大到了1M,速度变慢很多。为什么呢?这位同学无法解释,但我让他继续探索,找到真正的原因。 我让他从这样几个方面入手, (1)首先分析一些主要花费时间的代码,结果发现是memset这一段从512到1M后耗费时间增多,而且增多并不是线性的,我让他先看一下glibc的memset源代码,如下: #if defined _LIBC || defined STDC_HEADERS || defined USG # include # define flood memset #else static void flood (__ptr_t, int, __malloc_size_t); static void flood (ptr, val, size) __ptr_t ptr; int val; __malloc_size_t size; { char *cp = ptr; while (size--) *cp++ = val; } #endif 由此可知memset是每字节每字节的赋值的,这并不是机器喜欢的方式,机器希望的是在4字节对齐的位置上进行操作(32位机器,64位机器喜欢8字节对齐),一次读取32位(4个字节)。因此memset完全可以自己实现一个一次性写4个字节的代码。 (2)接下来需要探索的是malloc,事实上linux内存分配有两种,brk,mmap,前者分配128k以内的内存,后者分配128k以上的内存,在改成1M后, void* buffer = malloc(SIZE); 这一段是很快的,因为只是分配了虚存,并没有载入内存,可以查看/proc/pid/statm,考察内存分配,memset操作前后的变化。 而memset,就需要进行实际的内存分配,缺页中断,加载TLB等等。 而brk分配的内存是glibc管理的内存,分配很快,释放也方便(很多时候其实并不释放)。因此512字节是,使用的brk分配(效率很高),而变成1M后,使用mmap分配(加上memset的低效)因此效率要低很多。 (3) 这段代码如果改成,效果等价性能也会大幅度提升。 void* buffer = malloc(SIZE); for(;;) { memset(buffer,SIZE); process(buffer) } free buffer; (4)最后需要质疑的是为什么需要开辟1M大小的空间,是否通过了验证,这样做是否有必要,实际情况是怎样的,memset是否需要,是否可以通过什么其他方法来避免这种计算。 由此可见,很多问题,不好的编码习惯,对机器理解的不够透彻是很难再一般的工作中发现,必须在大规模数据处理的实践场合(处理数据量足够大),才能体现出来,因此大规模数据处理技术是软件、硬件相结合的技术,而且不仅仅是技术上的问题还包括了业务上的问题,废代码,废计算应该去掉,不合理的计算应该变得合理。

    时间:2018-06-25 关键词: 程序员

  • 堆栈溢出技术从入门到精通

     虽然溢出在程序开发过程中不可完全避免,但溢出对系统的威胁是巨大的,由于系统的特殊性,溢出发生时攻击者可以利用其漏洞来获取系统的高级权限root,因此本文将详细介绍堆栈溢出技术…… 在您开始了解堆栈溢出前,首先你应该了解win32汇编语言,熟悉寄存器的组成和功能。你必须有堆栈和存储分配方面的基础知识,有关这方面的计算机书籍很多,我将只是简单阐述原理,着重在应用。其次,你应该了解linux,本讲中我们的例子将在linux上开发。 1、首先复习一下基础知识。 从物理上讲,堆栈是就是一段连续分配的内存空间。在一个程序中,会声明各种变量。静态全局变量是位于数据段并且在程序开始运行的时候被加载。而程序的动态的局部变量则分配在堆栈里面。 从操作上来讲,堆栈是一个先入后出的队列。他的生长方向与内存的生长方向正好相反。我们规定内存的生长方向为向上,则栈的生长方向为向下。压栈的操作push=ESP-4,出栈的操作是pop=ESP+4.换句话说,堆栈中老的值,其内存地址,反而比新的值要大。请牢牢记住这一点,因为这是堆栈溢出的基本理论依据。 在一次函数调用中,堆栈中将被依次压入:参数,返回地址,EBP。如果函数有局部变量,接下来,就在堆栈中开辟相应的空间以构造变量。函数执行结束,这些局部变量的内容将被丢失。但是不被清除。在函数返回的时候,弹出EBP,恢复堆栈到函数调用的地址,弹出返回地址到EIP以继续执行程序。 在C语言程序中,参数的压栈顺序是反向的。比如func(a,b,c)。在参数入栈的时候,是:先压c,再压b,最后a。在取参数的时候,由于栈的先入后出,先取栈顶的a,再取b,最后取c。这些是汇编语言的基础知识,用户在开始前必须要了解这些知识。 2、现在我们来看一看什么是堆栈溢出。 运行时的堆栈分配 堆栈溢出就是不顾堆栈中数据块大小,向该数据块写入了过多的数据,导致数据越界,结果覆盖了老的堆栈数据。 例如程序一:       #include   int main ( )  {  char name[8];  printf("Please type your name: ");  gets(name);  printf("Hello, %s!", name);  return 0;  } 编译并且执行,我们输入ipxodi,就会输出Hello,ipxodi!。程序运行中,堆栈是怎么操作的呢? 在main函数开始运行的时候,堆栈里面将被依次放入返回地址,EBP。 我们用gcc -S 来获得汇编语言输出,可以看到main函数的开头部分对应如下语句:       pushl %ebp  movl %esp,%ebp  subl $8,%esp 首先他把EBP保存下来,,然后EBP等于现在的ESP,这样EBP就可以用来访问本函数的局部变量。之后ESP减8,就是堆栈向上增长8个字节,用来存放name[]数组。最后,main返回,弹出ret里的地址,赋值给EIP,CPU继续执行EIP所指向的指令。 堆栈溢出 现在我们再执行一次,输入ipxodiAAAAAAAAAAAAAAA,执行完gets(name)之后,由于我们输入的name字符串太长,name数组容纳不下,只好向内存顶部继续写‘A’。由于堆栈的生长方向与内存的生长方向相反,这些‘A’覆盖了堆栈的老的元素。 我们可以发现,EBP,ret都已经被‘A’覆盖了。在main返回的时候,就会把‘AAAA’的ASCII码:0x41414141作为返回地址,CPU会试图执行0x41414141处的指令,结果出现错误。这就是一次堆栈溢出。 3、如何利用堆栈溢出 我们已经制造了一次堆栈溢出。其原理可以概括为:由于字符串处理函数(gets,strcpy等等)没有对数组越界加以监视和限制,我们利用字符数组写越界,覆盖堆栈中的老元素的值,就可以修改返回地址。 在上面的例子中,这导致CPU去访问一个不存在的指令,结果出错。事实上,当堆栈溢出的时候,我们已经完全的控制了这个程序下一步的动作。如果我们用一个实际存在指令地址来覆盖这个返回地址,CPU就会转而执行我们的指令。 在UINX/linux系统中,我们的指令可以执行一个shell,这个shell将获得和被我们堆栈溢出的程序相同的权限。如果这个程序是setuid的,那么我们就可以获得root shell。下一讲将叙述如何书写一个shell code。 如何书写一个shell code 一:shellcode基本算法分析 在程序中,执行一个shell的程序是这样写的:       shellcode.c  ------------------------------------------------------------------------  #include   void main() {  char *name[2];  name[0] = "/bin/sh"  name[1] = NULL;  execve(name[0], name, NULL);  }  ------------------------------------------------------------------------ execve函数将执行一个程序。他需要程序的名字地址作为第一个参数。一个内容为该程序的argv[i](argv[n-1]=0)的指针数组作为第二个参数,以及(char*) 0作为第三个参数。 我们来看以看execve的汇编代码:       [nkl10]$Content$nbsp;gcc -o shellcode -static shellcode.c  [nkl10]$Content$nbsp;gdb shellcode  (gdb) disassemble __execve  Dump of assembler code for function __execve:  0x80002bc <__execve>: pushl %ebp ;  0x80002bd <__execve+1>: movl %esp,%ebp  ;上面是函数头。  0x80002bf <__execve+3>: pushl %ebx  ;保存ebx  0x80002c0 <__execve+4>: movl $0xb,%eax  ;eax=0xb,eax指明第几号系统调用。  0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx  ;ebp+8是第一个参数"/bin/sh\0"  0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx  ;ebp+12是第二个参数name数组的地址  0x80002cb <__execve+15>: movl 0x10(%ebp),%edx  ;ebp+16是第三个参数空指针的地址。  ;name[2-1]内容为NULL,用来存放返回值。  0x80002ce <__execve+18>: int $0x80  ;执行0xb号系统调用(execve)  0x80002d0 <__execve+20>: movl %eax,%edx  ;下面是返回值的处理就没有用了。  0x80002d2 <__execve+22>: testl %edx,%edx  0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42>  0x80002d6 <__execve+26>: negl %edx  0x80002d8 <__execve+28>: pushl %edx  0x80002d9 <__execve+29>: call 0x8001a34  <__normal_errno_location>  0x80002de <__execve+34>: popl %edx  0x80002df <__execve+35>: movl %edx,(%eax)  0x80002e1 <__execve+37>: movl $0xffffffff,%eax  0x80002e6 <__execve+42>: popl %ebx  0x80002e7 <__execve+43>: movl %ebp,%esp  0x80002e9 <__execve+45>: popl %ebp  0x80002ea <__execve+46>: ret  0x80002eb <__execve+47>: nop  End of assembler dump. 经过以上的分析,可以得到如下的精简指令算法:       movl $execve的系统调用号,%eax  movl "bin/sh\0"的地址,%ebx  movl name数组的地址,%ecx  movl name[n-1]的地址,%edx  int $0x80 ;执行系统调用(execve) 当execve执行成功后,程序shellcode就会退出,/bin/sh将作为子进程继续执行。可是,如果我们的execve执行失败,(比如没有/bin/sh这个文件),CPU就会继续执行后续的指令,结果不知道跑到哪里去了。所以必须再执行一个exit()系统调用,结束shellcode.c的执行。 我们来看以看exit(0)的汇编代码:       (gdb) disassemble _exit  Dump of assembler code for function _exit:  0x800034c <_exit>: pushl %ebp  0x800034d <_exit+1>: movl %esp,%ebp  0x800034f <_exit+3>: pushl %ebx  0x8000350 <_exit+4>: movl $0x1,%eax ;1号系统调用  0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx ;ebx为参数0  0x8000358 <_exit+12>: int $0x80 ;引发系统调用  0x800035a <_exit+14>: movl 0xfffffffc(%ebp),%ebx  0x800035d <_exit+17>: movl %ebp,%esp  0x800035f <_exit+19>: popl %ebp  0x8000360 <_exit+20>: ret  0x8000361 <_exit+21>: nop  0x8000362 <_exit+22>: nop  0x8000363 <_exit+23>: nop  End of assembler dump. 看来exit(0)〕的汇编代码更加简单:    movl $0x1,%eax ;1号系统调用  movl 0,%ebx ;ebx为exit的参数0  int $0x80 ;引发系统调用 那么总结一下,合成的汇编代码为:       movl $execve的系统调用号,%eax  movl "bin/sh\0"的地址,%ebx  movl name数组的地址,%ecx  movl name[n-1]的地址,%edx  int $0x80 ;执行系统调用(execve)  movl $0x1,%eax ;1号系统调用  movl 0,%ebx ;ebx为exit的参数0  int $0x80 ;执行系统调用(exit)

    时间:2018-06-25 关键词: 堆栈

  • 嵌入式C语言编程小知识总结

     1. 流水线被指令填满时才能发挥最大效能,即每时钟周期完成一条指令的执行(仅指单周期指令)。如果程序发生跳转,流水线会被清空,这将需要几个时钟才能使流水线再次填满。因此,尽量少的使用跳转指令可以提高程序执行效率,解决发案就是尽量使用指令的“条件执行”功能。 2. 在LPC2200系列中: 可以通过过下面的程序延迟10毫秒: for(i=0;i<200;i++) { for(j=0;j<200;j++); } 3. 同过下面语句将一个16位的变量放在两个8位的变量中。 //IP数据报总长度高字节 IpHeadUint8[10]=(IpHead.e_ip.Crc&0xff00)>>8; //IP数据报总长度低字节 IpHeadUint8[11]=IpHead.e_ip.Crc&0x00ff; 4. 在对全部数组元素赋初值时,可以不指定数组长度。 eg;inta[]={1,2,3,4,5}; 但如果当输出第a[5]以上的元素时,系统回输出随机数值,所以使用此方法时,不能使用超过初始值元素以上的元素。 5. 由于ADS先天性的对printf不支持;因此不便于我们调试,可以利用串口输出来代替printf来调试。 6. 用或运算,可使某位置为1,其它位不变 eg: PINSEL0 |= 0x00000005; //设置串口引脚 使第0位和第二位置一,其他位不变。 7. 函数指针 1> C语言中函数名直接对应于函数生成的指令代码在内存中的地址,因此函数名可以直接赋给指向函数的指针 2> 调用函数实际上等同于“调用指令+参数传递处理+回归位置入栈”,本质上最核心的操作是将函数生成的目标代码的首地址赋给CPU的PC寄存器。 3> 因为函数调用的本质是跳转到某一个地址单元的code去执行,所以可以“调用一个根本就不存在在函数实体 4> int (*p)();定义p是一个指向函数的指针变量,次函数返回带回整型的返回值。*P两侧的括号不能省略,表示p先于*结合,是指针变量,然后再与后面的()结合,表示此指针指向函数。 区别:int *p()表示这个函数的返回值是指向整型变量的指针。 说明: (1) 指向函数的指针变量的一般定义形式为: 数据类型 (*指针变量名)(); 1> 此处的“数据类型”是指函数返回值的类型 (2) 返回指针值的函数: 类型名 *函数名(参数表) eg: int * func(int x,int y) func是函数名,调用它以后能返回一个指向整型数据的指针。x,y是func的形参。 区别方法: a.从右往左找第一个括号,括号里面的是函数的形参。 b.括号外面的第一个标识符是函数的名字,函数前面的表示函数的返回数值。 8. 数组指针 1>int (*p)[4] 表示*p有4个元素,每个元素为整型。也就是p所指的对象有4个整型元素的数组,既P是行指针。 2> 指针数组 Ø 一个数组,其元素均为指针类型数据,称为指针数组;即指针数组中的每一个元素都相当于一个指针变量。 Ø 一维指针数组的定义形式为: 类型名 *数组名[数组长度] eg:int *p[4]: 作用:它用于指向若干个字符串,使字符串处理更加方便灵活。适用于一个二维字符串数组,其中每一行的字符数组的长度各不相同 eg:char * name[]={“Follow me”,”BASIC”,”GreatWall”}; 9. 结构体 1> 可以用结构体变量做实参。但是用结构体变量作实参时,采取的是“值传递”的方式,将结构体变量所占的内存单元的内容全部顺序递给形参。形参也必须是同类型的结构体变量。 eg:pint(su);//注在此处su为结构体 注:这种传递方式在空间和时间上开销较大,如果结构体的规模较大时,开销是很可观的。 2> 用直向结构体变量(或数组)的指针作实参,将结构体变量(或数组)的地址传给形参 eg:print(&su);//注在此处su为结构体 10. 共用体 1> 共用体把几种不同数据类型的变量存放在同一块内存里。公用体中的变量共享同一块内存。 2> 定义公用体类型变量的一般形式为: union 共用体名 { 成员列表; }变量列表; 3>在共用体中同一块内存可以用来存放几种不同类型的数据,但在某一时刻只能在其中存放一个成员变量。共用体变量中起作用的成员是最后一次存入的数据。 eg: union data { int i; char c; double d; }; union data a; 共用体变量a中的成员i,c,d三个变量在内存中从同一个地址开始存储。如进行如下赋值: a.i = 100; a.c = ‘A’; 那么此时共用体变量a中的成员i已经没有值了,因为存储该值的内存现在已经被用来存储成员c的值了。 3> 共用体变量的长度取决于其成员的最大长度: 说明: ² 结构体变量所占内存的长度是各个成员的总和,每个成员分别占有自己的存储空间。共用体变量所占内存的长度是其最长成员的长度。当然,编译器出于提高访问效率的目的,在编译分配存储空间时往往要进行对齐操作。 ² 对齐操作以最大基本类型为准。即以最大基本类型为基本单元。若按实际算下的长度不是基本单元的整数倍,则其实际长度应该是基本单元的整数倍。 (在TurboC中不进行对齐,在linux中进行对齐) 11. CPU字长与存储器位宽不一致处理 例如:使用共用体来解决这一冲突: union send_temp{ uint16 words; uint8 bytes[2]; }send_buff; eg:send_buff.bytes[0]=a;//此处a 是8位 send_buff.bytes[1]=b;//此处 b 是8位; 此时就将8位字拼成了16位字存储了。 发送时send(send_buff.words)就可以每次发送一个16位的数据了。 12. C语言符号优先级: 1>复合赋值运算符号: a+=3*5; 等价于a=a+(3*5); 13.一个常见的调试策略是把一些printf函数的调用散布于程序中,确定错误出现的具体位置。但是,这些函数调用的输出结果被写入到缓冲区中,并不立即显示于屏幕上。事实上,如果程序失败,缓冲输去可能不会被实际写入,因此得到的错误位置就是错误的。解决的方法是在每个用于调试的printf函数之后立即调用fflush函数即可得到。 printf(“something or other”); fflush(stdout); 14.关键字volatile的用法 volatile变量可能用于如下几种情况: 1>设备的硬件寄存器(如:状态寄存器) 2>一个中断服务子程序中会访问到的全局变量 3>多线程应用中被几个任务共享的变量。 15.关键字register的用法: 当对一个变量频繁被读写时,需要反复访问内存,从而花费大量的存取时间。为此,C语言提供了一种变量,即寄存器变量。这种变量存放在CPU的寄存器中,使用时,不需要访问内存,而直接从寄存器中读写,从而提高效率。寄存器变量的说明符是register。对于循环次数较多的循环控制变量及循环体内反复使用的变量均可定义为寄存器变量,而循环计数是应用寄存器变量的最好候选者。 (1) 只有局部自动变量和形参才可以定义为寄存器变量。因为寄存器变量属于动态存储方式,凡需要采用静态存储方式的量都不能定义为寄存器变量,包括:模块间全局变量、模块内全局变量、局部static变量; (2) register是一个"建议"型关键字,意指程序建议该变量放在寄存器中,但最终该变量可能因为条件不满足并未成为寄存器变量,而是被放在了存储器中,但编译器中并不报错(在C++语言中有另一个"建议"型关键字:inline)。 14对于程序代码,已经被烧录在FLASH或ROM中,我们可以让CPU直接从其中读取代码执行,但通常这不是一个好办法,我们最好在系统启动后将FLASH或ROM中的目标代码拷贝入RAM中后再执行以提高取指令速度; CPU对各种存储器的访问速度,基本上是: CPU内部RAM > 外部同步RAM > 外部异步RAM > FLASH/ROM 15. 宏定义 在C语言中,宏是产生内嵌代码的唯一方法。对于嵌入式系统而言,为了能达到性能要求,宏是一种很好的代替函数的方法。 1>宏定义“像”函数; 2>宏定义不是函数,因而需要括上所有“参数”; 3>宏定义可能产生副作用。因而不要给宏定义传入有副作用的"参数"。

    时间:2018-06-25 关键词: 嵌入式 C语言

  • C语言中随机函数应用

    前言:有些程序每次执行时不应该产生相同的结果,如游戏和模拟,此时随机数就非常有用。下面这两个函数合在一起使用能够产生伪随机数(pseudo-random number):通过计算产生的随机数,可能有重复出现,所以并不是真正的随机数。 int rand(void); void srand(unsigned int seed); 1>rand返回一个范围在0和RAND_MAX(至少为32767)之间的伪随机数。当它重复调用时,函数返回这个范围内其他数。为了得到一个更小范围的伪随机数,首先把这个函数的返回值根据所需范围的大小进行取模,然后通过加上或减去一个偏移量对它进行调整。 2>为了避免程序每次运行时获得相同的随机数序列,可以调用srand函数。它用它的参数值对随机数发生器进行初始化。 一个常用的技巧是使用每天的时间作为随机数产生器的种子(seed),例如: srand((unsigned int )time(0)); 2.实例: 程序一: #include #include int main() { int k; k=rand(); printf("%d\n", k); return 0; } 大家可以把以上的代码编译运行一下,发现他的确产生随机数了,但是你会发现,每次运行程序产生的随机数都是一样的,为什么呢?因为随机数取在C语言中采用的是固定序列,所以每次执行所取的是同一个数。 那么如何写一个程序,让它每次运行时产生的随机数都不一样呢? 请看下面的例子: #include #include #include Int main(void) { int i; srand((unsigned int )time(0)); printf("Tenrandom numbers from 0 to 99\n\n"); for (i=0; i<10; i++) printf("%d\n", rand()%100); return 0; } 这时运行程序,会发现每次产生的随机数都不一样。 那么为什么第一个程序一样而第二个程序不一样呢? 第二个程序用到了一个新的函数srand,这个函数是给随机数产生一个随机种子(seed),函数原型是srand( (unsigned) time(NULL)); time的值每时每刻都不同。所以种子不同,所以,产生的随机数也不同。 所以说,要想产生不同的随机数,在使用rand之前需要先调用srand 由于rand产生的随机数从0到rand_max,而rand_max是一个很大的数,那么如何产生从X~Y的数呢? 从X到Y,有Y-X+1个数,所以要产生从X到Y的数,只需要这样写: k=rand()%(Y-X+1)+X; 这样,就可以产生你想要的任何范围内的随机数了。

    时间:2018-06-25 关键词: C语言

  • C语言复习之结构体基础知识

     一.基础知识 1.聚合数据类型(aggregate data type)能够同时存储超过一个的单独数据。C提供了两种类型的聚合数据类型,数组和结构。 (1)数组是相同类型的元素的集合,它的每个元素是通过下标引用或指针间接访问来选择的。 (2)结构也是一些值的集合,这些值称为它的成员(member),但一个结构的各个成员可能具有不同的类型。 2.数组元素可以通过下标访问,这只是因为数组的元素长度相同。 3.由于一个结构的成员可能长度不同,所以不能使用小标来访问它们。相反,每个结构成员都有自己的名字,它们是通过名字访问的。 4.结构并不是一个它自身成员的数组。和数组名不同,当一个结构变量在表达式中使用时,它并不被置换成一个指针。结构变量也无法使用下标来选择特定的成员。 5.结构变量属于标量类型,结构也可以作为传递给函数的参数,它们也可以作为返回值从函数返回,相同类型的结构变量相互之间可以赋值。 6.可以声明指向结构的指针,取一个结构变量的地址,也可以声明结构数组。 二.结构声明 1.在声明结构时,必须列出它包含的所有成员。该列表包括每个成员的类型和名字。 eg: struct tag{ member-list; }variable-list; 结构体声明由三部分组成,tag,member-list,variable-list。所有可选部分不能全部省略---它们至少出现两个。 1>例子: struct { int a; char b; float c; }x; 这个声明创建了一个名叫x的变量,它包含三个成员:一个整数、一个字符和一个浮点数。 struct { int a; char b; float c; }y[20],*z; 这个声明创建了y和z。y是一个数组,它包含了20个结构。Z是一个指针,它指向这个类型的结构。 2>说明: 以上两个声明被编译器当作两种截然不同的类型,即使它们的成员列表完全相同。因此,变量y和z的类型和x的类型不同,所以下面这条语句。 z = &x;是非法的 3>但是,这是不是意味着某种特定类型的所有结构都必须使用一个单独的声明来创建呢。其实不然,标签(tag)字段允许为成员列表提供一个名字。 eg: struct SIMPLE { int a; char b; float c; }; 这个声明把标签SIMPLE和这个成员列表联系在一起。该声明并没有提供变量列表,所以它并未创建任何变量。 ² 标签标识了一种模式,用于声明未来的变量,但无论是标签还是模式本身都不是变量。 eg struct SIMPLE x; struct SIMPLE y[20],*z; 这些声明使用标签来创建变量。它们创建和前面的例子是一样的,不同的是:现在x,y和z都是同一种类型的结构变量。 2.声明结构时可以使用的另一种良好技巧是用typedef创建一种新的类型。 typedef struct { int a; char b; float c; } Simple; 这个技巧和声明一个结构标签的效果几乎相同。区别在于:Simple现在是个类型名而不是个结构标签,所以后续的声明可能像下面: Simple x; Simple y[20],*z; 注:如果想在多个源文件中使用同一种类型的结构,你应该把标签声明或typedef形式的声明放在一个头文件中。当源文件需要使用这个声明时可以使用#include指令把该头文件包含进来。 3.结构成员 1>结构成员可以是任何变量。结构成员可以是标量,数组,指针或者是其他结构。 2>一个结构的成员的名字可以和其他结构的成员的名字相同。并不会产生冲突。 三.结构成员的访问 1.结构成员的直接访问 结构变量的成员是通过点操作符号(.)访问的。点操作符接受两个操作数,左操作数就是结构变量的名字,右操作数就是需要访问的成员的名字。这个表达式的结果就是指定的成员。 2.结构体成员的间接访问 如果你拥有一个指向结构的指针,我们使用->操作符(箭头操作符)和点操作符一样,箭头操作符对左操作符执行间接访问取得指针所指向的结构,然后和点操作符一样,根据右操作数选择一个指定的结构成员。 3.结构的自引用 在一个结构内部包含一个类型为该结构本身的成员是否是合法呢? Eg: struct SELF_REF1 { int a; struct SELF_REF1 b; int c; }; 该中类型的应用是非法的,因为成员b是另一个完整的结构,其内部还将包含它自己的成员b。这第2个成员又是另一个完整的结构,它还将包含它自己的成员b。这样就会永无止境。 1>下面的方法是合法的 struct SELF_REF2 { int a; struct SELF_REF2 *b; int c; }; 这个声明和前面的声明区别在于b现在是一个指针而不是结构。编译器在结构的长度确定之前就已经知道指针的长度。所以该中类型的自引用是合法的。 2>以下是个错误的用法 typedef struct { int a; SELF_REF3 *b; int c; }SELF_REF3 该声明的目的是为这个结构创建类型名SELF_REF3。但是,它是错误的,类型名直到声明的末尾才定义,所以在结构声明的内部它尚未定义。 使用一个结构标签来声明b,如下所示: typedef struct SELF_REF3_TAG { int a; struct SELF_REF3_TAG *b; int c; }SELF_REF3; 4.不完整的声明 有时候,你必须声明一些相互之间存在依赖的结构。即:其中一个结构包含了另一个结构的一个成员或多个成员。和自引用一样,至少有一个结构必须在另一个结构体内部以指针的形式存在。问题在于声明部分:如果每个结构都引用了其他结构的标签,哪个结构应该首先被声明呢? 1>该问题采用不完整声明来解决。它声明一个作为结构标签的标识符。然后,把这个标签用在不需要知道这个结构的长度的声明中,如声明指向这个结构的指针。接下来的声明把这个标签与成员列表联系在一起。 2>看下面的例子,两个不同类型的结构内部都有一个指向另一个结构的指针。 struct B; struct A { struct B *partner; /*other declarations*/ }; struct B { struct A *partner; /*other declarations*/ }; 在A成员列表中需要标签B的不完整的声明。一旦A被声明之后,B的成员列表也可以被声明。 四结构的初始化 1.结构的初始化方式和数组的初始化方式很相似。一个位于一对花括号内部、由逗号分隔的初始值列表可用于结构各个成员的初始化。这些值根据结构成员列表的顺序写出。如果初始列表的值不够,剩余的结构成员将使用缺省值进行初始化。 2.结构中如果包含数组或结构成员,其初始化方式类似于多维数组的初始化。一个完整的聚合类型成员的初始值列表可以嵌套于结构的初始值列表内部。 eg: struct INIT_EX { int a; short b[10]; Simple c; }x = { 10; {1,2,3,4,5}, {25,’x’,1.9} };

    时间:2018-06-25 关键词: C语言

  • 编译C程序有很多步骤,其中第一步为预处理(preprocessing)阶段

     一.前言 1.编译一个C程序涉及很多步骤。其中第一步骤称为预处理(preprocessing)阶段。C预处理器(preprocessor)在源代码编译之前对其进行文本性质的操作。 2.它的主要任务包括删除注释、插入被#include指令包含的内容、定义和替换由#define指令定义的符号以及确定代码的部分内容是否应该根据一些条件编译指令经行编译。 二.预定义符号 1.以下为预处理器定义的符号。它们的值或者是字符串常量,或者是十进制数字常量。 2.__FILE__和__LINE__在确认调试输出时很有用。__DATE__和__TIME__常常用于在被编译的程序中加入版本信息。 3.__STDC__用于那些在ANSI环境和非ANSI环境都必须进行编译的程序中结合条件编译。 注: 此处的前缀是两个下划线. ² __FILE__:用%s进行输出,输出结果为源程序名。 ² __LINE__:用%d进行输出,输出结果为文件当前行号。 ² __DATE__:用%s进行输出,输出结果为文件被编译的日期 ² __STDC__:用%d进行输出,如果编译器遵循ANSIC,其数值为1。否则未定义。 三.#define 1.#define的用法: #define name stuff 有了这条指令以后,每当有符号name出现在这条指令后面时,预处理器就会把它替换成stuff。 2.替换文本并不仅限于数值字面值常量。使用#define指令,可以把文本替换到程序中。 3.如果定义中的stuff非常长,可以将其分成几行,除了最后一行之外,每行的末尾都要加一个反斜杠。 Eg: #define DEBUG_PRINT printf(“File %s line%d:” \ ”x=%d,y=%d,z=%d”,\ __FILE__,__LINE__,\ x,y,z) 说明:此处利用了相邻的字符串常量被自动连接为一个字符串的这个特性。 4.在宏定义的末尾不要加上分号。如果加了则会出现一条空语句。 Eg: DEBUG_PRINT; 此时,编译器替换后会都一条空语句. 1>有时候只允许出现一条语句,如果放入两条语句就会出现问题 Eg: if(…) DEBUG_PRINT; else ….. 四.宏 1.#define机制包括了一个规定,只允许把参数替换到文本中,这种实现通常称为宏或定义宏(defined macro) 2.宏的声明方式: #define name(parament-list) stuff 1>其中,parament-list(参数列表)是一个由逗号分隔的符号列表,它们可能出现在stuff中。参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会解释为stuff的一部分。 2>当宏被调用时,名字后面是一个由逗号分隔的列表,每个值都与宏定义中的一个参数相对应,整个列表用一对括号包围。但参数出现在程序中时,与每个参数对应的实际值都将被替换到stuff中。 eg: #define SQUARE(x) ( (x)*(x)) 如果在上述声明之后,调用 SQUARE(5) 预处理器就会用用下面这个表达式进行替换: 5*5。 说明: 在完整定义的参数宏中要加上括号,并且对宏定义中每个参数的两边也加上括号 3.#define替换 在程序中扩展#define定义符号和宏时,需要涉及几个步骤 1>在调用宏时,首先对参数进行检查,看看是否包含了任何由#define定义的符号。如果是,它们首先被替换 2>替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被它们的值所替代 3>最后,再次对结果文本进行扫描,看看它是否包含了任何由#define定义的符号。如果是,就崇光伏上述处理过程。 因此,宏参数和#define定义可以包含其他#define定义的符号。但是宏不可以出现递归。 说明: 1.当预处理器搜索#define定义的符号时,字符串常量的内容并不进行检查。如果想要把宏参数插入到字符串常量中,可以使用如下方法: 1>使用邻近字符串自动连接的特性,把一个字符串分成几段,每段实际上都是一个宏参数。 eg: #include #define PRINT(FORMAT,VALUE) \ printf(“thevalue is “ FORMAT “\n”,VALUE) int main() { int x= 12; PRINT(“%d”,x+3); } 说明: 此技巧只有字符串常量作为宏参数给出时才能使用。 2>第二个技巧使用预处理器把一个宏参数转换为一个字符串。#argument这种结构被预处理器翻译为”argument”. eg: #define PRINT(FORMAT,VALUE) \ printf(“thevalue of #VALUE \ “ is “ FORMAT “\n”,VALUE) int main() { int x= 12; PRINT(“%d”,x+3); } 输出结果为: the value of x+3 is 15 3>## 结构则执行一种不同的任务。它把位于它两边的符号连接成一个符号。作为用途之一,它允许宏定义从分离的文本片段创建标识符。 下面的实例使用这种连接把一个值添加到几个变量之一: #define ADD_TO_SUM(sum_number,value) \ sum ## sum_number += value …. ADD_TO_SUM(5,25); 最后一条语句把值25加到变量sum5上。注意这种连接必须产生一个合法的标识符。否则,其结果就是未定的。 五.宏与函数 1.宏非常频繁地用于执行简单的计算,比如在两个表达式中寻找其中较大或较小的一个: 可以用: #define MAX(a,b) ((a) > (b) ? (a) : (b) ) 2此处不用函数的原因是: 1>首先用于调用和从函数返回的代码很可能比实际执行这个小型计算工作的代码更大 2>函数的参数必须声明为一种特定的类型,所以它只能在类型合适的表达式上使用。因此,上面的宏可以用于整型、长整型、单浮点型、双浮点型以及其他类型中。既:宏是与类型无关的。 3>使用宏的不好之处在于,一份宏定义代码的拷贝都将插入到程序中。除非宏非常短,否则使用宏可能会大幅度增加程序的长度。 4>还有一些任务根本无法用函数实现 Eg:#define MALLOC(n,type) \ ((type *)malloc( (n)*sizeof(type) ) ) 此宏中的第二个参数是一种类型,它无法作为函数参数进行传递。 5>宏参数具有副作用。 3.宏与函数的区别 1>代码长度: ² #define宏:每次使用时,宏代码都被插入到程序中。除了非常小的宏之外,程序的长度将大幅度增加 ² 函数:函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码 2>执行速度: ² #define宏:更块 ² 函数: 存在函数调用/返回的额外开销 3>操作符优先级 ² #define宏参数的求值是在所有周围表达式的上下文环境里,除非它们加上括号,否则邻近操作符的优先级可能会产生不可预料的结果 ² 函数:函数参数只在函数调用时求值一次,它的结果传递给函数。表达式的求值结果更容易预测。 4>参数求值 ² #define宏:参数每次用于宏定义时,它们都将重新求值。由于多次求值,具有副作用的参数可能会产生不可预料的结果。 ² 函数:参数在函数被调用前只求值一次。在函数中多次使用参数并不会导致多种求值过程。参数的副作用并不会造成任何特殊的问题。 5>参数类型 ² #define宏:宏与类型无关。只要对参数的操作是合法的,它可以适用于任何参数类型 ² 函数:函数的参数是与类型有关的。如果参数的类型不同,就需要使用不同的函数,即使它们执行的任务是相同的。 六.#undef 1.该预处理指令用于移除一个宏定义 #undef name 2.如果一个现存的名字需要被重新定义,那么它的旧定义首先必须用#undef移除。 七.命令行定义 1.许多C编译器都可以实现:允许在命令行中定义符号,用于启动编译过程。当同一个源文件被编译成一个程序的不同版本时,该特性很有用。 Eg:假定某个程序声明了一个某种长度的数组。如果某个机器的内存很有限,这个数组必须很小,但在另一个内存很多的机器上,可能希望数组能够大写。 1>定义数组为: Int array[ARRAY_SIZE]; 那么我们希望ARRY_SIZE在命令行中定义。 例如: gcc -DARRY_SIZE=100 tiger.c。 即可实现在命令行中指定数组的大小为100。 2.在Linux编译器中,使用-D选项来完成该功能。 可以用两种方式使用该选项: Ø -Dname Ø -Dname=stuff 说明 1>此处的name即为程序中的标量名。 2>第一种形式定义了符号name,它的数值为。也可以用于条件编译中 3>第二中形式把该符号的值定义为等号后面的stuff。 3.提供符号命令行定义的编辑器通常也提供在命令行中去除符号的定义。在Linux编译器上,-U选项用于执行这项任务。指定-Uname将导致程序中符号name的定义被忽略。当它与条件编译结合使用时,该特性很有用。 八.条件编译 1.在编译一个程序时,如果可以选择某条语句或某组语句进行翻译或者被忽略,常常显得很方便。用于调试程序的语句就是一个明显的例子。它门不应该出现在程序的产品版本中,但是,如果以后做一些维护性修改时,又可能需要重新调试该语句。因此就需要条件编译。 2.条件编译(conditional compilation)用于实现该目的。使用条件编译,可以选择代码的一部分是被正常编译还是完全忽略。 3.用于支持条件编译的基本结构是#if指令和其匹配的#endif指令。 #if constant-expression statements #endif 1>其中constant-expression(常量表达式)由预处理器进行求值。如果它的值是非0值(真),那么statements部分被正常编译,否则预处理器就安静地删除它们。 2>所谓常量表达式,就是说它或者是字面值常量,或者是一个由#define定义的符号。如果变量在执行期间无法获得它们的值,那么它们如果出现在常量表达式中就是一非法的。因为它们的数值在编译时是不可预测的。 Eg: Ø 将所有的调试代码都以下面的形式出现 #if DEBUG printf(“x=%d ,y=%d\n”,x,y); #endif 1>如果我们想编译这个代码,可以用下面的代码实现 #define DEBUG 1 2>如果想忽略,则只要把这个符号定义为0就可以了。 Ø 条件编译的另一个用途是在编译时选择不同的代码部分。为了支持该功能,#if指令还具有可选的#elif和#else字句。 1>语法功能是: #if constant-expression statements #elif constant-expriession other statements …. #else other statements #endif #elif字句出现的次数可以不限。每个constant-expression(常量表达式)只有当前面所有常量表达式的值都为假时才会被编译。#else子句中的语句只有当前面所有的常量表达式值都为假时才会被编译,在其他情况下它都会被编译。 4.是否被定义 1>测试一个符号石佛已经被定义是可能的。在条件编译中完成这个任务往往更为方便,因为程序如果并不需要控制编译的符号所控制的特性,它就不需要被定义。 Eg: if defined(symbol) #ifdef symbol 九.文件包含 1.函数库文件包含两种不同类型的#include文件包含:函数库文件和本地文件。 1>函数库头文件 Ø 函数库头文件使用的语法 #include Ø 对于fiename,并不存在任何限制,标准库文件以一个.h后缀结尾。编译器在标准位置处查找函数头文件 Ø 编译器通过观察由编译器定义的“一系列标准位置”查找函数库头文件。在编译器的文档中应该说明这些位置是什么,以及怎样修改它们或者在列表中添加其他位置。 Ø Eg:在Linux系统上的C编译器在/user/include目录查找函数库头文件,该编译器有一个命令行选项,允许把其他目录添加到这个列表中,这样就可以创建自己的头文件函数库。 2>本地文件包含 Ø 语法格式: #include “filename” Ø 标准允许编译器自行决定是否把本地形式的#include和函数库形式的#include区别对待。 Ø 可以对本地头文件先使用一种特殊的处理方式,如果失败,编译器再按照函数库头文件的处理方式对它们进行处理。 Ø 处理本地头文件的一种常见策略就是在源文件所在的当前目录进行查找,如果该头文件并未找到,编译器就像查找函数库头文件一样在标准位置查找本地头文件。 Ø 可以在所有的#include语句中使用双括号而不是尖括号。但是,使用这种方法,有些编译器在查找函数库头文件时可能会浪费时间。 2.对函数库头文件使用尖括号的另一个较好的理由是可以给人们提示为函数头文件而不是本地文件。 3.UNIX系统和Borland C编译器也支持使用绝对路径名(absolute pathname),它不仅指定文件的名字,而且指定了文件的位置。 1>UNIX系统中的绝对路径名是以一个斜杠头开头,如下所示: Eg:/home/fred/c/my_proj/declaration2.h 2>在MS-DOS系统中,它所使用的是反斜杠而不是斜杠。 3>如果一个绝对路径名出现在任何一种形式的#include,那么正常的目录查找就被跳过,因为这个路径名指定了头文件的位置。 4.嵌套文件包含 1>嵌套#include文件的一个不利之处在于它使得很难判断源文件之间的真正依赖关系。 2>嵌套#include文件的另一个不利之处在于一个头文件可能会被多次包含。 3>多重包含在绝大多数情况下出现于大型程序中,它往往需要使用很多头文件,因此要发现这种情况并不容易。要解决这个问题,可以使用条件编译,这样编写: #ifndef _HEADERNAME_H #define _HEADERNAME_H 1 /* **All the stuff thatyou want in the header file */ #endif 那么,多重包含的危险就被消除了。当头文件第1次被包含时,它被正常处理,符号_HEADERNAME_H 被定义为1。如果头文件被再次包含,通过条件编译,它的所有内容被忽略。符号_HEADERNAME_H 按照被包含文件的文件名进行取名,以避免由于头文件使用相同的符号而引起的冲突。 说明: 前面的例子也可以改为 #define _HEADERNAME_H 使用该条语句,与前面的#define _HEADNAME_H 1效果是等同的。 说明: 1.当头文件被包含时,位于头文件内所有内容都要被编译。因此,每个头文件只应该包含一组函数或数据的声明。 2.使用几个头文件,每个头文件包含用于某个特定函数或模块的声明的做法会更好一些。 3.只把必要的声明包含于一个文件中,这样文件中的语句就不会意外的访问应该属于私有的函数或变量。 总结: 1.#argument结构由预处理器转换为字符串常量”argument”. 2.##操作符用于把它两边的文本粘切成同一个标识符。 3.有些任何既可以用宏也可以用函数实现。但是宏与类型无关。宏的执行速度快于函数,因为它存在函数调用/返回的开销。但是,使用宏通常会增加程序的长度,但函数确不会。 4.#include指令用于实现文件包含。它具有两种形式。 Ø 如果文件名位于一对尖括号中,编译器将在由编译器定义的标准位置查找这个文件,这种形式通常用于包含函数库头文件。 Ø 另一种形式,文件名出现在一对双括号内。不同的编译器可以用不同的方式处理这种形式。但是,如果用于处理本地头文件的任何特殊处理方式无法找到这个头文件,那么编译器接下来就使用标准查找过程来寻找它。这种形式通常用于包含自己编写的头文件。 5.文件包含可以嵌套,但很少需要进行超过一层或两层的文件包含嵌套。嵌套的包含文件将会增加多次包含同一个文件的危险,而且很难以确定某个特定的源文件依赖的究竟是那个头文件。 6.不要在一个宏定义的末尾加上分号,使其成为一条完整的语句。 7.头文件只应该包含一组函数函数和(或)数据的声明 8.把不同集合的声明分离到不同的头文件中可以改善信息隐蔽

    时间:2018-06-25 关键词: C语言 预处理 编译

  • MMU,cache,裸机嵌入式C编程还有带操作系统的编程

     通过CMSIS-utrealos项目中的CTBUG调试,使我对裸机C编程加深了认识。那个BUG调试,现象是出现hard fault,但是fault出现地的汇编指令看着貌似没啥问题,解决一处的fault后,其他处又出现fault了。最后我看到原来是fault出现地的指令中源地址错误了,源地址应该在数据段中,却意外地落到了代码段中。这个现象我忙活了半天才找到。 然后通过看那奇怪的源地址,对照它四周的现象代码,最终才发现是我的demo C文件中的头文件包含错误了(这个错误显然易见,但是在review代码时却没发现)。导致该问题指令的那个源地址计算错误,落到了代码段中。 调试时间花了不少,最后这段调试经历使我对裸机C编程发生了兴趣,从而认真读了armcm3编程权威指南一书。 还有王SAN那个栈溢出调试,王SAN本来还想研究是否是内存分配方面的错误,但是和我最后讨论后,结果用arm权威指南及我们组项目笔记文章中的那个栈溢出调试方法,很快就找出BUG原因了。 我的那个BUG调试告诉我ARMCM3上没有MMU那种保护功能,也没有操作系统(linux操作系统中的那种开启MMU保护以及虚存访问权限检查的功能这里没有)。虽然有MPU,但是这个是可选器件。 linux下的MMU及虚存保护,使得我们PC机编程中,可以不用C指针时,及时将其置为空从而起到保护程序的作用,但是回到裸机下,尤其没MMU部件时,空指针的作用意义在这里都消失了。这也就是嵌入式编程的复杂性。 但是通过嵌入式编程,尤其王san那个BUG调试,毕竟能加深对C编程底层调试的认识。比如现在嵌入式调试,可以直接通过观看内存地址处的数据信息是否异常,各个汇编指令和cpu寄存器值变化是否异常,嵌入式调试使人真正回到了二进制时代。 不可否认,MMU和虚拟存储的设计,真正简化了上层程序的设计,提供给用户一个好的开发应用程序的界面。详情见uclinux与linux的不同一文。 但是由于MMU的存在必要,这延缓了CPU访问内存的速度。为了解决这一问题,又带来cache,还有TLB(之前作linux性能检证搞过TLB的性能分析一文可以参考)。而ARM CM3中没有这一切,毕竟cortex-m3和A5的应用范围是不同的。

    时间:2018-06-20 关键词: mmu cache 嵌入式c

发布文章