单片机

一、总线概述

计算机系统是以微处理器为核心的,各器件要与微处理器相连,且必须协调工作,所以在微处理机中引入了总线的概念,各器件共同享用总线,任何时候只能有一个器件发送数据(可以有多个器件同时接收数据)。

计算机的总线分为控制总线、地址总线和数据总线等三种。而数据总线用于传送数据,控制总线用于传送控制信号,地址总线则用于选择存储单元或外设。

二、单片机的三总线结构

51系列单片机具有完善的总线接口时序,可以扩展控制对象,其直接寻址能力达到64k( 2的16次方) 。在总线模式下,不同的对象共享总线,独立编址、分时复用总线,CPU通过地址选择访问的对象,完成与各对象之间的信息传递。

“5分钟了解单片机数据、地址、控制总线结构!"

单片机三总线扩展示意如图1所示。

1、数据总线

51单片机的数据总线为P0口,P0口为双向数据通道,CPU从P0口送出和读回数据。

2、地址总线

51系列单片机的地址总线为16位。

为了节约芯片引脚,采用P0口复用方式,除了作为数据总线外,在ALE信号时序匹配下,通过外置的数据锁存器,在总线访问前半周期从P0口送出低8位地址,后半周期从P0口送出8位数据。

高8位地址则通过P2口送出。

3、控制总线

51系列单片机的控制总线包括读控制信号P3.7和写控制信号P3.6等,二者分别作为总线模式下数据读和数据写的使能信号。

三、单片机总线时序分析

51 单片机总线时序如图2 所示。

“5分钟了解单片机数据、地址、控制总线结构!"

从图2中可以看出,完成一次总线(读写)操作周期为T,P0口分时复用,在T0期间,P0口送出低8位地址,在ALE的下降沿完成数据锁存,送出低8位地址信号。在T1期间,P0口作为数据总线使用,送出或读入数据,数据的读写操作在读、写控制信号的低电平期间完成。

需要注意的是,在控制信号(读、写信号)有效期间,P2口送出高8位地址,配合数据锁存器输出的低8 位地址,实现16位地址总线,即64kB范围的内的寻址。

由于CPU不可能同时执行读和写操作,所以读、写信号不可能同时有效。

四、常见单片机编址电路

1、简单地址扩展

51单片机的P2口可以直接作为高8位地址总线使用,在一些简单系统电路中,常使用P2口直接编址驱动。

下面以使用数据缓冲器74LS273驱动数码显示为例,分析P2口编址驱动的静态数码显示电路的设计。

一位LED数码显示单元电路如图3所示。

“5分钟了解单片机数据、地址、控制总线结构!"

WR与A8(P2.0)相或提供74LS273的时钟信号,当执行“MOVX @DPTR,A”指令时,地址信息由DPTR寄存器确定,会出现有效的写信号WR,只有当地址A8为满足“0”时,写信号才可以作为74LS273的时钟信号输入,完成数据锁存。

P2口为A8~A15的8位地址线,很容易扩展到8只LED数码管,WR信号分别与A8~A15按或关系连接,每位地址线均为低电平有效,即可实现8个有效地址。

该方案电路简单,但有效地址数太少,不适用于复杂系统设计。

2、低8位地址锁存

通常的设计电路是使用8D锁存器74LS373实现地址锁存,74HC573与之逻辑功能相同,只是引脚布局不一样,使用74HC573布线更容易。

74LS373真值表如图4所示。

“5分钟了解单片机数据、地址、控制总线结构!"

在输出允许OE为L、控制使能LE为H时,输出为跟随状态;OE为L、LE为L时,输出为保持状态。

地址锁存电路如图5所示。OE接地,LE接单片机的ALE脚将产生满足时序的低8位地址信号。

执行以下三条指令会得到如图6所示的时序图。

MOV DPTR,# 0FF55H; 低8 位地址为55H

MOV A,# 0AAH; 待发送数据0AAH→A( 55H 取反)

MOVX,@DPTR,A; A 中的0AAH送地址为0FF55H 的对象中会。

“5分钟了解单片机数据、地址、控制总线结构!"

从图6中可以看出,P0口先送55H,在ALE下降沿实现地址锁存,随后送出数据0AAH,在WR有效( 低电平)期间锁存器输出低8位地址55H,P0口送出数据0AAH。

3、带译码器的复杂地址接口电路

理论上高8位地址线可以产生256个有效地址,如何实现地址“扩展”呢?地址扩展准确描述是地址译码,例如3根地址线可以译码成8 个地址,4根译码成16个有效地址。这里选择3-8译码器实现地址译码,电路图以及对应的编址如表1 所示。

“5分钟了解单片机数据、地址、控制总线结构!"

五、单片机总线编址电路实例

总线扩展接口的单片机系统,包括外部32k RAM扩展、LCD1602接口、输入输出口。

“5分钟了解单片机数据、地址、控制总线结构!"

D0~D7接数据总线P0口,地址线A0~A14接单片机地址总线低15位,单片机地址线A15接RAM片选信号,低电平有效,这样RAM地址分配从0000H到7FFFH,与74138译码地址不冲突。

LCD1602接口电路如图9所示。

“5分钟了解单片机数据、地址、控制总线结构!"

RS、RW分别接A12、A13,使能信号编址为Y7,这样LCD的四个驱动地址(数据读写和命令读写)为0CFFFH到0FFFFH(无关位为1)或者8700H到0B700H(无关位为0)。

有些时候单片机引脚不够用,还要进行扩展,输入口扩展电路如图10 所示。

“5分钟了解单片机数据、地址、控制总线结构!"

利用74HC573(74LS373)的高阻态功能,将其输出Q0~Q7接P0口,在满足总线地址读操作中,可以把输入InPORT的数据读入单片机的累加器,地址为0F8FFH或8000H。

输出口扩展电路如图11所示。

“5分钟了解单片机数据、地址、控制总线结构!"

利用74LS273数据锁存功能,在满足总线地址写操作中,可以把单片机累加器里的数据写入273 锁存输出,地址为0F8FFH或8000H。由于所用控制总线不同,可以和输入共用地址。

六、结束语

总线扩展是设计单片机控制电路必须掌握的技术,大量的特殊功能IC都支持总线接口,如ADC0809,TLC7528,DDS器件AD9851等。

总线接口的要点就是在严格的控制时序下,总线被分时复用,以实现复杂系统设计。

来源:CSDN博客
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 155

摘要:不知道大家有没有这样一种感觉,就是感觉自己玩单片机还可以,各个功能模块也都会驱动,但是如果让你完整的写一套代码,却无逻辑与框架可言,上来就是开始写!东抄抄写抄抄。说明编程还处于比较低的水平,那么如何才能提高自己的编程水平呢?学会一种好的编程框架或者一种编程思想,可能会受用终生!比如模块化编程,框架式编程,状态机编程等等,都是一种好的框架。

今天说的就是状态机编程,由于篇幅较长,大家慢慢欣赏。那么状态机是一个这样的东东?状态机(state machine)有5个要素,分别是状态(state)、迁移(transition)、事件(event)、动作(action)、条件(guard)。

1、什么是状态机?

状态机是一个这样的东东:状态机(state machine)有 5 个要素,分别是状态(state)、迁移(transition)、事件(event)、动作(action)、条件(guard)。

状态:一个系统在某一时刻所存在的稳定的工作情况,系统在整个工作周期中可能有多个状态。例如一部电动机共有正转、反转、停转这 3 种状态。

一个状态机需要在状态集合中选取一个状态作为初始状态。

迁移:系统从一个状态转移到另一个状态的过程称作迁移,迁移不是自动发生的,需要外界对系统施加影响。停转的电动机自己不会转起来,让它转起来必须上电。

事件:某一时刻发生的对系统有意义的事情,状态机之所以发生状态迁移,就是因为出现了事件。对电动机来讲,加正电压、加负电压、断电就是事件。

动作:在状态机的迁移过程中,状态机会做出一些其它的行为,这些行为就是动作,动作是状态机对事件的响应。给停转的电动机加正电压,电动机由停转状态迁移到正转状态,同时会启动电机,这个启动过程可以看做是动作,也就是对上电事件的响应。

