STM32

STM32是STMicroelectronics(意法半导体)推出的一系列基于ARM Cortex-M内核的32位微控制器(MCU)产品。这些微控制器提供了广泛的产品系列,覆盖了多种不同的性能和功能需求,适用于各种应用领域,包括工业控制、汽车电子、消费类电子、医疗设备等。

STM32系列微控制器以其高性能、低功耗、丰富的外设接口和灵活的开发工具而闻名。它们通常具有丰富的存储器、多种通信接口(如UART、SPI、I2C、CAN等)、模拟数字转换器(ADC)、定时器、PWM输出等功能,以满足不同应用场景下的需求。

STM32微控制器通常使用标准的ARM Cortex-M内核,包括Cortex-M0、M0+、M3、M4和M7等,这些内核具有不同的性能和功耗特性,可根据具体应用的需求进行选择。此外,STM32系列还提供了多种封装和引脚配置,以满足不同尺寸和集成度的要求。

STMicroelectronics为STM32系列提供了丰富的开发工具和支持资源,包括基于ARM开发环境的集成开发环境(IDE)、调试器、评估板和参考设计等。这些工具和资源有助于开发人员快速开发和部署他们的应用,并提供了全面的技术支持和文档资料,帮助用户充分发挥STM32微控制器的性能和功能优势。

1、指针变量及例子

位带操作牵涉到的一个重要知识点就是指针变量。

这种位带映射操作,就是操作映射过后的地址,其实就是操作指针变量(存放地址的变量)。

指针变量是一种特殊的变量,它不同于一般的变量,一般变量存放的是数据本身,而指针变量存放的是数据的地址。《摘自百度百科【指针变量】》

指针变量的例子:

int main(void)
{
  uint32_t *p;

  p = (uint32_t *)(0x42210184);

  System_Initializes();
  while(1)
  {
    *p = 0;
    TIMDelay_Nms(500);

    *p = 1;
    TIMDelay_Nms(500);
  }
}

上面例子中给p指针变量赋的值是“0x42210184”,只是强制转换成(uint32_t *)这种指针类型。

而*p = 0;代表该地址上的数据值为0;也就是上面说的该地址存放的数据为0;

前面有一个朋友问过我关于指针变量的问题,看到这里,相信你应该知道使用指针变量,直接打印指针就可以判断指针是否越界。

2、指针变量---位带操作

上面代码中“0x42210184”代表STM32F103系列芯片中PA1的位带别名地址(就是映射过去的地址),截一个图,大家看看:

“STM32位带引申的指针变量问题"

提示:上图中对p的赋值,其实是一样的(在STM32中),都是0x42210184。

结合公式理解:

之前文章《位带操作原理》列出了关于片上外设区计算公式:

AliasAddr = 0x42000000+(A-0x40000000)*32 + n*4

对比截图中第一个p赋的值,就是片上外设的计算公式。

第二个p只是对代码优化了:“&”到“-”的优化,可以看编译器相关手册。

第4个p就是上一节代码中值,有没有发现,位带操作其实就操作指针变量啊?

这样相比读出寄存器,再&或者|再写入寄存器的效率要高多啦?

3、位带别名区最低有效位

有朋友发现,*p = 0;这样操作对地址0x42210184(PA1输出)写入0,PA1输出低。假如我写入0x10,那么PA1输出多少呢?

答案:输出低。

原因在于:在位带区中,每个比特都映射到别名地址区的一个字只有 LSB 有效,也就是最低一位有效。

4、位带操作另一种宏定义

有通过之前的两个公式,可以推出下图的公式:

“STM32位带引申的指针变量问题"

上面框起来的定义适合RAM和外设两种,假如定义一个LED为PA1,只需要将PA1相关参数传入即可。

LED另外一种定义:

#define LED BIT_ADDR((GPIOA_BASE + 12), 1)

这种定义需要注意:+12,其实是ODR相对GPIOA的基地址的偏移地址。

我曾在这里遇到的坑:我将STM32F1的移植到F4上,出现了问题,我找了半天才发现由于这个偏移地址不一样导致的。

STM32F1的ODR偏移是12,而F4的ODR偏移是20。所以,建议大家使用GPIOA->ODR这种方式。(不管是标准外设库还是HAL库都有这样定义)。

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

围观 69

本文将以STM32F10x为例,对标准库开发进行概览。主要分为三块内容:

  • STM32系统结构

  • 寄存器

  • 通过点灯案例,详解如何基于标准库构建STM32工程

STM32系统结构

“STM32f10xxx系统结构"
STM32f10xxx系统结构

内核IP

从结构框图上看,Cortex-M3内部有若干个总线接口,以使CM3能同时取址和访内(访问内存),它们是:指令存储区总线(两条)、系统总线、私有外设总线。有两条代码存储区总线负责对代码存储区(即 FLASH 外设)的访问,分别是 I-Code 总线和 D-Code 总线。

I-Code用于取指,D-Code用于查表等操作,它们按最佳执行速度进行优化。

系统总线(System)用于访问内存和外设,覆盖的区域包括SRAM,片上外设,片外RAM,片外扩展设备,以及系统级存储区的部分空间。

私有外设总线负责一部分私有外设的访问,主要就是访问调试组件。它们也在系统级存储区。

还有一个DMA总线,从字面上看,DMA是data memory access的意思,是一种连接内核和外设的桥梁,它可以访问外设、内存,传输不受CPU的控制,并且是双向通信。简而言之,这个家伙就是一个速度很快的且不受老大控制的数据搬运工。

处理器外设(内核之外的外设)

从结构框图上看,STM32的外设有串口、定时器、IO口、FSMC、SDIO、SPI、I2C等,这些外设按照速度的不同,分别挂载到AHB、APB2、APB1这三条总线上。

寄存器

什么是寄存器?寄存器是内置于各个IP外设中,是一种用于配置外设功能的存储器,并且有想对应的地址。一切库的封装始于映射。

“入手STM32单片机的知识点总结"

“入手STM32单片机的知识点总结"

是不是看的眼都花了,如果进行寄存器开发,就需要怼地址以及对寄存器进行字节赋值,不仅效率低而且容易出错。

库的存在就是为了解决这类问题,将代码语义化。语义化思想不仅仅是嵌入式有的,前端代码也在追求语义特性。

从点灯开始学习STM32

“入手STM32单片机的知识点总结"

内核库文件分析

cor_cm3.h

这个头文件实现了:

1、内核结构体寄存器定义。

2、内核寄存器内存映射。

3、内存寄存器位定义。跟处理器相关的头文件stm32f10x.h实现的功能一样,一个是针对内核的寄存器,一个是针对内核之外,即处理器的寄存器。

misc.h

内核应用函数库头文件,对应stm32f10x_xxx.h。

misc.c

内核应用函数库文件,对应stm32f10x_xxx.c。在CM3这个内核里面还有一些功能组件,如NVIC、SCB、ITM、MPU、CoreDebug,CM3带有非常丰富的功能组件,但是芯片厂商在设计MCU的时候有一些并不是非要不可的,是可裁剪的,比如MPU、ITM等在STM32里面就没有。

其中NVIC在每一个CM3内核的单片机中都会有,但都会被裁剪,只能是CM3 NVIC的一个子集。在NVIC里面还有一个SysTick,是一个系统定时器,可以提供时基,一般为操作系统定时器所用。misc.h和mics.c这两个文件提供了操作这些组件的函数,并可以在CM3内核单片机直接移植。

处理器外设库文件分析

startup_stm32f10x_hd.s

这个是由汇编编写的启动文件,是STM32上电启动的第一个程序,启动文件主要实现了

  • 初始化堆栈指针 SP;
  • 设置 PC 指针=Reset_Handler ;
  • 设置向量表的地址,并 初始化向量表,向量表里面放的是 STM32 所有中断函数的入口地址;
  • 调用库函数 SystemInit,把系统时钟配置成 72M,SystemInit 在库文件 stytem_stm32f10x.c 中定义;
  • 跳转到标号_main,最终去到 C 的世界。

system_stm32f10x.c

这个文件的作用是里面实现了各种常用的系统时钟设置函数,有72M,56M,48, 36,24,8M,我们使用的是是把系统时钟设置成72M。

Stm32f10x.h

这个头文件非常重要,这个头文件实现了:

1、处理器外设寄存器的结构体定义。

2、处理器外设的内存映射。

3、处理器外设寄存器的位定义。

关于 1 和 2 我们在用寄存器点亮 LED 的时候有讲解。

其中 3:处理器外设寄存器的位定义,这个非常重要,具体是什么意思?

我们知道一个寄存器有很多个位,每个位写 1 或者写 0 的功能都是不一样的,处理器外设寄存器的位定义就是把外设的每个寄存器的每一个位写 1 的 16 进制数定义成一个宏,宏名即用该位的名称表示,如果我们操作寄存器要开启某一个功能的话,就不用自己亲自去算这个值是多少,可以直接到这个头文件里面找。

我们以片上外设 ADC 为例,假设我们要启动 ADC 开始转换,根据手册我们知道是要控制 ADC_CR2 寄存器的位 0:ADON,即往位 0 写 1,即:

ADC->CR2=0x00000001;

这是一般的操作方法。现在这个头文件里面有关于 ADON 位的位定义:

 #define ADC_CR2_ADON ((uint32_t)0x00000001)

有了这个位定义,我们刚刚的代码就变成了:

ADC->CR2=ADC_CR2_ADON

stm32f10x_xxx.h

外设 xxx 应用函数库头文件,这里面主要定义了实现外设某一功能的结构体,比如通用定时器有很多功能,有定时功能,有输出比较功能,有输入捕捉功能,而通用定时器有非常多的寄存器要实现某一个功能。

比如定时功能,我们根本不知道具体要操作哪些寄存器,这个头文件就为我们打包好了要实现某一个功能的寄存器,是以机构体的形式定义的,比如通用定时器要实现一个定时的功能,我们只需要初始化 TIM_TimeBaseInitTypeDef 这个结构体里面的成员即可,里面的成员就是定时所需要操作的寄存器。

有了这个头文件,我们就知道要实现某个功能需要操作哪些寄存器,然后再回手册中精度这些寄存器的说明即可。

stm32f10x_xxx.c

stm32f10x_xxx.c:外设 xxx 应用函数库,这里面写好了操作 xxx 外设的所有常用的函数,我们使用库编程的时候,使用的最多的就是这里的函数。

SystemInit

工程中新建main.c 。

在此文件中编写main函数后直接编译会报错:

Undefined symbol SystemInit (referred from startup_stm32f10x_hd.o).

错误提示说SystemInit没有定义。从分析启动文件startup_stm32f10x_hd.s时我们知道,

;Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
;IMPORT SystemInit
;LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP

汇编中;分号是注释的意思

第五行第六行代码Reset_Handler调用了SystemInit该函数用来初始化系统时钟,而该函数是在库文件system_stm32f10x.c中实现的。我们重新写一个这样的函数也可以,把功能完整实现一遍,但是为了简单起见,我们在main文件里面定义一个SystemInit空函数,为的是骗过编译器,把这个错误去掉。

关于配置系统时钟之后会出文章RCC时钟树详细介绍,主要配置时钟控制寄存器(RCC_CR)和时钟配置寄存器(RCC_CFGR)这两个寄存器,但最好是直接使用CubeMX直接生成,因为它的配置过程有些冗长。

如果我们用的是库,那么有个库函数SystemInit,会帮我们把系统时钟设置成72M。

现在我们没有使用库,那现在时钟是多少?答案是8M,当外部HSE没有开启或者出现故障的时候,系统时钟由内部低速时钟LSI提供,现在我们是没有开启HSE,所以系统默认的时钟是LSI=8M。

库封装层级

“入手STM32单片机的知识点总结"

如图,达到第四层级便是我们所熟知的固件库或HAL库的效果。当然库的编写还需要考虑许多问题,不止于这些内容。我们需要的是了解库封装的大概过程。

将库封装等级分为四级来介绍是为了有层次感,就像打怪升级一样,进行认知理解的升级。

我们都知道,操作GPIO输出分三大步:

时钟控制:

STM32 外设很多,为了降低功耗,每个外设都对应着一个时钟,在系统复位的时候这些时钟都是被关闭的,如果想要外设工作,必须把相应的时钟打开。

STM32 的所有外设的时钟由一个专门的外设来管理,叫RCC(reset and clockcontrol),RCC 在STM32 参考手册的第六章。

