单片机

1、前言

本文设计了一款针对空调设备的智能学习型红外遥控器,采用记录脉冲宽度的方法,成功实现了对多种红外空调遥控信号的学习与再现,真正实现了"万能"。本文在阐述了系统的总体结构及硬件设计的基础上,详细研究了系统学习,发送及通信功能的软件设计与实现。

2、系统总体结构与硬件设计

系统采用模块化设计,各模块通过接口电路与主控芯片相连。主要模块有:矩阵键盘,液晶显示,存储模块,红外发送模块,红外接收模块,RS232、RS485 通信模块,以及温度检测模块。

系统以Atmega16 单片机作为主控芯片,Atmega16具有16K 字节的系统内可编程Flash ,512 字节EEPROM,1K 字节SRAM,32 个通用I/O 口线,32 个通用工作寄存器,用于边界扫描的JTAG 接口,支持片内调试与编程,三个具有比较模式的灵活的定时器/计数器(T/C),片内/外中断,可编程串行USART,有起始条件检测器的通用串行接口,8 路10 位具有可选差分输入级可编程增益的ADC,具有片内振荡器的可编程看门狗定时器,一个SPI 串行端口,以及六个可以通过软件进行选择的省电模式。该芯片功能强大,满足系统设计需要并提供了充分的扩展空间。主控芯片使用8MHz 的晶振,晶振电路靠近主控芯片,尽量减少输入噪声。复位电路采用低电平复位。
矩阵键盘采用3*3 的设计,设置了8 个功能键,方便用户进行手动操作。其中单独设计了一颗模式切换键,可在学习、发射、通信模式中切换。为了实现学习功能, 红外接收模块使用了一体化接收头NB1838,其光电检测和前置放大器集成于同一封装,中心频率为37.9KHz. NB1838 的环氧树脂封装结构为其提供了一个特殊的红外滤光器,对自然光和电场干扰有很强的防护性。NB1838 对接收到的红外信号进行放大、检波、整形,并调制出红外编码,得到TTL 波形,反相后输入单片机,再由单片机进行进一步的处理,存储到EEPROM 中。

考虑到系统需要的存储空间比较大,设计了单独的存储模块,选用的EEPROM 是AT24C64,它提供了8KB 的容量,通过IIC 协议与Atmega16 TWI 接口通信,将学习到的红外指令存储在此,掉电不丢失。
在发射模式下,系统从EEPROM 读取相应数据信息,利用三极管9013 组成的放大电路,通过大功率红外发射管将调制好的红外信号发射出去。发射电路如图3所示,非发送状态时,三极管工作在截止状态,红外发射管不工作,有利于降低功耗以及延长红外发射管的使用寿命。经实际测试,发射距离可达到10m 左右。

通信模式中,系统通过RS232 电路与上位机通信,在与上位机通信时使用DS18B20 反馈温度信息,DS18B20 一线总线设计大大提高了系统的抗干扰性,独特而且经济。系统还增加了RS485 模块,便于组网,以实现对多个红外设备进行控制。RS485 在组网时只需要用一对双绞线将子设备的"A"、"B"端连接起来,这种接线方式为总线式拓扑结构,在同一总线上可挂接多个结点,连接方便。

为了增加设备的实用性,系统设计了两个电源方案,一个是直接接入5V 直流电源,一个是接入12V直流电源,然后通过L7805 构成的变压电路降压为5V使用。

3、系统软件设计与实现

系统程序主要分为三个部分:学习模式,发送模式以及通信模式。当第一次进入系统时,初始化设置设备地址,然后设置通信的波特率,提供1200、9600 以及19200 三种选择。系统主程序即在三个模式间切换,默认进入通信模式,可以通过模式切换按键改变模式,也可以通过上位机直接更改。出于系统的稳定性需要,在程序中加入了软件看门狗,防止程序"跑飞".

3.1 学习功能设计

3.1.1 学习模式

红外遥控器的码型多样,编码一般包括:帧头、系统码、操作码、同步码、帧间隔码、帧尾,且同步码与帧间隔码出现的位置不固定,因此码型格式灵活多变,很难区分各种码型的编码含义;各个红外遥控的编码长度不尽相同,发送方式也多种多样,最常用的有三种:完整帧只发送一次、完整帧重复发送两次、先发送一个完整帧,后重复发送帧头和一个脉冲。面对如此多样化的编码方式,如果区分每种编码的含义进行学习,学习的复杂度将会很高,并且通用性也会受到影响。所以,为了避开各色码型的干扰,系统在学习时并不关心码型数据的实际意义,只记录脉冲的时间宽度。系统主要针对载波频率为38KHz(周期为26us)的红外遥控器,利用变量IR_time 记录接收到的脉冲宽度。

3.1.2 压缩存储

由于不考虑具体的码型数据意义,只记录脉冲的宽度,系统的学习功能通用性得到了提高,但这种方式学习到的数据量很大,对存储的要求就变得很高。

尽管系统针对存储的大容量需求设计了单独的存储模块,但考虑到应在不增加硬件开销的情况下保证足够的存储容量,以及满足未来扩展的需要,在进行数据存储时,采取了数据压缩技术。

从学习到的电平数据可以发现,无论数据是1 还是0,都有相同时长的电平出现,这符合游程编码的特点。游程编码是一种简单的非破坏性资料压缩法,其好处是加压缩和解压缩都非常快,其方法是计算连续出现的资料长度压缩之。比如:一张二值图像的数据为:

WWWWWWWWBWWWWBBBWWWWWWWBWWWWW

使用游程编码压缩可得:8W1B4W3B7W1B 5W.

可见,压缩效率极高,且可避免复杂的编码和解码运算。所以,在存储时,系统对学习到的数据进行游程编码压缩[7,8].例如,学习到的一组空调遥控器的数据为[157 153 23 53 … 23 53 23 180 156 152 23 53 …53 23],如图5 所示,对重复的电平数据采用游程编码压缩后,原本需要199 字节的空调遥控码,只需要106个字节即可存储,压缩率达53.27%.因此,在存储时针对学习到的数据特点采取游程编码压缩,可以有效节约存储空间。

3.2 发射功能设计

现有的红外遥控器很多都是采用外部电路产生载波信号,例如使用NEC555 振荡器产生载波信号。为了减少硬件开销,本系统使用单片机内部的定时器产生载波。系统使用的是Atmega16 单片机,其定时器功能强大,具有普通模式、CTC 模式、快速PWM 模式、相位修正PWM 模式等工作模式,系统利用定时器1,使其工作在快速PWM 模式,产生占空比为1:3 的38KHz 的PWM 波。当发送某条指令时,单片机从对应的EEPROM 中提取指令信息,然后调制到生成的载波上,再通过发射电路即可完成红外信号的发射。

3.3 通信功能设计

3.3.1 上位机通信

本遥控器除了能通过功能按键实现手动操作外,还可以通过上位机软件对遥控器进行控制。遥控器与上位机通过RS232 模块进行通信,首先配置上位机软件,确定串口号,选择与设备相同的波特率及主从设备地址,然后根据需要选择相应的指令,点击发送即可通过上位机对设备进行控制。由于本遥控器是基于空调遥控器进行研究的,在与上位机通信时,系统中的温度检测模块会上传实时温度,便于用户进行调整。

3.3.2 组网控制

为了实现对多个设备的联网控制,还设计了RS485 模块。各子遥控器通过RS485 模块的"A"、"B"端连接在一起,组成控制网络,如图7 所示,其中一个作为主遥控器,与上位机通过RS232 模块进行串口通信。当上位机需要对某个子设备进行控制时,选择相应的子设备地址号,发送指令即可,主遥控器收到指令信息后,会将指令发给对应的子设备。与主遥控器相连的上位机PC 连接Internet,作为本地服务器,可实现远程控制。

用户登录远程客户端,经身份验证后与服务器建立连接,可发送指令给本地服务器,本地服务器再经过串口通信对遥控器进行相应操作。如果遥控器主机与上位机距离较远,RS232 不能满足通信需要,也可不使用遥控器主机,在上位机PC 上使用RS232-485 转接头,通过RS485 直接将遥控器网络与PC 机485 接口相连,利用上位机对遥控器网络直接进行控制。