条件:状态机对事件并不是有求必应的,有了事件,状态机还要满足一定的条件才能发生状态迁移。还是以停转状态的电动机为例,虽然合闸上电了,但是如果供电线路有问题的话,电动机还是不能转起来。

只谈概念太空洞了,上一个小例子:一单片机、一按键、俩 LED 灯(记为L1和L2)、一人, 足矣!

规则描述:

1、L1L2状态转换顺序OFF/OFF--->ON/OFF--->ON/ON--->OFF/ON--->OFF/OFF

2、通过按键控制L1L2的状态,每次状态转换需连续按键5次

3、L1L2的初始状态OFF/OFF

“单片机编程技巧—状态机编程"
图1

下面这段程序是根据功能要求写成的代码。

程序清单List1:

void main(void)
{
 sys_init();
 led_off(LED1);
 led_off(LED2);
 g_stFSM.u8LedStat = LS_OFFOFF;
 g_stFSM.u8KeyCnt = 0;
 while(1)
 {
  if(test_key()==TRUE)
  {
   fsm_active();
  }
  else
  {
   ; /*idle code*/
  }
 }
}
void fsm_active(void)
{
 if(g_stFSM.u8KeyCnt > 3) /*击键是否满 5 次*/
 {
  switch(g_stFSM.u8LedStat)
  {
   case LS_OFFOFF:
    led_on(LED1); /*输出动作*/
    g_stFSM.u8KeyCnt = 0;
    g_stFSM.u8LedStat = LS_ONOFF; /*状态迁移*/
    break;
   case LS_ONOFF:
    led_on(LED2); /*输出动作*/
    g_stFSM.u8KeyCnt = 0;
    g_stFSM.u8LedStat = LS_ONON; /*状态迁移*/
    break;
   case LS_ONON:
    led_off(LED1); /*输出动作*/
    g_stFSM.u8KeyCnt = 0;
    g_stFSM.u8LedStat = LS_OFFON; /*状态迁移*/
    break;
   case LS_OFFON:
    led_off(LED2); /*输出动作*/
    g_stFSM.u8KeyCnt = 0;
    g_stFSM.u8LedStat = LS_OFFOFF; /*状态迁移*/
    break;
   default: /*非法状态*/
    led_off(LED1);
    led_off(LED2);
    g_stFSM.u8KeyCnt = 0;
    g_stFSM.u8LedStat = LS_OFFOFF; /*恢复初始状态*/
    break;
  }
 }
 else
 {
  g_stFSM.u8KeyCnt++; /*状态不迁移,仅记录击键次数*/
 }
}

实际上在状态机编程中,正确的顺序应该是先有状态转换图,后有程序,程序应该是根据设计好的状态图写出来的。不过考虑到有些童鞋会觉得代码要比转换图来得亲切,我就先把程序放在前头了。

这张状态转换图是用UML(统一建模语言)的语法元素画出来的,语法不是很标准,但拿来解释问题足够了。

“图2按键控制流水灯状态转换图"
图2按键控制流水灯状态转换图

圆角矩形代表状态机的各个状态,里面标注着状态的名称。

带箭头的直线或弧线代表状态迁移,起于初态,止于次态。

图中的文字内容是对迁移的说明,格式是:事件[条件]/动作列表(后两项可选)。

“事件[条件]/动作列表”要说明的意思是:如果在某个状态下发生了“事件”,并且状态机

满足“[条件]”,那么就要执行此次状态转移,同时要产生一系列“动作”,以响应事件。在这个例子里,我用“KEY”表示击键事件。

图中有一个黑色实心圆点,表示状态机在工作之前所处的一种不可知的状态,在运行之前状态机必须强制地由这个状态迁移到初始状态,这个迁移可以有动作列表(如图1所示),但不需要事件触发。

图中还有一个包含黑色实心圆点的圆圈,表示状态机生命周期的结束,这个例子中的状态机生生不息,所以没有状态指向该圆圈。

关于这个状态转换图就不多说了,相信大家结合着上面的代码能很容易看明白。现在我们再聊一聊程序清单List1。

先看一下fsm_active()这个函数,g_stFSM.u8KeyCnt = 0;这个语句在switch—case里共出现了 5 次,前 4 次是作为各个状态迁移的动作出现的。从代码简化提高效率的角度来看,我们完全可以把这 5 次合并为 1 次放在 switch—case 语句之前,两者的效果是完全一样的,代码里之所以这样啰嗦,是为了清晰地表明每次状态迁移中所有的动作细节,这种方式和图2的状态转换图所要表达的意图是完全一致的。

再看一下g_stFSM这个状态机结构体变量,它有两个成员:u8LedStat和 u8KeyCnt。用这个结构体来做状态机好像有点儿啰嗦,我们能不能只用一个像 u8LedStat 这样的整型变量来做状态机呢?

当然可以!我们把图 2中的这 4 个状态各自拆分成 5 个小状态,这样用 20 个状态同样能实现这个状态机,而且只需要一个 unsigned char 型的变量就足够了,每次击键都会引发状态迁移, 每迁移 5 次就能改变一次 LED 灯的状态,从外面看两种方法的效果完全一样。

假设我把功能要求改一下,把连续击键5次改变L1L2的状态改为连续击键100次才能改变L1L2的状态。这样的话第二种方法需要4X100=400个状态!而且函数fsm_active()中的switch—case语句里要有400个case,这样的程序还有法儿写么?!

同样的功能改动,如果用g_stFSM这个结构体来实现状态机的话,函数fsm_active()只需要将if(g_stFSM.u8KeyCnt>3)改为if(g_stFSM.u8KeyCnt > 98)就可以了!

g_stFSM结构体的两个成员中,u8LedStat可以看作是质变因子,相当于主变量;u8KeyCnt可以看作是量变因子,相当于辅助变量。量变因子的逐步积累会引发质变因子的变化。

像g_stFSM这样的状态机被称作Extended State Machine,我不知道业内正规的中文术语怎么讲,只好把英文词组搬过来了。

2、状态机编程的优点

说了这么多,大家大概明白状态机到底是个什么东西了,也知道状态机化的程序大体怎么写了,那么单片机的程序用状态机的方法来写有什么好处呢?

(1)提高CPU使用效率

话说我只要见到满篇都是delay_ms()的程序就会蛋疼,动辄十几个ms几十个ms的软件延时是对CPU资源的巨大浪费,宝贵的CPU机时都浪费在了NOP指令上。那种为了等待一个管脚电平跳变或者一个串口数据而岿然不动的程序也让我非常纠结,如果事件一直不发生,你要等到世界末日么?

把程序状态机化,这种情况就会明显改观,程序只需要用全局变量记录下工作状态,就可以转头去干别的工作了,当然忙完那些活儿之后要再看看工作状态有没有变化。只要目标事件(定时未到、电平没跳变、串口数据没收完)还没发生,工作状态就不会改变,程序就一直重复着“查询—干别的—查询—干别的”这样的循环,这样CPU就闲不下来了。在程序清单 List3 中,if{}else{}语句里else下的内容(代码中没有添加,只是加了一条/*idle code*/的注释示意)就是上文所说的“别的工作” 。

这种处理方法的实质就是在程序等待事件的过程中间隔性地插入一些有意义的工作,好让CPU不是一直无谓地等待。

(2) 逻辑完备性

我觉得逻辑完备性是状态机编程最大的优点。

不知道大家有没有用C语言写过计算器的小程序,我很早以前写过,写出来一测试,那个惨不忍睹啊!当我规规矩矩的输入算式的时候,程序可以得到正确的计算结果,但要是故意输入数字和运算符号的随意组合,程序总是得出莫名其妙的结果。

后来我试着思维模拟一下程序的工作过程,正确的算式思路清晰,流程顺畅,可要碰上了不规矩的式子,走着走着我就晕菜了,那么多的标志位,那么多的变量,变来变去,最后直接分析不下去了。