STM32 的外设因为速率的不同,分别挂载到三条总系上:AHB、APB2、APB1,AHB为高速总线,APB2 次之,APB1 再次之。所以的IO 口都挂载到APB2 总线上,属于高速外设。

模式配置:

这个由端口配置寄存器来控制。端口配置寄存器分为高低两个,每4bit 控制一个IO 口,所以端口配置低寄存器:CRL 控制这IO 口的低8 位,端口配置高寄存器:CRH控制这IO 口的高8bit。

在4 位一组的控制位中,CNFy[1:0] 用来控制端口的输入输出,MODEy[1:0]用来控制输出模式的速率,又称驱动电路的响应速度,注意此处速率与程序无关,GPIO引脚速度、翻转速度、输出速度区别输入有4种模式,输出有4种模式,我们在控制LED 的时候选择通用推挽输出。

输出速率有三种模式:2M、10M、50M,这里我们选择2M。

电平控制:

STM32的IO口比较复杂,如果要输出1和0,则要通过控制:端口输出数据寄存器ODR来实现,ODR 是:Output data register的简写,在STM32里面,其寄存器的命名名称都是英文的简写,很容易记住。

从手册上我们知道ODR是一个32位的寄存器,低16位有效,高16位保留。低16位对应着IO0~IO16,只要往相应的位置写入0或者1就可以输出低或者高电平。

第一层级:基地址宏定义

“入手STM32单片机的知识点总结"

时钟控制:

“入手STM32单片机的知识点总结"

在STM32中,每个外设都有一个起始地址,叫做外设基地址,外设的寄存器就以这个基地址为标准按照顺序排列,且每个寄存器32位,(后面作为结构体里面的成员正好内存对齐)。

查表看到时钟由APB2外设时钟使能寄存器(RCC_APB2ENR)来控制,其中PB端口的时钟由该寄存器的位3写1使能。我们可以通过基地址+偏移量0x18,算出RCC_APB2ENR的地址为:0x40021018。那么使能PB口的时钟代码则如下所示:

 #define RCC_APB2ENR *(volatile unsigned long *)0x40021018

 // 开启端口B 时钟
 RCC_APB2ENR |= 1<<3;

模式配置:

“入手STM32单片机的知识点总结"

同RCC_APB2ENR一样,GPIOB的起始地址是:0X4001 0C00,我们也可以算出GPIO_CRL的地址为:0x40010C00。那么设置PB0为通用推挽输出,输出速率为2M的代码则如下所示:

“入手STM32单片机的知识点总结"

同上,从手册中我们看到ODR寄存器的地址偏移是:0CH,可以算出GPIOB_ODR寄存器的地址是:0X4001 0C00 + 0X0C = 0X4001 0C0C。现在我们就可以定义GPIOB_ODR这个寄存器了,代码如下:

#define GPIOB_ODR *(volatile unsigned long *)0x40010C0C

//PB0 输出低电平
GPIOB_ODR = 0<<0;

第一层级:基地址宏定义完成用STM32控制一个LED的完整代码:

#define RCC_APB2ENR *(volatile unsigned long *)0x40021018
#define GPIOB_CRL *(volatile unsigned long *)0x40010C00
#define GPIOB_ODR *(volatile unsigned long *)0x40010C0C

int main(void)
{
 // 开启端口B 的时钟
 RCC_APB2ENR |= 1<<3;

 // 配置PB0 为通用推挽输出模式,速率为2M
 GPIOB_CRL = (2<<0) | (0<<2);

 // PB0 输出低电平,点亮LED
 GPIOB_ODR = 0<<0;
}

void SystemInit(void)
{
}

第二层级:基地址宏定义+结构体封装

外设寄存器结构体封装

上面我们在操作寄存器的时候,操作的是寄存器的绝对地址,如果每个寄存器都这样操作,那将非常麻烦。我们考虑到外设寄存器的地址都是基于外设基地址的偏移地址,都是在外设基地址上逐个连续递增的,每个寄存器占32个或者16个字节,这种方式跟结构体里面的成员类似。

所以我们可以定义一种外设结构体,结构体的地址等于外设的基地址,结构体的成员等于寄存器,成员的排列顺序跟寄存器的顺序一样。这样我们操作寄存器的时候就不用每次都找到绝对地址,只要知道外设的基地址就可以操作外设的全部寄存器,即操作结构体的成员即可。

下面我们先定义一个GPIO寄存器结构体,结构体里面的成员是GPIO的寄存器,成员的顺序按照寄存器的偏移地址从低到高排列,成员类型跟寄存器类型一样。

typedef struct 
{
 volatile uint32_t CRL;
 volatile uint32_t CRH;
 volatile uint32_t IDR;
 volatile uint32_t ODR;
 volatile uint32_t BSRR;
 volatile uint32_t BRR;
 volatile uint32_t LCKR;
} GPIO_TypeDef;

在《STM32 中文参考手册》8.2 寄存器描述章节,我们可以找到结构体里面的7个寄存器描述。在点亮LED的时候我们只用了CRL和ODR这两个寄存器,至于其他寄存器的功能大家可以自行看手册了解。

在GPIO结构体里面我们用了两个数据类型,一个是uint32_t,表示无符号的32位整型,因为GPIO的寄存器都是32位的。这个类型声明在标准头文件stdint.h 里面使用typedef对unsigned int重命名,我们在程序上只要包含这个头文件即可。

另外一个是volatile作用就是告诉编译器这里的变量会变化不因优化而省略此指令,必须每次都直接读写其值,这样就能确保每次读或者写寄存器都真正执行到位。

STM32F1系列的GPIO端口分A~G,即GPIOA、GPIOB。GPIOG。每个端口都含有GPIO_TypeDef结构体里面的寄存器,我们可以根据手册各个端口的基地址把GPIO的各个端口定义成一个GPIO_TypeDef类型指针,然后我们就可以根据端口名(实际上现在是结构体指针了)来操作各个端口的寄存器,代码实现如下:

#define GPIOA ((GPIO_TypeDef *) 0X4001 0800)
#define GPIOB ((GPIO_TypeDef *) 0X4001 0C00)
#define GPIOC ((GPIO_TypeDef *) 0X4001 1000)
#define GPIOD ((GPIO_TypeDef *) 0X4001 1400)
#define GPIOE ((GPIO_TypeDef *) 0X4001 1800)
#define GPIOF ((GPIO_TypeDef *) 0X4001 1C00)
#define GPIOG ((GPIO_TypeDef *) 0X4001 2000)

外设内存映射

讲到基地址的时候我们再引人一个知识点:Cortex-M3存储器系统,这个知识点在《Cortex-M3权威指南》第5章里面讲到。CM3的地址空间是4GB,如下图所示:

“入手STM32单片机的知识点总结"

我们这里要讲的是片上外设,就是我们所说的寄存器的根据地,其大小总共有512MB,512MB是其极限空间,并不是每个单片机都用得完,实际上各个MCU厂商都只是用了一部分而已。STM32F1系列用到了:0x4000 0000 ~0x5003 FFFF。现在我们说的STM32的寄存器就是位于这个区域

APB1、APB2、AHB 总线基地址

现在我们说的STM32的寄存器就是位于这个区域,这里面ST设计了三条总线:AHB、APB2和APB1,其中AHB和APB2是高速总线,APB1是低速总线。不同的外设根据速度不同分别挂载到这三条总线上。

从下往上依次是:APB1、APB2、AHB,每个总线对应的地址分别是:APB1:0x40000000,APB2:0x4001 0000,AHB:0x4001 8000。

这三条总线的基地址我们是从《STM32 中文参考手册》2.3小节—存储器映像得到的:APB1的基地址是TIM2定时器的起始地址,APB2的基地址是AFIO的起始地址,AHB的基地址是SDIO的起始地址。其中APB1地址又叫做外设基地址,是所有外设的基地址,叫做PERIPH_BASE。

现在我们把这三条总线地址用宏定义出来,以后我们在定义其他外设基地址的时候,只需要在这三条总线的基址上加上偏移地址即可,代码如下:

#define PERIPH_BASE ((uint32_t)0x40000000)
#define APB1PERIPH_BASE PERIPH_BASE
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)

GPIO 端口基地址

因为GPIO挂载到APB2总线上,那么现在我们就可以根据APB2的基址算出各个GPIO端口的基地址,用宏定义实现代码如下:

#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
#define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)
#define GPIOD_BASE (APB2PERIPH_BASE + 0x1400)
#define GPIOE_BASE (APB2PERIPH_BASE + 0x1800)
#define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00)
#define GPIOG_BASE (APB2PERIPH_BASE + 0x2000)

第二层级:基地址宏定义+结构体封装完成用STM32控制一个LED的完整代码:

#include <stdint.h>
#define __IO volatile

typedef struct 
{
 __IO uint32_t CRL;
 __IO uint32_t CRH;
 __IO uint32_t IDR;
 __IO uint32_t ODR;
 __IO uint32_t BSRR;
 __IO uint32_t BRR;
 __IO uint32_t LCKR;
} GPIO_TypeDef;

typedef struct 
{
 __IO uint32_t CR;
 __IO uint32_t CFGR;
 __IO uint32_t CIR;
 __IO uint32_t APB2RSTR;
 __IO uint32_t APB1RSTR;
 __IO uint32_t AHBENR;
 __IO uint32_t APB2ENR;
 __IO uint32_t APB1ENR;
 __IO uint32_t BDCR;
 __IO uint32_t CSR;
} RCC_TypeDef;

#define PERIPH_BASE ((uint32_t)0x40000000)

#define APB1PERIPH_BASE PERIPH_BASE
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)

#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
#define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)
#define GPIOD_BASE (APB2PERIPH_BASE + 0x1400)
#define GPIOE_BASE (APB2PERIPH_BASE + 0x1800)
#define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00)
#define GPIOG_BASE (APB2PERIPH_BASE + 0x2000)
#define RCC_BASE (AHBPERIPH_BASE + 0x1000)

#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
#define RCC ((RCC_TypeDef *) RCC_BASE)


#define RCC_APB2ENR *(volatile unsigned long *)0x40021018
#define GPIOB_CRL *(volatile unsigned long *)0x40010C00
#define GPIOB_ODR *(volatile unsigned long *)0x40010C0C

int main(void)
{
 // 开启端口B 的时钟
 RCC->APB2ENR |= 1<<3;

 // 配置PB0 为通用推挽输出模式,速率为2M
 GPIOB->CRL = (2<<0) | (0<<2);

 // PB0 输出低电平,点亮LED
 GPIOB->ODR = 0<<0;
}

void SystemInit(void)
{
}

第二层级变化:

①、定义一个外设(GPIO)寄存器结构体,结构体的成员包含该外设的所有寄存器,成员的排列顺序跟寄存器偏移地址一样,成员的数据类型跟寄存器的一样。

②外设内存映射,即把地址跟外设建立起一一对应的关系。

③外设声明,即把外设的名字定义成一个外设寄存器结构体类型的指针。

④通过结构体操作寄存器,实现点亮LED。

第三层级:基地址宏定义+结构体封装+“位封装”(每一位的对应字节封装)

上面我们在控制GPIO输出内容的时候控制的是ODR(Output data register)寄存器,ODR是一个16位的寄存器,必须以字的形式控制其实我们还可以控制BSRR和BRR这两个寄存器来控制IO的电平,下面我们简单介绍下BRR寄存器的功能,BSRR自行看手册研究。

“入手STM32单片机的知识点总结"

位清除寄存器BRR只能实现位清0操作,是一个32位寄存器,低16位有效,写0没影响,写1清0。现在我们要使PB0输出低电平,点亮LED,则只要往BRR的BR0位写1即可,其他位为0,代码如下:

GPIOB->BRR = 0X0001;

这时PB0就输出了低电平,LED就被点亮了。

如果要PB2输出低电平,则是:

GPIOB->BRR = 0X0004;

如果要PB3/4/5/6。这些IO输出低电平呢?

道理是一样的,只要往BRR的相应位置赋不同的值即可。因为BRR是一个16位的寄存器,位数比较多,赋值的时候容易出错,而且从赋值的16进制数字我们很难清楚的知道控制的是哪个IO。

这时,我们是否可以把BRR的每个位置1都用宏定义来实现,如GPIO_Pin_0就表示0X0001,GPIO_Pin_2就表示0X0004。只要我们定义一次,以后都可以使用,而且还见名知意。“位封装”(每一位的对应字节封装) 代码如下:

#define GPIO_Pin_0 ((uint16_t)0x0001) /*!< Pin 0 selected */
#define GPIO_Pin_1 ((uint16_t)0x0002) /*!< Pin 1 selected */
#define GPIO_Pin_2 ((uint16_t)0x0004) /*!< Pin 2 selected */
#define GPIO_Pin_3 ((uint16_t)0x0008) /*!< Pin 3 selected */
#define GPIO_Pin_4 ((uint16_t)0x0010) /*!< Pin 4 selected */
#define GPIO_Pin_5 ((uint16_t)0x0020) /*!< Pin 5 selected */
#define GPIO_Pin_6 ((uint16_t)0x0040) /*!< Pin 6 selected */
#define GPIO_Pin_7 ((uint16_t)0x0080) /*!< Pin 7 selected */
#define GPIO_Pin_8 ((uint16_t)0x0100) /*!< Pin 8 selected */
#define GPIO_Pin_9 ((uint16_t)0x0200) /*!< Pin 9 selected */
#define GPIO_Pin_10 ((uint16_t)0x0400) /*!< Pin 10 selected */
#define GPIO_Pin_11 ((uint16_t)0x0800) /*!< Pin 11 selected */
#define GPIO_Pin_12 ((uint16_t)0x1000) /*!< Pin 12 selected */
#define GPIO_Pin_13 ((uint16_t)0x2000) /*!< Pin 13 selected */
#define GPIO_Pin_14 ((uint16_t)0x4000) /*!< Pin 14 selected */
#define GPIO_Pin_15 ((uint16_t)0x8000) /*!< Pin 15 selected */
#define GPIO_Pin_All ((uint16_t)0xFFFF) /*!< All pins selected */

这时PB0就输出了低电平的代码就变成了:

GPIOB->BRR = GPIO_Pin_0;

(如果同时让PB0/PB15输出低电平,用或运算,代码:

GPIOB->BRR = GPIO_Pin_0|GPIO_Pin_15;

为了不使main函数看起来冗余,上述库封装 的代码不应该放在main里面,因为其是跟GPIO相关的,我们可以把这些宏放在一个单独的头文件里面。

在工程目录下新建stm32f10x_gpio.h,把封装代码放里面,然后把这个文件添加到工程里面。这时我们只需要在main.c里面包含这个头文件即可。

第四层级:基地址宏定义+结构体封装+“位封装”+函数封装

我们点亮LED的时候,控制的是PB0这个IO,如果LED接到的是其他IO,我们就需要把GPIOB修改成其他的端口,其实这样修改起来也很快很方便。

但是为了提高程序的可读性和可移植性,我们是否可以编写一个专门的函数用来复位GPIO的某个位,这个函数有两个形参,一个是GPIOX(X=A...G),另外一个是GPIO_Pin(0...15),函数的主体则是根据形参GPIOX 和GPIO_Pin来控制BRR寄存器,代码如下:

void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
 GPIOx->BRR = GPIO_Pin;
}

这时,PB0输出低电平,点亮LED的代码就变成了:

GPIO_ResetBits(GPIOB,GPIO_Pin_0);

同理, 我们可以控制BSRR这个寄存器来实现关闭LED,代码如下:

// GPIO 端口置位函数
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
 GPIOx->BSRR = GPIO_Pin;
}

这时,PB0输出高电平,关闭LED的代码就变成了:

GPIO_SetBits(GPIOB,GPIO_Pin_0);

同样,因为这个函数是控制GPIO的函数,我们可以新建一个专门的文件来放跟gpio有关的函数。

在工程目录下新建stm32f10x_gpio.c,把GPIO相关的函数放里面。这时我们是否发现刚刚新建了一个头文件stm32f10x_gpio.h,这两个文件存放的都是跟外设GPIO相关的。

C文件里面的函数会用到h头文件里面的定义,这两个文件是相辅相成的,故我们在stm32f10x_gpio.c 文件中也包含stm32f10x_gpio.h这个头文件。别忘了把stm32f10x.h这个头文件也包含进去,因为有关寄存器的所有定义都在这个头文件里面。

如果我们写其他外设的函数,我们也应该跟GPIO一样,新建两个文件专门来存函数,比如RCC这个外设我们可以新建stm32f10x_rcc.c和stm32f10x_rcc.h。其他外依葫芦画瓢即可。

实例编写

以上,是对库封住过程的概述,下面我们正在地使用库函数编写LED程序

①管理库的头文件

当我们开始调用库函数写代码的时候,有些库我们不需要,在编译的时候可以不编译,可以通过一个总的头文件stm32f10x_conf.h来控制,该头文件主要代码如下:

//#include "stm32f10x_adc.h"
//#include "stm32f10x_bkp.h"
//#include "stm32f10x_can.h"
//#include "stm32f10x_cec.h"
//#include "stm32f10x_crc.h"
//#include "stm32f10x_dac.h"
//#include "stm32f10x_dbgmcu.h"
//#include "stm32f10x_dma.h"
//#include "stm32f10x_exti.h"
//#include "stm32f10x_flash.h"
//#include "stm32f10x_fsmc.h"
#include "stm32f10x_gpio.h"
//#include "stm32f10x_i2c.h"
//#include "stm32f10x_iwdg.h"
//#include "stm32f10x_pwr.h"
#include "stm32f10x_rcc.h"
//#include "stm32f10x_rtc.h"
//#include "stm32f10x_sdio.h"
//#include "stm32f10x_spi.h"
//#include "stm32f10x_tim.h"
//#include "stm32f10x_usart.h"
//#include "stm32f10x_wwdg.h"
//#include "misc.h"

这里面包含了全部外设的头文件,点亮一个LED我们只需要RCC和GPIO 这两个外设的库函数即可,其中RCC控制的是时钟,GPIO控制的具体的IO口。所以其他外设库函数的头文件我们注释掉,当我们需要的时候就把相应头文件的注释去掉即可。

stm32f10x_conf.h这个头文件在stm32f10x.h这个头文件的最后面被包含,在第8296行:

#ifdef USE_STDPERIPH_DRIVER
#include "stm32f10x_conf.h"
#endif

代码的意思是,如果定义了USE_STDPERIPH_DRIVER这个宏的话,就包含stm32f10x_conf.h这个头文件。

我们在新建工程的时候,在魔术棒选项卡C/C++中,我们定义了USE_STDPERIPH_DRIVER 这个宏,所以stm32f10x_conf.h 这个头文件就被stm32f10x.h包含了,我们在写程序的时候只需要调用一个头文件:stm32f10x.h即可。

②编写LED初始化函数

经过寄存器点亮LED的操作,我们知道操作一个GPIO输出的编程要点大概如下:

1、开启GPIO的端口时钟

2、选择要具体控制的IO口,即pin

3、选择IO口输出的速率,即speed

4、选择IO口输出的模式,即mode

5、输出高/低电平

STM32的时钟功能非常丰富,配置灵活,为了降低功耗,每个外设的时钟都可以独自的关闭和开启。STM32中跟时钟有关的功能都由RCC这个外设控制,RCC中有三个寄存器控制着所以外设时钟的开启和关闭:RCC_APHENR、RCC_APB2ENR和RCC_APB1ENR,AHB、APB2和APB1代表着三条总线,所有的外设都是挂载到这三条总线上,GPIO属于高速的外设,挂载到APB2总线上,所以其时钟有RCC_APB2ENR控制。

GPIO 时钟控制
固件库函数:RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE)函数的

原型为:

void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph,
                              FunctionalState NewState)
{
 /* Check the parameters */
 assert_param(IS_RCC_APB2_PERIPH(RCC_APB2Periph));
 assert_param(IS_FUNCTIONAL_STATE(NewState));
 if (NewState != DISABLE) 
 {
  RCC->APB2ENR |= RCC_APB2Periph;
 } 
 else 
 {
  RCC->APB2ENR &= ~RCC_APB2Periph;
 }
}

当程序编译一次之后,把光标定位到函数/变量/宏定义处,按键盘的F12或鼠标右键的Go to definition of,就可以找到原型。固件库的底层操作的就是RCC外设的APB2ENR这个寄存器,宏RCC_APB2Periph_GPIOB的原型是:0x00000008,即(1<<3),还原成存器操作就是:RCC->APB2ENR |= 1<<<3。相比固件库操作,寄存器操作的代码可读性就很差,只有才查阅寄存器配置才知道具体代码的功能,而固件库操作恰好相反,见名知意。

GPIO 端口配置

GPIO的pin,速度,模式,都由GPIO的端口配置寄存器来控制,其中IO0~IO7由端口配置低寄存器CRL控制,IO8~IO15由端口配置高寄存器CRH配置。固件库把端口配置的pin,速度和模式封装成一个结构体:

typedef struct 
{
 uint16_t GPIO_Pin;
 GPIOSpeed_TypeDef GPIO_Speed;
 GPIOMode_TypeDef GPIO_Mode;
} GPIO_InitTypeDef;

pin可以是GPIO_Pin_0~GPIO_Pin_15或者是GPIO_Pin_All,这些都是库预先定义好的宏。speed也被封装成一个结构体:

typedef enum 
{
 GPIO_Speed_10MHz = 1,
 GPIO_Speed_2MHz,
 GPIO_Speed_50MHz
} GPIOSpeed_TypeDef;

速度可以是10M,2M或者50M,这个由端口配置寄存器的MODE位控制,速度是针对IO口输出的时候而言,在输入的时候可以不用设置。mode也被封装成一个结构体:

typedef enum 
{
 GPIO_Mode_AIN = 0x0, // 模拟输入
 GPIO_Mode_IN_FLOATING = 0x04, // 浮空输入(复位后的状态)
 GPIO_Mode_IPD = 0x28, // 下拉输入
 GPIO_Mode_IPU = 0x48, // 上拉输入
 GPIO_Mode_Out_OD = 0x14, // 通用开漏输出
 GPIO_Mode_Out_PP = 0x10, // 通用推挽输出
 GPIO_Mode_AF_OD = 0x1C, // 复用开漏输出
 GPIO_Mode_AF_PP = 0x18 // 复用推挽输出
} GPIOMode_TypeDef;

IO口的模式有8种,输入输出各4种,由端口配置寄存器的CNF配置。平时用的最多的就是通用推挽输出,可以输出高低电平,驱动能力大,一般用于接数字器件。

最终用固件库实现就变成这样:

// 定义一个GPIO_InitTypeDef 类型的结构体
GPIO_InitTypeDef GPIO_InitStructure;

// 选择要控制的IO 口
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;

// 设置引脚为推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;

// 设置引脚速率为50MHz
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;

/*调用库函数,初始化GPIOB0*/
GPIO_Init(GPIOB, &GPIO_InitStructure);

倘若同一端口下不同引脚有不同的模式配置,每次对每个引脚配置完成后都要调用GPIO初始化函数,代码如下:

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15 ;                      
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;                  //上拉输入
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 ;                     
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;               //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); 

GPIO 输出控制

GPIO输出控制,可以通过端口数据输出寄存器ODR、端口位设置/清除寄存器BSRR和端口位清除寄存器BRR这三个来控制。端口输出寄存器ODR是一个32位的寄存器,低16位有效,对应着IO0~IO15,只能以字的形式操作,一般使用寄存器操作。

// PB0 输出高电平,点亮LED
GPIOB->ODR = 1<<0;

端口位清除寄存器BRR是一个32位的寄存器,低十六位有效,对应着IO0~IO15,只能以字的形式操作,可以单独对某一个位操作,写1清0。

// PB0 输出低电平,点亮LED
GPIO_ResetBits(GPIOB, GPIO_Pin_0);

BSRR是一个32位的寄存器,低16位用于置位,写1有效,高16位用于复位,写1有效,相当于BRR寄存器。高16位我们一般不用,而是操作BRR这个寄存器,所以BSRR这个寄存器一般用来置位操作。

// PB0 输出高电平,熄灭LED
GPIO_SetBits(GPIOB, GPIO_Pin_0);

综上:固件库LED GPIO初始化函数

void LED_GPIO_Config(void)
{
 // 定义一个GPIO_InitTypeDef 类型的结构体
 GPIO_InitTypeDef GPIO_InitStructure;

 // 开启GPIOB 的时钟
 RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE);

 // 选择要控制的IO 口
 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;

 // 设置引脚为推挽输出
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;

 // 设置引脚速率为50MHz
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

 /*调用库函数,初始化GPIOB0*/
 GPIO_Init(GPIOB, &GPIO_InitStructure);

 // 关闭LED
 GPIO_SetBits(GPIOB, GPIO_Pin_0);
}
主函数
#include "stm32f10x.h"


