正点原子STM32(基于HAL库)3

目录

高级定时器实验

本章我们主要来学习高级定时器,STM32F103 有2 个高级定时器(TIM1 和TIM8)。我们
将通过四个实验来学习高级定时器的各个功能,分别是高级定时器输出指定个数PWM 实验、高级定时器输出比较模式实验、高级定时器互补输出带死区控制实验和高级定时器PWM 输入模式实验。

高级定时器简介

在这里插入图片描述

高级定时器的框图和通用定时器框图很类似,只是添加了其它的一些功能,如:重复计数器、带死区控制的互补输出通道、断路输入等。这些功能在高级定时器框图的位置如下:

在这里插入图片描述

上图中,框出来三个部分,这是和通用定时器不同的地方,下面来分别介绍它们。

① 重复计数器
在F1 系列中,高级定时器TIM1 和TIM8 都有重复计数器。下面来介绍一下重复计数器有
什么作用?在学习基本定时器和通用定时器的时候,我们知道定时器发生上溢或者下溢时,会直接生成更新事件。但是有重复计数器的定时器并不完全是这样的,定时器每次发生上溢或下溢时,重复计数器的值会减一,当重复计数器的值为0 时,再发生一次上溢或者下溢才会生成定时器更新事件。如果我们设置重复计数器寄存器RCR 的值为N,那么更新事件将在定时器发生N+1 次上溢或下溢时发生。
这里需要注意的是重复计数器寄存器是具有影子寄存器的,所以RCR 寄存器只是起缓冲的作用。RCR 寄存器的值会在更新事件发生时,被转移至其影子寄存器中,从而真正生效。重复计数器的特性,在控制生成PWM 信号时很有用,后面会有相应的实验。

② 输出比较

高级定时器输出比较部分和通用定时器相比,多了带死区控制的互补输出功能。图2 2.1.1第②部分的TIMx_CH1N、TIMx_CH2N 和TIMx_CH3N 分别是定时器通道1、通道2 和通道3的互补输出通道,通道4 是没有互补输出通道的。
DTG 是死区发生器,死区时间由DTG[7:0]位来配置。如果不使用互补通道和死区时间控制,那么高级定时器TIM1 和TIM8 和通用定时器的输出比较部分使用方法基本一样,只是要注意MOE 位得置1 定时器才能输出。
如果使用互补通道,那么就有一定的区别了,具体我们在高级定时器互补输出带死区控制实验小节再来介绍。

③ 断路功能
断路功能也称刹车功能,一般用于电机控制的刹车。F1 系列有一个断路通道,断路源可以是刹车输入引脚(TIMx_BKIN),也可以是一个时钟失败事件。时钟失败事件由复位时钟控制器中的时钟安全系统产生。系统复位后,断路功能默认被禁止,MOE 位为低。
使能断路功能的方法:将TIMx_BDTR 的位BKE 置1。断路输入引脚TIMx_BKIN 的输入有效电平可通过TIMx_BDTR 寄存器的位BKP 设置。
使能刹车功能后:由TIMx_BDTR 的MOE、OSSI、OSSR 位,TIMx_CR2 的OISx、OISxN位,TIMx_CCER 的CCxE、CCxNE 位控制OCx 和OCxN 输出状态。无论何时,OCx 和OCxN输出都不能同时处在有效电平。
当发生断路输入后,会怎么样?
1,MOE 位被异步地清零,OCx 和OCxN 为无效、空闲或复位状态(由OSSI 位选择)。
2,OCx 和OCxN 的状态:由相关控制位状态决定,当使用互补输出时:根据情况自动控制输出电平,参考《STM32F10xxx 参考手册_V10(中文版).pdf》手册第245 页的表75 带刹车功能的互补通道Ocx 和OcxN 的控制位。
3,BIF 位置1,如果使能了BIE 位,还会产生刹车中断;如果使能了TDE 位,会产生DMA请求。
4,如果AOE 位置1,在下一个更新事件UEV 时,MOE 位被自动置1。
高级定时器框图部分就简单介绍到这里,下面通过实际的实验来学习高级定时器。

高级定时器输出指定个数PWM 实验

要实现定时器输出指定个数PWM,只需要掌握下面几点内容:
第一,如果大家还不清楚定时器是如何输出PWM 的,请回顾通用定时器PWM 输出实验的内容,这部分的知识是一样的。但是需要注意的是:我们需要把MOE 位置1,这样高级定时器的通道才能输出。
第二,要清楚重复计数器特性,设置重复计数器寄存器RCR 的值为N,那么更新事件将在定时器发生N+1 次上溢或下溢时发生。换句话来说就是,想要指定输出N 个PWM,只需要把N-1 写入RCR 寄存器。因为在边沿对齐模式下,定时器溢出周期对应着PWM 周期,我们只要在更新事件发生时,停止输出PWM 就行。如下图:

在这里插入图片描述

第三,为了保证定时器输出指定个数的PWM 后,定时器马上停止继续输出,我们使能更新中断,并在定时器中断里关闭计数器。

高级定时器输出指定个数PWM原理

在这里插入图片描述

在这里插入图片描述

TIM1/TIM8 寄存器

下面介绍TIM1/TIM8 这些高级定时器中使用到的几个重要的寄存器,其他更多关于定时器的资料可以参考《STM32F10xxx 参考手册_V10(中文版).pdf》的第13 章。
⚫ 控制寄存器1(TIMx_CR1)
TIM1/TIM8 的控制寄存器1 描述如图22.2.1.1 所示:

在这里插入图片描述

上图中我们只列出了本章需要用的一些位,其中:位7(APRE)用于控制自动重载寄存器是否具有缓冲作用,在基本定时器的时候已经讲过,请回顾。在本实验中我们把该位要置1,这样就算改变ARR 寄存器的值,该值也不会马上生效,而是等待之前设置的PWM 完整输出后(发生更新事件)才生效。位4(DIR)用于配置计数器的计数方向,这里我们默认置0。位0(CEN),用于使能计数器的工作,必须要设置该位为1,才可以开始计数。
⚫ 捕获/比较模式寄存器1/2(TIMx_CCMR1/2)
TIM1/TIM8 的捕获/比较模式寄存器(TIMx_CCMR1/2),该寄存器一般有2 个:TIMx _CCMR1 和TIMx _CCMR2。TIMx_CCMR1 控制CH1 和CH2,而TIMx_CCMR2 控制CH3 和CH4。TIMx_CCMR1 寄存器描述如图22.2.1.2 所示:

在这里插入图片描述

该寄存器的有些位在不同模式下,功能不一样,我们前面已经说过。比如我们要让TIM1 的CH1 输出PWM 波为例,该寄存器的模式设置位OC1M[2:0]就是对应着通道1 的模式设置,此部分由3 位组成,总共可以配置成8 种模式,我们使用的是PWM 模式,所以这3 位必须设置为110 或者111,分别对应PWM 模式1 和PWM 模式2。这两种PWM 模式的区别就是输出有效电平的极性相反,这里我们设置为PWM 模式1。位3 OC1PE 是输出比较通道1 的预装使能,该位需要置1,另外CC1S[1:0]用于设置通道1 的方向(输入/输出)默认设置为0,就是设置通道作为输出使用。
⚫ 捕获/比较使能寄存器(TIMx_ CCER)
TIM1/TIM8 的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关。
TIMx_CCER 寄存器描述如图22.2.1.3 所示:

在这里插入图片描述

该寄存器比较简单,要让TIM1 的CH1 输出PWM 波,这里我们要使能CC1E 位,该位是通道1 输入/输出使能位,要想PWM 从IO 口输出,这个位必须设置为1。CC1P 位是设置通道1 的输出极性,我们设置0,即OC1 高电平有效。
⚫ 事件产生寄存器(TIMx_ EGR)
TIM1/TIM8 的事件产生寄存器,该寄存器作用是让用户用软件方式产生各类事件。
TIMx_EGR 寄存器描述如图22.2.1.4 所示:

在这里插入图片描述

UG 位是更新事件的控制位,作用和定时器溢出时产生的更新事件一样,区别是这里是通过软件产生的,而定时器溢出是硬件自己完成的。只有开启了更新中断,这两种方式都可以产更新中断。本实验用到该位去产生软件更新器事件,在需要的时候把UG 位置1 即可,会由硬件自动清零。
⚫ 重复计数器寄存器(TIMx_ RCR)
重复计数器寄存器用于设置重复计数器值,因为它具有影子寄存器,所以它本身只是起缓冲作用。当更新事件发生时,该寄存器的值会转移到其影子寄存器中,从而真正起作用。TIMx_ RCR 寄存器描述如图22.2.1.5 所示:

在这里插入图片描述

该寄存器的REP[7:0]位是低8 位有效,即最大值255。因为这个寄存器只是起缓冲作用,如果大家对该寄存器写入值后,想要立即生效,可以通过对UG 位写1,产生软件更新事件。
⚫ 捕获/比较寄存器1/2/3/4(TIMx_CCR1/2/3/4)
捕获/比较寄存器(TIMx_CCR1/2/3/4),该寄存器总共有4 个,对应4 个通道CH1~CH4。
我们使用的是通道1,所以来看看TIMx_CCR1 寄存器的描述,如图22.2.1.6 所示:

在这里插入图片描述

在输出模式下,捕获/比较寄存器影子寄存器的值与CNT 的值比较,根据比较结果产生相应动作,利用这点,我们通过修改这个寄存器的值,就可以控制PWM 的占空比了。
⚫ 断路和死区寄存器(TIMx_ BDTR)
高级定时器TIM1/8 的通道用作输出时,还必须配置断路和死区寄存器(TIMx_BDTR)的位MOE,该寄存器各位描述如图22.3.1.7 所示:

在这里插入图片描述

本实验,我们只需要关注该寄存器的位15(MOE),要想高级定时器的PWM 正常输出,则必须设置MOE 位为1,否则不会有输出。

硬件设计

  1. 例程功能
    通过TIM8_CH1(由PC6 复用)输出PWM,然后为了指示PWM 的输出情况,我们用杜邦线将PC6 和PE5 引脚的排针连接起来,从而实现PWM 输出控制LED1(硬件已连接在PPE5引脚上)的亮灭。注意的点是:PE5 要设置成浮空输入,避免引脚冲突,我们在main 函数中设置好了,请看源码。上电默认输出5 个PWM 波,连接好杜邦线后可以看见LED1 亮灭五次。
    之后按一下按键KEY0,就会输出5 个PWM 波控制LED1 亮灭五次。LED0 闪烁提示系统正在运行。
  2. 硬件资源
    1)LED 灯:
    LED0 – PB5
    LED1 – PE5
    2)独立按键:
    KEY0 – PE4
    3)定时器8,使用TIM8 通道1,由PC6 复用。用杜邦线将PC6 和PE5 引脚连接起来。
  3. 原理图
    定时器属于STM32F103 的内部资源,只需要软件设置好即可正常工作。我们通过LED1 来指示STM32F103 的定时器的PWM 输出情况,所以需要用一根杜邦线连接PC6 和PE5,同时还用按键KEY0 进行控制。

课堂源码(输出指定个数PWM灯就闪几次)

本实验用到的HAL 库函数介绍请回顾通用定时器PWM 输出实验。下面介绍一下定时器输出指定个数PWM 的配置步骤。

定时器输出指定个数PWM 配置步骤

在这里插入图片描述
在这里插入图片描述

1)开启TIMx 和通道输出的GPIO 时钟,配置该IO 口的复用功能输出
首先开启TIMx 的时钟,然后配置GPIO 为复用功能输出。本实验我们默认用到定时器8 通道1,对应IO 是PC6,它们的时钟开启方法如下:

__HAL_RCC_TIM8_CLK_ENABLE(); /* 使能定时器8 */
__HAL_RCC_GPIOC_CLK_ENABLE(); /* 开启GPIOC时钟*/

IO 口复用功能是通过函数HAL_GPIO_Init 来配置的。
2)初始化TIMx,设置TIMx 的ARR 和PSC 等参数
使用定时器的PWM 模式功能时,我们调用的是HAL_TIM_PWM_Init 函数来初始化定时器ARR 和PSC 等参数。
注意:该函数会调用:HAL_TIM_PWM_MspInit函数,我们可以通过后者存放定时器和GPIO时钟使能、GPIO 初始化、中断使能以及优先级设置等代码。
3)设置定时器为PWM 模式,输出比较极性,比较值等参数
在HAL 库中,通过HAL_TIM_PWM_ConfigChannel 函数来设置定时器为PWM1 模式或者PWM2 模式,根据需求设置输出比较的极性,设置比较值(控制占空比)等。
本实验我们设置TIM8 的通道1 为PWM1 模式,使用杜邦线把PC6 与PE5 进行连接,因为我们的LED1(连接PE5)是低电平亮,而我们希望输出最后一个PWM 波的时候,LED1 就灭,所以我们设置输出比较极性为高。捕获/比较寄存器的值(即比较值)设置为自动重装载值的一半,即PWM 占空比为50%。
4)使能定时器更新中断,开启定时器并输出PWM,配置定时器中断优先级
通过__HAL_TIM_ENABLE_IT 函数使能定时器更新中断。
通过HAL_TIM_PWM_Start 函数使能定时器并开启输出PWM。
通过HAL_NVIC_EnableIRQ 函数使能定时器中断。
通过HAL_NVIC_SetPriority 函数设置中断优先级。
5)编写中断服务函数
定时器中断服务函数为:TIMx_IRQHandler 等,当发生中断的时候,程序就会执行中断服务函数。HAL 库提供了一个定时器中断公共处理函数HAL_TIM_IRQHandler,该函数会根据中断类型调用相关的中断回调函数。用户根据自己的需要重定义这些中断回调函数来处理中断程序。本实验我们不使用HAL 库的中断回调机制,而是把中断程序写在定时器中断服务函数里。

在这里插入图片描述

atim.h

#ifndef __ATIM_H
#define __ATIM_H

#include "./SYSTEM/sys/sys.h"


void atim_timx_npwm_chy_init(uint16_t arr, uint16_t psc);
void atim_timx_npwm_chy_set(uint8_t npwm);

#endif

atim.c

#include "./BSP/TIMER/atim.h"


TIM_HandleTypeDef g_timx_npwm_chy_handle;     /* 定时器x句柄 */

static uint8_t g_npwm_remain = 0;

 /* 高级定时器TIMX 通道Y 输出指定个数PWM 初始化函数 */
void atim_timx_npwm_chy_init(uint16_t arr, uint16_t psc)
{
    TIM_OC_InitTypeDef timx_oc_npwm_chy = {0};

    g_timx_npwm_chy_handle.Instance = TIM8;                            /* 定时器x */
    g_timx_npwm_chy_handle.Init.Prescaler = psc;                       /* 定时器分频 */
    g_timx_npwm_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;      /* 递增计数模式 */
    g_timx_npwm_chy_handle.Init.Period = arr;                          /* 自动重装载值 */
    g_timx_npwm_chy_handle.Init.RepetitionCounter = 0;                 /* 重复计数器初始值 */
    HAL_TIM_PWM_Init(&g_timx_npwm_chy_handle);                         /* 初始化PWM */


    timx_oc_npwm_chy.OCMode = TIM_OCMODE_PWM1;                         /* 模式选择PWM 1*/
    timx_oc_npwm_chy.Pulse = arr / 2;                                  /* 设置比较值,此值用来确定占空比50% */
                                                                       /* 这里默认设置比较值为自动重装载值的一半,即占空比为50% */
    timx_oc_npwm_chy.OCPolarity = TIM_OCPOLARITY_HIGH;                 /* 输出比较极性为高 */
    HAL_TIM_PWM_ConfigChannel(&g_timx_npwm_chy_handle, &timx_oc_npwm_chy, TIM_CHANNEL_1);

    __HAL_TIM_ENABLE_IT(&g_timx_npwm_chy_handle, TIM_IT_UPDATE);
    HAL_TIM_PWM_Start(&g_timx_npwm_chy_handle, TIM_CHANNEL_1);
}

/* 定时器 PWM输出 MSP初始化函数 */
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM8)
    {
        GPIO_InitTypeDef gpio_init_struct;
        __HAL_RCC_GPIOC_CLK_ENABLE();
        __HAL_RCC_TIM8_CLK_ENABLE();

        gpio_init_struct.Pin = GPIO_PIN_6;
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;        /* 推挽式复用功能 */
        gpio_init_struct.Pull = GPIO_PULLUP;            /* 上拉 */
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;  /* 高速 */
        HAL_GPIO_Init(GPIOC, &gpio_init_struct);
        
        HAL_NVIC_SetPriority(TIM8_UP_IRQn, 1, 3);
        HAL_NVIC_EnableIRQ(TIM8_UP_IRQn);
    }
}

/* 高级定时器TIMX NPWM设置PWM个数函数 */
void atim_timx_npwm_chy_set(uint8_t npwm)
{
    if(npwm == 0) return;
    
    g_npwm_remain = npwm;
    HAL_TIM_GenerateEvent(&g_timx_npwm_chy_handle, TIM_EVENTSOURCE_UPDATE);
    __HAL_TIM_ENABLE(&g_timx_npwm_chy_handle);
}

/* 定时器8中断服务函数 */
void TIM8_UP_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&g_timx_npwm_chy_handle);
}

/* 定时器更新中断回调函数 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM8)
    {
        if(g_npwm_remain)
        {
            TIM8->RCR = g_npwm_remain - 1;//N-1写入到RCR寄存器
            HAL_TIM_GenerateEvent(&g_timx_npwm_chy_handle, TIM_EVENTSOURCE_UPDATE);//RCR寄存器的值缓冲到影子寄存器里,通过软件更新事件方式
            __HAL_TIM_ENABLE(&g_timx_npwm_chy_handle);//启动计数器
            g_npwm_remain = 0;
        }
        else
        {
            TIM8->CR1 &= ~(1 << 0);//关闭计数器,这里也可以直接调用HAL库函数
        }
    }
}

main.c

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/KEY/key.h"
#include "./BSP/TIMER/atim.h"


int main(void)
{
    uint8_t key;
    uint8_t t = 0;
    GPIO_InitTypeDef gpio_init_struct;
    
    HAL_Init();                         /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    delay_init(72);                     /* 延时初始化 */
    usart_init(115200);                 /* 串口初始化为115200 */
    led_init();                         /* 初始化LED */
    key_init();                         /* 初始化按键 */
    
    /* 把PE5设置为输入,避免与 PC6 冲突(杜邦线连接两引脚) */
    __HAL_RCC_GPIOE_CLK_ENABLE();
    gpio_init_struct.Pin = GPIO_PIN_5;
    gpio_init_struct.Mode = GPIO_MODE_INPUT;                /* 输入 */
    gpio_init_struct.Pull = GPIO_PULLUP;                    /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;          /* 高速 */
    HAL_GPIO_Init(GPIOE, &gpio_init_struct);
    
    atim_timx_npwm_chy_init(5000 - 1, 7200 - 1);
    
    atim_timx_npwm_chy_set(5);//灯闪5次
    
    while (1)
    {
        key = key_scan(0);
        if(key == KEY0_PRES)//按键 0 按下
        {
            atim_timx_npwm_chy_set(6);//灯闪6次
        }
 
        t++;
        delay_ms(10);

        if (t > 50)                    /* 控制LED0闪烁, 提示程序运行状态 */
        {
            t = 0;
            LED0_TOGGLE();
        }
    }
}

程序设计

程序流程图
在这里插入图片描述

程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。高级定时器驱动源码包括两个文件:atim.c 和atim.h。本章节的四个实验源码都是存放在atim.c 和atim.h 中,源码中也有明确的注释。
首先看atim.h 头文件的几个宏定义:

/* TIMX 输出指定个数PWM 定义
* 这里输出的PWM通过PC6(TIM8_CH1)输出, 我们用杜邦线连接PC6和PE5, 然后在程序里面将PE5设
* 置成浮空输入就可以看到TIM8_CH1控制LED1(GREEN)的亮灭, 亮灭一次表示一个PWM波
* 默认使用的是TIM8_CH1.
* 注意: 通过修改这几个宏定义, 可以支持TIM1/TIM8定时器, 任意一个IO口输出指定个数的PWM
*/
#define ATIM_TIMX_NPWM_CHY_GPIO_PORT GPIOC
#define ATIM_TIMX_NPWM_CHY_GPIO_PIN GPIO_PIN_6
#define ATIM_TIMX_NPWM_CHY_GPIO_CLK_ENABLE() do{__HAL_RCC_GPIOC_CLK_ENABLE();\
}while(0) /* PC口时钟使能*/
#define ATIM_TIMX_NPWM TIM8
#define ATIM_TIMX_NPWM_IRQn TIM8_UP_IRQn
#define ATIM_TIMX_NPWM_IRQHandler TIM8_UP_IRQHandler
#define ATIM_TIMX_NPWM_CHY TIM_CHANNEL_1 /* 通道Y, 1<= Y <=4 */
#define ATIM_TIMX_NPWM_CHY_CCRX TIM8->CCR1/* 通道Y的输出比较寄存器*/
#define ATIM_TIMX_NPWM_CHY_CLK_ENABLE() do{ __HAL_RCC_TIM8_CLK_ENABLE(); \
}while(0)

可以把上面的宏定义分成两部分,第一部分是定时器8 输入通道1 对应的IO 口的宏定义,第二部分则是定时器8 输入通道1 的相应宏定义。
下面看atim.c 的程序,首先是输出指定个数PWM 初始化函数,其定义如下:

/**
* @brief 高级定时器TIMX 通道Y 输出指定个数PWM 初始化函数
* @note
* 高级定时器的时钟来自APB2, 而PCLK2 = 72Mhz, 我们设置PPRE2不分频, 因此
* 高级定时器时钟= 72Mhz
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
* Ft=定时器工作频率,单位:Mhz
* @param arr: 自动重装值
* @param psc: 时钟预分频数
* @retval 无
*/
void atim_timx_npwm_chy_init(uint16_t arr, uint16_t psc)
{
	GPIO_InitTypeDef gpio_init_struct;
	TIM_OC_InitTypeDef timx_oc_npwm_chy; /* 定时器输出*/
	ATIM_TIMX_NPWM_CHY_GPIO_CLK_ENABLE(); /* TIMX 通道IO口时钟使能*/
	ATIM_TIMX_NPWM_CHY_CLK_ENABLE(); /* TIMX 时钟使能*/
	g_timx_npwm_chy_handle.Instance = ATIM_TIMX_NPWM; /* 定时器x */
	g_timx_npwm_chy_handle.Init.Prescaler = psc; /* 定时器分频*/
	g_timx_npwm_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;/* 递增计数*/
	g_timx_npwm_chy_handle.Init.Period = arr; /* 自动重装载值*/
	g_timx_npwm_chy_handle.Init.AutoReloadPreload =
	TIM_AUTORELOAD_PRELOAD_ENABLE; /*使能TIMx_ARR进行缓冲*/
	g_timx_npwm_chy_handle.Init.RepetitionCounter = 0; /* 重复计数器初始值*/
	HAL_TIM_PWM_Init(&g_timx_npwm_chy_handle); /* 初始化PWM */
	gpio_init_struct.Pin = ATIM_TIMX_NPWM_CHY_GPIO_PIN;/* 通道y的CPIO口*/
	gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推完输出*/
	gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉*/
	gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速*/
	HAL_GPIO_Init(ATIM_TIMX_NPWM_CHY_GPIO_PORT, &gpio_init_struct);
	timx_oc_npwm_chy.OCMode = TIM_OCMODE_PWM1; /* 模式选择PWM 1*/
	timx_oc_npwm_chy.Pulse = arr / 2; /* 设置比较值,此值用来确定占空比*/
	timx_oc_npwm_chy.OCPolarity = TIM_OCPOLARITY_HIGH; /* 输出比较极性为高*/
	HAL_TIM_PWM_ConfigChannel(&g_timx_npwm_chy_handle, &timx_oc_npwm_chy,
	ATIM_TIMX_NPWM_CHY); /* 配置TIMx通道y */
	/* 设置中断优先级,抢占优先级1,子优先级3 */
	HAL_NVIC_SetPriority(ATIM_TIMX_NPWM_IRQn, 1, 3);
	HAL_NVIC_EnableIRQ(ATIM_TIMX_NPWM_IRQn); /* 开启ITMx中断*/
	__HAL_TIM_ENABLE_IT(&g_timx_npwm_chy_handle, TIM_IT_UPDATE);/* 允许更新中断*/
	HAL_TIM_PWM_Start(&g_timx_npwm_chy_handle, ATIM_TIMX_NPWM_CHY);/* 使能输出*/
}

atim_timx_npwm_chy_init 函数包含了输出通道对应IO 的初始代码、NVIC、使能时钟、定时器基础工作参数和输出模式配置的所有代码。下面来看看该函数的代码内容。
第一部分使能定时器和GPIO 的时钟。
第二部分调用HAL_TIM_PWM_Init 函数初始化定时器基础工作参数,如:ARR 和PSC 等。
第三部分是定时器输出通道对应的IO 的初始化。
第四部分调用HAL_TIM_PWM_ConfigChannel 设置PWM 模式以及比较值等参数。
第五部分是NVIC 的初始化,配置抢占优先级、响应优先级和开启NVIC 定时器中断。
最后是使能更新中断和使能通道输出。
为了方便代码的管理和移植性等,这里就没有使用HAL_TIM_PWM_MspInit 这个函数来存放使能时钟、GPIO、NVIC 相关的代码,而是全部存放在gtim_timx_npwm_chy_init 函数中。
下面我们看设置PWM 个数的函数,其定义如下:

/* g_npwm_remain表示当前还剩下多少个脉冲要发送
* 每次最多发送256个脉冲
*/
static uint32_t g_npwm_remain = 0;
/**
* @brief 高级定时器TIMX NPWM设置PWM个数
* @param rcr: PWM的个数, 1~2^32次方个
* @retval 无
*/
void atim_timx_npwm_chy_set(uint32_t npwm)
{
	if (npwm == 0)return ;
	g_npwm_remain = npwm; /* 保存脉冲个数*/
	/* 产生一次更新事件,在中断里面处理脉冲输出*/
	HAL_TIM_GenerateEvent(&g_timx_npwm_chy_handle, TIM_EVENTSOURCE_UPDATE);
	__HAL_TIM_ENABLE(&g_timx_npwm_chy_handle); /* 使能定时器TIMX */
}

我们要输出多少个周期的PWM 就用这个函数来设置。该函数作用是把我们设置输出的PWM 个数的值赋值给静态全局变量g_npwm_remain,该变量会在更新中断服务函数回调函数中发挥作用。最后对TIMx_EGR 寄存器UG 位写1,产生一次更新事件,并使能定时器。
下面来介绍定时器中断服务函数,其定义如下:

/**
 * @brief 定时器中断服务函数
 * @param 无
 * @retval 无
 */
void ATIM_TIMX_NPWM_IRQHandler(void)
{
        uint16_t npwm = 0;
        /* 以下代码没有使用定时器HAL库共用处理函数来处理,而是直接通过判断中断标志位的方式*/
        if(__HAL_TIM_GET_FLAG(&g_timx_npwm_chy_handle, TIM_FLAG_UPDATE) != RESET)
        {
                if (g_npwm_remain >= 256) /* 还有大于256个脉冲需要发送*/
                {
                        g_npwm_remain=g_npwm_remain - 256;
                        npwm = 256;
                }
                else if (g_npwm_remain % 256) /* 还有位数(不到256)个脉冲要发送*/
                {
                        npwm = g_npwm_remain % 256;
                        g_npwm_remain = 0; /* 没有脉冲了*/
                }
                if (npwm) /* 有脉冲要发送*/
                {
                        ATIM_TIMX_NPWM->RCR = npwm - 1; /* 设置RCR值为npwm-1, 即npwm个脉冲*/
                        HAL_TIM_GenerateEvent(&g_timx_npwm_chy_handle,
                                        TIM_EVENTSOURCE_UPDATE); /* 产生一次更新事件,以更新RCR寄存器*/
                        __HAL_TIM_ENABLE(&g_timx_npwm_chy_handle); /* 使能定时器TIMX */
                }
                else
                {
                        /* 关闭定时器TIMX,使用__HAL_TIM_DISABLE需要失能通道输出,所以不用*/
                        ATIM_TIMX_NPWM->CR1 &= ~(1 << 0);
                }
                /* 清除定时器更新中断标志位*/
                __HAL_TIM_CLEAR_IT(&g_timx_npwm_chy_handle, TIM_IT_UPDATE);
        }
}

这里我们没有使用HAL 库的中断回调机制,而是想寄存器操作一样,直接通过判断中断标志位处理中断。通过__HAL_TIM_GET_FLAG 函数宏判断是否发生更新中断,然后进行更新中断的代码处理,最后通过__HAL_TIM_CLEAR_IT 函数宏清除更新中断标志位。
因为重复计数器寄存器(TIM8_RCR)是8 位有效的,所以在定时器中断服务函数中首先对全局变量g_npwm_remain(即我们要输出的PWM 个数)进行判断,是否大于256,如果大于256,那就得分次写入重复计数器寄存器。写入重复计数寄存器后,需要产生软件更新事件把RCR 寄存器的值更新到RCR 影子寄存器中,最后一定不要忘记清除定时器更新中断标志位。
在main 函数里面编写如下代码:

int main(void)
{
        uint8_t key = 0;
        uint8_t t = 0;
        GPIO_InitTypeDef gpio_init_struct;
        HAL_Init(); /* 初始化HAL库*/
        sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
        delay_init(72); /* 延时初始化*/
        usart_init(115200); /* 串口初始化为115200 */
        led_init(); /* 初始化LED */
        key_init(); /* 初始化按键*/
        /* 将LED1 引脚设置为输入模式, 避免和PC6冲突*/
        gpio_init_struct.Pin = LED1_GPIO_PIN; /* LED1 引脚*/
        gpio_init_struct.Mode = GPIO_MODE_INPUT; /* 设置输入状态*/
        gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉*/
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM; /* 中速*/
        HAL_GPIO_Init(LED1_GPIO_PORT, &gpio_init_struct); /* 初始化LED1引脚*/
        atim_timx_npwm_chy_init(5000 - 1, 7200 - 1);/*10Khz的计数频率,2hz的PWM频率*/
        /* 设置PWM占空比,50%,这样可以控制每一个PWM周期,LED1(BLUE) 有一半时间是亮的,
           一半时间是灭的,LED1亮灭一次,表示一个PWM波*/
        ATIM_TIMX_NPWM_CHY_CCRX = 2500;
        atim_timx_npwm_chy_set(5); /* 输出5个PWM波(控制LED1)闪烁5次) */
        while (1)
        {
                key = key_scan(0);
                if (key == KEY0_PRES) /* KEY0按下*/
                {
                        gtim_timx_npwm_chy_set(5); /* 输出5个PWM波(控制LED1闪烁5次) */
                }
                t++;
                delay_ms(10);
                if (t > 50) /* 控制LED1闪烁, 提示程序运行状态*/
                {
                        t = 0;
                        LED0_TOGGLE();
                }
        }
}

先看gtim_timx_npwm_chy_init(5000 - 1, 7200 - 1);这个语句,这两个形参分别设置自动重载寄存器的值为4999,以及预分频器寄存器的值为7199。按照sys_stm32_clock_init 函数的配置,定时器8 的时钟频率等于APB2 总线时钟频率,即72MHz,可以得到计数器的计数频率是10KHz。
自动重载寄存器的值决定的是PWM 周期或频率(请回顾21.3 小节的内容),计数器计5000 个数所用的时间是PWM 的周期。在边沿对齐模式下,定时器的溢出周期等于PWM 的周期。根据定时器溢出时间计算公式,可得:

Tout= ((arr+1)*(psc+1))/Tclk= ((4999+1)*(7199+1))/72000000=0.5s

再由频率是周期的倒数关系得到PWM 的频率为2Hz。
占空比则由捕获/比较寄存器(TIMx_CCRx)的值决定,这里就是由TIM8_CCR1 寄存器决定。初始化定时器8 时我们设置通道输出比较极性为高,GTIM_TIMX_NPWM_CHY_CCRX =2500,就设置了占空比为50%。因为我们的LED 灯是低电平点亮,所以正占空比期间LED 灯熄灭,负占空比期间LED 灯亮。

下载验证

首先用杜邦线连接好PE5 和PC6 引脚的排针。下载代码后,可以看到LED1 亮灭五次,然后我们每按一下按键KEY0,LED1 都会亮灭五次。
下面我们使用正点原子DS100 手持数字示波器,把PC6 引脚的波形截获,具体如下:

在这里插入图片描述
由LED 的原理图可以知道,PC6 引脚输出低电平LED1 亮、输出高电平LED1 灭。图22.2.4中,从左往右看,可以知道,LED0 一开始是熄灭的,然后经过5 次亮灭,最后就是一直保持熄灭的状态。PWM 频率是2Hz,占空比50%,请大家自行测量。

高级定时器输出比较模式实验

