单片机

想必大家对蓝牙都不陌生,平时或多或少都用过,但单片机中的蓝牙模块是怎样的呢?这一期的干货将带你走进蓝牙模块的世界。

1、蓝牙模块简介

蓝牙模块是一种常用的无线通信模块,广泛应用于各种各类智能设备。无线蓝牙模块是指集成蓝牙功能的芯片基本电路集合,用于短距离2.4G的无线通讯模块。

对于最终用户来说,蓝牙模块是半成品,通过在模块的基础上功能再开发、封装外壳等工序,实现能够利用蓝牙通讯的最终产品。其中低功耗蓝牙模块(BLE)是指支持蓝牙协议4.0或更高的模块,也称为BLE模块。

“JDY-31"
JDY-31

“
HC-05
更多蓝牙模块小伙伴们可自行搜索

2、版本

蓝牙版本有V1.0、V2.0、V3.0、V4.0,最新的是V4.0,蓝牙向下兼容,比如带有V2.0的手机不能搜索识别蓝牙3.0的模块,反过来则可以。

由于苹果设备对蓝牙设备进行特殊设置,所以V4.0之前的蓝牙串口模块不能直接应用于苹果。使用苹果的朋友如果遇到手机与蓝牙模块配对不成功的情况,可以试试在应用商店搜索LightBlue,至于LightBlue的使用方法,小编也不太清楚,有需要的小伙伴可以去摸索摸索(或者另外买个安卓的~)。

3、工作模式

蓝牙模块具有两种工作模式:

命令响应工作模式和自动连接工作模式。

当模块处于自动连接工作模式时,将自动根据事先设定的方式连接的数据传输;当模块处于命令响应工作模式时能执行 AT 命令,用户可向模块发送各种 AT 指令,为模块设定控制参数或发布控制命令。

AT 指令只能在模块未连接状态下才能生效,一旦蓝牙模块与设备连接上,蓝牙模块即进入数据透传模式(AT指令是应用于终端设备与PC应用之间的连接与通信的指令,AT 即Attention。AT指令是以AT作首,以字符结束的字符串,AT指令的响应数据包含在其中。每个指令执行成功与否都有相应的返回。)
不同的蓝牙模块对应的蓝牙AT指令集是有差别的,一般在卖家提供的蓝牙模块中文数据手册中,里面都有对应的AT指令集。

4、配置

将蓝牙模块与usb转ttl的RXD、TXD、GND、VCC连接好,按着蓝牙模块上的小按键不放,然后上电(按住上电是配置模式,直接上电是正常使用模式),使用蓝牙测试软件,让模块进入绝对AT模式,获取模块信息,可以得到蓝牙模块的信息。

通常配置波特率、主从机、配对密码,可以设置模块参数,设置设备名称、连接密码等。须确定波特率一致,不然会出现乱码而不能成功地进行之间的信息互传。

蓝牙模块可以当全双工串口使用,无需了解复杂的蓝牙协议。

5、硬件连接

和连接转换模块一样,蓝牙模块的RX连接单片机的TX,蓝牙模块的TX连接单片机的RX,还有VCC和GND管脚连上就好了,其他管脚通常不用管。

6、程序

下面这个程序是用手机连接蓝牙,在蓝牙串口助手发送1,单片机执行数码管显示函数,发送0,数码管灭。

主函数

“”"

在中断中接收数据

“”"

51单片机的串口中断是一个字节一个字节地接收数据的,比如手机向单片机发送了数据“123”,单片机中的中断函数会进入三次,把数据“123”分三次接收完,一次接收一个字符。

7、手机端操作

手机连接蓝牙,通过手机蓝牙串口助手发送数据(蓝牙串口助手直接在手机应用商店找)。
蓝牙模块有带一个指示灯,在连接成功蓝牙之前,蓝牙上面的红灯一直在闪烁,成功连接后,红灯会停止闪烁,每发送一条指令,电脑端的串口调试助手就能收到手机端发送的消息。

“”

大部分蓝牙串口app能修改手机页面的按键名称,可根据需要自行修改。

调试好了蓝牙模块和手机之间的通信之后,可以通过手机蓝牙串口调试助手发送一些字符,让单片机执行特定的命令。可以尝试做一个蓝牙遥控小车,控制家电开关通断等等。

以上是一个简单的蓝牙模块的使用,还有各种神操作就让大家自己去尝试啦~

本文转载自:华师无协
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。

围观 399

在入门单片机时,想必大家都都会遇到一下这种情况:

unsigned char a = 0x12;
unsigned char b = 0x34;
unsigned int c = 0;

如何把两个8位数据合在一起变成16位数据呢?

一般情况下大家都会这样做,我最初是也是这么做的。

方法1 【使用移位指令】

 int c = (a<<8)|b;

方法2 【使用指针】

unsigned char *cptr;
cptr = (unsigned char*)(&d);
cptr[0] = a;
cptr[1] = b;

方法3 【强制指针类型转换】

*((unsigned char*)(&d)) = a;
*((unsigned char*)(&d)+1) = b;
或
((unsigned char*)(&d))[0] = a;
((unsigned char*)(&d))[1] = b;

