发帖数

53

原创数

53

关注者

11

阅读数

10236

点赞数

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 浏览 286
  • 带你在单片机编程中熟练使用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 浏览 234
  • 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 浏览 232
  • 单片机学习之GPIO

    大家好,我是张飞实战电子黄忠老师,今天分享如何通过手册理解单片机IO知识点

    含义解释:

    1. GPIO:同我们常说的IO口一样, General Purpose Input Output (通用输入/输出)简称为GPIO,每个GPIO端口可通过软件分别配置成输入或输出模式。

    2. 外设:指的是除CPU以外的外围功能模块,只不过这部分电路依旧被封装在单片机内部,比如IOADCDACTIM等。

    3. 复位:把MCU恢复到最开始的状态,比如说我们把电脑重启了一次,就相当于复位了一次,在这里我们把MCU恢复到初始的状态称为复位。

    4. 往某一位写1,在硬件上就相当于把把它设置成高电平,清0则与之相反。

    芯片的缩略封装图:

    图片1.png 

    STM32F373CCT6 总共有48个引脚(图中左上角红色圈起来的1代表芯片的1号引脚,后面的以此类推,我们这里把1脚简称1Pin),分以下几个类别

    1.可以编程控制的引脚PAx(x表示012)PBx(x表示012)等以相同类似方式命名的。STM32F373CCT6分多组 IO口,分别用大写字母表示,即

    x=A/B/C/D/E/F,例如GPIOA,表示AIO口,这组IO口下面又有很多引脚,那么我们就用PA0PA1PA2等方式来表示,每组下面最多16IO口。通俗点来讲:GPIOA就相当于八年级五班这个班级,PA0PA1相当于班里的学生,有叫李刚的,有叫张的等等,每个班最多16个学生。

    我们看到有的可编程控制的引脚,例如PC14-OSC32-IN那么说明这个引脚有多种功能,可以当IO口用,也可以当做OSC32-IN用,在下面我们会具体解释这样的引脚。

    2. 不可编程控制的引脚1Pin(备用电源正脚),7 Pin(复位脚), 8 Pin(模拟电源负脚), 9 Pin(模拟电源/参考电压正脚), 17 Pin(数字电源正脚),23 PinSDADC1, SDADC2, SDADC3 地),24 PinSDADC1, SDADC2, SDADC3 电源)25 PinSDADC1, SDADC2, SDADC3的外部参考电压正),44 Pin(启动内存选择引脚),47 Pin(数字电源负脚),48 Pin(数字电源正脚)。

    1. 后备区域供电脚 VBAT 脚的供电采用 CR1220 纽扣电池和 VCC3.3 混合供电的方式,在有外部电源 (VCC3.3) 的时候, CR1220 不给 VBAT 供电, 而在外部电源断开的时候, 则由 CR1220给其供电。这样,VBAT 总是有电的,以保证 RTC 的走时以及后备寄存器的内容不丢失。

    2. BOOT0

    图片2.png 

    关于详细的引脚功能定义可以查阅《STM32F373xx》数据手册第33页,这里我们解释下关于引脚的功能问题:

    1. 默认功能:也就是引脚的普通功能。

    2. 复用功能:即将IO口用作普通输入输出以外的功能,通过配置相关寄存器后选择的功能,例如串口输入输出,使用时需要配置复用模式。

    想要配置成复用功能,首先需要查看引脚定义看看这个IO口可不可以被配成复用功能,这个是由IO的内部电路决定的。如果有才可以被配置,配置成复用功能不仅仅是要通过寄存器GPIOx_MODER配制成复用功能模式,而且还要通过GPIOx_AFRLGPIOx_AFRH寄存器选择IO复用功能。这样IO口才能真正被配成复用功能

    3. 附加功能:配置相关外设寄存器来选择的功能,比方配置ADC使能某些通道等来使能相应管脚的附加功能同样想要配置成附加功能,首先需要查看引脚定义看看这个IO口可不可以被配成附加功能,这个也是由IO的内部电路决定的。如果有,那么通过寄存器GPIOx_MODER配制成模拟功能模式

    每组通用 I/O 端口包括 4 32 位配置寄存器 (MODEROTYPEROSPEEDRPUPDR) 、2 32 位数据寄存器(IDR ODR) 、1 32 位置位/复位寄存器 (BSRR)1 32 位锁定寄存器 (LCKR) 2 32 位复用功能选择寄存器(AFRH AFRL)等,可以被配置成一下几种不同的模式:

    输入上拉IO口上拉就是在IO口通过接一个电阻到电源(注意这个电压要和单片机供电电压相同,否则过高会烧毁IO),电阻的大小决定了电源到IO口电流的不同,这就是我们常说的弱上拉等。下面附图一张。

    图片3.png 

     输入下拉:下拉就是在IO口通过接一个电阻到地,电阻的大小决定了IO口到地电流的不同,这就是我们常说的弱下拉等。下面附图一张。

    图片4.png 

    输入浮空/模拟输入:浮空(floating)就是逻辑器件的输入引脚即不接高电平,也不接低电平。浮空最大的特点就是电压的不确定性,它可能是0V,也可能是VCC,还可能是介于两者之间的某个值浮空一般用来做ADC输入用,可能有的芯片把浮空模式和模拟输入模式分开了,在此解释一下,在浮空模式下使能了IO模拟功能就相当于是模拟输入

    图片5.png 

    开漏输出:开漏输出就是我们所说的OC输出,不输出电压,相当于N型三极管的集电极作为单片机的IO,需要在外部加一个上拉电阻配合使用。如图:

    图片6.png 

    推挽输出:可以输出高,低电平,但相对于普通的输出而言,这种输出方式增加了输出能力。如图:

     

    图片7.png 

    复用开漏输出、复用推挽输出:可以理解为GPIO口被用作第二功能时的配置情况(即并非作为通用IO口使用)。

    图片8.png 

    上图为引脚的内部电路框图(红圈内或旁边数字代表序号,下面简称1号等)

    输出部分解析:输出分三路

    第一路,1(/写动作-由片内外设控制)——>3号(经过一个逻辑门->输出控制电路)

    第二路,15号(写动作)——>14号(Bit Set/Reset register 位设置/清零寄存器),——>13号(Output data register数据输出寄存器)——>3号(经过一个逻辑门->输出控制电路)

    第三路,2号(复用功能输出)——>3号(经过一个逻辑门->输出控制电路)。

    三路都通过控制4号(MOS管电路,根据配置的不同模式,驱动P-MOS或者N-MOS或者两个一起驱动)——>5/7号的下拉/上拉电阻(我们可以看到上/下拉电阻有开关控制,意思就是可以通过外部的某些东西去控制使能或者失能上/下拉)——>6号的保护二极管(这里利用了二极管钳位的功能,可以在一部分程度上起到保护引脚的作用)——>IO口。

    输入操作解析:同样分三路

    第一路,IO——>6号的保护二极管输出到——>9号(模拟输入)——> 片上外设

    第二路,IO——>6号的保护二极管输出到——>8号(开关,可靠外部控制)——>10号(复用功能输入)——> 片上外设

    第三路,IO——>6号的保护二极管输出到——>8号(开关,可靠外部控制)——>12号(Input data register输入数据寄存器)——> 11号(可供读取数据)。

    如何结合寄存器以及硬件电路来实现具体输入输出请看下篇分析~


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

    由于在嵌入式系统中必须考虑程序规模的问题,因此,对程序中的变量的初始化也需要进行慎重的考虑。在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 浏览 228
×
黄忠