很久之后我认识了状态机,才恍然明白,当时的程序是有逻辑漏洞的。如果把这个计算器程序当做是一个反应式系统,那么一个数字或者运算符就可以看做一个事件,一个算式就是一组事件组合。对于一个逻辑完备的反应式系统,不管什么样的事件组合,系统都能正确处理事件,而且系统自身的工作状态也一直处在可知可控的状态中。反过来,如果一个系统的逻辑功能不完备,在某些特定事件组合的驱动下,系统就会进入一个不可知不可控的状态,与设计者的意图相悖。

状态机就能解决逻辑完备性的问题。

状态机是一种以系统状态为中心,以事件为变量的设计方法,它专注于各个状态的特点以及状态之间相互转换的关系。状态的转换恰恰是事件引起的,那么在研究某个具体状态的时候,我们自然而然地会考虑任何一个事件对这个状态有什么样的影响。这样,每一个状态中发生的每一个事件都会在我们的考虑之中,也就不会留下逻辑漏洞。

这样说也许大家会觉得太空洞,实践出真知,某天如果你真的要设计一个逻辑复杂的程序,

我保证你会说:哇!状态机真的很好用哎!

(3)程序结构清晰

用状态机写出来的程序的结构是非常清晰的。

“单片机编程技巧—状态机编程"

程序员最痛苦的事儿莫过于读别人写的代码。如果代码不是很规范,而且手里还没有流程图,读代码会让人晕了又晕,只有顺着程序一遍又一遍的看,很多遍之后才能隐约地明白程序大体的工作过程。有流程图会好一点,但是如果程序比较大,流程图也不会画得多详细,很多细节上的过程还是要从代码中理解。

相比之下,用状态机写的程序要好很多,拿一张标准的UML状态转换图,再配上一些简明的文字说明,程序中的各个要素一览无余。程序中有哪些状态,会发生哪些事件,状态机如何响应,响应之后跳转到哪个状态,这些都十分明朗,甚至许多动作细节都能从状态转换图中找到。可以毫不夸张的说,有了UML状态转换图,程序流程图写都不用写。

来源:嵌入式Linux
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:
cathy@eetrend.com)。

围观 172

01、确定任务

开发单片机最小系统。

02、任务分析

该系统具有的功能:

(1)具有2位LED数码管显示功能。
(2)具有八路发光二极管显示各种流水灯。
(3)可以完成各种奏乐,报警等发声音类实验。
(4)具有复位功能。

03、功能分析

(1)两位LED数码管显示功能,我们可以利用单片机的P0口接两个数码管来现这个功能;

(2)八路发光二极管显示可以利用P1口接八个发光二极管实现这个功能;

(3)各种奏乐、报警等发声功能可以采用P2.0这个引脚接一蜂鸣器来实现。

(4)利用单片机的第9脚可以设计成复位系统,我们采用按键复位;利用单片机的18、19脚可以设计成时钟电路,我们利用单片机的内部振荡方式设计的。

04、设计框图

“实例分析!单片机最小系统开发运行的全过程"

05、硬件电路设计

根据本系统的功能,和单片机的工作条件,我们设计出下面的电路图。

“实例分析!单片机最小系统开发运行的全过程"

06、元件清单的确定

数码管:共阴极2只(分立)
电解电容:10UF的一只
30PF的电容2只
220欧的电阻9只
4.7K的电阻一只
1.2K的电阻一只
4.7K的排阻一只,
12MHz的晶振一只
有源5V蜂名器一只
AT89S51单片机一片
常开按钮开关1只
紧锁座一只(方便芯取下来的,绿色的)
发光二极管(5MM红色)8只
万能板电路版15*17CM
S8550三极管一只
4.5V电池盒一只,导线若干。

07、硬件电路的焊接

按照原理图把上面的元件焊接好。

08、相关程序编写

针对上面的电路原理图,设计出本系统的详细功能:

(1)第一个发光二极管点亮,同时数码管显示“1”。
(2)第二个发光二极管点亮,同时数码管显示“2”。
(3)依次类推到第八个发光二极管点亮,同时数码管显示“8”。

以上出现的是流水灯的效果

(4)所有的发光二极管灭了,同时数码管现实“0”。
(5)数码管显示“1”。
(6)数码管显示“2、……”直到“9、A、B、C、D、E、F、Y”。
(7)蜂鸣器发出九声报警声后重复上面所有步骤。
(8)程序如下:

ORG 0000H;伪指令,定义下面的程序代码(机器代码)从地址为0000H的单元存放。

LJMP START;跳转到标号为START的地方去执行。

ORG 0030H;伪指令,定义下面的程序代码(机器代码)从地址为0030H的单元存放。

START:MOV P1,#0FEH ;点亮第一个发光二极管。

CLR P2.7 ;送低电平到第一个数码管,开启数码管。

CLR P2.6 ;送低电平到第二个数码管,开启数码管。

MOV P0,#06H;让数码管显示“1” 。

LCALL DELAY;调用延时子程序,起到延时的目的。

MOV P1,#0FDH;点亮第二个发光二极管。

MOV P0,#5bH;让数码管显示“2” 。

LCALL DELAY;调用延时子程序,起到延时的目的。

MOV P1,#0FBH;点亮第三个发光二极管。

MOV P0,#4fH;让数码管显示“3” 。

LCALL DELAY;调用延时子程序,起到延时的目的。

MOV P1,#0F7H;点亮第四个发光二极管。

MOV P0,#66H;让数码管显示“4” 。

LCALL DELAY;调用延时子程序,起到延时的目的。

MOV P1,#0EFH;点亮第五个发光二极管。

MOV P0,#6dH;让数码管显示“5” 。

LCALL DELAY;调用延时子程序,起到延时的目的。

MOV P1,#0DFH;点亮第六个发光二极管。

MOV P0,#7dH;让数码管显示“6” 。

LCALL DELAY;调用延时子程序,起到延时的目的。

MOV P1,#0BFH;点亮第七个发光二极管。

MOV P0,#07H;让数码管显示“7” 。

LCALL DELAY;调用延时子程序,起到延时的目的。

MOV P1,#7FH;点亮第八个发光二极管。

MOV P0,#7fH;让数码管显示“8” 。

LCALL DELAY;调用延时子程序,起到延时的目的。

MOV P1,#00H;灭了所有的发光二极管。

MOV P0,#3FH;让数码管显示“0” 。

LCALL DELAY;调用延时子程序,起到延时的目的。

MOV P0,#06H;让数码管显示“1” 。

LCALL DELAY;调用延时子程序,起到延时的目的。

MOV P0,#5bH;让数码管显示“2” 。

LCALL DELAY;调用延时子程序,起到延时的目的。

MOV P0,#4fH;让数码管显示“3” 。

LCALL DELAY;调用延时子程序,起到延时的目的。

MOV P0,#66H;让数码管显示“4” 。

LCALL DELAY;调用延时子程序,起到延时的目的。

MOV P0,#6dH;让数码管显示“5” 。

LCALL DELAY;调用延时子程序,起到延时的目的。

MOV P0,#7dH;让数码管显示“6” 。

LCALL DELAY;调用延时子程序,起到延时的目的。

MOV P0,#07H;让数码管显示“7” 。

LCALL DELAY;调用延时子程序,起到延时的目的。

MOV P0,#7fH;让数码管显示“8” 。

LCALL DELAY;调用延时子程序,起到延时的目的。

MOV P0,#6fH;让数码管显示“9” 。

LCALL DELAY;调用延时子程序,起到延时的目的。

MOV R4,#9;送蜂鸣器的报警次数。

LOOP:SETB P2.0;不发声

LCALL DELAY;调用延时子程序,起到延时的目的。

CLR P2.0;发声。

LCALL DELAY;调用延时子程序,起到延时的目的。

DJNZ R4,LOOP;发声九次后向下执行。

SETB P2.0;关闭发声

LJMP START;跳转到标号为START处执行,循环本程序。

DELAY:MOV R7,#200;这是延时子程序。

D1:MOV R6,#200

D2:MOV R5,#200

D3:DJNZ R5,D3

DJNZ R6,D2

DJNZ R7,D1

RET;延时子程序返回指令。

END;结束伪指令。

09、调试和编译