本小节我们来学习使用高级定时器输出比较模式下翻转功能,通过定时器4 个通道分别输出4 个50%占空比、不同相位的PWM。
输出比较模式下翻转功能作用是:当计数器的值等于捕获/比较寄存器影子寄存器的值时,OC1REF 发生翻转,进而控制通道输出(OCx)翻转。通过翻转功能实现输出PWM 的具体原理如下:PWM 频率由自动重载寄存器(TIMx_ARR)的值决定,在这个过程中,只要自动重载寄存器的值不变,那么PWM 占空比就固定为50%。我们可以通过捕获/比较寄存器(TIMx_CCRx)的值改变PWM 的相位。生成PWM 的原理如图22.3.1 所示:

在这里插入图片描述

本实验就是根据图22.3.1 的原理来设计的,具体实验是:我们设置固定的ARR 值为999,那么PWM 占空比固定为50%,通过改变4 个通道的捕获/比较寄存器(TIMx_CCRx)的值使得每个通道输出的PWM 的相位都不一样,注意捕获/比较寄存器的值设置范围是:0 ~ ARR。比如:TIMx_CCR1=250-1,TIMx_CCR2=500-1,TIMx_CCR3=750-1,TIMx_CCR4=1000-1,那么可以得到通道1~通道4 输出的PWM 的相位分别是:25%、50%、75%、100%。翻转功能输出的PWM 周期,这里用T 表示,其计算公式如下:

在这里插入图片描述
在这里插入图片描述

TIM1/TIM8 寄存器

高级定时器输出比较模式除了用到定时器的时基单元:计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR) 之外。主要还用到以下这些寄存器:
⚫ 控制寄存器1(TIMx_CR1)
TIM1/TIM8 的控制寄存器1 描述如图22.3.1.1 所示。
在这里插入图片描述

上图中我们只列出了本实验需要用的一些位,其中:位7(APRE)用于控制自动重载寄存器是否具有缓冲作用,在基本定时器的时候已经讲过,请回顾。本实验中,我们把该位置1。
位4(DIR)用于配置计数器的计数方向,本实验默认置0 即可。
位CEN 位,用于使能计数器的工作,必须要设置该位为1,才可以开始计数。
其它位保持复位值即可。
⚫ 捕获/比较模式寄存器1/2(TIMx_CCMR1/2)
TIM1/TIM8 的捕获/比较模式寄存器(TIMx_CCMR1/2 ),该寄存器一般有2 个:
TIMx_CCMR1 和TIMx CCMR2。TIMx_CCMR1 控制CH1 和CH2,而TIMx_CCMR2 控制CH3和CH4。TIMx_CCMR1 寄存器描述如图22.3.1.2 所示:
在这里插入图片描述
该寄存器的有些位在不同模式下,功能不一样,我们现在用到输出比较模式。关于该寄存器的详细说明,请参考《STM32F10xxx 参考手册
V10(中文版).pdf》第240 页,13.4.7 节。
本实验我们用到了定时器8 输出比较的4 个通道,所以我们需要配置TIM1_CCMR1 和TIM1_CCMR2。以TIM1_CCMR1 寄存器为例,模式设置位OC1M[2:0]就是对应着通道1 的模式设置,此部分由3 位组成,总共可以配置成8 种模式,我们使用的是翻转功能,所以这3 位必须设置为011。通道2 也是如此,将位OC2M[2:0] 设置为011。通道3 和通道4 就要设置TIM1_CCMR2 寄存器的位OC3M[2:0]和位OC4M[2:0]。除此之外,我们还要设置输出比较的预装载使能位,如通道1 对应输出比较的预装载使能位OC1PE 置1,其他通道也要把相应位置1。

⚫ 捕获/比较使能寄存器(TIMx_ CCER)
TIM1/TIM8 的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。
TIMx_CCER 寄存器描述如图22.3.1.3 所示:
在这里插入图片描述
该寄存器比较简单,要让TIM8 的4 个通道都输出,我们需要把对应的捕获/比较1 输出使能位置1。通道1 到通道4 的使能位分别是:CC1E、CC2E、CC3E、CC4E,我们把这4 个位置1,使能通道输出。
⚫ 捕获/比较寄存器1/2/3/4(TIMx_ CCR1/2/3/4)
捕获/比较寄存器(TIMx_ CCR1/2/3/4),该寄存器总共有4 个,对应4 个通道CH1~CH4。
本实验4 个通道都要使用到,以通道1 对应的TIMx_ CCR1 寄存器为例,其描述如下图所示:

在这里插入图片描述

这里,我们通过改变TIMx_ CCR1/2/3/4 寄存器的值来改变4 个通道输出的PWM 的相位。
⚫ TIM1/TIM8 断路和死区寄存器(TIMx_ BDTR)
本实验用的是高级定时器,我们还需要配置:断路和死区寄存器(TIMx_BDTR),该寄存器各位描述如图22.3.1.5 所示。
在这里插入图片描述
该寄存器,我们只需要关注位15(MOE),要想高级定时器的通道正常输出,则必须设置MOE 位为1,否则不会有输出。

硬件设计

  1. 例程功能
    使用输出比较模式的翻转功能,通过定时器8 的4 路通道输出占空比固定为50%、相位分别是25%、50%、75%和100%的PWM。
  2. 硬件资源
    1)LED 灯
    LED0 –PB5
    2)PC6 复用为TIM8_CH1
    PC7 复用为TIM8_CH2
    PC8 复用为TIM8_CH3
    PC9 复用为TIM8_CH4
  3. 原理图
    定时器属于STM32F103 的内部资源,只需要软件设置好即可正常工作。我们需要通过示波器观察PC6、PC7、PC8 和PC9 引脚PWM 输出的情况。

程序设计

定时器的HAL 库驱动

在这里插入图片描述

定时器在HAL 库中的驱动代码在前面已经介绍了部分,请回顾,这里我们再介绍几个本实验用到的函数。

  1. HAL_TIM_OC_Init 函数
    定时器的输出比较模式初始化函数,其声明如下:
HAL_StatusTypeDef HAL_TIM_OC_Init(TIM_HandleTypeDef *htim);

⚫ 函数描述:
用于初始化定时器的输出比较模式。
⚫ 函数形参:
形参1 是TIM_HandleTypeDef 结构体类型指针变量,基本定时器的时候已经介绍。
⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值。
2. HAL_TIM_OC_ConfigChannel 函数
定时器的输出比较通道设置初始化函数。其声明如下:

HAL_StatusTypeDef HAL_TIM_OC_ConfigChannel(TIM_HandleTypeDef *htim,
											TIM_OC_InitTypeDef *sConfig, uint32_t Channel);

⚫ 函数描述:
该函数用于初始化定时器的输出比较通道。
⚫ 函数形参:
形参1 是TIM_HandleTypeDef 结构体类型指针变量,用于配置定时器基本参数。
形参2 是TIM_OC_InitTypeDef 结构体类型指针变量,用于配置定时器的输出比较参数。
在通用定时器PWM 输出实验已经介绍过TIM_OC_InitTypeDef 结构体指针类型。
形参3 是定时器通道,范围:TIM_CHANNEL_1 到TIM_CHANNEL_4。
⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值。
3. HAL_TIM_OC_Start 函数
定时器的输出比较启动函数,其声明如下:

HAL_StatusTypeDef HAL_TIM_OC_Start(TIM_HandleTypeDef *htim, uint32_t Channel);

⚫ 函数描述:
用于启动定时器的输出比较模式。
⚫ 函数形参:
形参1 是TIM_HandleTypeDef 结构体类型指针变量。
形参2 是定时器通道,范围:TIM_CHANNEL_1 到TIM_CHANNEL_4。
⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值。
⚫ 注意事项:
HAL 库也同样提供了单独使能定时器的输出通道函数,函数为:

void TIM_CCxChannelCmd(TIM_TypeDef *TIMx, uint32_t Channel,
						uint32_t ChannelState);

HAL_TIM_OC_Start 函数内部也调用了该函数。

定时器输出比较模式配置步骤

在这里插入图片描述

1)开启TIMx 和通道输出的GPIO 时钟,配置该IO 口的复用功能输出。
首先开启TIMx 的时钟,然后配置GPIO 为复用功能输出。本实验我们默认用到定时器8 通
道1、2、3、4,对应IO 是PC6\PC7\PC8\PC9,它们的时钟开启方法如下:

__HAL_RCC_TIM8_CLK_ENABLE(); /* 使能定时器8 */
__HAL_RCC_GPIOC_CLK_ENABLE(); /* 开启GPIOC时钟*/

IO 口复用功能是通过函数HAL_GPIO_Init 来配置的。

2)初始化TIMx,设置TIMx 的ARR 和PSC 等参数。
使用定时器的输出比较模式时,我们调用的是HAL_TIM_OC_Init 函数来初始化定时器ARR 和PSC 等参数。
注意:该函数会调用HAL_TIM_OC_MspInit 函数,我们可以通过后者存放定时器和GPIO时钟使能、GPIO 初始化、中断使能以及优先级设置等代码。
3)设置定时器为输出比较模式,输出比较极性,输出比较值、翻转功能等参数。
在HAL 库中,通过HAL_TIM_OC_ConfigChannel 函数来设置定时器为输出比较模式,根据需求设置输出比较的极性,设置输出比较值、翻转功能等。
最后我们通过__HAL_TIM_ENABLE_OCxPRELOAD 函数使能通道的预装载。
4)开启定时器并输出PWM
通过HAL_TIM_OC_Start 函数使能定时器并开启输出。

课堂源码

在这里插入图片描述

在这里插入图片描述

atim.h

#ifndef __ATIM_H
#define __ATIM_H

#include "./SYSTEM/sys/sys.h"


void atim_timx_comp_pwm_init(uint16_t arr, uint16_t psc);

#endif

atim.c

#include "./BSP/TIMER/atim.h"


TIM_HandleTypeDef g_timx_comp_pwm_handle;       /* 定时器x句柄 */

/* 高级定时器 输出比较模式 初始化函数 */
void atim_timx_comp_pwm_init(uint16_t arr, uint16_t psc)
{
    TIM_OC_InitTypeDef timx_oc_comp_pwm = {0};
    
    g_timx_comp_pwm_handle.Instance = TIM8;                       /* 定时器8 */
    g_timx_comp_pwm_handle.Init.Prescaler = psc  ;                /* 定时器分频 */
    g_timx_comp_pwm_handle.Init.CounterMode = TIM_COUNTERMODE_UP; /* 递增计数模式 */
    g_timx_comp_pwm_handle.Init.Period = arr;                     /* 自动重装载值 */
    HAL_TIM_OC_Init(&g_timx_comp_pwm_handle);                     /* 输出比较模式初始化 */

    timx_oc_comp_pwm.OCMode = TIM_OCMODE_TOGGLE;
    timx_oc_comp_pwm.OCPolarity = TIM_OCPOLARITY_HIGH;
    HAL_TIM_OC_ConfigChannel(&g_timx_comp_pwm_handle, &timx_oc_comp_pwm, TIM_CHANNEL_1);
    HAL_TIM_OC_ConfigChannel(&g_timx_comp_pwm_handle, &timx_oc_comp_pwm, TIM_CHANNEL_2);
    HAL_TIM_OC_ConfigChannel(&g_timx_comp_pwm_handle, &timx_oc_comp_pwm, TIM_CHANNEL_3);
    HAL_TIM_OC_ConfigChannel(&g_timx_comp_pwm_handle, &timx_oc_comp_pwm, TIM_CHANNEL_4);
    
    __HAL_TIM_ENABLE_OCxPRELOAD(&g_timx_comp_pwm_handle, TIM_CHANNEL_1);
    __HAL_TIM_ENABLE_OCxPRELOAD(&g_timx_comp_pwm_handle, TIM_CHANNEL_2);
    __HAL_TIM_ENABLE_OCxPRELOAD(&g_timx_comp_pwm_handle, TIM_CHANNEL_3);
    __HAL_TIM_ENABLE_OCxPRELOAD(&g_timx_comp_pwm_handle, TIM_CHANNEL_4);
    
    HAL_TIM_OC_Start(&g_timx_comp_pwm_handle, TIM_CHANNEL_1);
    HAL_TIM_OC_Start(&g_timx_comp_pwm_handle, TIM_CHANNEL_2);
    HAL_TIM_OC_Start(&g_timx_comp_pwm_handle, TIM_CHANNEL_3);
    HAL_TIM_OC_Start(&g_timx_comp_pwm_handle, TIM_CHANNEL_4);
}

/* 定时器 输出比较 MSP初始化函数 */
void HAL_TIM_OC_MspInit(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM8)
    {
        GPIO_InitTypeDef gpio_init_struct;

        __HAL_RCC_TIM8_CLK_ENABLE();
        __HAL_RCC_GPIOC_CLK_ENABLE();

        gpio_init_struct.Pin = GPIO_PIN_6;
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;
        gpio_init_struct.Pull = GPIO_NOPULL;
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
        HAL_GPIO_Init(GPIOC, &gpio_init_struct);

        gpio_init_struct.Pin = GPIO_PIN_7;
        HAL_GPIO_Init(GPIOC, &gpio_init_struct);

        gpio_init_struct.Pin = GPIO_PIN_8;
        HAL_GPIO_Init(GPIOC, &gpio_init_struct);

        gpio_init_struct.Pin = GPIO_PIN_9;
        HAL_GPIO_Init(GPIOC, &gpio_init_struct);
    }
}

main.c

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/TIMER/atim.h"


extern TIM_HandleTypeDef g_timx_comp_pwm_handle;       /* 定时器x句柄 */

int main(void)
{
    uint8_t t = 0;

    HAL_Init();                         /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    delay_init(72);                     /* 延时初始化 */
    usart_init(115200);                 /* 串口初始化为115200 */
    led_init();                         /* 初始化LED */
    atim_timx_comp_pwm_init(1000 - 1, 72 - 1);
    
    __HAL_TIM_SET_COMPARE(&g_timx_comp_pwm_handle, TIM_CHANNEL_1, 250 - 1);
    __HAL_TIM_SET_COMPARE(&g_timx_comp_pwm_handle, TIM_CHANNEL_2, 500 - 1);
    __HAL_TIM_SET_COMPARE(&g_timx_comp_pwm_handle, TIM_CHANNEL_3, 750 - 1);
    __HAL_TIM_SET_COMPARE(&g_timx_comp_pwm_handle, TIM_CHANNEL_4, 1000 - 1);

    while (1)
    {
        delay_ms(10);
        t++;

        if (t >= 20)
        {
            LED0_TOGGLE(); /* LED0(RED)闪烁 */
            t = 0;
        }
    }
}

程序流程图
在这里插入图片描述

程序解析

这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。高级定时器驱动源码包括两个文件:atim.c 和atim.h。
首先看atim.h 头文件的几个宏定义:

/* TIMX 输出比较模式定义
 * 这里通过TIM8的输出比较模式,控制PC6,PC7,PC8,PC9输出4路PWM,占空比50%,并且每一路PWM
 * 之间的相位差为25%,修改CCRx可以修改相位. 默认是针对TIM8
 * 注意: 通过修改这些宏定义,可以支持TIM1/TIM8任意一个定时器,任意一个IO口使用输出比较模式,
 * 输出PWM
 */
#define ATIM_TIMX_COMP_CH1_GPIO_PORT GPIOC
#define ATIM_TIMX_COMP_CH1_GPIO_PIN GPIO_PIN_6
#define ATIM_TIMX_COMP_CH1_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* PC口时钟使能*/
#define ATIM_TIMX_COMP_CH2_GPIO_PORT GPIOC
#define ATIM_TIMX_COMP_CH2_GPIO_PIN GPIO_PIN_7
#define ATIM_TIMX_COMP_CH2_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* PC口时钟使能*/
#define ATIM_TIMX_COMP_CH3_GPIO_PORT GPIOC
#define ATIM_TIMX_COMP_CH3_GPIO_PIN GPIO_PIN_8
#define ATIM_TIMX_COMP_CH3_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* PC口时钟使能*/
#define ATIM_TIMX_COMP_CH4_GPIO_PORT GPIOC
#define ATIM_TIMX_COMP_CH4_GPIO_PIN GPIO_PIN_9
#define ATIM_TIMX_COMP_CH3_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* PC口时钟使能*/
#define ATIM_TIMX_COMP TIM8
#define ATIM_TIMX_COMP_CH1_CCRX ATIM_TIMX_COMP->CCR1 /* 通道1的输出比较寄存器*/
#define ATIM_TIMX_COMP_CH2_CCRX ATIM_TIMX_COMP->CCR2 /* 通道2的输出比较寄存器*/
#define ATIM_TIMX_COMP_CH3_CCRX ATIM_TIMX_COMP->CCR3 /* 通道3的输出比较寄存器*/
#define ATIM_TIMX_COMP_CH4_CCRX ATIM_TIMX_COMP->CCR4 /* 通道4的输出比较寄存器*/
#define ATIM_TIMX_COMP_CLK_ENABLE()
do{ __HAL_RCC_TIM8_CLK_ENABLE();}while(0) /* TIM8 时钟使能*/

可以把上面的宏定义分成两部分,第一部分是定时器1 输出通道1~通道4 对应的IO 口的宏定义。第二部分则是定时器8 的相应宏定义。
下面来看到atim.c 文件的程序,首先是高级定时器输出比较模式初始化函数,其定义如下:

/**
* @brief 高级定时器TIMX 输出比较模式初始化函数(使用输出比较模式)
* @note
* 配置高级定时器TIMX 4路输出比较模式PWM输出,实现50%占空比,不同相位控制
* 注意,本例程输出比较模式,每2个计数周期才能完成一个PWM输出,因此输出频率减半
* 另外,我们还可以开启中断在中断里面修改CCRx,从而实现不同频率/不同相位的控制
* 但是我们不推荐这么使用,因为这可能导致非常频繁的中断,从而占用大量CPU资源
*
* 高级定时器的时钟来自APB2, 而PCLK2 = 72Mhz, 我们设置PPRE2不分频, 因此
* 高级定时器时钟= 72Mhz
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
* Ft=定时器工作频率,单位:Mhz
*
* @param arr: 自动重装值。
* @param psc: 时钟预分频数
* @retval 无
*/
void atim_timx_comp_pwm_init(uint16_t arr, uint16_t psc)
{
	TIM_OC_InitTypeDef timx_oc_comp_pwm = {0};
	g_timx_comp_pwm_handle.Instance = ATIM_TIMX_COMP; /* 定时器x */
	g_timx_comp_pwm_handle.Init.Prescaler = psc ; /* 定时器分频*/
	g_timx_comp_pwm_handle.Init.CounterMode = TIM_COUNTERMODE_UP;/* 递增计数*/
	g_timx_comp_pwm_handle.Init.Period = arr; /* 自动重装载值*/
	g_timx_comp_pwm_handle.Init.AutoReloadPreload =
	TIM_AUTORELOAD_PRELOAD_ENABLE; /* 使能影子寄存器TIMx_ARR */
	HAL_TIM_OC_Init(&g_timx_comp_pwm_handle); /* 输出比较模式初始化*/
	timx_oc_comp_pwm.OCMode = TIM_OCMODE_TOGGLE; /* 比较输出模式翻转功能*/
	timx_oc_comp_pwm.Pulse = 250 - 1; /* 设置输出比较寄存器的值*/
	timx_oc_comp_pwm.OCPolarity = TIM_OCPOLARITY_HIGH;/* 输出比较极性为高*/
	HAL_TIM_OC_ConfigChannel(&g_timx_comp_pwm_handle, &timx_oc_comp_pwm,
	TIM_CHANNEL_1); /* 初始化定时器的输出比较通道1 */
	/* CCR1寄存器预装载使能*/
	__HAL_TIM_ENABLE_OCxPRELOAD(&g_timx_comp_pwm_handle, TIM_CHANNEL_1);
	tim_oc_handle.Pulse = 500;
	HAL_TIM_OC_ConfigChannel(&g_timx_comp_pwm_handle, &tim_oc_handle,
	TIM_CHANNEL_2); /* 初始化定时器的输出比较通道2 */
	/* CCR2寄存器预装载使能*/
	__HAL_TIM_ENABLE_OCxPRELOAD(&g_timx_comp_pwm_handle, TIM_CHANNEL_2);
	tim_oc_handle.Pulse = 750;
	HAL_TIM_OC_ConfigChannel(&g_timx_comp_pwm_handle, &tim_oc_handle,
	TIM_CHANNEL_3); /* 初始化定时器的输出比较通道3 */
	/* CCR3寄存器预装载使能*/
	__HAL_TIM_ENABLE_OCxPRELOAD(&g_timx_comp_pwm_handle, TIM_CHANNEL_3);
	tim_oc_handle.Pulse = 1000;
	HAL_TIM_OC_ConfigChannel(&g_timx_comp_pwm_handle, &tim_oc_handle,
	TIM_CHANNEL_4); /* 初始化定时器的输出比较通道4 */
	/* CCR4寄存器预装载使能*/
	__HAL_TIM_ENABLE_OCxPRELOAD(&g_timx_comp_pwm_handle, TIM_CHANNEL_4);
	HAL_TIM_OC_Start(&g_timx_comp_pwm_handle, TIM_CHANNEL_1);
	HAL_TIM_OC_Start(&g_timx_comp_pwm_handle, TIM_CHANNEL_2);
	HAL_TIM_OC_Start(&g_timx_comp_pwm_handle, TIM_CHANNEL_3);
	HAL_TIM_OC_Start(&g_timx_comp_pwm_handle, TIM_CHANNEL_4);
}

在atim_timx_comp_pwm_init 函数中,首先调用HAL_TIM_OC_Init 函数初始化定时器的ARR 和PSC 等参数。然后通过调用函数HAL_TIM_OC_ConfigChannel 设置通道1~ 通道4 的工作参数,包括:输出比较模式功能、输出比较寄存器的值,输出极性等。接着调用__HAL_TIM_ENABLE_OCxPRELOAD 函数宏使能CCR1/2/3/4 寄存器的预装载。最后通过调用函数HAL_TIM_OC_Start 来使能TIM1 通道1~通道4 输出。
HAL_TIM_OC_Init 函数会调用HAL_TIM_OC_MspInit 回调函数,我们把使能定时器和通道对应的IO 时钟、IO 初始化的代码存放到该函数里,其定义如下:

/**
* @brief 定时器底层驱动,时钟使能,引脚配置
此函数会被HAL_TIM_OC_Init()调用
* @param htim:定时器句柄
* @retval 无
*/
void HAL_TIM_OC_MspInit(TIM_HandleTypeDef *htim)
{
	if (htim->Instance == ATIM_TIMX_COMP)
	{
		GPIO_InitTypeDef gpio_init_struct;
		ATIM_TIMX_COMP_CLK_ENABLE();
		ATIM_TIMX_COMP_CH1_GPIO_CLK_ENABLE();
		ATIM_TIMX_COMP_CH2_GPIO_CLK_ENABLE();
		ATIM_TIMX_COMP_CH3_GPIO_CLK_ENABLE();
		ATIM_TIMX_COMP_CH4_GPIO_CLK_ENABLE();
		gpio_init_struct.Pin = ATIM_TIMX_COMP_CH1_GPIO_PIN;
		gpio_init_struct.Mode = GPIO_MODE_AF_PP;
		gpio_init_struct.Pull = GPIO_NOPULL;
		gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
		HAL_GPIO_Init(ATIM_TIMX_COMP_CH1_GPIO_PORT, &gpio_init_struct);
		gpio_init_struct.Pin = ATIM_TIMX_COMP_CH2_GPIO_PIN;
		HAL_GPIO_Init(ATIM_TIMX_COMP_CH2_GPIO_PORT, &gpio_init_struct);
		gpio_init_struct.Pin = ATIM_TIMX_COMP_CH3_GPIO_PIN;
		HAL_GPIO_Init(ATIM_TIMX_COMP_CH3_GPIO_PORT, &gpio_init_struct);
		gpio_init_struct.Pin = ATIM_TIMX_COMP_CH4_GPIO_PIN;
		HAL_GPIO_Init(ATIM_TIMX_COMP_CH4_GPIO_PORT, &gpio_init_struct);
	}
}

该函数主要是使能定时器和通道对应的IO 时钟,初始化IO 口。
在main.c 里面编写如下代码:

int main(void)
{
        uint8_t t = 0;
        HAL_Init(); /* 初始化HAL库*/
        sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
        delay_init(72); /* 延时初始化*/
        usart_init(115200); /* 串口初始化为115200 */
        led_init(); /* 初始化LED */
        atim_timx_comp_pwm_init(1000 - 1, 72 - 1); /* 1Mhz的计数频率1Khz的周期. */
        ATIM_TIMX_COMP_CH1_CCRX = 250 - 1; /* 通道1 相位25% */
        ATIM_TIMX_COMP_CH2_CCRX = 500 - 1; /* 通道2 相位50% */
        ATIM_TIMX_COMP_CH3_CCRX = 750 - 1; /* 通道3 相位75% */
        ATIM_TIMX_COMP_CH4_CCRX = 999 - 1; /* 通道4 相位99% */
        while (1)
        {
                delay_ms(10);
                t++;
                if (t >= 20)
                {
                        LED0_TOGGLE(); /* LED0(RED)闪烁*/
                        t = 0;
                }
        }
}

本小节开头我们讲解了输出比较模式翻转功能如何产生PWM 波,下面结合程序一起计算出PWM 波的周期,频率等参数。
定时器8 时钟源的时钟频率等于APB2 总线时钟频率,即72MHz ,而调用atim_timx_comp_pwm_init(1000 - 1, 72 - 1)初始化函数之后,就相当于写入预分频寄存器的值为71,写入自动重载寄存器的值为999。将这些参数代入本小节介绍的翻转功能输出的PWM 周期计算公式,可得:

T = 2*(arr+1)*((psc+1)/ Tclk) = 2*(999+1)*((71+1)/ 72000000) = 0.002s

由上述式子得到PWM 周期为2ms,频率为500Hz。ARR 值为固定为1000,所以占空比则固定为50%。定时器8 通道1~通道4 输出的PWM 波的相位分别是:25%、50%、75%、100%。

下载验证

下载代码后,可以看到LED0 在闪烁,说明程序已经正常在跑了。我们需要借助示波器观察PC6、PC7、PC8 和PC9 引脚PWM 输出的情况,如下图所示:

在这里插入图片描述

图22.3.4.1 中,由上到下分别是引脚PE9、PE11、PE13 和PE14 输出的PWM,即分别对应的是TIM8_CH1、TIM8_CH2、TIM8_CH3 和TIM8_CH4 输出的相位为25%、50%、75%和100%的PWM。大家可以把其中一个通道的捕获/比较寄存器的值设置为0,那么就可以得到PWM 初相位的波形,即相位为0%。

高级定时器互补输出带死区控制实验

本小节我们来学习使用高级定时器的互补输出带死区控制功能。对于刚接触这个知识的朋友可能会问:什么是互补输出?还带死区控制?What?下面给大家简单说一下。

在这里插入图片描述

上图中,CH1 输出黄色的PWM,它的互补通道CH1N 输出绿色的PWM。通过对比,可以知道这两个PWM 刚好是反过来的,CH1 的PWM 为高电平期间,CH1N 的PWM 则是低电平,反之亦然,这就是互补输出。

下面来看一下什么是带死区控制的互补输出?

在这里插入图片描述

上图中,CH1 输出的PWM 和CH1N 输出的PWM 在高低电平转换间,插入了一段时间才实现互补输出。这段时间称为死区时间,可以通过DTG[7:0]位配置控制死区时间的长度,后面会详细讲解如何配置死区时间。上图中,箭头指出的两段死区时间的长度是一样的,因为都是由同一个死区发生器产生。

理解了互补输出和带死区控制的互补输出,下面来看一下带死区控制的互补输出有什么用?
带死区控制的互补输出经常被用于控制电机的H 桥中,下面给大家画了一个H 桥的简图:

在这里插入图片描述

图22.4.3 是H 桥的简图,实际控制电机正反转的H 桥会根据复杂些,而且更多的是使用MOS 管,这里只是为了解释带死区控制的互补输出在H 桥中的控制逻辑原理,大家理解原理就行。上图的H 桥搭建全部使用的是NPN,并且导通逻辑都是基极为高电平时导通。如果Q1和Q4 三极管导通,那么电机的电流方向是从左到右(假设电机正转);如果Q2 和Q3 三极管导通,那么电机的电流方向是从右到左(假设电机反转)。上述就是H 桥控制电机正反转的逻辑原理。但是同一侧的三极管是不可以同时导通的,否则会短路,比如:Q1 和Q2 同时导通或者Q3 和Q4 同时导通,这都是不可取的。

下面大家想一下图22.4.1 的OC1(CH1)和OC1N(CH1N)输出的PWM 输入到图22.4.3的H 桥中,会怎样?按理来说应该是OC1N 输出高电平的时候,OC1 输出就是低电平,刚好Q2 和Q3 导通,电机的电流方向是从右到左(假设电机反转);反之,OC1 输出高电平的时候,OC1N 输出就是低电平,刚好Q1 和Q4 导通,电机的电流方向是从左到右(假设电机正转),这似乎已经完美解决电机正反转问题了。

实际上,元器件是有延迟特性的,比如:控制信号从OC1 传导至电机,是要经过一定的时间的,复杂的H 桥电路更是如此。由于元器件特性,就会导致直接使用互补输出信号驱动H 桥时存在短路现象。为了避免这种情况,于是就有了带死区控制的互补输出来驱动H 桥电路。如图22.4.2 的死区时间就是为了解决元器件延迟特性的。用户必须根据与输出相连接的器件及其特性(电平转换器的固有延迟、开关器件产生的延迟)来调整死区时间。

在这里插入图片描述

死区时间计算

下面来看一下定时器的死区时间是怎么计算并设置的?死区时间是由TIMx_CR1 寄存器的CKD[1:0]位和TIMx_BDTR 寄存器的DTG[7:0]位来设置,如下图所示:

在这里插入图片描述

死区时间计算分三步走:
第一步:通过CKD[1:0]位确定tDTS。根据CKD[1:0]位的描述,可以得到下面的式子:

在这里插入图片描述
在这里插入图片描述

下面给大家举个例子,假设定时器时钟源频率是72MHz,我们设置CKD[1:0]位的值为2,DTG[7:0]位的值为250。从上面的例子知道CKD[1:0]位的值为2,得到的tDTS=55.56ns。下面来看一下DTG[7:0]位的值为250,应该选择DTG[7:0]位描述中哪条公式?250 的二进制数为11111010,即DTG[7:5]为111,所以选择第四条公式:DT=(32+ DTG[4:0]) * t dtg,其中t dtg = 16 * tDTS。可以看到手册上的式子符号大小写乱乱的,这里大小写不敏感。由手册的公式可以得到DT = (32+ DTG[4:0]) * 16 * tDTS = (32+ 26) * 16 * 55.56ns = 51559.68ns = 51.56us,即死区时间为51.56us。死区时间计算方法就给大家介绍到这里。

关于互补输出和死区插入的更多内容请看《STM32F10xxx 参考手册_V10(中文版).pdf》手册的13.3.11 小节,下面我们介绍相关的寄存器。

在这里插入图片描述

在这里插入图片描述

TIM1/TIM8 寄存器

高级定时器互补输出带死区控制除了用到定时器的时基单元:计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR) 之外。主要还用到以下这些寄存器:
⚫ 控制寄存器1(TIMx_CR1)
TIM1/TIM8 的控制寄存器1 描述如图22.4.1.1 所示:
在这里插入图片描述
上图中我们只列出了本实验需要用的一些位,其中:位7(APRE)用于控制自动重载寄存器是否进行缓冲,在基本定时器的时候已经讲过,请回顾。本实验中,我们把该位置1。
CKD[1:0]位指示定时器时钟(CK_INT)频率与死区发生器以及数字滤波器(ETR、TIx)所使用的死区及采样时钟(tDTS)之间的分频比。我们设置CKD[1:0]位为10,结合高级定时器时钟源频率等于APB2 总线时钟频率,即72MHz,可以得到tDTS=55.56ns。
CEN 位,用于使能计数器的工作,必须要设置该位为1,才可以开始计数。
⚫ 捕获/比较模式寄存器1/2(TIMx_CCMR1/2)
TIM1/TIM8 的捕获/比较模式寄存器(TIMx_CCMR1/2 ),该寄存器一般有2 个:
TIMx_CCMR1 和TIMx CCMR2。TIMx_CCMR1 控制CH1 和CH2,而TIMx_CCMR2 控制CH3和CH4。TIMx_CCMR1 寄存器描述如图22.4.1.2 所示:
在这里插入图片描述
该寄存器的有些位在不同模式下,功能不一样,我们现在用到输出比较模式。关于该寄存器的详细说明,请参考《STM32F10xxx 参考手册_V10(中文版).pdf》第240 页。
本实验我们用到了定时器1 输出比较的通道1,所以我们需要配置TIM1_CCMR1 模式设置位OC1M[2:0],我们使用的是PWM 模式1,所以这3 位必须设置为110。
⚫ 捕获/比较使能寄存器(TIMx
CCER)
TIM1/TIM8 的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。
TIMx_CCER 寄存器描述如图22.4.1.3 所示:
在这里插入图片描述
该寄存器比较简单,要让TIM1 的通道1 输出,我们需要把对应的捕获/比较1 输出使能位CC1E 置1。因为本实验中,我们需要实现互补输出,所以还需要把CC1NE 位置1,使能互补通道输出。CC1P 和CC1NP 分别是通道1 输出和通道1 互补输出的极性设置位。这里我们把CC1P 和CC1NP 位都置1,即输出极性为低,就可以得到互补的PWM。
⚫ 捕获/比较寄存器1/2/3/4(TIMx_CCR1/2/3/4)
捕获/比较寄存器(TIMx_ CCR1/2/3/4),该寄存器总共有4 个,对应4 个通道CH1~CH4。
我们使用的是通道1,所以来看看TIMx_ CCR1 寄存器描述如图22.4.1.4 所示:
在这里插入图片描述
该寄存器16 位有效位,本实验中可以通过改变该寄存器的值来改变PWM 的占空比。
⚫ 断路和死区寄存器(TIMx_ BDTR)
TIM1/TIM8 断路和死区寄存器,该寄存器各位描述如图22.4.1.5 所示:
在这里插入图片描述