4、结语

本文设计了一款智能空调遥控器。该系统采用只记录红外信号脉冲宽度,不考虑红外编码格式的方式,通过游程编码算法将红外信号压缩后保存到EEPROM 中,并直接利用主控芯片定时器的PWM 模式产生38KHz 的载波,节约了硬件成本,除手动操作外还可以通过上位机对遥控器进行控制,使用方便。
系统成功实现了对多种空调遥控器的学习与功能再现,操作灵活,性能稳定。本系统还可用于智能家居中,对不同的红外设备进行控制,也可用于远程网络控制,为智能家居及远程监控提供了一种实现方法。

来源:51单片机设计

围观 486

1、简述

下面这张图是一条外部中断线或外部事件线的示意图。图中的蓝色虚线箭头,标出了外部中断信号的传输路径;图中红色虚线箭头,标出了外部事件信号的传输路径。

单片机STM32——中断与事件的区别

图中信号线上划有一条斜线,旁边标志19字样的注释,表示这样的线路共有19套。

2、概念

事件:是表示检测到某一动作(电平边沿)触发事件发生了。

中断:有某个事件发生并产生中断,并跳转到对应的中断处理程序中。

中断有可能被更优先的中断屏蔽,事件不会。

事件本质上就是一个触发信号,是用来触发特定的外设模块或核心本身(唤醒)。

事件只是一个触发信号(脉冲),而中断则是一个固定的电平信号 。

事件是中断的触发源,事件可以触发中断,也可以不触发,开放了对应的中断屏蔽位,则事件可以触发相应的中断。 事件还是其它一些操作的触发源,比如DMA,还有TIM中影子寄存器的传递与更新;

简单点就是中断一定要有中断服务函数,但是事件却没有对应的函数。

事件可以在不需要CPU干预的情况下,执行这些操作,但是中断则必须要CPU介入.。

ps:注意把事件与事件驱动区分开。事件驱动相关内容,可以看我的博客上别的文章。

3、中断传输路径

图中的蓝色虚线箭头,标出了外部中断信号的传输路径。首先,外部信号从编号1的芯片管脚进入,经过编号2的边沿检测电路,通过编号3的或门进入中断挂起请求寄存器,最后经过编号4的与门输出到NVIC中断检测电路。

首先是编号2的边沿检测电路:这个边沿检测电路受上升沿或下降沿选择寄存器控制,用户可以使用这两个寄存器控制需要哪一个边沿产生中断,因为选择上升沿或下降沿是分别受2个平行的寄存器控制,所以用户可以同时选择上升沿或下降沿,而如果只有一个寄存器控制,那么只能选择一个边沿了。

接下来,是编号3的或门,这个或门的另一个输入是软件中断/事件寄存器,从这里可以看出,软件可以优先于外部信号请求一个中断或事件,即当软件中断/事件寄存器的对应位为"1"时,不管外部信号如何,编号3的或门都会输出有效信号。

然后,一个中断或事件请求信号经过编号3的或门后,进入挂起请求寄存器,到此之前,中断和事件的信号传输通路都是一致的,也就是说,挂起请求寄存器中记录了外部信号的电平变化。

外部请求信号最后经过编号4的与门,向NVIC中断控制器发出一个中断请求,如果中断屏蔽寄存器的对应位为"0",则该请求信号不能传输到与门的另一端,实现了中断的屏蔽。

4、事件传输路径

明白了外部中断的请求机制,就很容易理解事件的请求机制了。图中红色虚线箭头,标出了外部事件信号的传输路径,外部请求信号经过编号3的或门后,进入编号5的与门,这个与门的作用与编号4的与门类似,用于引入事件屏蔽寄存器的控制;最后脉冲发生器的一个跳变的信号转变为一个单脉冲,输出到芯片中的其它功能模块。

5、区别

从这张图上我们也可以知道,从外部激励信号来看,中断和事件的产生源都可以是一样的。之所以分成2个部分,由于中断是需要CPU参与的,需要软件的中断服务函数才能完成中断后产生的结果。但是事件,是靠脉冲发生器产生一个脉冲,进而由硬件自动完成这个事件产生的结果,当然相应的联动部件需要先设置好,比如引起DMA操作,AD转换等。

简单举例:外部I/O触发AD转换,来测量外部物品的重量;如果使用传统的中断通道,需要I/O触发产生外部中断,外部中断服务程序启动AD转换,AD转换完成中断服务程序提交最后结果;要是使用事件通道,I/O触发产生事件,然后联动触发AD转换,AD转换完成中断服务程序提交最后结果;相比之下,后者不要软件参与AD触发,并且响应速度也更快。要是使用事件触发DMA操作,就完全不用软件参与就可以完成某些联动任务了。

6、总结

可以这样简单的认为,事件机制提供了一个完全有硬件自动完成的触发到产生结果的通道,不要软件的参与,降低了CPU的负荷,节省了中断资源,提高了响应速度(硬件总快于软件),是利用硬件来提升CPU芯片处理事件能力的一个有效方法。

——如有不对的地方,非常欢迎给予指导!
——【感谢】资料来源于
http://blog.sina.com.cn/s/blog_4d1854230101dcui.html

围观 421

工业和家用电器市场中的各种应用要求使用数学运算来实现不同的算法和计算。基于 Cortex®-M0+的单片机包含加法、减法和乘法指令。Cortex-M0+架构没有用于除法运算的汇编指令,除法逻辑可以根据不同的编译器而变化。基于 Arm® Cortex-M0+的单片机(MCU)具有一个可配置选项,可通过该选项使用快速乘法器进行乘法运算。基于该可配置选项,乘法运算可以为单个周期指令到最多 32 个周期指令不等。

SAMC21(一款 Cortex-M0+ MCU)非常适合需要数学计算的应用。SAMC21 MCU 具有可进行乘法运算的快速单周期乘法器选项,还具有一个新的外设,称为除法和平方根加速器(Division and Square Root Accelerator,DIVAS),可用于执行快速除法和平方根运算。

1. 概念

适用于 Arm 架构的应用程序二进制接口(Application Binary Interface,ABI)包含一系列标准,其中有些是开放的标准,还有一些是 Arm 架构专用标准。ABI 可管控各种基于 Arm 的执行环境中二进制文件和开发工具的互操作。支持 Arm MCU 的编译器需符合这些标准。这些标准的其中一项是适用于 Arm 架构的运行时 ABI。此标准为 ABI 指定辅助函数,使之能够支持 C、C++和算术运算。对于除法,编译器会用各自的库代码替换除法和模运算符(即,使用重复减法来实现除法)。该库代码将数百个字节添加到代码存储器,MCU 消耗 50 到 400 之间任意数量的时钟周期,具体取决于操作数的大小。编译器可通过过载运行时ABI 辅助方法来使用 DIVAS 功能。DIVAS 展现出的性能优于编译器除法(即,比除法 65535/3 少 50 个时钟周期)。DIVAS 支持整数平方根运算,而不需要任何额外的库依赖关系。

注: 模运算符使用除法来取得余数,因此需要过载。DIVAS 的性能表现可能会随着被除数和除数的值而变化。

2. 解决方案/实现

DIVAS 只支持 32 位整数除法。用于除法运算的运行时 ABI 辅助方法过载,以便编译器了解除法应使用DIVAS 功能进行除法。根据运行时 ABI 标准,32 位整数除法函数在 r0 中返回商,或在{r0, r1}中返回商和余数。

在下面的示例中,使用 Arm 专用原型表示法描述二值返回函数。

注: 有些编译器可以使用 64 位有符号/无符号整数作为返回类型,而不是 idiv 或 uidiv 结构。