利用KEIL C51 软件对上面的程序进行调试和编译,产生*.HEX文件,以便用编程器写入单片机芯片中。

10、编程器的使用

用编程器把电脑里生成的*.HEX文件写到芯片中。

11、产品调试

通上4.5V(三节电池)的电源,可以调试本系统,如果没有错误就可以正常工作了。

来源:嵌入式ARM
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 65

XTAL1和XTAL2指的是8051系单片机上常见的用于接“晶振”(晶体谐振器-Crystal Resonator”)的两个引脚。从原理上来说,这两个引脚和MCU内部一个反相器相连接。这个反相器与外部的“晶振”组成一个构成一个皮尔斯振荡器(Pierce oscillator)。因为这个振荡器集成在器件内部的组件实在是不能更简单啦,就一个反相器和一个电阻,非常合适于各种数字IC的设计制造流程。

深入地分析这个皮尔斯振荡器的工作原理时,不妨把它表述成以下理想的电路形式:

“深入解析:单片机晶振脚原理是什么?"

模电知识告诉我们,当期望得到一个输出信号频率为图片的振荡电路时,这个电路在图片必须满足两个条件:

“深入解析:单片机晶振脚原理是什么?"的环路相移。
● 闭环增益为1。

在上面的皮尔斯振荡器的电路原理图中,不难发现反相器U1对任意的频率分量均提供了180°,即图片的相移量。同时,反相器在输入输出之间可以看作是一个buffer,因此通过对反相器的输出特性进行调教,较容易得到1的loop gain。

到这里有人会问了,相移量只有图片,上面的两个条件连一个都没达到,这哪能起振呢?问题的关键在于电路中的其它元件上。

首先,对电路中的一颗“晶振”来说,石英晶体本身具有压电效应,对石英晶体进行适当处理后可以得到一种压电谐振器件,这就是常见的石英晶体谐振器(以下简称QCR)。对QCR的物理特性进行分析,可以发现QCR的压电谐振过程可以用以下的理想电路模型近乎完美地表示出来。

“深入解析:单片机晶振脚原理是什么?"

右图的电路模型中,L1-C1-R1组成了一个RLC串联谐振电路,再加上一个实际很小的C0,整个QCR电路模型有两个很接近的谐振点。QCR在电路中与反相器并联,充当的是一个选频网络的作用。整个振荡电路在上电时可以看作是反相器的输出端打进去了一个阶跃信号,QCR把阶跃中谐振点频率的信号挑出来,其他没用的踢掉,在环路增益为1的情况下整个电路趋于稳态平衡。

模电的知识告诉我们,在QCR // inverter的组合下,这个皮尔斯振荡器已经具备了一个理想的振荡电路中的两大网络(选频+放大)。貌似振荡器中的R1和C1//C2没有什么卵用啊。且慢,这个R1和C1//C2,正是这个电路中最美妙的地方。

把R1与C1//C2单独抽出来配合反相器的电路组合貌似并不好理解,假如我们换种方式呢?

“图中引进R’是为了方便理解反相器中的loop
图中引进R’是为了方便理解反相器中的loop voltage gain

右边的运放电路除了反相结构本身提供的-180°相移外,R-C组合也提供了额外的相位延迟。更加奇妙的是这个电路组合在设计得当的情况下能够根据实际电路中各元件的误差自动调整相移大小与反相结构相互匹配(当然了谐振频率也会有少许改变),进而保证整个loop的相移满足条件1。这个“自动调整”的过程推导起来很占篇幅,在这里略过不表。

在上面的图中,还有一个很巧妙的地方,即R1是并联在反相器的输入输出端的。这个小小的电阻和反相器构成了一个反馈通路,进而使得人们能将各种模拟电路的分析设计方法用在这样一个逻辑门电路上,比如通过反馈的方法提高反相器的线性度。在这里将现实电路中反相器的非理想特性引入设计考虑的同时,却又能使电路图保持简洁易懂。

实际的MCU振荡电路是“Isolated” Pierce-Gate Oscillator,要考虑的因素比这个理论模型复杂得多,但根本原理都是一样的。振荡电路输出的波形,通过下一级的时钟发生电路(Clock Generator)进行整形调整后,得到具有稳定形状的矩形信号并输出至时钟树,作用于整个MCU的同步逻辑。

*原文地址:(来源:知乎,文:疯狂的蔬菜)
https://www.zhihu.com/question/30930577/answer/55822425

转载来源:嵌入式ARM
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 150

分享这篇文章,谈一下STM32启动流程。如果读者朋友已经有过汇编相关基础,能够够好理解本文内容。汇编语言是比C语言更接近机器底层的编程语言,能让我们更好的理解和操纵硬件底层。

STM32三种启动模式

下好程序后,重启芯片时,SYSCLK的第4个上升沿,BOOT引脚的值将被锁存,这就是所谓的启动过程。

STM32上电或者复位后,代码区始终从0x00000000开始,其实就是将存储空间的地址映射到0x00000000中。三种启动模式如下:

  • 从主闪存存储器启动,将主Flash地址0x08000000映射到0x00000000,这样代码启动之后就相当于从0x08000000开始。主闪存存储器是STM32内置的Flash,作为芯片内置的Flash,是正常的工作模式。一般我们使用JTAG或者SWD模式下载程序时,就是下载到这个里面,重启后也直接从这启动程序。
  • 从系统存储器启动。首先控制BOOT0、BOOT1管脚,复位后,STM32与上述两种方式类似,从系统存储器地址0x1FFF F000开始执行代码。系统存储器是芯片内部一块特定的区域,芯片出厂时在这个区域预置了一段Bootloader,就是通常说的ISP程序。这个区域的内容在芯片出厂后没有人能够修改或擦除,即它是一个ROM区。启动的程序功能由厂家设置。系统存储器存储的其实就是STM32自带的bootloader代码。
  • 从内置SRAM启动,将SRAM地址0x20000000映射到0x00000000,这样代码启动之后就相当于从0x20000000开始。内置SRAM,也就是STM32的内存,既然是SRAM,自然也就没有程序存储的能力了,这个模式一般用于程序调试。假如我只修改了代码中一个小小的地方,然后就需要重新擦除整个Flash,比较的费时,可以考虑从这个模式启动代码,用于快速的程序调试,等程序调试完成后,在将程序下载到SRAM中。

用户可以通过设置BOOT1和BOOT0引脚的状态,来选择在复位后的启动模式。STM32三种启动模式对应的存储介质均是芯片内置的,如下图:

“STM32单片机的启动过程"

串口下载程序原理

从系统存储器启动,这种模式启动的程序功能是由厂家设置的。一般来说,这种启动方式用的比较少。系统存储器是芯片内部一块特定的区域,STM32在出厂时,由ST在这个区域内部预置了一段BootLoader,也就是我们常说的ISP程序,这是一块ROM,出厂后无法修改。

一般来说,我们选用这种启动模式时,是为了从串口下载程序,因为在厂家提供的BootLoader中,提供了串口下载程序的固件,可以通过这个BootLoader将程序下载到系统的Flash中。

这个下载方式需要以下步骤:

  • 将BOOT0设置为1,BOOT1设置为0,然后按下复位键,这样才能从系统存储器启动BootLoader;
  • 在BootLoader的帮助下,通过串口下载程序到Flash中;
  • 程序下载完成后,又有需要将BOOT0设置为GND,手动复位,这样,STM32才可以从Flash中启动。

从汇编代码分析STM32启动过程

STM32的启动文件与编译器有关,不同编译器,它的启动文件不同。虽然启动文件(汇编)代码各有不同,但它们原理类似,都属于汇编程序。拿基于MDK-ARM的启动文件来举例,说一下要点内容。在基于MDK的启动文件开始,有一段汇编代码是分配堆栈大小的。

“STM32单片机的启动过程"

这里重点知道堆栈数值大小就行。还有一段AREA(区域),表示分配一段堆栈数据段。可以使用STM32CubeMX对上面的数值大小进行配置:

“STM32单片机的启动过程"

在IAR中,是通过工程配置堆栈大小:

“STM32单片机的启动过程"

看下面的汇编代码,程序上电之后,是跳到Reset_Handler这个位置。

