您现在的位置是:首页 >技术杂谈 >5。STM32裸机开发(6)网站首页技术杂谈

5。STM32裸机开发(6)

西伯利亚大草原的狼 2024-06-19 13:56:33
简介5。STM32裸机开发(6)

嵌入式软件开发学习过程记录,本部分结合本人的学习经验撰写,系统描述各类基础例程的程序撰写逻辑。构建裸机开发的思维,为RTOS做铺垫(本部分基于库函数版实现),如有不足之处,敬请批评指正。 

(6)中主要对前面内容进行两个补充,也是两个公共层的功能,即低功耗和实时RTC

一 待机唤醒实验(低功耗)

        很多单片机具有低功耗模式,比如 MSP430、STM8L 等,STM32 也不例外。默认情况下,系统复位或上电复位后,微控制器进入运行模式。在运行模式下,HCLK 为 CPU 提供时钟,并执行程序代码。当 CPU 不需继续运行(例如等待外部事件)时,可以利用多种低功耗模式来节省功耗。用户需要根据最低电源消耗、最快速启动时间和可用的唤醒源等条件,选定一个最佳的低功耗模式。
当然在运行模式下,也可以通过如下方式降低功耗:
1)降低系统时钟速度
2)不使用 APBx 和 AHB 外设时,将对应的外设时钟关闭

STM32的三种低功耗模式,这三种模式所需的功耗是逐级递减,也就是说待机模式功耗是最低的。

(1)睡眠模式( CM3 内核停止工作,外设仍在运行)
(2)停止模式(所有时钟都停止)
(3)待机模式( 1.8 V 内核电源关闭)

此处针对待机模式进行介绍

        在睡眠模式中,仅关闭了内核时钟,内核停止运行,但其片上外设, CM3 核心的外设全都照常运行。在停止模式中,进一步关闭了其它所有的时钟,于是所有的外设都停止了工作,但由于其 1.8V 区域的部分电源没有关闭,还保留了内核的寄存器、内存的信息,所以从停止模式唤醒,并重新开启时钟后,还可以从上次停止处继续执行代码。在待机模式中,它除了关闭所有的时钟,还把 1.8V 区域的电源也完全关闭了,也就是说,从待机模式唤醒后,由于没有之前代码的运行记录,只能对芯片复位,重新检测 BOOT 条件,从头开始执行程序。

待机模式配置步骤

(电源管理相关库函数在 stm32f10x_pwr.c 和 stm32f10x_pwr.h 文件中)
(1)使能电源时钟
        因为低功耗模式是通过 STM32 电源(PWR)系统进行管理的,所以需要使能电源时钟,调用的库函数为:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);//使能 PWR 外设时钟
(2)设置 WK_UP 引脚为唤醒源
        待机唤醒方式有很多种,我们选择 WK_UP 引脚(PA0)上升沿来退出待机模式。在库函数中,设置使能 WK_UP 用于唤醒 CPU 待机模式的函数是: PWR_WakeUpPinCmd(ENABLE);
因为按键 K_UP 连接在 PA0 管脚上,并且是高电平有效,这样一来就可以使用 K_UP 按键来退出待机模式。
(3)进入待机模式
        进入待机模式,首先要设置 SLEEPDEEP 位( 详见《 Cortex M3 权威指南 》,chpt13 Cortex-M3 的其它特性--电源管理章节),接着我们通过 PWR_CR 设置 PDDS 位,使得 CPU 进入深度睡眠时进入待机模式,最后执行 WFI 指令开始进入待机模式,并等待 WK_UP 中断的到来。整个操作可以通过一个库函数完成,如下:
PWR_EnterSTANDBYMode();//进入待机模式
通常在进入待机模式前,我们会清除唤醒标志,以等待下次进入。清除唤醒标志库函数为:
PWR_ClearFlag(PWR_FLAG_WU);//清除 Wake-up 标志
注意:如果使能了 RTC 闹钟中断的时候,进入待机模式前,必须按如下操作处理:
1.禁止 RTC 中断( ALRAIE、 ALRBIE、 WUTIE、 TAMPIE 和 TSIE 等)。
2.清零对应中断标志位。
3.清除 PWR 唤醒(WUF)标志(通过设置 PWR_CR 的 CWUF 位实现)。
4.重新使能 RTC 对应中断。
5.进入低功耗模式。
#include "wkup.h"