int __aeabi_idiv(int numerator, int denominator);
unsigned __aeabi_uidiv(unsigned numerator, unsigned denominator);
typedef struct { int quot; int rem; } idiv_return;
typedef struct { unsigned quot; unsigned rem; } uidiv_return;
__value_in_regs idiv_return __aeabi_idivmod(int numerator, int denominator);
__value_in_regs uidiv_return __aeabi_uidivmod(unsigned numerator, unsigned denominator);

注:
ASFv3 框架为 DIVAS 驱动程序提供支持。DIVAS ASF API 包含整数除法、模和平方根的 API。将符号定义 DIVAS_OVERLOAD_MODE 设置为 true,即可帮助 DIVAS 的 ASF 驱动程序中过载的 ABI 辅助方法来执行内部除法运算。包含运行时 ABI 辅助方法的驱动程序使用 DIVAS ASF 驱动程序 API 进行过载。

图 2-1. DIVAS 除法运算

如何利用 Cortex®-M0+ 单片机实现更快的数学计算

函数可以使用 DIVAS 平方根功能,不再需要使用基于数学库浮点运算的函数调用。

图 2-2. DIVAS 平方根运算

如何利用 Cortex®-M0+ 单片机实现更快的数学计算

DIVAS 可用于以下应用场景:
• ADC 和振荡器的运行时校准,用于微调工业和电机控制应用中 ADC/振荡器输出的性能。
• 需要更快 PID 环的工业控制应用。

提示:

被零除:Cortex-M0+是不包括除法指令的 Armv6-M 架构,因此没有硬件异常。用户可以通过确认分母是否为零来进行验证,基于此,可以使用引发 API 来引发软件用户异常,或者提供默认值(零或被除数)作为输出。请参见 https://www.gnu.org/software/libc/manual/html_node/Signaling-Yourself.html 了解 GCC 编译器支持的信号/引发 API 的信息。

该除法可能导致有符号位溢出:当被除数-2147483648(位模式 0x80000000)除以值为-1 的分母时,输出数 2147483648 用符号表示,没有值。以上是一种特殊情况,用户可以根据应用需求定义实现方式(即,可以返回被除数或默认值)。

从 ISR 和主上下文进行的除法/模运算:如果同时从 ISR 和主上下文进行除法运算,则过载方法应受到中断锁定的保护。那么,在每个过载方法开始和结束时,实现应包含全局中断禁止和全局中断允许方法。

浮点除法与长除法:Arm Cortex-M0+没有浮点单元(floating-point unit,FPU),而且 DIVAS 只支持 32 位整数除法。编译器继续使用自己的库代码来执行浮点除法和长(64 位)除法,而不是使用 DIVAS。

3. 相关资源

http://www.atmel.com/Images/Atmel-42465-Using-DIVAS-on-SAMCMicrocontroll...
• Application Binary Interface for the ARM® Architecture http://infocenter.arm.com/help/topic/com.arm.doc.ihi0036b/IHI0036B_bsabi...
• Run-time ABI for the ARM® Architecture(http://infocenter.arm.com/help/topic/com.arm.doc.ihi0043d/IHI0043D_rtabi...
http://asf.atmel.com/docs/latest/samc20/html/group__asfdoc__sam0__divas_...
https://www.gnu.org/software/libc/manual/html_node/Program-Error-Signals...

来源:Microchip工程师社区

围观 2949

单片机执行程序的过程,实际上就是执行我们所编制程序的过程。即逐条指令的过程。计算机每执行一条指令都可分为三个阶段进行。即取指令-----分析指令-----执行指令。

取指令的任务是:根据程序计数器PC中的值从程序存储器读出现行指令,送到指令寄存器。

分析指令阶段的任务是:将指令寄存器中的指令操作码取出后进行译码,分析其指令性质。如指令要求操作数,则寻找操作数地址。

计算机执行程序的过程实际上就是逐条指令地重复上述操作过程,直至遇到停机指令可循环等待指令。

一般计算机进行工作时,首先要通过外部设备把程序和数据通过输入接口电路和数据总线送入到存储器,然后逐条取出执行。但单片机中的程序一般事先我们都已通过写入器固化在片内或片外程序存储器中。因而一开机即可执行指令。

下面我们将举个实例来说明指令的执行过程:

开机时,程序计算器PC变为0000H。然后单片机在时序电路作用下自动进入执行程序过程。执行过程实际上就是取出指令(取出存储器中事先存放的指令阶段)和执行指令(分析和执行指令)的循环过程。

例如执行指令:MOV A,#0E0H,其机器码为“74H E0H”,该指令的功能是把操作数E0H送入累加器,0000H单元中已存放74H,0001H单元中已存放E0H。当单片机开始运行时,首先是进入取指阶段,其次序是:

1 程序计数器的内容(这时是0000H)送到地址寄存器;

2 程序计数器的内容自动加1(变为0001H);

3 地址寄存器的内容(0000H)通过内部地址总线送到存储器,以存储器中地址译码电跟,使地址为0000H的单元被选中;

4 CPU使读控制线有效;

5 在读命令控制下被选中存储器单元的内容(此时应为74H)送到内部数据总线上,因为是取指阶段,所以该内容通过数据总线被送到指令寄存器。

至此,取指阶段完成,进入译码分析和执行指令阶段。

由于本次进入指令寄存器中的内容是74H(操作码),以译码器译码后单片机就会知道该指令是要将一个数送到A累加器,而该数是在这个代码的下一个存储单元。所以,执行该指令还必须把数据(E0H)从存储器中取出送到CPU,即还要在存储器中取第二个字节。其过程与取指阶段很相似,只是此时PC已为0001H。指令译码器结合时序部件,产生74H操作码的微操作系列,使数字E0H从0001H单元取出。因为指令是要求把取得的数送到A累加器,所以取出的数字经内部数据总线进入A累加器,而不是进入指令寄存器。至此,一条指令的执行完毕。单片机中PC=0002H,PC在CPU每次向存储器取指或取数时自动加1,单片机又进入下一取指阶段。这一过程一直重复下去,直至收到暂停指令或循环等待指令暂停。CPU就是这样一条一条地执行指令,完成所有规定的功能。

来源:网络

围观 440

由于单片机的性能同电脑的性能是天渊之别的,无论从空间资源上、内存资源、工作频率,都是无法与之比较的。PC 机编程基本上不用考虑空间的占用、内存的占用的问题,最终目的就是实现功能就可以了。对于单片机来说就截然不同了,一般的单片机的Flash 和Ram 的资源是以KB 来衡量的,可想而知,单片机的资源是少得可怜,为此我们必须想法设法榨尽其所有资源,将它的性能发挥到最佳,程序设计时必须遵循以下几点进行优化:

1. 使用尽量小的数据类型

能够使用字符型(char)定义的变量,就不要使用整型(int)变量来定义;能够使用整型变量定义的变量就不要用长整型(long int),能不使用浮点型(float)变量就不要使用浮点型变量。当然,在定义变量后不要超过变量的作用范围,如果超过变量的范围赋值,C 编译器并不报错,但程序运行结果却错了,而且这样的错误很难发现。

2. 使用自加、自减指令

通常使用自加、自减指令和复合赋值表达式(如a-=1 及a+=1 等)都能够生成高质量的程序代码,编译器通常都能够生成inc 和dec 之类的指令,而使用a=a+1 或a=a-1 之类的指令,有很多C 编译器都会生成二到三个字节的指令。

3. 减少运算的强度

可以使用运算量小但功能相同的表达式替换原来复杂的的表达式。

(1) 求余运算
N= N %8 可以改为N = N &7
说明:位操作只需一个指令周期即可完成,而大部分的C 编译器的“%”运算均是调用子程序来完成,代码长、执行速度慢。通常,只要求是求2n 方的余数,均可使用位操作的方法来代替。

(2) 平方运算
N=Pow(3,2) 可以改为N=3*3
说明:在有内置硬件乘法器的单片机中(如51 系列),乘法运算比求平方运算快得多, 因为浮点数的求平方是通过调用子程序来实现的,乘法运算的子程序比平方运算的子程序代码短,执行速度快。

(3) 用位移代替乘法除法
N=M*8 可以改为N=M<<3
N=M/8 可以改为N=M>>3
说明:通常如果需要乘以或除以2n,都可以用移位的方法代替。如果乘以2n,都可以生成左移的代码,而乘以其它的整数或除以任何数,均调用乘除法子程序。用移位的方法得到代码比调用乘除法子程序生成的代码效率高。实际上,只要是乘以或除以一个整数,均可以用移位的方法得到结果。如N=M*9可以改为N=(M<<3)+M;

(4) 自加自减的区别
例如我们平时使用的延时函数都是通过采用自加的方式来实现。
void DelayNms(UINT16 t)
{
UINT16 i,j;
for(i=0;i for(j=0;i<1000;j++)
}
可以改为
void DelayNms(UINT16 t)
{
UINT16 i,j;
for(i=t;i>=0;i--)
for(j=1000;i>=0;j--)
}

说明:两个函数的延时效果相似,但几乎所有的C 编译对后一种函数生成的代码均比前一种代码少1~3个字节,因为几乎所有的MCU 均有为0 转移的指令,采用后一种方式能够生成这类指令。

4. while 与do...while 的区别

void DelayNus(UINT16 t)
{
while(t--)
{
NOP();
}
}
可以改为
void DelayNus(UINT16 t)
{
do
{
NOP();
}while(--t)
}

说明:使用do…while 循环编译后生成的代码的长度短于while 循环。

5. register 关键字

void UARTPrintfString(INT8 *str)
{
while(*str && str)
{
UARTSendByte(*str++)
}
}
可以改为
void UARTPrintfString(INT8 *str)
{
register INT8 *pstr=str;
while(*pstr && pstr)
{
UARTSendByte(*pstr++)
}
}

说明:在声明局部变量的时候可以使用register 关键字。这就使得编译器把变量放入一个多用途的寄存器中,而不是在堆栈中,合理使用这种方法可以提高执行速度。函数调用越是频繁,越是可能提高代码的速度,注意register 关键字只是建议编译器而已。

6. volatile 关键字

volatile 总是与优化有关,编译器有一种技术叫做数据流分析,分析程序中的变量在哪里赋值、在哪里使用、在哪里失效,分析结果可以用于常量合并,常量传播等优化,进一步可以死代码消除。一般来说,volatile 关键字只用在以下三种情况:
a) 中断服务函数中修改的供其它程序检测的变量需要加volatile(参考本书高级实验程序)
b) 多任务环境下各任务间共享的标志应该加volatile
c) 存储器映射的硬件寄存器通常也要加volatile