void SOFT_Delay(__IO uint32_t nCount);
void LED_GPIO_Config(void);

int main(void)
{
 // 程序来到main 函数之前,启动文件:statup_stm32f10x_hd.s 已经调用
 // SystemInit()函数把系统时钟初始化成72MHZ
 // SystemInit()在system_stm32f10x.c 中定义
 // 如果用户想修改系统时钟,可自行编写程序修改

 LED_GPIO_Config();

 while ( 1 ) 
 {
  // 点亮LED
  GPIO_ResetBits(GPIOB, GPIO_Pin_0);
  Time_Delay(0x0FFFFF);

  // 熄灭LED
  GPIO_SetBits(GPIOB, GPIO_Pin_0);
  Time_Delay(0x0FFFFF);
 }
}
// 简陋的软件延时函数
void Time_Delay(volatile uint32_t Count)
{
 for (; Count != 0; Count--);
}

注意void Time_Delay(volatile uint32_t Count)只是一个简陋的软件延时函数,如果小伙伴们有兴趣可以看一看MultiTimer,它是一个软件定时器扩展模块,可无限扩展所需的定时器任务,取代传统的标志位判断方式, 更优雅更便捷地管理程序的时间触发时序。

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

围观 71

单片机可以替代PLC 吗?

这个问题如同面粉能代替面条一样,答案是否定的。第一次听到这个答案可能很多人都有疑问:

单片机和PLC分别是什么?

它们之间有什么区别?

单片机

单片微型计算机(Single Chip Microcomputer ),亦称微控制单元(Microcontroller Unit),简称MCU,是一种集成电路芯片,是采用超大规模集成电路技术把具有数据处理能力的中央处理器(Central Process Unit;CPU)、随机存储器(Random Access Memory;RAM)、只读存储器(Read-Only Memory;ROM)、多种I/O口和中断系统、定时器/计数器等功能(可能还包括显示驱动电路、脉宽调制电路、模拟多路转换器、A/D转换器等电路)集成到一块硅片上构成的一个小而完善的微型计算机系统,在各个领域广泛应用。诸如手机、PC外围、遥控器,至汽车电子、工业上的步进马达、机器手臂的控制等,都可见到MCU的身影。

“入手STM32单片机的知识点总结"

单片机出现的历史并不长,但发展十分迅猛。它的产生与发展和微处理器的产生与发展大体同步,自1971年美国Intel公司首先推出4位微处理器以来,它的发展到目前为止大致可分为5个阶段。

单片机发展的初级阶段(1971年至1976年)

1971年11月Intel公司首先设计出集成度为2000只晶体管/片的4位微处理器Intel 4004, 并配有RAM、 ROM和移位寄存器, 构成了第一台MCS—4微处理器, 而后又推出了8位微处理器Intel 8008, 以及其它各公司相继推出的8位微处理器。

单片机发展的初级阶段(1971年至1976年)

1971年11月Intel公司首先设计出集成度为2000只晶体管/片的4位微处理器Intel 4004, 并配有RAM、 ROM和移位寄存器, 构成了第一台MCS—4微处理器, 而后又推出了8位微处理器Intel 8008, 以及其它各公司相继推出的8位微处理器。

低性能单片机阶段(1976年至1980年)

以1976年Intel公司推出的MCS—48系列为代表, 采用将8位CPU、 8位并行I/O接口、8位定时/计数器、RAM和ROM等集成于一块半导体芯片上的单片结构, 虽然其寻址范围有限(不大于4 KB), 也没有串行I/O, RAM、 ROM容量小, 中断系统也较简单, 但功能可满足一般工业控制和智能化仪器、仪表等的需要。

“入手STM32单片机的知识点总结"

高性能单片机阶段(1980年至1990年)

这一阶段推出的高性能8位单片机普遍带有串行口, 有多级中断处理系统, 多个16位定时器/计数器。片内RAM、 ROM的容量加大,且寻址范围可达64 KB,个别片内还带有A/D转换接口。

16位单片机阶段(1983年至1989年)

1983年Intel公司又推出了高性能的16位单片机MCS-96系列, 由于其采用了最新的制造工艺, 使芯片集成度高达12万只晶体管/片。

全方位高水平发展阶段(1990年至今)

到目前为止,单片机也有从传统的8位处理器平台向32位高级RISC处理器平台转变的趋势,但8位机依然难以被取代。8位单片机成本低,价格廉,便于开发,其性能可以满足大部分的需要,只有在航天、汽车、机器人等高技术领域,需要高速处理大量数据时,才需要选用16/32位,而在一般工业领域,8位通用型单片机,仍然是目前应用最广的单片机。单片机在集成度、功能、速度、可靠性、应用领域等全方位向更高水平发展。

单片机的特点是编程、维护相对复杂,编程方式常用C语言或者汇编语言,成本较低,I/O接口相对有限。

PLC

PLC,全称Programmable Logic Controller,即可编程逻辑控制器,是一种专门为在工业环境下应用而设计的数字运算操作电子系统。它采用一种可编程的存储器,在其内部存储执行逻辑运算、顺序控制、定时、计数和算术运算等操作的指令,通过数字式或模拟式的输入输出来控制各种类型的机械设备或生产过程。

“入手STM32单片机的知识点总结"

单片机为什么不能取代PLC呢?

1. 稳定性与可靠性

有人说这是个伪问题,单片机是元器件,PLC是由元器件以及庞大的软件构成的系统,两者在这一方面没有可比性。这话没有错,大多PLC的控制芯片实际上就是单片机,也就是说可以将PLC看成是单片机的二次开发,单论工业防护等级,单片机的稳定性和可靠性能根本比不了PLC这种IP67类的产品( IP为标记字母,第一标记数字表示接触保护和外来物保护等级,第二标记数字表示防水保护等级)。而且就PLC这种能应对工业恶劣环境的产品还开发出一套冗余系统。如果稳定性与可靠性对比没有意义,那么我们就从其他方面分析。

“入手STM32单片机的知识点总结"

2. I/O功能

单片机的I/O点实在有限,而反观PLC呢?针对不同的现场信号,均有相应的I/O点可与工业现场的器件(如按钮、开关、传感电流变送器、电机启动器或控制阀等)直接连接,并通过总线与CPU主板连接。工业里几乎任意一条生产线,都有上百甚至上千I/O点,就这点单片机完全无法比拟。

“入手STM32单片机的知识点总结"

3. 扩展功能

一条完整的工业生产线除了控制,还有通信、上位、组态、运动控制与显示等等,这些东西都需要依靠完整的工业体系与通信协议去做,例如西门子公司的PROFIBUS-DP通信、三菱重工的CC-LINK等等。而单片机和PC、单片机和单片机之间的通信大都用串口。单片机的串口是全双工异步通信串口,那么像MODBUS、PROFIBUS、CAN open、以太网等通信协议单片机是否能一一实现?或许单片机可以做到,但是这就涉及到下一个分析点,开发周期。

“入手STM32单片机的知识点总结"

4. 开发周期

PLC的品牌多达200多种,几乎每个品牌都有不同编程软件,而且都在不断完善自己的编程软件,使之能够越来越简单的服务于电气工程师,而各种程序块也是越来越方便人性化的任意去调用,比如PID模块、运动控制模块等,大大减轻了工程师的开发压力也缩短了开发周期。那单片机要如何实现?没有现成的模块使用,那就只能开发,那么做过非标自动化设备的工程师都会遇到一个问题——工期不足。PLC这种高度集成化模块化的产品在达到满足设备所需的开发周期,在工期面前也是抓襟见肘,更不用说如同白纸一张的单片机。

5. 通信距离

现在大多数流水线是要跨区域整合与监视的,所用的通讯方式多为以太网加中继器,或者直接走民用宽带光纤,所用的东西到最后很可能是用的就是微软的IE浏览器,很明显PLC是有RJ-45接口,即使本体没有RJ-45也可以配备以太网模块,可单片机搭载的PCB板能加上这个接口然后开发出以太网通信吗?开发需要多久?

“入手STM32单片机的知识点总结"

6. 编程语言

这点对单片机来讲是一个优势,同时也是一个劣势。上面提到PLC的品牌有两百多种,编程软件更多,尽管大多数PLC的编程语言都大同小异,但是每接触一款不同品牌的PLC,电气工程师就要从PLC的硬件参数、软元件、编程软件等等各个方面从头了解一次才能使用的得心应手。而单片机的编程语言用的是C语言或者汇编语言,这对于任何单片机都是通用的。换句话说,学会C语言或者汇编语言,便可以应用任何单片机开发想要的功能(前提是要有相关的电工电子学基础)。但话又说回来,电气工程师不是电子工程师,他们的工作不是单单考虑单片机如何驱动继电器来控制机床的,甚至有的电气工程师都不会C语言、汇编语言之类的MCU开发语言。近些年,IEC-61131-3标准的推广,越来越多的PLC支持多种编程语言,如类似C语言的ST语言,类似电路图的CFC语言。这种便利的功能是传统单片机开发环境真的无法实现。

结论

经过上面阐述,我们可以看出,PLC实际上可以看成是单片机的二次应用开发,但是它又有自己鲜明的特点。到目前为止,中国的单片机应用和嵌入式系统开发走过了二十余年的历程,国民经济建设、军事及家用电器等各个领域,尤其是手机、汽车自动导航设备、PDA、智能玩具、智能家电、医疗设备等行业都是应用了单片机。行业高端目前有超过10余万名从事单片机开发应用的工程师。

来源地址:

https://www.sohu.com/a/274772593_100279726

本文素材来源网络,版权归原作者所有。如不支持转载,请联系删除。

围观 229

STM32基本系统主要有下面几个部分:

电源

无论是否使用模拟部分和AD部分,MCU外围出去VCC和GND,VDDA、VSSA、Vref(如果封装有该引脚)都必需要连接,不可悬空。

对于每组对应的VDD和GND都应至少放置一个104的陶瓷电容用于滤波,并接该电容应放置尽量靠近MCU。

用万用表测试供电电压是否正确,调试时最好用数字电源供电,以便过压或过流烧坏板子,电压最好一步一步从进线端测试到芯片供电端。

复位、启动选择

Boot引脚与JTAG无关。其仅是用于MCU启动后,判断执行代码的起始地址在电路设计上可能Boot引脚不会使用,但要求一定要外部连接电阻到地或电源,切不可悬空;STM32三种启动模式对应的存储介质均是芯片内置的,它们是:

用户闪存 = 芯片内置的Flash

SRAM = 芯片内置的RAM区,就是内存

系统存储器 = 芯片内部一块特定的区域,芯片出厂时在这个区域预置了一段Bootloader,就是通常说的ISP程序,这个区域的内容在芯片出厂后没有人能够修改或擦除,即它是一个ROM区。

在每个STM32的芯片上都有两个管脚BOOT0和BOOT1,这两个管脚在芯片复位时的电平状态决定了芯片复位后从哪个区域开始执行程序,见下表:

“STM32基本系统主要分几个部分?"

BOOT1=x BOOT0=0 从用户闪存启动,这是正常的工作模式。

BOOT1=0 BOOT0=1 从系统存储器启动,这种模式启动的程序功能由厂家设置。

BOOT1=1 BOOT0=1 从内置SRAM启动,这种模式可以用于调试。

用JTAG口或SWD模式烧写 选择从用户闪存启动。

用串口ISP模式烧写程序时时选择从系统存储启动

烧写接口

如果要减小插座的数量,就用SWD模式的仿真,在这个模式下,如果用JLINK只要四根线就可以了,这四根线分别是:3.3V、GND、SWDIO、SWCLK。

其中STM32的JTMS/SWDIO接JTAG口的TMS,STM32的JTCK/SWCLK接JTAG口的TCK。如果要用ULINK2,则再加多一条“NRST”,即5条。这个接口你可自行定义,在使用时用杜邦线跳接或做块转换接口板联接仿真器与目标板即可。

在烧写时出现了IDCODE如图有序列号,证明烧写接口是好的!也就是硬件调试通了。如没有也许焊接不过关,从新加固焊接芯片。

调试烧录失败的常见原因

目标芯片没有正确连接,不能正常工作 —— 解决方法:确保目标板的最小系统正确连接,芯片能正常工作:VDD、VDDA及VSS 、VDDS已全部正确连接,复位电路能够可靠复位,各复位源不互相影响。