以上这三种方法都是没有错误的,但在keil编译器中编译出的结果是不一样的。第三种方法编译出的代码会更简洁。

今天就交给大家第4种方法

方法4 【联合体】

typedef union{
unsigned int i;
unsigned char c[2];
}u_int;
unsigned char dH = 0x11, dL=0x22;
unsigned int d;
u_int ud;
ud.c[0] = dH;
ud.c[1] = dL;
d = ud.i;
此时d = 0x1122;

这里就是利用了联合体union的特性来实现把两个8位数据合并成一个16位数据的方法。在C语言里操作指针最容易出现错误,所以在遇到这样类似的问题,大家不妨使用联合体的方式进行处理数据,既不容易出现错误,生成的代码又简洁。

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

围观 21

在单片机系统里,按键是常见的输入设备,在本文将介绍几种按键硬件、软件设计方面的技巧。一般的在按键的设计上,一般有四种方案。

一是 GPIO 口直接检测单个按键,如图1.1所示;

二是按键较多则使用矩阵键盘,如图1.2所示;

三是将按键接到外部中断引脚上,利用按键按下产生的边沿信号进行按键检测,如图1.3所示;

四是利用单片机的 ADC,在不同的按键按下后,能够使得 ADC 接口上的电压不同,根据电压的不同,则可以识别按键,如图 1.4 所示。

“图
图 1.1 方案一

“图
图 1.2 方案二

“图
图 1.3 方案三

“图
图 1.4 方案四

在以上四种设计上,各有优点和不足。

第一种,是最简单和最基础的,对于单片机初学者很容易理解和使用,但是缺点是,需要在主循环中不断检测按键是否按下,并且需要做消抖处理。若主循环中某个函数任务占用时间较长,则按键会有不同程度的“失灵”。

第二种,优点是能够在有限的 GPIO 情况下,扩展尽可能多的按键。但缺点同上,需要不停检测按键是否按下。

第三种,是效率最高,不需要循环检测按键是否按下,但是缺点是,需要单片机有足够的外部中断接口以供使用。

第四种,优点是,只需要单片机的一个 ADC 接口,一根线,就能对多个按键进行识别,缺点是按键一旦内部接触不良,则可能按键串位,且按键产生的抖动,会造成一定的识别错误。

在以上的三种常见按键设计的基础上,现在分享我学习和工作中总结的按键方案。

改进一:在原方案一的基础上,加上与门电路,使得任何一个按键按下,都能产生中断,然后在中断里面识别是哪个按键被按下。因此不需要循环扫描,大大提高了效率。方案如图 1.5 所示。只需要每个按键对应地增加一个二极管,利用二极管的线与特性,可以实现按下任何按键,都能产生中断信号,但是按键之间互不影响。二极管选用普通整流二极管即可,本人亲测可行。

“图
图 1.5 改进一

改进二:在原有的 ADC 按键的基础上,也可用增加二极管的方式,实现按键中断,并在中断服务程序里进行 AD 转换,从而识别按键。电路如图 1.6 所示。

“
图 1.6 改进二

改进三:因为按键不可避免的有抖动,因此按键消抖可以通过硬件消抖和软件消抖。现在分享一个十分简单且有效的硬件消痘方法:给按键并联一个 104 左右的电容。软件上基本不用处理即可避免抖动。

改进四:在按键扫描检测的方案下,如果主循环中有某个函数占用时间较长,则按键会发生或长或短的“失灵”,现分享我的一个解决方案。将按键扫描放到定时器中断里面,这样就可周期性地检测按键按下情况,不受主循环的影响。并且,能解析出按键的不同状态,即按下、按住、弹起、未按下这四种状态,用以实现更丰富的功能。

但需注意两点:

一是定时器的定时时间,不可过长也不可过短,过长容易检测不到按下,过短会占用大量时间资源。

二是中断服务程序需简单明了,只做检测用,通过全局变量传递,在主循环内完成按键响应,中断服务函数内尽量不要占用太多时间。

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

围观 419

对于单片机程序占了多少字节?单片机还剩多少存储空间?

想必你看到这篇文章时对这两个问题也很迷糊吧,接下来我就把自己所了解到的关于单片机程序占用空间大小的问题做一个整理,方便自己也方便他人。

1、STM32类单片机在程序编译后会出现下面图片中所示内容

“”

上图中分别有Code、RO-data、RW-data、ZI-data,

Code:表示所要执行的代码,程序中所有的函数都位于此处。

RO-data:表示只读数据,程序中所定义的全局常量数据和字符串都位于此处。

RW-data:表示已初始化的读写数据,程序中定义并且初始化的全局变量和静态变量位于此处。

ZI-data:表示未初始化的读写数据,程序中定义了但没有初始化的全局变量和静态变量位于此处。

通过上面的描述就可以看出:

  • 下载到单片机FLASH的程序是:Code+RO-data+RW-data(上图中数据为字节数,kb=byte/1024);

  • 运行在RAM中的数据是:

    RW-data+ZI-data;

2、51类单片机编译后会有如下画面

“”

上图分别有data、xdata、const、code。

data:片内RAM区消耗。

xdata:片内扩展区RAM区消耗。