“STM32单片机的启动过程"

知道代码是从Reset_Handler开始执行,再来看如下Reset_Handler汇编代码。在启动的时候,执行了SystemInit这个函数。

“STM32单片机的启动过程"

执行完SystemInit函数,初始化了系统时钟,之后跳转到main函数执行。

来源:单片机与嵌入式
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:
cathy@eetrend.com)。

围观 376

程序员多数都用过版本管理工具SVN,该软件具有代码的比较功能,既能很好的管理不同版本的代码,又能比较版本的更改,是一个很好用的软件工具。除此之外,还有哪些代码比较工具呢?和大家分享一下。

俗话说:三句不离本行,对于程序员这个可爱的群体来说也是一样,即使面对无休无止的编程工作,程序员们依旧任劳任怨的埋头苦干,梦想着用自己码下的代码改变世界。工欲善其事,必先利其器,每一位程序员都有自己私藏的编程必备工具,接下来就给大家推荐5款程序员最佳的代码比较工具。

一、Beyond Compare

Beyond Compare可以很方便地对比出两份源代码文件之间的不同之处,相差的每一个字节用颜色加以表示,查看方便,支持多种规则对比。

Beyond Compare选择最好的方法来突出不同之处,文本文件可以用语法高亮和设置比较规则的方法进行查看和编辑,适用于用于文档、源代码和HTML。

“单片机编程如何查看版本之间代码的不同:代码比较工具”

二、Diffuse

“单片机编程如何查看版本之间代码的不同:代码比较工具”

Diffuse在命令行中的速度是相当快的,支持像 C++、Python、Java、XML 等语言的语法高亮显示。可视化比较,非常直观,支持两相比较和三相比较。这就是说,使用 Diffuse 你可以同时比较两个或三个文本文件。

支持常见的版本控制工具,包括 CVS、subversion、git、mercurial 等,你可以通过 Diffuse 直接从版本控制系统获取源代码,以便对其进行比较和合并。

三、WinMerge

“单片机编程如何查看版本之间代码的不同:代码比较工具”

一款运行于Windows系统下的文件比较和合并工具,使用它可以非常方便地比较多个文档内容,适合程序员或者经常需要撰写文稿的朋友使用。

WinMerge会将两个文件内容做对比,并在相异之处以高亮度的方式显示,让使用者可以很快的查知;可以直接让左方的文件内容直接覆盖至右方,或者反过来也可以覆盖。

四、Code Compare

“单片机编程如何查看版本之间代码的不同:代码比较工具”

Code Compare是一款用于程序代码文件的比较工具,目前Code Compare支持的对比语言有:C#、C++、CSS、HTML、Java、JavaScrip等代码语言。

Visual Studio环境源代码比较是一个方便,易于使用的工具,独特的Visual Studio集成,可以帮助你同时在一个环境内,使所有的方便程序开发设计。

五、AptDiff

“单片机编程如何查看版本之间代码的不同:代码比较工具”

AptDiff是一个文件比较工具,可以对文本和二进制文件进行比较和合并,适用于软件开发、网络设计和其它的专业领域。

它使用方便,支持键盘快捷键,可以同步进行横向和纵向卷动,支持Unicode格式和大于4GB的文件,可以生成HTML格式的比较报告。

来源:玩转嵌入式
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 92

一、单片机内部结构分析

我们来思考一个问题,当我们在编程器中把一条指令写进单片机内部,然后取下单片机,单片机就可以执行这条指令,那么这条指令一定保存在单片机的某个地方,并且这个地方在单片机掉电后依然可以保持这条指令不会丢失,这是个什么地方呢?这个地方就是单片机内部的只读存储器即ROM(READ ONLY MEMORY)。为什么称它为只读存储器呢?刚才我们不是明明把两个数字写进去了吗?

原来在89C51中的ROM是一种电可擦除的ROM,称为FLASH ROM,刚才我们是用的编程器,在特殊的条件下由外部设备对ROM进行写的操作,在单片机正常工作条件下,只能从那面读,不能把数据写进去,所以我们还是把它称为ROM。

二、几个基本概念

1、数的本质和物理现象

我们知道,计算机可以进行数学运算,这令我们非常难以理解,计算机吗,我们虽不了解它的组成,但它们只是一些电子元器件,怎么可以进行数学运算呢?我们做数学题如37+45是这样做的,先在纸上写37,然后在下面写45,然后大脑运算,最后写出结果,运算的原材料:37、45和结果:82都是写在纸上的,计算机中又是放在什么地方呢?

为了解决这个问题,先让我们做一个实验:这里有一盏灯,我们知道灯要么亮,要么不亮,就有两种状态,我们可以用’0’和’1’来代替这两种状态,规定亮为’1’,不亮为’0’。现在放上两盏灯,一共有几种状态呢?我们列表来看一下:

请大家自己写上3盏灯的情况000 001 010 011 100 101 110 111我们来看,这个000,001,101 不就是我们学过的的二进制数吗?本来,灯的亮和灭只是一种物理现象,可当我们把它们按一定的顺序排好后,灯的亮和灭就代表了数字了。让我们再抽象一步,灯为什么会亮呢?是因为输出电路输出高电平,给灯通了电。因此,灯亮和灭就可以用电路的输出是高电平还是低电平来替代了。这样,数字就和电平的高、低联系上了。

2、位的含义

通过上面的实验我们已经知道:一盏灯亮或者说一根线的电平的高低,可以代表两种状态:0和1。实际上这就是一个二进制位,因此我们就把一根线称之为一“位”,用BIT表示。

3、字节的含义

一根线可以表示0和1,两根线可以表达00,01,10,11四种状态,也就是可以表达0到3,而三根可以表达0~7,计算机中通常用8根线放在一起,同时计数,就可以表示0-255一共256种状态。这8根线或者8位就称之为一个字节(BYTE)。

三、存储器的工作原理

1、存储器构造

“”

存储器就是用来存放数据的地方。它是利用电平的高低来存放数据的,也就是说,它存放的实际上是电平的高、低,而不是我们所习惯认为的1234这样的数字,这样,我们的一个谜团就解开了,计算机也没什么神秘的吗。

如上图左所示:一个存储器就象一个个的小抽屉,一个小抽屉里有八个小格子,每个小格子就是用来存放“电荷”的,电荷通过与它相连的电线传进来或释放掉,至于电荷在小格子里是怎样存的,就不用我们操心了,你可以把电线想象成水管,小格子里的电荷就象是水,那就好理解了。存储器中的每个小抽屉就是一个放数据的地方,我们称之为一个“单元”。

有了这么一个构造,我们就可以开始存放数据了,想要放进一个数据12,也就是00001100,我们只要把第二号和第三号小格子里存满电荷,而其它小格子里的电荷给放掉就行了(看上图右)。可是问题出来了,看上图右,一个存储器有好多单元,线是并联的,在放入电荷的时候,会将电荷放入所有的单元中,而释放电荷的时候,会把每个单元中的电荷都放掉,这样的话,不管存储器有多少个单元,都只能放同一个数,这当然不是我们所希望的,因此,要在结构上稍作变化,看上图右,在每个单元上有个控制线,我想要把数据放进哪个单元,就把一个信号给这个单元的控制线,这个控制线就把开关打开,这样电荷就可以自由流动了,而其它单元控制线上没有信号,所以开关不打开,不会受到影响,这样,只要控制不同单元的控制线,就可以向各单元写入不同的数据了,同样,如果要从某个单元中取数据,也只要打开相应的控制开关就行了。

2、存储器译码

那么,我们怎样来控制各个单元的控制线呢?这个还不简单,把每个单元的控制线都引到集成电路的外面不就行了吗?事情可没那么简单,一片27512存储器中有65536个单元,把每根线都引出来,这个集成电路就得有6万多个脚?不行,怎么办?要想法减少线的数量。我们有一种方法称这为译码,简单介绍一下:一根线可以代表2种状态,2根线可以代表4种状态,3根线可以代表几种,256种状态又需要几根线代表?8种,8根线,所以65536种状态我们只需要16根线就可以代表了。

