单片机

如何实现单片机用一个I/O采集多个按键信号

使用模数转换(ADC)的特点就可以实现单片机用一个I/O采集多个按键信号。

一、单片机的I/O口检测按键简说

我们知道,一般情况下单片机的一个I/O口作为普通I/O口的话,只能检测识别一个按键。

日常设计中,如果碰到按键数量较多的话,会采用行列式键盘,例如最常见的4X4矩阵键盘,这样可以实现用8个I/O口检测16个按键。

如何实现单片机用一个I/O采集多个按键信号?

还有就是键盘接口,典型的是我们计算机上用的键盘,其采用PS/2接口,现在一般计算机上用的是USB接口的键盘。

另外还有使用串口或者IIC、SPI接口的键盘芯片,这些使用常见的串口、IIC、SPI通信协议实现。

但是这些都一个以上的I/O口,不是真正的用多个按键。

如何实现单片机用一个I/O采集多个按键信号?

那么有没有更简单的办法,使用更少的I/O口资源检测更多的按键呢?

二、基于模数转换的AD键盘

我们知道按键检测实际上是检测连接按键的端口的高低电平值,在单个I/O口检测单个按键时,只是简单的判断连接按键的端口的电平是高电平(+5V)还是低电平(0V)。那么是否可以通过电平的微小变化来检测按键是否被按下呢?

下图为一个A/D键盘的原理图,从图中可以看出,当不同的按键被按下时,ADC端点处的电压不同,通过判断不同的电压值就可以判断出是那个按键被按下。

如何实现单片机用一个I/O采集多个按键信号?

对于具有AD转换功能的单片机来说,直接接到一个AD通道即可。对于没有AD转换功能的单片机,可采用一个AD转换模块。

对于这种按键,有以下缺点:

1、对于同一点处的电压值,A/D多次采样的结果不可能完全相同。

2)、电阻的误差。电阻值由于电阻的精度和环境温度的原因,误差较大,所以A/D键盘各个按键点的分压不准确。

3)、为尽量减少误差,可以采取增加电阻精度、增加温度补偿等方法,另外在软件处理时候要注意消除按键抖动等因素,还要对实际转换值和标准值给出误差补偿。

4)、如果按键按下,经过A/D转换,若实际转换值在允许误差范围之内(需要实际测量各点电压,并计算各点电压平均值),则认为按键按下,否则程序不响应。

5)、实际试验过程中,还要考虑电阻的累积误差,选用精度越高的电阻,可分辨的按键数目越多。

转自:畅学单片机

围观 10
20

时间片轮询法,在很多书籍中有提到,而且有很多时候都是与操作系统一起出现,也就是说很多时候是操作系统中使用了这一方法。不过我们这里要说的这个时间片轮询法并不是挂在操作系统下,而是在前后台程序中使用此法。也是本文要详细说明和介绍的方法。

对于时间片轮询法,虽然有不少书籍都有介绍,但大多说得并不系统,只是提提概念而已。下面本人将详细介绍本人模式,并参考别人的代码建立的一个时间片轮询架构程序的方法,我想将给初学者有一定的借鉴性。

这里我们先介绍一下定时器的复用功能。

使用1个定时器,可以是任意的定时器,这里不做特殊说明,下面假设有3个任务,那么我们应该做如下工作:

1. 初始化定时器,这里假设定时器的定时中断为1ms(当然你可以改成10ms,这个和操作系统一样,中断过于频繁效率就低,中断太长,实时性差)。

2. 定义一个数值:

代码:
#define TASK_NUM (3) // 这里定义的任务数为3,表示有三个任务会使用此定时器定时。
uint16 TaskCount[TASK_NUM] ; // 这里为三个任务定义三个变量来存放定时值
uint8 TaskMark[TASK_NUM]; // 同样对应三个标志位,为0表示时间没到,为1表示定时时间到。

3. 在定时器中断服务函数中添加:

代码:
void TimerInterrupt(void)
{
uint8 i;

for (i=0; i
{
if (TaskCount[i])
{
TaskCount[i]--;
if (TaskCount[i] == 0)
{
TaskMark[i] = 0x01;
}
}
}
}

代码解释:定时中断服务函数,在中断中逐个判断,如果定时值为0了,表示没有使用此定时器或此定时器已经完成定时,不着处理。否则定时器减一,知道为零时,相应标志位值1,表示此任务的定时值到了。

4. 在我们的应用程序中,在需要的应用定时的地方添加如下代码,下面就以任务1为例:

代码:
TaskCount[0] = 20; // 延时20ms
TaskMark[0] = 0x00; // 启动此任务的定时器

到此我们只需要在任务中判断TaskMark[0] 是否为0x01即可。其他任务添加相同,至此一个定时器的复用问题就实现了。用需要的朋友可以试试,效果不错哦。

通过上面对1个定时器的复用我们可以看出,在等待一个定时的到来的同时我们可以循环判断标志位,同时也可以去执行其他函数。

循环判断标志位:
那么我们可以想想,如果循环判断标志位,是不是就和上面介绍的顺序执行程序是一样的呢?一个大循环,只是这个延时比普通的for循环精确一些,可以实现精确延时。

执行其他函数:
那么如果我们在一个函数延时的时候去执行其他函数,充分利用CPU时间,是不是和操作系统有些类似了呢?但是操作系统的任务管理和切换是非常复杂的。

下面我们就将利用此方法架构一直新的应用程序。

时间片轮询法的架构:

1.设计一个结构体:

代码:
// 任务结构
typedef struct _TASK_COMPONENTS
{
uint8 Run; // 程序运行标记:0-不运行,1运行
uint8 Timer; // 计时器
uint8 ItvTime; // 任务运行间隔时间
void (*TaskHook)(void); // 要运行的任务函数
} TASK_COMPONENTS; // 任务定义

这个结构体的设计非常重要,一个用4个参数,注释说的非常详细,这里不在描述。

2. 任务运行标志出来,此函数就相当于中断服务函数,需要在定时器的中断服务函数中调用此函数,这里独立出来,并于移植和理解。

代码:
void TaskRemarks(void)
{
uint8 i;
for (i=0; i// 逐个任务时间处理
{
if (TaskComps[i].Timer) // 时间不为0
{
TaskComps[i].Timer--; // 减去一个节拍
if (TaskComps[i].Timer == 0) // 时间减完了
{
TaskComps[i].Timer = TaskComps[i].ItvTime; // 恢复计时器值,从新下一次
TaskComps[i].Run = 1; // 任务可以运行
}
}
}
}

大家认真对比一下次函数,和上面定时复用的函数是不是一样的呢?

3. 任务处理

代码:
void TaskProcess(void)
{
uint8 i;
for (i=0; i// 逐个任务时间处理
{
if (TaskComps[i].Run) // 时间不为0
{
TaskComps[i].TaskHook(); // 运行任务
TaskComps[i].Run = 0; // 标志清0
}
}
}

此函数就是判断什么时候该执行那一个任务了,实现任务的管理操作,应用者只需要在main()函数中调用此函数就可以了,并不需要去分别调用和处理任务函数。

到此,一个时间片轮询应用程序的架构就建好了,大家看看是不是非常简单呢?此架构只需要两个函数,一个结构体,为了应用方面下面将再建立一个枚举型变量。

下面我就就说说怎样应用吧,假设我们有三个任务:时钟显示,按键扫描,和工作状态显示。

1. 定义一个上面定义的那种结构体变量

代码:

static TASK_COMPONENTS TaskComps[] =
{
{0, 60, 60, TaskDisplayClock}, // 显示时钟
{0, 20, 20, TaskKeySan}, // 按键扫描
{0, 30, 30, TaskDispStatus}, // 显示工作状态
// 这里添加你的任务。。。。
};

来源:网络

围观 5
36

1 系统设计原理

步进电机控制系统主要由单片机、键盘LED、驱动/放大和PC上位机等4个模块组成,其中PC机模块是软件控制部分。为保护单片机控制系统硬件电路,在单片机和步进电机之间增加过流保护电路。图l为步进电机控制系统框图。

5分钟实现单片机步进电机控制设计
图1 步进电机控制系统框图

2 系统硬件电路设计

2.1 单片机模块

单片机模块主要由MSP430FG4618单片机及外围滤波、电源管理和晶振等电路组成。

MSP430FG4618单片机内部的8 KB RAM和116 KB Flash满足控制系统的存储要求,P1和P2端口在步进电机工作过程中根据按键状态判断是否跳入中断服务程序来改变步进电机的工作状态,USART模块实现单片机和PC上位机之间的通信,实现PC机对步进电机控制。

5分钟实现单片机步进电机控制设计
图2 单片机模块设计结构框图

2.2 键盘/LED模块

为实现人机对话,该系统设计扩展了3x4按钮矩阵键盘和4片8段LED数码管,可手动直接操作该控制系统。

系统上电后,通过键盘输入步进电机的启停、步数转速和转向等,由LED管动态显示步进电机的转速和转向。键盘的输入和LED管的输出由8279进行控制,减少单片机工作负担。

8279编程工作在键盘扫描输入方式,读入键盘时具有去抖动功能,避免误触发。图3为键盘LED模块设计结构框图。

5分钟实现单片机步进电机控制设计
图3 键盘LED模块框图

2.3 驱动/放大模块

控制系统采用步进电机控制用的脉冲分配器(又称逻辑转换器)PMM8713,该器件是CMOS集成电路,相输出驱动能力(源电流或吸入电源)为20 mA,适用于控制三相或四相步进电机,可选择下列6种激励方式:三相步进电进:1相,2相,1-2相;四相步进电进:1相,2相,1-2相。输入方式可选择单时钟(加方向信号)和双时钟(正转或反转时钟)两种方式,具有正反转控制、初始化复位、原点监视、激励方式监视和输入脉冲监视等功能。

5分钟实现单片机步进电机控制设计
图4 驱动放大电路

3 系统软件设计

3.1 单片机程序

利用单片机的定时器TIMER_A(TA)中断产生脉冲信号,通过在响应的中断程序中实现步进电机步数和圈数的准确计数,通过PWM实现转速控制;利用P1.0端口的中断关闭TA中断程序,并推入堆栈,停止电机;P1.1中断则开启TA中断,堆栈推入程序计数器(PC),开启电机;P3.1端口输出高电平由PMM8713的U/D端口控制电机的转向;P3.0~P3.7端口接8279的8个数据接口,当单片机扫描到矩阵键盘有键按下时,利用P2端口的中断设置TA,控制启停、调速和转向等,同时单片机反馈给8279控制LED管显示转速和转向。其程序流程如图5所示。

5分钟实现单片机步进电机控制设计
图5 单片机程序流程

3.2 PC上位机模块

PC上位机模块实现PC机对步进电机的控制。利用MSP430单片机的USART模块实现与PC上位机的通信,PC机通过串口向单片机发送控制命令,实现电机控制。

单片机所接收到控制命令暂存在RXBUFFER中,然后与存储在片内Flash的中断程序的入口地址相比较,相同就进入中断,实现步进电机的控制。操作该模块时需要开启8 MHz晶振为USART模块设置波特率(设置波特率为9 600)。

控制软件由VB6.0编写,利用MSComm控件实现串行通讯功能。其控制软件界面如图6所示。

5分钟实现单片机步进电机控制设计
图6 控制软件界面

4 系统检测

为检验该控制系统的实际工作情况,在给定PMM2101输出工作电流的状态下采用能量转化法测得步进电机输出的最大静转矩。选取输出电流间隔0.2 A,测到步进电机最大静转矩与电流之间关系的静特性曲线,如图7所示,说明该控制系统设计较合理。

5分钟实现单片机步进电机控制设计
静特性曲线

来源:电子发烧友网

围观 7
32

在三位数字中,从左至右的第一、第二位为有效数字,第三位表示前两位数字乘10的N次方(单位为Ω)。如果阻值中有小数点,则用“R”表示,并占一位有效数字。例如:标示为“103”的阻值为10&TImes;10=10kΩ;标示为“222”的阻值为2200Ω即2.2kΩ;标示为“105”的阻值为1MΩ。需要注意的是,要将这种标示法与一般的数字表示方法区别开来,如标示为220的电阻器阻值为22Ω,只有标志为221的电阻器阻值才为220Ω。

标示为“0”或…000”的排阻阻值为OΩ,这种排阻实际上是跳线(短路线)。
一些精密排阻采用四位数字加一个字母的标示方法(或者只有四位数字)。前三位数字分别表示阻值的百位、十位、个位数字,第四位数字表示前面三个数字乘10的N次方,单位为欧姆;数字后面的第一个英文字母代表误差(G=2%、F=1%、D=0.25%、B=O.1%、A或W=0.05%、Q=0.02%、T=0.01%、V=0.005%)。如标示为“2341”的排阻的电阻为234&TImes;10=2340Ω。

一文看懂单片机排阻的作用

排阻的作用

内存芯片下方均匀分布的“芝麻粒”,实际上是位于内存颗粒和金手指之间的“排阻”。排阻,是一排电阻的简称。我们知道,内存在处理、传输数据时会产生大小不一的工作电流。而在内存颗粒走线的必经之处安装一排电阻,则能够帮助内存起到稳压作用,让内存工作更稳定。从而提升内存的稳定性,增强内存使用寿命。而你说的内存右边角上的“小绿豆”。我们一般称之为SPD。SPD是一存储体,它存储了厂商对内存的详细配置信息:如内存的工作电压,位宽,操作时序等。每次开机后自检时,系统都会首先读取内存SPD中的相关信息,来自动配置硬件资源,以避免出错。上拉、限流。和普通电阻一样,相比而言简化了PCB的设计、安装,减小空间,保证焊接质量。

排阻引脚说明

一文看懂单片机排阻的作用

1与a2与b3与c4与d之间的电阻都是10欧,与其它的管脚没有任何关系.就是一排电阻,做在了一个原件上.

有的还有一个公脚,就是为了方便使用,拿万用表量一下就会发现所有脚对公共脚的阻值均是标称值,除公共脚外其它任意两脚阻值是标称值的两倍,很明显任意两脚通过公共脚脚串联的嘛!用在有很多上下拉电阻的场合应用特方便,比如并行通讯线上,还节省空间。

51单片机最小系统排阻作用

起上拉作用:

上拉就是将不确定的信号通过一个电阻嵌位在高电平,电阻同时起限流作用,下拉同理。上拉是对器件注入电流,下拉是输出电流,弱强只是上拉电阻的阻值不同,没有什么严格区分,对于非集电极(或漏极)开路输出型电路(如普通门电路)提升电流和电压的能力是有限的,上拉电阻的功能主要是为集电极开路输出型电路输出电流通道。

另外其他I/O口都是准双向口且都有驱动能力,P0口也是准双向口但是驱动能力小,加排阻说白了就是给P0加驱动电路,电源通过排阻向P0口供电,使其能够驱动与P0口相连的元件。

一文看懂单片机排阻的作用

单片机中排阻的焊接方法

先找出排阻的公共端。公共端在排阻标有小白点的一侧。也可以用万用表电阻档测量一下,任意选择一端,测量该端与其余引脚的电阻,若个引脚的电阻相等,该端为公共端,否则,另一端为公共端。公共端连接单片机电源,其它引脚分别连接单片机IO口。具体焊接方法与焊接普通电阻一样,只是引脚多一点而已。可先焊接两端,定位后,再焊接中间引脚。

如图:带点的一端为排阻的公共端。

一文看懂单片机排阻的作用

RP是排阻,J0就接数码管的段码。把排阻有字的一面对着自己,最左端有一个圆点右或方点,对应的管脚就是公共脚了。

一文看懂单片机排阻的作用

来源:网络

围观 8
69

很多单片机项目恐怕都是没有操作系统的前后台结构,就是main函数里用while无限循环各种任务,中断处理紧急任务。这种结构最简单,上手很容易,可是当项目比较大时,这种结构就不那么适合了,编写代码前你必须非常小心的设计各个模块和全局变量,否则最终会使整个代码结构杂乱无序,不利于维护,而且往往会因为修改了某部分代码而莫名其妙的影响到其他功能,而使调试陷入困境。

改变其中局面的最有效措施当然是引入嵌入式操作系统,但是大多数的操作系统都是付费的(特别是商业项目)。我们熟悉的uc-os/II如果你应用于非商业项目它是免费的,而应用于商业项目的话则要付费,而且价格不菲。

我们也可以自己编写一套嵌入式OS,这当然最好了。可要编写一套完整的OS并非易事,而且当项目并不是非常复杂的话也不需要一个完整的os支持。我们只要用到OS最基本的任务调度和上下文切换就够了。正是基于这样的想法,最近的一个项目中我就尝试采用事件驱动的思想重新构建了代码架构,实际使用的效果还不错,在这里做个总结。

本质上新架构仍然是前后台结构,只不过原来的函数直接调用改成通过指向函数的指针来调用。实际上这也是嵌入式OS任务调度的一个核心。

C语言中可以定义指向函数的指针:
void (*handle)(void);

这里的handle就是一个指向函数的指针,我们只要将某函数的函数名赋给该指针,就能通过实现函数的调用了:

void func1(void)
{
// Code
}
handle = func1;
(*handle)(); // 实现func1的调用

有了这个函数调用新方法,我们就可以想办法将某个事件与某个函数关联,实现所谓的事件驱动。例如,按键1按下就是一个事件,func1响应按键1按下事件。但是,如果是单纯的调用方法替代又有什么意义呢?这又怎么会是事件驱动呢?关键就在于使用函数指针调用方法可以使模块和模块之间的耦合度将到最低。一个例子来说明这个问题,一个按键检测模块用于检测按键,一个电源模块处理按键1动作。

传统的前后台处理方法:
main.c

void main()
{
...
while(1)
{
...
keyScan();
if(flagKeyPress)
{
keyHandle(); // 检测到按键就设置flagKeyPress标志,进入处理函数
}
}
}

key.c

void keyHandle(void)
{
switch (_keyName) // 存放按键值的全局变量
{
...
case KEY1: pwrOpen(); break;
case KEY2: pwrClose(); break;
}
}

power.c

void pwrOpen(void)
{
...
}
void pwrClose(void)
{
...
}

这样的结构的缺点在哪里呢?

1. key代码中直接涉及到power代码的函数,如果power代码里的函数变更,将引起key代码的变更

2. 一个按键值对应一个处理函数,如果要增加响应处理函数就要再次修改key代码

3. 当项目越来越大时,引入的全局变量会越来越多,占用过多的内存

很显然key模块与其他模块的耦合程度太高了,修改其他模块的代码都势必去修改key代码。理想的状态是key模块只负责检测按键,并触发一个按键事件,至于这个按键被哪个模块处理,它压根不需要知道,大大减少模块之间的耦合度,也减少出错的几率。这不正好是事件驱动的思想吗?

接下来,该如何实现呢?

事件驱动的实现

需要一个事件队列:

u16 _event[MAX_EVENT_QUEUE];

它是一个循环队列,保存事件编号,我们可以用一个16位数为各种事件编号,可以定义65535个事件足够使用了。

一个处理函数队列:

typedef struct
{
u16 event; // 事件编号
void (*handle)(void); // 处理函数
}handleType;

handleType _handle[MAX_HANDLE_QUEUE];

它实际是一个数组,每个元素保存事件编号和对应的处理函数指针。

一个驱动函数:

void eventProc(void)
{
u16 event;
u8 i;
if((_eventHead!=_eventTail) || _eventFull) // 事件队列里有事件时才处理
{
event = _eq[_eventHead];
_event [_eventHead++] = 0; // 清除事件

if(_eventHead>= MAX_EVENT_QUEUE)
{
_eventHead= 0; // 循环队列
}
// 依次比较,执行与事件编号相对应的函数
for(i=0; i<_handleTail; i++)
{
if(_handle[i].event == event)
{
(*_handle[i].handle)();
}
}
}
}

main函数可以精简成这样:

void main(void)
{
...
while(1)
{
eventProc();
}
}

这样代码的缺陷是需要为处理函数队列分配一个非常大的内存空间,项目越复杂分配的空间越大,显然不可取。需要做个变通,减少内存空间的使用量。

改进与变通

这样代码的缺陷是需要为处理函数队列分配一个非常大的内存空间,项目越复杂分配的空间越大,显然不可取。需要做个变通,减少内存空间的使用量。

typedef struct
{
void (*handle)(u16 event); // 仅保存模块总的散转函数
}handleType;
handleType _handle[MAX_HANDLE_QUEUE];

修改驱动函数:

void eventProc(void)
{
u16 event;
u8 i;
if((_eventHead!=_eventTail) || _eventFull) // 事件队列里有事件时才处理
{
...
for(i=0; i<_handleTail; i++)
{
(*_handle[i].handle)(event); // 将事件编号传递给模块散转函数
}
}
}

把散转处理交回给各模块,例如power模块的散转函数:

void pwrEventHandle(u16 event)
{
switch (event)
{
...
case EVENT_KEY1_PRESS: pwrOpen(); break;
...
}
}

在power模块的初始化函数中,将该散转函数加入到处理函数队列中:
// 该函数在系统初始化时调用
void pwrInit(void)
{
...
addEventListener(pwrEventHandle);
...
}

addEventListener定义如下:
void addEventListener(void (*pFunc)(u16 event))
{
if(!_handleFull)
{
_handle[_handleTail].handle = pFunc;
_handleTail++;

if(_handleTail>= MAX_HANDLE_QUEUE)
{
_handleFull= TRUE;
}
}
}

每个模块都定义各自的散转处理,然后在初始化的时候将该函数存入处理事件队列中,即能实现事件处理又不会占用很多的内存空间。

加入到事件队列需要封装成统一的函数dispatchEven,由各模块直接调用。例如,key模块就可以dispatchEvent(EVENT_KEY1_PRESS)来触发一个事件

void dispatchEvent(u16 event)
{
u8 i;
bool canDispatch;
canDispatch = TRUE;
if(!_eventFull)
{
// 为了避免同一事件被多次加入到事件队列中
for(i=_eventHead; i!=_eventTail;)
{
if(_event[i] == event)
{
canDispatch = FALSE;
break;
}
i++;
if(i >= MAX_EVENT_QUEUE)
{
i = 0;
}
}
if(canDispatch)
{
_event[_eventTail++] = event;
if(_eventTail>= MAX_EVENT_QUEUE)
{
_eventTail= 0;
}
if(_eventTail== _eventHead)
{
_eventFull = TRUE;
}
}
}
}

深一步:针对与时间相关的事件

对于与时间相关的事件(循环事件和延时处理事件)需要做进一步处理。

首先要设定系统Tick,可以用一个定时器来生成,例如配置一个定时器,每10ms中断一次。
注:tick一般指os的kernel计时单位,用于处理定时、延时事件之类。一般使用硬件定时器中断处理tick事件

定义一个时间事件队列:

typedef struct
{
u8 type; // 事件类别,循环事件还是延时事件
u16 event; // 触发的事件编号
u16 timer; // 延时或周期时间计数器
u16 timerBackup; // 用于周期事件的时间计数备份
}timerEventType;
timerEventType _timerEvent[MAX_TIMER_EVENT_Q];

在定时器Tick中断中将时间事件转换成系统事件:

void SysTickHandler(void)
{
...
for(i=0; i<_timerEventTail; i++)
{
_timerEvent[i].timer--;
if(_timerEvent[i].timer == 0)
{
dispatchEvent(_timerEvent[i].event);// 事件触发器

if(_timerEvent[i].type == CYCLE_EVENT)
{
// 循环事件,重新计数
_timerEvent[i].timer = _timerEvent[i].timerBackup;
}
else
{
// 延时事件,触发后删除
delTimerEvent(_timerEvent[i].event);
}
}
}
}

将增加和删除时间事件封装成函数,便以调用:

void addTimerEvent(u8 type, u16 event, u16 timer)
{
_timerEvent[_timerEventTail].type = type;
_timerEvent[_timerEventTail].event = event;
_timerEvent[_timerEventTail].timer = timer; // 时间单位是系统Tick间隔时间
_timerEvent[_timerEventTail].timerBackup = timer; // 延时事件并不使用
_timerEventTail++;
}

void delTimerEvent(u16 event)
{
...
for(i=0; i<_timerEventTail; i++)
{
if(_timerEvent[i].event == event)
{
for(j=i; j<_timerEventTail; j++)
{
_timerEvent[j] = _timerEvent[j+1];
}
_timerEventFull= FALSE;
_timerEventTail--;
}
}
}

对于延时处理,用事件驱动的方法并不理想,因为这可能需要将一段完整的代码拆成两个函数,破坏了代码的完整性。解决的方法需要采用OS的上下文切换,这就涉及到程序堆栈问题,用纯C代码不容易实现。

——【感谢】资料来源于https://wenku.baidu.com/view/5465391d10a6f524ccbf8591.html

转自:Engraver

围观 9
72

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单片机设计

围观 4
273

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

围观 31
325

工业和家用电器市场中的各种应用要求使用数学运算来实现不同的算法和计算。基于 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工程师社区

围观 14
614

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

取指令的任务是:根据程序计数器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就是这样一条一条地执行指令,完成所有规定的功能。

来源:网络

围观 12
200

由于单片机的性能同电脑的性能是天渊之别的,无论从空间资源上、内存资源、工作频率,都是无法与之比较的。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 N=M/8 可以改为N=M>>3
说明:通常如果需要乘以或除以2n,都可以用移位的方法代替。如果乘以2n,都可以生成左移的代码,而乘以其它的整数或除以任何数,均调用乘除法子程序。用移位的方法得到代码比调用乘除法子程序生成的代码效率高。实际上,只要是乘以或除以一个整数,均可以用移位的方法得到结果。如N=M*9可以改为N=(M

(4) 自加自减的区别
例如我们平时使用的延时函数都是通过采用自加的方式来实现。
void DelayNms(UINT16 t)
{
UINT16 i,j;
for(i=0;i for(j=0;i }
可以改为
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 {
uncrcReg = (uncrcReg > 8)
^ *buf++) & 0xFF];
uncrcConst }
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 {
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 for(i=0;i

指针方法的优点是,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] 结果: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 a|= Byte2Bits._bit6 a|= Byte2Bits._bit5 a|= Byte2Bits._bit4 a|= Byte2Bits._bit3 a|= Byte2Bits._bit2 a|= Byte2Bits._bit1 a|= Byte2Bits._bit0 结果: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 {
for(j=0;j {
………………
}
}
较大的循环嵌套较小的循环编译器会浪费更加多的时间,推荐的做法就是较小的循环嵌套较大的循环。
UINT8 i,j;
for(j=0;j {
for(i=0;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

围观 9
145

页面

订阅 RSS - 单片机