芯片内原先烧录的代码影响了新的调试操作,芯片内原先烧录的代码出错,芯片上电运行,进入未定义状态,不能进入调试模式。芯片内原先烧录的代码启动了某些外设,或者将SWJ引脚配置为普通I/O口 —— 解决方法:选择芯片的BOOT0/BOOT1引脚从RAM启动,或先擦除芯片内代码。

芯片已被读/写保护,调试工具不能读写芯片内置的Flash —— 解决方法:先使用调试工具解除芯片的读/写保护。

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

围观 60

ADC简介

STM32F103系列有3个ADC,精度为12位,每个ADC最多有16个外部通道。其中ADC1和ADC2都有16个外部通道,ADC3一般有8个外部通道,各通道的A/D转换可以单次、连续、扫描或间断执行,ADC转换的结果可以左对齐或右对齐储存在16位数据寄存器中。ADC的输入时钟不得超过14MHz,其时钟频率由PCLK2分频产生。

ADC功能框图讲解

学习STM32开发板上的外设时首先要了解其外设的功能框图,如下:

“详解STM32中的ADC"

功能框图可以大体分为7部分,下面一一讲解:

电压输入范围

ADC所能测量的电压范围就是VREF- ≤ VIN ≤ VREF+,把 VSSA 和 VREF-接地,把 VREF+和 VDDA 接 3V3,得到ADC 的输入电压范围为:0~3.3V。

输入通道

ADC的信号输入就是通过通道来实现的,信号通过通道输入到单片机中,单片机经过转换后,将模拟信号输出为数字信号。STM32中的ADC有着18个通道,其中外部的16个通道已经在框图中标出,如下:

“详解STM32中的ADC"

这16个通道对应着不同的IO口,此外ADC1/2/3 还有内部通道:ADC1 的通道 16 连接到了芯片内部的温度传感器, Vrefint 连接到了通道 17。ADC2 的模拟通道 16 和 17 连接到了内部的 VSS。

ADC的全部通道如下图所示:

“详解STM32中的ADC"

外部的16个通道在转换时又分为规则通道和注入通道,其中规则通道最多有16路,注入通道最多有4路(注入通道貌似使用不多),下面简单介绍一下两种通道:

规则通道顾名思义就是,最平常的通道、也是最常用的通道,平时的ADC转换都是用规则通道实现的。

注入通道是相对于规则通道的,注入通道可以在规则通道转换时,强行插入转换,相当于一个“中断通道”吧。当有注入通道需要转换时,规则通道的转换会停止,优先执行注入通道的转换,当注入通道的转换执行完毕后,再回到之前规则通道进行转换。

转换顺序

知道了ADC的转换通道后,如果ADC只使用一个通道来转换,那就很简单,但如果是使用多个通道进行转换就涉及到一个先后顺序了,毕竟规则转换通道只有一个数据寄存器。多个通道的使用顺序分为俩种情况:规则通道的转换顺序和注入通道的转换顺序。

规则通道中的转换顺序由三个寄存器控制:SQR1、SQR2、SQR3,它们都是32位寄存器。SQR寄存器控制着转换通道的数目和转换顺序,只要在对应的寄存器位SQx中写入相应的通道,这个通道就是第x个转换。具体的对应关系如下:

“详解STM32中的ADC"

通过SQR1寄存器就能了解其转换顺序在寄存器上的实现了:

“详解STM32中的ADC"

和规则通道转换顺序的控制一样,注入通道的转换也是通过注入寄存器来控制,只不过只有一个JSQR寄存器来控制,控制关系如下:

“详解STM32中的ADC"

需要注意的是,只有当JL=4的时候,注入通道的转换顺序才会按照JSQ1、JSQ2、JSQ3、JSQ4的顺序执行。当JL<4时,注入通道的转换顺序恰恰相反,也就是执行顺序为:JSQ4、JSQ3、JSQ2、JSQ1。

配置转换顺序的函数如下代码所示:

/**
  * @brief  Configures for the selected ADC regular channel its corresponding
  *         rank in the sequencer and its sample time.
  * @param  ADCx: where x can be 1, 2 or 3 to select the ADC peripheral.
  * @param  ADC_Channel: the ADC channel to configure. 
  *   This parameter can be one of the following values:
  *     @arg ADC_Channel_0: ADC Channel0 selected
  *     @arg ADC_Channel_1: ADC Channel1 selected
  *     @arg ADC_Channel_2: ADC Channel2 selected
  *     @arg ADC_Channel_3: ADC Channel3 selected
  *     @arg ADC_Channel_4: ADC Channel4 selected
  *     @arg ADC_Channel_5: ADC Channel5 selected
  *     @arg ADC_Channel_6: ADC Channel6 selected
  *     @arg ADC_Channel_7: ADC Channel7 selected
  *     @arg ADC_Channel_8: ADC Channel8 selected
  *     @arg ADC_Channel_9: ADC Channel9 selected
  *     @arg ADC_Channel_10: ADC Channel10 selected
  *     @arg ADC_Channel_11: ADC Channel11 selected
  *     @arg ADC_Channel_12: ADC Channel12 selected
  *     @arg ADC_Channel_13: ADC Channel13 selected
  *     @arg ADC_Channel_14: ADC Channel14 selected
  *     @arg ADC_Channel_15: ADC Channel15 selected
  *     @arg ADC_Channel_16: ADC Channel16 selected
  *     @arg ADC_Channel_17: ADC Channel17 selected
  * @param  Rank: The rank in the regular group sequencer. This parameter must be between 1 to 16.
  * @param  ADC_SampleTime: The sample time value to be set for the selected channel. 
  *   This parameter can be one of the following values:
  *     @arg ADC_SampleTime_1Cycles5: Sample time equal to 1.5 cycles
  *     @arg ADC_SampleTime_7Cycles5: Sample time equal to 7.5 cycles
  *     @arg ADC_SampleTime_13Cycles5: Sample time equal to 13.5 cycles
  *     @arg ADC_SampleTime_28Cycles5: Sample time equal to 28.5 cycles
  *     @arg ADC_SampleTime_41Cycles5: Sample time equal to 41.5 cycles
  *     @arg ADC_SampleTime_55Cycles5: Sample time equal to 55.5 cycles
  *     @arg ADC_SampleTime_71Cycles5: Sample time equal to 71.5 cycles
  *     @arg ADC_SampleTime_239Cycles5: Sample time equal to 239.5 cycles
  * @retval None
  */
void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime)
{
  函数内容略;
}

触发源

ADC转换的输入、通道、转换顺序都已经说明了,但ADC转换是怎么触发的呢?就像通信协议一样,都要规定一个起始信号才能传输信息,ADC也需要一个触发信号来实行模/数转换。

其一就是通过直接配置寄存器触发,通过配置控制寄存器CR2的ADON位,写1时开始转换,写0时停止转换。在程序运行过程中只要调用库函数,将CR2寄存器的ADON位置1就可以进行转换,比较好理解。

另外,还可以通过内部定时器或者外部IO触发转换,也就是说可以利用内部时钟让ADC进行周期性的转换,也可以利用外部IO使ADC在需要时转换,具体的触发由控制寄存器CR2决定。

在参考手册中可以找到,ADC_CR2寄存器的详情如下:

“详解STM32中的ADC"

“详解STM32中的ADC"

“详解STM32中的ADC"

转换时间

还有一点,就是转换时间的问题,ADC的每一次信号转换都要时间,这个时间就是转换时间,转换时间由输入时钟和采样周期来决定。

由于ADC在STM32中是挂载在APB2总线上的,所以ADC的时钟是由PCLK2(72MHz)经过分频得到的,分频因子由 RCC 时钟配置寄存器RCC_CFGR 的位 15:14 ADCPRE[1:0]设置,可以是 2/4/6/8 分频,一般配置分频因子为8,即8分频得到ADC的输入时钟频率为9MHz。

采样周期是确立在输入时钟上的,配置采样周期可以确定使用多少个ADC时钟周期来对电压进行采样,采样的周期数可通过 ADC采样时间寄存器 ADC_SMPR1 和 ADC_SMPR2 中的 SMP[2:0]位设置,ADC_SMPR2 控制的是通道 0~9, ADC_SMPR1 控制的是通道 10~17。每个通道可以配置不同的采样周期,但最小的采样周期是1.5个周期,也就是说如果想最快时间采样就设置采样周期为1.5.

转换时间=采样时间+12.5个周期

12.5个周期是固定的,一般我们设置 PCLK2=72M,经过 ADC 预分频器能分频到最大的时钟只能是 12M,采样周期设置为 1.5 个周期,算出最短的转换时间为 1.17us。

数据寄存器

转换完成后的数据就存放在数据寄存器中,但数据的存放也分为规则通道转换数据和注入通道转换数据的。

规则数据寄存器负责存放规则通道转换的数据,通过32位寄存器ADC_DR来存放:

“详解STM32中的ADC"

当使用ADC独立模式(也就是只使用一个ADC,可以使用多个通道)时,数据存放在低16位中,当使用ADC多模式时高16位存放ADC2的数据。需要注意的是ADC转换的精度是12位,而寄存器中有16个位来存放数据,所以要规定数据存放是左对齐还是右对齐。

当使用多个通道转换数据时,会产生多个转换数据,然鹅数据寄存器只有一个,多个数据存放在一个寄存器中会覆盖数据导致ADC转换错误,所以我们经常在一个通道转换完成之后就立刻将数据取出来,方便下一个数据存放。一般开启DMA模式将转换的数据,传输在一个数组中,程序对数组读操作就可以得到转换的结果。

注入通道转换的数据寄存器有4个,由于注入通道最多有4个,所以注入通道转换的数据都有固定的存放位置,不会跟规则寄存器那样产生数据覆盖的问题。ADC_JDRx 是 32 位的,低 16 位有效,高 16 位保留,数据同样分为左对齐和右对齐,具体是以哪一种方式存放,由ADC_CR2 的 11 位 ALIGN 设置。

“详解STM32中的ADC"

中断

“详解STM32中的ADC"

从框图中可以知道数据转换完成之后可以产生中断,有三种情况:

规则通道数据转换完成之后,可以产生一个中断,可以在中断函数中读取规则数据寄存器的值。这也是单通道时读取数据的一种方法。

注入通道数据转换完成之后,可以产生一个中断,并且也可以在中断中读取注入数据寄存器的值,达到读取数据的作用。

当输入的模拟量(电压)不再阈值范围内就会产生看门狗事件,就是用来监视输入的模拟量是否正常。

以上中断的配置都由ADC_SR寄存器决定:

“详解STM32中的ADC"

当然,在转换完成之后也可以产生DMA请求,从而将转换好的数据从数据寄存器中读取到内存中。

电压转换

要知道,转换后的数据是一个12位的二进制数,我们需要把这个二进制数代表的模拟量(电压)用数字表示出来。比如测量的电压范围是0~3.3V,转换后的二进制数是x,因为12位ADC在转换时将电压的范围大小(也就是3.3)分为4096(2^12)份,所以转换后的二进制数x代表的真实电压的计算方法就是:

y=3.3* x / 4096

初始化结构体

每个外设的核心就是其对应的初始化结构体了,ADC的初始化结构体代码如下:

typedef struct
 {
 uint32_t ADC_Mode; // ADC 工作模式选择
 FunctionalState ADC_ScanConvMode; // ADC 扫描(多通道)或者单次(单通道)模式选择 
 FunctionalState ADC_ContinuousConvMode; // ADC 单次转换或者连续转换选择
 uint32_t ADC_ExternalTrigConv; // ADC 转换触发信号选择
 uint32_t ADC_DataAlign; // ADC 数据寄存器对齐格式
 uint8_t ADC_NbrOfChannel; // ADC 采集通道数
 } ADC_InitTypeDef;

通过配置初始化结构体来设置ADC的相关信息。

单通道电压采集

用这个程序来简单熟练一下ADC的单通道电压采集吧,程序使用了ADC1的通道11,对应的IO口是PC^1,因为博主的开发板上PC ^1引脚没有任何复用,使用中断,在中断中读取转换的电压。

头文件

为了提高文件的可移植性,头文件中定义了一些与ADC和中断相关的量,在移植程序的时候只需要修改头文件中的定义即可。