3、存储器的选片及总线的概念

至此,译码的问题解决了,让我们再来关注另外一个问题。送入每个单元的八根线是用从什么地方来的呢?它就是从计算机上接过来的,一般地,这八根线除了接一个存储器之外,还要接其它的器件。这样问题就出来了,这八根线既然不是存储器和计算机之间专用的,如果总是将某个单元接在这八根线上,就不好了,比如这个存储器单元中的数值是0FFH另一个存储器的单元是00H,那么这根线到底是处于高电平,还是低电平?岂非要打架看谁历害了?所以我们要让它们分离。

办法当然很简单,当外面的线接到集成电路的引脚进来后,不直接接到各单元去,中间再加一组开关就行了。平时我们让开关打开着,如果确实是要向这个存储器中写入数据,或要从存储器中读出数据,再让开关接通就行了。这组开关由三根引线选择:读控制端、写控制端和片选端。要将数据写入片中,先选中该片,然后发出写信号,开关就合上了,并将传过来的数据(电荷)写入片中。如果要读,先选中该片,然后发出读信号,开关合上,数据就被送出去了。读和写信号同时还接入到另一个存储器,但是由于片选端不同,所以虽有读或写信号,但没有片选信号,所以另一个存储器不会“误会”而开门,造成冲突。那么会不同时选中两片芯片呢?只要是设计好的系统就不会,因为它是由计算控制的,而不是我们人来控制的,如果真的出现同时出现选中两片的情况,那就是电路出了故障了,这不在我们的讨论之列。

从上面的介绍中我们已经看到,用来传递数据的八根线并不是专用的,而是很多器件大家共用的,所以我们称之为数据总线,总线英文名为BUS,总即公交车道,谁也可以走。而十六根地址线也是连在一起的,称之为地址总线。

来源:嵌入式资讯精选
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:
cathy@eetrend.com)。

围观 87

一、五大内存分区

内存分成5个区,它们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。

1、栈区(stack):FIFO就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。

2、堆区(heap):就是那些由new分配的内存块,它们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。

3、自由存储区:就是那些由malloc等分配的内存块,它和堆是十分相似的,不过它是用free来结束自己的生命。

4、全局/静态存储区:全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。

5、常量存储区:这是一块比较特殊的存储区,它们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多)

code/data/stack

内存主要分为代码段,数据段和堆栈。代码段放程序代码,属于只读内存。数据段存放全局变量,静态变量,常量等,堆里存放自己malloc或new出来的变量,其他变量就存放在栈里,堆栈之间空间是有浮动的。数据段的内存会到程序执行完才释放。调用函数先找到函数的入口地址,然后计算给函数的形参和临时变量在栈里分配空间,拷贝实参的副本传给形参,然后进行压栈操作,函数执行完再进行弹栈操作。字符常量一般放在数据段,而且相同的字符常量只会存一份。

二、C语言程序的存储区域

1、由C语言代码(文本文件)形成可执行程序(二进制文件),需要经过编译-汇编-连接三个阶段。编译过程把C语言文本文件生成汇编程序,汇编过程把汇编程序形成二进制机器代码,连接过程则将各个源文件生成的二进制机器代码文件组合成一个文件。

2、C语言编写的程序经过编译-连接后,将形成一个统一文件,它由几个部分组成。在程序运行时又会产生其他几个部分,各个部分代表了不同的存储区域:

1)代码段(Code或Text)

代码段由程序中执行的机器代码组成。在C语言中,程序语句执行编译后,形成机器代码。在执行程序的过程中,CPU的程序计数器指向代码段的每一条机器代码,并由处理器依次运行。

2)只读数据段(RO data)

只读数据段是程序使用的一些不会被更改的数据,使用这些数据的方式类似查表式的操作,由于这些变量不需要更改,因此只需要放置在只读存储器中即可。

3)已初始化读写数据段(RW data)

已初始化数据是在程序中声明,并且具有初值的变量,这些变量需要占用存储器的空间,在程序执行时它们需要位于可读写的内存区域内,并且有初值,以供程序运行时读写。

4)未初始化数据段(BBS)

未初始化数据是在程序中声明,但是没有初始化的变量,这些变量在程序运行之前不需要占用存储器的空间。

5)堆(heap)

堆内存只在程序运行时出现,一般由程序员分配和释放。在具有操作系统的情况下,如果程序没有释放,操作系统可能在程序(例如一个进程)结束后会后内存。

6)栈(statck)

堆内存只在程序运行时出现,在函数内部使用的变量,函数的参数以及返回值将使用栈空间,栈空间由编译器自动分配和释放。

“”

3、代码段、只读数据段、读写数据段、未初始化数据段属于静态区域,而堆和栈属于动区域。代码段、只读数据段和读写数据段将在连接之后产生,未初始化数据段将在程序初始化的时候开辟,而对堆和栈将在程序饿运行中分配和释放。

4、C语言程序分为映像和运行时两种状态。在编译-连接后形成的映像中,将只包含代码段(Text)、只读数据段(R0 Data)和读写数据段(RW Data)。在程序运行之前,将动态生成未初始化数据段(BSS),在程序的运行时还将动态生成堆(Heap)区域和栈(Stack)区域。

注:

1、一般来说,在静态的映像文件中,各个部分称之为节(Section),而在运行时的各个部分称之为段(Segment)。如果不详细区分,统称为段。

2、C语言在编译连接后,将生成代码段(TEXT),只读数据段(RO Data)和读写数据段(RW Data)。在运行时,除了上述三个区域外,还包括未初始化数据段(BBS)区域和堆(heap)区域和栈(Stack)区域。

三、C语言程序的段

1、段的分类

每一个源程序生成的目标代码将包含源程序所需要表达的所有信息和功能。目标代码中各段生成情况如下:

1)代码段(Code)

代码段由程序中的各个函数产生,函数的每一个语句将最终经过编译和汇编生成二进制机器代码

2)只读数据段(RO Data)

只读数据段由程序中所使用的数据产生,该部分数据的特点在运行中不需要改变,因此编译器会将数据放入只读的部分中。C语言的一些语法将生成只读数据数据段。

2、只读数据段(RO Data)

只读数据段(RO Data)由程序中所使用的数据产生,该部分数据的特点是在运行中不需要改变,因此编译器会将数据放入只读的部分中。以下情况将生成只读数据段。

1)只读全局变量

定义全局变量const char a[100]=”abcdefg”将生成大小为100个字节的只读数据区,并使用字符串“abcdefg”初始化。如果定义为const char a[]=”abcdefg”,没有指定大小,将根据“abcdefgh”字串的长度,生成8个字节的只读数据段。

2)只读局部变量

例如:在函数内部定义的变量const char b[100]=”9876543210”;其初始化的过程和全局变量。

3)程序中使用的常量

例如:在程序中使用printf("informationn”),其中包含了字串常量,编译器会自动把常量“information n”放入只读数据区。

注:在const char a[100]={“ABCDEFG”}中,定义了100个字节的数据区,但是只初始化了前面的8个字节(7个字符和表示结束符的‘0’)。在这种用法中,实际后面的字节米有初始化,但是在程序中也不能写,实际上没有任何用处。因此,在只读数据段中,一般都需要做完全的的初始化。

3、读写数据段(RW Data)

读写数据段表示了在目标文件中一部分可以读也可以写的数据区,在某些场合它们又被称为已初始化数据段。这部分数据段和代码,与只读数据段一样都属于程序中的静态区域,但是具有科协的特点。

1)已初始化全局变量

例如:在函数外部,定义全局的变量char a[100]=”abcdefg”

2)已初始化局部静态变量

例如:在函数中定义static char b[100]=”9876543210”。函数中由static定义并且已经初始化的数据和数组将被编译为读写数据段。

说明:

读写数据区的特点是必须在程序中经过初始化,如果只有定义,没有初始值,则不会生成读写数据区,而会定义为未初始化数据区(BSS)。如果全局变量(函数外部定义的变量)加入static修饰符,写成static char a[100]的形式,这表示只能在文件内部使用,而不能被其他文件使用。

4、未初始化数据段(BSS)