说明:因为每次对它的读写都可能由不同意义总之,volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

7. 以空间换时间

在数据校验实战当中,CRC16 循环冗余校验其实还有一种方法是查表法,通过查表可以更加快获得校验值,效率更高,当校验数据量大的时候,使用查表法优势更加明显,不过唯一的缺点是占用大量的空间。
//查表法:
code UINT16 szCRC16Tbl[256] = {
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
};
UINT16 CRC16CheckFromTbl(UINT8 *buf,UINT8 len)
{
UINT16 i;
UINT16 uncrcReg = 0, uncrcConst = 0xffff;
for(i = 0;i < len;i ++)
{
uncrcReg = (uncrcReg << 8) ^ szCRC16Tbl[(((uncrcConst ^ uncrcReg) >> 8)
^ *buf++) & 0xFF];
uncrcConst <<= 8;
}
return uncrcReg;
}

如果系统要求实时性比较强,在CRC16 循环冗余校验当中,推荐使用查表法,以空间换时间。

8. 宏函数取代函数

首先不推荐所有函数改为宏函数,以免出现不必要的错误。但是一些基本功能的函数很有必要使用宏函数来代替。
UINT8 Max(UINT8 A,UINT8 B)
{
return (A>B?A:B)
}
可以改为
#define MAX(A,B) {(A)>(B)?(A):(B)}

说明:函数和宏函数的区别就在于,宏函数占用了大量的空间,而函数占用了时间。大家要知道的是,函数调用是要使用系统的栈来保存数据的,如果编译器里有栈检查选项,一般在函数的头会嵌入一些汇编语句对当前栈进行检查;同时,cpu 也要在函数调用时保存和恢复当前的现场,进行压栈和弹栈操作,所以,函数调用需要一些cpu 时间。而宏函数不存在这个问题。宏函数仅仅作为预先写好的代码嵌入到当前程序,不会产生函数调用,所以仅仅是占用了空间,在频繁调用同一个宏函数的时候,该现象尤其突出。

9. 适当地使用算法

假如有一道算术题,求1~100 的和。
作为程序员的我们会毫不犹豫地点击键盘写出以下的计算方法:

UINT16 Sum(void)
{
UINT8 i,s;
for(i=1;i<=100;i++)
{
s+=i;
}
return s;
}

很明显大家都会想到这种方法,但是效率方面并不如意,我们需要动脑筋,就是采用数学算法解决问题,使计算效率提升一个级别。

UINT16 Sum(void)
{
UINT16 s;
s=(100 *(100+1))>>1;
return s;
}

结果很明显,同样的结果不同的计算方法,运行效率会有大大不同,所以我们需要最大限度地通过数学的方法提高程序的执行效率。

10. 用指针代替数组

在许多种情况下,可以用指针运算代替数组索引,这样做常常能产生又快又短的代码。与数组索引相比,指针一般能使代码速度更快,占用空间更少。使用多维数组时差异更明显。下面的代码作用是相同的,但是效率不一样。

UINT8 szArrayA[64];
UINT8 szArrayB[64];
UINT8 i;
UINT8 *p=szArray;
for(i=0;i<64;i++)szArrayB=szArrayA;
for(i=0;i<64;i++)szArrayB=*p++;

指针方法的优点是,szArrayA 的地址装入指针p 后,在每次循环中只需对p 增量操作。在数组索引方法中,每次循环中都必须进行基于i 值求数组下标的复杂运算。

11. 强制转换

C 语言精髓第一精髓就是指针的使用,第二精髓就是强制转换的使用,恰当地利用指针和强制转换不但可以提供程序效率,而且使程序更加之简洁,由于强制转换在C 语言编程中占有重要的地位,下面将已五个比较典型的例子作为讲解。

例子1:将带符号字节整型转换为无符号字节整型
UINT8 a=0;
INT8 b=-3;
a=(UINT8)b;

例子2:在大端模式下(8051 系列单片机是大端模式),将数组a[2]转化为无符号16 位整型值。

方法1:采用位移方法。

UINT8 a[2]={0x12,0x34};
UINT16 b=0;
b=(a[0]<<8)|a[1];
结果:b=0x1234
方法2:强制类型转换。
UINT8 a[2]={0x12,0x34};
UINT16 b=0;
b= *(UINT16 *)a; //强制转换
结果:b=0x1234

例子3:保存结构体数据内容。

方法1:逐个保存。

typedef struct _ST
{
UINT8 a;
UINT8 b;
UINT8 c;
UINT8 d;
UINT8 e;
}ST;
ST s;
UINT8 a[5]={0};
s.a=1;
s.b=2;
s.c=3;
s.d=4;
s.e=5;
a[0]=s.a;
a[1]=s.b;
a[2]=s.c;
a[3]=s.d;
a[4]=s.e;
结果:数组a 存储的内容是1、2、3、4、5。

方法2:强制类型转换。