该寄存器控制定时器的断路和死区控制的功能。我们先看断路控制,用到断路输入功能(断路输入引脚为PE15),位BKE 置1 即可。

位BKP 选择断路输入信号有效电平。本实验中,我们选择高电平有效,即BKP 置1。
位AOE 是自动输出使能位,如果使能AOE 位,那么在我们输入刹车信号后再断开了刹车信号,互补的PWM 会自动恢复输出,如果失能AOE 位,那么在输入刹车信号后再断开了刹车信号,互补的PWM 就不会恢复输出,而是一直保持刹车信号输入时的状态。为了方便观察,我们使能该位,即置1。

位MOE 是使能主输出,想要高级定时器的通道正常输出,则必须设置MOE 位为1。
最后是DTG[7:0]位,用于设置死区时间,前面已经教过大家怎么设置了。这里以我们例程的设置为例,CKD[1:0] 设置为10,定时器时钟源频率是72MHz,所以tDTS = 55.56ns。
本例程的DTG[7:0]位的值设置为十进制100,即二进制数0110 0100。DTG[7:5]=011,符合第一条式子:DT=DTG[7:0] * t dtg,其中t dtg = tDTS。DT 是死区时间,可以得到DT = 100*55.56 ns = 5. 56us。到后面下载验证小节,我们通过示波器验证一下这个死区时间计算的理论值和实际值是否一样。

硬件设计

  1. 例程功能
    1,利用TIM1_CH1(PE9)输出70%占空比的PWM 波,它的互补输出通道(PE8)则是输出30%占空比的PWM 波。
    2,刹车功能,当给刹车输入引脚(PE15)输入高电平时,进行刹车,即PE8 和PE9 停止输出PWM 波。
    3,LED0 闪烁指示程序运行。
  2. 硬件资源
    1)LED 灯
    LED0 –PB5
    2)定时器1
    TIM1 正常输出通道PE9
    TIM1 互补输出通道PE8
    TIM1 刹车输入PE15
  3. 原理图
    定时器属于STM32F103 的内部资源,只需要软件设置好即可正常工作。我们需要通过示波器观察PE8 和PE9 引脚PWM 输出的情况。还可以通过给PE15 引脚接入高电平进行刹车。

程序设计

定时器的HAL 库驱动

在这里插入图片描述

定时器在HAL 库中的驱动代码在前面已经介绍了部分,这里我们再介绍几个本实验用到的函数。

  1. HAL_TIMEx_ConfigBreakDeadTime 函数
    定时器的断路和死区时间配置初始化函数,其声明如下:
HAL_StatusTypeDef HAL_TIMEx_ConfigBreakDeadTime(TIM_HandleTypeDef *htim,
												TIM_BreakDeadTimeConfigTypeDef *sBreakDeadTimeConfig);

⚫ 函数描述:
用于初始化定时器的断路(即刹车)和死区时间。
⚫ 函数形参:
形参1 是TIM_HandleTypeDef 结构体类型指针变量,基本定时器的时候已经介绍。
形参2 是TIM_BreakDeadTimeConfigTypeDef 结构体类型指针变量,用于配置断路和死区参数,其定义如下:

typedef struct
{
	uint32_t OffStateRunMode; /* 运行模式下的关闭状态选择*/
	uint32_t OffStateIDLEMode; /* 空闲模式下的关闭状态选择*/
	uint32_t LockLevel; /* 寄存器锁定配置*/
	uint32_t DeadTime; /* 死区时间设置*/
	uint32_t BreakState; /* 断路(即刹车)输入使能控制*/
	uint32_t BreakPolarity; /* 断路输入极性*/
	uint32_t BreakFilter; /* 断路输入滤波器*/
	uint32_t AutomaticOutput; /* 自动恢复输出使能控制*/
} TIM_BreakDeadTimeConfigTypeDef;

⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值。
2. HAL_TIMEx_PWMN_Start 函数
定时器的互补输出启动函数。其声明如下:

HAL_StatusTypeDef HAL_TIMEx_PWMN_Start(TIM_HandleTypeDef *htim,
										uint32_t Channel);

⚫ 函数描述:
该函数用于启动定时器的互补输出。
⚫ 函数形参:
形参1 是TIM_HandleTypeDef 结构体类型指针变量,用于配置定时器基本参数。
形参2 是定时器通道,范围:TIM_CHANNEL_1 到TIM_CHANNEL_4。
⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值。

定时器互补输出带死区控制配置步骤

在这里插入图片描述

1)开启TIMx 和通道输出以及刹车输入的GPIO 时钟,配置该IO 口的复用功能输出首先开启TIMx 的时钟,然后配置GPIO 为复用功能输出。本实验我们默认用到定时器1 通道1,对应IO 是PE9,互补输出通道引脚是PE8,刹车输入引脚是PE15,它们的时钟开启方法如下:

__HAL_RCC_TIM1_CLK_ENABLE(); /* 使能定时器1 */
__HAL_RCC_GPIOE_CLK_ENABLE(); /* 开启GPIOE时钟*/

IO 口复用功能是通过函数HAL_GPIO_Init 来配置的。
2)初始化TIMx,设置TIMx 的ARR 和PSC 等参数
这里我们要使用定时器的PWM 模式功能,所以调用的是HAL_TIM_PWM_Init 函数来初始化定时器ARR 和PSC 等参数。注意:本实验要使用该函数配置TIMx_CR1 寄存器的CKD[1:0]位,从而确定t DTS,方便后续设置死区时间。
注意:该函数会调用:HAL_TIM_PWM_MspInit 函数,但是为不跟前面的实验共用该回调函数,提高独立性,我们就直接在atim_timx_cplm_pwm_init 函数中,使能定时器时钟和GPIO时钟,初始化通道对应IO 引脚等。
3)设置定时器为PWM 模式,输出比较极性,互补输出极性等参数
通过HAL_TIM_PWM_ConfigChannel 函数来设置定时器为PWM1 模式,根据需求设置OCy输出极性和OCyN 互补输出极性等。
4)设置死区参数
通过HAL_TIMEx_ConfigBreakDeadTime 函数来设置死区参数,比如:设置死区时间、运行模式的关闭输出状态、空闲模式的关闭输出状态、刹车输入有效信号极性和是否允许刹车后自动恢复输出等。
5)启动Ocy 输出以及OCyN 互补输出
通过HAL_TIM_PWM_Start 函数启动OCy 输出,通过HAL_TIMEx_PWMN_Start 函数启动启动OCyN 互补输出。

程序流程图

在这里插入图片描述

课堂源码

atim.h

#ifndef __ATIM_H
#define __ATIM_H

#include "./SYSTEM/sys/sys.h"


void atim_timx_cplm_pwm_init(uint16_t arr, uint16_t psc);
void atim_timx_cplm_pwm_set(uint16_t ccr, uint8_t dtg);

#endif

atim.c

#include "./BSP/TIMER/atim.h"


TIM_HandleTypeDef g_timx_cplm_pwm_handle;                                  /* 定时器x句柄 */
TIM_BreakDeadTimeConfigTypeDef g_sbreak_dead_time_config;                  /* 死区时间设置 */

/* 高级定时器 互补输出 初始化函数(使用PWM模式1) */
void atim_timx_cplm_pwm_init(uint16_t arr, uint16_t psc)
{
    TIM_OC_InitTypeDef tim_oc_cplm_pwm = {0};

    g_timx_cplm_pwm_handle.Instance = TIM1;                                 /* 定时器x */
    g_timx_cplm_pwm_handle.Init.Prescaler = psc;                            /* 定时器预分频系数 */
    g_timx_cplm_pwm_handle.Init.CounterMode = TIM_COUNTERMODE_UP;           /* 递增计数模式 */
    g_timx_cplm_pwm_handle.Init.Period = arr;                               /* 自动重装载值 */
    g_timx_cplm_pwm_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV4;     /* CKD[1:0] = 10, tDTS = 4 * tCK_INT = Ft / 4 = 18Mhz */
    HAL_TIM_PWM_Init(&g_timx_cplm_pwm_handle);

    tim_oc_cplm_pwm.OCMode = TIM_OCMODE_PWM1;                               /* PWM模式1 */
    tim_oc_cplm_pwm.OCPolarity = TIM_OCPOLARITY_HIGH;                       /* OCy 高电平有效 */
    tim_oc_cplm_pwm.OCNPolarity = TIM_OCNPOLARITY_HIGH;                     /* OCyN 高电平有效 */
    tim_oc_cplm_pwm.OCIdleState = TIM_OCIDLESTATE_RESET;                    /* 当MOE=0,OCx=0 */
    tim_oc_cplm_pwm.OCNIdleState = TIM_OCNIDLESTATE_RESET;                  /* 当MOE=0,OCxN=0 */
    HAL_TIM_PWM_ConfigChannel(&g_timx_cplm_pwm_handle, &tim_oc_cplm_pwm, TIM_CHANNEL_1);

    /* 设置死区参数,开启死区中断 */
    g_sbreak_dead_time_config.OffStateRunMode = TIM_OSSR_DISABLE;           /* 运行模式的关闭输出状态 */
    g_sbreak_dead_time_config.OffStateIDLEMode = TIM_OSSI_DISABLE;          /* 空闲模式的关闭输出状态 */
    g_sbreak_dead_time_config.LockLevel = TIM_LOCKLEVEL_OFF;                /* 不用寄存器锁功能 */
    g_sbreak_dead_time_config.BreakState = TIM_BREAK_ENABLE;                /* 使能刹车输入 */
    g_sbreak_dead_time_config.BreakPolarity = TIM_BREAKPOLARITY_HIGH;       /* 刹车输入有效信号极性为高 */
    g_sbreak_dead_time_config.AutomaticOutput = TIM_AUTOMATICOUTPUT_ENABLE; /* 使能AOE位,允许刹车结束后自动恢复输出 */
    HAL_TIMEx_ConfigBreakDeadTime(&g_timx_cplm_pwm_handle, &g_sbreak_dead_time_config);

    HAL_TIM_PWM_Start(&g_timx_cplm_pwm_handle, TIM_CHANNEL_1);              /* OCy 输出使能 */
    HAL_TIMEx_PWMN_Start(&g_timx_cplm_pwm_handle,TIM_CHANNEL_1);            /* OCyN 输出使能 */
}


/* 定时器 PWM输出 MSP初始化函数 */
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim) 
{
    if (htim->Instance == TIM1)
    {
        GPIO_InitTypeDef gpio_init_struct = {0};

        __HAL_RCC_TIM1_CLK_ENABLE();
        __HAL_RCC_GPIOE_CLK_ENABLE();

        gpio_init_struct.Pin = GPIO_PIN_9;
        gpio_init_struct.Mode = GPIO_MODE_AF_PP; 
        gpio_init_struct.Pull = GPIO_PULLDOWN;
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH ;
        HAL_GPIO_Init(GPIOE, &gpio_init_struct);

        gpio_init_struct.Pin = GPIO_PIN_8;
        HAL_GPIO_Init(GPIOE, &gpio_init_struct);

        gpio_init_struct.Pin = GPIO_PIN_15;
        HAL_GPIO_Init(GPIOE, &gpio_init_struct);

        __HAL_RCC_AFIO_CLK_ENABLE();	//端口重映射
        __HAL_AFIO_REMAP_TIM1_ENABLE();
    }
}

/**
 * @brief       定时器TIMX 设置输出比较值 & 死区时间
 * @param       ccr: 输出比较值
 * @param       dtg: 死区时间
 *   @arg       dtg[7:5]=0xx时, 死区时间 = dtg[7:0] * tDTS
 *   @arg       dtg[7:5]=10x时, 死区时间 = (64 + dtg[6:0]) * 2  * tDTS
 *   @arg       dtg[7:5]=110时, 死区时间 = (32 + dtg[5:0]) * 8  * tDTS
 *   @arg       dtg[7:5]=111时, 死区时间 = (32 + dtg[5:0]) * 16 * tDTS
 *   @note      tDTS = 1 / (Ft /  CKD[1:0]) = 1 / 18M = 55.56ns
 * @retval      无
 */
void atim_timx_cplm_pwm_set(uint16_t ccr, uint8_t dtg)
{
    __HAL_TIM_SET_COMPARE(&g_timx_cplm_pwm_handle, TIM_CHANNEL_1, ccr);
    g_sbreak_dead_time_config.DeadTime = dtg;
    HAL_TIMEx_ConfigBreakDeadTime(&g_timx_cplm_pwm_handle, &g_sbreak_dead_time_config);
}

main.c

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/KEY/key.h"
#include "./BSP/TIMER/atim.h"


int main(void)
{
    uint8_t t = 0;

    HAL_Init();                                /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);        /* 设置时钟, 72Mhz */
    delay_init(72);                            /* 延时初始化 */
    usart_init(115200);                        /* 串口初始化为115200 */
    led_init();                                /* 初始化LED */
    atim_timx_cplm_pwm_init(1000 - 1, 72 - 1);
    atim_timx_cplm_pwm_set(700 - 1, 100);
    
    while (1)
    {
        delay_ms(10);
        t++;

        if (t >= 20)
        {
            LED0_TOGGLE(); /* LED0(RED)闪烁 */
            t = 0;
        }
    }
}

死区功能和刹车功能实验验证现象参照下面的图。

程序解析

这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。高级定时器驱动源码包括两个文件:atim.c 和atim.h。
首先看atim.h 头文件的几个宏定义:

/*****************************************************************************/
/* TIMX 互补输出模式定义
* 这里设置互补输出相关硬件配置, CHY即正常输出, CHYN即互补输出
* 修改CCRx可以修改占空比.
* 默认是针对TIM1
* 注意: 通过修改这些宏定义,可以支持TIM1/TIM8定时器, 任意一个IO口输出互补PWM(前提是必须有互补输出功能)
*/
/* 输出通道引脚*/
#define ATIM_TIMX_CPLM_CHY_GPIO_PORT GPIOE
#define ATIM_TIMX_CPLM_CHY_GPIO_PIN GPIO_PIN_9
#define ATIM_TIMX_CPLM_CHY_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0) /* PE口时钟使能*/
/* 互补输出通道引脚*/
#define ATIM_TIMX_CPLM_CHYN_GPIO_PORT GPIOE
#define ATIM_TIMX_CPLM_CHYN_GPIO_PIN GPIO_PIN_8
#define ATIM_TIMX_CPLM_CHYN_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0) /* PE口时钟使能*/
/* 刹车输入引脚*/
#define ATIM_TIMX_CPLM_BKIN_GPIO_PORT GPIOE
#define ATIM_TIMX_CPLM_BKIN_GPIO_PIN GPIO_PIN_15
#define ATIM_TIMX_CPLM_BKIN_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0) /* PE口时钟使能*/
/* TIMX REMAP设置
* 因为PE8/PE9/PE15, 默认并不是TIM1的复用功能脚, 必须开启完全重映射, 才可以将:
TIM1_CH1->PE9; TIM1_CH1N->PE8; TIM1_BKIN->PE15;
* 这样, PE8/PE9/PE15, 才能用作TIM1的CH1N/CH1/BKIN功能.
* 所以必须实现ATIM_TIMX_CPLM_CHYN_GPIO_REMAP, 通过sys_gpio_remap_set函数设置重映射
* 如果我们使用默认的复用功能输出, 则不用设置重映射, 是可以不需要该函数的! 根据具体需要来实现.
*/
#define ATIM_TIMX_CPLM_CHYN_GPIO_REMAP() do{__HAL_RCC_AFIO_CLK_ENABLE();\
__HAL_AFIO_REMAP_TIM1_ENABLE();\
}while(0)
/* 互补输出使用的定时器*/
#define ATIM_TIMX_CPLM TIM1
#define ATIM_TIMX_CPLM_CHY TIM_CHANNEL_1
#define ATIM_TIMX_CPLM_CHY_CCRY ATIM_TIMX_CPLM->CCR1
#define ATIM_TIMX_CPLM_CLK_ENABLE()
do{ __HAL_RCC_TIM1_CLK_ENABLE(); }while(0) /* TIM1 时钟使能*/
/*****************************************************************************/

可以把上面的宏定义分成两部分,第一部分包括是定时器1 输出、互补输出和刹车输入通道对应的IO 口的宏定义,第二部分则是定时器1 的相应宏定义。注意:因为PE8/PE9/PE15, 默认并不是TIM1 的复用功能脚, 必须开启完全重映射,具体请参考《STM32F10xxx 参考手册_V10(中文版).pdf》第123 页,AFIO_MAPR 寄存器的描述。
下面来看atim.c 文件的程序,首先是高级定时器互补输出初始化函数,其定义如下:

/**
* @brief 高级定时器TIMX 互补输出初始化函数(使用PWM模式1)
* @note
* 配置高级定时器TIMX 互补输出, 一路OCy 一路OCyN, 并且可以设置死区时间
*
* 高级定时器的时钟来自APB2, 而PCLK2 = 72Mhz, 我们设置PPRE2不分频, 因此
* 高级定时器时钟= 72Mhz
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
* Ft=定时器工作频率,单位:Mhz
* @param arr: 自动重装值。
* @param psc: 时钟预分频数
* @retval 无
*/
void atim_timx_cplm_pwm_init(uint16_t arr, uint16_t psc)
{
	GPIO_InitTypeDef gpio_init_struct = {0};
	TIM_OC_InitTypeDef tim_oc_cplm_pwm = {0};
	ATIM_TIMX_CPLM_CLK_ENABLE(); /* TIMx 时钟使能*/
	ATIM_TIMX_CPLM_CHY_GPIO_CLK_ENABLE(); /* 通道X对应IO口时钟使能*/
	ATIM_TIMX_CPLM_CHYN_GPIO_CLK_ENABLE(); /* 通道X互补通道对应IO口时钟使能*/
	ATIM_TIMX_CPLM_BKIN_GPIO_CLK_ENABLE(); /* 通道X刹车输入对应IO口时钟使能*/
	gpio_init_struct.Pin = ATIM_TIMX_CPLM_CHY_GPIO_PIN;
	gpio_init_struct.Mode = GPIO_MODE_AF_PP;
	gpio_init_struct.Pull = GPIO_PULLUP;
	gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH ;
	HAL_GPIO_Init(ATIM_TIMX_CPLM_CHY_GPIO_PORT, &gpio_init_struct);
	gpio_init_struct.Pin = ATIM_TIMX_CPLM_CHYN_GPIO_PIN;
	HAL_GPIO_Init(ATIM_TIMX_CPLM_CHYN_GPIO_PORT, &gpio_init_struct);
	gpio_init_struct.Pin = ATIM_TIMX_CPLM_BKIN_GPIO_PIN;
	HAL_GPIO_Init(ATIM_TIMX_CPLM_BKIN_GPIO_PORT, &gpio_init_struct);
	ATIM_TIMX_CPLM_CHYN_GPIO_REMAP(); /* 重映射定时器IO */
	g_timx_cplm_pwm_handle.Instance = ATIM_TIMX_CPLM; /* 定时器x */
	g_timx_cplm_pwm_handle.Init.Prescaler = psc; /* 定时器预分频系数*/
	g_timx_cplm_pwm_handle.Init.CounterMode = TIM_COUNTERMODE_UP;/* 递增计数*/
	g_timx_cplm_pwm_handle.Init.Period = arr; /* 自动重装载值*/
	/* CKD[1:0] = 10, tDTS = 4 * tCK_INT = Ft / 4 = 18Mhz */
	g_timx_cplm_pwm_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV4;
	g_timx_cplm_pwm_handle.Init.AutoReloadPreload =
	TIM_AUTORELOAD_PRELOAD_ENABLE; /* 使能影子寄存器TIMx_ARR */
	HAL_TIM_PWM_Init(&g_timx_cplm_pwm_handle);
	tim_oc_cplm_pwm.OCMode = TIM_OCMODE_PWM1; /* PWM模式1 */
	tim_oc_cplm_pwm.OCPolarity = TIM_OCPOLARITY_LOW; /* OCy 低电平有效*/
	tim_oc_cplm_pwm.OCNPolarity = TIM_OCNPOLARITY_LOW; /* OCyN 低电平有效*/
	tim_oc_cplm_pwm.OCIdleState = TIM_OCIDLESTATE_SET; /* 当MOE=0,OCx=1 */
	tim_oc_cplm_pwm.OCNIdleState = TIM_OCNIDLESTATE_SET;/* 当MOE=0,OCxN=1 */
	HAL_TIM_PWM_ConfigChannel(&g_timx_cplm_pwm_handle, &tim_oc_cplm_pwm,
	ATIM_TIMX_CPLM_CHY);
	/* 设置死区参数,开启死区中断*/
	/* 运行模式的关闭输出状态*/
	g_sbreak_dead_time_config.OffStateRunMode = TIM_OSSR_DISABLE;
	/* 空闲模式的关闭输出状态*/
	g_sbreak_dead_time_config.OffStateIDLEMode = TIM_OSSI_DISABLE;
	g_sbreak_dead_time_config.LockLevel = TIM_LOCKLEVEL_OFF;/* 不用寄存器锁功能*/
	g_sbreak_dead_time_config.BreakState = TIM_BREAK_ENABLE;/* 使能刹车输入*/
	/* 刹车输入有效信号极性为高*/
	g_sbreak_dead_time_config.BreakPolarity = TIM_BREAKPOLARITY_HIGH;
	/* 使能AOE位,允许刹车结束后自动恢复输出*/
	g_sbreak_dead_time_config.AutomaticOutput = TIM_AUTOMATICOUTPUT_ENABLE;
	HAL_TIMEx_ConfigBreakDeadTime(&g_timx_cplm_pwm_handle,
	&g_sbreak_dead_time_config);
	/* 使能OCy输出*/
	HAL_TIM_PWM_Start(&g_timx_cplm_pwm_handle, ATIM_TIMX_CPLM_CHY);
	/* 使能OCyN输出*/
	HAL_TIMEx_PWMN_Start(&g_timx_cplm_pwm_handle, ATIM_TIMX_CPLM_CHY);
}

在atim_timx_cplm_pwm_init 函数中,没有使用HAL 库的MSP 回调,而是把相关的初始化都写到该函数里面。
第一部分,使能定时器和相关通道对应的GPIO 时钟,以及初始化相关IO 引脚。
第二部分,通过HAL_TIM_PWM_Init 函数初始化定时器的ARR 和PSC 等参数。
第三部分,通过HAL_TIM_PWM_ConfigChannel 函数设置PWM 模式1、输出极性,以及输出空闲状态等。
第四部分,通过HAL_TIMEx_ConfigBreakDeadTime 函数配置断路功能。
最后一定记得要调用HAL_TIM_PWM_Start 函数和HAL_TIMEx_PWMN_Start 函数启动通道输出和互补通道输出。
为了方便,我们还定义了设置输出比较值和死区时间的函数,其定义如下:

/**
* @brief 定时器TIMX 设置输出比较值& 死区时间
* @param ccr: 输出比较值
* @param dtg: 死区时间
* @arg dtg[7:5]=0xx时, 死区时间= dtg[7:0] * tDTS
* @arg dtg[7:5]=10x时, 死区时间= (64 + dtg[6:0]) * 2 * tDTS
* @arg dtg[7:5]=110时, 死区时间= (32 + dtg[5:0]) * 8 * tDTS
* @arg dtg[7:5]=111时, 死区时间= (32 + dtg[5:0]) * 16 * tDTS
* @note tDTS = 1 / (Ft / CKD[1:0]) = 1 / 18M = 55.56ns
* @retval 无
*/
void atim_timx_cplm_pwm_set(uint16_t ccr, uint8_t dtg)
{
	g_sbreak_dead_time_config.DeadTime = dtg; /* 死区时间设置*/
	HAL_TIMEx_ConfigBreakDeadTime(&g_timx_cplm_pwm_handle,
	&g_sbreak_dead_time_config); /*重设死区时间*/
	__HAL_TIM_MOE_ENABLE(&g_timx_cplm_pwm_handle); /* MOE=1,使能主输出*/
	ATIM_TIMX_CPLM_CHY_CCRY = ccr; /* 设置比较寄存器*/
}

通过重新调用HAL_TIMEx_ConfigBreakDeadTime 函数设置死区时间,注意这里的g_sbreak_dead_time_config 是全局结构体变量,在atim_timx_cplm_pwm_init 函数已经初始化其他结构体成员了,这里只是对DeadTime 成员(死区时间)配置。死区时间的计算方法前面已经讲解过,这里只要把要设置的DTG[7:0]值,通过dtg 形参赋值给DeadTime 结构体成员就行。
另外一个形参是ccr,用于设置捕获/比较寄存器的值,即控制PWM 的占空比。
在main.c 里面编写如下代码:

int main(void)
{
        uint8_t t = 0;
        HAL_Init(); /* 初始化HAL库*/
        sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
        delay_init(72); /* 延时初始化*/
        usart_init(115200); /* 串口初始化为115200 */
        led_init(); /* 初始化LED */
        atim_timx_cplm_pwm_init(1000 - 1, 72 - 1); /* 1Mhz的计数频率1Khz的周期. */
        atim_timx_cplm_pwm_set(300, 100); /* 占空比:7:3,死区时间100 * tDTS */
        while (1)
        {
                delay_ms(10);
                t++;
                if (t >= 20)
                {
                        LED0_TOGGLE(); /* LED0(RED)闪烁*/
                        t = 0;
                }
        }
}

先看atim_timx_cplm_pwm_init(1000 - 1, 72 - 1)这个语句,这两个形参分别设置自动重载寄存器的值为999,以及定时器预分频器寄存器的值为71。先看预分频系数,我们设置为72 分频,定时器1 的时钟源频率等于APB2 总线时钟频率,即72MHz,可以得到计数器的计数频率是1MHz,即每1us 计数一次。再到自动重载寄存器的值为999 决定的是PWM 的频率(周期),可以得到PWM 的周期为(999+1) * 1us = 1000us = 1ms。边沿对齐模式下,使用PWM 模式1 或者PWM 模式2,得到的PWM 周期是定时器溢出时间。这里的1ms,也可以直接通过定时器溢出时间计算公式Tout= ((arr+1) * (psc+1))/Tclk 得到。

调用atim_timx_cplm_pwm_set(300, 100) 这个语句,相当于设置捕获/比较寄存器的值为300,DTG[7:0]的值为100。通过计算可以得到PWM 的占空比为70%,死区时间为5.56us。根据PWM生成原理分析,再结合图21.3.2 产生PWM 示意图,以及我们在atim_timx_cplm_pwm_init 函数配置PWM 模式1、OCy 输出极性为低,占空比的计算很简单,可以由(1000-300)/1000 得到。
关于死区时间的计算方法,前面已经讲解过,这里以DTG[7:0]的值为100 为例,再来讲解一遍计算过程。由前面讲解的内容知道,我们例程配置CKD[1:0]位的值为2,可以得到tDTS= 55.56ns。
基于这个前提,通过改变DTG[7:0]的值,可以得到不同的死区时间。这里我们配置DTG[7:0]的值为100,即二进制数0110 0100,符合第一种情况dtg[7:5]=0xx 时,死区时间DT = DTG [7:0] * tDTS。可以得到死区时间DT = 100*55.56 ns = 5.56us。

下面我们下载到开发板子验证一下。

下载验证

下载代码后,可以看到LED0 在闪烁,说明程序已经正常在跑了。我们需要借助示波器观察PE9 正常输出和PE8 互补输出PWM 的情况,示波器显示截图如图22.4.4.1 所示:

在这里插入图片描述

图22.4.4.1 中的由上到下分别是PE9 输出70%占空比的PWM 波和PE8 互补输出30% 占空比的PWM 波。互补输出的PWM 波的正脉宽减去正常的PWM 的负脉宽的值除以2 就是死区时间,也可以是正常的PWM 的正脉宽减去互补输出的PWM 波的负脉宽的值除以2。我们使用第一种方法得到:死区时间=(705 –694)/2 us=5.5us(上图示波器两个Wid值)。与我们理论到的的值5.56us 基本一样,这样的误差是正常的。

要是不相信,我们再举个例子,我们把调用的函数改为atim_timx_cplm_pwm_set(300, 250),即配置DTG[7:0]的值为250,这个例子的计算过程在本实验前面死区时间计算的内容讲过,这里就不再赘述。经过计算得到死区时间DT =51.56us。修改好后,示波器显示截图如下图所示:

在这里插入图片描述

由图22.4.4.2 可得到,死区时间=(751–648)/2 us= 51.5us。与我们理论到的的值51.56us也是差不多的,误差在正常范围。由此证明我们的死区时间设置是没有问题。

刹车功能验证:当给刹车输入引脚(PE15)接入高电平(这里直接用杜邦线连接PE15 到3.3V)时,就会进行刹车,即PE9 和PE8 停止输出PWM 波,如图22.4.4.3 所示:
刹车功能验证:当给刹车输入引脚(PE15)接入高电平(这里直接用杜邦线把PE15 连接到3.3V)时,就会进行刹车,MOE 位被硬件清零。由《STM32F10xxx 参考手册_V10(中文版).pdf》第245 页表75 可以知道刹车信号输入后,如果存在时钟,经过死区后OCx=OISx 且OCxN=OISxN。在atim_timx_cplm_pwm_init 函数中,我们设置当MOE=0 时,OCx=1、OCxN=1,即PE9 和PE8 都是输出高电平。下面通过示波器来验证一下,如图刹车后的输出情况所示:

在这里插入图片描述

从上图可以看到PE9 和PE8 输出的都是高电平,符合我们预期的设置。

另外因为我们使能了AOE 位(即把该位置1),如果刹车输入为无效极性时,MOE 位在发生下一个更新事件时自动置1,恢复运行模式(即继续输出PWM)。因此当停止给PE15 接入低电平(拔掉之前连接的杜邦线),PWM 会自动恢复输出

高级定时器PWM输入模式实验(测量周期/频率/占空比)

本小节我们来学习使用高级定时器PWM 输入模式,此模式是输入捕获模式的一个特例。
PWM 输入模式经常被应用于测量PWM 脉宽和频率。PWM 输入模式在《STM32F10xxx 参考手册_V10(中文版).pdf》手册216 页有详细的文字描述。下面我们结合这些文字,配合高级定时器框图给大家介绍PWM 输入的工作原理。

在这里插入图片描述

第一,确定定时器时钟源。本实验中我们使用内部时钟(CK_INT),F1 系列高级定时器挂载在APB2 总线上,按照sys_stm32_clock_init 函数的配置,定时器时钟频率等于APB2 总线时钟频率,即72MHz。计数器的计数频率确定了测量的精度。

第二,确定PWM 输入的通道。PWM 输入模式下测量PWM,PWM 信号输入只能从通道1(CH1)或者通道2(CH2)输入。

第三,确定IC1 和IC2 的捕获边沿。这里以通道1(CH1)输入PWM 为例,一般我们习惯设置IC1 捕获边沿为上升沿捕获,IC2 捕获边沿为下降沿捕获。

第四,选择触发输入信号(TRGI)。这里也是以通道1(CH1)输入PWM 为例,那么我们就应该选择TI1FP1 为触发输入信号。如果是通道2(CH2)输入PWM,那就选择TI2FP2 为触发输入信号。可以看到这里并没有对应通道3(CH3)或者通道4(CH4)的触发输入信号,所以我们只选择通道1 或者通道2 作为PWM 输入的通道。

第五,从模式选择:复位模式。复位模式的作用是:在出现所选触发输入(TRGI) 上升沿时,重新初始化计数器并生成一个寄存器更新事件(也就是从0开始计数)。

第六,读取一个PWM 周期内计数器的计数个数,以及高电平期间的计数个数,再结合计数器的计数周期(即计一个数的时间),最终通过计算得到输入的PWM 周期和占空比等参数。

以通道1(CH1)输入PWM,设置IC1 捕获边沿为上升沿捕获,IC2 捕获边沿为下降沿捕获为例,那么CCR1 寄存器的值+1 就是PWM 周期内计数器的计数个数,CCR2 寄存器的值+1 就是PWM 高电平期间计数器的计数个数。通过这两个值就可以计算出PWM 的周期或者占空比等参数。

