发帖数

53

原创数

53

关注者

11

阅读数

9260

点赞数

1

黄忠

  • 聊一聊单片机堆栈


    大家好!我是张飞实战电子黄忠老师!今天给大家聊一聊单片机堆栈.

    单片机堆栈是什么?

    简单来说是在RAM区的一块存储空间,在系统空间中用作临时数据存储,遵循后进先出的原则。

    栈空间操作的关键点之一就是栈指针寄存器,每次执行栈操作时,栈指针的内容自动调整。

    按照通常的说法,向栈中存储数据称为“压栈”(使用PUSH指令),恢复数据称为“出栈”(使用POP指令)。根据所使用架构的不同,有些处理器在向栈存入数据时地址会自动增加,而有些则会减小。

    这就意味着栈指针始终指向栈空间的最后一个数据,在执行数据存储前(PUSH),栈指针会首先减小。

    PUSHPOP通常用在函数或子程序的开始和结尾处。在函数开始执行时,PUSH操作将寄存器的当前内容存入栈空间;执行结束前,POP又将栈空间存储的数据恢复。一般说来,对每个寄存器的PUSH操作都应相应的进行POP操作。否则恢复的数据可能无法对应之前的寄存器,这样会导致无法预期的后果,比如栈溢出。

    下面看一下压栈和出栈的操作过程:

    图片1.jpg


    再来说一下堆栈的作用:

    子程序调用和中断服务时,CPU自动将当前PC值压栈保存,返回时自动将PC值弹栈。

    保护现场/恢复现场。

    数据传输

    再来说一下堆栈操作的一些规则。

    比如Cortex-M0处理器每次出栈以及压栈操作的最小单位是4字节(32位),还可以使用一条指令实现对多个寄存器的压栈和出栈操作。显然Cortex-M0的栈空间被设计为字对齐的(地址值必须是4的倍数,比如0x00x40x8等)。

    对于Cortex-M0处理器,可以通过R13SP访问R13SP,根据处理器状态和CONTROL寄存器值的不同,访问的栈指针可以是主栈指针(MSP),也可以是进程栈指针(PSP)。许多简单的应用只会用到一个栈指针,一般默认是主栈指针(MSP),进程栈指针通过只用于嵌入式应用的操作系统(OS)。

    对于Cortex-M0处理器由于栈是向下生长的(满递减),内存的上边界通常会被用作栈指针的初始值。例如,如果内存区域为0x20000000~0x20007FFF,我们可以将栈指针的初始值设为0x20008000,在这种情况下,第一次压栈操作会将数据存至0x20007FFC开始的字中,这也是内存的最高4字节。


    收藏 0 回复 0 浏览 265
  • 变量的初始化技巧

    由于在嵌入式系统中必须考虑程序规模的问题,因此,对程序中的变量的初始化也需要进行慎重的考虑。在C语言中,基本数据结构(字符型、整型)的初始化相对简单;数组、结构体属于C语言中的构造类型,其变量在初始化的时候相对复杂,也有一些比较特殊的技巧和方法。

    数组的初始化

    以下的代码是一个关于数组的初始化的示例:

    image.png

    从程序上来看,方式1直接使用数组初始化的方式,方式2使用了函数完成数组的赋值。方式3是方式2的等价形式。

    从表面上来看方式1要简单很多,实际上,无论从代码的规模上,还是效率上,二者都没有太大区别。

    方式1看似直接使用初始化的过程完成赋值,实际上对于类似char a[10]=abcde形式的语句,编译器还是需要做很多事情才能完成。a是函数中使用局部数组变量,开辟在栈内存空间上。当程序运行至fun处,不会凭空得到一段字符串,也就是说abcde必须有地方存放,这就是只读区(RO Data)。因此,程序运行赋值语句时,要在栈上开辟10个字节的空间,然后将调用内存复制函数将只读区的abcde复制到这个栈空间上。

    由此可见,方式1和方式2的运行没有本质区别,只是方式1利用编译器完成的操作,方式2要在运行程序时完成,二者依赖的库不同,但是都是内存复制一类的功能,同时二者的abcde都需要占用只读数据区的空间。

    从占用空间和运行效率上,方式1,方式2,方式3基本都是等价的。无论程序中有没有声明,abcde所占用的只读数据区(RO Data)都是必需的,复制的过程也是必需的。

    方式4是直接把a定义为已初始化可读写的全局变量,在使用的时候直接操作。作为已初始化的全局变量(RW Data),将在程序总体初始化的阶段复制到内存中,而不是在函数调用的时候复制。其优点是不用在函数调用的时候完成内存复制操作,缺点是全局的数据会一直占用内存,而栈上数据将在函数退出的时候释放。

    实质上,在数组的定义中,变量可以是全局变量或者局部变量,如果是全局变量,将会增加10字节已初始化的数据区(RW Data),初始化的内容将被放入,这段数据区是可读写的,对全局变量的访问就是对这段已初始化的数据区的访问。如果是局部变量,内容被放入只读数据区,函数运行到的时候要在栈上分配相应的数据区,把只读区的内容复制到栈上,对数组的访问是访问这段在栈上的内存。

    结构体的初始化

    在数组初始化的时候可以使用直接赋值的方式,而在结构体初始化的时候可以使用参数列表。这两种形式比较类似,因此结构体在初始化阶段和数组的情况是相似的。

    例如:

    image.png

    结构体的两种初始化方式和上面数组的两种初始化方式有一定的对应关系。第一种方式使用成员列表的方式初始化,第二种使用对结构体成员变量赋值的方式。实质上,第1种方式编译器将自动生成一些指令完成变量a的初始化,而第2种方式编译器在处理Score a语句的时候只需要开辟栈空间,而在后面在对其每个成员进行赋值,开辟栈空间和赋值都是简单的处理语句,编译器没有做过多的工作。

    在嵌入式系统中,对程序性能是非常敏感的,有以下几个方面的开销:首先是程序各段执行的效率,这是程序开销的主要方面,其次是函数的参数和返回值传递中入栈和出栈的时间。由于各个处理器一般都具有直接栈操作的指令(入栈和出栈),因此函数中使用的局部变量的可以使用处理器的基本的入栈和出栈指令来完成,这种指令的执行性能是很高的。但如果是为变量赋初值,虽然是C语言中基本的语法,却并不能以简单的方式处理,编译器实际上需要做一些附加的工作,来完成对局部变量的初始化。也就是说在程序中没有写出的语句,编译器也需要处理。根据以上的程序和分析,可见如果栈上变量需要初始化,有可能也会带来一定的开销。


    收藏 0 回复 0 浏览 211
  • 带你在单片机编程中熟练使用const

    C语言关键字中const举足轻重,我们今天就深度聊一聊const的定义和实际应用,让它不再是迷。

    C语言中const关键字是constant的缩写,是恒定不变的意思。通常翻译为常量、常数等,我们一看到const关键字马上就想到了常量。这是不精确的,精确来说应该是只读变量,其值在编译时不能被使用,因为编译器在编译时不知道其存储的内容。那么const推出的初始目的正是为了取代预编译指令,消除它的缺点,同时继承它的优点。

    事实上在C语言中const功能很强大,它可以修饰变量、数组、指针、函数参数等。

    1const 修饰的只读变量

    C语言中采用const修饰变量,功能是对变量声明为只读特性,并保护变量值以防被修改。

    例如:

    const  int Max = 100;

    int  Array[Max]

    这个大家可以在Visual C++6.0创建一个.c文件测试一下,你会发现在.c文件中编译器会提示出错。我们知道定义一个数组必须指定其元素的个数,这也从侧面证实在C语言中const修饰的Max仍然是变量,只不过是只读属性罢了。

    还有值得注意的是,定义变量的同时,必须初始化,并且不能再重新赋值。

    2节省空间,避免不必要的内存分配,同时提高效率

    编译器通常不为普通const只读变量分配存储空间,而是将他们保存在符号表中,这使得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高。

    例如:

    #define  M  3   //宏常量

    const int N= 5;   //此时并未将N放入内存中

    int i = N;     //此时为N分配内存,以后不再分配

    int I = M;     //预编译期间进行宏替换,分配内存

    int j = N;     //没有内存分配

    int J = M;    //再进行宏替换,又一次分配内存

    const定义的只读变量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数。所以,const定义的只读变量在程序运行过程中只有一份备份(因为它是全局的只读变量,存放在静态区),而#define定义的宏常量在内存中有若干个备份。#define宏是在预编译阶段进行替换,而const修饰的只读变量是在编译的时候确定其值。#define宏没有类型,而const修饰的只读变量具有特定的类型。

    3、修饰一般变量

    一般变量是指简单类型的只读变量。这种只读变量在定义时,修饰符const可以用在类型说明符前,也可以用在类型说明符后,例如:

    int  const  i = 2;     const  int  i  =  2;

    4 修饰数组

    C语言中const还可以修饰数组,举例如下:

    const int array[5] = {1,2,3,4,5};

    array[0] = array[0]+1; //错误

    数组元素与变量类似,具有只读属性,不能被更改;一旦更改,如程序将会报错。

    5 修饰指针

    C语言中const修饰指针要特别注意,共有两种形式,一种是用来限定指向空间的值不能修改;另一种是限定指针不可更改。举例说明如下:

    Const离谁近修饰谁的原则,

    例如:

    const int * p1; //定义1,p1可变,p1指向的对象不可变

    int * const p2; //定义2,p2不可变,p2指向的对象可变

    上面定义了两个指针p1p2

    在定义1const限定的是*p1,即其指向空间的值不可改变,若改变其指向空间的值如*p1=20,则程序会报错;但p1的值是可以改变的,对p1重新赋值如p1=&k是没有任何问题的。

    在定义2const限定的是指针p2,若改变p2的值如p2=&k,程序将会报错;但*p2,即其所指向空间的值可以改变,如*p2=80是没有问题的,程序正常执行。

    6修饰函数参数

    const修饰符也可以修饰函数的参数,当不希望这个参数值在函数体内被意外改变时使用。所限定的函数参数可以是普通变量,也可以是指针变量。举例如下:

    void fun1(const int i)

    {

    其它语句

    ……

    i++; //i的值进行了修改,程序报错

    其它语句

    }

    告诉编译器i在函数体中不能改变,从而防止了使用者的一些无意或者错误的修改。

    void fun2(const int *p)

    {

    其它语句

    ……

    (*p)++; //p指向空间的值进行了修改,程序报错

    其它语句

    }

    7修饰函数的返回值

    Const修饰符也可以修饰函数的返回值,返回值不可被改变。

    Const int Fun(void);

    到这里const的定义和常用的说明我给大家做了描述,有疑问和其他想法欢迎交流~


    收藏 0 回复 0 浏览 209
  • 什么是单片机的闩锁效应?

    大家好!我是张飞实战电子黄忠老师!今天给大家分享什么是单片机的闩锁效应?

    什么是“闩锁效应”?这个词儿对我们来讲可能有点陌生。从构造上来看,单片机由大量的PN结组成。有一个由四重结构“PNPN”组成的部分,其中连接了两个PN结。PNPN的结构是用作功率开关元件的“晶闸管”的结构,并且单片机中的PNPN部分被称为“寄生晶闸管”。
        晶闸管由三个端子组成:阳极(正极),阴极(负极)和栅极(门)。通常,电流不从阳极流向阴极,但是当信号输入到栅极时,电流从阳极流向阴极。一旦电流开始流动,除非电源关闭,否则它将继续流动。由于此时的导通电阻非常小,因此流过大电流。在单片机的寄生晶闸管中发生相同现象的现象称为“闩锁”。
        当单片机发生闩锁时,大电流流入内部,不仅导致无法正常工作,而且还可能导致单片机内部的导线熔化并损坏元件。如果使用正确,将不会发生闩锁,但是如果您错误地启动电源或陡峭的高压噪声进入引脚,则会发生闩锁。图1是单片机表面上的金属布线的图片,该金属布线实际上已被闩锁电流熔化。

    图片1.jpg

    一、晶闸管结构是什么样的?
    2显示了晶闸管(PNPN)的结构。当将正电势施加到阳极而将负电势施加到阴极时,由于J1和J3为正向,而J2为反向,因此没有电流从阳极流向阴极。

    图片2.jpg

    然而,当将电压施加到栅极并且电流流动时,J2的反向电流被栅极电流加速,并且电流流过J2。由于J1和J3本质上是向前的,因此当发生这种现象时,电流开始从阳极流向阴极。一旦电流开始流动,除非阳极电源关闭,否则它将继续流动。这是晶闸管切换操作。利用这种操作,晶闸管被用作电力设备中的开关元件。PNPN结被认为是PNP晶体管和NPN型的组合,如图2-b所示。电路图显示了如图2-c所示的双晶体管配置。Tr1的发射极(E)成为晶闸管阳极,基极是Tr1的集电极(C),Tr2的基极(B),阴极是Tr2的发射极(E)。

    二、闩锁发生的机理?
    3显示了应用于单片机CMOS中的两个晶体管。

    图片3.jpg

    上图中的示例适用于N型衬底。此外,存在P型衬底的情况,但在两种情况下均会形成寄生PNPN结,因此可以以相同方式考虑闩锁的原理。Tr1由PMOS的源极的P沟道形成,该PMOS的源极连接到N型衬底的电源,然后再连接到P阱。然后,Tr2由从N型衬底连接到P阱和GND的NMOS源极的N够到路径形成。

    Tr1和Tr2形成为如图3的CMOS中的黄线所示。电源侧为阳极,GND侧为阴极,而栅极等效于NMOS P阱。CMOS输入线连接到NMOS的栅极。栅极和P-WELL在插入栅极氧化膜的情况下形成与电容器相同的结构。电容器很容易通过高频信号,因此,如果噪声进入输入线,并且噪声的dV/dt大(高频分量大),则它会穿过栅氧化膜并到达P阱。这将触发PNPN结导通,从而导致大电流从电源流向GND。
    另外,如果电源线急剧波动,特别是如果它向负侧波动,则栅极电压将高于电源电压,并且状态将与噪声进入栅极时相同。如果在建立单片机的电源之前在端子上施加了电压,则会发生此状态。

     


    收藏 0 回复 0 浏览 200
  • USB的包结构以及包的类型

    今天我们来详细地说说数据包的结构以及它们的传输过程。

    USB是串行总线,所以数据是一位一位地在数据线上传送的。既然是一位一位地传送,就存在着一个数据位先后的问题。usb使用的是LSB在前的方式,即先出来的是最低位数据,接下来是次低位,最后是最高位(MSB)。一个包,又被分成了很多个域(field),LSBMSB就是以域为单位来划分的。

    前面说过,USB数据在发送到总线上之前,要先经过位填充,再经过NRZ1编码。在这里讨论时,所用的数据都是原始的数据,即没有经过位填充和NRZ编码的原始数据。以后也是如此,凡是没有明确说明是位填充或NRZI编码过的数据,默认为原始的数据。另外还有一个数据传输方向的问题,因为在USB系统中,主机处于主导地位,所以把从设备到主机的数据叫做输入,从主机到设备的数据叫做输出。

    USB总线上传输数据是以包为基本单位的。一个包被分成不同的域。根据不同类型的包,所包含的域是不一样的。但是不同的包有个共同的特点,就是都要以同步域开始,紧跟一个包标识符PD( Packet Identifier),最终以包结束符EOP(End Of Packet)来结束这个包。

    同步域是用来告诉USB的串行接口引擎数据要开始传输了,请做好准备。除此之外,同步域还可以用来同步主机端和设备端的数据时钟,因为同步域是以一串0开始的,0USB总线上就被编码为电平翻转,结果就是每个数据位都发生电平变化,这让串行接口引擎很容易就能恢复出采样时钟信号;对于全速设备和低速设备,同步域使用的是0000001(二进制数,线上的发送顺序);对于高速设备,同步域使用的是310,后面跟11(需要注意的是,这是对发送端的要求,接收端解码时,0的个数可以少于这个数)

    1是一个全速或者低速USB数据包的同步域经过NRZ编码后的波形。这个波形有7次电平翻转,即对应着70,最后一个电平不翻转,即对应着11当串行接口引擎检测到一个位的数据未发生翻转后(即收到数据1),就认为包标识符PID开始了,如图1.9.1中的PID0PD1,就是包标识符的最低两位。

    image.png

    1 全速设备和低速设备的同步域

     

    包结束符EOP,对于高速设备和全速/低速设备也是不一样的。全速/低速设备的EOP是一个大约为2个数据位宽度的单端0(SE0)信号。SE0的意思就是,D+D同时都保持为低电平。由于USB使用的是差分数据线,通常都是一高一低的,SE0不同,是一种都为低特殊的状态。SE0用来表示一些特殊的意义,例如包结束、复位信号等。前面提到USB集线器对USB设备进行复位的操作,就是通过将总线设置为SE0状态大约10ms来实现的。对于高速设备的EOP,使用故意的位填充错误来表示。那么如何判断一个位填充错误是真的位填充错误还是包结束呢?这个由CRC校验来判断。如果CRC校验正确,则说明这个位填充错误是EOP;否则,说明传输出错。具体的定义请参看USB协议,这里只要知道有EOP这么一个东西就行了。

    包标识符PID是用来标识一个包的类型的它总共有8,其中USB协议使用的只有4(PID~PID3),另外4(PI4~PID7)PID~PD3的取反,用来校验PIDUSB协议规定了4类包,分别是令牌包(token packet,PD1~001)、数据包( data packet,pid1~011)、握手包(handshake packet,piD~010)和特殊包( special packet,PiD1~000)。不同类的包又分成几种具体的包。图2 USB2.0协议中规定的各种PID,其中有些是在USB1.1协议中没有的,用号标出。

    image.png

    2  USB2.0中定义的各种PID

    以上是数据包的结构以及它们传输的过程,今天的分享就到这里。


    收藏 0 回复 0 浏览 200
×
黄忠