嵌入式软件开发学习过程记录,本部分结合本人的学习经验撰写,系统描述各类基础例程的程序撰写逻辑。构建裸机开发的思维,为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\n",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\n",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);
}