STM32学习笔记2


[success]好久没有接触硬件了,主要是我现在接触Python都好久了,今天感觉很累不想在碰Python了,所以我就继续去接触一下硬件,学点新的换换心情。[/success]

看门狗

看门狗包括独立看门狗和窗口看门狗,我们先讲一下独立看门狗:

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

看门狗原理:单片机系统在外界的干扰下会出现程序跑飞的现象导致出现死循环,看门狗电路就是为了避免
这种情况的发生。看门狗的作用就是在一定时间内(通过定时计数器实现)没有接收喂狗信号
(表示 MCU 已经挂了),便实现处理器的自动复位重启(发送复位信号)。

我们启用看门狗一般需要以下几步:

1.  取消寄存器 写保护 (向 向 IWDG_KR 写入 0X5555 )

通过这步,我们取消 IWDG_PR 和 IWDG_RLR 的写保护,使后面可以操作这两个寄存器,
设置 IWDG_PR 和 IWDG_RLR 的值。这在库函数中的实现函数是:IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);
这个函数非常简单,顾名思义就是开启/取消写保护,也就是使能/失能写权限。

2.  设置独立看门狗的预分频系数和重装载值

设置看门狗的分频系数的函数是:void IWDG_SetPrescaler(uint8_t IWDG_Prescaler); //设置 IWDG 预分频值

设置看门狗的重装载值的函数是:void IWDG_SetReload(uint16_t Reload); //设置 IWDG 重装载值

我们可以通过设置预分频系数和重转载值来确定看门狗的溢出时间:

计算公式:Tout=((4×2^prer) ×rlr) /40,prer是预分频值(0-7),rlr是重装载值,tout就是看门狗的溢出时间。我们这里采用prer=4,rlr为625,计算得到tout值为1000也就是1s,大家可以自己设置自己需要的时间。当然这里要注意:因为这个值并不是准确的,所以我们必须提前喂狗。

3 重载计数值喂狗  向 IWDG_KR 写入 0XAAAA

这个就是喂狗的函数,我们想让它正常工作就需要定时执行这个函数,不断喂狗,不让看门狗复位。

函数是:IWDG_ReloadCounter(); //按照 IWDG 重装载寄存器的值重装载 IWDG 计数器

4启动看门狗(向 IWDG_KR 写入 0XCCCC)

函数:IWDG_Enable(); //使能 IWDG

注意: IWDG 在一旦启用,就不能再被关闭!想要关闭,只能重启,并且重启之后不能打开 IWDG。

说完了基本操作,我们直接上源代码(这个是看门狗文件的代码):

void IWDG_Init(u8 prer,u16 rlr) 
{	
     IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);  //使能对寄存器IWDG_PR和IWDG_RLR的写操作
    
    IWDG_SetPrescaler(prer);  //设置IWDG预分频值:设置IWDG预分频值为64
    
    IWDG_SetReload(rlr);  //设置IWDG重装载值
    
    IWDG_ReloadCounter();  //按照IWDG重装载寄存器的值重装载IWDG计数器
    
    IWDG_Enable();  //使能IWDG
}
//喂独立看门狗
void IWDG_Feed(void)
{   
     IWDG_ReloadCounter();//reload										   
}

主函数的代码:

int main(void)
 {		
    delay_init();	    	 //延时函数初始化	  
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 	 //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
    uart_init(115200);	 //串口初始化为115200
     LED_Init();		  	 //初始化与LED连接的硬件接口
    KEY_Init();          //按键初始化	 
    delay_ms(500);   	 //让人看得到灭
    IWDG_Init(4,625);    //与分频数为64,重载值为625,溢出时间为1s	   
    LED0=0;				 //点亮LED0
    while(1)
    {
        if(KEY_Scan(0)==WKUP_PRES)
        {
            IWDG_Feed();//如果WK_UP按下,则喂狗
        }
        delay_ms(10);
    };	 
}

主函数是一个综合案例,只要两行代码才是关键部分,一个是看门狗的初始化,还有一个是喂狗的操作。

 

说完了独立看门狗,我们这里说一下窗口看门狗,这个其实和独立看门狗差不多,只不过这个更加高级一点,这个其实就是设定了一个区间,你只能在这个区间内进行喂狗操作低于或者高于都会让程序复位。

这里我们也只需要设置一下上窗口时间,因为下窗口是固定的,我们只需要在下窗口-上窗口时间内进行喂狗操作就可以了。