void Enter_Standby_Mode(void)
{
		
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);//使能PWR外设时钟

	PWR_ClearFlag(PWR_FLAG_WU);//清除Wake-up 标志

	PWR_WakeUpPinCmd(ENABLE);//使能唤醒管脚	使能或者失能唤醒管脚功能
	
	PWR_EnterSTANDBYMode();//进入待机模式
}

二 RTC 实时时钟实验(获取当前的时间和日期)

        STM32 的实时时钟( RTC)是一个独立的定时器。 STM32 的 RTC 模块拥有一组连续计数的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。
        从 RTC 的定时器特性来说,它是一个 32 位的计数器,只能向上计数。它的时钟来源有三种,分别为高速外部时钟的 128 分频( HSE/128)、 低速内部时钟 LSI 以及低速外部时钟 LSE。使用 HSE 分频时钟或 LSI 的话,在主电源 VDD 掉电的情况下,这两个时钟来源都会受到影响,因此没法保证 RTC 正常工作。所以 RTC 一般使用低速外部时钟 LSE, 在设计中, 频率通常为实时时钟模块中常用的 32.768KHz,这是因为 32768 = 2^15,分频容易实现,所以它被广泛应用到 RTC 模块。在主电源 VDD 有效的情况下(待机), RTC 还可以配置闹钟事件使 STM32 退出待机模式。
RTC 配置步骤
(RTC 相关库函数在 stm32f10x_rtc.c 和 stm32f10x_rtc.h 文件中)
(1)使能电源时钟和后备域时钟,开启 RTC 后备寄存器写访问
        要访问 RTC 和 RTC 备份区域就必须先使能电源及后备域时钟,然后使能 RTC 后备区域访问。电源时钟使能,通过 RCC_APB1ENR 寄存器来设置;RTC 及 RTC 备份寄存器的写访问,通过 PWR_CR 寄存器的 DBP 位设置。调用库函数为:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);//打开电源时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);//打开 RTC 后备域时钟
PWR_BackupAccessCmd(ENABLE);//打开后备寄存器访问
(2)复位备份区域,开启外部低速振荡器
        在取消备份区域写保护之后,我们可以先对这个区域复位,以清除前面的设置,当然这个操作不要每次都执行,因为备份区域的复位将导致之前存在的数据丢失,所以要不要复位,要视情况而定。然后我们使能外部低速振荡器,注意这里一般要先判断 RCC_BDCR 的 LSERDY 位来确定低速振荡器已经就绪了才开始下面的操作。备份区域复位的库函数为:
BKP_DeInit(); //复位备份区域
开启外部低速振荡器的函数是:
RCC_LSEConfig(RCC_LSE_ON);//开启外部 32.768K RTC 时钟
(3)选择 RTC 时钟,并使能
选择 LSE 为 RTC 时钟源库函数是:
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //选择 LSE 作为 RTC 时钟
使能 RTC 时钟库函数是:

RCC_RTCCLKCmd(ENABLE);//使能 RTC 时钟

(4)设置 RTC 的分频以及配置 RTC 时钟

        在开启了 RTC 时钟之后,我们要做的就是设置 RTC 时钟的分频数,通过 RTC_PRLH 和 RTC_PRLL 来设置,然后等待 RTC 寄存器操作完成,并同步之后,设置秒钟中断。然后设置 RTC 的允许配置位( RTC_CRH 的 CNF 位),设置时间(其实就是设置 RTC_CNTH 和 RTC_CNTL 两个寄存器)。

        在进行 RTC 配置之前首先要打开允许配置位(CNF),调用的库函数是:

RTC_EnterConfigMode();// 允许配置
        在配置完成之后,注意更新配置同时退出配置模式,调用的库函数是:
RTC_ExitConfigMode();//退出配置模式,更新配置
        设置 RTC 时钟分频数,调用的库函数是:这个函数只有一个参数,就是 RTC 时钟的分频数
void RTC_SetPrescaler(uint32_t PrescalerValue);

        然后是设置秒中断允许,RTC 使能中断的函数是:函数的第一个参数用来选择 RTC 的中断类型,可通过库文件的头文件查看, 第二个参数用于使能还是失能。比如要使能 RTC 秒中断,如下:

void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState);
RTC_ITConfig(RTC_IT_SEC, ENABLE);
        接下来便是设置时间了,设置时间实际上就是设置 RTC 的计数值,时间与计数值之间是需要换算的。库函数中设置 RTC 计数值的方法是:
void RTC_SetCounter(uint32_t CounterValue);
(5)更新配置,设置 RTC 中断分组
        在设置完时钟之后,我们将配置更新同时退出配置模式,这里还是通过RTC_CRH 的 CNF 来实现。 调用库函数的方法是:
RTC_ExitConfigMode();//退出配置模式,更新配置
        在退出配置模式更新配置之后我们在备份区域 BKP_DR1 中写入 0XA0A0 代表我们已经初始化过时钟了,下次开机(或复位)的时候,先读取 BKP_DR1 的值,然后判断是否是 0XA0A0 来决定是不是要配置。接着我们配置 RTC 的秒钟中断,并进行分组。
        往备份区域写用户数据的函数是:函数的第一个参数用来设置备份寄存器的标号,这个在 rtc 库文件头文件内有定义,第二个参数是我们往备份寄存器写入的数据。比如我们向 BKP_DR1 中
写入 0XA0A0。函数如下:
void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);
BKP_WriteBackupRegister(BKP_DR1, 0XA0A0);
同样库函数还提供一个读取备份寄存器内容的函数,函数参数作用和写备份寄存器是一样的功能
uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);
(6)编写 RTC 中断服务函数
        前面步骤中我们配置好了 RTC 的秒中断,所以我们还需要编写对应的中断服务函数。RTC 中断服务函数名在 STM32F1 启动文件内可以查找到,RTC 中断函数名如下:
RTC_IRQHandler
        因为 RTC 的中断类型有很多,所以进入中断后,我们需要在中断服务函数开头处通过读取 RTC 状态寄存器的值判断此次中断是哪种类型,然后做出相应的控制。库函数中用来读取 RTC 状态标志位的函数如下:参数 RTC_FLAG 用来选择 RTC 状态标志
FlagStatus RTC_GetFlagStatus(uint32_t RTC_FLAG);
在中断函数结束之前我们会清除下对应的中断标志。清除 RTC 秒中断标志函数如下:
RTC_ClearITPendingBit(RTC_IT_SEC);
#include "rtc.h" 
#include "SysTick.h"
#include "usart.h"
		    

	   
_calendar calendar;//时钟结构体 
 
static void RTC_NVIC_Config(void)
{	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn;		//RTC全局中断
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;	//先占优先级1位,从优先级3位
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;	//先占优先级0位,从优先级4位
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;		//使能该通道中断
	NVIC_Init(&NVIC_InitStructure);		//根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
}