code:表示所要执行的代码,程序中所有的函数都位于此处。

下载到单片机FLASH中的程序大小为:code+const

下载到单片机RAM的大小为:data+xdata

本文来源:博客园-Brianzhangzhang,转载请注明出处!
原文链接:
https://www.cnblogs.com/brianblog/p/7117896.html

围观 185

1、单片机内部需要储存器、累加器,这些都需要逻辑门电路。比如锁存器就是一个D触发器,而触发器的置1、清0、置数的功能都需要跳变沿。D触发器就是上升沿后存入数据,而这个上升沿就得外部提供脉冲,这就是脉冲信号 ,而这个脉冲信号就是我们稳定的时钟信号。

2、单片机运行需要时钟支持—–就像计算机的CPU一样,如果没有时钟电路来产生时钟驱动单片机,那单片机就不能执行程序。单片机可以看成是在时钟驱动下的时序逻辑电路。

以MCS–51单片机为例:MCS–51单片机为12个机器周期执行一条指令,也就是说单片机运行一条指令必须要用12个时钟周期。没有这个时钟,单片机就跑不起来,也就没办法定时和进行和时间有关的操作。

时钟电路是微型计算机的心脏。CPU就是通过复杂的时序电路完成不同的指令功能的。MCS—51的时钟信号可以由两种信号产生:一种是内部方式,利用芯片内部的振荡电路,产生时钟信号;另一种为外部方式,时钟信号由外部引入。

3、电路中的晶振即石英晶体震荡器。由于晶振具有非常好的频率稳定性和抗外界干扰的能力,所以,晶振是用来产生时钟信号的,通过时钟信号来控制电路工作。 晶振的应用范围是非常广的,它的质量、频率精度也是差别很大的。通讯系统用的信号发生器的信号源,绝大部分也用的是石英晶体振荡器,通讯系统对晶振的精准度也有比较高的要求。 晶振是时钟电路中最重要的部件,它的主要作用是向显卡、网卡、主板等配件的各部分提供基准频率,它就像个标尺,工作频率不稳定会造成相关设备工作频率不稳定,自然容易出现问题。 晶振在数字电路的基本作用是提供一个时序控制的标准时刻。数字电路的工作是根据电路设计,在某个时刻专门完成特定的任务,如果没有一个时序控制的标准时刻,整个数字电路就会成为“聋子”,不知道什么时刻该做什么事情了。

首先我们要明确的一点是,单片机是一个集成芯片,它是由非常复杂的数字电路和其他电路集成的。而数字电路包括时序逻辑电路,可以说,没有时序,就没有数字电路,也就没有单片机。所以,单片机离不开时钟。

其二,单片机中的众多寄存器,存储器等是由D触发器构成,而操作D触发器就需要时钟沿,自然也就离不开时钟。

其三,单片机执行程序需要一个程序计数器,而程序计数器是与时钟脉冲直接挂钩的,每来一个时钟脉冲,程序计数器就加1。就像上面提到的那样,51单片机每12个时钟周期就执行一条程序,没有时钟,单片机就没法执行程序。

所以,说时钟就像是单片机的”心脏”,恰当至极。

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

围观 173

工程师在开发一个电路系统,往往会需要用到中央处理器,比如单片机、FPGA、或者DSP等等;当然一些简单的纯硬件电路项目方案例外,如充电器、热水壶等等。

作为单片机研发设计的项目,它的最小电路工作系统包含电源电路、复位电路、时钟频率电路;其中电源电路与复位电路,相信工程师都非常容易理解与设计。然而时钟频率电路,由于不同的开发项目功能需求不一样,设计的方案选择也不尽相同,很难得到有效的统一设计。

比如:

  • A项目对研发成本要求较严格,功能较简单;

  • B项目电路系统需要与外界电路系统完成串口通信,通信数据要求不能出错;

  • C项目包含一个时钟万年历功能,时间要求不能间断而且精度要求高。

针对单片机的时钟频率电路,工程师依据不同的项目要求去设计与选择匹配的方案,具体的选择方案包含三类。

01、外部晶振方案

所谓外部晶振方案,是指在单片机的时钟引脚X1与X2外部连接一个晶振。

“单片机外部晶振图"
单片机外部晶振图

优点:时钟频率精度高,稳定性能好;对于一些数据处理能力要求较高的项目,尤其是多个电路系统彼此需要信息通讯,如包含USB通讯、CAN通讯的项目,选用外部晶振的方案较多。

缺点:由于增加了外部晶振,所以研发的BOM表元器件成本增加扩大了。

02、内部晶振方案

所谓内部晶振方案,是指单片机利用内部集成的RC振荡电路产生的时钟频率。

“单片机内部晶振图"
单片机内部晶振图

优点:省去外部晶振,工程师可以有效的节约研发BOM元器件成本。

缺点:RC振荡电路产生的时钟频率精度比较低,误差较大,容易引起一些高频率通信的数据交互错误。

03、时钟芯片方案

所谓时钟芯片方案,是指在单片机外部加入一个专门处理时钟的时钟芯片,用来给单片机提供精准的时钟信号。

“单片机与时钟芯片电路"
单片机与时钟芯片电路