typedef struct _ST
{
UINT8 a;
UINT8 b;
UINT8 c;
UINT8 d;
UINT8 e;
}ST;
ST s;
UINT8 a[5]={0};
UINT8 *p=(UINT8 *)&s;//强制转换
UINT8 i=0;
s.a=1;
s.b=2;
s.c=3;
s.d=4;
s.e=5;
for(i=0;i {
a=*p++;
}
结果:数组a 存储的内容是1、2、3、4、5。

例子4:在大端模式下(8051 系列单片机是大端模式)将含有位域的结构体赋给无符号字节整型值

方法1:逐位赋值。

typedef struct __BYTE2BITS
{
UINT8 _bit7:1;
UINT8 _bit6:1;
UINT8 _bit5:1;
UINT8 _bit4:1;
UINT8 _bit3:1;
UINT8 _bit2:1;
UINT8 _bit1:1;
UINT8 _bit0:1;
}BYTE2BITS;
BYTE2BITS Byte2Bits;
Byte2Bits._bit7=0;
Byte2Bits._bit6=0;
Byte2Bits._bit5=1;
Byte2Bits._bit4=1;
Byte2Bits._bit3=1;
Byte2Bits._bit2=1;
Byte2Bits._bit1=0;
Byte2Bits._bit0=0;
UINT8 a=0;
a|= Byte2Bits._bit7<<7;
a|= Byte2Bits._bit6<<6;
a|= Byte2Bits._bit5<<5;
a|= Byte2Bits._bit4<<4;
a|= Byte2Bits._bit3<<3;
a|= Byte2Bits._bit2<<2;
a|= Byte2Bits._bit1<<1;
a|= Byte2Bits._bit0<<0;
结果:a=0x3C

方法2:强制转换。

typedef struct __BYTE2BITS
{
UINT8 _bit7:1;
UINT8 _bit6:1;
UINT8 _bit5:1;
UINT8 _bit4:1;
UINT8 _bit3:1;
UINT8 _bit2:1;
UINT8 _bit1:1;
UINT8 _bit0:1;
}BYTE2BITS;
BYTE2BITS Byte2Bits;
Byte2Bits._bit7=0;
Byte2Bits._bit6=0;
Byte2Bits._bit5=1;
Byte2Bits._bit4=1;
Byte2Bits._bit3=1;
Byte2Bits._bit2=1;
Byte2Bits._bit1=0;
Byte2Bits._bit0=0;
UINT8 a=0;
a = *(UINT8 *)&Byte2Bits

结果:a=0x3C

例子5:在大端模式下(8051 系列单片机是大端模式)将无符号字节整型值赋给含有位域的结构体。

方法1:逐位赋值。

typedef struct __BYTE2BITS
{
UINT8 _bit7:1;
UINT8 _bit6:1;
UINT8 _bit5:1;
UINT8 _bit4:1;
UINT8 _bit3:1;
UINT8 _bit2:1;
UINT8 _bit1:1;
UINT8 _bit0:1;
}BYTE2BITS;
BYTE2BITS Byte2Bits;
UINT8 a=0x3C;
Byte2Bits._bit7=a&0x80;
Byte2Bits._bit6=a&0x40;
Byte2Bits._bit5=a&0x20;
Byte2Bits._bit4=a&0x10;
Byte2Bits._bit3=a&0x08;
Byte2Bits._bit2=a&0x04;
Byte2Bits._bit1=a&0x02;
Byte2Bits._bit0=a&0x01;

方法2:强制转换。

typedef struct __BYTE2BITS
{
UINT8 _bit7:1;
UINT8 _bit6:1;
UINT8 _bit5:1;
UINT8 _bit4:1;
UINT8 _bit3:1;
UINT8 _bit2:1;
UINT8 _bit1:1;
UINT8 _bit0:1;
}BYTE2BITS;
BYTE2BITS Byte2Bits;
UINT8 a=0x3C;
Byte2Bits= *(BYTE2BITS *)&a;

12. 减少函数调用参数

使用全局变量比函数传递参数更加有效率。这样做去除了函数调用参数入栈和函数完成后参数出栈所需要的时间。然而决定使用全局变量会影响程序的模块化和重入,故要慎重使用。

13. switch 语句中根据发生频率来进行case 排序

switch 语句是一个普通的编程技术,编译器会产生if-else-if 的嵌套代码,并按照顺序进行比较,发现匹配时,就跳转到满足条件的语句执行。使用时需要注意。每一个由机器语言实现的测试和跳转仅仅是为了决定下一步要做什么,就把宝贵的处理器时间耗尽。为了提高速度,没法把具体的情况按照它们发生的相对频率排序。换句话说,把最可能发生的情况放在第一位,最不可能的情况放在最后。

14. 将大的switch 语句转为嵌套switch 语句

当switch 语句中的case 标号很多时,为了减少比较的次数,明智的做法是把大switch 语句转为嵌套switch 语句。把发生频率高的case 标号放在一个switch 语句中,并且是嵌套switch 语句的最外层,发生相对频率相对低的case 标号放在另一个switch 语句中。比如,下面的程序段把相对发生频率低的情况放在缺省的case 标号内。

UINT8 ucCurTask=1;
void Task1(void);
void Task2(void);
void Task3(void);
void Task4(void);
……………
void Task16(void);
switch(ucCurTask)
{
case 1: Task1();break;
case 2: Task2();break;
case 3: Task3();break;
case 4: Task4();break;
………………………
case 16: Task16();break;
default:break;
}
可以改为
UINT8 ucCurTask=1;
void Task1(void);
void Task2(void);
void Task3(void);
void Task4(void);
……………
void Task16(void);
switch(ucCurTask)
{
case 1: Task1();break;
case 2: Task2();break;
default:
switch(ucCurTask)
{
case 3: Task3();break;
case 4: Task4();break;
………………………
case 16: Task16();break;
default:break;
}
Break;
}

由于switch 语句等同于if-else-if 的嵌套代码,如果大的if 语句同样要转换为嵌套的if 语句。

UINT8 ucCurTask=1;
void Task1(void);
void Task2(void);
void Task3(void);
void Task4(void);
……………
void Task16(void);
if (ucCurTask==1) Task1();
else if(ucCurTask==2) Task2();
else
{
if (ucCurTask==3) Task3();
else if(ucCurTask==4) Task4();
………………
else Task16();
}

15. 函数指针妙用

当switch 语句中的case 标号很多时,或者if 语句的比较次数过多时,为了提高程序执行速度,可以运用函数指针来取代switch 或if 语句的用法,这些用法可以参考电子菜单实验代码、USB 实验代码和网络实验代码。

UINT8 ucCurTask=1;
void Task1(void);
void Task2(void);
void Task3(void);
void Task4(void);
……………
void Task16(void);
switch(ucCurTask)
{
case 1: Task1();break;
case 2: Task2();break;
case 3: Task3();break;
case 4: Task4();break;
………………………
case 16: Task16();break;
default:break;
}
可以改为
UINT8 ucCurTask=1;
void Task1(void);
void Task2(void);
void Task3(void);
void Task4(void);
……………
void Task16(void);
void (*szTaskTbl)[16])(void)={Task1,Task2,Task3,Task4,…,Task16};
调用方法1:(*szTaskTbl[ucCurTask])();
调用方法2: szTaskTbl[ucCurTask]();

16. 循环嵌套

循环在编程中经常用到的,往往会出现循环嵌套。现在就已for 循环为例。

UINT8 i,j;
for(i=0;i<255;i++)
{
for(j=0;j<25;j++)
{
………………
}
}
较大的循环嵌套较小的循环编译器会浪费更加多的时间,推荐的做法就是较小的循环嵌套较大的循环。
UINT8 i,j;
for(j=0;j<25;j++)
{
for(i=0;i<255;i++)
{
………………
}
}

17. 内联函数

在C++中,关键字inline 可以被加入到任何函数的声明中。这个关键字请求编译器用函数内部的代码替换所有对于指出的函数的调用。

这样做在两个方面快于函数调用: 第一,省去了调用指令需要的执行时间;第二,省去了传递变元和传递过程需要的时间。但是使用这种方法在优化程序速度的同时,程序长度变大了,因此需要更多的ROM。使用这种优化在inline 函数频繁调用并且只包含几行代码的时候是最有效的。

如果编译器允许在C 语言编程中能够支持inline 关键字,注意不是C++语言编程,而且单片机的ROM足够大,就可以考虑加上inline 关键字。支持inline 关键字的编译器如ADS1.2,RealView MDK 等。

18. 从编译器着手

