首先是修改主频的工程,我们主要的任务是研究一下system_stm32f10x.c和.h这两个文件,看一看这些代码是怎么运作的,顺便再利用里面给我们提供的宏,完成修改主频的任务

首先这两个system文件是用来配置系统时钟的,也就是配置RCC时钟树

左边是四个时钟源,HSI、HSE、LSE、LSI,用于提供时钟,右边是各个外设,就是使用时钟的地方,用的最多的就是AHB时钟,APB1时钟和APB2时钟,另外还有一些时钟,它们的来源不是AHB和APB,比如I2S的时钟,直接来源于SYSCLK,USB的时钟,直接来源于PLL,我们主要要关心的是这个外部8MHz晶振,它如何进行选择,如何倍频才能得到这个72MHz的SYSCLK,系统主频,然后系统主频如何分配才能得到AHB、APB1和APB2的时钟频率,一般默认晶振接的是8MHz,主频是72MHz,AHB和APB2是72MHz,APB1是36MHz,但其实这些并不绝对,可以根据需求进行更改。我们来看看这个文件怎么配置RCC时钟树

system这两个文件提供了两个外部可调用的函数和一个外部可调用的变量,两个函数是SystemInit()和SystemCoreClockUpdate(),一个变量是SystemCoreClock

SystemInit这个函数是用来配置时钟树的,也是整个文件最主要的东西,使用HSE配置主频为72MHz。并且这个函数在复位后,执行main函数之前会在启动文件里自动调用的。

变量SystemCoreClock表示主频率的值,我们想知道目前的主频是多少直接显示一下这个变量就可以了。

最后一个函数,SystemCoreClockUpdate()这个函数是用来更新上面这个变量的,因为这个变量只有最开始的一次赋值,之后如果我们改变了主频频率,这个值不会自动跟着变换,所以我们就要调用一下这个函数,根据当前的时钟配置更新上面这个变量。

下面这块就是用来更改主频的宏定义:

#if defined (STM32F10X_LD_VL) || (defined STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
/* #define SYSCLK_FREQ_HSE HSE_VALUE */
#define SYSCLK_FREQ_24MHz 24000000
#else
/* #define SYSCLK_FREQ_HSE HSE_VALUE */
/* #define SYSCLK_FREQ_24MHz 24000000 */
/* #define SYSCLK_FREQ_36MHz 36000000 */
/* #define SYSCLK_FREQ_48MHz 48000000 */
/* #define SYSCLK_FREQ_56MHz 56000000 */
#define SYSCLK_FREQ_72MHz 72000000
#endif

解除对应的注释来选择想要的系统主频,如果使用的是VL这些设备,也就是超值系列,那可选主频就只有两个,HSE的8M和24M,否则的话,可以选择剩下的主频,当前解除注释的是72M,所以主频默认就是72M,这里#if叫做预编译主要用来兼容不同型号的设备。我们简单修改一个宏定义是如何作用与电路的配置的呢

首先我们找到SystemInit函数,这是最先调用的函数,可以看到第一步是开启HSI也就是默认使用内部的8M晶振,之后的操作都是各种Reset,各种Disable作用就是恢复缺省配置,最后恢复完之后调用SystemClock函数,在这个函数里我们就能知道为什么改变宏定义就可以修改主频了

static void SetSysClock(void)
{
#ifdef SYSCLK_FREQ_HSE
SetSysClockToHSE();
#elif defined SYSCLK_FREQ_24MHz
SetSysClockTo24();
#elif defined SYSCLK_FREQ_36MHz
SetSysClockTo36();
#elif defined SYSCLK_FREQ_48MHz
SetSysClockTo48();
#elif defined SYSCLK_FREQ_56MHz
SetSysClockTo56();
#elif defined SYSCLK_FREQ_72MHz
SetSysClockTo72();
#endif

/* If none of the define above is enabled, the HSI is used as System clock
source (default after reset) */
}

这个函数其实就是一个分配函数,根据你定义的不同宏定义选择执行不同的配置函数,比如前面解除了这个72M的宏,那它就执行设置时钟到72M这个函数,根据所选的宏,执行不同的函数,我们当前解除的是72M的宏,就继续挖掘这个函数