再举个例子,以通道1(CH1)输入PWM,设置IC1 捕获边沿为下降沿捕获,IC2 捕获边沿为上升沿捕获为例,那么CCR1 寄存器的值+1 依然是PWM 周期内计数器的计数个数,但是CCR2 寄存器的值+1 就是PWM 低电平期间计数器的计数个数。通过这两个得到的参数依然可以计算出PWM 的其它参数。这个大家了解一下就可以了,一般我们使用第六介绍的例子。

通过上面的描述,如果大家还不理解,下面我们结合PWM 输入模式时序来分析一下。PWM输入模式时序图如图22.5.2 所示:

在这里插入图片描述

图22.5.2 是以通道1(CH1)输入PWM,设置IC1 捕获边沿为上升沿捕获,IC2 捕获边沿为下降沿捕获为例的PWM 输入模式时序图。

从时序图可以看出,计数器的计数模式是递增计数模式。从左边开始看,当TI1 来了上升沿时,计数器的值被复位为0(原因是从模式选择为复位模式),IC1 和IC2 都发生捕获事件。

然后计数器的值计数到2 的时候,IC2 发生了下降沿捕获,捕获事件会导致这时候的计数器的值被锁存到CCR2 寄存器中,该值+1 就是高电平期间计数器的计数个数。最后计数器的值计数到4 的时候,IC1 发生了上升沿捕获,捕获事件会导致这时候的计数器的值被锁存到CCR1 寄存器中,该值+1 就是PWM 周期内计数器的计数个数。

假设计数器的计数频率是72MHz,那我们就可以计算出这个PWM 的周期、频率和占空比等参数了。下面就以这个为例给大家计算一下。由计数器的计数频率为72MHz,可以得到计数器计一个数的时间是13.8ns(即测量的精度是13.8ns)。知道了测量精度,再来计算PWM 的周期,PWM 周期=(4+1)*(1/72000000) = 69.4ns,那么PWM 的频率就是14.4MHz。占空比= (2+1)/(4+1) =3/5(即占空比为60%)。

TIM1/TIM8 寄存器

高级定时器PWM 输入模式实验除了用到定时器的时基单元:计数器寄存器(TIMx_CNT)、预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR) 之外。主要还用到以下这些寄存器:
⚫ 从模式控制寄存器(TIMx_SMCR)
TIM1/TIM8 的从模式控制寄存器描述如图22.5.1.1 所示:
在这里插入图片描述
该寄存器的SMS[2:0]位,用于从模式选择。比如在本实验中我们需要用到复位模式,所以设置SMS[2:0]=100。TS[2:0]位是触发选择,我们设置为滤波后的定时器输入1 (TI1FP1),即TS[2:0]为101。
⚫ 捕获/比较模式寄存器1/2(TIMx_CCMR1/2)
TIM1/TIM8 的捕获/比较模式寄存器(TIMx_CCMR1/2 ),该寄存器一般有2 个:
TIMx_CCMR1 和TIMx CCMR2。TIMx_CCMR1 控制CH1 和CH2,而TIMx_CCMR2 控制CH3和CH4。TIMx_CCMR1 寄存器描述如图22.5.1.2 所示:
在这里插入图片描述
该寄存器的有些位在不同模式下,功能不一样,我们现在用到输入捕获模式。关于该寄存
器的详细说明,请参考《STM32F10xxx 参考手册_V10(中文版).pdf》第240 页,13.4.7 节。
本实验我们通过定时器1 通道1 输入PWM 信号,所以IC1 和IC2 都映射到TI1 上。配置
CC1S[1:0]=01、CC2S [1:0]=10,其他位不用设置,默认为0 即可。
⚫ 捕获/比较使能寄存器(TIMx
CCER)
TIM1/TIM8 的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。
TIMx_CCER 寄存器描述如图22.5.1.3 所示:
在这里插入图片描述
IC1 捕获上升沿,所以CC1P 位置0,即捕获发生在IC1 的上升沿。IC2 捕获下降沿,所以CC2P 位置1,即捕获发生在IC1 的下降沿。设置好捕获边沿后,还需要使能这两个通道捕获,即CC1E 和CC2E 位置1。
⚫ 捕获/比较寄存器1/2/3/4(TIMx_CCR1/2/3/4)
捕获/比较寄存器(TIMx_ CCR1/2/3/4),该寄存器总共有4 个,对应4 个通道CH1~CH4。
我们使用的是通道1,所以来看看TIMx_ CCR1 寄存器描述如图22.5.1.4 所示:
在这里插入图片描述
本实验中,CCR1 寄存器用于获取PWM 周期内计数器的计数个数。CCR2 寄存器用于获取PWM 高电平期间计数器的计数个数。
⚫ DMA/中断使能寄存器(TIMx_DIER)
DMA/中断使能寄存器描述如图22.5.1.5 所示:
在这里插入图片描述
该寄存器位0(UIE)用于使能或者禁止更新中断,因为本实验我们用到更新中断,所以该位需要置1。位1(CC1IE)用于使能或者禁止捕获/比较1 中断,我们用到捕获中断,所以该位需要置1。

硬件设计

  1. 例程功能
    首先通过TIM3_CH2(PB5)输出PWM 波。然后把PB5 输出的PWM 波用杜邦线接入PC6(定时器8 通道1),最后通过串口打印PWM 波的脉宽和频率等信息。通过LED1 闪烁来提示程序正在运行。
  2. 硬件资源
    1)LED 灯
    LED0 –PB5
    2)定时器3 通道2(PB5)输出PWM 波
    定时器8 通道1(PC6)输入PWM 波
  3. 原理图
    定时器属于STM32F103 的内部资源,只需要软件设置好即可正常工作。我们把PB5 引脚输出的PWM 波用杜邦线接入PC6 引脚,然后通过电脑串口上位机软件观察打印出来的信息。

程序设计

定时器PWM 输入模式实验用到的HAL 库中的驱动代码在前面实验都有介绍过了。我们在程序解析再详细讲解应用到的函数,下面介绍一下高级定时器PWM 输入模式的配置步骤。

高级定时器PWM 输入模式配置步骤
在这里插入图片描述
在这里插入图片描述

1)开启TIMx 和输入通道的GPIO 时钟,配置该IO 口的复用功能输入。
首先开启TIMx 的时钟,然后配置GPIO 为复用功能输出。本实验我们默认用到定时器8 通道1,对应IO 是PC6,它们的时钟开启方法如下:

__HAL_RCC_TIM8_CLK_ENABLE(); /* 使能定时器8 */
__HAL_RCC_GPIOC_CLK_ENABLE(); /* 开启GPIOC时钟*/

IO 口复用功能是通过函数HAL_GPIO_Init 来配置的。
2)初始化TIMx,设置TIMx 的ARR 和PSC 等参数。
使用定时器的输入捕获功能时,我们调用的是HAL_TIM_IC_Init 函数来初始化定时器ARR和PSC 等参数。
注意:该函数会调用:HAL_TIM_IC_MspInit 函数,但是为不跟前面的实验共用该回调函数,提高独立性,我们就直接在atim_timx_pwmin_chy_init 函数中,使能定时器时钟和GPIO 时钟,初始化通道对应IO 引脚等。
3)从模式配置,IT1 触发更新
通过HAL_TIM_SlaveConfigSynchronization 函数,配置从模式:复位模式、定时器输入触发源、边缘检测、是否滤波等。
4)设置IC1 捕获相关参数
通过HAL_TIM_IC_ConfigChannel 函数来设置定时器捕获通道1 的工作方式,包括边缘检测极性、映射关系,输入滤波和输入分频等。
5)设置IC2 捕获相关参数
通过HAL_TIM_IC_ConfigChannel 函数来设置定时器捕获通道2 的工作方式,包括边缘检测极性、映射关系,输入滤波和输入分频等。
6)使能定时器更新中断,开启捕获功能,配置定时器中断优先级
通过__HAL_TIM_ENABLE_IT 函数使能定时器更新中断。
通过HAL_TIM_IC_Start_IT 函数使能定时器并开启通道1 或者通道2 的捕获功能,使能捕获中断。
通过HAL_NVIC_EnableIRQ 函数使能定时器中断。

通过HAL_NVIC_SetPriority 函数设置中断优先级。

7)编写中断服务函数
TIM1 和TIM8 有独立的输入捕获中断服务函数,分别是TIM1_CC_IRQHandler 和TIM8_CC_IRQHandler,其他定时器则没有,所以如果是TIM1 和TIM8 可以直接使用输入捕获中断服务函数来处理输入捕获中断。在使用TIM1 的时候,如果要考虑定时器1 溢出,可以重定义更新中断服务函数TIM1_UP_IRQHandler。如果使用HAL 库的中断回调机制,可以在相关中断服务函数中直接调用定时器中断公共处理函数HAL_TIM_IRQHandler,然后我们直接重定义相关的中断回调函数来编写中断程序即可。本实验为了兼容性,我们自定义一个中断处理函数atim_timx_pwmin_chy_process,里面包含了捕获中断和更新中断的处理,具体看源码。

课堂源码

在这里插入图片描述
通用定时器部分:
gtim.h

#ifndef __GTIM_H
#define __GTIM_H

#include "./SYSTEM/sys/sys.h"


void gtim_timx_pwm_chy_init(uint16_t arr, uint16_t psc);

#endif

gtim.c

#include "./BSP/TIMER/gtim.h"


TIM_HandleTypeDef g_timx_pwm_chy_handle;

/* 通用定时器PWM输出初始化函数 */
void gtim_timx_pwm_chy_init(uint16_t arr, uint16_t psc)
{
    TIM_OC_InitTypeDef timx_oc_pwm_chy;
    
    g_timx_pwm_chy_handle.Instance = TIM3;
    g_timx_pwm_chy_handle.Init.Prescaler = psc;
    g_timx_pwm_chy_handle.Init.Period = arr;
    g_timx_pwm_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;
    HAL_TIM_PWM_Init(&g_timx_pwm_chy_handle);
    
    timx_oc_pwm_chy.OCMode = TIM_OCMODE_PWM1;
    timx_oc_pwm_chy.Pulse = arr / 2;
    timx_oc_pwm_chy.OCPolarity = TIM_OCPOLARITY_LOW;
    HAL_TIM_PWM_ConfigChannel(&g_timx_pwm_chy_handle, &timx_oc_pwm_chy, TIM_CHANNEL_2);
    HAL_TIM_PWM_Start(&g_timx_pwm_chy_handle, TIM_CHANNEL_2);
}

/* 定时器输出PWM MSP初始化函数 */
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
    if(htim->Instance == TIM3)
    {
        GPIO_InitTypeDef gpio_init_struct;
        __HAL_RCC_GPIOB_CLK_ENABLE();
        __HAL_RCC_TIM3_CLK_ENABLE();

        gpio_init_struct.Pin = GPIO_PIN_5;
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;            /* 推挽复用 */
        gpio_init_struct.Pull = GPIO_PULLUP;                /* 上拉 */
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;      /* 高速 */
        HAL_GPIO_Init(GPIOB, &gpio_init_struct);
        
        __HAL_RCC_AFIO_CLK_ENABLE();
        __HAL_AFIO_REMAP_TIM3_PARTIAL();
    }
}
``
高级定时器部分:
atim.h