#ifndef __ADC_H
#define __ADC_H
#include "stm32f10x.h"
/* 采用ADC1的通道11  引脚为PC^1 模式必须是模拟输入*/
#define ADC_GPIO_RCC     RCC_APB2Periph_GPIOC
#define ADC_GPIO_PORT    GPIOC
#define ADC_GPIO_PIN     GPIO_Pin_1
#define ADC_GPIO_MODE    GPIO_Mode_AIN  
/* 配置与中断有关的信息 */
#define ADC_IRQn         ADC1_2_IRQn
#define ADC_RCC          RCC_APB2Periph_ADC1
/* 配置ADC初始化结构体的宏定义 */
#define ADCx                          ADC1
#define ADCx_ContinuousConvMode       ENABLE                      //连续转换模式
#define ADCx_DataAlign                ADC_DataAlign_Right         //转换结果右对齐
#define ADCx_ExternalTrigConv         ADC_ExternalTrigConv_None      //不使用外部触发转换,采用软件触发
#define ADCx_Mode                     ADC_Mode_Independent        //只使用一个ADC,独立模式
#define ADCx_NbrOfChannel             1                          //一个转换通道
#define ADCx_ScanConvMode             DISABLE                     //禁止扫描模式,多通道时使用
/* 通道信息和采样周期 */
#define ADC_Channel                   ADC_Channel_11
#define ADC_SampleTime                ADC_SampleTime_55Cycles5
/* 函数声明 */
void ADC_COnfig(void);
void ADC_NVIC_Config(void);
void ADC_GPIO_Config(void);
void ADCx_Init(void);
#endif  /* __ADC_H */

引脚配置函数

首先配置相应的GPIO引脚,毕竟模拟信号是通过GPIO引脚传输到开发板的,注意的是,引脚的模式一定要是模拟输入!

void ADC_GPIO_Config(void)
{
GPIO_InitTypeDef   GPIO_InitStruct;
RCC_APB2PeriphClockCmd(ADC_GPIO_RCC,  ENABLE);
GPIO_InitStruct.GPIO_Pin = ADC_GPIO_PIN ;
GPIO_InitStruct.GPIO_Mode = ADC_GPIO_MODE ;
GPIO_Init(ADC_GPIO_PORT , &GPIO_InitStruct);
}

配置引脚就是老套路:声明结构体变量、开启时钟、写入结构体、初始化GPIO。

NVIC配置函数

因为我们是在转换完成后利用中断,在中断函数中读取数据,所以要首先配置中断函数的优先级,因为程序中只有这一个中断,所以优先级的配置就比较随意。

void ADC_NVIC_Config(void)
{
NVIC_InitTypeDef NVIC_InitStruct ;
/* 配置中断优先级分组(设置抢占优先级和子优先级的分配),在函数在misc.c */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1) ;
/* 配置初始化结构体 在misc.h中 */
/* 配置中断源 在stm32f10x.h中 */
NVIC_InitStruct.NVIC_IRQChannel = ADC_IRQn ;
/* 配置抢占优先级 */
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1 ;
/* 配置子优先级 */
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1 ;
/* 使能中断通道 */
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE ;
/* 调用初始化函数 */
NVIC_Init(&NVIC_InitStruct) ;
}

ADC配置函数

ADC的配置函数是ADC的精髓,在这个函数中包含的内容有:ADC的初始化结构体配置、配置了时钟分频、配置了通道转换顺序、打开转换中断、进行校准、软件触发ADC采集等。

函数中都有详细的注释:

void ADC_COnfig(void)
{
  ADC_InitTypeDef  ADC_InitStruct;
  RCC_APB2PeriphClockCmd(ADC_RCC,  ENABLE);
  /* 配置初始化结构体,详情见头文件 */
  ADC_InitStruct.ADC_ContinuousConvMode = ADCx_ContinuousConvMode  ;
  ADC_InitStruct.ADC_DataAlign = ADCx_DataAlign ;
  ADC_InitStruct.ADC_ExternalTrigConv = ADCx_ExternalTrigConv ;
  ADC_InitStruct.ADC_Mode = ADCx_Mode ;
  ADC_InitStruct.ADC_NbrOfChannel = ADCx_NbrOfChannel ;
  ADC_InitStruct.ADC_ScanConvMode = ADCx_ScanConvMode ;
  ADC_Init(ADCx, &ADC_InitStruct);
  /* 配置ADC时钟为8分频,即9M */
  RCC_ADCCLKConfig(RCC_PCLK2_Div8);
  /* 配置ADC通道转换顺序和时间 */
  ADC_RegularChannelConfig(ADCx, ADC_Channel, 1, ADC_SampleTime );
  /* 配置为转换结束后产生中断 在中断中读取信息 */
  ADC_ITConfig(ADCx, ADC_IT_EOC,ENABLE);
  /* 开启ADC,进行转换 */
  ADC_Cmd(ADCx, ENABLE );
  /* 重置ADC校准 */
  ADC_ResetCalibration(ADCx);
  /* 等待初始化完成 */
  while(ADC_GetResetCalibrationStatus( ADCx))
    /* 开始校准 */
    ADC_StartCalibration(ADCx);
  /* 等待校准完成 */
  while (ADC_GetCalibrationStatus(ADCx));
  /* 软件触发ADC转换 */
  ADC_SoftwareStartConvCmd(ADCx, ENABLE);
}

中断函数

在中断函数中进行读取数据,将数据存放在变量result中,此处使用关键字extern声明,代表变量result已经在其他文件中定义,关于extern的介绍在之前发的文章中有extern关键字的介绍。

extern uint16_t resurt;
void ADC1_2_IRQHandler(void)
{
    /* 判断产生中断请求 */
  while(ADC_GetITStatus(ADCx, ADC_IT_EOC) == SET)
    resurt=ADC_GetConversionValue(ADCx);
  /* 清除中断标志 */
  ADC_ClearITPendingBit(ADCx, ADC_IT_EOC);
}

主函数

主函数负责接收转换的值,并将其转换为电压值,然后通过串口打印在计算机上,便于调试。

变量result是主函数中的全局变量,注意最后的结果应该转换为浮点型。

#include "stm32f10x.h"
#include "usart.h"
#include "adc.h"
uint16_t result;
void delay(void)
{
  uint16_t k=0xffff;
  while(k--);
}
int main(void)
{
  float voltage;
  /* 串口调试函数 */
  DEBUG_USART_Config();
  /* 与ADC相关的函数打包在此函数中 */
  ADCx_Init();
  while(1)
  {
      /* 强制转换为浮点型 */
    voltage = (float) result/4096*3.3;
    printf("\n电压值为:%f\n",voltage);
    delay();
  }
}

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

围观 420

ST 为开发者提供了非常方便的开发库:有标准外设库(SPL库)、HAL 库(Hardware Abstraction Layer,硬件抽象层库)、LL 库(Low-Layer,底层库)三种。前者是ST的老库已经停更了,后两者是ST现在主推的开发库。

相比标准外设库,STM32Cube HAL库表现出更高的抽象整合水平,HAL API集中关注各外设的公共函数功能,这样便于定义一套通用的用户友好的API函数接口,从而可以轻松实现从一个STM32产品移植到另一个不同的STM32系列产品。HAL库是ST未来主推的库,ST新出的芯片已经没有STD库了,比如F7系列。目前,HAL库已经支持STM32全线产品。

通过文字描述可以知道HAL库的几个点:

1、最大可移植性。
2、提供了一整套一致的中间件组件,如RTOS,USB,TCP / IP和图形等。
3、通用的用户友好的API函数接口。
4、ST新出的芯片已经没有标准库。
5、HAL库已经支持STM32全线产品。

网友认为,“HAL我觉得是极好的,就是SPI接收时速度实在太慢,不用DMA的话,吃不消。“

通常新手在入门STM32的时候,首先都要先选择一种要用的开发方式,不同的开发方式会导致你编程的架构是完全不一样的。一般大多数都会选用标准库和HAL库,而极少部分人会通过直接配置寄存器进行开发。网上关于标准库、HAL库的描述相信是数不胜数。可是一个对于很多刚入门的朋友还是没法很直观的去真正了解这些不同开发方式彼此之间的区别,所以笔者想以一种非常直白的方式,用自己的理解去将这些东西表述出来,如果有描述的不对的地方或者是不同意见的也可以大家提出。

01、直接配置寄存器

不少先学了51的朋友可能会知道,会有一小部分人或是教程是通过汇编语言直接操作寄存器实现功能的,这种方法到了STM32就变得不太容易行得通了,因为STM32的寄存器数量是51单片机的十数倍,如此多的寄存器根本无法全部记忆,开发时需要经常的翻查芯片的数据手册,此时直接操作寄存器就变得非常的费力了。但还是会有很小一部分人,喜欢去直接操作寄存器,因为这样更接近原理,知其然也知其所以然。

02、标准库

上面也提到了,STM32有非常多的寄存器,而导致了开发困难,所以为此ST公司就为每款芯片都编写了一份库文件,也就是工程文件里stm32F1xx…之类的。在这些 .c .h文件中,包括一些常用量的宏定义,把一些外设也通过结构体变量封装起来,如GPIO口时钟等。所以我们只需要配置结构体变量成员就可以修改外设的配置寄存器,从而选择不同的功能。也是目前最多人使用的方式,也是学习STM32接触最多的一种开发方式,我也就不多阐述了。

03、HAL库

HAL库是ST公司目前主力推的开发方式,全称就是Hardware Abstraction Layer(抽象印象层)。库如其名,很抽象,一眼看上去不太容易知道他的作用是什么。它的出现比标准库要晚,但其实和标准库一样,都是为了节省程序开发的时期,而且HAL库尤其的有效,如果说标准库把实现功能需要配置的寄存器集成了,那么HAL库的一些函数甚至可以做到某些特定功能的集成。也就是说,同样的功能,标准库可能要用几句话,HAL库只需用一句话就够了。并且HAL库也很好的解决了程序移植的问题,不同型号的stm32芯片它的标准库是不一样的,例如在F4上开发的程序移植到F3上是不能通用的,而使用HAL库,只要使用的是相通的外设,程序基本可以完全复制粘贴,注意是相通外设,意思也就是不能无中生有,例如F7比F3要多几个定时器,不能明明没有这个定时器却非要配置,但其实这种情况不多,绝大多数都可以直接复制粘贴。是而且使用ST公司研发的STMcube软件,可以通过图形化的配置功能,直接生成整个使用HAL库的工程文件,可以说是方便至极,但是方便的同时也造成了它执行效率的低下,在各种论坛帖子真的是被吐槽的数不胜数。

HAL库和标准固件库区别

STM32的开发中,我们可以操作寄存器:

GPIOF->BSRR=0x00000001;//这里是针对STM32F1系列

这种方法当然可以,但是这种方法的劣势是你需要去掌握每个寄存器的用法,你才能正确使用STM32,而对于STM32这种级别的MCU,数百个寄存器记起来又是谈何容易。于是ST(意法半导体)推出了官方标准固件库,标准固件库将这些寄存器底层操作都封装起来,提供一整套接口(API)供开发者调用,大多数场合下,你不需要去知道操作的是哪个寄存器,你只需要知道调用哪些函数即可。

比如上面的控制 BRR 寄存器实现电平控制,官方库封装了一个函数:

void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
GPIOx->BRR= GPIO_Pin;
}

这个时候你不需要再直接去操作 BRR 寄存器了,你只需要知道怎么使用 GPIO_ResetBits()这个函数就可以了。在你对外设的工作原理有一定的了解之后,你再去看标准库函数,基本上函数名字能告诉你这个函数的功能是什么,该怎么使用,这样开发就方便很多。

标准固件库自推出以来受到广大工程师推崇,现在很多工程师和公司还在使用标准库函数开发。不过,ST官方已经不再更新STM32标准固件库,而是力推新的固件库:HAL库。

比如上面的控制BSRRL 寄存器实现电平控制,官方 HAL 库封装了一个函数:

voidHAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin,
GPIO_PinStatePinState)
{
assert_param(IS_GPIO_PIN(GPIO_Pin));
assert_param(IS_GPIO_PIN_ACTION(PinState));
if(PinState!= GPIO_PIN_RESET)
{
GPIOx->BSRR= GPIO_Pin;
}
else
{
GPIOx->BSRR= (uint32_t)GPIO_Pin << 16;
}
}

这个时候你不需要再直接去操作BSRRL 寄存器了,你只需要知道怎么使用HAL_GPIO_WritePin这个函数就可以了。