static void SetSysClockTo72(void)
{
__IO uint32_t StartUpCounter = 0, HSEStatus = 0;

/* SYSCLK, HCLK, PCLK2 and PCLK1 configuration —————————*/
/* Enable HSE */
RCC->CR |= ((uint32_t)RCC_CR_HSEON);

/* Wait till HSE is ready and if Time out is reached exit */
do
{
HSEStatus = RCC->CR & RCC_CR_HSERDY;
StartUpCounter++;
} while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT));

if ((RCC->CR & RCC_CR_HSERDY) != RESET)
{
HSEStatus = (uint32_t)0x01;
}
else
{
HSEStatus = (uint32_t)0x00;
}

if (HSEStatus == (uint32_t)0x01)
{
/* Enable Prefetch Buffer */
FLASH->ACR |= FLASH_ACR_PRFTBE;

/* Flash 2 wait state */
FLASH->ACR &= (uint32_t)((uint32_t)~FLASH_ACR_LATENCY);
FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_2;

/* HCLK = SYSCLK */
RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1;

/* PCLK2 = HCLK */
RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1;

/* PCLK1 = HCLK */
RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2;

/* PLL configuration: PLLCLK = HSE * 9 = 72 MHz */
RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE |
RCC_CFGR_PLLMULL));
RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9);
#endif /* STM32F10X_CL */

/* Enable PLL */
RCC->CR |= RCC_CR_PLLON;

/* Wait till PLL is ready */
while((RCC->CR & RCC_CR_PLLRDY) == 0)
{
}

/* Select PLL as system clock source */
RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW));
RCC->CFGR |= (uint32_t)RCC_CFGR_SW_PLL;

/* Wait till PLL is used as system clock source */
while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x08)
{
}
}
else
{ /* If HSE fails to start-up, the application will have wrong clock
configuration. User can add here some code to deal with this error */
}
}

到了这里才是正式的时钟配置部分,配置第一步是使能HSE,外部高速时钟,第二步是一个循环,等待HSERDY或者超时退出,接下来根据HSERDY标志位给HSEState置1或0,如果HSEState等于1,表示晶振启动成功进入if,配置HCLK、PCLK2和PCLK1的分频器,HCLK就是AHB的时钟,PCLK就是APB的时钟。函数里分频分别是1、1、2对应到时钟树就SystemClock是72M,AHB和APB2是72M,APB1是36M。跳过#ifdef里无关的内容,可以看到这里配置的是锁相环,PLLCLK = HSE * 9 = 72 MHz ,HSE是8M,锁相环选择9倍频,最终锁相环输出就是72M,之后就是使能锁相环,等待锁相环准备就绪,选择锁相环的输出作为系统时钟,最后等待锁相环成为系统时钟

其他的函数还有SetTo72,还有SetTo56的这里面的执行流程几乎是一样的主要区别就是锁相环的倍频。

最后总结一下:首先是SystemInit函数,进到函数首先启动HSI之后就是各种恢复缺省配置最后调用SetSysClock函数,SetSysClock是一个分配函数,根据我们前面解除注释的宏定义选择执行不同的配置函数,比如SetSysClockTo72、To56、To48等等,最后在这些函数里才是真正的配置,比如To72的配置是,选择HSE作为锁相环输入,之后锁相环进行9倍频再选择锁相环输出作为主频这样主频就是72M了。

接下来我们来完成代码,首先我们前面说过,有个变量可以显示当前主频,我们先显示一下看看:

int main(void)
{
OLED_Init();
OLED_ShowString(1, 1, “SYSCLK:”);
OLED_ShowNum(1, 8, SystemCoreClock, 8);
while (1)
{
}
}
while (1)
{
OLED_ShowString(2, 1, “Runing”);
Delay_ms(500);
OLED_ShowString(2, 1, ” “);
Delay_ms(500);
}
修改主频可以观察到,频率被修改并且running字符闪烁频率变慢

接下来我们要实现睡眠模式+串口发送+接收,就是在串口收发的基础上加上低功耗的代码