```c
#ifndef __ATIM_H
#define __ATIM_H

#include "./SYSTEM/sys/sys.h"


void atim_timx_pwmin_chy_init(void);
void atim_timx_pwmin_chy_restart(void);

#endif

atim.c

#include "./BSP/TIMER/atim.h"


TIM_HandleTypeDef g_timx_pwmin_chy_handle;   /* 定时器x句柄 */

/* PWM输入状态(g_timxchy_cap_sta)
 * 0,没有成功捕获.
 * 1,已经成功捕获了
 */
uint8_t g_timxchy_pwmin_sta  = 0;   /* PWM输入状态 */
uint16_t g_timxchy_pwmin_psc  = 0;  /* PWM输入分频系数 */
uint32_t g_timxchy_pwmin_hval = 0;  /* PWM的高电平脉宽 */
uint32_t g_timxchy_pwmin_cval = 0;  /* PWM的周期宽度 */

/* PWM输入模式 初始化函数,采样时钟频率为72Mhz,精度约13.8ns */
void atim_timx_pwmin_chy_init(void)
{
    TIM_SlaveConfigTypeDef slave_config = {0};
    TIM_IC_InitTypeDef tim_ic_pwmin_chy = {0};

    g_timx_pwmin_chy_handle.Instance = TIM8;                        /* 定时器8 */
    g_timx_pwmin_chy_handle.Init.Prescaler = 0;                     /* 定时器预分频系数 */
    g_timx_pwmin_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;  /* 递增计数模式 */
    g_timx_pwmin_chy_handle.Init.Period = 65535;                    /* 自动重装载值 */
    HAL_TIM_IC_Init(&g_timx_pwmin_chy_handle);
    
    /* 从模式配置,IT1触发更新 */
    slave_config.SlaveMode = TIM_SLAVEMODE_RESET;                   /* 从模式:复位模式 */
    slave_config.InputTrigger = TIM_TS_TI1FP1;                      /* 定时器输入触发源:TI1FP1 */
    slave_config.TriggerPolarity = TIM_TRIGGERPOLARITY_RISING;      /* 上升沿检测 */
    slave_config.TriggerFilter = 0;                                 /* 不滤波 */
    HAL_TIM_SlaveConfigSynchro(&g_timx_pwmin_chy_handle, &slave_config);

    /* IC1捕获:上升沿触发TI1FP1 */
    tim_ic_pwmin_chy.ICPolarity = TIM_ICPOLARITY_RISING;            /* 上升沿检测 */
    tim_ic_pwmin_chy.ICSelection = TIM_ICSELECTION_DIRECTTI;        /* 选择输入端IC1映射到TI1 */
    tim_ic_pwmin_chy.ICPrescaler = TIM_ICPSC_DIV1;                  /* 不分频 */
    tim_ic_pwmin_chy.ICFilter = 0;                                  /* 不滤波 */
    HAL_TIM_IC_ConfigChannel(&g_timx_pwmin_chy_handle, &tim_ic_pwmin_chy, TIM_CHANNEL_1);

    /* IC2捕获:下降沿触发TI1FP2 */
    tim_ic_pwmin_chy.ICPolarity = TIM_ICPOLARITY_FALLING;           /* 下降沿检测 */
    tim_ic_pwmin_chy.ICSelection = TIM_ICSELECTION_INDIRECTTI;      /* 选择输入端IC2映射到TI1 */
    HAL_TIM_IC_ConfigChannel(&g_timx_pwmin_chy_handle, &tim_ic_pwmin_chy, TIM_CHANNEL_2);

    HAL_TIM_IC_Start_IT(&g_timx_pwmin_chy_handle, TIM_CHANNEL_1);
    HAL_TIM_IC_Start(&g_timx_pwmin_chy_handle, TIM_CHANNEL_2);
}

/* 定时器 输入捕获 MSP初始化函数 */
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{
    if(htim->Instance == TIM8)
    {
        GPIO_InitTypeDef gpio_init_struct = {0};

        __HAL_RCC_TIM8_CLK_ENABLE();
        __HAL_RCC_GPIOC_CLK_ENABLE();

        gpio_init_struct.Pin = GPIO_PIN_6;
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;
        gpio_init_struct.Pull = GPIO_PULLDOWN;
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
        HAL_GPIO_Init(GPIOC, &gpio_init_struct);
        
        /* TIM1/TIM8有独立的输入捕获中断服务函数 */
        HAL_NVIC_SetPriority(TIM8_CC_IRQn, 1, 3);
        HAL_NVIC_EnableIRQ(TIM8_CC_IRQn);
    }
}

/* 定时器8 输入捕获 中断服务函数,仅TIM1/TIM8有这个函数,其他普通定时器没有这个中断服务函数! */
void TIM8_CC_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&g_timx_pwmin_chy_handle); /* 定时器共用处理函数 */
}

/* PWM输入模式 重新启动捕获 */
void atim_timx_pwmin_chy_restart(void)
{
    sys_intx_disable();                     /* 关闭中断 */

    g_timxchy_pwmin_sta = 0;                /* 清零状态,重新开始检测 */
    g_timxchy_pwmin_hval=0;
    g_timxchy_pwmin_cval=0;

    sys_intx_enable();                      /* 打开中断 */
}

/* 定时器输入捕获中断处理回调函数 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    if(htim->Instance == TIM8)
    {
        if(g_timxchy_pwmin_sta == 0)
        {
            if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
            {
                g_timxchy_pwmin_hval = HAL_TIM_ReadCapturedValue(&g_timx_pwmin_chy_handle, TIM_CHANNEL_2) + 1 + 1;//高电平计数值
                g_timxchy_pwmin_cval = HAL_TIM_ReadCapturedValue(&g_timx_pwmin_chy_handle, TIM_CHANNEL_1) + 1 + 1;//这里加2的原因:1、计数值从0开始 2、修正系数
                g_timxchy_pwmin_sta = 1;
            }
        }
    }
}

main.c

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/TIMER/atim.h"
#include "./BSP/TIMER/gtim.h"


extern uint16_t g_timxchy_pwmin_sta;    /* PWM输入状态 */
extern uint16_t g_timxchy_pwmin_psc;    /* PWM输入分频系数 */
extern uint32_t g_timxchy_pwmin_hval;   /* PWM的高电平脉宽 */
extern uint32_t g_timxchy_pwmin_cval;   /* PWM的周期宽度 */

int main(void)
{
    uint8_t t = 0;
    double ht, ct, f, tpsc;

    HAL_Init();                                 /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);         /* 设置时钟, 72Mhz */
    delay_init(72);                             /* 延时初始化 */
    usart_init(115200);                         /* 串口初始化为115200 */
    led_init();                                 /* 初始化LED */
    gtim_timx_pwm_chy_init(10 - 1, 72 - 1);
    
    TIM3->CCR2 = 5;//设置比较值  占空比为50%
    
    atim_timx_pwmin_chy_init();
    
    while (1)
    {
        delay_ms(10);
        t++;

        if (t >= 20)    /* 每200ms输出一次结果,并闪烁LED0,提示程序运行 */
        {
            if (g_timxchy_pwmin_sta)    /* 捕获了一次数据 */
            {
                printf("\r\n");                                     /* 输出空,另起一行 */
                printf("PWM PSC  :%d\r\n", g_timxchy_pwmin_psc);    /* 打印分频系数 */
                printf("PWM Hight:%d\r\n", g_timxchy_pwmin_hval);   /* 打印高电平脉宽 */
                printf("PWM Cycle:%d\r\n", g_timxchy_pwmin_cval);   /* 打印周期 */
                tpsc = ((double)g_timxchy_pwmin_psc + 1) / 72;      /* 得到PWM采样时钟周期时间 */ 
                ht = g_timxchy_pwmin_hval * tpsc;                   /* 计算高电平时间 */
                ct = g_timxchy_pwmin_cval * tpsc;                   /* 计算周期长度 */
                f = (1 / ct) * 1000000;                             /* 计算频率 */
                printf("PWM Hight time:%.3fus\r\n", ht);            /* 打印高电平脉宽长度 */
                printf("PWM Cycle time:%.3fus\r\n", ct);            /* 打印周期时间长度 */
                printf("PWM Frequency :%.3fHz\r\n", f);             /* 打印频率 */ 
                atim_timx_pwmin_chy_restart(); /* 重启PWM输入检测 */
            } 

            LED1_TOGGLE();  /* LED1(GREEN)闪烁 */
            t = 0;
        }
    }
}

在这里插入图片描述

22.5.3.1 程序流程图

在这里插入图片描述

程序解析

这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。高级定时器驱动源码包括两个文件:atim.c 和atim.h。
首先看atim.h 头文件的几个宏定义:

/* TIMX PWM输入模式定义
* 这里的输入捕获使用定时器TIM8_CH1
* 默认是针对TIM1/TIM8等高级定时器
* 注意: 通过修改这几个宏定义,可以支持TIM1~TIM8任意一个定时器的通道1/通道2
*/
#define ATIM_TIMX_PWMIN_CHY_GPIO_PORT GPIOC
#define ATIM_TIMX_PWMIN_CHY_GPIO_PIN GPIO_PIN_6
#define ATIM_TIMX_PWMIN_CHY_GPIO_CLK_ENABLE() \
do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* PC口时钟使能*/
#define ATIM_TIMX_PWMIN TIM8
#define ATIM_TIMX_PWMIN_IRQn TIM8_UP_IRQn
#define ATIM_TIMX_PWMIN_IRQHandler TIM8_UP_IRQHandler
#define ATIM_TIMX_PWMIN_CHY TIM_CHANNEL_1 /* 通道Y, 1<= Y <=2*/
#define ATIM_TIMX_PWMIN_CHY_CLK_ENABLE()
do{ __HAL_RCC_TIM8_CLK_ENABLE(); }while(0) /* TIM8 时钟使能*/
/* TIM1 / TIM8 有独立的捕获中断服务函数,需要单独定义,对于TIM2~5等,则不需要以下定义*/
#define ATIM_TIMX_PWMIN_CC_IRQn TIM8_CC_IRQn
#define ATIM_TIMX_PWMIN_CC_IRQHandler TIM8_CC_IRQHandler

可以把上面的宏定义分成三部分,第一部分包括是定时器8 通道1 对应的IO 口的宏定义,第二部分则是定时器8 的相应宏定义,另外针对TIM1/ TIM8 有独立的捕获中断服务函数,需要单独定义。
下面看atim.c 的程序,首先是高级定时器PWM 输入模式初始化函数,其定义如下:

/**
* @brief 定时器TIMX 通道Y PWM输入模式初始化函数
* @note
* 高级定时器的时钟来自APB2, 而PCLK2 = 72Mhz, 我们设置PPRE2不分频, 因此
* 高级定时器时钟= 72Mhz
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
* Ft=定时器工作频率,单位:Mhz
*
* 本函数初始化的时候: 使用psc=0, arr固定为65535. 得到采样时钟频率为72Mhz,
精度约13.8ns
*
* @param 无
* @retval 无
*/
void atim_timx_pwmin_chy_init(void)
{
	{
	GPIO_InitTypeDef gpio_init_struct = {0};
	TIM_SlaveConfigTypeDef slave_config = {0};
	TIM_IC_InitTypeDef tim_ic_pwmin_chy = {0};
	ATIM_TIMX_PWMIN_CHY_CLK_ENABLE();
	ATIM_TIMX_PWMIN_CHY_GPIO_CLK_ENABLE();
	__HAL_RCC_AFIO_CLK_ENABLE();
	AFIO_REMAP_PARTIAL(AFIO_EVCR_PORT_PC,AFIO_EVCR_PIN_PX6);/*复用TIM8_CH1/PC6*/
	gpio_init_struct.Pin = ATIM_TIMX_PWMIN_CHY_GPIO_PIN;
	gpio_init_struct.Mode = GPIO_MODE_AF_PP;
	gpio_init_struct.Pull = GPIO_PULLDOWN;
	gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH ;
	HAL_GPIO_Init(ATIM_TIMX_PWMIN_CHY_GPIO_PORT, &gpio_init_struct);
	g_timx_pwmin_chy_handle.Instance = ATIM_TIMX_PWMIN; /* 定时器8 */
	g_timx_pwmin_chy_handle.Init.Prescaler = 0; /* 定时器预分频系数*/
	g_timx_pwmin_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;/* 递增计数*/
	g_timx_pwmin_chy_handle.Init.Period = 65535; /* 自动重装载值*/
	HAL_TIM_IC_Init(&g_timx_pwmin_chy_handle);
	/* 从模式配置,IT1触发更新*/
	slave_config.SlaveMode = TIM_SLAVEMODE_RESET; /* 从模式:复位模式*/
	slave_config.InputTrigger = TIM_TS_TI1FP1; /* 定时器输入触发源:TI1FP1 */
	slave_config.TriggerPolarity = TIM_INPUTCHANNELPOLARITY_RISING;/*上升沿检测*/
	slave_config.TriggerFilter = 0; /* 不滤波*/
	HAL_TIM_SlaveConfigSynchro(&g_timx_pwmin_chy_handle, &slave_config);
	/* IC1捕获:上升沿触发TI1FP1 */
	tim_ic_pwmin_chy.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;/* 上升沿检测*/
	tim_ic_pwmin_chy.ICSelection = TIM_ICSELECTION_DIRECTTI;/* IC1映射到TI1上*/
	tim_ic_pwmin_chy.ICPrescaler = TIM_ICPSC_DIV1; /* 不分频*/
	tim_ic_pwmin_chy.ICFilter = 0; /* 不滤波*/
	HAL_TIM_IC_ConfigChannel(&g_timx_pwmin_chy_handle, &tim_ic_pwmin_chy,
	TIM_CHANNEL_1 );
	/* IC2捕获:上升沿触发TI1FP2 */
	tim_ic_pwmin_chy.ICPolarity = TIM_INPUTCHANNELPOLARITY_FALLING;/*下降沿检测*/
	tim_ic_pwmin_chy.ICSelection = TIM_ICSELECTION_INDIRECTTI;/*IC2映射到TI1上*/
	HAL_TIM_IC_ConfigChannel(&g_timx_pwmin_chy_handle, &tim_ic_pwmin_chy,
	TIM_CHANNEL_2);
	/* 设置中断优先级,抢占优先级1,子优先级3 */
	HAL_NVIC_SetPriority(ATIM_TIMX_PWMIN_IRQn, 1, 3);
	HAL_NVIC_EnableIRQ( ATIM_TIMX_PWMIN_IRQn ); /* 开启TIMx中断*/
	/* TIM1/TIM8有独立的输入捕获中断服务函数*/
	if ( ATIM_TIMX_PWMIN == TIM1 || ATIM_TIMX_PWMIN == TIM8)
	{
		/* 设置中断优先级,抢占优先级1,子优先级3 */
		HAL_NVIC_SetPriority(ATIM_TIMX_PWMIN_CC_IRQn, 1, 3);
		HAL_NVIC_EnableIRQ(ATIM_TIMX_PWMIN_CC_IRQn); /* 开启TIMx中断*/
	}
	__HAL_TIM_ENABLE_IT(&g_timx_pwmin_chy_handle, TIM_IT_UPDATE);
	HAL_TIM_IC_Start_IT(&g_timx_pwmin_chy_handle, TIM_CHANNEL_1);
	HAL_TIM_IC_Start_IT(&g_timx_pwmin_chy_handle, TIM_CHANNEL_2);
}

在atim_timx_pwmin_chy_init 函数中,没有使用HAL 库的MSP 回调,而是把相关的初始化都写到该函数里面。
第一部分,使能定时器和相关通道对应的GPIO 时钟,以及初始化相关IO 引脚。
第二部分,通过HAL_TIM_IC_Init 函数初始化定时器的ARR 和PSC 等参数。
第三部分,通过HAL_TIM_SlaveConfigSynchronization 函数配置从模式,复位模式等。
第四部分,通过HAL_TIM_IC_ConfigChannel 函数分别配置IC1 和IC2。
第五部分,配置NVIC,如使能定时器中断,配置抢占优先级和响应优先级。
最后,通过调用HAL_TIM_IC_Start_IT 函数和__HAL_TIM_ENABLE_IT 函数宏使能捕获中断和更新中断,并且使能定时器。
为了方便,我们定义了重新启动捕获函数,其定义如下:

/**
* @brief 定时器TIMX PWM输入模式重新启动捕获
* @param 无
* @retval 无
*/
void atim_timx_pwmin_chy_restart(void)
{
	sys_intx_disable(); /* 关闭中断*/
	g_timxchy_pwmin_sta = 0; /* 清零状态,重新开始检测*/
	g_timxchy_pwmin_psc = 0; /* 分频系数清零*/
	/* 以最大的计数频率采集,以得到最好的精度*/
	__HAL_TIM_SET_PRESCALER(&g_timx_pwmin_chy_handle, 0);
	__HAL_TIM_SET_COUNTER(&g_timx_pwmin_chy_handle, 0); /* 计数器清零*/
	__HAL_TIM_ENABLE_IT(&g_timx_pwmin_chy_handle, TIM_IT_CC1);/* 使能捕获中断*/
	__HAL_TIM_ENABLE_IT(&g_timx_pwmin_chy_handle, TIM_IT_UPDATE);/*使能更新中断*/
	__HAL_TIM_ENABLE(&g_timx_pwmin_chy_handle); /* 使能定时器TIMX */
	ATIM_TIMX_PWMIN->SR = 0; /* 清除所有中断标志位*/
	sys_intx_enable(); /* 打开中断*/
}

该函数首先关闭所有中断,然后把一些状态标志位清零、设置定时器预分频系数、计数器值、使能相关中断、以及清除相关中断标志位,最后才允许被中断。
最后要介绍的是中断服务函数,在定时器1 的输入捕获中断服务函数TIM1_CC_IRQHandler和更新中断服务函数TIM1_UP_IRQHandler 里面都是直接调用atim_timx_pwmin_chy_process 函数。输入捕获中断服务函数和更新中断服务函数都是用到宏定义的,这三个函数定义如下:

/**
 * @brief 定时器TIMX 更新/溢出中断服务函数
 * @note TIM1/TIM8的这个函数仅用于更新/溢出中断服务,捕获在另外一个函数!
 * 其他普通定时器则更新/溢出/捕获,都在这个函数里面处理!
 * @param 无
 * @retval 无
 */
void ATIM_TIMX_PWMIN_IRQHandler(void)
{
        atim_timx_pwmin_chy_process();
}
/**
 * @brief 定时器TIMX 输入捕获中断服务函数
 * @note 仅TIM1/TIM8有这个函数,其他普通定时器没有这个中断服务函数!
 * @param 无
 * @retval 无
 */
void ATIM_TIMX_PWMIN_CC_IRQHandler(void)
{
        atim_timx_pwmin_chy_process();
}
/**
 * @brief 定时器TIMX 通道Y PWM输入模式中断处理函数
 * @note
 * 因为TIM1/TIM8等有多个中断服务函数,而TIM2~5/TIM12/TIM15等普通定时器只有1个中断服务
 * 函数,为了更好的兼容,我们对中断处理统一放到atim_timx_pwin_chy_process函数里面进行处理
 *
 * @param 无
 * @retval 无
 */
static void atim_timx_pwmin_chy_process(void)
{
        static uint8_t sflag = 0; /* 启动PWMIN输入检测标志*/
        if (g_timxchy_pwmin_sta)
        {
                g_timxchy_pwmin_psc = 0;
                ATIM_TIMX_PWMIN->SR = 0; /* 清除所有中断标志位*/
                __HAL_TIM_SET_COUNTER(&g_timx_pwmin_chy_handle, 0); /* 计数器清零*/
                return ;
        }
        /* 如果发生了更新中断*/
        if (__HAL_TIM_GET_FLAG(&g_timx_pwmin_chy_handle, TIM_FLAG_UPDATE))
        {
                /* 清除更新中断标记*/
                __HAL_TIM_CLEAR_FLAG(&g_timx_pwmin_chy_handle, TIM_FLAG_UPDATE);
                /* 没有发生周期捕获中断,且捕获未完成*/
                if (__HAL_TIM_GET_FLAG(&g_timx_pwmin_chy_handle, TIM_FLAG_CC1) == 0)
                {
                        sflag = 0;
                        if (g_timxchy_pwmin_psc == 0) /* 从0 到1 */
                        {
                                g_timxchy_pwmin_psc ++;
                        }
                        else
                        {
                                if (g_timxchy_pwmin_psc == 65535) /* 已经最大了,可能是无输入状态*/
                                {
                                        g_timxchy_pwmin_psc = 0; /* 重新恢复不分频*/
                                }
                                else if (g_timxchy_pwmin_psc > 32767)/* 不能倍增了*/
                                {
                                        g_timxchy_pwmin_psc = 65535; /* 直接等于最大分频系数*/
                                }
                                else
                                {
                                        g_timxchy_pwmin_psc += g_timxchy_pwmin_psc; /* 倍增*/
                                }
                        }
                        __HAL_TIM_SET_PRESCALER(&g_timx_pwmin_chy_handle,
                                        g_timxchy_pwmin_psc); /* 设置定时器预分频系数*/
                        __HAL_TIM_SET_COUNTER(&g_timx_pwmin_chy_handle, 0); /* 计数器清零*/
                        ATIM_TIMX_PWMIN->SR = 0; /* 清除所有中断标志位*/
                        return ;
                }
        }
        if (sflag == 0) /* 第一次采集到捕获中断*/
        {
                /* 检测到了第一次周期捕获中断*/
                if (__HAL_TIM_GET_FLAG(&g_timx_pwmin_chy_handle, TIM_FLAG_CC1))
                {
                        sflag = 1; /* 标记第一次周期已经捕获, 第二次周期捕获可以开始了*/
                }
                ATIM_TIMX_PWMIN->SR = 0; /* 清除所有中断标志位*/
                return ; /* 完成此次操作*/
        }
        if (g_timxchy_pwmin_sta == 0)/* 还没有成功捕获*/
        {
                /* 检测到了周期捕获中断*/
                if (__HAL_TIM_GET_FLAG(&g_timx_pwmin_chy_handle, TIM_FLAG_CC1))
                {
                        g_timxchy_pwmin_hval = HAL_TIM_ReadCapturedValue(
                                        &g_timx_pwmin_chy_handle, TIM_CHANNEL_2) + 1; /* 高定平脉宽捕获值*/
                        g_timxchy_pwmin_cval = HAL_TIM_ReadCapturedValue(
                                        &g_timx_pwmin_chy_handle, TIM_CHANNEL_1) + 1; /* 周期捕获值*/
                        /* 高电平脉宽必定小于周期长度*/
                        if (g_timxchy_pwmin_hval < g_timxchy_pwmin_cval)
                        {
                                g_timxchy_pwmin_sta = 1; /* 标记捕获成功*/
                                g_timxchy_pwmin_psc = ATIM_TIMX_PWMIN->PSC;/* 获取PWM输入分频系数*/
                                if (g_timxchy_pwmin_psc == 0) /* 分频系数为0的时候, 修正读取数据*/
                                {
                                        g_timxchy_pwmin_hval++; /* 修正系数为1, 加1 */
                                        g_timxchy_pwmin_cval++; /* 修正系数为1, 加1 */
                                }
                                sflag = 0;
                                /* 每次捕获PWM输入成功后, 停止捕获,避免频繁中断影响系统正常代码运行*/
                                ATIM_TIMX_PWMIN->CR1 &= ~(1 << 0); /* 关闭定时器TIMX */
                                /* 关闭通道1捕获中断*/
                                __HAL_TIM_DISABLE_IT(&g_timx_pwmin_chy_handle, TIM_IT_CC1);
                                /* 关闭通道2捕获中断*/
                                __HAL_TIM_DISABLE_IT(&g_timx_pwmin_chy_handle, TIM_IT_CC2);
                                /* 关闭更新中断*/
                                __HAL_TIM_DISABLE_IT(&g_timx_pwmin_chy_handle, TIM_IT_UPDATE);
                                ATIM_TIMX_PWMIN->SR = 0; /* 清除所有中断标志位*/
                        }
                        else
                        {
                                atim_timx_pwmin_chy_restart();
                        }
                }
        }
        ATIM_TIMX_PWMIN->SR = 0; /* 清除所有中断标志位*/
}

atim_timx_pwmin_chy_process 函数包含了捕获中断程序和更新中断程序的处理。如果发生了更新中断(即定时器溢出),证明超出定时器量程,这里会加大预分频系数,以得到更大的量程。量程变大了,那么测量的精度就会降低,所谓鱼和熊掌不可兼得。代码中的“if (sflag == 0) /* 第一次采集到捕获中断*/”这个程序段,表示第一次采集到捕获中断。这时候相当于第一次捕获到上升沿,我们只是把sflag 标志位置1,然后清除所有中断标志位,等待下次的捕获中断发生。如果再次发生捕获中断,就会来到“if (g_timxchy_pwmin_sta == 0) /* 还没有成功捕获*/”程序段。通过HAL_TIM_ReadCapturedValue 函数获取CCR1 和CCR2 寄存器的值,把这个获取到的寄存器值+1 才是对应的计数器计数个数。如果预分频系数为0 的时候,还要把这两个寄存器的值再+1,这样计算的结果更准确。其它的代码细节请大家自行查看源码,有详细的注释。
注释代码:实验10-1 高级定时器输出指定个数PWM 实验使用到TIM8_UP_IRQHandler 中
断服务函数,本实验同样使用到该函数,编译会报错,这里的做法是屏蔽实验10-1 的相关代码。
具体请看atim.c 文件源码。atim.c 文件的程序就介绍到这。

下面介绍一下待测试的PWM 怎么得到。因为在实验9-2 通用定时器PWM 输出实验我们已经编写了PWM 波输出的程序,所以这里直接使用通用定时器的PWM 输出实验的代码进行初始化,从而让TIM3_CH2(PB5)输出PWM 波。然后我们用杜邦线把PB5 和PC6 连接起来。
这样PB5 输出的PWM 就可以输入到PC6(定时器8 通道1)进行测量。
在main.c 里面编写如下代码:

int main(void)
{
        uint8_t t = 0;
        double ht, ct, f, tpsc;
        HAL_Init(); /* 初始化HAL库*/
        sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
        delay_init(72); /* 延时初始化*/
        usart_init(115200); /* 串口初始化为115200 */
        led_init(); /* 初始化LED */
        gtim_timx_pwm_chy_init(10 - 1, 72 - 1); /* 1Mhz的计数频率, 100Khz PWM */
        atim_timx_pwmin_chy_init(); /* 初始化PWM输入捕获*/
        GTIM_TIMX_PWM_CHY_CCRX = 2; /* 低电平宽度2,高电平宽度8 */
        while (1)
        {
                delay_ms(10);
                t++;
                if (t >= 20) /* 每200ms输出一次结果,并闪烁LED0,提示程序运行*/
                {
                        if (g_timxchy_pwmin_sta) /* 捕获了一次数据*/
                        {
                                printf("\r\n"); /* 输出空,另起一行*/
                                printf("PWM PSC :%d\r\n", g_timxchy_pwmin_psc); /* 打印分频系数*/
                                printf("PWM Hight:%d\r\n", g_timxchy_pwmin_hval);/*打印高电平脉宽*/
                                printf("PWM Cycle:%d\r\n", g_timxchy_pwmin_cval);/* 打印周期*/
                                /* 得到PWM采样时钟周期时间*/
                                tpsc = ((double)g_timxchy_pwmin_psc + 1)/72;
                                ht = g_timxchy_pwmin_hval * tpsc; /* 计算高电平时间*/
                                ct = g_timxchy_pwmin_cval * tpsc; /* 计算周期长度*/
                                f = (1 / ct) * 1000000; /* 计算频率*/
                                printf("PWM Hight time:%.3fus\r\n", ht); /* 打印高电平脉宽长度*/
                                printf("PWM Cycle time:%.3fus\r\n", ct); /* 打印周期时间长度*/
                                printf("PWM Frequency :%.3fHz\r\n", f); /* 打印频率*/
                                atim_timx_pwmin_chy_restart(); /* 重启PWM输入检测*/
                        }
                        LED0_TOGGLE(); /* LED0(RED)闪烁*/
                        t = 0;
                }
        }
}

先看gtim_timx_pwm_chy_init(10 - 1, 72 - 1)这个语句,这两个形参分别设置自动重载寄存器的值为9,以及定时器预分频寄存器的值为71。先看预分频系数,我们设置为72 分频,定时器1 的时钟频率等于APB2 总线时钟频率,即72MHz,可以得到计数器的计数频率是1MHz,即1us 计数一次。再到自动重载寄存器的值为9 决定的是PWM 波的频率(周期),可以得到PWM 的周期为10*1us = 10us。然后通过GTIM_TIMX_PWM_CHY_CCRX = 2 这个语句设置占空比,低电平宽度2,总的周期宽度是10,所以高电平宽度8。即产生的PWM 波周期为10us,频率为100KHz,占空比为80%。下载验证的时候验证一下捕获到的与输出的是否一致。

atim_timx_pwmin_chy_init 这个语句,就初始化PWM 输入捕获。然后在无限循环中每200ms判断是否g_timxchy_pwmin_sta 标志变量,是否捕获到数据,捕获到就打印和计数相关信息。

下面我们下载到开发板验证一下。

下载验证

下载代码后,可以看到LED0 在闪烁,说明程序已经正常在跑了,我们再打开串口调试助手,选择对应的串口端口。然后用杜邦线把PB5 引脚连接到PC6 引脚,就可以看到串口助手不断打印PWM 波的信息,如图22.5.4.1 所示:

在这里插入图片描述

可以看到打印出来的PWM 波信息为:周期是10us,频率是100KHz,占空比是80%,和我们的预想结果一样。
大家可以通过gtim_timx_pwm_chy_init 函数的形参设置其他参数的PWM 波,以及GTIM_TIMX_PWM_CHY_CCRX 设置占空比。这里的测试的PWM 波有一定的范围,不是全范围的PWM 都可以进行准确的测试,大家可以进行验证。

独立看门狗(IWDG)实验

本章我们学习如何使用STM32F1 的独立看门狗(以下简称IWDG)。STM32F1 内部自带了2 个看门狗:独立看门狗(IWDG)和窗口看门狗(WWDG)。这一章我们只介绍独立看门狗,窗口看门狗将在下一章介绍。在本章中,我们将通过按键KEY_UP 来喂狗,然后通过LED0 提示复位状态。

IWDG 简介

在这里插入图片描述
在这里插入图片描述

独立看门狗本质上是一个定时器,这个定时器有一个输出端,可以输出复位信号。该定时器是一个12 位的递减计数器,当计数器的值减到0 的时候,就会产生一个复位信号。如果在计数没减到0 之前,重置计数器的值的话,那么就不会产生复位信号,这个动作我们称为喂狗。

看门狗功能由VDD 电压域供电,在停止模式和待机模式下仍然可以工作。

在这里插入图片描述

IWDG 框图

下面先来学习IWDG 框图,通过学习IWDG 框图会有一个很好的整体掌握,同时对之后的编程也会有一个清晰的思路。

在这里插入图片描述

从IWDG 框图整体认知就是,IWDG 有一个输入(时钟LSI),经过一个8 位的可编程预分频器提供时钟给一个12 位递减计数器,满足条件就会输出一个复位信号。IWDG 内部输入/输出信号如下表:

在这里插入图片描述

STM32F103 的独立看门狗由内部专门的40Khz 低速时钟(LSI)驱动,即使主时钟发生故障,它也仍然有效。这里需要注意独立看门狗的时钟是一个内部RC 时钟,所以并不是准确的40Khz,而是在30~60Khz 之间的一个可变化的时钟,只是我们在估算的时候,以40Khz 的频率来计算,看门狗对时间的要求不是很精确,所以,时钟有些偏差,都是可以接受的。

IWDG 寄存器

IWDG 的框图很简单,用到的寄存器也不多。我们主要用到其中3 个寄存器:

⚫ 键寄存器(IWDG_KR),上图的秘钥寄存器

键寄存器可以看作是独立看门狗的控制寄存器,其描述如图18.1.2.1 所示:

在这里插入图片描述

在键寄存器(IWDG_KR)中写入0xCCCC,开始启用独立看门狗;此时计数器开始从其复位值0xFFF 递减计数。当计数器计数到末尾0x000 时,会产生一个复位信号(IWDG_RESET)。无论何时,只要键寄存器IWDG_KR 中被写入0xAAAA,IWDG_RLR 中的值就会被重新加载到计数器中从而避免产生看门狗复位。

IWDG_PR 和IWDG_RLR 寄存器具有写保护功能。要修改这两个寄存器的值,必须先向IWDG_KR 寄存器中写入0x5555。将其他值写入这个寄存器将会打乱操作顺序,寄存器将重新被保护。重装载操作(即写入0xAAAA)也会启动写保护功能。

⚫ 预分频寄存器(IWDG_PR)
预分频寄存器描述如图18.1.2.2 所示:

在这里插入图片描述

该寄存器用来设置看门狗时钟(LSI)的分频系数,最低为4,最高位256,该寄存器是一个32 位的寄存器,但是我们只用了最低3 位,其他都是保留位。

⚫ 重载寄存器(IWDG_RLR)
在这里插入图片描述
该寄存器用来保存重装载到计数器中的值。该寄存器也是一个32 位寄存器,只有低12 位是有效的。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

硬件设计

  1. 例程功能
    在配置看门狗后,LED0 将常亮,如果KEY_UP 按键按下,就喂狗,只要KEY_UP 不停的按,看门狗就一直不会产生复位,保持LED0 的常亮,一旦超过看门狗定溢出时间(Tout)还没按,那么将会导致程序重启,这将导致LED0 熄灭一次。
  2. 硬件资源
    1)LED 灯
    LED0 –PB5
    2)独立按键
    WK_UP - PA0。
    3)独立看门狗
  3. 原理图
    独立看门狗实验的核心是在STM32F103 内部进行,并不需要外部电路。但是考虑到指示当前状态和喂狗等操作,我们需要2 个IO 口,一个用来触发喂狗信号,另外一个用来指示程序是否重启。喂狗我们采用板上的KEY_UP 键来操作,而程序重启,则是通过LED0 来指示的。

程序设计

IWDG 的HAL 库驱动

在这里插入图片描述

IWDG 在HAL 库中的驱动代码在stm32f1xx_hal_iwdg.c 文件(及其头文件)中。

  1. HAL_IWDG_Init 函数
    IWDG 的初始化函数,其声明如下:
    HAL_StatusTypeDef HAL_IWDG_Init(IWDG_HandleTypeDef *hiwdg);
    ⚫ 函数描述:
    用于初始化IWDG。
    ⚫ 函数形参:
    形参1 是IWDG 句柄,IWDG_HandleTypeDef 结构体类型,其定义如下:
typedef struct
{
	IWDG_TypeDef *Instance; /* IWDG寄存器基地址*/
	IWDG_InitTypeDef Init; /* IWDG初始化参数*/
} IWDG_HandleTypeDef;

1)Instance:指向IWDG 寄存器基地址。
2)Init:IWDG 初始化结构体,用于配置计数器的相关参数。
IWDG_InitTypeDef 这个结构体类型定义如下:

typedef struct
{
	uint32_t Prescaler; /* 预分频系数*/
	uint32_t Reload; /* 重装载值*/
} IWDG_InitTypeDef;

1)Prescaler:预分频系数,IWDG_PRESCALER_4 到IWDG_PRESCALER_256。
2)Reload:重装载值,范围:0 到0x0FFF。
3)Window:窗口值。
⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值。
2. HAL_IWDG_Refresh 函数
HAL_IWDG_Refresh 函数是独立看门狗的喂狗函数。其声明如下:

HAL_StatusTypeDef HAL_IWDG_Refresh(IWDG_HandleTypeDef *hiwdg);

⚫ 函数描述:
用于把重装载寄存器的值重载到计数器中,喂狗,防止IWDG 复位。
⚫ 函数形参:
形参1 是IWDG_HandleTypeDef 结构体指针类型的IWDG 句柄。
⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值。

独立看门狗配置步骤

在这里插入图片描述

1)取消寄存器写保护,设置看门狗预分频系数和重装载值
首先我们必须取消IWDG_PR 和IWDG_RLR 寄存器的写保护,这样才可以设置寄存器IWDG_PR 和IWDG_RLR 的值。取消写保护和设置预分频系数以及重装载值在HAL 库中是通过函数HAL_IWDG_Init 实现的。
通过该函数设置看门狗的分频系数,和重装载的值。看门狗的喂狗时间(也就是看门狗溢出时间)的计算方式为:

Tout=((4×2^prer) ×rlr) /40

其中Tout 为看门狗溢出时间(单位为ms)。
prer 为看门狗时钟预分频值(IWDG_PR 值),范围为0~7。
rlr 为看门狗的重装载值(IWDG_RLR 的值)。

比如我们设定prer 值为4(4 代表的是64 分频,HAL 库中可以使用宏定义标识符IWDG_PRESCALER_64),定时值为Tout=1 秒,那么就可以得到Tout=64×rlr/40Khz=1s,这样,得到看门狗的溢出时间要为1s 需要设置rlr 为625,只要你在一秒钟之内,有一次写入0XAAAA到IWDG_KR,就不会导致看门狗复位(当然写入多次也是可以的)。这里需要提醒大家的是,看门狗的时钟不是准确的40Khz,所以在喂狗的时候,最好不要太晚了,否则,有可能发生看门狗复位。

2)重载计数值喂狗(向IWDG_KR 写入0XAAAA)
在HAL 中重载计数值的函数是HAL_IWDG_Refresh,该函数的作用是把值0xAAAA 写入到IWDG_KR 寄存器,从而触发计数器重载,即实现独立看门狗的喂狗操作。

3)启动看门狗(向IWDG_KR 写入0XCCCC)
HAL 库函数里面启动独立看门狗是通过宏定义标识符来实现的:

#define __HAL_IWDG_START(__HANDLE__)
		WRITE_REG((__HANDLE__)->Instance->KR, IWDG_KEY_ENABLE);

我们只需要调用宏定义标识符__HAL_IWDG_START 即可实现看门狗使能。实际上,当我们调用了看门狗初始化函数HAL_IWDG_Init 之后,在内部已经调用了该宏启动看门狗。

课堂源码

在这里插入图片描述

wdg.h

#ifndef __WDG_H
#define __WDG_H

#include "./SYSTEM/sys/sys.h"


void iwdg_init(uint8_t prer, uint16_t rlr);
void iwdg_feed(void);

#endif

wdg.c

#include "./BSP/WDG/wdg.h"


IWDG_HandleTypeDef g_iwdg_handle;

/* IWDG初始化函数 */
void iwdg_init(uint8_t prer, uint16_t rlr)
{
    g_iwdg_handle.Instance = IWDG;
    g_iwdg_handle.Init.Prescaler = prer;//分频系数
    g_iwdg_handle.Init.Reload = rlr;	//重装载值
    HAL_IWDG_Init(&g_iwdg_handle);
}

/* 喂狗函数 */
void iwdg_feed(void)
{
    HAL_IWDG_Refresh(&g_iwdg_handle);
}

main.c

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/WDG/wdg.h"


int main(void)
{
    HAL_Init();                             /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);     /* 设置时钟为72Mhz */
    delay_init(72);                         /* 延时初始化 */
    usart_init(115200);                     /* 串口初始化为115200 */
    
    printf("您还没喂狗,请及时喂狗!!!\r\n");
    iwdg_init(IWDG_PRESCALER_32, 1250);     /* 预分频系数为32,重装载值为1250,溢出时间约为1s */
    while (1)
    {
        delay_ms(1000);//如果这个延时超过1000,则会重新执行main 反复打印:您还没喂狗,请及时喂狗!!
        iwdg_feed();
        printf("已经喂狗\r\n");
    }
}

程序流程图

下面看看本实验的程序流程图:

在这里插入图片描述

程序解析

  1. IWDG 驱动代码
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。独立看门狗(IWDG)驱动源码包括两个文件:wdg.c 和wdg.h。

wdg.h 头文件只有函数的声明,就不解释了。下面我们直接解析wdg.c 的程序,先看IWDG的初始化函数,其定义如下:

/**
* @brief 初始化独立看门狗
* @param prer: IWDG_PRESCALER_4~IWDG_PRESCALER_256,对应4~256分频
* @arg 分频因子= 4 * 2^prer. 但最大值只能是256!
* @param rlr: 自动重装载值,0~0XFFF.
* @note 时间计算(大概):Tout=((4 * 2^prer) * rlr) / 40 (ms).
* @retval 无
*/
void iwdg_init(uint8_t prer, uint16_t rlr)
{
	g_iwdg_handle.Instance = IWDG1;
	g_iwdg_handle.Init.Prescaler = prer; /* 设置IWDG分频系数*/
	g_iwdg_handle.Init.Reload = rlr; /* 重装载值*/
	g_iwdg_handle.Init.Window = IWDG_WINDOW_DISABLE; /* 关闭窗口功能*/
	HAL_IWDG_Init(&g_iwdg_handle);
}

IWDG_Init 是独立看门狗初始化函数,主要设置预分频数和重装载寄存器的值。通过这两个寄存器,就可以大概知道看门狗复位的时间周期为多少了。

/**
* @brief 喂独立看门狗
* @param 无
* * @retval 无
*/
void iwdg_feed(void)
{
	HAL_IWDG_Refresh(&g_iwdg_handle); /* 重装载计数器*/
}

iwdg_feed 函数用来喂狗,在该函数内部只需调用HLA 库函数HAL_IWDG_Refresh。
2. main.c 代码
在main.c 里面编写如下代码:

int main(void)
{
	HAL_Init(); /* 初始化HAL库*/
	sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
	delay_init(72); /* 延时初始化*/
	usart_init(115200); /* 串口初始化为115200 */
	led_init(); /* 初始化LED */
	key_init(); /* 初始化按键*/
	delay_ms(100); /* 延时100ms再初始化看门狗,LED0的变化"可见" */
	iwdg_init(IWDG_PRESCALER_64,625); /* 预分频数64,重载值为625,溢出时间约为1s */
	LED0(0); /* 点亮LED0(红灯) */
	while (1)
	{
		if (key_scan(1) == WKUP_PRES) /* 如果WK_UP按下,则喂狗*/
		{
			iwdg_feed(); /* 喂狗*/
		}
		delay_ms(10);
	}
}

在main 函数里,先初始化系统和用户的外设代码,然后先点亮LED0,在无限循环里开始获取按键的键值,并判断是不是按键WK_UP 按下,是的话就喂狗,不是则延时10ms,继续上述操作。当1 秒钟后都没测到按键WK_UP 按下,IWDG 就会产生一次复位信号,系统复位,可以看到LED0 因系统复位熄灭一次,再亮。反之,当按下按键WK_UP 后,1 秒内再按下按键WK_UP,就会及时喂狗,结果就是系统不会复位,LED0 也就不会闪烁。

iwdg_init(IWDG_PRESCALER_64, 625);这个语句有必要跟大家说明一下,这里的第一个形参直接使用HAL 库自定义的IWDG_PRESCALER_64,即预分频系数为64,重装载值是625,所以可由公式得到Tout=64×625/40=1000ms,即溢出时间就是1s。只要你在一秒钟之内,有一次写0XAAAA 到IWDG_KR,就不会导致看门狗复位(当然写入多次也是可以的)。这里需要提醒大家的是,看门狗的时钟不是准确的40Khz,所以在喂狗的时候,最好不要太晚了,否则,有可能发生看门狗复位。

下载验证

下载代码后,可以看到LED0 不停的闪烁,证明系统在不停的复位,否则LED0 常亮。这时我们试试不停的按KEY_UP 按键,可以看到LED0 就常亮了,不会再闪烁。说明我们的实验设计成功了。

窗口门狗(WWDG)实验(复习到这)

本章我们学习如何使用STM32F1 的另外一个看门狗,窗口看门狗(以下简称WWDG)。
我们将使用窗口看门狗的中断功能来喂狗,通过LED0 和LED1 提示程序的运行状态。

WWDG 简介

窗口看门狗(WWDG)通常被用来监测由外部干扰或不可预见的逻辑条件造成的应用程序背离正常的运行序列而产生的软件故障。窗口看门狗跟独立看门狗一样,也是一个递减计数器,不同的是它们的复位条件不一样。窗口看门狗产生复位信号有两个条件:

1)当递减计数器的数值从0x40 减到0x3F 时(T6 位跳变到0)。
2)当喂狗的时候如果计数器的值大于W[6:0]时,此数值在WWDG_CFR 寄存器定义。
上述的两个条件详细解释是,当计数器的值减到0x40 时还不喂狗的话,到下一个计数就会
产生复位,这个值称为窗口的下限值,是固定的值,不能改变。这个跟独立看门狗类似,不同的是窗口看门狗的计数器的值在减到某一个数之前喂狗的话也会产生复位,这个值叫窗口的上限,上限值W[6:0]由用户设置。窗口看门狗计数器的上限值和下限值就是窗口的含义,喂狗也必须在窗口之内,否则就会复位。

WWDG 框图

下面先来认识WWDG 的逻辑结构,可以帮助我们快速认识窗口看门狗的工作原理,如图19.1.1.1 所示。
在这里插入图片描述
WWDG 有一个来自RCC 的PCLK1 输入时钟,经过一个4096 的分频器(4096 分频在设计时已经设定死了,图中并没有给出来,但我们可以通过查看寄存器WWDG_CFR 的WDGTB 位的描述知道),再经过一个分频系数可选(1、2、4、8)的可编程预分频器提供时钟给一个7 位递减计数器,这里有两个输出信号。

结合寄存器分析窗口看门狗的上限值和下限值。W[6:0] 是WWDG_CFR 寄存器的低7 位,用于与递减计数器T[6:0]比较的窗口值,也就是我们说的上限值,由用户设置。0x40 就是下限值,递减计数器达到这个值就会产生复位。T6 位就是WWDG_CR 寄存器的位6,即递减计数器T[6:0]的最高位。他们的关系可以用图19.1.1.2 来说明:

在这里插入图片描述

图19.1.1.2 可以看出,递减计数器的值递减过程中,当T[6:0]>W[6:0]是不允许刷新T[6:0]的值,即不允许喂狗,否则会产生复位。只有在W[6:0]<T[6:0]< 0x3F 这个时间可以喂狗,这就是喂狗的窗口时间。当T[6:0]=0x3F,即T6 位为0 这一刻,也会产生复位。
上限值W[6:0]是由用户自己设置,但是一定要确保大于0x40,否则就不存在上图的窗口了,下限值0x40 是固定的,不可修改。
知道了窗口看门狗的工作原理,下面学习如何计算窗口看门狗的超时公式:

在这里插入图片描述
在这里插入图片描述

WWDG 寄存器

WWDG 只有3 个寄存器,具体如下:
⚫ 控制寄存器(WWDG_CR)
窗口看门狗的控制寄存器描述如图19.1.2.1 所示:
在这里插入图片描述
该寄存器中的EWI 位是提前唤醒中断,如果该位置1,当递减计数器等于0x40 时产生提前唤醒中断,我们就可以及时喂狗以避免WWDG 复位。因此,我们一般都会用该位来设置中断,当窗口看门狗的计数器值减到0X40 的时候,如果该位设置,并开启了中断,则会产生中断,我们可以在中断里面向WWDG_CR 重新写入计数器的值,来达到喂狗的目的。注意这里在进入中断后,必须在不大于1 个窗口看门狗计数周期的时间(在pclk1 频率为36M 且WDGTB为0 的条件下,该时间为113us)内重新写WWDG_CR,否则,看门狗将产生复位!

⚫ 状态寄存器(WWDG_SR)
该寄存器用来记录当前是否有提前唤醒的标志。该寄存器仅有位0 有效,其他都是保留位。
当计数器值达到0x40 时,此位由硬件置1。它必须通过软件写0 来清除。对此位写1 无效。即使中断未被使能,在计数器的值达到0x40 的时候,此位也会被置1。

硬件设计

  1. 例程功能
    先点亮LED0 延时300ms 后,初始化窗口看门狗,进入死循环,关闭LED0。然后等待窗口看门狗中断的到来,在中断里面,喂狗,并执行LED1 的翻转操作。我们将通过LED0 来指示STM32F1 是否被复位了,如果被复位了就会点亮300ms。LED1 用来指示中断喂狗,每次中断喂狗翻转一次。
  2. 硬件资源
    1)LED 灯
    LED0 –PB5
    LED1 –PE5
    2)窗口看门狗
  3. 原理图
    窗口看门狗属于STM32F103 的内部资源,只需要软件设置好即可正常工作。我们通过LED0和LED1 来指示STM32F103 的复位情况和窗口看门狗的喂狗情况。

程序设计

WWDG 的HAL 库驱动

WWDG 在HAL 库中的驱动代码在stm32f1xx_hal_wwdg.c 文件(及其头文件)中。

  1. HAL_WWDG_Init 函数
    IWDG 的初始化函数,其声明如下:
HAL_StatusTypeDef HAL_WWDG_Init(WWDG_HandleTypeDef *hwwdg);

⚫ 函数描述:
用于初始化WWDG。
⚫ 函数形参:
形参1 是WWDG 句柄,WWDG_HandleTypeDef 结构体类型,其定义如下:

typedef struct
{
	WWDG_TypeDef *Instance; /* WWDG寄存器基地址*/
	WWDG_InitTypeDef Init; /* WWDG初始化参数*/
}WWDG_HandleTypeDef;

1)Instance:指向WWDG 寄存器基地址。
2)Init:WWDG 初始化结构体,用于配置计数器的相关参数。
WWDG_InitTypeDef 这个结构体类型定义如下:

typedef struct
{
	uint32_t Prescaler; /* 预分频系数*/
	uint32_t Window; /* 窗口值*/
	uint32_t Counter; /* 计数器值*/
	uint32_t EWIMode; /* 提前唤醒中断使能*/
} WWDG_InitTypeDef;

1)Prescaler:预分频系数,WWDG_PRESCALER_1\ WWDG_PRESCALER_2
WWDG_PRESCALER_4\ WWDG_PRESCALER_8 四个值,分别表示1\2\4\8 分频。
2)Window:窗口值,即上限值。
3)Counter:计数器值,用于保存要设置计数器的值。
4)EWIMode:提前唤醒中断使能。
⚫ 函数返回值:

HAL_StatusTypeDef 枚举类型的值。
2. HAL_WWDG_Refresh 函数
HAL_WWDG_Refresh 函数是窗口看门狗的喂狗函数。其声明如下:

HAL_StatusTypeDef HAL_WWDG_Refresh(WWDG_HandleTypeDef *hwwdg);

⚫ 函数描述:
该函数实际就是往CR 寄存器重写Counte 这个预先保存的计数器值。
⚫ 函数形参:
形参1 是WWDG_HandleTypeDef 结构体指针类型的WWDG 句柄。
⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值。
窗口看门狗配置步骤
1)使能WWDG 时钟
WWDG 不同于IWDG,IWDG 有自己独立的40Khz 时钟。而WWDG 使用的是PCLK1 的时钟,需要先使能时钟。方法是:
__HAL_RCC_WWDG_CLK_ENABLE();
2)设置窗口值,分频数和计数器初始值
在HAL 库中,这三个值都是通过函数HAL_WWDG_Init 来设置的,详见本例程源码。
3)开启WWDG
通过设置WWDG_CR 寄存器的WDGA(bit7)位为1 来实现开启窗口看门狗,同样是在HAL_WWDG_Init 函数里面实现。
4)使能中断通道并配置优先级(如果开启了WWDG 中断)
WWDG 的中断也是通过:HAL_NVIC_EnableIRQ 函数使能,通过HAL_NVIC_SetPriority函数设置优先级。
HAL 库同样为看门狗提供了MSP 回调函数HAL_WWDG_MspInit,一般情况下,步骤1 和步骤4 的步骤,我们均放在该回调函数中。
5)编写中断服务函数
在最后,还是要编写窗口看门狗的中断服务函数,通过该函数来喂狗,喂狗要快,否则当窗口看门狗计数器值减到0X3F 的时候,就会引起软复位了。在中断服务函数里面也要将状态寄存器的EWIF 位清空。
窗口看门狗中断服务函数为:WWDG_IRQHandler,喂狗函数为:HAL_WWDG_Refresh。
6)重写窗口看门狗唤醒中断处理回调函数HAL_WWDG_EarlyWakeupCallback
HAL 库定义了一个WWDG 中断处理共用函数HAL_WWDG_IRQHandler,我们在WWDG中断服务函数中会调用该函数。同时该函数会调用回调函数HAL_WWDG_EarlyWakeupCallback,提前唤醒中断逻辑(喂狗、闪灯)我们写在回调函数HAL_WWDG_EarlyWakeupCallback 中。

程序流程图

本实验利用窗口看门狗的特性,配置一个合适的窗口时间,并开启了提前唤醒中断,如果程序未在合适的时间喂狗,则会触发窗口看门狗中断。我们在进入中断后需要第一时间喂狗,否则系统将会复位,并用LED1 的翻转来表示一次未及时喂狗的事件。
下面的流程图表示了main 函数必要的编程步骤。

在这里插入图片描述

程序解析

  1. WWDG 驱动代码
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。窗口看门狗(WWDG)驱动源码包括两个文件:wdg.c 和wdg.h。
    wdg.h 头文件只有函数的声明,就不解释了。下面我们直接解析wdg.c 的程序,先看WWDG的初始化函数,其定义如下:
/**
* @brief 初始化窗口看门狗
* @param tr: T[6:0],计数器值
* @param tw: W[6:0],窗口值
* @param fprer: 分频系数(WDGTB),范围:WWDG_PRESCALER_1~WWDG_PRESCALER_8,
* 表示2^WDGTB分频, Fwwdg=PCLK1/(4096*2^fprer). 一般PCLK1=36Mhz
* @retval 无
*/
void wwdg_init(uint8_t tr, uint8_t wr, uint32_t fprer)
{
wwdg_handler.Instance = WWDG;
wwdg_handler.Init.Prescaler = fprer; /* 设置分频系数*/
wwdg_handler.Init.Window = wr; /* 设置窗口值*/
wwdg_handler.Init.Counter = tr; /* 设置计数器值*/
wwdg_handler.Init.EWIMode = WWDG_EWI_ENABLE; /* 使能窗口看门狗提前唤醒中断*/
HAL_WWDG_Init(&wwdg_handler); /* 初始化WWDG */
}

WWDG_Init 是独立看门狗初始化函数,主要设置预分频数、窗口值和计数器的值,以及选择是否使能窗口看门狗提前唤醒中断。
因为用到中断,我们用HAL_WWDG_MspInit 函数来编写窗口看门狗中断的初始化代码。
当然大家也可以HAL_WWDG_MspInit 函数的代码放到wwdg_init 函数里面。这个初始化框架就是HAL 库的特点。

void HAL_WWDG_MspInit(WWDG_HandleTypeDef *hwwdg)
{
	__HAL_RCC_WWDG1_CLK_ENABLE(); /* 使能窗口看门狗时钟*/
	HAL_NVIC_SetPriority(WWDG_IRQn, 2, 3); /* 抢占优先级2,子优先级为3 */
	HAL_NVIC_EnableIRQ(WWDG_IRQn); /* 使能窗口看门狗中断*/
}

HAL_WWDG_MspInit 函数会被HAL_WWDG_Init 函数调用。该函数使能窗口看门狗的时钟,并设置窗口看门狗中断的抢占优先级为2,响应优先级为3。

/**
* @brief 窗口看门狗中断服务程序
* @param 无
* @retval 无
*/
void WWDG_IRQHandler(void)
{
	HAL_WWDG_IRQHandler(&g_wwdg_handle);
}

WWDG_IRQHandler 函数是窗口看门狗中断服务函数,而这个函数实际上就是调用HAL 库的中断处理函数HAL_WWDG_IRQHandler。逻辑程序在下面的这个回调函数中:

void HAL_WWDG_EarlyWakeupCallback(WWDG_HandleTypeDef* hwwdg)
{
	HAL_WWDG_Refresh(&g_wwdg_handle);/* 更新窗口看门狗值*/
	LED1_TOGGLE(); /* LED1闪烁*/
}

在回调函数内部调用HAL_WWDG_Refresh 函数喂狗,并翻转LED1。

  1. main.c 代码
    在main.c 里面编写如下代码:
int main(void)
{
	HAL_Init(); /* 初始化HAL库*/
	sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
	delay_init(72); /* 延时初始化*/
	usart_init(115200); /* 串口初始化为115200 */
	led_init(); /* 初始化LED */
	LED0(0); /* 点亮LED0 红灯*/
	delay_ms(300); /* 延时300ms再初始化看门狗,LED0的变化"可见" */
	/* 计数器值为7f,窗口寄存器为5f,分频数为8 */
	wwdg_init(0X7F, 0X5F, WWDG_PRESCALER_8);
	while (1)
	{
		LED0(1); /* 关闭红灯*/
	}
}

在main 函数里,先初始化系统和用户的外设代码,然后先点亮LED0,延时300ms 后,初始化窗口看门狗,进入死循环,关闭LED0。
调用wwdg_init(0X7F,0X5F,WWDG_PRESCALER_8)这个语句,就设置计数器值为7f,窗口寄存器为5f,分频数为8,然后可由前面的公式得到窗口上限时间Twwdg=4096×8×(0x7F-0x5F)/36MHz=29.12ms,窗口下限时间Twwdg=4096×8×(0x7F-0x3F)/36MHz=58.25ms,即喂狗的窗口区间为29.12~58.25ms。我们在程序的其它地方没有喂狗,所以程序会在58.25ms 左右进入死前中断,我们在中断中喂狗一次,并翻转LED1。

下载验证

下载代码后,可以看到LED0 亮了一下就熄灭,紧接着LED1 开始不停的闪烁。可以接入示波器测试得每秒钟闪烁17 次左右,说明程序在中断不停的喂狗,和我们预期的一致。

RTC 实时时钟实验

本章,我们将介绍STM32F103 的内部实时时钟(RTC)。我们将使用LCD 模块来显示日期和时间,实现一个简单的实时时钟,并可以设置闹铃,另外还将介绍BKP 的使用。

RTC 时钟简介

STM32F103 的实时时钟(RTC)是一个独立的定时器。STM32 的RTC 模块拥有一组连续计数的计数器,在相对应的软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统的当前时间和日期。

RTC 模块和时钟配置系统(RCC_BDCR 寄存器)是在后备区域,即在系统复位或从待机模式唤醒后RTC 的设置和时间维持不变,只要后备区域供电(纽扣电池)正常,那么RTC 将可以一直运行。

但是在系统复位后,会自动禁止访问后备寄存器和RTC,以防止对后备区域(BKP)的意外写操作。所以在要设置时间之前,先要取消备份区域(BKP)写保护。

RTC 框图

下面先来学习RTC 框图,通过学习RTC 框图会有一个很好的整体掌握,同时对之后的编程也会有一个清晰的思路。RTC 的框图,如图27.1.1 所示:

在这里插入图片描述

我们在讲解RTC 架构之前,说明一下框图中浅灰色的部分,他们是属于备份域的,在VDD掉电时可在VBAT的驱动下继续工作,这部分包括RTC 的分频器,计数器以及闹钟控制器。在寄存器部分才展开解释一下备用域。下面把RTC 框图分成以下2 个部分讲解:

  • ①APB1 接口: 用来和APB1 总线相连。通过APB1 总线可以访问RTC 相关的寄存器,对其进行读写操作。
  • ②RTC 核心: 由一组可编程计数器组成,主要分成两个模块。第一个模块是RTC 的预分频模块,它可编程产生1 秒的RTC 时间基准TR_CLK。RTC 的预分频模块包括了一个20 位的可编程分频器(RTC 预分频器)。如果在RTC_CR 寄存器中设置相对应的允许位,则在每个TR_CLK 周期中RTC 产生一个中断(秒中断)。第二个模块是一个32 位的可编程计数器,可被初始化为当前的系统时间,一个32 位的时钟计数器,按秒钟计算,可以记录4294967296 秒,约合136 年左右,作为一般应用足够了。

RTC 还有一个闹钟寄存器RTC_ALR,用于产生闹钟。系统时间按TR_CLK 周期累加并与存储在RTC_ALR 寄存器中的可编程时间相比较,如果RTC_CR 控制寄存器中设置了相应允许位,比较匹配时将产生一个闹钟中断

由于备份域的存在,所以RTC 内核可以完全独立于RTC APB1 接口。而软件是通过APB1接口访问RTC 的预分频值、计数器值和闹钟值的。但是相关可读寄存器只在RTC APB1 时钟进行重新同步的RTC 时钟的上升沿被更新,RTC 标志也是如此。这就意味着,如果APB1 接口刚刚被开启之后,在第一次的内部寄存器更新之前,从APB1 上读取的RTC 寄存器值可能被破坏了(通常读到0)。因此,若在读取RTC 寄存器曾经被禁止的RTC APB1 接口,软件首先必须等待RTC_CRL 寄存器的RSF 位(寄存器同步标志位,bit3)被硬件置1。

RTC 寄存器

接下来,我们介绍本实验我们要用到的RTC 寄存器。

⚫ RTC 控制寄存器(RTC_CRH/CRL)

RTC 控制寄存器共有两个控制寄存器RTC_CRH 和RTC_CRL,两个都是16 位的。
RTC 控制寄存器高位RTC_CRH,描述如图27.1.2.1 所示:

在这里插入图片描述

该寄存器是RTC 控制寄存器高位,本章将用到秒钟中断,所以在该寄存器必须设置最低位为1,以允许秒钟中断。

RTC 控制寄存器低位RTC_CRL,描述如图27.1.2.2 所示:

在这里插入图片描述

该寄存器是RTC 控制寄存器低位,本章我们用到的是该寄存器的0,3~5 这几个位,第0位是秒钟标志位,我们在进入闹钟中断的时候,通过判断这位来决定是不是发生了秒钟中断。然后必须通过软件将该位清零(写零)。第3 位为寄存器同步标志位,我们在修改控制寄存器RTC_CRH/RTC_CRL 之前,必须先判断该位,是否已经同步了,如果没有则需要等待同步,在没同步的情况下修改RTC_CRH/RTC_CRL 的值是不行的。第4 位为配置标志位,在软件修改RTC_CNT/RTC_PRL 的值的时候,必须先软件置位该位,以允许进入配置模式。第5 位为RTC操作位,该位由硬件操作,软件只读。通过该位可以判断上次对RTC 寄存器的操作是否完成,如果没有,我们必须等待上一次操作结束才能开始下一次操作。

⚫ RTC 预分频装载寄存器(RTC_PRLH/RTC_PRLL)

RTC 预分频装载寄存器也是有两个寄存器组成,RTC_PRLH 和RTC_PRLL。这两个寄存器用来配置RTC 时钟的分频数的,比如我们使用外部32.768K 的晶振作为时钟的输入频率,那么我们要设置这两个寄存器的值为32767,得到一秒钟的计数频率。
RTC 预分频装载寄存器高位描述如图27.1.2.3 所示:

在这里插入图片描述

该寄存器是RTC 预分频装载寄存器高位,低四位有效用来存放PRL 的19~16 位。关于PRL的其余位存放在RTC_PRLL 寄存器。
RTC 预分频装载寄存器低位描述如图27.1.2.4 所示:

在这里插入图片描述

该寄存器是RTC 预分频装载寄存器低位,存放RTC 预分频装载值低位。如果输入时钟是32.768kHz,这个预分频寄存器中写入0x7FFF 可获得周期1 秒钟的信号。

⚫ RTC 预分频余数寄存器(RTC_DIVH/RTC_DIVL)

RTC 预分频余数寄存器是用来获得比秒钟更加准确的时钟,比如可以得到0.1 秒,甚至0.01秒等,该寄存器的值自减的,用于保存还需要多少时钟周期获得一个秒信号。在一次秒钟更新后,由硬件重新装载。这两个寄存器和RTC 预分频装载寄存器的各位是一样的,这里我们就不列出来了。

⚫ RTC 计数器寄存器(RTC_CNTH/RTC_CNTL)

RTC 计数器寄存器RTC_CNT,由2 个16 位寄存器组成RTC_CNTH 和RTC_CNTL,总共32 位,用来记录秒钟值。注意的是,修改这两个寄存器的时候要先进入配置模式。RTC_CNT 描述如图27.1.2.5 所示:

在这里插入图片描述

⚫ RTC 闹钟寄存器(RTC_ALRH/RTC_ALRL)

RTC 闹钟寄存器,该寄存器也是由2 个16 位的寄存器组成RTC_ALRH 和RTC_ALRL。总共32 位,用来标记闹钟产生的时间(以秒为单元)。对于STM32F1 系列的芯片来说,RTC 外设没有专门的年用日寄存器来分别存放这些信息,全部日期信息以秒的格式存储在这两个寄存器中,后面编程时会对时间进行特殊处理。如果RTC_CNT 的值与RTC_ALR 的值相等,并使能了中断的话,会产生一个闹钟中断。注意:该寄存器的修改也是要进入配置模式才能进行。
RTC 闹钟寄存器描述如图27.1.2.6 所示:

在这里插入图片描述

⚫ 备份数据寄存器(BKP_DRx)

备份数据寄存器描述如图27.1.2.6 所示。

在这里插入图片描述

该寄存器是一个16 位寄存器,可以用来存储用户数据,可在VDD 电源关闭时,通过VBAT保持上电状态。备份数据寄存器不会在系统复位、电源复位、从待机模式唤醒时复位。
那么在MCU 复位后,对RTC 和备份数据寄存器的写访问就被禁止,需要执行一下操作才可以对RTC 及备份数据寄存器进行写访问:

  1. 通过设置寄存器RCC_APB1ENR 的PWREN 和BKPEN 位来打开电源和后备接口时钟
  2. 电源控制寄存器(PWR_CR)的DBP 位来使能对后备寄存器和RTC 访问

⚫ 备份区域控制寄存器(RCC_BDCR)
备份区域控制寄存器描述如图25.1.2.7 所示。

在这里插入图片描述

RTC 的时钟源选择及使能设置都是通过这个寄存器来实现的,所以我们在RTC 操作之前先要通过这个寄存器选择RTC 的时钟源,然后才能开始其他的操作。

硬件设计

  1. 例程功能
    本实验通过LCD 显示RTC 时间,并可以通过usmart 设置RTC 时间,从而调节时间,或设置RTC 闹钟,还可以写入或者读取RTC 后备区域SRAM。LED0 闪烁,提示程序运行。

  2. 硬件资源
    1)LED 灯
    LED0 – PE5
    LED1 – PB5
    2)串口1(PA9/PA10 连接在板载USB 转串口芯片CH340 上面)
    3)RTC(实时时钟)
    4)正点原子2.8/3.5/4.3/7/10 寸TFTLCD 模块(仅限MCU 屏,16 位8080 并口驱动)

  3. 原理图
    RTC 属于STM32F103 内部资源,通过软件设置好就可以了。不过RTC 不能断电,否则数据就丢失了,我们如果想让时间在断电后还可以继续走,那么必须确保开发板的电池有电。

程序设计

RTC 的HAL 库驱动

RTC 在HAL 库中的驱动代码在stm32f1xx_hal_rtc.c 文件(及其头文件)中。下面介绍几个重要的RTC 函数,其他没有介绍的请看源码。

  1. HAL_RTC_Init 函数
    RTC 的初始化函数,其声明如下:
HAL_StatusTypeDef HAL_RTC_Init(RTC_HandleTypeDef *hrtc);

⚫ 函数描述:
用于初始化RTC。
⚫ 函数形参:
形参1 是RTC_HandleTypeDef 结构体类型指针变量,其定义如下:

typedef struct
{
	RTC_TypeDef *Instance; 			/* 寄存器基地址*/
	RTC_InitTypeDef Init; 			/* RTC配置结构体*/
	RTC_DateTypeDef DataToUpdata; 	/* RTC 日期结构体*/
	HAL_LockTypeDef Lock; 			/* RTC锁定对象*/
	__IO HAL_RTCStateTypeDef State; /* RTC设备访问状态*/
}RTC_HandleTypeDef;

1)Instance:指向RTC 寄存器基地址。
2)Init:是真正的RTC 初始化结构体,其结构体类型RTC_InitTypeDef 定义如下:

typedef struct
{
	uint32_t AsynchPrediv; /* 异步预分频系数*/
	uint32_t OutPut; 		/* 选择连接到RTC_ALARM输出的标志*/
}RTC_InitTypeDef;

AsynchPrediv 用来设置RTC 的异步预分频系数,也就是设置两个预分频重载寄存器的相关位,因为异步预分频系数是19 位,所以最大值为0x7FFFF,不能超过这个值。
OutPut 用来选择RTC 输出到Tamper 引脚的信号,取值为:RTC_OUTPUTSOURCE_NONE(没有输出),RTC_OUTPUTSOURCE_CALIBCLOCK(RTC 时钟经过64分频输出到TAMPER),RTC_OUTPUTSOURCE_ALARM(闹钟脉冲信号输出)和RTC_ OUTPUTSOURCE_SECOND(秒脉冲信号输出)。本实验选择没有输出,不配置即默认值,RTC_OUTPUTSOURCE_NONE。
3)DataToUpdata:日期结构体。
4)Lock:用于配置锁状态。
5)State:RTC 设备访问状态。
⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值。

注意:实验中没有使用HAL 库自带的设置RTC 时间的函数HAL_RTC_SetTime、设置RTC日期的函数HAL_RTC_SetDate、获取当前RTC 日期的函数HAL_RTC_GetTime。原因在于版本的HAL 库函数不满足我们同时更新年月日时分秒的要求,且在实测中发现写时间会覆盖日期,写日期亦然,所以我们直接通过操作寄存器的方式去编写功能更加全面的函数。

RTC 配置步骤

1)使能电源时钟,并使能RTC 及RTC 后备寄存器写访问。
我们要访问RTC 和RTC 备份区域就必须先使能电源时钟,然后使能RTC 即后备区域访问。电源时钟使能,通过RCC_APB1ENR 寄存器来设置;RTC 及RTC 备份寄存器的写访问,通过PWR_CR 寄存器的DBP 位设置。HAL 库设置方法为:

__HAL_RCC_PWR_CLK_ENABLE(); /* 使能电源时钟PWR */
__HAL_RCC_BKP_CLK_ENABLE(); /* 使能备份时钟*/
HAL_PWR_EnableBkUpAccess(); /* 取消备份区域写保护*/

2)开启外部低速振荡器LSE,选择RTC 时钟,并使能。
调用HAL_RCC_OscConfig 函数配置开启LSE。
调用HAL_RCCEx_PeriphCLKConfig 函数选择RTC 时钟源。
使能RTC 时钟函数为__HAL_RCC_RTC_ENABLE。
3)初始化RTC,设置RTC 的分频,以及配置RTC 参数。
在HAL 中,通过函数HAL_RTC_Init 函数配置RTC 分频系数,以及RTC 的工作参数。
注意:该函数会调用HAL_RTC_MspInit 函数来完成对RTC 的底层初始化,包括:RTC 时钟使能,时钟源选择等。
4)设置RTC 的日期和时间。
根据我们前面的说明,我们使用操作寄存器的方式重新定义了设置RTC 日期和时间的函数rtc_set_time,使用该函数就可以设置年月日时分秒。
5)获取RTC 当前日期和时间。
同样的,获取RTC 当前日期和时间的函数rtc_get_time,我们也是直接重新定义。该函数不直接返回时间,而是把时间保存在我们定义的时间结构体里。
通过以上5 个步骤,我们就完成了对RTC 的配置,RTC 即可正常工作,这些操作不是每次上电都必须执行的,视情况而定。我们还可以设置闹钟,这些将在后面介绍。

27.3.2 程序流程图

在这里插入图片描述

程序解析

  1. RTC 驱动代码
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。RTC 驱动源码包括两个文件:rtc.c 和rtc.h。
    限于篇幅,rtc.c 中的代码,我们不全部贴出了,只针对几个重要的函数进行介绍。
    rtc.h 头文件只有函数的声明,下面我们直接介绍rtc.c 的程序,先看RTC 的初始化函数,其定义如下:
/**
 * @brief RTC初始化
 * @note 默认尝试使用LSE,当LSE启动失败后,切换为LSI.
 * 通过BKP寄存器0的值,可以判断RTC使用的是LSE/LSI:
 * 当BKP0==0X5050时,使用的是LSE
 * 当BKP0==0X5051时,使用的是LSI
 * 注意:切换LSI/LSE将导致时间/日期丢失,切换后需重新设置.
 *
 * @param 无
 * @retval 0,成功
 * 1,进入初始化模式失败
 */
uint8_t rtc_init(void)
{
        /* 检查是不是第一次配置时钟*/
        uint16_t bkpflag = 0;
        __HAL_RCC_PWR_CLK_ENABLE(); /* 使能电源时钟*/
        __HAL_RCC_BKP_CLK_ENABLE(); /* 使能备份时钟*/
        HAL_PWR_EnableBkUpAccess(); /* 取消备份区写保护*/
        bkpflag = rtc_read_bkr(0); /* 读取BKP0的值*/
        g_rtc_handle.Instance = RTC;
        /*时钟周期设置,理论值:32767, 这里也可以用RTC_AUTO_1_SECOND */
        g_rtc_handle.Init.AsynchPrediv = 32767;
        if (HAL_RTC_Init(&g_rtc_handle) != HAL_OK)
        {
                return 1;
        }
        /* 之前未初始化过,重新配置*/
        if ((bkpflag != 0x5050) && (bkpflag != 0x5051))
        {
                rtc_set_time(2020, 4, 26, 9, 22, 35); /* 设置时间*/
        }
        __HAL_RTC_ALARM_ENABLE_IT(&g_rtc_handle, RTC_IT_SEC); /* 允许秒中断*/
        HAL_NVIC_SetPriority(RTC_IRQn, 0x2, 0); /* 优先级设置* /
                                                   HAL_NVIC_EnableIRQ(RTC_IRQn); /* 使能RTC中断通道*/
        rtc_get_time(); /* 更新时间*/
        return 0;
}

该函数用来初始化RTC 配置以及日期和时钟,但是只在第一次的时候设置时间,以后如果重新上电/复位都不会再进行时间设置了(前提是备份电池有电)。在第一次配置的时候,我们是按照上面介绍的RTC 初始化步骤调用函数HAL_RTC_Init 来实现的。
我们通过读取BKP 寄存器0 的值来判断是否需要进行时间的设置,对BKP 寄存器0 的写操作是在HAL_RTC_MspInit 回调函数中实现,下面会讲。第一次未对RTC 进行初始化BKP 寄存器0 的值非0x5050 非0x5051,当进行RTC 初始化时,BKP 寄存器0 的值就是0x50 50 或0x5051,所以以上代码操作确保时间只会设置一次,复位时不会重新设置时间。电池正常供电时,我们设置的时间不会因复位或者断电而丢失。
读取后备寄存器的函数其实还是调用HAL 库提供的函数接口,写后备寄存器函数同样也是。这两个函数如下:

uint32_t HAL_RTCEx_BKUPRead(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister);
void HAL_RTCEx_BKUPWrite(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister,
						uint32_t Data);

这两个函数的使用方法就非常简单,分别用来读和写BKR 寄存器的值。这里我们只是略微点到为止,详看例程源码。
这里设置时间和日期,是通过rtc_set_time 函数来实现的,我们之所以不是用HAL 库自带的设置时间和日期的函数,在前面已经提到了这个原因,就不用多说了。那么rtc_set_time 是我们直接操作寄存器,同时,它也可以为我们的USMART 所调用,十分方便我们调试时候使用。
接下来,我们用HAL_RTC_MspInit 函数来编写RTC 时钟配置等代码,其定义如下:

void HAL_RTC_MspInit(RTC_HandleTypeDef *hrtc)
{
        uint16_t retry = 200;
        __HAL_RCC_RTC_ENABLE(); /* RTC时钟使能*/
        RCC_OscInitTypeDef rcc_oscinitstruct;
        RCC_PeriphCLKInitTypeDef rcc_periphclkinitstruct;
        /* 使用寄存器的方式去检测LSE是否可以正常工作*/
        RCC->BDCR |= 1 << 0; /* 开启外部低速振荡器LSE */
        while (retry && ((RCC->BDCR & 0X02) == 0)) /* 等待LSE准备好*/
        {
                retry--;
                delay_ms(5);
        }
        if (retry == 0) /* LSE起振失败使用LSI */
        { /* 选择要配置的振荡器*/
                rcc_oscinitstruct.OscillatorType = RCC_OSCILLATORTYPE_LSI;
                rcc_oscinitstruct.LSEState = RCC_LSI_ON; /* LSI状态:开启*/
                rcc_oscinitstruct.PLL.PLLState = RCC_PLL_NONE; /* PLL无配置*/
                HAL_RCC_OscConfig(&rcc_oscinitstruct);
                /* 选择要配置的外设RTC */
                rcc_periphclkinitstruct.PeriphClockSelection = RCC_PERIPHCLK_RTC;
                /* RTC时钟源选择LSI */
                rcc_periphclkinitstruct.RTCClockSelection = RCC_RTCCLKSOURCE_LSI;
                HAL_RCCEx_PeriphCLKConfig(&rcc_periphclkinitstruct);
                rtc_write_bkr(0, 0X5051);
        }
        else
        {
                rcc_oscinitstruct.OscillatorType = RCC_OSCILLATORTYPE_LSE ;
                rcc_oscinitstruct.LSEState = RCC_LSE_ON; /* LSE状态:开启*/
                rcc_oscinitstruct.PLL.PLLState = RCC_PLL_NONE; /* PLL不配置*/
                rcc_periphclkinitstruct.PeriphClockSelection = RCC_PERIPHCLK_RTC;
                rcc_periphclkinitstruct.RTCClockSelection = RCC_RTCCLKSOURCE_LSE;
                HAL_RCCEx_PeriphCLKConfig(&rcc_periphclkinitstruct);
                rtc_write_bkr(0, 0X5050);
        }
}

介绍完RTC 初始化相关函数后,我们来介绍一下rtc_set_time 函数,代码如下:

/**
* @brief 设置时间, 包括年月日时分秒
* @note 以1970年1月1日为基准, 往后累加时间
* 合法年份范围为: 1970 ~ 2105年
HAL默认为年份起点为2000年
* @param syear : 年份
* @param smon : 月份
* @param sday : 日期
* @param hour : 小时
* @param min : 分钟
* @param sec : 秒钟
* @retval 0, 成功; 1, 失败;
*/
uint8_t rtc_set_time(uint16_t syear, uint8_t smon, uint8_t sday, uint8_t hour, uint8_t min, uint8_t sec)
{
	uint32_t seccount = 0;
	/* 将年月日时分秒转换成总秒钟数*/
	seccount = rtc_date2sec(syear, smon, sday, hour, min, sec);
	__HAL_RCC_PWR_CLK_ENABLE(); /* 使能电源时钟*/
	__HAL_RCC_BKP_CLK_ENABLE(); /* 使能备份域时钟*/
	HAL_PWR_EnableBkUpAccess(); /* 取消备份域写保护*/
	/* 上面三步是必须的! */
	RTC->CRL |= 1 << 4; /* 进入配置模式*/
	RTC->CNTL = seccount & 0xffff;
	RTC->CNTH = seccount >> 16;
	RTC->CRL &= ~(1 << 4); /* 退出配置模式*/
	/* 等待RTC寄存器操作完成, 即等待RTOFF == 1 */
	while (!__HAL_RTC_ALARM_GET_FLAG(&g_rtc_handle, RTC_FLAG_RTOFF));
	return 0;
}

该函数用于设置时间,把我们输入的时间,转换为以1970 年1 月1 日0 时0 分0 秒做起始时间的秒钟信号,后续的计算都以这个时间为基准,由于STM32 的秒钟计数器可以保存136年的秒钟数据,这样我们就可以计时到2106 年。

接着,我们介绍rtc_set_alarma 函数,该函数用于设置闹钟时间,同rtc_set_time 函数几乎一模一样,主要区别:就是将RTC→CNTL和RTC→CNTH 换成了RTC→ALRL和RTC→ALRH,用于设置闹钟时间。RTC 其实是有闹钟中断的,我们这里并没有用到,本实验用到了秒中断,所以在秒中断里顺带处理闹钟中断的事情。具体代码请参考本例程源码。

特别提醒:假如只是使用HAL 库的__HAL_RTC_ALARM_ENABLE_IT 函数来使能闹钟中断,但是没有设置闹钟相关的NVIC 和EXTI,实际上不会产生闹钟中断,只会产生闹钟标志(RTC->CRL 的ALRL 置位)。可以通过读取闹钟标志来判断是否发生闹钟事件。

接着,我们介绍一下rtc_get_time 函数,其定义如下:

/**
 * @brief 得到当前的时间
 * @note 该函数不直接返回时间, 时间数据保存在calendar结构体里面
 * @param 无
 * @retval 无
 */
void rtc_get_time(void)
{
        static uint16_t daycnt = 0;
        uint32_t seccount = 0;
        uint32_t temp = 0;
        uint16_t temp1 = 0;
        /* 平年的月份日期表*/
        const uint8_t month_table[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
        seccount = RTC->CNTH; /* 得到计数器中的值(秒钟数) */
        seccount <<= 16;
        seccount += RTC->CNTL;
        temp = seccount / 86400; /* 得到天数(秒钟数对应的) */
        if (daycnt != temp) /* 超过一天了*/
        {
                daycnt = temp;
                temp1 = 1970; /* 从1970年开始*/
                while (temp >= 365)
                {
                        if (rtc_is_leap_year(temp1)) /* 是闰年*/
                        {
                                if (temp >= 366)
                                {
                                        temp -= 366; /* 闰年的秒钟数*/
                                }
                                else
                                {
                                        break;
                                }
                        }
                        else
                        {
                                temp -= 365; /* 平年*/
                        }
                        temp1++;
                }
                calendar.year = temp1; /* 得到年份*/
                temp1 = 0;
                while (temp >= 28) /* 超过了一个月*/
                {
                        /* 当年是不是闰年/2月份*/
                        if (rtc_is_leap_year(calendar.year) && temp1 == 1)
                        {
                                if (temp >= 29)
                                {
                                        temp -= 29; /* 闰年的秒钟数*/
                                }
                                else
                                {
                                        break;
                                }
                        }
                        else
                        {
                                if (temp >= month_table[temp1])
                                {
                                        temp -= month_table[temp1]; /* 平年*/
                                }
                                else
                                {
                                        break;
                                }
                        }
                        temp1++;
                }
                calendar.month = temp1 + 1; /* 得到月份*/
                calendar.date = temp + 1; /* 得到日期*/
        }
        temp = seccount % 86400; /* 得到秒钟数*/
        calendar.hour = temp / 3600; /* 小时*/
        calendar.min = (temp % 3600) / 60; /* 分钟*/
        calendar.sec = (temp % 3600) % 60; /* 秒钟*/
        /* 获取星期*/
        calendar.week = rtc_get_week(calendar.year, calendar.month, calendar.date);
}

该函数其实就是将存储在秒钟寄存器RTC->CNTL 和RTC->CNTH 中的秒钟数据转换为真正的时间和日期。该代码还用到了一个calendar 的结构体,calendar 是我们在rtc.h 里面将要定义的一个时间结构体,用来存放时钟的年月日时分秒等信息。因为STM32 的RTC 只有秒钟计数器,而年月日,时分秒则需要我们自己软件计算。我们把计算好的值保存在calendar 里面,方便其他函数调用。

接着,我们介绍一下使用多次数最多的函数rtc_date2sec,该函数代码如下:

/**
 * @brief 将年月日时分秒转换成秒钟数
 * @note 以1970年1月1日为基准, 1970年1月1日, 0时0分0秒, 表示第0秒钟
 * 最大表示到2105年, 因为uint32_t最大表示136年的秒钟数(不包括闰年)!
 * 本代码参考只linux mktime函数, 原理说明见此贴:
 * http://www.openedv.com/thread-63389-1-1.html
 * @param syear : 年份
 * @param smon : 月份
 * @param sday : 日期
 * @param hour : 小时
 * @param min : 分钟
 * @param sec : 秒钟
 * @retval 转换后的秒钟数
 */
static long rtc_date2sec(uint16_t syear, uint8_t smon, uint8_t sday,
                uint8_t hour, uint8_t min, uint8_t sec)
{
        uint32_t Y, M, D, X, T;
        signed char monx = smon; /* 将月份转换成带符号的值, 方便后面运算*/
        if (0 >= (monx -= 2)) /* 1..12 -> 11,12,1..10 */
        {
                monx += 12; /* Puts Feb last since it has leap day */
                syear -= 1;
        }
        /* 公元元年1到现在的闰年数*/
        Y = (syear - 1) * 365 + syear / 4 - syear / 100 + syear / 400;
        M = 367 * monx / 12 - 30 + 59;
        D = sday - 1;
        X = Y + M + D - 719162; /* 减去公元元年到1970年的天数*/
        T = ((X * 24 + hour) * 60 + min) * 60 + sec; /* 总秒钟数*/
        return T;
}

该函数参考了linux 的mktime 函数,用于将年月日时分秒转化成秒钟数,进而被其他函数使用,例如rtc_set_time 和rtc_set_alarm,那两个函数的形参是需要使用rtc_date2sec 函数获取秒钟数,进而操作寄存器的方法把总秒数写入特定的寄存器完成相对应的功能。

前面介绍的函数rtc_init 中,存在RTC 中断使能操作,那么这里必定是有中断服务函数,接下来看中断服务函数,代码如下:

/**
 * @brief RTC时钟中断
 * @note 秒钟中断服务函数,顺带处理闹钟标志
 * 根据RTC_CRL寄存器的SECF 和ALRF 位区分是哪个中断
 * @param 无
 * @retval 无
 */
void RTC_IRQHandler(void)
{
        if (__HAL_RTC_ALARM_GET_FLAG(&g_rtc_handle,RTC_FLAG_SEC) != RESET)/*秒中断*/
        {
                rtc_get_time(); /* 更新时间*/
                __HAL_RTC_ALARM_CLEAR_FLAG(&g_rtc_handle, RTC_FLAG_SEC); /* 清除秒中断*/
                //printf("sec:%d\r\n", calendar.sec); /* 打印秒钟*/
        }
        /* 顺带处理闹钟标志*/
        if (__HAL_RTC_ALARM_GET_FLAG(&g_rtc_handle, RTC_FLAG_ALRAF) != RESET)
        {
                __HAL_RTC_ALARM_CLEAR_FLAG(&g_rtc_handle, RTC_FLAG_ALRAF); /*清除闹钟标志*/
                printf("Alarm Time:%d-%d-%d %d:%d:%d\n", calendar.year, calendar.month, calendar.date, calendar.hour, calendar.min, calendar.sec);
        }
        __HAL_RTC_ALARM_CLEAR_FLAG(&g_rtc_handle, RTC_FLAG_OW); /* 清除溢出中断标志*/
        /* 等待RTC寄存器操作完成, 即等待RTOFF == 1 */
        while (!__HAL_RTC_ALARM_GET_FLAG(&g_rtc_handle, RTC_FLAG_RTOFF));
}

RTC_IRQHandle 中断服务函数用于RTC 秒中断的,由于在rtc_init 中已经配置好了时钟周期为1 秒,所以每一秒都会跳进RTC 中断服务函数中。在函数中,判断秒中断是否触发,由于每一次都是秒中断触发,所以可以先更新时间,然后把printf 的注释去掉看一下效果,是不是每一秒打印一下。接着判断闹钟标志是否置位,这个闹钟标志跟我们的rtc_set_alarm 函数有关,假设时间到了闹钟设置的时间,就会跳进该秒中断中顺带处理闹钟标志,执行函数体的指令。执行完上述的任务之后,需要在最后清除溢出中断标志。

rtc.c 的其他程序,这里就不再介绍了,请大家直接看源码。

  1. main.c 代码
    在main.c 里面编写如下代码:
/* 定义字符数组用于显示周*/
char* weekdays[]={"Sunday","Monday","Tuesday","Wednesday",
        "Thursday","Friday","Saterday"};
int main(void)
{
        uint8_t tbuf[40];
        uint8_t t = 0;
        HAL_Init(); /* 初始化HAL库*/
        sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
        delay_init(72); /* 延时初始化*/
        usart_init(115200); /* 串口初始化为115200 */
        usmart_dev.init(72); /* 初始化USMART */
        led_init(); /* 初始化LED */
        lcd_init(); /* 初始化LCD */
        rtc_init(); /* 初始化RTC */
        rtc_set_alarm(2020, 4, 26, 9, 23, 45); /* 设置一次闹钟*/
        lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
        lcd_show_string(30, 70, 200, 16, 16, "RTC TEST", RED);
        lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
        while (1)
        {
                t++;
                if ((t % 10) == 0) /* 每100ms更新一次显示数据*/
                {
                        rtc_get_time();
                        sprintf((char *)tbuf, "Time:%02d:%02d:%02d", calendar.hour,
                                        calendar.min, calendar.sec);
                        lcd_show_string(30, 120, 210, 16, 16, (char *)tbuf, RED);
                        sprintf((char *)tbuf, "Date:%04d-%02d-%02d", calendar.year,
                                        calendar.month, calendar.date);
                        lcd_show_string(30, 140, 210, 16, 16, (char *)tbuf, RED);
                        sprintf((char *)tbuf, "Week:%s", weekdays[calendar.week]);
                        lcd_show_string(30, 160, 210, 16, 16, (char *)tbuf, RED);
                }
                if ((t % 20) == 0)
                {
                        LED0_TOGGLE(); /* 每200ms,翻转一次LED0 */
                }
                delay_ms(10);
        }
}

我们在无限循环中每100ms 读取RTC 的时间和日期(一次),并显示在LCD 上面。每200ms,
翻转一次LED0。

为方便RTC 相关函数的调用验证,在usmart_config.c 里面,修改了usmart_nametab 如下:

/* 函数名列表初始化(用户自己添加)
 * 用户直接在这里输入要执行的函数名及其查找串
 */
struct _m_usmart_nametab usmart_nametab[] =
{
#if USMART_USE_WRFUNS == 1 /* 如果使能了读写操作*/
        (void *)read_addr, "uint32_t read_addr(uint32_t addr)",
        (void *)write_addr, "void write_addr(uint32_t addr,uint32_t val)",
#endif
        (void *)delay_ms, "void delay_ms(uint16_t nms)",
        (void *)delay_us, "void delay_us(uint32_t nus)",
        (void *)rtc_read_bkr, "uint16_t rtc_read_bkr(uint32_t bkrx)",
        (void *)rtc_write_bkr, "void rtc_write_bkr(uint32_t bkrx, uint16_t data)",
        (void *)rtc_get_week, "uint8_t rtc_get_week(uint16_t year, uint8_t month, uint8_t day)",
        (void *)rtc_set_time, "uint8_t rtc_set_time(uint16_t syear, uint8_t smon,
        uint8_t sday, uint8_t hour, uint8_t min, uint8_t sec)",
        (void *)rtc_set_alarm, "uint8_t rtc_set_alarm(uint16_t syear, uint8_t smon,
        uint8_t sday, uint8_t hour, uint8_t min, uint8_t sec)",
};

将RTC 的一些相关函数加入了usmart,这样通过串口就可以直接设置RTC 时间、闹钟。至此,RTC 的软件设计就完成了,接下来就让我们来检验一下,程序是否正确。

下载验证

将程序下载到开发板后,可以看到LED0 不停的闪烁,提示程序已经在运行了。然后,可以看到LCD 开始显示时间,实际显示效果如图27.4.1 所示:

在这里插入图片描述

如果时间不正确,可以利用上一章介绍的usmart 工具,通过串口来设置,并且可以设置闹钟时间等,如图27.4.2 所示:

在这里插入图片描述

按照图中编号1、2 顺序,设置闹钟、设置时间。然后等待我们设置的时间到来后,串口打印Alarm Time:2020-9-15 12:30:45 这个字符串,证明我们的闹钟程序正常运行了!

低功耗实验

本章,我们将介绍STM32F103 的电源控制(PWR),并实现低功耗模式相关功能。我们将通过四个实验来学习并实现低功耗相关功能,分别是PVD 电压监控实验、睡眠模式实验、停止模式实验和待机模式实验。

电源控制(PWR)简介

电源控制部分(PWR)概述了不同电源域的电源架构以及电源配置控制器。PWR 的内容比较多,我们把它们的主要特性概括为以下3 点:
电源系统:VDDA供电区域、VDD供电区域、1.8V 供电区域、后备供电区域。
电源监控:POR/PDR 监控器、PVD 监控器。
电源管理:低功耗模式。

下面将分别对这3 个特性进行简单介绍。

电源系统

为了方便对电源系统进行管理,设计者把STM32 的内核和外设等器件跟据功能划分了不同的电源区域,具体如图28.1.1.1 所示。

在这里插入图片描述

在电源概述框图中我们划分了3 个区域①②③,分别是独立的A/D 转换器供电和参考电压、电压调节器、电池备份区域。下面分别进行简单介绍:
①独立的A/D 转换器供电和参考电压(VDDA供电区域)
VDDA供电区域,主要是ADC 电源以及参考电压,STM32 的ADC 模块配备独立的供电方式,使用了VDDA引脚作为输入,使用VSSA引脚作为独立地连接,VREF引脚为提供给ADC 的参考电压。
②电压调节器(VDD /1.8V 供电区域)
电压调节器是STM32 的电源系统中最核心部分,连接VDD供电区域和1.8 供电区域。VDD供电来自于VSS和VDD,给I/O 电路以及待机电路供电,电压调节器主要为备份域以及待机电路以外的所有数字电路供电,其中包括内核、数字外设以及RAM,调节器的输出电压约为1.8V,因此由调压器供电的区域称为1.8V 供电区域。

电压调节器根据应用方式不同有三种不同的工作模式。在运行模式下,调节器以正常工作模式为内核、内存和外设提供1.8V;在停止模式下,调节器以低功耗模式提供1.8V 电源,以保存寄存器和SRAM 的内容。在待机模式下,调节器停止供电,除了备用电路和备份域外,寄存器和SRAM 的内容全部丢失。
③电池备份区域(后备供电区域)
电池备份区域也就是后备供电区域,使用电池或者其他电源连接到VBAT脚上,当VDD断电时,可以保存备份寄存器的内容和维持RTC 的功能。同时VBAT 引脚也为RTC 和LSE 振荡器供电,这保证了当主要电源被切断时,RTC 能够继续工作。切换到VBAT供电由复位模块中的掉电复位功能控制。

电源监控

电源监控的部分我们主要关注PVD 监控器,此外还需要知道上电复位(POR)/掉电复位(PDR)。其他部分的内容请大家查看《STM32F10xxx 参考手册_V10(中文版).pdf》第4.2 节(38 页)。
⚫ 上电复位(POR)/掉电复位(PDR)
上电时,当VDD低于指定VPOR阈值时,系统无需外部复位电路便会保持复位模式。一旦VDD电源电压高于VPOR阈值,系统便会退出复位状态,芯片正常工作。掉电时,当VDD低于指定VPDR阈值时,系统就会保持复位模式。如图28.1.2.1 所示,RESET 为上电复位信号。

注意:POR 与PDR 的复位电压阈值是固定的,VPOR阈值(典型值)为1.92V,VPDR阈值(典型值)为1.88V。

在这里插入图片描述
⚫ 可编程电压检测器(PVD)
上面介绍的POR、PDR 功能都是设置电压阈值与外部供电电压VDD比较,当VDD低于设置的电压阈值时,就会直接进入复位状态,防止电压不足导致的误操作。
下面介绍可编程电压检测器(PVD),它可以实时监视VDD的电压,方法是将VDD与PWR

控制寄存器(PWR_CR)中的PLS[2:0]位所选的VPVD阈值进行比较。其中PWR_CSR 寄存器中的PVDO 位决定了VDD是高于VPVD还是低于VPVD,本实验中配置的是VDD低于VPVD阈值这个条件。当检测到电压低于VPVD阈值时,如果使能EXTI16 线中断,即使能PVD 中断,可以产生PVD 中断,具体取决于EXTI16 线配置为检测上升还是下降沿,然后在复位前,在中断服务程序中执行紧急关闭系统等任务。PVD 阀值检测波形,如图28.1.2.2 所示。

在这里插入图片描述

PVD 阀值有8 个等级,有上升沿和下降沿的区别,具体由PWR_CSR 寄存器中的PVDO 位决定。PVD 阈值等级表具体如表28.1.2.1 所示。

在这里插入图片描述

电源管理

电源管理的部分我们要关注低功耗模式,在STM32 的正常工作中,具有四种工作模式,运行、睡眠、停止以及待机。在上电复位后,STM32 处于运行状态时,当内核不需要继续运行,就可以选择进入后面的三种模式降低功耗。这三种低功耗模式电源消耗不同、唤醒时间不同和唤醒源不同,我们要根据自身的需要选择合适的低功耗模式。

下面是低功耗模式汇总介绍,如下表所示。

在这里插入图片描述
下面对睡眠模式、停止模式和待机模式,分开介绍。
1、睡眠模式
进入睡眠模式,Cortex_M3 内核停止,所有外设包括Cortex_M3 核心的外设,如NVIC、系统时钟(SysTick)等仍在运行,有两种进入睡眠模式的模式WFI 和WFE。WFI(Wait for interrupt)和WFE(Wait for event)是内核指令,会调用一些汇编指令,我们会使用即可,更详细的描述可以查看《CM3 权威指南》。睡眠后唤醒的方式即由等待“中断”唤醒和“事件”唤醒。

在这里插入图片描述

2、停止模式
进入停止模式,所有的时钟都关闭,所有的外设也就停止了工作。但是VDD电源是没有关闭的,所以内核的寄存器和内存信息都保留下来,等待重新开启时钟就可以从上次停止的地方继续执行程序。
值得注意的是:当电压调节器处于低功耗模式下,当系统从停止模式退出时,将会有一段额外的启动延时。如果在停止模式期间保持内部调节器开启,则退出启动时间会缩短,但相应的功耗会增加。

在这里插入图片描述
3、待机模式
待机模式可实现最低功耗。该模式是在CM3 深睡眠模式时关闭电压调节器,整个1.8V 供电区域被断电。PLL、HSI 和HSE 振荡器也被断电。除备份域(RTC 寄存器、RTC 备份寄存器和备份SRAM)和待机电路中的寄存器外,SRAM 和其他寄存器内容都将丢失。不过如果我们使能了备份区域(备份SRAM、RTC、LSE),那么待机模式下的功耗,将达到3.8uA 左右。
那么我们如何进入待机模式呢?其实很简单,只要按表28.1.3.3 所示的步骤执行就可以了:
在这里插入图片描述

PVD 电压监控实验

本小节我们来学习PVD 电压监控实验,该部分的知识点内容请回顾28.1.2 电源监控。我们直接从寄存器介绍开始。

PWR 寄存器

本实验用到PWR 的部分寄存器,在《STM32F10XXX 参考手册(中文版)》的4.4 小节可以找到PWR 寄存器描述。这里我们只介绍PVD 电压监控实验用到的PWR 控制寄存器(PWR_CR),还有就是我们要用到EXTI16 线中断,所以还要配置EXTI 相关的寄存器,具体如下:
⚫ PWR 控制寄存器(PWR_CR)
PWR 控制寄存器描述如图28.2.1.1 所示:

在这里插入图片描述
位[7:5] PLS 用于设置PVD 检测的电压阀值,即前面我们介绍PVD 的8 个等级阀值选择。
位4 PVDE 位,用于使能或者禁止PVD 检测,显然我们要使能PVD 检测,该位置1。
这个寄存器还有其它的位我们没有列出来,也是跟电源相关的,如待机,掉电等,我们后面的实验再讲解这些功能,这里先跳过。
⚫ EXTI 中断屏蔽寄存器(EXTI_IMR)
EXTI 中断屏蔽寄存器描述如图28.2.1.2 所示:

在这里插入图片描述
我们要使用到EXTI16 线中断,所以MR16 位要置1,即开放来自EXTI16 线的中断请求。
⚫ EXTI 上升沿触发选择寄存器(EXTI_RTSR)
EXTI 上升沿触发选择寄存器描述如图28.2.1.3 所示:
在这里插入图片描述
我们要使用到EXTI16 线中断,所以TR16 位要置1,即允许EXTI16 线的上升沿触发。
⚫ EXTI 下降沿触发选择寄存器(EXTI_FTSR)
EXTI 下降沿触发选择寄存器描述如图28.2.1.4 所示:
在这里插入图片描述
我们要使用到EXTI16 线中断,所以TR16 位要置1,即允许EXTI16 线上的下降沿触发。
⚫ EXTI 挂起寄存器(EXTI_PR)
EXTI 挂起寄存器描述如图28.2.1.5 所示:
在这里插入图片描述
EXTI 挂起寄存器EXTI_PR 管理的是EXTI0 线到EXTI19 线的中断标志位。在PVD 中断服务函数里面,我们记得要对PR16 位写1,来清除EXTI16 线的中断标志。

硬件设计

  1. 例程功能
    开发板供电正常的话,LCD 屏会显示"PVD Voltage OK!"。当供电电压过低,则会通过PVD中断服务函数将LED1 点亮;当供电电压正常后,会在PVD 中断服务函数将LED1 熄灭。LED0闪烁,提示程序运行。
  2. 硬件资源
    1)LED 灯
    LED0 –PB5
    LED1 –PE5
    2)PVD(可编程电压监测器)
    3)正点原子2.8/3.5/4.3/7/10 寸TFTLCD 模块(仅限MCU 屏,16 位8080 并口驱动)
  3. 原理图
    PVD 属于STM32F103 的内部资源,只需要软件设置好即可正常工作。我们通过LED0 和
    LCD 来指示进入PVD 中断的情况。

程序设计

28.2.3.1 PWR 的HAL 库驱动
PWR 在HAL 库中的驱动代码在stm32f1xx_hal_pwr.c 文件(及其头文件)中。

  1. HAL_PWR_ConfigPVD 函数
    PVD 的初始化函数,其声明如下:
void HAL_PWR_ConfigPVD (PWR_PVDTypeDef *sConfigPVD);

⚫ 函数描述:
用于初始化PWR。
⚫ 函数形参:
形参1 是PWR_PVDTypeDef 结构体类型变量,其定义如下:

typedef struct
{
	uint32_t PVDLevel; /* 指定PVD检测级别*/
	uint32_t Mode; /* 指定PVD的EXTI检测模式*/
}PWR_PVDTypeDef;

1)PVDLevel:指向PVD 检测级别,对应PWR_CR 寄存器的PLS 位的设置,取值范围PWR_PVDLEVEL_0 到PWR_PVDLEVEL_7,共八个级别。
2)Mode:指定PVD 的EXTI 边沿检测模式。
⚫ 函数返回值:

PVD 电压监控配置步骤
1)配置PVD,使能PVD 时钟。
调用HAL_PWR_ConfigPVD 函数配置PVD,包括检测电压级别、使用中断线触发方式等。
2)使能PVD 检测,配置PVD/AVD 中断优先级,开启PVD 中断。
通过HAL_PWR_EnablePVD 函数使能PVD 检测。
通过HAL_NVIC_EnableIRQ 函数使能PVD 中断。
通过HAL_NVIC_SetPriority 函数设置中断优先级。
3)编写中断服务函数。
PVD 中断服务函数为PVD_IRQHandler,当发生中断的时候,程序就会执行中断服务函数。
HAL 库有专门的PVD 中断处理函数,我们只需要在PVD 中断服务函数里面调用
HAL_PWR_PVD_IRQHandler() 函数,然后逻辑代码在PVD 中断服务回调函数
HAL_PWR_PVDCallback 中编写,详见本例程源码。

28.2.3.2 程序流程图

在这里插入图片描述

程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。PWR 源码包括两个文件:pwr.c 和pwr.h。该章节有四个实验,每一个实验的代码都是在上一个实验后面追加。
pwr.h 头文件只有函数声明,下面直接开始介绍pwr.c 的程序,首先是PVD 初始化函数。

/**
* @brief 初始化PVD电压监视器
* @param pls: 电压等级(PWR_PVD_detection_level)
* @arg PWR_PVDLEVEL_0,2.2V;
* @arg PWR_PVDLEVEL_1,2.3V;
* @arg PWR_PVDLEVEL_2,2.4V;
* @arg PWR_PVDLEVEL_3,2.5V;
* @arg PWR_PVDLEVEL_4,2.6V;
* @arg PWR_PVDLEVEL_5,2.7V;
* @arg PWR_PVDLEVEL_6,2.8V;
* @arg PWR_PVDLEVEL_7,2.9V;
* @retval 无
*/
void pwr_pvd_init(uint32_t pls)
{
	PWR_PVDTypeDef pwr_pvd = {0};
	__HAL_RCC_PWR_CLK_ENABLE(); /* 使能PWR时钟*/
	pwr_pvd.PVDLevel = pls; /* 检测电压级别*/
	/* 使用中断线的上升沿和下降沿双边缘触发*/
	pwr_pvd.Mode = PWR_PVD_MODE_IT_RISING_FALLING;
	HAL_PWR_ConfigPVD(&pwr_pvd);
	HAL_NVIC_SetPriority(PVD_IRQn, 3 ,3);
	HAL_NVIC_EnableIRQ(PVD_IRQn);
	HAL_PWR_EnablePVD(); /* 使能PVD检测*/
}

这里需要注意的就是PVD 中断线选择的是上升沿和下降沿双边沿触发,其他的内容前面已经讲过。
下面介绍的是PVD 中断服务函数及其回调函数,函数定义如下:

/**
 * @brief PVD中断服务函数
 * @param 无
 * @retval 无
 */
void PVD_IRQHandler(void)
{
        HAL_PWR_PVD_IRQHandler();
}
/**
 * @brief PVD中断服务回调函数
 * @param 无
 * @retval 无
 */
void HAL_PWR_PVDCallback(void)
{
        if (__HAL_PWR_GET_FLAG(PWR_FLAG_PVDO)) /* 电压比PLS所选电压还低*/
        {
                /* LCD显示电压低*/
                lcd_show_string(30, 130, 200, 16, 16, "PVD Low Voltage!", RED);
                LED1(0); /* 点亮LED1, 表明电压低了*/
        }
        else
        {
                /* LCD显示电压正常*/
                lcd_show_string(30, 130, 200, 16, 16, "PVD Voltage OK! ", BLUE);
                LED1(1); /* 灭掉绿灯*/
        }
}

HAL_PWR_PVDCallback 回调函数中首先是判断VDD电压是否比PLS 所选电压还低,是的话,就在LCD 显示PVD Low Voltage!并且点亮LED1,否则,在LCD 显示PVD Voltage OK!并且关闭LED1。
在main 函数里面编写如下代码:

int main(void)
{
	uint8_t t = 0;
	HAL_Init(); /* 初始化HAL库*/
	sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
	delay_init(72); /* 延时初始化*/
	usart_init(115200); /* 串口初始化为115200 */
	led_init(); /* 初始化LED */
	lcd_init(); /* 初始化LCD */
	pwr_pvd_init(PWR_PVDLEVEL_7); /* PVD 2.9V检测*/
	lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
	lcd_show_string(30, 70, 200, 16, 16, "PVD TEST", RED);
	lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
	/* 默认LCD显示电压正常*/
	lcd_show_string(30, 130, 200, 16, 16, "PVD Voltage OK! ", BLUE);
	while (1)
	{
		if ((t % 20) == 0)
		{
			LED0_TOGGLE(); /* 每200ms,翻转一次LED0 */
		}
		delay_ms(10);
		t++;
	}
}

这里我们选择PVD 的检测电压阀值为2.9V,其他的代码很好理解,最后下载验证一下。

下载验证

下载代码后,默认LCD 屏会显示"PVD Voltage OK!“,当供电电压过低,则LED1 会点亮,并且LCD 屏会显示PVD Low Voltage!。当开发板供电正常,LED1 会熄灭,LCD 屏会继续显示"PVD Voltage OK!”。

睡眠模式实验

本小节我们来学习睡眠模式实验,该部分的知识点内容请回顾28.1.2.3 电源管理。我们直接从寄存器介绍开始。

EXTI 寄存器

本实验我们用到外部中断来唤醒睡眠模式。进入睡眠模式很简单,直接调用内核指令WFI(参考28.1.3 电源管理对这个指令的讲解)即可进入。用外部中断唤醒,就要在进入睡眠模式前,先对外部中断进行配置。例如,使能中断线(EXTI_IMR ),使用何种触发模式(EXTI_FTSR/EXTI_RTSR)。当中断触发(即EXTI_PR 中某位能查询到1),跳转到中断服务函数里,最后还得手动清除该标记即(EXTI_PR 中对某位进行置1 处理)。
⚫ EXTI 中断屏蔽寄存器(EXTI_IMR)
EXTI 中断屏蔽寄存器描述如图28.3.1.1 所示:
在这里插入图片描述

本实验使用WK_UP(PA0)唤醒,即EXTI0 线中断,所以在外部中断服务函数要把MR0位置1。
⚫ EXTI 上升沿触发选择寄存器(EXTI_RTSR)
EXTI 上升沿触发选择寄存器描述如图28.3.1.2 所示:
在这里插入图片描述
我们要使用到EXTI0 线中断,所以TR0 位要置1,即EXTI0 使用的是上升沿进行触发。
⚫ EXTI 挂起寄存器(EXTI_PR)
EXTI 挂起寄存器描述如图28.3.1.3 所示:
在这里插入图片描述
在EXTI0 中断服务函数里面,需要清除EXTI0 中断标记,即对PR0 位写1。

硬件设计

  1. 例程功能
    LED0 闪烁,表明代码正在运行。按下按键KEY0 后,LED1 点亮,提示进入睡眠模式,此时LED0 不再闪烁,说明已经进入睡眠模式。按下按键WK_UP 后,LED1 熄灭,提示退出睡眠模式,此时LED0 继续闪烁,说明已经退出睡眠模式。
  2. 硬件资源
    1)LED 灯
    LED0 –PB5 LED1 –PE5
    2)独立按键
    KEY0 –PE4 WK_UP - PA0
    3)电源管理(低功耗模式- 睡眠模式)
    4)正点原子2.8/3.5/4.3/7/10 寸TFTLCD 模块(仅限MCU 屏,16 位8080 并口驱动)
  3. 原理图
    PWR 属于STM32F103 的内部资源,只需要软件设置好即可正常工作。我们通过KEY0 让CPU 进入睡眠模式,再通过WK_UP 触发EXTI 中断来唤醒CPU。LED0 指示程序是否执行,LED1 指示CPU 是否进入睡眠模式。

程序设计

28.3.3.1 PWR 的HAL 库驱动

  1. HAL_PWR_EnterSLEEPMode 函数
    进入睡眠模式函数,其声明如下:
void HAL_PWR_EnterSLEEPMode (uint32_t Regulator, uint8_t SLEEPEntry);

⚫ 函数描述:
用于设置CPU 进入睡眠模式。
⚫ 函数形参:
形参1 指定稳压器的状态。有两个选择,PWR_MAINREGULATOR_ON 表示稳压器处于正常模式,PWR_LOWPOWERREGULATOR_ON 表示稳压器处于低功耗模式。对应的是PWR_CR 寄存器的LPDS 位的设置(该形参在该函数中没有实质用处)。

形参2 指定进入睡眠模式的方式。有两个选择,PWR_SLEEPENTRY _WFI 表示使用WFI指令,PWR_SLEEPENTRY_WFE 表示使用WFE 指令。我们选择前者,这两个指令的区别,请参考本教程28.1.3 电源管理的描述。
⚫ 函数返回值:

睡眠模式配置步骤

1)配置唤醒睡眠模式的方式
这里我们用外部中断的方式唤醒睡眠模式,所以这里需要配置一个外部中断功能,我们用WK_UP 按键作为中断触发源,接下来就是配置PA0(连接按键WK_UP)。
通过__HAL_RCC_GPIOA_CLK_ENABLE 函数使能GPIOA 的时钟。
通过HAL_GPIO_Init 函数配置PA0 为上升沿触发检测的外部中断模式,开启下拉电阻等。
通过HAL_NVIC_EnableIRQ 函数使能EXTI0 中断。
通过HAL_NVIC_SetPriority 函数设置中断优先级。
编写EXTI0_IRQHandle 中断函数,在中断服务函数中调用HAL_GPIO_EXTI_IRQHandler函数。
最后编写HAL_GPIO_EXTI_Callback 回调函数。由于前面已经介绍过外部中断的配置步骤,这里就介绍到这里,详见本例程源码。
2)进入CPU 睡眠模式
通过HAL_PWR_EnterSLEEPMode 函数进入睡眠模式。
3)通过按下按键触发外部中断唤醒睡眠模式
在本实验中,通过按下KEY0 按键进入睡眠模式,然后通过按下WK_UP 按键触发外部中断唤醒睡眠模式。
28.3.3.2 程序流程图

在这里插入图片描述
程序解析

这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。PWR 源码包括两个文件:pwr.c 和pwr.h。睡眠模式实验代码在电压监控实验源码后追加。
首先看本实验在pwr.h 头文件定义的几个宏定义:

/* PWR WKUP 按键引脚和中断定义
* 我们通过WK_UP按键唤醒MCU, 因此必须定义这个按键及其对应的中断服务函数
*/
#define PWR_WKUP_GPIO_PORT GPIOA
#define PWR_WKUP_GPIO_PIN GPIO_PIN_0
#define PWR_WKUP_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE();}while(0)
#define PWR_WKUP_INT_IRQn EXTI0_IRQn
#define PWR_WKUP_INT_IRQHandler EXTI0_IRQHandler

这些定义是WK_UP 按键的相关宏定义,以及其对应的外部中断线0 的相关定义。
pwr.h 头文件就介绍这部分的程序,下面是pwr.c 文件,先看低功耗模式下的按键初始化函数,其定义如下:

/**
* @brief 低功耗模式下的按键初始化(用于唤醒睡眠模式/停止模式)
* @param 无
* @retval 无
*/
void pwr_wkup_key_init(void)
{
	GPIO_InitTypeDef gpio_init_struct;
	PWR_WKUP_GPIO_CLK_ENABLE(); /* WKUP时钟使能*/
	gpio_init_struct.Pin = PWR_WKUP_GPIO_PIN; /* WKUP引脚*/
	gpio_init_struct.Mode = GPIO_MODE_IT_RISING; /* 中断,上升沿*/
	gpio_init_struct.Pull = GPIO_PULLDOWN; /* 下拉*/
	gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速*/
	HAL_GPIO_Init(PWR_WKUP_GPIO_PORT, &gpio_init_struct); /* WKUP引脚初始化*/
	HAL_NVIC_SetPriority(PWR_WKUP_INT_IRQn, 2, 2); /* 抢占优先级2,子优先级2 */
	HAL_NVIC_EnableIRQ(PWR_WKUP_INT_IRQn);
}

该函数初始化WK_UP 按键(PA0),并设置上升沿触发的外部中断线0,最后设置中断优先级并使能外部中断线0。
下面介绍的是进入CPU 睡眠模式函数,其定义如下:

/**
* @brief 进入CPU睡眠模式
* @param 无
* @retval 无
*/
void pwr_enter_sleep(void)
{
	/* 进入睡眠模式*/
	HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
}

函数内直接调用HAL_PWR_EnterSLEEPMode 函数使用WFI 指令进入睡眠模式。
下面介绍的是WK_UP 按键外部中断服务函数及其回调函数,函数定义如下:

/**
* @brief WK_UP按键外部中断服务程序
* @param 无
* @retval 无
*/
void PWR_WKUP_INT_IRQHandler(void)
{
	HAL_GPIO_EXTI_IRQHandler(PWR_WKUP_GPIO_PIN);
}
/**
* @brief 外部中断回调函数
* @param GPIO_Pin:中断线引脚
* @note 此函数会被PWR_WKUP_INT_IRQHandler()调用
* @retval 无
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	if (GPIO_Pin == PWR_WKUP_GPIO_PIN)
	{
		/* HAL_GPIO_EXTI_IRQHandler()函数已经为我们清除了中断标志位,
		所以我们进了回调函数可以只关注中断时的控制逻辑,不用再操作寄存器*/
	}
}

在WK_UP 按键外部中断服务函数中我们调用HAL 库的HAL_GPIO_EXTI_IRQHandler 函数来处理外部中断。该函数会调用__HAL_GPIO_EXTI_CLEAR_IT 函数取消屏蔽对应的外部中断线位,这里是EXTI_IMR 寄存器相应位,还有其他寄存器控制其他外部中断线。我们只是唤醒睡眠模式而已,不需要其他的逻辑程序,所以HAL_GPIO_EXTI_Callback 回调函数可以什么都不用做,甚至也可以不重新定义这个回调函数(屏蔽该回调函数也可以)。

最后在main.c 里面编写如下代码:

int main(void)
{
        uint8_t t = 0;
        uint8_t key = 0;
        HAL_Init(); /* 初始化HAL库*/
        sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
        delay_init(72); /* 延时初始化*/
        usart_init(115200); /* 串口初始化为115200 */
        led_init(); /* 初始化LED */
        lcd_init(); /* 初始化LCD */
        key_init(); /* 初始化按键*/
        pwr_wkup_key_init(); /* 唤醒按键初始化*/
        lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
        lcd_show_string(30, 70, 200, 16, 16, "SLEEP TEST", RED);
        lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
        lcd_show_string(30, 110, 200, 16, 16, "KEY0:Enter SLEEP MODE", RED);
        lcd_show_string(30, 130, 200, 16, 16, "KEY_UP:Exit SLEEP MODE", RED);
        while (1)
        {
                key = key_scan(0);
                if (key == KEY0_PRES)
                {
                        LED1(0); /* 点亮绿灯,提示进入睡眠模式*/
                        pwr_enter_sleep(); /* 进入睡眠模式*/
                        LED1(1); /* 关闭绿灯,提示退出睡眠模式*/
                }
                if ((t % 20) == 0)
                {
                        LED0_TOGGLE(); /* 每200ms,翻转一次LED0 */
                }
                delay_ms(10);
                t++;
        }
}

该部分程序,功能就是按下KEY0 后,点亮LED1,进入睡眠模式。然后一直等待外部中断唤醒,当按下按键WK_UP,就触发外部中断,睡眠模式就被唤醒,然后继续执行后面的程序,关闭LED1 等。

下载验证

下载代码后,LED0 闪烁,表明代码正在运行。按下按键KEY0 后,LED1 点亮,提示进入睡眠模式,此时LED0 不再闪烁,说明已经进入睡眠模式。按下按键WK_UP 后,LED1 熄灭,提示退出睡眠模式,此时LED0 继续闪烁,说明已经退出睡眠模式。

停止模式实验

本小节我们来学习停止模式实验,该部分的知识点内容请回顾28.1.2.3 电源管理。我们直接从寄存器介绍开始。

PWR 寄存器

本实验我们用到外部中断来唤醒停止模式。我们用到WFI 指令进入停止模式,这个后面会讲,进入停止模式后,使用外部中断唤醒。外部中断部分内容参照睡眠模式即可,都是共用同样的配置。
下面主要介绍PWR_CR 寄存器相关位。
⚫ PWR 控制寄存器(PWR_CR)
PWR 的控制寄存器描述如图28.4.1.1 所示:

在这里插入图片描述
通过PDDS 位选择进入停止模式还是待机模式,停止模式即对PDDS 位置0 即可。在停止模式下,电压调节器有两种模式:开启或者低功耗,选择低功耗模式,即LPDS 置1。
⚫ 系统控制寄存器(SCB_SCR)
系统控制寄存器描述如图28.4.1.2 所示:
在这里插入图片描述
该寄存器存在于ARM 内核中,详细描述可查阅《Cortex-M3 权威指南》,在本实验中,我们需要把SLEEPDEEP 位置1,这样子后面调用WFI 命令时,进入的就是停止模式了。在唤醒后,需要清除SLEEPDEEP 位,进行置0。

硬件设计

  1. 例程功能
    LED0 闪烁,表明代码正在运行。按下按键KEY0 后,LED1 点亮,提示进入停止模式,此时LED0 不再闪烁,说明已经进入停止模式。按下按键WK_UP 后,LED1 熄灭,提示退出停止模式,此时LED0 继续闪烁,说明已经退出停止模式。
  2. 硬件资源
    1)LED 灯
    LED0 –PB5
    LED1 –PE5
    2)独立按键
    KEY0 –PE4
    WK_UP - PA0
    3)电源管理(低功耗模式–停止模式)
    4)正点原子2.8/3.5/4.3/7/10 寸TFTLCD 模块(仅限MCU 屏,16 位8080 并口驱动)
  3. 原理图
    PWR 属于STM32F103 的内部资源,只需要软件设置好即可正常工作。我们通过KEY0 让CPU 进入停止模式,再通过WK_UP 触发EXTI 中断来唤醒CPU。LED0 指示程序是否执行,LED1 指示CPU 是否进入停止模式。

程序设计

28.4.3.1 PWR 的HAL 库驱动

  1. HAL_PWR_EnterSTOPMode 函数
    进入停止模式函数,其声明如下:
void HAL_PWR_EnterSTOPMode (uint32_t Regulator, uint8_t STOPEntry);

⚫ 函数描述:
用于设置CPU 进入停止模式。
⚫ 函数形参:
形参1 指定稳压器在停止模式下的状态。有两个选择,PWR_MAINREGULATOR_ON 表示稳压器处于正常模式,PWR_LOWPOWERREGULATOR_ON 表示稳压器处于低功耗模式。对应的是PWR_CR1 寄存器的LPDS 位的设置。
形参2 指定用WFI 还是WFE 指令进入停止模式。有两个选择,PWR_STOPENTRY_WFI表示使用WFI 指令,PWR_STOPENTRY_WFE 表示使用WFE 指令。我们选择前者,不了解这两种指令的区别,可以回看28.1.3 小节的知识。
⚫ 函数返回值:

停止模式配置步骤
1)配置唤醒停止模式的方式
这里我们用外部中断的方式唤醒停止模式,所以这里需要配置一个外部中断功能,我们用WK_UP 按键作为中断触发源,接下来就是配置PA0(连接按键WK_UP)。
通过__HAL_RCC_GPIOA_CLK_ENABLE 函数使能GPIOA 的时钟。
通过HAL_GPIO_Init 函数配置PA0 为上升沿触发检测的外部中断模式,开启下拉电阻等。
通过HAL_NVIC_EnableIRQ 函数使能EXTI0 中断。
通过HAL_NVIC_SetPriority 函数设置中断优先级。

编写EXTI0_IRQHandle 中断函数,在中断服务函数中调用HAL_GPIO_EXTI_IRQHandl er。
最后编写HAL_GPIO_EXTI_Callback 回调函数。由于前面已经介绍过外部中断的配置步骤,这里就介绍到这里,详见本例程源码。
2)进入CPU 停止模式
通过HAL_PWR_EnterSTOPMode 函数进入停止模式。
3)通过按下按键触发外部中断唤醒停止模式
在本实验中,通过按下KEY0 按键进入停止模式,然后通过按下WK_UP 按键触发外部中断唤醒停止模式。
28.4.3.2 程序流程图
在这里插入图片描述

程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。PWR 源码包括两个文件:pwr.c 和pwr.h。停止模式实验代码在睡眠模式实验源码后追加。
首先看pwr.h 头文件,因为我们还是用到WK_UP 对应的外部中断线来唤醒停止模式的CPU,pwr.h 头文件的WK_UP 按键对应的宏定义我们也是用到的,上个实验已经讲过,这里不再赘述。下面是pwr.c 文件,WK_UP 按键的相关函数我们还是用上个实验的,我们主要介绍进入停止模式函数,其定义如下:

/**
* @brief 进入停止模式
* @param 无
* @retval 无
*/
void pwr_enter_stop(void)
{
	__HAL_RCC_PWR_CLK_ENABLE();
	/* 进入停止模式,设置稳压器为低功耗模式,等待中断唤醒*/
	HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
}

该函数因为涉及对电源控制寄存器的操作,所以先调用__HAL_RCC_PWR_CLK_ENABLE函数使能PWR 时钟,然后调用HAL_PWR_EnterSTOPMode 函数进入停止模式,形参1 即PWR_LOWPOWERREGULATOR_ON 设置稳压器为低功耗模式,形参2 则是选择WFI 指令。
最后在main.c 里面编写如下代码:

int main(void)
{
        uint8_t t = 0;
        uint8_t key = 0;
        HAL_Init(); /* 初始化HAL库*/
        sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
        delay_init(72); /* 延时初始化*/
        usart_init(115200); /* 串口初始化为115200 */
        led_init(); /* 初始化LED */
        lcd_init(); /* 初始化LCD */
        key_init(); /* 初始化按键*/
        pwr_wkup_key_init(); /* 唤醒按键初始化*/
        lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
        lcd_show_string(30, 70, 200, 16, 16, "STOP TEST", RED);
        lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
        lcd_show_string(30, 110, 200, 16, 16, "KEY0:Enter STOP MODE", RED);
        lcd_show_string(30, 130, 200, 16, 16, "KEY_UP:Exit STOP MODE", RED);
        while (1)
        {
                key = key_scan(0);
                if (key == KEY0_PRES)
                {
                        LED1(0); /* 点亮绿灯,提示进入停止模式*/
                        pwr_enter_stop(); /* 进入停止模式*/
                        /* 从停止模式唤醒, 需要重新设置系统时钟, 72Mhz */
                        sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
                        delay_init(72); /* 延时初始化*/
                        LED1(1); /* 关闭绿灯,提示退出停止模式*/
                }
                if ((t % 20) == 0)
                {
                        LED0_TOGGLE(); /* 每200ms,翻转一次LED0 */
                }
                delay_ms(10);
                t++;
        }
}

该部分程序,功能就是按下KEY0 后,点亮LED1,进入停止模式。然后一直等待外部中断唤醒,当按下按键WK_UP,就触发外部中断,停止模式就被唤醒,然后继续执行后面的程序,重新设置系统时钟72MHZ 和延时初始化,关闭LED1 等。

下载验证

下载代码后,LED0 闪烁,表明代码正在运行。按下按键KEY0 后,LED1 点亮,提示进入停止模式,此时LED0 不再闪烁,说明已经进入停止模式。按下按键WK_UP 后,LED1 熄灭,提示退出停止模式,此时LED0 继续闪烁,说明已经退出停止模式。

待机模式实验

本小节我们来学习待机模式实验,该部分的知识点内容请回顾28.1.2.3 电源管理。我们直接从寄存器介绍开始。

PWR 寄存器

本实验是先对相关的电源控制寄存器配置待机模式的参数,然后通过WFI 指令进入待机模式,使用WKUP 引脚的上升沿来唤醒(这是特定的唤醒源)。
下面主要介绍本实验用到的寄存器,系统控制寄存器的部分与上一实验一致,就不介绍了
⚫ PWR 控制寄存器(PWR_CR)
PWR 的控制寄存器描述如图28.5.1.1 所示:
在这里插入图片描述
这里我们通过设置PDDS 位,使CPU 进入深度睡眠时进入待机模式,同时,我们需要通过CWUF 位清除之前的唤醒位。
⚫ 电源控制/状态寄存器(PWR_CSR)
电源控制/状态寄存器描述如图28.5.1.2 所示:

在这里插入图片描述
该寄存器我们只关心EWUP 位,设置EWUP 为1,即WKUP 引脚作为待机模式的唤醒源。

硬件设计

  1. 例程功能
    LED0 闪烁,表明代码正在运行。按下按键KEY0 后,进入待机模式,待机模式下大部分引脚处于高阻态,所以说这时候LED0 会熄灭,TFTLCD 也会熄灭。按下WK_UP 按键后,退出待机模式(相当于复位操作),程序重新执行,LED0 继续闪烁,TFTLCD 屏点亮。
  2. 硬件资源
    1)LED 灯
    LED0 –PB5
    2)独立按键
    KEY0 –PE4
    WK_UP - PA0
    3)电源管理(低功耗模式–待机模式)
    4)正点原子2.8/3.5/4.3/7/10 寸TFTLCD 模块(仅限MCU 屏,16 位8080 并口驱动)
  3. 原理图
    PWR 属于STM32F103 的内部资源,只需要软件设置好即可正常工作。我们通过KEY0 让CPU 进入待机模式,再通过WK_UP 上升沿来唤醒CPU。LED0 指示程序是否执行。

程序设计

28.5.3.1 PWR 的HAL 库驱动

  1. HAL_PWR_EnableWakeUpPin 函数
    使能唤醒引脚函数,其声明如下:
void HAL_PWR_EnableWakeUpPin (uint32_t WakeUpPinPolarity);

⚫ 函数描述:
用于使能唤醒引脚。
⚫ 函数形参:.
形参1 取值范围:PWR_WAKEUP_PIN1。
⚫ 函数返回值:

⚫ 注意事项:
禁止某个唤醒引脚使用的函数如下:

void HAL_PWR_DisableWakeUpPin (uint32_t WakeUpPinPolarity);
  1. HAL_PWR_EnterSTANDBYMode 函数
    进入待机模式函数,其声明如下:
void HAL_PWR_EnterSTANDBYMode (void);

⚫ 函数描述:
用于使CPU 进入待机模式,进入待机模式,首先要设置SLEEPDEEP 位,接着我们通过PWR_CR 设置PDDS 位,使得CPU 进入深度睡眠时进入待机模式,最后执行WFI 指令开始进入待机模式,并等待WK_UP 上升沿的到来。
⚫ 函数形参:

⚫ 函数返回值:

待机模式配置步骤
1)进入CPU 待机模式
在进入待机模式之前我们需要做一些准备:
涉及到操作PWR 寄存器的内容,所以首先先进行PWR 时钟的初始化,用__HAL_RCC_PWR_CLK_ENABLE 函数实现。
通过HAL_PWR_EnableWakeUpPin 函数使能WKUP 的唤醒功能。
通过__HAL_PWR_CLEAR_FLAG 函数清除唤醒标记,详看源码。

通过HAL_PWR_EnterSTANDBYMode 函数进入待机模式。
2)通过WKUP 引脚上升沿触发唤醒睡眠模式
在本实验中,通过按下KEY0 按键进入待机模式,然后通过按下WK_UP 按键,使用待机模式中的WKUP 引脚上升沿的唤醒信号,而不是普通的外部中断,唤醒待机模式。
28.5.3.2 程序流程图
在这里插入图片描述

程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。PWR 源码包括两个文件:pwr.c 和pwr.h。待机模式实验代码在停止模式实验源码后追加。
pwr.h 头文件上的宏定义我们是没有用到的,这里我们使用的是特定唤醒源,与外部中断无关。下面是pwr.c 文件,我们主要介绍进入待机模式函数,其定义如下:

/**
* @brief 进入待机模式
* @param 无
* @retval 无
*/
void pwr_enter_standby(void)
{
	__HAL_RCC_PWR_CLK_ENABLE(); /* 使能电源时钟*/
	HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1); /* 使能KEY_UP引脚的唤醒功能*/
	__HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU); /* 需要清此标记,否则将保持唤醒状态*/
	HAL_PWR_EnterSTANDBYMode(); /* 进入待机模式*/
}

该函数首先是调用__HAL_RCC_PWR_CLK_ENABLE 来使能PWR 时钟,然后调用函数HAL_PWR_EnableWakeUpPin 用来设置WK_UP 引脚作为唤醒源。在进入待机模式前,还得调
用__HAL_PWR_CLEAR_FLAG 函数清除一下唤醒标志,要不然会保持唤醒状态。最后调用函数HAL_PWR_EnterSTANDBYMode 进入待机模式。
最后在main.c 里面编写如下代码:

int main(void)
{
        uint8_t t = 0;
        uint8_t key = 0;
        HAL_Init(); /* 初始化HAL库*/
        sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
        delay_init(72); /* 延时初始化*/
        usart_init(115200); /* 串口初始化为115200 */
        led_init(); /* 初始化LED */
        lcd_init(); /* 初始化LCD */
        key_init(); /* 初始化按键*/
        pwr_wkup_key_init(); /* 唤醒按键初始化*/
        lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
        lcd_show_string(30, 70, 200, 16, 16, "STANDBY TEST", RED);
        lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
        lcd_show_string(30, 110, 200, 16, 16, "KEY0:Enter STANDBY MODE", RED);
        lcd_show_string(30, 130, 200, 16, 16, "KEY_UP:Exit STANDBY MODE", RED);
        while (1)
        {
                key = key_scan(0);
                if (key == KEY0_PRES)
                {
                        pwr_enter_standby(); /* 进入待机模式*/
                        /* 从待机模式唤醒相当于系统重启(复位), 因此不会执行到这里*/
                }
                if ((t % 20) == 0)
                {
                        LED0_TOGGLE(); /* 每200ms,翻转一次LED0 */
                }
                delay_ms(10);
                t++;
        }
}

该部分程序,经过一系列初始化后,判断到KEY0 按下就调用pwr_enter_standby 函数进入待机模式,然后等待按下WK_UP 按键产生WKUP 上升沿唤醒CPU。注意待机模式唤醒后,系统会进行复位。

下载验证

下载代码后,LED0 闪烁,表明代码正在运行。按下按键KEY0 后,TFTLCD 屏熄灭,此时LED0 不再闪烁,说明已经进入待机模式。按下按键WK_UP 后,TFTLCD 屏点亮,LED0 闪烁,说明系统从待机模式中唤醒相当于复位。

DMA 串口发送实验

本章,我们将介绍STM32F103 的DMA。我们将利用DMA 来实现串口数据传送,并在LCD模块上显示当前的传送进度。

DMA 简介

DMA,全称为:Direct Memory Access,即直接存储器访问。DMA 传输方式无需CPU 直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为RAM 与I/O 设备开辟一条直接传送数据的通路,能使CPU 的效率大为提高。
STM32F103 内部有2 个DMA 控制器(DMA2 仅存大容量产品中),DMA1 有7 个通道。
DMA2 有5 个通道。每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个DMA 请求的优先权。
STM32F103 的DMA 有以下一些特性:
●每个通道都直接连接专用的硬件DMA 请求,每个通道都同样支持软件触发。这些功能通过软件来配置。
●在七个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),假如在相等优先权时由硬件决定(请求0 优先于请求1,依此类推)。
●独立的源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐。
●支持循环的缓冲器管理。
●每个通道都有3 个事件标志(DMA 半传输,DMA 传输完成和DMA 传输出错),这3 个事件标志逻辑或成为一个单独的中断请求。
●存储器和存储器间的传输。
●外设和存储器,存储器和外设的传输。
●闪存、SRAM、外设的SRAM、APB1、APB2 和AHB 外设均可作为访问的源和目标。
●可编程的数据传输数目:最大为65536。

DMA 框图

STM32F103ZET6 有两个DMA 控制器,DMA1 和DMA2,本章,我们仅针对DMA1 进行介绍。
下面先来学习DMA 控制器框图,通过学习DMA 控制器框图会有一个很好的整体掌握,
同时对之后的编程也会有一个清晰的思路。STM32F103 的DMA 控制器框图如图29.1.1.1 所示:

在这里插入图片描述
图中,我们标记了3 处位置,起作用分别是:
①DMA 请求
如果外设想要通过DMA 来传输数据,必须先给DMA 控制器发送DMA 请求,DMA 收到请求信号之后,控制器会给外设一个应答信号,当外设应答后且DMA 控制器收到应答信号之后,就会启动DMA 的传输,直到传输完毕。
STM32F103 共有DMA1 和DMA2 两个控制器,DMA1 有7 个通道,DMA2 有5 个通道,不同的DMA 控制器的通道对应着不同的外设请求,这决定了我们在软件编程上该怎么设置,具体见29.1.1.1DMA 请求映像表。
在这里插入图片描述
在这里插入图片描述
②通道
DMA 具有12 个独立可编程的通道,其中DMA1 有7 个通道,DMA2 有5 个通道,每个通道对应不同的外设的DMA 请求。虽然每个通道可以接收多个外设的请求,但是同一时间只能接收一个,不能同时接收多个。
③仲裁器
当发生多个DMA 通道请求时,就意味着有先后响应处理的顺序问题,这个就由仲裁器管理。仲裁器管理DMA 通道请求分为两个阶段。第一阶段属于软件阶段,可以在DMA_CCRx寄存器中设置,有4 个等级:非常高,高,中和低四个优先级。第二阶段属于硬件阶段,如果两个或以上的DMA 通道请求设置的优先级一样,则他们优先级取决于通道编号,编号越低优先权越高,比如通道0 高于通道1。在大容量产品和互联型产品中,DMA1 控制器拥有高于DMA2 控制器的优先级。

DMA 寄存器

⚫ DMA 中断状态寄存器(DMA_ISR)
DMA 中断状态寄存器描述如图29.1.2.1 所示:
在这里插入图片描述
该寄存器是查询当前DMA 传输的状态,我们常用的是TCIFx 位,即通道DMA 传输完成与否的标志。注意此寄存器为只读寄存器,所以在这些位被置位之后,只能通过其他的操作来清除。
⚫ DMA 中断标志清除寄存器(DMA_IFCR)
DMA 中断标志清除寄存器描述如图29.1.2.2 所示:
在这里插入图片描述
该寄存器是用来清除DMA_ISR 的对应位的,通过写0 清除。在DMA_ISR 被置位后,我们必须通过向该寄存器对应的位写0 来清除。
⚫ DMA 通道x 传输数量寄存器(DMA_CNDTRx)
DMA 通道x 传输数量寄存器描述如图29.1.2.3 所示:
在这里插入图片描述
该寄存器控制着DMA 通道x 的每次传输所要传输的数据量。其设置范围为0~65535。并且该寄存器的值随着传输的进行而减少,当该寄存器的值为0 的时候就代表此次数据传输已经全部发送完成。所以可以通过这个寄存器的值来获取当前DMA 传输的进度。
⚫ DMA 通道x 配置寄存器(DMA_CCRx)
DMA 通道x 配置寄存器描述如图29.1.2.4 所示

在这里插入图片描述

该寄存器控制着DMA 很多相关信息,包括数据宽度、外设及存储器宽度、通道优先级、增量模式、传输方向、中断允许、使能等,所以说DMA_CCRx 是DMA 传输的核心控制寄存器。
⚫ DMA 通道x 外设地址寄存器(DMA_CPARx)
DMA 通道x 外设地址寄存器描述如图29.1.2.5 所示:

在这里插入图片描述

该寄存器是用来存储STM32 外设的地址,比如我们平常使用串口1,那么该寄存器必须写入0x40013804(其实就是&USART1_DR)。其他外设就可以修改成其他对应外设地址就好了。
⚫ DMA 通道x 存储器地址寄存器(DMA_CMARx)
DMA 通道x 存储器地址寄存器用来存放存储器的地址,该寄存器和DMA_CPARx 差不多,所以就不列出来了。举个应用的例子,在程序中,我们使用到一个g_sendbuf[5200]数组来做存储器,那么我们在DMA_CMARx 中写入&g_sendbuf 即可。

硬件设计

  1. 例程功能
    每按下按键KEY0,串口1 就会以DMA 方式发送数据,同时在LCD 上面显示传送进度。打开串口调试助手,可以收到DMA 发送的内容。LED0 闪烁用于提示程序正在运行。
  2. 硬件资源
    1)LED 灯
    LED0 –PB5
    2)独立按键KEY0 –PE4
    3)串口1(PA9/PA10 连接在板载USB 转串口芯片CH340 上面)
    4)正点原子2.8/3.5/4.3/7/10 寸TFTLCD 模块(仅限MCU 屏,16 位8080 并口驱动)
  3. 原理图
    DMA 属于STM32F103 内部资源,通过软件设置好就可以了。

程序设计

DMA 的HAL 库驱动

DMA 在HAL 库中的驱动代码在stm32f1xx_hal_dma.c 文件(及其头文件)中。

  1. HAL_DMA_Init 函数
    DMA 的初始化函数,其声明如下:
HAL_StatusTypeDef HAL_DMA_Init(DMA_HandleTypeDef *hdma);

⚫ 函数描述:
用于初始化DMA1,DMA2。
⚫ 函数形参:
形参1 是DMA_HandleTypeDef 结构体类型指针变量,其定义如下:

typedef struct __DMA_HandleTypeDef
{
	void *Instance; /* 寄存器基地址*/
	DMA_InitTypeDef Init; /* DAM通信参数*/
	HAL_LockTypeDef Lock; /* DMA锁对象*/
	__IO HAL_DMA_StateTypeDef State; /* DMA传输状态*/
	void *Parent; /* 父对象状态,HAL库处理的中间变量*/
	void (*XferCpltCallback)( struct __DMA_HandleTypeDef *hdma);/*DMA传输完成回调*/
	/* DMA一半传输完成回调*/
	void (* XferHalfCpltCallback)( struct __DMA_HandleTypeDef * hdma);
	/* DMA传输完整的Memory1回调*/
	void (* XferM1CpltCallback)( struct __DMA_HandleTypeDef * hdma);
	/* DMA传输半完全内存回调*/
	void (* XferM1HalfCpltCallback)( struct __DMA_HandleTypeDef * hdma);
	/*DMA传输错误回调*/
	void (* XferErrorCallback)( struct __DMA_HandleTypeDef * hdma);
	/* DMA传输中止回调*/
	void (* XferAbortCallback)( struct __DMA_HandleTypeDef * hdma);
	__IO uint32_t ErrorCode; /* DMA存取错误代码*/
	DMA_TypeDef *DmaBaseAddress; /* DMA通道基地址*/
	uint32_t ChannelIndex; /* DMA通道索引*/
}DMA_HandleTypeDef;

这个结构体内容比较多,上面已注释中文翻译,下面列出几个成员说明一下。
Instance:是用来设置寄存器基地址,例如要设置的对象是串口1 的发送,那么就要参考表29.1.1.1,串口1 的DMA 传输需要用到的是DMA1 的通道4,即DMA1_Channel4。
Parent:是HAL 库处理中间变量,用来指向DMA 通道外设句柄。
XferCpltCallback(传输完成回调函数),XferHalfCpltCallback(半传输完成回调函数),XferM1CpltCallback(Memory1 传输完成回调函数)和XferErrorCallback(传输错误回调函数)是四个函数指针,用来指向回调函数入口地址。
其他成员变量是HAL 库处理过程状态标识变量,这里就不做过多讲解。
接下来我们重点介绍Init,它是DMA_InitTypeDef 结构体类型变量,该结构体定义如下:

typedef struct
{
	uint32_t Direction; /* 传输方向,例如存储器到外设DMA_MEMORY_TO_PERIPH */
	uint32_t PeriphInc; /* 外设(非)增量模式,非增量模式DMA_PINC_DISABLE */
	uint32_t MemInc; /* 存储器(非)增量模式,增量模式DMA_MINC_ENABLE */
	uint32_t PeriphDataAlignment; /* 外设数据大小:8/16/32位*/
	uint32_t MemDataAlignment; /* 存储器数据大小:8/16/32位*/
	uint32_t Mode; /* 模式:循环模式/普通模式*/
	uint32_t Priority; /* DMA优先级:低/中/高/非常高*/
}DMA_InitTypeDef;

该结构体成员变量非常多,但每个成员变量的配置基本都是DMA_CCRx 寄存器的相关位。
⚫ 函数返回值:
HAL_StatusTypeDef 枚举类型的值。
以DMA 的方式传输串口数据的配置步骤
1)使能DMA 时钟。
DMA 的时钟使能是通过AHB1ENR 寄存器来控制的,这里我们要先使能时钟,才可以配置DMA 相关寄存器。HAL 库方法为:

__HAL_RCC_DMA1_CLK_ENABLE(); /* DMA1时钟使能*/
__HAL_RCC_DMA2_CLK_ENABLE(); /* DMA2时钟使能*/

2)初始化DMA。
调用HAL_DMA_Init 函数初始化DMA 的相关参数,包括配置通道,外设地址,存储器地址,传输数据量等。
HAL 库为了处理各类外设的DMA 请求,在调用相关函数之前,需要调用一个宏定义标识符,来连接DMA 和外设句柄。例如要使用串口DMA 发送,所以方式为:

__HAL_LINKDMA(&g_uart1_handler, hdmatx, g_dma_handle);

其中g_uart1_handler 是串口初始化句柄,我们在usart.c 中定义过了。g_dma_handle 是DMA初始化句柄。hdmatx 是外设句柄结构体的成员变量,在这里实际就是g_uart1_handler 的成员变量。在HAL 库中,任何一个可以使用DMA 的外设,它的初始化结构体句柄都会有一个DMA_HandleTypeDef 指针类型的成员变量,是HAL 库用来做相关指向的。hdmatx 就是DMA_HandleTypeDef 结构体指针类型。
这句话的含义就是把g_uart1_handler 句柄的成员变量hdmatx 和DMA 句柄g_dma_handle连接起来,是纯软件处理,没有任何硬件操作。
这里我们就点到为止,如果大家要详细了解HAL 库指向关系,请查看本实验宏定义标识符__HAL_LINKDMA 的定义和调用方法,就会很清楚了。
3)使能串口的DMA 发送,启动传输。
串口1 的DMA 发送实际是串口控制寄存器CR3 的位7 来控制的,在HAL 库中操作该寄存器来使能串口DMA 发送的函数为HAL_UART_Transmit_DMA。
这里大家需要注意,调用该函数后会开启相应的DMA 中断,对于本章实验,我们是通过查询的方法获取数据传输状态,所以并没有做中断相关处理,也没有编写中断服务函数。
HAL 库还提供了对串口的DMA 发送的停止,暂停,继续等操作函数:

HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart); /* 停止*/
HAL_StatusTypeDef HAL_UART_DMAPause(UART_HandleTypeDef *huart); /* 暂停*/
HAL_StatusTypeDef HAL_UART_DMAResume(UART_HandleTypeDef *huart); /* 恢复*/

4)查询DMA 传输状态。
在DMA 传输过程中,我们要查询DMA 传输通道的状态,使用的方法是通过检测DMA 寄存器的相关位实现:

__HAL_DMA_GET_FLAG(&g_dma_handle, DMA_FLAG_TC4);

获取当前传输剩余数据量:

__HAL_DMA_GET_COUNTER(&g_dma_handle);

同样,我们也可以设置对应的DMA 数据流传输的数据量大小,函数为:

__HAL_DMA_SET_COUNTER (&g_dma_handle, 1000);

DMA 相关的库函数我们就讲解到这里,大家可以查看HAL 库中文手册详细了解。
5)DMA 中断使用方法。
DMA 中断对于每个通道都有一个中断服务函数,比如DMA1_Channel4 的中断服务函数为DMA1_Channel4_IRQHandler。HAL 库提供了通用DMA 中断处理函数HAL_DMA_IRQHandler,
在该函数内部,会对DMA 传输状态进行分析,然后调用相应的中断处理回调函数:

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); /* 传输出错回调函数*/

程序流程图

在这里插入图片描述

程序解析

  1. DMA 驱动代码
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。DMA 驱动源码包括两个文件:dma.c 和dma.h。
    dma.h 头文件只有函数得声明,就不解释了,我们直接介绍dma.c 的程序。下面是与DMA初始化相关的函数,其定义如下:
/**
* @brief 串口TX DMA初始化函数
* @note 这里的传输形式是固定的, 这点要根据不同的情况来修改
* 从存储器-> 外设模式/8位数据宽度/存储器增量模式
*
* @param dmax_chy : DMA的通道, DMA1_Channel1 ~ 7, DMA2_Channel1 ~ 5
* 某个外设对应哪个DMA, 哪个通道, 请参考<<STM32中文参考手册>> 10.3.7节
* 必须设置正确的DMA及通道, 才能正常使用!
* @retval 无
*/
void dma_init(DMA_Channel_TypeDef* DMAx_CHx)
{ /* 大于DMA1_Channel7, 则为DMA2的通道了*/
	if ((uint32_t)DMAx_CHx > (uint32_t)DMA1_Channel7)
	{
		__HAL_RCC_DMA2_CLK_ENABLE(); /* DMA2时钟使能*/
	}
	else
	{
		__HAL_RCC_DMA1_CLK_ENABLE(); /* DMA1时钟使能*/
	}
	/* 将DMA与USART1联系起来(发送DMA) */
	
	__HAL_LINKDMA(&g_uart1_handle, hdmatx, g_dma_handle);
	/* Tx DMA配置*/
	g_dma_handle.Instance = DMAx_CHx; /* USART1_TX的DMA通道: DMA1_Channel4 */
	g_dma_handle.Init.Direction = DMA_MEMORY_TO_PERIPH; /* 存储器到外设模式*/
	g_dma_handle.Init.PeriphInc = DMA_PINC_DISABLE; /* 外设非增量模式*/
	g_dma_handle.Init.MemInc = DMA_MINC_ENABLE; /* 存储器增量模式*/
	g_dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; /* 外设位宽*/
	g_dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; /* 存储器位宽*/
	g_dma_handle.Init.Mode = DMA_NORMAL; /* DMA模式:正常模式*/
	g_dma_handle.Init.Priority = DMA_PRIORITY_MEDIUM; /* 中等优先级*/
	
	HAL_DMA_Init(&g_dma_handle);
}

该函数是一个通用的DMA 配置函数,DMA1/DMA2 的所有通道,都可以利用该函数配置,不过有些固定参数可能要适当修改(比如位宽、传输方向等)。该函数在外部只能修改DMA 数据通道,更多的其他设置只能在该函数内部修改。对照前面的配置步骤的详细讲解来分析这部分代码即可。
2. main.c 代码
在main.c 里面编写如下代码:

/* 要循环发送的字符串*/
const uint8_t TEXT_TO_SEND[] ={"正点原子STM32 DMA 串口实验"};
#define SEND_BUF_SIZE (sizeof(TEXT_TO_SEND) + 2) * 200 /* 发送数据长度*/
uint8_t g_sendbuf[SEND_BUF_SIZE]; /* 发送数据缓冲区*/
int main(void)
{
        uint8_t key = 0;
        uint16_t i, k;
        uint16_t len;
        uint8_t mask = 0;
        float pro = 0; /* 进度*/
        HAL_Init(); /* 初始化HAL库*/
        sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
        delay_init(72); /* 延时初始化*/
        usart_init(115200); /* 串口初始化为115200 */
        led_init(); /* 初始化LED */
        lcd_init(); /* 初始化LCD */
        key_init(); /* 初始化按键*/
        dma_init(DMA1_Channel4); /* 初始化串口1 TX DMA */
        lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
        lcd_show_string(30, 70, 200, 16, 16, "DMA TEST", RED);
        lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
        lcd_show_string(30, 110, 200, 16, 16, "KEY0:Start", RED);
        len = sizeof(TEXT_TO_SEND);
        k = 0;
        for (i = 0; i < SEND_BUF_SIZE; i++) /* 填充ASCII字符集数据*/
        {
                if (k >= len) /* 入换行符*/
                {
                        if (mask)
                        {
                                g_sendbuf[i] = 0x0a;
                                k = 0;
                        }
                        else
                        {
                                g_sendbuf[i] = 0x0d;
                                mask++;
                        }
                }
                else /* 复制TEXT_TO_SEND语句*/
                {
                        mask = 0;
                        g_sendbuf[i] = TEXT_TO_SEND[k];
                        k++;
                }
        }
        i = 0;
        while (1)
        {
                key = key_scan(0);
                if (key == KEY0_PRES) /* KEY0按下*/
                {
                        printf("\r\nDMA DATA:\r\n");
                        lcd_show_string(30, 130, 200, 16, 16, "Start Transimit....", BLUE);
                        lcd_show_string(30, 150, 200, 16, 16, " %", BLUE); /* 显示百分号*/
                        HAL_UART_Transmit_DMA(&g_uart1_handle, g_sendbuf, SEND_BUF_SIZE);
                        /* 等待DMA传输完成,此时我们来做另外一些事情,比如点灯
                         * 实际应用中,传输数据期间,可以执行另外的任务
                         */
                        while (1)
                        {
                                /* 等待DMA1_Channel4 传输完成*/
                                if ( __HAL_DMA_GET_FLAG(&g_dma_handle, DMA_FLAG_TC4))
                                {
                                        __HAL_DMA_CLEAR_FLAG(&g_dma_handle, DMA_FLAG_TC4);
                                        HAL_UART_DMAStop(&uartx_handler); /* 传输完成以后关闭串口DMA */
                                        break;
                                }
                                pro = __HAL_DMA_GET_COUNTER(&g_dma_handle);
                                len = SEND_BUF_SIZE; /* 总长度*/
                                pro = 1 - (pro / len); /* 得到百分比*/
                                pro *= 100; /* 扩大100倍*/
                                lcd_show_num(30, 150, pro, 3, 16, BLUE);
                        }
                        lcd_show_num(30, 150, 100, 3, 16, BLUE); /* 显示100% */
                        /* 提示传送完成*/
                        lcd_show_string(30, 130, 200, 16, 16, "Transimit Finished!", BLUE);
                }
                i++;
                delay_ms(10);
                if (i == 20)
                {
                        LED0_TOGGLE(); /* LED0闪烁,提示系统正在运行*/
                        i = 0;
                }
        }
}

main 函数的流程大致是:先初始化发送数据缓冲区g_sendbuf 的值,然后通过KEY0 开启串口DMA 发送,在发送过程中,通过__HAL_DMA_GET_COUNTER(&g_dma_handle)获取当前还剩余的数据量来计算传输百分比,最后在传输结束之后清除相应标志位,提示已经传输完成。

下载验证

将程序下载到开发板后,可以看到LED0 不停的闪烁,提示程序已经在运行了。LCD 显示的内容如图29.4.1 所示:

在这里插入图片描述

我们打开串口调试助手,然后按KEY0,可以看到串口显示如图29.4.2 所示的内容:

在这里插入图片描述
可以看到串口收到了开发板发送过来的数据,同时可以看到TFTLCD 上显示了进度等信息,如图29.4.3 所示:

在这里插入图片描述

至此,我们整个DMA 实验就结束了,希望大家通过本章的学习,掌握STM32F103 的DMA使用。DMA 是个非常好的功能,它不但能减轻 CPU 负担,还能提高数据传输速度,合理的应用DMA,往往能让你的程序设计变得简单。

  • 4
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

行稳方能走远

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值