很多编译器都具有偏向于代码执行速度上的优化、代码占用空闲太小的优化。例如Keil 开发环境编译时可以选择偏向于代码执行速度上的优化(Favor Speed)还是代码占用空间太小的优化(Favor Size)。还有其他基于GCC 的开发环境一般都会提供-O0、-O1、-O2、—O3、-Os 的优化选项,而使用-O2 的优化代码执行速度上最理想,使用-Os 优化代码占用空间大小最小。

19. 嵌入汇编

汇编语言是效率最高的计算机语言,在一般项目开发当中一般都采用C 语言来开发的,因为嵌入汇编之后会影响平台的移植性和可读性,不同平台的汇编指令是不兼容的。但是对于一些执着的程序员要求程序获得极致的运行的效率,他们都在C 语言中嵌入汇编,即“混合编程”。

注意:如果想嵌入汇编,一定要对汇编有深刻的了解。不到万不得已的情况,不要使用嵌入汇编。

来源:tq3101955

围观 456

单片机的基准电压一般为3.3V,如果外部信号超过了AD测量范围,可以采用电阻分压的方法,但是要注意阻抗匹配问题。比如,SMT32的模数输入阻抗约为10K,如果外接的分压电阻无法远小于该阻值,则会因为信号源输出阻抗较大,AD的输入阻抗较小,从而输入阻抗对信号源信号的电压造成分压,最终导致电压读取误差较大。

因此对于使用单片机读取外部信号电压,外接分压电阻必须选用较小的电阻,或者在对功耗有要求的情况下,可选用大阻值的电压分压后,使用电压跟随器进行阻抗匹配(电压跟随器输入阻抗可达到几兆欧姆,输出阻抗为几欧姆甚至更小)。如果信号源的输出阻抗较大,可采用电压跟随器匹配后再接电阻分压。

对于外置的ADC芯片,在选型时,要留意其类型(SAR型、开关电容型、FLASH型、双积分型、Sigma-Delta型),不同类型的ADC芯片输入阻抗不同——

1、SAR型:这种ADC内阻都很大,一般500K以上。即使阻抗小的ADC,阻抗也是固定的。所以即使只要被测源内阻稳定,只是相当于电阻分压,可以被校正;

2、开关电容型:如TLC2543之类,其要求很低的输入阻抗用于对内部采样电容快速充电。这时最好有低阻源,否则会引起误差。实在不行,可以外部并联一很大的电容,每次被取样后,大电容的电压下降不多。因此并联外部大电容后,开关电容输入可以等效为一个纯阻性阻抗,可以被校正;

3、FLASH型(直接比较型):大多高速ADC都是直接比较型,也称闪速型(FLASH),一般都是低阻抗的。要求低阻源。对外表现纯阻性,可以和运放直接连接;

4、双积分型:这种类型大多输入阻抗极高,几乎不用考虑阻抗问题;

5、Sigma-Delta型:这是目前精度最高的ADC类型,需要重点注意如下问题:

  • a. 测量范围问题:SigmaDelta型ADC属于开关电容型输入,必须有低阻源。所以为了简化外部设计,内部大多集成有缓冲器。缓冲器打开,则对外呈现高阻,使用方便。但要注意了,缓冲器实际是个运放。那么必然有上下轨的限制。大多数缓冲器都是下轨50mV,上轨AVCC-1.5V。在这种应用中,共莫输入范围大大的缩小,而且不能到测0V。一定要特别小心!一般用在电桥测量中,因为共模范围都在1/2VCC附近。不必过分担心缓冲器的零票,通过内部校零寄存器很容易校正的;
  • b. 输入端有RC滤波器的问题:SigmaDelta型ADC属于开关电容型输入,在低阻源上工作良好。但有时候为了抑制共模或抑制乃奎斯特频率外的信号,需要在输入端加RC滤波器,一般DATASHEET上会给一张最大允许输入阻抗和C和Gain的关系表。这时很奇怪的一个特性是,C越大,则最大输入阻抗必须随之减小!刚开始可能很多人不解,其实只要想一下电容充电特性久很容易明白的。还有一个折衷的办法是,把C取很大,远大于几百万倍的采样电容Cs(一般4~20PF),则输入等效纯电阻,分压误差可以用GainOffset寄存器校正。
  • c. 运放千万不能和SigmaDelta型ADC直连!前面说过,开关电容输入电路电路周期用采样电容从输入端采样,每次和运放并联的时候,会呈现低阻,和运放输出阻抗分压,造成电压下降,负反馈立刻开始校正,但运放压摆率(SlewRate)有限,不能立刻响应。于是造成瞬间电压跌落,取样接近完毕时,相当于高阻,运放输出电压上升,但压摆率使运放来不及校正,结果是过冲。而这时正是最关键的采样结束时刻。所以,运放和SD型ADC连接,必须通过一个电阻和电容连接(接成低通)。而RC的关系又必须服从datasheet所述规则。
  • d. 差分输入和双极性的问题:SD型ADC都可以差分输入,都支持双极性输入。但这里的双极性并不是指可以测负压,而是Vi+ Vi-两脚之间的电压。假设Vi-接AGND,那么负压测量范围不会超过-0.3V。正确的接法是Vi+ Vi- 共模都在-0.3~VCC之间差分输入。一个典型的例子是电桥。另一个例子是Vi-接Vref,Vi+对Vi-的电压允许双极性输入

来源:思考与实践并行

围观 939

很多新手在单片机上走的第一步是点亮第一个LED灯,实际上因为开发板的不同,所编写的代码也不同,关键是你要去了解你用的开发板的电路布局。对于电路方面的知识我这里也不祥讲,我要做的是无论你用哪一种开发板我的文章都能帮助你。

  P0 = 0xFE;

  这句代码大家不陌生。

  void main(){
    unsigned char count = 0;
    while(1){
      P0 = ~(0x01 << count);
      Delay();    //单独实现一个延时函数
      count++;
      if(count > =8){
        count = 0;
      }
    }
  }

以上就是实现流水灯的基本代码,这里没有电路供你分析,但是无论什么开发板,核心代码可以用以上代码实现。

我相信你能看到这里也是有点基础的,这里的延时函数Delay,接下来要讲的是定时器,定时器就是可以替代延时函数的。

定时器

标准的51单片机内部有T0和T1两个定时器,实际上就是TCON特殊功能的寄存器来控制这两个定时器的。

单片机定时器与数码管静态显示

除此之外,定时值存储寄存器有TH和TL,给TL赋值后,TL会自动加1,加到255后TH加1,有趣的TH也可以提前赋值,但这只是定时器工作的一种模式,定时器有四种模式,这里我不祥讲,而且我们几乎用的模式就是这种,后面涉及到会详细讲解。这里只需要知道TCON(地址0x88)位分配,以后会经常用到。

还有一个TMOC就是定时器作用的模式,位分配如下图:

单片机定时器与数码管静态显示

代码:

void main()
{
  TH0 = 0xB8;  //给TH0赋值,后面的0代表是给定时器T0的TH赋值
  TL0 = 0x00;
  TR0 = 1;   //启动T0定时器
  if(TF0 == 1)    //判断T0是否溢出,TF是个标志位
  {
    //重置
  TH0 = 0xB8;  
  TL0 = 0x00;
  } 
}

以上就是定时器,时间多少呢?

我们以晶振位11.0592为例,时钟周期是1/11059200,机器周期(1ms)12/11059200,如果我们定时20ms,那个要执行20*(12/110592)次,算出来是18432次,换成十六进制是B800,所以对TH0赋值B8,对TL0赋值00;

数码管

#include <reg52.h>
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;
void main() {
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 1;
ADDR3 = 1;
ENLED = 0;
P0 = 0XF8;
while(1);
}

上面代码是用位STC-51开发板写的,在最后一个数码管上显示数字7,数码管难度简单,只需要针对数码管等的排布编程即可。

下面我们用关键字code定义数码管所能够显示所有字符的数组,这里再结合定时器一起。