/*******************************************************************************
* 函 数 名         : RTC_Init
* 函数功能		   : RTC初始化
* 输    入         : 无
* 输    出         : 0,初始化成功
        			 1,LSE开启失败
*******************************************************************************/ 
u8 RTC_Init(void)
{
	//检查是不是第一次配置时钟
	u8 temp=0;
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);	//使能PWR和BKP外设时钟   
	PWR_BackupAccessCmd(ENABLE);	//使能后备寄存器访问  
	if (BKP_ReadBackupRegister(BKP_DR1) != 0xA0A0)		//从指定的后备寄存器中读出数据:读出了与写入的指定数据不相乎
	{	 			
		BKP_DeInit();	//复位备份区域 	
		RCC_LSEConfig(RCC_LSE_ON);	//设置外部低速晶振(LSE),使用外设低速晶振
		while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET&&temp<250)	//检查指定的RCC标志位设置与否,等待低速晶振就绪
		{
			temp++;
			delay_ms(10);
		}
		if(temp>=250)return 1;//初始化时钟失败,晶振有问题	    
		RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);		//设置RTC时钟(RTCCLK),选择LSE作为RTC时钟    
		RCC_RTCCLKCmd(ENABLE);	//使能RTC时钟  
		RTC_WaitForLastTask();	//等待最近一次对RTC寄存器的写操作完成
		RTC_WaitForSynchro();		//等待RTC寄存器同步  
		RTC_ITConfig(RTC_IT_SEC, ENABLE);		//使能RTC秒中断
		RTC_WaitForLastTask();	//等待最近一次对RTC寄存器的写操作完成
		RTC_EnterConfigMode();// 允许配置	
		RTC_SetPrescaler(32767); //设置RTC预分频的值
		RTC_WaitForLastTask();	//等待最近一次对RTC寄存器的写操作完成
		RTC_Set(2017,3,22,17,34,55);  //设置时间	
		RTC_ExitConfigMode(); //退出配置模式  
		BKP_WriteBackupRegister(BKP_DR1, 0XA0A0);	//向指定的后备寄存器中写入用户程序数据
	}
	else//系统继续计时
	{
		RTC_WaitForSynchro();	//等待最近一次对RTC寄存器的写操作完成
		RTC_ITConfig(RTC_IT_SEC, ENABLE);	//使能RTC秒中断
		RTC_WaitForLastTask();	//等待最近一次对RTC寄存器的写操作完成
	}
	RTC_NVIC_Config();//RCT中断分组设置		    				     
	RTC_Get();//更新时间	
	return 0; //ok

}		 				    
//RTC时钟中断
//每秒触发一次  
//extern u16 tcnt; 
void RTC_IRQHandler(void)
{		 
	if (RTC_GetITStatus(RTC_IT_SEC) != RESET)//秒钟中断
	{							
		RTC_Get();//更新时间  
		printf("RTC Time:%d-%d-%d %d:%d:%d
",calendar.w_year,calendar.w_month,calendar.w_date,calendar.hour,calendar.min,calendar.sec);//输出闹铃时间	
				
 	}
	if(RTC_GetITStatus(RTC_IT_ALR)!= RESET)//闹钟中断
	{
		RTC_ClearITPendingBit(RTC_IT_ALR);		//清闹钟中断	  	
		RTC_Get();				//更新时间   
		printf("Alarm Time:%d-%d-%d %d:%d:%d
",calendar.w_year,calendar.w_month,calendar.w_date,calendar.hour,calendar.min,calendar.sec);//输出闹铃时间	
		
  	} 				  								 
	RTC_ClearITPendingBit(RTC_IT_SEC|RTC_IT_OW);		//清闹钟中断
	RTC_WaitForLastTask();	  	    						 	   	 
}
//判断是否是闰年函数
//月份   1  2  3  4  5  6  7  8  9  10 11 12
//闰年   31 29 31 30 31 30 31 31 30 31 30 31
//非闰年 31 28 31 30 31 30 31 31 30 31 30 31
//输入:年份
//输出:该年份是不是闰年.1,是.0,不是
u8 Is_Leap_Year(u16 year)
{			  
	if(year%4==0) //必须能被4整除
	{ 
		if(year%100==0) 
		{ 
			if(year%400==0)return 1;//如果以00结尾,还要能被400整除 	   
			else return 0;   
		}else return 1;   
	}else return 0;	
}	 			   


//月份数据表											 
u8 const table_week[12]={0,3,3,6,1,4,6,2,5,0,3,5}; //月修正数据表	  
//平年的月份日期表
const u8 mon_table[12]={31,28,31,30,31,30,31,31,30,31,30,31};

/*******************************************************************************
* 函 数 名         : RTC_Set
* 函数功能		   : RTC设置日期时间函数(以1970年1月1日为基准,把输入的时钟转换为秒钟)
						1970~2099年为合法年份
* 输    入         : syear:年  smon:月  sday:日
					hour:时   min:分	 sec:秒			
* 输    出         : 0,成功
        			 1,失败
*******************************************************************************/
u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec)
{
	u16 t;
	u32 seccount=0;
	if(syear<1970||syear>2099)return 1;	   
	for(t=1970;t<syear;t++)	//把所有年份的秒钟相加
	{
		if(Is_Leap_Year(t))seccount+=31622400;//闰年的秒钟数
		else seccount+=31536000;			  //平年的秒钟数
	}
	smon-=1;
	for(t=0;t<smon;t++)	   //把前面月份的秒钟数相加
	{
		seccount+=(u32)mon_table[t]*86400;//月份秒钟数相加
		if(Is_Leap_Year(syear)&&t==1)seccount+=86400;//闰年2月份增加一天的秒钟数	   
	}
	seccount+=(u32)(sday-1)*86400;//把前面日期的秒钟数相加 
	seccount+=(u32)hour*3600;//小时秒钟数
    seccount+=(u32)min*60;	 //分钟秒钟数
	seccount+=sec;//最后的秒钟加上去

	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);	//使能PWR和BKP外设时钟  
	PWR_BackupAccessCmd(ENABLE);	//使能RTC和后备寄存器访问 
	RTC_SetCounter(seccount);	//设置RTC计数器的值

	RTC_WaitForLastTask();	//等待最近一次对RTC寄存器的写操作完成  	
	return 0;	    
}