窗口看门狗的超时公式如下:
Twwdg=(4096×2^WDGTB×(T[5:0]+1)) /Fpclk1;
其中:
Twwdg:WWDG 超时时间(单位为 ms)
Fpclk1:APB1 的时钟频率(单位为 Khz)
WDGTB:WWDG 的预分频系数
T[5:0]:窗口看门狗的计数器低 6 位

W[6:0]则是窗口看门狗的上窗口,下窗口值是固定的(0X40)。
当窗口看门狗的计数器在上窗口值之外被刷新,或者低于下窗口值都会产生复位。
上窗口值(W[6:0])是由用户自己设定的,根据实际要求来设计窗口值,但是一定要确保窗口值大于 0X40,否则窗口就不存在了。

这里我们假设一下Fpclk1=36Mhz,那么可以得到最小-最大超时时间表:

那么我们直接上函数吧:

1使能 WWDG时钟:

WWDG 不同于 IWDG,IWDG 有自己独立的 40Khz 时钟,不存在使能问题。而 WWDG使用的是 PCLK1 的时钟,需要先使能时钟。函数:RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE); // WWDG 时钟使能

2 设置窗口值和分频数

设置窗口值的函数是:void WWDG_SetWindowValue(uint8_t WindowValue);这个函数的入口参数 WindowValue 用来设置看门狗的上窗口值。

设置分频数的函数是:void WWDG_SetPrescaler(uint32_t WWDG_Prescaler);这个函数同样只有一个入口参数,用来设置看门狗的分频值。

3开启 WWDG 中断并分组

开启 WWDG 中断的函数为:WWDG_EnableIT(); //开启窗口看门狗中断
接下来是进行中断优先级配置,这里就不重复了,使用 NVIC_Init()函数即可。

4 设置计数器初始值并使能看门狗

这一步在库函数里面是通过一个函数实现的:void WWDG_Enable(uint8_t Counter);该函数既设置了计数器初始值,同时使能了窗口看门狗。

那么我们直接上代码吧:

//保存WWDG计数器的设置值,默认为最大. 
u8 WWDG_CNT=0x7f; 
//初始化窗口看门狗 	
//tr   :T[6:0],计数器值 
//wr   :W[6:0],窗口值 
//fprer:分频系数(WDGTB),仅最低2位有效 
//Fwwdg=PCLK1/(4096*2^fprer). 

void WWDG_Init(u8 tr,u8 wr,u32 fprer)
{ 
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE);  //   WWDG时钟使能

    WWDG_CNT=tr&WWDG_CNT;   //初始化WWDG_CNT.   
    WWDG_SetPrescaler(fprer);////设置IWDG预分频值

    WWDG_SetWindowValue(wr);//设置窗口值

    WWDG_Enable(WWDG_CNT);	 //使能看门狗 ,	设置 counter .                  

    WWDG_ClearFlag();//清除提前唤醒中断标志位 

    WWDG_NVIC_Init();//初始化窗口看门狗 NVIC

    WWDG_EnableIT(); //开启窗口看门狗中断
} 
//重设置WWDG计数器的值
void WWDG_Set_Counter(u8 cnt)
{
    WWDG_Enable(cnt);//使能看门狗 ,	设置 counter .	 
}
//窗口看门狗中断服务程序
void WWDG_NVIC_Init()
{
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = WWDG_IRQn;    //WWDG中断
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;   //抢占2,子优先级3,组2	
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;	 //抢占2,子优先级3,组2	
  NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE; 
    NVIC_Init(&NVIC_InitStructure);//NVIC初始化
}

void WWDG_IRQHandler(void)
    {

    WWDG_SetCounter(WWDG_CNT);	  //当禁掉此句后,窗口看门狗将产生复位

    WWDG_ClearFlag();	  //清除提前唤醒中断标志位

    LED1=!LED1;		 //LED状态翻转
    }