#include <reg52.h>
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;
//数组 unsigned char code led[] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E };
void main() {
unsigned char count = 0;
//记录T0中断次数
unsigned char secnt = 0;
//记录经过的秒数
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 0;
ADDR3 = 1;
ENLED = 0;
//设置T0模式
TMOD = 0x01;
//为T0的TH0,TL0初始化
TH0 = 0xB8;
TL0 = 0x00;
//启动T0 TR0 = 1;
while(1)
{ if(TF0 ==1)
{ TH0 = 0xB8;
TL0 = 0x00;
count++;
TF0 = 0; }
if(count >=50)
{ count = 0;
P0 = led[secnt];
secnt++;
if(secnt>=16)
{ secnt = 0; }
}
}
}

这里代码比较紧凑,不过不影响。上面的代码我相信你也能懂,但是你能发现定时器在这里起到了一个定时中断的作用。

这里讲一下中断。

中断

下面是中断IE寄存器位分配图:

单片机定时器与数码管静态显示

直接上代码:

#include <reg52.h>
//数码管显示字符真值数组
unsigned char code ledchar[]={0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E };
//数码管显示区数组
unsigned char ledbuff[6] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

unsigned char i = 0;
//动态扫描索引
unsigned int c = 0;
//记录中断次数
void main() {
unsigned long s = 0;
//记录秒数
//使能U3
ADDR3 = 1;
ENLED = 0;
//设置T0模式
TMOD = 0x01;
//初始化TH0,TL0
TH0 = 0xFC; TL0 = 0x66;
//启动TR0
TR0 = 1;
//使能总中断
EA = 1;
//使能T0中断
ET0 = 1;
//主循环
while(1)
{
//1s中断
if(c>=1000)
{
s++;
c=0;
//为数码管显示区赋值
ledbuff[0] = ledchar[s%10];
ledbuff[1] = ledchar[s/10%10];
ledbuff[2] = ledchar[s/100%10];
ledbuff[3] = ledchar[s/1000%10];
ledbuff[4] = ledchar[s/10000%10];
ledbuff[5] = ledchar[s/100000%10];
}
}
}

//定时器T0中断服务
void InterruptTimer0() interrupt 1
{
//重新赋值
TH0 = 0xFC;
TL0 = 0x66;
c++;
//显示消隐
P0 = 0xFE;
//完成数码管动态扫描
switch(i)
{
case 0:
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 1;
i++;
P0 = ledbuff[0];
break;
case 1:
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 0;
i++;
P0 = ledbuff[1];
break;
case 2:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 1;
i++;
P0 = ledbuff[2];
break;
case 3:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 0;
i++;
P0 = ledbuff[3];
break;
case 4:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 1;
i++;
P0 = ledbuff[4];
break;
case 5:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 0;
i=0;
P0 = ledbuff[5];
break;
default:break; } }

这组代码能够按照我们计算好的时间为单位显示秒数。

我们能够提出中断核心代码

EA = 1//中断总开关

ET0 = 1//确认使用T0定时器中断开关

TR0 = 1//肯定要启动T0定时器

void InterruptTimer0() interrupt 1//定时器T0中断服务,中断代码写在这里面,至于interrupt 1是因为interrupt会去寻找地址' 1 ',而T0定时器中断的地址就是1,所以我们可以直接在此函数中写中断期间的代码。至于各种中断的地址我也不再这里多写了。值得一谈的是IP——中断优先级寄存器位分配

单片机定时器与数码管静态显示

各级中断都差不多,中断发生的也很多,当同时有许多中断发生时,可以通过置上面的值为1升级成优先级中断。

转自:嵌入式开发员

围观 483

一、使用USART发送数据

我们在写单片机程序的时候,在Debug时,往往要用到串口输出信息,这是会使用printf打印出我们想要的信息来,但是printf有一个弊端,就是输出打印时间较长。这样在一些对时间精度要求非常高的场合,使用printf将会带来一系列问题,这时,如果使用单片机的USART自定义一个协议,直接发送数据到上位机,将会得到我们想要的效果。

下面对怎样使用USART发送数据做一个整理。

1、发送单个字符

void USART1_PutChar(u8 ch)
{
USART_SendData8(USART1,(u8)ch);

while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);

while(USART_GetFlagStatus(USART1,USART_FLAG_TC) == RESET);
}

2、发送固定长度的字符串

void USART1_PutStrLen(u8 *buf,u16 len)
{
for(;len > 0 ; len--)
{
    USART_SendData8(USART1,*buf++);

    while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
}

while(USART_GetFlagStatus(USART1,USART_FLAG_TC) == RESET);
}

3、发送任意长度的字符串

void USART1_PutStr(u8 *buf)
{
while(*buf)
{
    USART_SendData8(USART1,*buf++);

    while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
}
while(USART_GetFlagStatus(USART1,USART_FLAG_TC) == RESET);
}

二、如何发送16bit的数据

单片机(STM8)的USART发送的是8bit的数据,所以如果要发送16bit的数据,则需要将16bit的数据转换为8bit的高低两个字节进行发送,需做如下处理。

u16 data;
u8 high_byte,low_byte;
high_byte=data>>8;
low_byte=data;

则经过这样的转换之后,就可以直接使用USART进行发送了。

三、使用翻转电平的方式测量程序执行时间

我们想要知道某一段代码的执行时间,可以通过示波器来测量,在需要测量的代码处做一个翻转电平的程序,就可以通过示波器来查看程序的执行时间了。

代码如下:

u8 toggle_flag=1;

if(toggle_flag)
{
    GPIO_SetBits(GPIOC,GPIO_Pin_0); 
    toggle_flag=0;
}
else
{
    GPIO_ResetBits(GPIOC,GPIO_Pin_0);
    toggle_flag=1;
}

来源:Tangledice

围观 440

首先什么是执行效率。我们平常所说的执行效率就是使用相同的算法在相同输入条件下完成相同计算所产生的系统开销,目前来说一般会更多关注执行时间方面的开销。所有语言编写的代码最终要运行,都要转化成机器码。在更短的时间内完成相同的事那么效率就高。

关于如何提高C语言程序的执行效率,有如下建议:

1.尽量避免调用延时函数

没有带操作系统的程序只能在while(1)里面循环执行,如果在这里面调用大量的延时这样会很消耗CPU的资源,延时等于是让他在这歇着不干事了,只有中断里面的才会执行。如果仅仅是做一个LED一秒闪烁一次的程序,那么很简单,可以直接调用延时函数,但是实际的项目中往往在大循环里有很多事要做,对于实时性要求较高的场合就不行了。为了避免使用延时,可以使用定时器中断产生一个标志位,到了时间标志位置1,在主程序里面只需要检测标志位,置1了才执行一次,然后清标志。其他时间就去做别的事了,而不会在这等待了。最好的例子就是数码管的显示,使用中断调显示,在我们的例程里面有。然后是那个按键检测的,一般的程序都是做的while(!key)等待按键释放,如果按键一直按着,那后面的程序就永远得不到运行死在这了,其实可以做一个按键标志检测下降沿和上升沿就可以避免这个问题了。

2.写出来的代码要尽量简洁,避免重复

在10天学会单片机那本书上看到他写的数码管显示那部分代码,选中一个位,然后送数据,再选中一个位,再送数据,依次做完。代码重复率太高了,不仅占用过多的类存,而且执行效率差可读性差,仅仅是实现了功能而已,实际的编程可以做一个循环,for循环或者while循环。这样的代码看起来更有水平。

3.合理使用宏定义

在程序中如果某个变量或寄存器经常用到,可以使用宏定义定义一个新的名代替他,这样的好处是方便修改,比如液晶的数据端总线接的P1,现在想改到P0,那么只需要修改宏定义这里就可以了,编译器编译的时候,他会自动的把定义的名替换成实际的名称。

4.使用尽量小的数据类型

比如某个变量的值范围是0-255,那么就定义成unsigned char,当然也可以定义成unsigned int,但是这样造成了内存的浪费,而且运算时效率要低一点。如果数据没有负数的话,尽量定义成无符号的类型。应尽量避免定义成浮点型数据类型或双精度(占8个字节)类型,这两种类型运算时很消耗CPU资源。比如采集电压范围是0-5v,精确到小数点后三位,可以把采集到的数据扩大1000倍,即使最大也才到5000,然后多采集几次做个滤波算法,最后电压算出来后只需要在第一位后面加个小数点就可以了,变量定义成unsigned int 型变量就没问题了。