标准固件库和HAL库一样都是固件库函数,由ST官方硬件抽象层而设计的软件函数包,由程序、数据结构和宏组成,包括了STM32所有外设的性能特征。 这些固件库为开发者底层硬件提供了中间API,通过使用固件库,无需掌握底层细节,开发者就可以轻松应用每一个外设。

HAL 库和标准库本质上是一样的,都是提供底层硬件操作 API,而且在使用上也是大同小异。有过标准库基础的同学对 HAL 库的使用也很容易入手。ST 官方之所以这几年大力推广 HAL 库,是因为 HAL 的结构更加容易整合 STM32Cube,而 STM32CubeMX 是 ST 这几年极力推荐的程序生成开发工具。所以这几年新出的 STM32 芯片,ST 直接只提供 HAL 库。

在ST的官方声明中,HAL库是大势所趋。ST最新开发的芯片中,只有HAL库没有标准库。标准库和HAL库虽然都是对外设进行操作的函数,但由于标准库官方已经停止更新,而且标准库在STM32创建工程和初始化时,不能由CubMX软件代码生成使用,也就是说CubMX软件在生产代码时,工程项目和初始化代码就自动生成,这个工程项目和初始化代码里面使用的库都是基于HAL库的。STM32CubeMX是一个图形化的工具,也是配置和初始化C代码生成器 ,与STM32CubeMX配合使用的是HAL库(硬件抽象层软件库)。

基本配置

工程创建

通过内核芯片的选择,创建相应的工程文件。

“STM32

“STM32

对时钟系统进行配置,对引脚及基本功能进行配置。

配置时钟系统我们首要思考的是:我们需要怎样的时钟系统,而不是如何配置时钟系统。

“STM32

配置SWD程序烧录接口,使用ST-Link进行烧录下载。

“STM32

工程管理设置推荐图中所示配置,实现更快编译和更简洁的文件系统。

点击图中“GENERATE CODE”生成Keil工程文件

“STM32

“STM32

如果你已经安装了编译环境MDK了,可点击直接打开工程。

GPIO使用

开发环境搭建好以后,可以依开始STM32的开发,下面是使用GPIO的例程,让LED灯每隔400ms闪烁一次

打开STM32CubeMX新建工程,选择STM32F103ZET6芯片。选择外部高速晶振(HSE)。

“STM32

“STM32

根据下位机主控引脚分配图及原理图,选择LED引脚

“STM32

“STM32

PD7为LED1输出控制管脚,选择GPIO_OUTPUT模式。

“STM32

点击Clock Configuration配置系统时钟为72M最高速度。

“STM32

点击Configuration->GPIO配置管脚。LED管脚配置为低速推挽输出模式,既不上拉也不下拉(即默认的模式不用配置)。

“STM32

点击图片生成报告,软件会提示新建工程,输入工程名,选择工程保存路径。IDE选择MDK-ARM V5。

“STM32

在Code Generator中找到Generated files框,勾选Generated periphera initialization as a pair of '.c/.h'files per IP。外设初始化为独立的C文件和头文件。

“STM32

点击图片生成代码。点击Open Project打开工程。到这里我们就配置好工程外设初始化。

“STM32

点击Build按钮,然后等一会,Build Optput信息框会输出没有错误没有警告。
在main函数里添加如下代码

“STM32

延时电平翻转函数,这样LED灯就能开始闪烁。

再点击Build按钮,然后等一会,Build Optput信息框会输出没有错误没有警告。
代码烧写。

现在开始烧写程序,烧写程序有两种,一种是使用ST-LINK工具烧写,一种是直接用与上位机通讯的串口1烧写。烧写工具使用mcuisp。

“STM32,软件可自行网上搜索,下载配置如下。

“STM32

选择好串口端口后,可以开始下载。

HAL库固件库安装与用户手册

1.首先设置让Cube可以自动联网下载相关固件库选择updater Settings

“STM32

设置如下

“STM32

2.根据芯片选择所需固件

版本是向下兼容的,可以直接选择最新版。但如果觉得最新版太大,可以阅读下面的Main Changes.能够支持你目前的芯片就好。

“STM32

选好了,点击Install Now就行,过程可能有点长。建议直接官网下载到本地,再安装

文件会被下载到如下位置,建议更改此目录,不要选在C盘!!!

“STM32

查找帮助手册

3.寻找用户帮助手册进入固件所在文件夹,里面包含很多内容。

“STM32

比如说官方提供的开发板程序

“STM32

每个型号下面都有对应功能的实现

“STM32

用户手册就在Drivers文件夹下面。

“STM32

STM32 HAL库与标准库的区别——浅谈句柄、MSP函数、Callback函数

01、句柄

句柄(handle),有多种意义,其中第一种是指程序设计,第二种是指Windows编程。现在大部分都是指程序设计/程序开发这类。

· 第一种解释:句柄是一种特殊的智能指针。当一个应用程序要引用其他系统(如数据库、操作系统)所管理的内存块或对象时,就要使用句柄。

· 第二种解释:整个Windows编程的基础。一个句柄是指使用的一个唯一的整数值,即一个4字节(64位程序中为8字节)长的数值,来标识应用程序中的不同对象和同类中的不同的实例,诸如,一个窗口,按钮,图标,滚动条,输出设备,控件或者文件等。应用程序能够通过句柄访问相应的对象的信息,但是句柄不是指针,程序不能利用句柄来直接阅读文件中的信息。如果句柄不在I/O文件中,它是毫无用处的。句柄是Windows用来标志应用程序中建立的或是使用的唯一整数,Windows大量使用了句柄来标识对象。

STM32的标准库中,句柄是一种特殊的指针,通常指向结构体!

在STM32的标准库中,假设我们要初始化一个外设(这里以USART为例),我们首先要初始化他们的各个寄存器。在标准库中,这些操作都是利用固件库结构体变量+固件库Init函数实现的:

USART_InitTypeDef USART_InitStructure;

USART_InitStructure.USART_BaudRate = bound;//串口波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;//收发模式

USART_Init(USART3, &USART_InitStructure); //初始化串口1

可以看到,要初始化一个串口,需要:

· 1、对六个位置进行赋值,

· 2、然后引用Init函数,

USART_InitStructure并不是一个全局结构体变量,而是只在函数内部的局部变量,初始化完成之后,USART_InitStructure就失去了作用。

而在HAL库中,同样是USART初始化结构体变量,我们要定义为全局变量。

UART_HandleTypeDef UART1_Handler;

右键查看结构体成员

typedef struct
{
  USART_TypeDef                 *Instance;        /*!< UART registers base address        */
  UART_InitTypeDef              Init;             /*!< UART communication parameters      */
  uint8_t                       *pTxBuffPtr;      /*!< Pointer to UART Tx transfer Buffer */
  uint16_t                      TxXferSize;       /*!< UART Tx Transfer size              */
  uint16_t                      TxXferCount;      /*!< UART Tx Transfer Counter           */
  uint8_t                       *pRxBuffPtr;      /*!< Pointer to UART Rx transfer Buffer */
  uint16_t                      RxXferSize;       /*!< UART Rx Transfer size              */
  uint16_t                      RxXferCount;      /*!< UART Rx Transfer Counter           */  
  DMA_HandleTypeDef             *hdmatx;          /*!< UART Tx DMA Handle parameters      */
  DMA_HandleTypeDef             *hdmarx;          /*!< UART Rx DMA Handle parameters      */
  HAL_LockTypeDef               Lock;             /*!< Locking object                     */
  __IO HAL_UART_StateTypeDef    State;            /*!< UART communication state           */
  __IO uint32_t                 ErrorCode;        /*!< UART Error code                    */
}UART_HandleTypeDef;

我们发现,与标准库不同的是,该成员不仅:

· 1、包含了之前标准库就有的六个成员(波特率,数据格式等),
· 2、还包含过采样、(发送或接收的)数据缓存、数据指针、串口 DMA 相关的变量、各种标志位等等要在整个项目流程中都要设置的各个成员。该UART1_Handler就被称为串口的句柄,它被贯穿整个USART收发的流程,比如开启中断:

HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, RXBUFFERSIZE);

比如后面要讲到的MSP与Callback回调函数:

void HAL_UART_MspInit(UART_HandleTypeDef *huart);
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);

在这些函数中,只需要调用初始化时定义的句柄UART1_Handler就好。

02、MSP函数

MSP: MCU Specific Package 单片机的具体方案

MSP是指和MCU相关的初始化,引用一下正点原子的解释,个人觉得说的很明白:我们要初始化一个串口,首先要设置和 MCU 无关的东西,例如波特率,奇偶校验,停止位等,这些参数设置和 MCU 没有任何关系,可以使用 STM32F1,也可以是 STM32F2/F3/F4/F7上的串口。而一个串口设备它需要一个 MCU 来承载,例如用 STM32F4 来做承载,PA9 做为发送,PA10 做为接收,MSP 就是要初始化 STM32F4 的 PA9,PA10,配置这两个引脚。所以 HAL驱动方式的初始化流程就是:

HAL_USART_Init()—>HAL_USART_MspInit() ,先初始化与 MCU无关的串口协议,再初始化与 MCU 相关的串口引脚。

在 STM32 的 HAL 驱动中HAL_PPP_MspInit()作为回调,HAL_PPP_Init()函数所调用。当我们需要移植程序到 STM32F1平台的时候,我们只需要修改 HAL_PPP_MspInit 函数内容而不需要修改 HAL_PPP_Init 入口参数内容。

在HAL库中,几乎每初始化一个外设就需要设置该外设与单片机之间的联系,比如IO口,是否复用等等,可见,HAL库相对于标准库多了MSP函数之后,移植性非常强,但与此同时却增加了代码量和代码的嵌套层级。可以说各有利弊。

同样,MSP函数又可以配合句柄,达到非常强的移植性:

void HAL_UART_MspInit(UART_HandleTypeDef *huart);

入口参数仅仅需要一个串口句柄,这样就能看出句柄的方便。

03、Callback函数

类似于MSP函数,个人认为Callback函数主要帮助用户应用层的代码编写。

还是以USART为例,在标准库中,串口中断了以后,我们要先在中断中判断是否是接收中断,然后读出数据,顺便清除中断标志位,然后再是对数据的处理,这样如果我们在一个中断函数中写这么多代码,就会显得很混乱:

void USART3_IRQHandler(void)                //串口1中断服务程序
{
u8 Res;
if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)  //接收中断(接收到的数据必须是0x0d 0x0a结尾)
{
Res =USART_ReceiveData(USART3);//读取接收到的数据
/*数据处理区*/
}    
     }
}

而在HAL库中,进入串口中断后,直接由HAL库中断函数进行托管:

void USART1_IRQHandler(void)                
{
HAL_UART_IRQHandler(&UART1_Handler);//调用HAL库中断处理公用函数
/***************省略无关代码****************/
}

HAL_UART_IRQHandler这个函数完成了判断是哪个中断(接收?发送?或者其他?),然后读出数据,保存至缓存区,顺便清除中断标志位等等操作。比如我提前设置了,串口每接收五个字节,我就要对这五个字节进行处理。在一开始我定义了一个串口接收缓存区:

/*HAL库使用的串口接收缓冲,处理逻辑由HAL库控制,接收完这个数组就会调用HAL_UART_RxCpltCallback进行处理这个数组*/
/*RXBUFFERSIZE=5*/
u8 aRxBuffer[RXBUFFERSIZE];

在初始化中,我在句柄里设置好了缓存区的地址,缓存大小(五个字节)

/*该代码在HAL_UART_Receive_IT函数中,初始化时会引用*/
huart->pRxBuffPtr = pData;//aRxBuffer
    huart->RxXferSize = Size;//RXBUFFERSIZE
    huart->RxXferCount = Size;//RXBUFFERSIZE

则在接收数据中,每接收完五个字节,HAL_UART_IRQHandler才会执行一次Callback函数:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);

在这个Callback回调函数中,我们只需要对这接收到的五个字节(保存在aRxBuffer[]中)进行处理就好了,完全不用再去手动清除标志位等操作。所以说Callback函数是一个应用层代码的函数,我们在一开始只设置句柄里面的各个参数,然后就等着HAL库把自己安排好的代码送到手中就可以了~

综上,就是HAL库的三个与标准库不同的地方之个人见解。个人觉得从这三个小点就可以看出HAL库的可移植性之强大,并且用户可以完全不去理会底层各个寄存器的操作,代码也更有逻辑性。但与此带来的是复杂的代码量,极慢的编译速度,略微低下的效率。看怎么取舍了。