优点:精度高,误差小;适用于一些要求较高的电路项目。

缺点:电路设计复杂,工程师开发难度较高,研发BOM元器件成本高。

关于时钟芯片的一些电路特性,以美信的DS1338型号为例说明:

“DS1338时钟芯片"
DS1338时钟芯片

(1)供电

时钟芯片的供电电源包含两个部分:

  • VCC供电,是指电路项目系统的电源,同时也是单片机的电源。

  • Vbat供电,是指电池供电的电源,由于某种原因在VCC供电突然失去的条件下,时钟芯片自动启用Vbat电池电源,用以保持时钟芯片内部的时钟信号处理,不必因为电路系统电源VCC断电而失去电路工作。

(2)功能

时钟芯片内部集成时间的“秒”“分”“时”“日”“周”“月”和“年”详细信息计时电路功能,通过IIC通信方式将时间的信息发送至单片机,单片机即可获得高精度的时钟信息。

(3)接口

时钟芯片与单片机的接口是IIC通信接口,此接口方式为串口通信,工程师开发设计较为简单,容易实现电路功能;

(4)精度

精度,是指时钟芯片在正常工作条件下产生的时钟误差;例如美信的DS1338时钟芯片精度控制在10PPM,换算成一天24小时误差精度在0.8秒左右。

(5)应用

时钟芯片,一般用来处理精确计算时间的电路项目,如时间万年历。

结语

当然这三个方案都是针对一些工业与民用领域,如果涉及到航空航天应用领域,比如卫星导航与遥感测量等,则需要选择更高精度的时钟频率电路,如原子钟方案。

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

围观 86

我们通过IO和串口的软件开发,已经体验了嵌入式软件开发。不知道大家有没有疑惑,为什么软件能控制硬件?反正当年我学习51的时候,有这个疑惑。今天我们就暂停软件开发,分析单片机到底是如何软硬件结合的。并通过一个基本的程序,分析单片机程序的编译,运行。

软硬件结合

初学者,通常有一个困惑,就是为什么软件能控制硬件?就像当年的51,为什么只要写P1=0X55,就可以在IO口输出高低电平?要理清这个问题,先要认识一个概念:地址空间。

寻址空间

什么是地址空间呢?所谓的地址空间,就是PC指针的寻址范围,因此也叫寻址空间。

大家应该都知道,我们的电脑有32位系统和64位系统之分,为什么呢?因为32位系统,PC指针就是一个32位的二进制数,也就是0xffffffff,范围只有4G寻址空间。现在内存越来越大,4G根本不够,所以需要扩展,为了能访问超出4G范围的内存,就有了64位系统。STM32是多少位的?是32位的,因此PC指针也是32位,寻址空间也就是4G。

我们来看看STM32的寻址空间是怎么样的。在数据手册《STM32F407_数据手册.pdf》中有一个图,这个图,就是STM32的寻址空间分配。所有的芯片,都会有这个图,名字基本上都是叫Memory map,用一个新芯片,就先看这个图。

“”
  • 最左边,8个block,每个block 512M,总共就是4G,也就是芯片的寻址空间。
  • block 0 里面有一段叫做FLASH,也就是内部FLASH,我们的程序就是下载到这个地方,起始地址是0X800 0000,大家注意,这个只有1M空间。现在STM32已经有2M flash的芯片了,超出1M的FLASH放在哪里呢?请自行查看对应的芯片手册。
  • 3 在block 1 内,有两段SRAM,总共128K,这个空间,也就是我们前面说的内存,存放程序使用的变量。如果需要,也可以把程序放到SRAM中运行。407不是有196K吗?
  • 其实407有196K内存,但是有64k并不是普通的SRAM,而是放在block 0 内的CCM。这两段区域不连续,而且,CCM只能内核使用,外设不能使用,例如DMA就不能用CCM内存,否则就死机。
  • block 2,是Peripherals,也就是外设空间。我们看右边,主要就是APB1/APB2、AHB1/AHB2,什么东西呢?回头再说。
  • block 3、block4、block5,是FSMC的空间,FSMC可以外扩SRAM,NAND FALSH,LCD等外设。

好的,我们分析了寻址空间,我们回过头看看,软件是如何控制硬件的。在IO口输出的例程中,我们配置IO口是调用库函数,我们看看库函数是怎么做的。

例如:

GPIO_SetBits(GPIOG, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3);

这个函数其实就是对一个变量赋值,对GPIOx这个结构体的成员BSRRL赋值。

void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
 /* Check the parameters */
 assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
 assert_param(IS_GPIO_PIN(GPIO_Pin));

 GPIOx->BSRRL = GPIO_Pin;
}

assert_param:这个是断言,用于判断输入参数是否符合要求GPIOx是一个输入参数,是一个GPIO_TypeDef结构体指针,所以,要用->获取其成员

GPIOx是我们传入的参数GPIOG,具体是啥?在stm32f4xx.h中有定义。

#define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)

GPIOG_BASE同样在文件中有定义,如下:

#define GPIOG_BASE           (AHB1PERIPH_BASE + 0x1800)

AHB1PERIPH_BASE,AHB1地址,有点眉目了吧?在进一步看看