//初始化闹钟		  
//以1970年1月1日为基准
//1970~2099年为合法年份
//syear,smon,sday,hour,min,sec:闹钟的年月日时分秒   
//返回值:0,成功;其他:错误代码.
u8 RTC_Alarm_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec)
{
	u16 t;
	u32 seccount=0;
	if(syear<1970||syear>2099)return 1;	   
	for(t=1970;t<syear;t++)	//把所有年份的秒钟相加
	{
		if(Is_Leap_Year(t))seccount+=31622400;//闰年的秒钟数
		else seccount+=31536000;			  //平年的秒钟数
	}
	smon-=1;
	for(t=0;t<smon;t++)	   //把前面月份的秒钟数相加
	{
		seccount+=(u32)mon_table[t]*86400;//月份秒钟数相加
		if(Is_Leap_Year(syear)&&t==1)seccount+=86400;//闰年2月份增加一天的秒钟数	   
	}
	seccount+=(u32)(sday-1)*86400;//把前面日期的秒钟数相加 
	seccount+=(u32)hour*3600;//小时秒钟数
    seccount+=(u32)min*60;	 //分钟秒钟数
	seccount+=sec;//最后的秒钟加上去 			    
	//设置时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);	//使能PWR和BKP外设时钟   
	PWR_BackupAccessCmd(ENABLE);	//使能后备寄存器访问  
	//上面三步是必须的!
	
	RTC_SetAlarm(seccount);
 
	RTC_WaitForLastTask();	//等待最近一次对RTC寄存器的写操作完成  	
	
	return 0;	    
}


//得到当前的时间
//返回值:0,成功;其他:错误代码.
u8 RTC_Get(void)
{
	static u16 daycnt=0;
	u32 timecount=0; 
	u32 temp=0;
	u16 temp1=0;	  
    timecount=RTC_GetCounter();	 
 	temp=timecount/86400;   //得到天数(秒钟数对应的)
	if(daycnt!=temp)//超过一天了
	{	  
		daycnt=temp;
		temp1=1970;	//从1970年开始
		while(temp>=365)
		{				 
			if(Is_Leap_Year(temp1))//是闰年
			{
				if(temp>=366)temp-=366;//闰年的秒钟数
				else {temp1++;break;}  
			}
			else temp-=365;	  //平年 
			temp1++;  
		}   
		calendar.w_year=temp1;//得到年份
		temp1=0;
		while(temp>=28)//超过了一个月
		{
			if(Is_Leap_Year(calendar.w_year)&&temp1==1)//当年是不是闰年/2月份
			{
				if(temp>=29)temp-=29;//闰年的秒钟数
				else break; 
			}
			else 
			{
				if(temp>=mon_table[temp1])temp-=mon_table[temp1];//平年
				else break;
			}
			temp1++;  
		}
		calendar.w_month=temp1+1;	//得到月份
		calendar.w_date=temp+1;  	//得到日期 
	}
	temp=timecount%86400;     		//得到秒钟数   	   
	calendar.hour=temp/3600;     	//小时
	calendar.min=(temp%3600)/60; 	//分钟	
	calendar.sec=(temp%3600)%60; 	//秒钟
	calendar.week=RTC_Get_Week(calendar.w_year,calendar.w_month,calendar.w_date);//获取星期   
	return 0;
}	 
//获得现在是星期几
//功能描述:输入公历日期得到星期(只允许1901-2099年)
//输入参数:公历年月日 
//返回值:星期号																						 
u8 RTC_Get_Week(u16 year,u8 month,u8 day)
{	
	u16 temp2;
	u8 yearH,yearL;
	
	yearH=year/100;	yearL=year%100; 
	// 如果为21世纪,年份数加100  
	if (yearH>19)yearL+=100;
	// 所过闰年数只算1900年之后的  
	temp2=yearL+yearL/4;
	temp2=temp2%7; 
	temp2=temp2+day+table_week[month-1];
	if (yearL%4==0&&month<3)temp2--;
	return(temp2%7);
}	

 

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。