5.避免使用乘除法

乘除法很消耗CPU资源,查看汇编代码会发现,一个乘除法运算会编译出10几甚至几10行代码。如果是乘以或除以2的n次方,可以用<<或>>来实现,这种移位运算在编译时就已经算好了,所以代码很简洁,运算效率就高。但是需要特别注意运算符的优先级问题。

6.尽量使用复合赋值运算符

a=a+b与a+=b 这两个表达式有什么区别呢?前者是先计算a+b的值,然后保存到ACC寄存器,然后再把ACC寄存器的值赋给a,而后者是直接将a+b的值赋给a,节省一个步骤,虽然只节省了一条指令,但是当这个运算循环几千次几万次呢,那么效果很明显了。像其他的-=、*=、/=、%=等都是一样的。

7.尽量不要定义成全局变量

先来看一下局部变量,全局变量,静态局部变量,静态全局变量的异同:

(1)局部变量:在一个函数中或复合语句中定义的变量,在动态存储区分配存储单元,在调用时动态分配,在函数或复合语句结束时自动释放;

(2)静态局部变量:在一个函数中定义局部变量时,若加上static声明,则此变量为静态局部变量,在静态存储区分配存储单元,在程序运行期间都不释放;静态局部变量只能在该函数中使用;静态局部变量在编译时赋值(若在定义时未进行赋值处理,则默认赋值为0(对数值型变量)或空字符(对字符型变量));静态局部变量在函数调用结束后不自动释放,保留函数调用结束后的值;

(3)全局变量:在函数外定义的变量称为全局变量;全局变量在静态存储区分配存储单元,在程序运行期间都不释放,在文件中的函数均可调用该全局变量,其他文件内的函数调用全局变量,需加extern声明;

(4)静态全局变量:在函数外定义变量时,若加上static声明,则此变量为静态全局变量;静态全局变量在静态存储区分配存储单元,在程序运行期间都不释放,静态全局变量在编译时赋值(若在定义时未进行赋值处理,则默认赋值为0(对数值型变量)或空字符(对字符型变量));只能在当前文件中使用。

一般情况下就定义成局部变量,这样不仅运行更高效,而且很方便移植。局部变量大多定位于MCU内部的寄存器中,在绝大多数MCU中,使用寄存器操作速度比数据存储器快,指令也更多更灵活,有利于生成质量更高的代码,而且局部变量所的占用的寄存器和数据存储器在不同的模块中可以重复利用。

当中断里需要用到的变量时,就需要定义成全局变量,并且加volatile修饰一下,防止编译器优化。如果数据是只读的比如数码管的断码、汉字取模的字库需要放在ROM里,这样可以节省RAM,51单片机是加code,高级点的单片机都是加const修饰。

8.选择合适的算法和数据结构

应该熟悉算法语言,知道各种算法的优缺点,具体资料请参见相应的参考资料,有很多计算机书籍上都有介绍。将比较慢的顺序查找法用较快的二分查找或乱序查找法代替,插入排序或冒泡排序法用快速排序、合并排序或根排序代替,都可以大大提高程序执行的效率。.

选择一种合适的数据结构也很重要。 指针是一个包含地址的变量,可对他指向的变量进行寻址。使用指针可以很容易的从一个变量移到下一个变量,故特别适合对大量变量进行操作的场合。数组与指针语句具有十分密切的关系,一般来说,指针比较灵活简洁,而数组则比较直观,容易理解。对于大部分的编译器,使用指针比使用数组生成的代码更短,执行效率更高。但是在Keil中则相反,使用数组比使用的指针生成的代码更短。

9.使用条件编译

一般情况下对C语言程序进行编译时,所有的程序都参加编译,但是有时希望对其中一部分内容只在满足一定条件才编译,这就是条件编译。条件编译可以根据实际情况,选择不同的编译范围,从而产生不同的代码。

10.嵌入汇编---杀手锏

汇编语言是效率最高的计算机语言,在一般项目开发当中一般都采用C 语言来开发的,因为嵌入汇编 之后会影响平台的移植性和可读性,不同平台的汇编指令是不兼容的。但是对于一些执着的程序员要求程序获得极致的运行的效率,他们都在C 语言中嵌入汇编,即“混合编程”。

注意:如果想嵌入汇编,一定要对汇编有深刻的了解。不到万不得已的情况,不要使用嵌入汇编。

来源:畅学电子网

围观 272

启动代码通常都烧写在flash中,它是系统一上电就执行的一段程序,它运行在任何用户c代码之前。上电后,arm处理器处于arm态,运行于管理模式,同时系统所有中断被禁止,pc到地址0处取指令执行。一个可执行映像文件必须有个入口点,而能放在rom起始处的映像文件的入口地址也必须设置为0。

在汇编语言中,我们已经说过怎样定义一个程序的入口点,当工程中有多个入口点时,需要在连接器中使用-entry指出程序的入口点。如果用户创建的程序中,包含了main函数,则与c库初始化代码对应的也会有个入口点。

总的来说,启动代码主要完成两方面的工作,一是初始化执行环境,例如中断向量表、堆栈、i/o等;二是初始化c库和用户应用程序。

在第一阶段,启动代码的人物可以描述为:

(1)建立中断向量表;
(2)初始化存储器;
(3)初始化堆栈寄存器;
(4)初始化i/o以及其他必要的设备;
(5)根据需要改变处理器的状态。

建立中断向量表

初始化代码必须建立好中断向量表,以备应用程序后续使用。如果系统的地址0处是rom,则中断向量表直接是一些跳转指令就可以了,他们转到相应的中断处理函数执行。如果系统的0地址处不是rom,则中断向量表是通过动态的方式创建的,这主要是通过存储器映射的方式来实现:即上电后,rom中的地址被映射到地址0,它首先开始执行以便完成环境的初始化,最重要的它会将中断向量表拷贝到ram中,然后通过地址映射将ram地址映射为0,这样ram中的中断向量就可以使用了。

初始化存储系统

对于有mmu的处理器,需要正确初始化mmu,没有的只需正确初始化存储控制器,为每个bank配置正确的参数就可以了。

初始化堆栈指针

初始化代码必须初始化处理器各个模式下的堆栈指针,所有系统或用户程序会涉及的处理器模式对应的堆栈指针都应该初始化。通常未定义指令和预取指终止异常对应模式的堆栈指针不需要配置,除非用户需要使用它们作为调试使用。

初始化堆栈指针

初始化代码必须初始化处理器各种模式下的堆栈指针,所有系统或用户程序会涉及的处理器模式对应的堆栈指针都应该被初始化。通常未定义指令和预取指终止异常对应模式的堆栈指针不需要配置,除非用户需要使用它们作为调试使用。

初始化i/o以及其他必要设备

关键的输入输出模块必须在中断打开之前被配置,例如看门狗,否则它们会在系统启动后产生复位信号。

改变处理器状态和模式

启动代码运行时,处理器状态认为管理模式,如果用户程序需要运行在用户模式,可以切换转入用户模式;所有处理器上电后是处于arm状态的,如果需要改变处理器状态,也可以在启动代码里切换到thumb态。

在执行环境建立起来后,接下来就是应用程序的初始化,简单点就是讲用户程序加载到他们相应的运行地址,初始化数据区等,这个阶段完成后,才能进入用户最终的c代码区域。用户应用程序的初始化过程包括:将rw段的数据拷贝到他们的运行地址处,同时在rw段后面初始化相应大小的zi段数据,把他们初始化为0,使用了库函数的程序(工程中有main函数)是在库函数_main中自动完成这些工作的。

本文转自:博客园 - 张凌001,转载此文目的在于传递更多信息,版权归原作者所有。

围观 351

页面

订阅 RSS - 单片机