/*!< Peripheral memory map */
#define APB1PERIPH_BASE       PERIPH_BASE
#define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000)
#define AHB1PERIPH_BASE       (PERIPH_BASE + 0x00020000)
#define AHB2PERIPH_BASE       (PERIPH_BASE + 0x10000000)

再找找PERIPH_BASE的定义

#define PERIPH_BASE           ((uint32_t)0x40000000)    

到这里,我们可以看出,操作IO口G,其实就是操作0X40000000+0X1800这个地址上的一个结构体里面的成员。说白了,就是操作了这个地方的寄存器。实质跟我们操作普通变量一样,就像下面的两句代码,区别就是变量i是SRAM空间地址,0X40000000+0X1800是外设空间地址。

u32 i;
i = 0x55aa55aa;

这个外设空间地址的寄存器是IO口硬件的一部分。如下图,左边的输出数据寄存器,就是我们操作的寄存器(内存、变量),它的地址就是0X40000000+0X1800+0x14.

“”

控制其他外设也类似,就是将数据写到外设寄存器上,跟操作内存一样,就可控制外设了。

寄存器,其实应该是内存的统称,外设寄存器应该叫做特殊寄存器。慢慢的,所有人都把外设的叫做寄存器,其他的统称内存或RAM。寄存器为什么能控制硬件外设呢?因为,粗略的说,一个寄存器的一个BIT,就是一个开关,开就是1,关就是0。通过这个电子开关去控制电路,从而控制外设硬件。

纯软件-包罗万象的小程序

我们已经完成了串口和IO口的控制,但是我们仅仅知道了怎么用,对其他一无所知。程序怎么跑的?代码到底放在哪里?内存又是怎么保存的?下面,我们通过一个简单的程序,学习嵌入式软件的基本要素。

分析启动代码

函数从哪里开始运行?

每个芯片都有复位功能,复位后,芯片的PC指针(一个寄存器,指示程序运行位置,对于多级流水线的芯片,PC可能跟真正执行的指令位置不一致,这里暂且认为一致)会复位到固定值,一般是0x00000000,在STM32中,复位到0X08000004。因此复位后运行的第一条代码就是0X08000004。前面我们不是拷贝了一个启动代码文件到工程吗?startup_stm32f40_41xxx.s,这个汇编文件为什么叫启动代码?因为里面的汇编程序,就是复位之后执行的程序。在文件中,有一段数据表,称为中断向量,里面保存了各个中断的执行地址。复位,也是一个中断。

芯片复位时,芯片从中断表中将Reset_Handler这个值(函数指针)加载到PC指针,芯片就会执行Reset_Handler函数了。(一个函数入口就是一个指针)

; Vector Table Mapped to Address 0 at Reset
                AREA    RESET, DATA, READONLY
                EXPORT  __Vectors
                EXPORT  __Vectors_End
                EXPORT  __Vectors_Size

__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler
                DCD     HardFault_Handler          ; Hard Fault Handler
                DCD     MemManage_Handler          ; MPU Fault Handler
                DCD     BusFault_Handler           ; Bus Fault Handler
                DCD     UsageFault_Handler         ; Usage Fault Handler

Reset_Handler函数,先执行SystemInit函数,这个函数在标准库内,主要是初始芯片时钟。然后跳到__main执行,__main函数是什么函数?

是我们在main.c中定义的main函数吗?后面我们再说这个问题。

; Reset handler
Reset_Handler    PROC
                 EXPORT  Reset_Handler             [WEAK]
        IMPORT  SystemInit
        IMPORT  __main

                 LDR     R0, =SystemInit
                 BLX     R0
                 LDR     R0, =__main
                 BX      R0
                 ENDP

芯片是怎么知道开始就执行启动代码的呢?或者说,我们如何把这个启动代码放到复位的位置?这就牵涉到一个一般情况下不关注的文件wujique.sct,这个文件在wujique\prj\Objects目录下,通常把这个文件叫做分散加载文件,编译工具在链接时,根据这个文件放置各个代码段和变量。

在MDK软件Options菜单Linker下有关于这个菜单的设置。

“”

把Use Memory Layout from Target Dialog前面的勾去掉,之前不可设置的框都可以设置了。点击Edit进行编辑。

“”

在代码编辑框出现了分散加载文件内容,当前文件只有基本的内容。

其实这个文件功能很强大,通过修改这个文件可以配置程序的很多功能,例如:1 指定FLASH跟RAM的大小于起始位置,当我们把程序分成BOOT、CORE、APP,甚至进行驱动分离的时候,就可以用上了。2 指定函数与变量的位置,例如把函数加载到RAM中运行。

“”