04、STM32 HAL库结构

说到STM32的HAL库,就不得不提STM32CubeMX,其作为一个可视化的配置工具,对于开发者来说,确实大大节省了开发时间。STM32CubeMX就是以HAL库为基础的,且目前仅支持HAL库及LL库!首先看一下,官方给出的HAL库的包含结构:

“STM32

· 4.1 stm32f4xx.h主要包含STM32同系列芯片的不同具体型号的定义,是否使用HAL库等的定义,接着,其会根据定义的芯片信号包含具体的芯片型号的头文件:

#if defined(STM32F405xx)
  #include "stm32f405xx.h"
#elif defined(STM32F415xx)
  #include "stm32f415xx.h"
#elif defined(STM32F407xx)
  #include "stm32f407xx.h"
#elif defined(STM32F417xx)
  #include "stm32f417xx.h"
#else
 #error "Please select first the target STM32F4xx device used in your application (in stm32f2xx.h file)"
#endif

紧接着,其会包含stm32f4xx_hal.h。

· 4.2 stm32f4xx_hal.h:stm32f4xx_hal.c/h 主要实现HAL库的初始化、系统滴答相关函数、及CPU的调试模式配置

· 4.3 stm32f4xx_hal_conf.h:该文件是一个用户级别的配置文件,用来实现对HAL库的裁剪,其位于用户文件目录,不要放在库目录中。

接下来对于HAL库的源码文件进行一下说明,HAL库文件名均以stm32f4xx_hal开头,后面加上_外设或者模块名(如:stm32f4xx_hal_adc.c):

· 4.4 库文件

stm32f4xx_hal_ppp.c/.h // 主要的外设或者模块的驱动源文件,包含了该外设的通用API

stm32f4xx_hal_ppp_ex.c/.h // 外围设备或模块驱动程序的扩展文件。这组文件中包含特定型号或者系列的芯片的特殊API。以及如果该特定的芯片内部有不同的实现方式,则该文件中的特殊API将覆盖_ppp中的通用API

stm32f4xx_hal.c/.h // 此文件用于HAL初始化,并且包含DBGMCU、重映射和基于systick的时间延迟等相关的API

· 4.5 其他库文件

用户级别文件:

stm32f4xx_hal_msp_template.c // 只有.c没有.h。它包含用户应用程序中使用的外设的MSP初始化和反初始化(主程序和回调函数)。使用者复制到自己目录下使用模板。

stm32f4xx_hal_conf_template.h // 用户级别的库配置文件模板。使用者复制到自己目录下使用

system_stm32f4xx.c // 此文件主要包含SystemInit()函数,该函数在刚复位及跳到main之前的启动过程中被调用。 它不在启动时配置系统时钟(与标准库相反)。 时钟的配置在用户文件中使用HAL API来完成。

startup_stm32f4xx.s // 芯片启动文件,主要包含堆栈定义,终端向量表等stm32f4xx_it.c/.h // 中断处理函数的相关实现

· 4.6 main.c/.h //

根据HAL库的命名规则,其API可以分为以下三大类:

· 初始化/反初始化函数:HAL_PPP_Init(), HAL_PPP_DeInit()

· IO 操作函数:HAL_PPP_Read(), HAL_PPP_Write(),HAL_PPP_Transmit(), HAL_PPP_Receive()

· 控制函数:HAL_PPP_Set (), HAL_PPP_Get ().

· 状态和错误: ** HAL_PPP_GetState (), HAL_PPP_GetError ().

注意:目前LL库是和HAL库捆绑发布的,所以在HAL库源码中,还有一些名为 stm32f2xx_ll_ppp的源码文件,这些文件就是新增的LL库文件。

使用CubeMX生产项目时,可以选择LL库 

HAL库最大的特点就是对底层进行了抽象。在此结构下,用户代码的处理主要分为三部分:

· 处理外设句柄(实现用户功能)

· 处理MSP

· 处理各种回调函数

相关知识如下:

· (1) 外设句柄定义  

用户代码的第一大部分:对于外设句柄的处理。HAL库在结构上,对每个外设抽象成了一个称为ppp_HandleTypeDef的结构体,其中ppp就是每个外设的名字。*所有的函数都是工作在ppp_HandleTypeDef指针之下。  

1. 多实例支持:每个外设/模块实例都有自己的句柄。因此,实例资源是独立的 

2. 外围进程相互通信:该句柄用于管理进程例程之间的共享数据资源。下面,以ADC为例

/** 
 * @brief  ADC handle Structure definition
 */ 
typedef struct
{
  ADC_TypeDef                   *Instance;                   /*!< Register base address */
  ADC_InitTypeDef               Init;                        /*!< ADC required parameters */
  __IO uint32_t                 NbrOfCurrentConversionRank;  /*!< ADC number of current conversion rank */
  DMA_HandleTypeDef             *DMA_Handle;                 /*!< Pointer DMA Handler */
  HAL_LockTypeDef               Lock;                        /*!< ADC locking object */
  __IO uint32_t                 State;                       /*!< ADC communication state */
  __IO uint32_t                 ErrorCode;                   /*!< ADC Error code */
}ADC_HandleTypeDef;

从上面的定义可以看出,ADC_HandleTypeDef中包含了ADC可能出现的所有定义,对于用户想要使用ADC只要定义一个ADC_HandleTypeDef的变量,给每个变量赋好值,对应的外设就抽象完了。接下来就是具体使用了。

当然,对于那些共享型外设或者说系统外设来说,他们不需要进行以上这样的抽象,这些部分与原来的标准外设库函数基本一样。例如以下外设:

- GPIO 
- SYSTICK  
- NVIC  
- RCC  
- FLASH

以GPIO为例,对于HAL_GPIO_Init() 函数,其只需要GPIO 地址以及其初始化参数即可。

· (2) 三种编程方式  HAL库对所有的函数模型也进行了统一。在HAL库中,支持三种编程模式:轮询模式、中断模式、DMA模式(如果外设支持)。其分别对应如下三种类型的函数(以ADC为例):

HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef* hadc);
HAL_StatusTypeDef HAL_ADC_Stop(ADC_HandleTypeDef* hadc);

HAL_StatusTypeDef HAL_ADC_Start_IT(ADC_HandleTypeDef* hadc);
HAL_StatusTypeDef HAL_ADC_Stop_IT(ADC_HandleTypeDef* hadc);

HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc, uint32_t* pData, uint32_t Length);
HAL_StatusTypeDef HAL_ADC_Stop_DMA(ADC_HandleTypeDef* hadc);

其中,带_IT的表示工作在中断模式下;带_DMA的工作在DMA模式下(注意:DMA模式下也是开中断的);什么都没带的就是轮询模式(没有开启中断的)。至于使用者使用何种方式,就看自己的选择了。  此外,新的HAL库架构下统一采用宏的形式对各种中断等进行配置(原来标准外设库一般都是各种函数)。针对每种外设主要由以下宏:

__HAL_PPP_ENABLE_IT(HANDLE, INTERRUPT): 使能一个指定的外设中断
__HAL_PPP_DISABLE_IT(HANDLE, INTERRUPT):失能一个指定的外设中断
__HAL_PPP_GET_IT (HANDLE, __ INTERRUPT __):获得一个指定的外设中断状态
__HAL_PPP_CLEAR_IT (HANDLE, __ INTERRUPT __):清除一个指定的外设的中断状态
__HAL_PPP_GET_FLAG (HANDLE, FLAG):获取一个指定的外设的标志状态
__HAL_PPP_CLEAR_FLAG (HANDLE, FLAG):清除一个指定的外设的标志状态
__HAL_PPP_ENABLE(HANDLE) :使能外设__HAL_PPP_DISABLE(HANDLE) :失能外设
__HAL_PPP_XXXX (HANDLE, PARAM) :指定外设的宏定义
_HAL_PPP_GET IT_SOURCE (HANDLE, __ INTERRUPT __):检查中断源

· (3)三大回调函数

在HAL库的源码中,到处可见一些以__weak开头的函数,而且这些函数,有些已经被实现了,比如:

__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
  /*Configure the SysTick to have interrupt in 1ms time basis*/
  HAL_SYSTICK_Config(SystemCoreClock/1000U);
  /*Configure the SysTick IRQ priority */
  HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority ,0U);
  /* Return function status */
  return HAL_OK;
}

有些则没有被实现,例如:

__weak void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
  /* Prevent unused argument(s) compilation warning */
  UNUSED(hspi);
  /* NOTE : This function should not be modified, when the callback is needed,the HAL_SPI_TxCpltCallback should be implemented in the user file
  */
}

所有带有__weak关键字的函数表示,就可以由用户自己来实现。如果出现了同名函数,且不带__weak关键字,那么连接器就会采用外部实现的同名函数。通常来说,HAL库负责整个处理和MCU外设的处理逻辑,并将必要部分以回调函数的形式给出到用户,用户只需要在对应的回调函数中做修改即可。HAL库包含如下三种用户级别回调函数(PPP为外设名):

1. 外设系统级初始化/解除初始化回调函数(用户代码的第二大部分:对于MSP的处理):

HAL_PPP_MspInit()和 HAL_PPP_MspDeInit** 例如:__weak void
HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)。在HAL_PPP_Init() 函数中被调用,用来初始化底层相关的设备(GPIOs, clock, DMA, interrupt)

2.处理完成回调函数:HAL_PPP_ProcessCpltCallback*(Process指具体某种处理,如UART的Tx),例如:__weak void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)。当外设或者DMA工作完成后时,触发中断,该回调函数会在外设中断处理函数或者DMA的中断处理函数中被调用

3.错误处理回调函数:HAL_PPP_ErrorCallback例如:__weak void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi)**。当外设或者DMA出现错误时,触发终端,该回调函数会在外设中断处理函数或者DMA的中断处理函数中被调用

绝大多数用户代码均在以上三大回调函数中实现。

HAL库结构中,在每次初始化前(尤其是在多次调用初始化前),先调用对应的反初始化(DeInit)函数是非常有必要的。

某些外设多次初始化时不调用返回会导致初始化失败。完成回调函数有多种,例如串口的完成回调函数有HAL_UART_TxCpltCallback 和 HAL_UART_TxHalfCpltCallback等(用户代码的第三大部分:对于上面第二点和第三点的各种回调函数的处理)在实际使用中,发现HAL仍有不少问题,例如在使用USB时,其库配置存在问题

05、HAL库移植使用

基本步骤

1.复制stm32f2xx_hal_msp_template.c,参照该模板,依次实现用到的外设的HAL_PPP_MspInit()和 HAL_PPP_MspDeInit。

2.复制stm32f2xx_hal_conf_template.h,用户可以在此文件中自由裁剪,配置HAL库。

3.在使用HAL库时,必须先调用函数:HAL_StatusTypeDef HAL_Init(void)(该函数在stm32f2xx_hal.c中定义,也就意味着第一点中,必须首先实现HAL_MspInit(void)和HAL_MspDeInit(void))

4.HAL库与STD库不同,HAL库使用RCC中的函数来配置系统时钟,用户需要单独写时钟配置函数(STD库默认在system_stm32f2xx.c中)

5.关于中断,HAL提供了中断处理函数,只需要调用HAL提供的中断处理函数。用户自己的代码,不建议先写到中断中,而应该写到HAL提供的回调函数中。

6.对于每一个外设,HAL都提供了回调函数,回调函数用来实现用户自己的代码。整个调用结构由HAL库自己完成。例如:Uart中,HAL提供了void HAL_UART_IRQHandler(UART_HandleTypeDef *huart);函数,用户只需要触发中断后,用户只需要调用该函数即可,同时,自己的代码写在对应的回调函数中即可!如下:

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart);

使用了哪种就用哪个回调函数即可!

基本结构 

综上所述,使用HAL库编写程序(针对某个外设)的基本结构(以串口为例)如下:

1.配置外设句柄 例如,建立UartConfig.c,在其中定义串口句柄 UART_HandleTypeDef huart;,接着使用初始化句柄(HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef huart))

2.编写Msp 例如,建立UartMsp.c,在其中实现void HAL_UART_MspInit(UART_HandleTypeDef huart) 和 void HAL_UART_MspDeInit(UART_HandleTypeDef* huart)

3.实现对应的回调函数 例如,建立UartCallBack.c,在其中实现上文所说明的三大回调函数中的完成回调函数和错误回调函数

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

围观 414

页面

订阅 RSS - STM32