未初始化数据段常被称之为BSS(英文名为Block start by symbol的缩写)。与读写数据段类似,它也属于静态数据区。但是该段中数据没有经过初始化。因此它只会在目标文件中被标识,而不会真正称为目标文件中的一个段,该段将会在运行时产生。未初始化数据段只有在运行的初始化阶段才会产生,因此它的大小不会影响目标文件的大小。

四、在C语言的程序中,对变量的使用需要注意的问题

1、在函数体中定义的变量通常是在栈上,不需要在程序中进行管理,由编译器处理。

2、用malloc,calloc,realoc等分配分配内存的函数所分配的内存空间在堆上,程序必须保证在使用后使用后freee释放,否则会发生内存泄漏。

3、所有函数体外定义的是全局变量,加了static修饰符后的变量不管在函数内部或者外部存放在全局区(静态区)。

4、使用const定义的变量将放于程序的只读数据区。

说明:

在C语言中,可以定义static变量:在函数体内定义的static变量只能在该函数体内有效;在所有函数体外定义的static变量,也只能在该文件中有效,不能在其他源文件中使用;对于没有使用 static修饰的全局变量,可以在其他的源文件中使用。这些区别是编译的概念,即如果不按要求使用变量,编译器会报错。使用static 和没使用static修饰的全局变量最终都将放置在程序的全局去(静态去)。

五、程序中段的使用

C语言中的全局区(静态区),实际上对应着下述几个段:

只读数据段:RO Data
读写数据段:RW Data
未初始化数据段:BSS Data

一般来说,直接定义的全局变量在未初始化数据区,如果该变量有初始化则是在已初始化数据区(RW Data),加上const修饰符将放置在只读区域(RO Data).

例如:

const char ro[ ]=”this is a readonlydata”; //只读数据段,不能改变ro数组中的内容,ro存放在只读数据段。

char rw1[ ]=”this is global readwrite data”; //已初始化读写数据段,可以改变数组rw1中的内容。应为数值/是赋值不是把”this is global readwrite data” 地址给了rw1,不能改变char rw1[ ]=”this is global readwrite data”; //已初始化读写数据段,可以改变数组rw1中的内容。应为数值/是赋值不是把”this is global readwrite data” 地址给了rw1,不能改变”this is global readwrite data”的数值。因为起是文字常量放在只读数据段中

char bss_1[100];//未初始化数据段

const char *ptrconst = “constant data”; //”constant data”放在只读数据段,不能改变ptrconst中的值,因为其是地址赋值。ptrconst指向存放“constant data”的地址,其为只读数据段。但可以改变ptrconst地址的数值,因其存放在读写数据段中。

实例讲解:

int main( )
{
short b;//b放置在栈上,占用2个字节
char a[100];//需要在栈上开辟100个字节,a的值是其首地址
char s[]=”abcde”;
//s在栈上,占用4个字节,“abcde”本身放置在只读数据存储区,占6字节。s是一个地址
//常量,不能改变其地址数值,即s++是错误的。
char *p1;//p1在栈上,占用4个字节
char *p2 ="123456";//"123456"放置在只读数据存储区,占7个字节。p2在栈上,p2指向的内容不能更
//改,但是p2的地址值可以改变,即p2++是对的。
static char bss_2[100]; //局部未初始化数据段
static int c=0 ; //局部(静态)初始化区
p1 = (char *)malloc(10*sizeof(char)); //分配的内存区域在堆区
strcpy(p1,”xxx”); //”xxx”放置在只读数据存储区,占5个字节
free(p1); //使用free释放p1所指向的内存
return 0;
}

说明:

1、只读数据段需要包括程序中定义的const型的数据(如:const char ro[]),还包括程序中需要使用的数据如“123456”。对于const char ro[]和const char * ptrconst的定义,它们指向的内存都位于只读数据据区,其指向的内容都不允许修改。区别在于前者不允许在程序中修改ro的值,后者允许在程序中修改ptrconst本身的值。对于后者,改写成以下的形式,将不允许在程序中修改ptrconst本身的值:

const char * const ptrconst = “const data”;

2、读写数据段包含了已经初始化的全局变量static char rw1[]以及局部静态变量static char
rw2[]。rw1和rw2的差别在于编译时,是在函数内部使用的还是可以在整个文件中使用。对于前者,static修饰在于控制程序的其他文件时候可以访问rw1变量,如果有static修饰,将不能在其他的C语言源文件中使用rw1,这种影响针对编译-连接的特性,但无论有static,变量rw1都将被放置在读写数据段。对于后者rw2,它是局部的静态变量,放置在读写数据区;如果不使用static修饰,其意义将完全改变,它将会是开辟在栈空间局部变量,而不是静态变量。

3、未初始化数据段,事例1中的bss_1[100]和 bss_2[200]在程序中代表未初始化的数据段。其区别在于前者是全局的变量,在所有文件中都可以使用;后者是局部的变量,只在函数内部使用。未初始化数据段不设置后面的初始化数值,因此必须使用数值指定区域的大小,编译器将根据大小设置BBS中需要增加的长度。

4、栈空间包括函数中内部使用的变量如short b和char a[100],以及char *p1中p1这个变量的值。

1)变量p1指向的内存建立在堆空间上,堆空间只能在程序内部使用,但是堆空间(例如p1指向的内存)可以作为返回值传递给其他函数处理。

2)栈空间主要用于以下3类数据的存储:

a、函数内部的动态变量
b、函数的参数
c、函数的返回值

3)栈空间主要的用处是供函数内部的动态变量使用,变量的空间在函数开始之前开辟,在函数退出后由编译器自动回收。看一个例:

int main( )
{
char *p = "tiger";
p[1] = 'I';
p++;
printf("%sn",p);
}

编译后提示:段错误

分析:

char *p = "tiger";系统在栈上开辟了4个字节存储p的数值。"tiger"在只读存储区中存储,因此"tiger"的内容不能改变,*p="tiger",表示地址赋值,因此,p指向了只读存储区,因此改变p指向的内容会引起段错误。但是因为p是存放在栈上,因此p的数值是可以改变的,因此p++是正确的。

六、const的使用

1、前言:

const是一个C语言的关键字,它限定一个变量不允许被改变。使用const在一定程序上可以提高程序的健壮性,另外,在观看别人代码的时候,清晰理解const所起的作用,对理解别人的程序有所帮助。

2、const变量和常量

1)const修饰的变量,其值存放在只读数据段中,其值不能被改变。称为只读变量。

其形式为 const int a=5;此处可以用a代替5

2)常量:其也存在只读数据段中,其数值也不能被改变。其形式为"abc" ,5

3、const 变量和const限定的内容,先看一个事例:

typedef char* pStr;
int main( )
{
char string[6] = “tiger”;
const char *p1 = string;
const pStr p2 = string;
p1++;
p2++;
printf(“p1=%snp2=%sn”,p1,p2);
}

程序经过编译后,提示错误为

error:increment of read-only variable ‘p2’

1)const 使用的基本形式为:

const char m;//限定m 不可变

2)替换1式中的m,

const char *pm;

//限定*pm不可变,当然pm是可变的,因此p1++是对的。

3)替换1式中的char,

const newType m;

//限定m不可变,问题中的pStr是一种新类型,因此问题中p2不可变,p2++是错误的。

4、const 和指针

类型声明中const用来修饰一个常量,有如下两种写法:

1)const在前面

const int nValue;//nValue是const
const char *pContent;//*pContent是const,pConst可变
const (char *)pContent;//pContent是const,*pContent可变
char *const pContent;//pContent是const,*pContent可变
const char * const pContent;//pContent和*pContent都是const

2)const 在后面与上面的声明对等

int const nValue;// nValue是const
char const *pContent;//*pContent是const, pContent可变
(char *) constpContent;//pContent是const, *pContent可变
char* const pContent;// pContent是const, *pContent可变
char const* const pContent;//pContent和*pContent都是const

说明:const和指针一起使用是C语言中一个很常见的困惑之处,下面是两天规则:

1)沿着*号划一条线,如果const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量。你可以根据这个规则来看上面声明的实际意义,相信定会一目了然。