从这个基本的分散加载文件我们可以看出:

  • 第6行 ER_IROM1 0x08000000 0x00080000定义了ER_IROM1,也就是我们说的内部FLASH,从0x08000000开始,大小0x00080000。
  • 第7行 .o (RESET, +First)从0x08000000开始,先放置一个.o文件, 并且用(RESET, +First)指定RESET块优先放置,RESET块是什么?请查看启动代码,中断向量就是一个AREA,名字叫RESET,属于READONLY。这样编译后,RESET块将放在0x08000000位置,也就是说,中断向量就放在这个地方。DCD是分配空间,4字节,第一个就是__initial_sp,第二个就是Reset_Handler函数指针。也就是说,最后编译后的程序,将Reset_Handler这个函数的指针(地址),放在0x800000+4的地方。所以芯片在复位的时候,就能找到复位函数Reset_Handler。
  • 第8行 *(InRoot$$Sections)什么鬼?GOOGLE啊!回头再说。
  • 第9行 .ANY (+RO)意思就是其他的所有RO,顺序往后放。就是说,其他代码,跟着启动代码后面。
  • 第11行 RW_IRAM1 0x20000000 0x00020000定义了RAM大小。
  • 第12行 .ANY (+RW +ZI)所有的RW ZI,全部放到RAM里面。RW,ZI,也就是变量,这一行指定了变量保存到什么地址。

分析用户代码

到此,基本启动过程已经分析完。下一步开始分析用户代码,就从main函数开始。1 程序跳转到main函数后:RCC_GetClocksFreq获取RCC时钟频率;SysTick_Config配置SysTick,在这里打开了SysTick中断,10毫秒一次。

Delay(5);延时50毫秒。

int main(void)
{
  GPIO_InitTypeDef GPIO_InitStructure;

 /*!< At this stage the microcontroller clock setting is already configured,
       this is done through SystemInit() function which is called from startup
       files before to branch to application main.
       To reconfigure the default setting of SystemInit() function,
       refer to system_stm32f4xx.c file */

  /* SysTick end of count event each 10ms */
  RCC_GetClocksFreq(&RCC_Clocks);
  SysTick_Config(RCC_Clocks.HCLK_Frequency / 100);

  /* Add your application code here */
  /* Insert 50 ms delay */
  Delay(5);

2 初始化IO就不说了,进入while(1),也就是一个死循环,嵌入式程序,都是一个死循环,否则就跑飞了。

/*初始化LED IO口*/
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE);

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;

GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOG, &GPIO_InitStructure);    

/* Infinite loop */
mcu_uart_open(3);
while (1)
{
  GPIO_ResetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);
  Delay(100);
  GPIO_SetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);
  Delay(100);
  mcu_uart_test();

  TestFun(TestTmp2);
}

3 在while(1)中调用TestFun函数,这个函数使用两个全局变量,两个局部变量。

/* Private functions ---------------------------------------------------------*/
u32 TestTmp1 = 5;//全局变量,初始化为5
u32 TestTmp2;//全局变量,未初始化

const u32 TestTmp3[10] = {6,7,8,9,10,11,12,13,12,13};

u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值
{
 u8 test_tmp1 = 4;//局部变量,初始化
 u8 test_tmp2;//局部变量,未初始化

 static u8 test_tmp3 = 0;//静态局部变量

 test_tmp3++;

 test_tmp2 = x;

 if(test_tmp2> TestTmp1)
  test_tmp1 = 10;
 else
  test_tmp1 = 5;

 TestTmp2 +=TestTmp3[test_tmp1];

 return test_tmp1;
}

然后程序就一直在main函数的while循环里面执行。中断呢?对,还有中断。中断中断,就是中断正常的程序执行流程。我们查看Delay函数,uwTimingDelay不等于0就死等?谁会将uwTimingDelay改为0?

/**
  * @brief  Inserts a delay time.
  * @param  nTime: specifies the delay time length, in milliseconds.
  * @retval None
  */
void Delay(__IO uint32_t nTime)
{
  uwTimingDelay = nTime;

  while(uwTimingDelay != 0);
}

搜索uwTimingDelay变量,函数TimingDelay_Decrement会将变量一直减到0。

/**
  * @brief  Decrements the TimingDelay variable.
  * @param  None
  * @retval None
  */
void TimingDelay_Decrement(void)
{
  if (uwTimingDelay != 0x00)
  {
    uwTimingDelay--;
  }
}

这个函数在哪里执行?经查找,在SysTick_Handler函数中运行。谁用这个函数?

/**
  * @brief  This function handles SysTick Handler.
  * @param  None
  * @retval None
  */
void SysTick_Handler(void)
{
  TimingDelay_Decrement();
}

经查找,在中断向量表中有这个函数,也即是说这个函数指针保存在中断向量表内。当发生中断时,就会执行这个函数。当然,在进出中断会有保存和恢复现场的操作。这个主要涉及到汇编,暂时不进行分析了。有兴趣自己研究研究。通常,现在我们开发程序不用关心上下文切换了。

__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler
                DCD     HardFault_Handler          ; Hard Fault Handler
                DCD     MemManage_Handler          ; MPU Fault Handler
                DCD     BusFault_Handler           ; Bus Fault Handler
                DCD     UsageFault_Handler         ; Usage Fault Handler
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     SVC_Handler                ; SVCall Handler
                DCD     DebugMon_Handler           ; Debug Monitor Handler
                DCD     0                          ; Reserved
                DCD     PendSV_Handler             ; PendSV Handler
                DCD     SysTick_Handler            ; SysTick Handler

余下问题

1 __main函数是什么函数?是我们在main.c中定义的main函数吗?2 分散加载文件中*(InRoot$$Sections)是什么?3 ZI段,也就是初始化为0的数据段,什么时候初始化?谁初始化?