/********************************** 
                 主函数
/*************************************
 int main(void)
 {		
    delay_init();	    	 //延时函数初始化	  
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组2:2位抢占优先级,2位响应优先级
    uart_init(115200);	 //串口初始化为115200
     LED_Init();
    KEY_Init();          //按键初始化	 
    LED0=0;
    delay_ms(300);	  
    WWDG_Init(0X7F,0X5F,WWDG_Prescaler_8);//计数器值为7f,窗口寄存器为5f,分频数为8	   
     while(1)
    {
        LED0=1;			  	   
    }   
}

它这里运用了定时器中断,WWDG_IRQHandler其实就是定时器的中断函数,每次执行中断函数都是重装载值,然后在清除中断标志位。

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

定时器中断

STM32有8个定时器,其中1和8是高级定时器。2-5是通用定时器,6和7是通用定时器。这里我们主要讲解一下怎么使用通用定时器。

先说一下通用定时器的用途:测量输入信号的脉冲长度(输入捕获)或者产
生输出波形(输出比较和 PWM)等。

STM3F1 的通用 TIMx (TIM2、TIM3、TIM4 和 TIM5)定时器功能包括:
1)16 位向上、向下、向上/向下自动装载计数器(TIMx_CNT)。
2)16 位可编程(可以实时修改)预分频器(TIMx_PSC),计数器时钟频率的分频系数为 1~
65535 之间的任意数值。
3)4 个独立通道(TIMx_CH1~4),这些通道可以用来作为:
A.输入捕获
B.输出比较
C.PWM 生成(边缘或中间对齐模式)
D.单脉冲模式输出
4)可使用外部信号(TIMx_ETR)控制定时器和定时器互连(可以用 1 个定时器控制另外
一个定时器)的同步电路。
5)如下事件发生时产生中断/DMA:
A.更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发)
B.触发事件(计数器启动、停止、初始化或者由内部/外部触发计数)
C.输入捕获
D.输出比较
E.支持针对定位的增量(正交)编码器和霍尔传感器电路
F.触发输入作为外部时钟或者按周期的电流管理

因为寄存器的配置比较复杂,所以这里也很难一时半会讲清,我们还是直接来讲一下库函数的配置吧。(这里这里针对定时器3来进行配置)

1 TIM3 时钟使能

TIM3 是挂载在 APB1 之下,所以我们通过 APB1 总线(这里2-7都在APB1总线下,只有1和8在APB2下)下的使能使能函数来使能 TIM3。调用的函数是:RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //时钟使能

2初始化定时器参数 数, 设置 自动重装值 , 分频系数 ,计数方式 等

定时器的初始化参数是通过初始化函数 TIM_TimeBaseInit 实现的voidTIM_TimeBaseInit(TIM_TypeDef*TIMx,TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);第一个参数是确定是哪个定时器,这个比较容易理解。第二个参数是定时器初始化参数结构体指针,结构体类型为 TIM_TimeBaseInitTypeDef。

这个是结构体的类型:

typedef struct
{
uint16_t TIM_Prescaler;
uint16_t TIM_CounterMode;
uint16_t TIM_Period;
uint16_t TIM_ClockDivision;
uint8_t TIM_RepetitionCounter;
} TIM_TimeBaseInitTypeDef;

要说明的是,对于通用定时器只有前面四个参数有用,
最后一个参数 TIM_RepetitionCounter 是高级定时器才有用的,这里不多解释。

第一个参数 TIM_Prescaler 是用来设置分频系数的,(分频系数可以将计数器的时钟频率按1到65536之间的任意值分频。它是基于一个(在TIMx_PSC
寄存器中的)16位寄存器控制的16位计数器。)

第二个参数 TIM_CounterMode 是用来设置计数方式,上面讲解过,可以设置为向上计数,向下计数方式还有中央对齐计数方式,比较常用的是向上计数模式 TIM_CounterMode_Up 和向下计数模式 TIM_CounterMode_Down。

第三个参数是设置自动重载计数周期值。

第四个参数是用来设置时钟分频因子。

3设置 TIM3_DIER 允许更新中断

因为我们要使用 TIM3 的更新中断,寄存器的相应位便可使能更新中断。在库函数里面定时器中断使能是通过 TIM_ITConfig 函数来实现的:void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);
第一个参数是选择定时器号,这个容易理解,取值为 TIM1~TIM17。
第二个参数非常关键,是用来指明我们使能的定时器中断的类型,定时器中断的类型有很多种,包括更新中断 TIM_IT_Update,触发中断 TIM_IT_Trigger,以及输入捕获中断等等。第三个参数就很简单了,就是失能还是使能。例如我们要使能 TIM3 的更新中断,格式为:TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE );

4 TIM3中断优先级设置

在定时器中断使能之后,因为要产生中断,必不可少的要设置 NVIC 相关寄存器,设置中断优先级。之前多次讲解到用 NVIC_Init 函数实现中断优先级的设置,这里就不重复讲解。

5允许TIM3工作能 ,也就是使能 TIM3 。

光配置好定时器还不行,没有开启定时器,照样不能用。我们在配置完后要开启定时器,通过 TIM3_CR1 的 CEN 位来设置。在固件库里面使能定时器的函数是通过 TIM_Cmd 函数来实现的:void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState)这个函数非常简单,比如我们要使能定时器 3,方法为TIM_Cmd(TIM3, ENABLE); //使能 TIMx 外设

6编写中断服务函数

在最后,还是要编写定时器中断服务函数,通过该函数来处理定时器产生的相关中断。在中断产生后,通过状态寄存器的值来判断此次产生的中断属于什么类型。然后执行相关的操作,我们这里使用的是更新(溢出)中断,所以在状态寄存器 SR 的最低位。在处理完中断之后应该向 TIM3_SR 的最低位写 0,来清除该中断标志。
在固件库函数里面,用来读取中断状态寄存器的值判断中断类型的函数是:ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t)
该函数的作用是,判断定时器 TIMx 的中断类型 TIM_IT 是否发生中断。比如,我们要判断定时器 3 是否发生更新(溢出)中断,方法为:
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET){}
固件库中清除中断标志位的函数是:void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT)
该函数的作用是,清除定时器 TIMx 的中断 TIM_IT 标志位。使用起来非常简单,比如我们在TIM3 的溢出中断发生后,我们要清除中断标志位,方法是:TIM_ClearITPendingBit(TIM3, TIM_IT_Update );
这里需要说明一下,固件库还提供了两个函数用来判断定时器状态以及清除定时器状态标志位的函数 TIM_GetFlagStatus 和 TIM_ClearFlag,他们的作用和前面两个函数的作用类似。只是在 TIM_GetITStatus 函数中会先判断这种中断是否使能,使能了才去判断中断标志位,而TIM_GetFlagStatus 直接用来判断状态标志位。通过以上几个步骤,我们就可以达到我们的目的了,使用通用定时器的更新中断,来控制DS1 的亮灭。

这里我们先把代码贴出来:

void TIM3_Int_Init(u16 arr,u16 psc)
{
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //时钟使能
    
    //定时器TIM3初始化
    TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值	
    TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据指定的参数初始化TIMx的时间基数单位
 
    TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE ); //使能指定的TIM3中断,允许更新中断

    //中断优先级NVIC设置
    NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;  //TIM3中断
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;  //先占优先级0级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;  //从优先级3级
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
    NVIC_Init(&NVIC_InitStructure);  //初始化NVIC寄存器


    TIM_Cmd(TIM3, ENABLE);  //使能TIMx					 
}
//定时器3中断服务程序
void TIM3_IRQHandler(void)   //TIM3中断
{
    if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)  //检查TIM3更新中断发生与否
        {
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update  );  //清除TIMx更新中断标志 
        LED1=!LED1;
        }
}
/**********************
主函数
/**********************
 int main(void)
 {		
 
    delay_init();	    	 //延时函数初始化	  
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
    uart_init(115200);	 //串口初始化为115200
     LED_Init();			     //LED端口初始化
    TIM3_Int_Init(4999,7199);//10Khz的计数频率,计数到5000为500ms  
       while(1)
    {
        LED0=!LED0;
        delay_ms(200);		   
    }	 

}	 

这里我们讲一下定时器的分频系数,重载周期值,还有分频因子:

分频因子我们一般都是直接设置为0,也就是TIM_CKD_DIV1。就是计数器的频率和时钟输入频率一样。

那么这个500ms是怎么算出来的?

我们这里使能的是APB1的时钟,它的时钟值为APB1的两倍(这里我也搞的不是很清楚为什么是两倍),而APB1的频率是36MHZ。所以我们需要分频(这里我们设置为7199)所以相当于每隔1ms计数一次,而我们设置的计数值为5000,也就是500ms产生一次中断。

PWM

TM32 的定时器除了 TIM6 和 7。其他的定时器都可以用来产生 PWM 输出。其中高级定时器 TIM1 和 TIM8 可以同时产生多达 7 路的 PWM 输出。而通用定时器也能同时产生多达 4路的 PWM 输出,这样,STM32 最多可以同时产生 30 路 PWM 输出!这里我们仅利用 TIM3的 CH2 产生一路 PWM 输出。如果要产生多路输出,大家可以根据代码做出修改。

1. 开启 TIM3 时钟 以及复用功能时钟置 ,配置 PB5 为复用输出。

 

 

 

 


文章作者: 小游
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 小游 !
  目录