2)对于const (char *) ; 因为char *是一个整体,相当于一个类型(如char),因此,这是限定指针是const。

七、单片机C语言中的data,idata,xdata,pdata,code

从数据存储类型来说,8051系列有片内、片外程序存储器,片内、片外数据存储器,片内程序存储器还分直接寻址区和间接寻址类型,分别对应code、data、xdata、idata以及根据51系列特点而设定的pdata类型,使用不同的存储器,将使程序执行效率不同,在编写C51程序时,最好指定变量的存储类型,这样将有利于提高程序执行效率(此问题将在后面专门讲述)。与ANSI-C稍有不同,它只分SAMLL、COMPACT、LARGE模式,各种不同的模式对应不同的实际硬件系统,也将有不同的编译结果。

在51系列中data,idata,xdata,pdata的区别:

data:固定指前面0x00-0x7f的128个RAM,可以用acc直接读写的,速度最快,生成的代码也最小。

idata:固定指前面0x00-0xff的256个RAM,其中前128和data的128完全相同,只是因为访问的方式不同。idata是用类似C中的指针方式访问的。汇编中的语句为:mox ACC,@Rx.(不重要的补充:c中idata做指针式的访问效果很好)

xdata:外部扩展RAM,一般指外部0x0000-0xffff空间,用DPTR访问。

pdata:外部扩展RAM的低256个字节,地址出现在A0-A7的上时读写,用movx ACC,@Rx读写。这个比较特殊,而且C51好象有对此BUG,建议少用。但也有他的优点,具体用法属于中级问题,这里不提。

单片机C语言unsigned char code table[]code 是什么作用?

code的作用是告诉单片机,我定义的数据要放在ROM(程序存储区)里面,写入后就不能再更改,其实是相当与汇编里面的寻址MOVX(好像是),因为C语言中没办法详细描述存入的是ROM还是RAM(寄存器),所以在软件中添加了这一个语句起到代替汇编指令的作用,对应的还有data是存入RAM的意思。

程序可以简单的分为code(程序)区,和data (数据)区,code区在运行的时候是不可以更改的,data区放全局变量和临时变量,是要不断的改变的,cpu从code区读取指令,对data区的数据进行运算处理,因此code区存储在什么介质上并不重要,象以前的计算机程序存储在卡片上,code区也可以放在rom里面,也可以放在ram里面,也可以放在flash里面(但是运行速度要慢很多,主要读flash比读ram要费时间),因此一般的做法是要将程序放到flash里面,然后load到 ram里面运行的;DATA区就没有什么选择了,肯定要放在RAM里面,放到rom里面改动不了。

bdata如何使用它呢?

若程序需要8个或者更多的bit变量,如果你想一次性给8个变量赋值的话就不方便了,(举个例子说说它的方便之处,想更深入的了解请在应用中自己琢磨)又不可以定义bit数组,只有一个方法

char bdata MODE;
sbit MODE_7 = MODE^7;
sbit MODE_6 = MODE^6;
sbit MODE_5 = MODE^5;
sbit MODE_4 = MODE^4;
sbit MODE_3 = MODE^3;
sbit MODE_2 = MODE^2;
sbit MODE_1 = MODE^1;
sbit MODE_0 = MODE^0;

8个bit变量MODE_n 就定义好了。

这是定义语句,Keilc 的特殊数据类型。记住一定要是sbit,不能 bit MODE_0 = MODE^0;

赋值语句要是这么写C语言就视为异或运算。

Flash相对单片机里的RAM属于外部存取器,虽其结构位置装在单片机中,其实xdata是放在相对RAM的外面,而flash正是相对RAM外面。

inta变量定义在内部RAM,xdatainta定义在外部RAM或flash,uchar codea定义在flash。

uchar code duma[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x40,0x00}; //共阴的数码管段选,P2口要取的数值

若定义 uchar aa[5],aa[5]中的内容是存放在数据存储区(RAM)中的,在程序运行工程中各个数组元素的值可以被修改,掉电后aa[5]中的数据无法保存。

若定义 uchar code bb[5]中的内容是存放在程序存储区(如flash)中的,只有在烧写程序时,才能改变bb[5]中的各元素的值,在程序运行工程中无法修改,并且掉电后bb[5]中的数据不消失。

八、C语言中堆和栈的区别

C语言程序经过编译连接后形成编译、连接后形成的二进制映像文件由栈、堆、数据段(由三部分部分组成:只读数据段,已经初始化读写数据段,未初始化数据段即BBS)和代码段组成,如下图所示:
图片

1、栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量等值。其操作方式类似于数据结构中的栈。

2、堆区(heap):一般由程序员分配释放,若程序员不释放,则可能会引起内存泄漏。注堆和数据结构中的堆栈不一样,其类是与链表。

3、程序代码区:存放函数体的二进制代码。

4、数据段:由三部分组成:

1)只读数据段:

只读数据段是程序使用的一些不会被更改的数据,使用这些数据的方式类似查表式的操作,由于这些变量不需要更改,因此只需要放置在只读存储器中即可。一般是const修饰的变量以及程序中使用的文字常量一般会存放在只读数据段中。

2)已初始化的读写数据段:

已初始化数据是在程序中声明,并且具有初值的变量,这些变量需要占用存储器的空间,在程序执行时它们需要位于可读写的内存区域内,并且有初值,以供程序运行时读写。在程序中一般为已经初始化的全局变量,已经初始化的静态局部变量(static修饰的已经初始化的变量)

3)未初始化段(BSS):

未初始化数据是在程序中声明,但是没有初始化的变量,这些变量在程序运行之前不需要占用存储器的空间。与读写数据段类似,它也属于静态数据区。但是该段中数据没有经过初始化。未初始化数据段只有在运行的初始化阶段才会产生,因此它的大小不会影响目标文件的大小。在程序中一般是没有初始化的全局变量和没有初始化的静态局部变量。

堆和栈的区别

1、申请方式

(1)栈(satck):由系统自动分配。例如,声明在函数中一个局部变量int b;系统自动在栈中为b开辟空间。

(2)堆(heap):需程序员自己申请(调用malloc,realloc,calloc),并指明大小,并由程序员进行释放。容易产生memory leak.
eg:charp;
p = (char *)malloc(sizeof(char));//但是,p本身是在栈中。

2、申请大小的限制

1)栈:在windows下栈是向底地址扩展的数据结构,是一块连续的内存区域(它的生长方向与内存的生长方向相反)。栈的大小是固定的。如果申请的空间超过栈的剩余空间时,将提示overflow。

2)堆:堆是高地址扩展的数据结构(它的生长方向与内存的生长方向相同),是不连续的内存区域。这是由于系统使用链表来存储空闲内存地址的,自然是不连续的,而链表的遍历方向是由底地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。

3、系统响应:

1)栈:只要栈的空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

2)堆:首先应该知道操作系统有一个记录空闲内存地址的链表,但系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的free语句才能正确的释放本内存空间。另外,找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

说明:对于堆来讲,对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题。

4、申请效率

1)栈由系统自动分配,速度快。但程序员是无法控制的。

2)堆是由malloc分配的内存,一般速度比较慢,而且容易产生碎片,不过用起来最方便。

5、堆和栈中的存储内容

1)栈:在函数调用时,第一个进栈的主函数中后的下一条语句的地址,然后是函数的各个参数,参数是从右往左入栈的,然后是函数中的局部变量。注:静态变量是不入栈的。

当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续执行。

2)堆:一般是在堆的头部用一个字节存放堆的大小。

6、存取效率

1)堆:char *s1=”hellowtigerjibo”;是在编译是就确定的。

2)栈:char s1[]=”hellowtigerjibo”;是在运行时赋值的;用数组比用指针速度更快一些,指针在底层汇编中需要用edx寄存器中转一下,而数组在栈上读取。

补充:

栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

7、分配方式:

1)堆都是动态分配的,没有静态分配的堆。

2)栈有两种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的。它的动态分配是由编译器进行释放,无需手工实现。

来源:硬件攻城狮
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:
cathy@eetrend.com)。

围观 21

页面

订阅 RSS - 单片机