为什么这几个问题前面留着不说?因为这是同一个问题。顺藤摸瓜!

通过MAP文件了解代码构成

编译结果

程序编译后,在下方的Build Output窗口会输出信息:

*** Using Compiler 'V5.06 update 5 (build 528)', folder: 'C:\Keil_v5\ARM\ARMCC\Bin'
Build target 'wujique'
compiling stm32f4xx_it.c...
...
assembling startup_stm32f40_41xxx.s...
compiling misc.c...
...
compiling mcu_uart.c...
linking...
Program Size: Code=9038 RO-data=990 RW-data=40 ZI-data=6000  
FromELF: creating hex file...
".\Objects\wujique.axf" - 0 Error(s), 0 Warning(s).
Build Time Elapsed:  00:00:32
  • 编译目标是wujique
  • C文件compiling,汇编文件assembling,这个过程叫编译
  • 编译结束后,就进行link,链接。
  • 最后得到一个编译结果,9038字节code,RO 990,RW 40,ZI 6000。CODE,是代码,很好理解,那RO、RW、ZI都是什么?
  • FromELF,创建hex文件,FromELF是一个好工具,需要自己添加到option中才能用

map文件配置

更多编译具体信息在map文件中,在MDK Options中我们可以看到,所有信息都放在\Listings\wujique.map

默认很多编译信息可能没钩,钩上所有信息会增加编译时间。

“”

map文件

打开map文件,好乱?习惯就好。我们抓重点就行了。

“”

map 总信息

从最后看起,看到没?最后的这一段map内容,说明了整个程序的基本概况。

有多少RO?RO到底是什么?

有多少RW?RW又是什么?

ROM为什么不包括ZI Data?为什么包含RW Data?

“”

Image component sizes

往上,看看Image component sizes,这个就比刚刚的总体统计更细了。

这部分内容,说明了每个源文件的概况

首先,是我们自己的源码,这个程序我们的代码不多,只有main.o,wujique_log.o,和其他一些STM32的库文件。

“”

第2部分是库里面的文件,看到没?里面有一个main.o。main函数是不是我们写的main函数?明显不是,我们的main函数是放在main.o文件。这么小的一个工程,用了这么多库,你以前关注过吗?估计没有,除非你曾经将一个原本在1M flash上的程序压缩到能在512K上运行。

“”

第3部分也是库,暂时没去分析这两个是什么东西。

“”

库文件是什么?库文件就是别人已经别写好的代码库。在代码中,我们经常会包含一些头文件,例如:

#include <stdarg.h>
#include <stdlib.h>
#include <string.h>  

这些就是库的头文件。这些头文件保存在MDK开发工具的安装目录下。我们经常用的库函数有:memcpy、memcmp、strcmp等。只要代码中包含了这些函数,就会链接库文件。

文件map

再往上,就是文件MAP了,也就是每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET

库文件是什么?

库文件就是别人已经别写好的代码库。

在代码中,我们经常会包含一些头文件,例如:

#include <stdarg.h>
#include <stdlib.h>
#include <string.h>   

这些就是库的头文件。这些头文件保存在MDK开发工具的安装目录下。

我们经常用的库函数有:

memcpy、memcmp、strcmp等。

只要代码中包含了这些函数,就会链接库文件。

文件map

再往上,就是文件MAP了,也就是每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET

“”

每个文件有有多行,例如串口,4个函数。

“”

然后是RAM的,main.o中的变量,放在0x20000000,总共有0x0000000c,类型是Data、RW。串口有两种变量,data和bss,什么是bss?这两个名称,是section name,也就是段的意思。看前面type和Attr,RW Data,放在.data段;RW Zero放在.bss段,RW Zero,其实就是ZI。到底哪些变量是RW,哪些是ZI?

“”

Image Symbol Table

再往上就是Image Symbol Table,就更进一步到每个函数或者变量的信息了

“”

例如,全局变量TestTmp1,是Data,4字节,分配的位置是0x20000004。

“”

TestTmp3数组放在哪里?放在0X080024E0这个地方,这可是代码区额。因为我们用const修饰了这个全局变量数组,告诉编译器,这个数组是不可以改变的,编译器就将这个数组保存到代码中了。程序中我们经常会使用一些大数组数据,例如字符点阵,通常有几K几十K大,不可能也没必要放到RAM区,整个程序运行过程这些数据都不改变,因此通过const修饰,将其存放到代码区。

const的用处比较多,可以修饰变量,也可以修饰函数。更多用法自行学习

“”

那局部变量存放在哪里呢?我们找到了test_tmp3,

“”

没找到test_tmp1/test_tmp2,为什么呢?在定义时,test_tmp3增加了static定义,意思就是静态局部变量,功能上,相当于全局变量,定义在函数内,限制了这个全局变量只能在这个函数内使用。那test_tmp1、test_tmp2放在哪里呢? 局部变量,在编译链接时,并没有分配空间,只有在运行时,才从栈分配空间。