假设我们目前要用这个STM32做一个下位机,下位机接收电脑串口发过来的指令,然后执行相应的功能,电脑随时都有可能通过串口发送指令,可能几个小时几天都不发送指令,为了随时能响应指令,对于这种靠中断触发,没有中断的时候就没事干的代码,我们就可以加上低功耗模式。对于这种代码我们能加上哪些低功耗模式呢:
首先睡眠模式肯定是可以的,CPU时钟关闭了,但是外设的时钟不会关,USART硬件电路还是可以接收数据的,USART接收数据后产生中断,唤醒CPU。
之后停机模式(停止模式)下所有1.8v区域的时钟都关闭了,CPU和外设都不能运行,自然USART也收不到数据产生不了中断,并且USART的中断也不能唤醒停止模式,所以当前程序用不了停止模式,之后待机模式就更不行了。
所以就只能加上睡眠模式,一般省电。
在睡眠模式之前我们先加上Running的指示看一下不加睡眠的情况
可以发现不管有没有接收数据,程序都会一直运行消耗电量
要使用睡眠模式的方法也很简单,只要在主循环最后加上WFI指令即可
__WFI();
还可以更加细致的配置这个睡眠模式,具体就是操作一下几个位
具体要操作寄存器,可以参考STM32F10xxx Cortex-M3编程手册的4.4.6章节SCB_SCR
使用睡眠模式之后,可以看到OLED平时就不再闪烁Running了,而是在发送数据到串口的时候才会闪烁
#include “stm32f10x.h”                  // Device header
#include “Delay.h”
#include “OLED.h”
#include “Serial.h”
uint8_t RxData;
int main(void)
{
OLED_Init();
OLED_ShowString(1, 1, “RxData:”);
Serial_Init();
while (1)
{
if (Serial_GetRxFlag() == 1)
{
RxData = Serial_GetRxData();//读DR可以自动清零标志位
Serial_SendByte(RxData);
OLED_ShowHexNum(1, 8, RxData, 2);
}
OLED_ShowString(2, 1, “Running”);
Delay_ms(100);
OLED_ShowString(2, 1, ”       “);
Delay_ms(100);
__WFI();
}
}
我们来分析一下总流程,首先程序开始,初始化把串口配置好之后进入主循环,检查标志位,Runing闪烁一次,在主循环的最后执行WFI,这是CPU就会立刻睡眠,程序就停在了WFI指令这里,这时CPU睡眠,但是各个外设比如USART还是工作状态,等到我们用串口助手发送数据的时候,USART外设收到数据产生中断唤醒CPU,睡眠模式唤醒之后程序会在暂停的地方继续运行,所以程序会运行到WFI后,但唤醒之后,产生唤醒的中断也是会立刻被申请的,所以程序在调回到while循环开头之前,会先进入USART的中断函数,在中断函数这里,读取数据、置RxFlag,清除RXNE,之后回到主循环,然后回到while循环开头,这是RxFlag刚刚置1,所以if成立,执行数据回传和显示的功能

接下来是停止模式的应用,停止模式只能通过外部中断触发(唤醒),所以和停止模式相关的代码肯定得用到外部中断