u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值
{
 u8 test_tmp1 = 4;//局部变量,初始化
 u8 test_tmp2;//局部变量,未初始化

 static u8 test_tmp3 = 0;//静态局部变量

上一部分,我们留了一个问题,哪些变量是RW,哪些是ZI?我们看看串口变量的情况,UartBuf3放在bss段,其他变量放在.data段。为什么数组就放在bss?bss是英文Block Started by Symbol的简称。

“”

到这里,我们可解释下面几个概念了:

Code就是代码,函数。

RO Data,就是只读变量,例如用const修饰的数组。

RW Data,就是读写变量,例如全局变量跟static修饰的局部变量。

ZI Data,就是系统自动初始化为0的读写变量,大部分是数组,放在bss段。

RO Size等于代码加只读变量。

RW Size等于读写变量(包括自动初始化为0的),这个也就是RAM的大小。

ROM Size,也就是我们编译之后的目标文件大小,也就是FLASH的大小。但是?为什么会包含RW Data呢?因为所有全局变量都需要一个初始化的值(就算没有真正初始化,系统也会分配一个初始化空间),例如我们定义一个变量u8 i = 8;这样的全局变量,8,这个值,就需要保存在FALSH区。

“”

我们看看函数的情况,前面我们不是有一个问题吗?__main和main是一个函数吗?查找main后发现,main是main,放在0x08000579

“”

main是main,放在0x08000189

“”

__main到main之间发生了什么?还记得分散加载文件中的这句吗?

*(InRoot$$Sections)

__main就在这个段内。下图是__main的地址,在0x08000189。__Vectors就是中断向量,放在最开始。

“”

在分散加载文件中,紧跟RESET的就是*(InRoot$$Sections)。

“”

而且,RESET段正好大小0x00000188。

“”

巧合?参考PPT文档《ARM嵌入式软件开发.ppt》,或自行GOOGLE。

“”

这一段代码都完成什么功能呢?主要完成ZI代码的初始化,也就是将一部分RAM初始化为0。其他环境初始化。通常,我们不用管这一部分。

其他再往上,就是其他信息了,例如优化了哪些东西,移除了哪些函数。

最后

到这里,一个程序,是怎么组成的,程序是如何运行的,基本有一个总体印象了。

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

围观 95

欠压复位(BOR:Brownout Reset)是单片机可靠性的一项重要功能,通常用于解决电源问题。

1、概述

单片机的“电量不足”是电源电压不足或暂时降低,低于可靠运行所需的水平。许多单片机具有保护电路,该电路可检测电源电压何时低于此水平,并将设备置于复位状态,以确保在电源恢复时正确启动。

此操作也称为“欠压复位”,英文缩写为“BOR”。类似的功能称为低电压检测(LVD),它更复杂,增加了对多个电压电平的检测,可以在触发复位之前产生中断。

BOR通常由控制寄存器中的某个位使能,当BOR引起复位时,状态位会置1。该状态位在复位后仍然有效,并允许程序检测到问题并执行其他恢复或记录事件。

现在的STM、NXP、PIC等常见的单片机中,基本都有欠压复位的功能。

2、如果BOR被禁用会怎样?

如果BOR被禁用,一般情况下,其表现为电源电压稳定性下降。

至于原因,可能是电源老化或电池放电。

“”

如上图所示,V1是正常电源电压。V2是微控制器可能无法可靠运行的电压,V3显示为操作完全停止的点。

在V2和V3之间是一个“危险区域”,在该区域可能发生错误并且操作不可靠。当电源进出危险区域时,该设备可以正常工作数年,然后损坏,出现故障。

BOR级别设置为高于V2,并通过复位设备来代替危险区域。重置不太可靠,但总比不确定好。

3、BOR如何保护你的系统?

接下来,介绍一种情况,其中电源正常运行,但使用BOR解决了另一个问题。

“”

当电源关闭时,电压不会一直下降。相反,其他电源将电源电压保持在危险区域。这种电压的另外一种叫法是“虚假电量”。

目前,没有BOR可以检测到这种情况并引起复位。再次打开电源时,设备可能无法正常上电,因为可能不会触发上电复位电路。由于电源电压低于最小值并且没有复位,因此后续操作不确定。

比如某单片机电源范围为+4.0V至+5.5V。模块内部的工作电压(V1)是+5V。而单片机(V2)上的虚假电量电压为+1.5V。

还有其他两种情况,“RAM数据保持电压(VDR)”为+1.5V,“典型值”。“VDD启动电压”(VPOR)以确保内部上电复位为0V,“典型值”。将所有这些情况加在一起可以告诉我们,该设备处于危险区域之内。由于电压远高于此电压,因此无法预期上电复位(VPOR)。此外,由于虚假电量处于RAM保持电压下,因此也无法预期的欠压会使设备保持活动状态(VDR)。谁知道设备的其余部分在做什么?

为什么打开BOR可以解决此问题?如果欠压复位触发规范(VBOR)的范围是+3.7V至+4.35V,典型值为+4.0V。虚假电量电平远低于BOR的触发电压,就会触发欠压复位,解决不确定性问题。

总结,虚假电量可能有几种情况:外部信号,电路中的多个电源,电容器需要时间才能完全放电。

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

围观 786

页面

订阅 RSS - 单片机