主函数:
#include “stm32f10x.h”                  // Device header
#include “Delay.h”
#include “OLED.h”
#include “Countsensor.h”
int main(void)
{
OLED_Init();
CountSensor_Init();
OLED_ShowString(1, 1, “Count:”);
while (1)
{
OLED_ShowNum(1, 7, CountSensor_Get(), 5);
}
}
对于这一个代码,思路和上一个睡眠模式很像,看这个CountSensor总是在执行Get,但如果外部一直没有中断信号的话,这个Get就是没有意义的耗电操作,所以对于这个代码,在空闲的时候我们可以让它进入低功耗,又因为这个代码可以使用外部中断触发唤醒,所以我们可以让它进入更为省电的停止模式,在停止模式下1.8v区域的时钟关闭,CPU和外设都没有时钟了,但是外部中断的工作是不需要时钟的,这一点从代码里也可以看出,初始化的时候,根本就没有开启EXTI时钟的参数,这也是EXTI能在时钟关闭的情况下工作的原因,因为它不需要时钟
接下来我们先来看一下库函数,前面的睡眠模式其实都只是内核操作,睡眠模式涉及的几个寄存器也都是在内核里,跟PWR外设关系不大,所以刚才都没用到PWR的库函数,现在的停止模式涉及到内核之外的电路操作就需要用到PWR外设了。
void PWR_DeInit(void);//恢复缺省配置
void PWR_BackupAccessCmd(FunctionalState NewState);//使能后备区域的访问
void PWR_PVDCmd(FunctionalState NewState);//配置PVD的阈值电压
void PWR_PVDLevelConfig(uint32_t PWR_PVDLevel);//使能PVD功能
void PWR_WakeUpPinCmd(FunctionalState NewState);//使能位于PA0位置的WKUP引脚,这个配合待机模式使用,待机模式可以用WKUP引脚的上升沿唤醒,如果需要开启WKUP引脚唤醒功能的话,就得调用一下这个函数使能一下
void PWR_EnterSTOPMode(uint32_t PWR_Regulator, uint8_t PWR_STOPEntry);//进入停止模式,调用这个函数就可以进入停止模式了
void PWR_EnterSTANDBYMode(void);//进入待机模式,调用这个函数进入待机模式
FlagStatus PWR_GetFlagStatus(uint32_t PWR_FLAG);//获取标志位
void PWR_ClearFlag(uint32_t PWR_FLAG);//清除标志位
接下来开始写代码:
停止模式和待机模式都需要PWR外设运行,所以我们首先要开启PWR时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
我们要让它在主循环最后进入停止模式,可以直接调用以下函数:
PWR_EnterSTOPMode(PWR_Regulator_ON, PWR_STOPEntry_WFI);//第一个参数是指定电压调节器在停止模式里的状态,开启或者低功耗,第二个参数是停止模式的入口参数,是选择WFI指令进入停止模式还是选择WFE指令进入停止模式
运行后可以看到在空闲时,Running是没有闪烁了,这说明主循环是停止运行了,每遮挡一次红外对射传感器,Running就会闪烁一次

接下来是今天最后一个代码待机模式+RTC实时时钟

在这个工程里,我们的主要任务就是,1.设置RTC闹钟,2.进入待机模式,3.使用闹钟信号,唤醒待机模式
我们先实现第一个任务,实现闹钟,我们可以在while循环上面这里设定,在每次复位后设置闹钟值,我们直接设置为当前秒数+10,这样每次复位后,闹钟都是10s后了,我们调用void RTC_SetAlarm(uint32_t AlarmValue);就行了
因为这个闹钟寄存器是只写的,库函数也没有RTC_GetAlarm这样的函数,为了避免写进去就不知道是什么了,我们可以在写之前就先给它存下来
uint32_t Alarm = RTC_GetCounter() + 10;
RTC_SetAlarm(Alarm);
OLED_ShowNum(2, 6, Alarm, 10);
while (1)
{
OLED_ShowNum(1, 6, RTC_GetCounter(), 10);
OLED_ShowNum(3, 6, RTC_GetFlagStatus(RTC_FLAG_ALR), 1);

OLED_ShowString(4, 1, “Running”);
Delay_ms(100);
OLED_ShowString(4, 1, ” “);
Delay_ms(100);
}

如此,我们的闹钟测试没有问题,现在可以加入待机模式来测试了,要使用待机模式,就需要PWR外设,使用PWR外设就不要忘了开启PWR的时钟
再在主函数死循环最后加入待机模式即可:
PWR_EnterSTANDBYMode();
复位后Running闪烁一次进入待机,这时CNT不会刷新了,等待10s后,闹钟触发待机模式唤醒,CNT和闹钟值会刷新一下,Running闪烁一次,并且每次唤醒之后闹钟值都会重新设定,因为待机模式唤醒后,代码是从头开始执行的,而且待机模式设置之后的代码会执行不到。
一般在进入待机模式之前,我们都要尽可能的把外挂的模块都关掉
另外还可以使用PA0的WKUP引脚来唤醒待机模式
PWR_WakeUpPinCmd(ENABLE);
这里使用到了GPIO的引脚,但并不需要进行初始化,因为这个WKUP设置的时候会强制置为输入下拉的配置。
产生的现象就是在PA0引脚接通高电平的时刻,待机模式就会被唤醒