一、STM32的启动模式配置与应用
三种BOOT模式
所谓启动,一般来说就是指我们下好程序后,重启芯片时,SYSCLK的第4个上升沿,BOOT引脚的值将被锁存。用户可以通过设置BOOT1和BOOT0引脚的状态,来选择在复位后的启动模式 . whaosoft的stm32合集
- Main Flash memory
是STM32内置的Flash,一般我们使用JTAG或者SWD模式下载程序时,就是下载到这个里面,重启后也直接从这启动程序。 - System memory
从系统存储器启动,这种模式启动的程序功能是由厂家设置的。一般来说,这种启动方式用的比较少。系统存储器是芯片内部一块特定的区域,STM32在出厂时,由ST在这个区域内部预置了一段BootLoader, 也就是我们常说的ISP程序, 这是一块ROM,出厂后无法修改。一般来说,我们选用这种启动模式时,是为了从串口下载程序,因为在厂家提供的BootLoader中,提供了串口下载程序的固件,可以通过这个BootLoader将程序下载到系统的Flash中。
但是这个下载方式需要以下步骤:
Step1:将BOOT0设置为1,BOOT1设置为0,然后按下复位键,这样才能从系统存储器启动BootLoader
Step2:最后在BootLoader的帮助下,通过串口下载程序到Flash中
Step3:程序下载完成后,又有需要将BOOT0设置为GND,手动复位,这样,STM32才可以从Flash中启动可以看到, 利用串口下载程序还是比较的麻烦, 需要跳帽跳来跳去的,非常的不注重用户体验。 - Embedded Memory
内置SRAM,既然是SRAM,自然也就没有程序存储的能力了,这个模式一般用于程序调试。假如我只修改了代码中一个小小的地方,然后就需要重新擦除整个Flash,比较的费时,可以考虑从这个模式启动代码(也就是STM32的内存中),用于快速的程序调试,等程序调试完成后,在将程序下载到SRAM中。
开发BOOT模式选择
通常使用程序代码存储在主闪存存储器,配置方式:BOOT0=0,BOOT1=X。
Flash锁死解决办法
开发调试过程中,由于某种原因导致内部Flash锁死,无法连接SWD以及Jtag调试,无法读到设备,可以通过修改BOOT模式重新刷写代码。
修改为BOOT0=1,BOOT1=0即可从系统存储器启动,ST出厂时自带Bootloader程序,SWD以及JTAG调试接口都是专用的。重新烧写程序后,可将BOOT模式重新更换到BOOT0=0,BOOT1=X即可正常使用。
二、在SMT32的HEX文件中加固件版本信息
使用MDK编译器,让STM32程序HEX文件中加入固件版本信息。
代码
代码如下:
//------------------------------------------------------------------------------
#include <absacc.h>
//------------------------------------------------------------------------------
#define VERINFO_ADDR_BASE (0x8009F00) // 版本信息在FLASH中的存放地址
const char Hardware_Ver[] __attribute__((at(VERINFO_ADDR_BASE + 0x00))) = "Hardware: 1.0.0";
const char Firmware_Ver[] __attribute__((at(VERINFO_ADDR_BASE + 0x20))) = "Firmware: 1.0.0";
const char Compiler_Date[] __attribute__((at(VERINFO_ADDR_BASE + 0x40))) = "Date: "__DATE__;
const char Compiler_Time[] __attribute__((at(VERINFO_ADDR_BASE + 0x60))) = "Time: "__TIME__;
//------------------------------------------------------------------------------
写入到程序中:
选项配置中:Flash地址与大小不用做任何修改!
HEX文件:
串口打印输出:
上述方法的缺点
上述操作可行, 但是有一个缺点:就是生成的bin文件都是满Flash大小的, 造成每次烧录都是整个Flash读写。
其实这个可以把存放地址放到前面,比如偏移1K的地方,都不用改指定地址。
按照上述操作,程序末尾到VERINFO_ADDR_BASE地址这一段会被填充成0x00。根据需要可以修改VERINFO_ADDR_BASE减小地址,或者说不强制指定地址,由编译器自动分配,但这样就要去找相应的版本标识字符串了。
优化方法
不想前面这一段被大量填充0x00,让HEX文件体积小一点的话, 可以把选项配置中Flash的Size改小一点,把VERINFO_ADDR_BASE设置成从FlashSize后面的空间开始,这样生成的HEX文件就小了,且未用空间就不会被大量填充0x00了。
方法如下:
三、获取STM32代码运行时间
测试代码的运行时间的两种方法:
- 使用单片机内部定时器,在待测程序段的开始启动定时器,在待测程序段的结尾关闭定时器。为了测量的准确性,要进行多次测量,并进行平均取值。
- 借助示波器的方法是:在待测程序段的开始阶段使单片机的一个GPIO输出高电平,在待测程序段的结尾阶段再令这个GPIO输出低电平。用示波器通过检查高电平的时间长度,就知道了这段代码的运行时间。显然,借助于示波器的方法更为简便。
借助示波器方法的实例
Delay_us函数使用STM32系统滴答定时器实现:
#include "systick.h"
/* SystemFrequency / 1000 1ms中断一次
* SystemFrequency / 100000 10us中断一次
* SystemFrequency / 1000000 1us中断一次
*/
#define SYSTICKPERIOD 0.000001
#define SYSTICKFREQUENCY (1/SYSTICKPERIOD)
/**
* @brief 读取SysTick的状态位COUNTFLAG
* @param 无
* @retval The new state of USART_FLAG (SET or RESET).
*/
static FlagStatus SysTick_GetFlagStatus(void)
{
if(SysTick->CTRL&SysTick_CTRL_COUNTFLAG_Msk)
{
return SET;
}
else
{
return RESET;
}
}
/**
* @brief 配置系统滴答定时器 SysTick
* @param 无
* @retval 1 = failed, 0 = successful
*/
uint32_t SysTick_Init(void)
{
/* 设置定时周期为1us */
if (SysTick_Config(SystemCoreClock / SYSTICKFREQUENCY))
{
/* Capture error */
return (1);
}
/* 关闭滴答定时器且禁止中断 */
SysTick->CTRL &= ~ (SysTick_CTRL_ENABLE_Msk | SysTick_CTRL_TICKINT_Msk);
return (0);
}
/**
* @brief us延时程序,10us为一个单位
* @param
* @arg nTime: Delay_us( 10 ) 则实现的延时为 10 * 1us = 10us
* @retval 无
*/
void Delay_us(__IO uint32_t nTime)
{
/* 清零计数器并使能滴答定时器 */
SysTick->VAL = 0;
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
for( ; nTime > 0 ; nTime--)
{
/* 等待一个延时单位的结束 */
while(SysTick_GetFlagStatus() != SET);
}
/* 关闭滴答定时器 */
SysTick->CTRL &= ~ SysTick_CTRL_ENABLE_Msk;
}
检验Delay_us执行时间中用到的GPIO(gpio.h、gpio.c)的配置:
#ifndef __GPIO_H
#define __GPIO_H
#include "stm32f10x.h"
#define LOW 0
#define HIGH 1
/* 带参宏,可以像内联函数一样使用 */
#define TX(a) if (a) \
GPIO_SetBits(GPIOB,GPIO_Pin_0);\
else \
GPIO_ResetBits(GPIOB,GPIO_Pin_0)
void GPIO_Config(void);
#endif
#include "gpio.h"
/**
* @brief 初始化GPIO
* @param 无
* @retval 无
*/
void GPIO_Config(void)
{
/*定义一个GPIO_InitTypeDef类型的结构体*/
GPIO_InitTypeDef GPIO_InitStructure;
/*开启LED的外设时钟*/
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
在main函数中检验Delay_us的执行时间:
#include "systick.h"
#include "gpio.h"
/**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
GPIO_Config();
/* 配置SysTick定时周期为1us */
SysTick_Init();
for(;;)
{
TX(HIGH);
Delay_us(1);
TX(LOW);
Delay_us(100);
}
}
示波器的观察结果:
可见Delay_us(100),执行了大概102us,而Delay_us(1)执行了2.2us。
更改一下main函数的延时参数:
int main(void)
{
/* LED 端口初始化 */
GPIO_Config();
/* 配置SysTick定时周期为1us */
SysTick_Init();
for(;;)
{
TX(HIGH);
Delay_us(10);
TX(LOW);
Delay_us(100);
}
}
示波器的观察结果:
可见Delay_us(100),执行了大概101us,而Delay_us(10)执行了11.4us。
结论:此延时函数基本上还是可靠的。
使用定时器方法的实例
Delay_us函数使用STM32定时器2实现:
#include "timer.h"
/* SystemFrequency / 1000 1ms中断一次
* SystemFrequency / 100000 10us中断一次
* SystemFrequency / 1000000 1us中断一次
*/
#define SYSTICKPERIOD 0.000001
#define SYSTICKFREQUENCY (1/SYSTICKPERIOD)
/**
* @brief 定时器2的初始化,,定时周期1uS
* @param 无
* @retval 无
*/
void TIM2_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
/*AHB = 72MHz,RCC_CFGR的PPRE1 = 2,所以APB1 = 36MHz,TIM2CLK = APB1*2 = 72MHz */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
/* Time base configuration */
TIM_TimeBaseStructure.TIM_Period = SystemCoreClock/SYSTICKFREQUENCY -1;
TIM_TimeBaseStructure.TIM_Prescaler = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
TIM_ARRPreloadConfig(TIM2, ENABLE);
/* 设置更新请求源只在计数器上溢或下溢时产生中断 */
TIM_UpdateRequestConfig(TIM2,TIM_UpdateSource_Global);
TIM_ClearFlag(TIM2, TIM_FLAG_Update);
}
/**
* @brief us延时程序,10us为一个单位
* @param
* @arg nTime: Delay_us( 10 ) 则实现的延时为 10 * 1us = 10us
* @retval 无
*/
void Delay_us(__IO uint32_t nTime)
{
/* 清零计数器并使能滴答定时器 */
TIM2->CNT = 0;
TIM_Cmd(TIM2, ENABLE);
for( ; nTime > 0 ; nTime--)
{
/* 等待一个延时单位的结束 */
while(TIM_GetFlagStatus(TIM2, TIM_FLAG_Update) != SET);
TIM_ClearFlag(TIM2, TIM_FLAG_Update);
}
TIM_Cmd(TIM2, DISABLE);
}
在main函数中检验Delay_us的执行时间:
怎么去看检测结果呢?用调试的办法,打开调试界面后,将Time变量添加到Watch一栏中。然后全速运行程序,既可以看到Time中保存变量的变化情况,其中TimeWidthAvrage就是最终的结果。
可以看到TimeWidthAvrage的值等于0x119B8,十进制数对应72120,滴答定时器的一个滴答为1/72M(s),所以Delay_us(1000)的执行时间就是72120*1/72M (s) = 0.001001s,也就是1ms。验证成功。
备注:定时器方法输出检测结果有待改善,你可以把得到的TimeWidthAvrage转换成时间(以us、ms、s)为单位,然后通过串口打印出来,不过这部分工作对于经常使用调试的人员来说也可有可无。
两种方法对比
软件测试方法
操作起来复杂,由于在原代码基础上增加了测试代码,可能会影响到原代码的工作,测试可靠性相对较低。由于使用32位的变量保存systick的计数次数,计时的最大长度可以达到2^32/72M = 59.65 s。
示波器方法
操作简单,在原代码基础上几乎没有增加代码,测试可靠性很高。由于示波器的显示能力有限,超过1s以上的程序段,计时效果不是很理想。但是,通常的单片机程序实时性要求很高,一般不会出现程序段时间超过秒级的情况。
四、STM32启动文件
对STM32启动文件startup_stm32f10x_hd.s的代码进行讲解,此文件的代码在任何一个STM32F10x工程中都可以找到。
启动文件使用的ARM汇编指令汇总
Stack——栈
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=
Stack_Mem SPACE Stack_Size
__initial_sp
开辟栈的大小为 0X00000400(1KB),名字为 STACK, NOINIT 即不初始化,可读可写, 8(2^3)字节对齐。whaosoft开发板商城中使用测试设备www.143ai.com
栈的作用是用于局部变量,函数调用,函数形参等的开销,栈的大小不能超过内部SRAM 的大小。如果编写的程序比较大,定义的局部变量很多,那么就需要修改栈的大小。如果某一天,你写的程序出现了莫名奇怪的错误,并进入了硬 fault 的时候,这时你就要考虑下是不是栈不够大,溢出了。
EQU:宏定义的伪指令,相当于等于,类似于C 中的 define。
AREA:告诉汇编器汇编一个新的代码段或者数据段。STACK 表示段名,这个可以任意命名;NOINIT 表示不初始化;READWRITE 表示可读可写, ALIGN=3,表示按照 2^3对齐,即 8 字节对齐。
SPACE:用于分配一定大小的内存空间,单位为字节。这里指定大小等于 Stack_Size。
标号__initial_sp 紧挨着 SPACE 语句放置,表示栈的结束地址,即栈顶地址,栈是由高向低生长的。
Heap——堆
开辟堆的大小为 0X00000200(512 字节),名字为 HEAP, NOINIT 即不初始化,可读可写, 8(2^3)字节对齐。__heap_base 表示对的起始地址, __heap_limit 表示堆的结束地址。堆是由低向高生长的,跟栈的生长方向相反。
堆主要用来动态内存的分配,像 malloc()函数申请的内存就在堆上面。这个在 STM32里面用的比较少。
PRESERVE8
THUMB
PRESERVE8:指定当前文件的堆栈按照 8 字节对齐。
THUMB:表示后面指令兼容 THUMB 指令。THUBM 是 ARM 以前的指令集, 16bit,现在 Cortex-M 系列的都使用 THUMB-2 指令集, THUMB-2 是 32 位的,兼容 16 位和 32 位的指令,是 THUMB 的超集。
向量表
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
定义一个数据段,名字为 RESET,可读。并声明 __Vectors、 __Vectors_End 和__Vectors_Size 这三个标号具有全局属性,可供外部的文件调用。
EXPORT:声明一个标号可被外部的文件使用,使标号具有全局属性。如果是 IAR 编译器,则使用的是 GLOBAL 这个指令。
当内核响应了一个发生的异常后,对应的异常服务例程(ESR)就会执行。为了决定 ESR的入口地址, 内核使用了―向量表查表机制‖。这里使用一张向量表。向量表其实是一个WORD(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 (即 FLASH 地址 0) 处必须包含一张向量表,用于初始时的异常分配。要注意的是这里有个另类:0 号类型并不是什么入口地址,而是给出了复位后 MSP 的初值。下图是F103的向量表。
__Vectors DCD __initial_sp ;栈顶地址
DCD Reset_Handler ;复位程序地址
DCD NMI_Handler
DCD HardFault_Handler
DCD MemManage_Handler
DCD BusFault_Handler
DCD UsageFault_Handler
DCD 0 ; 0 表示保留
DCD 0
DCD 0
DCD 0
DCD SVC_Handler
DCD DebugMon_Handler
DCD 0
DCD PendSV_Handler
DCD SysTick_Handler
;外部中断开始
DCD WWDG_IRQHandler
DCD PVD_IRQHandler
DCD TAMPER_IRQHandler
;限于篇幅,中间代码省略
DCD DMA2_Channel2_IRQHandler
DCD DMA2_Channel3_IRQHandler
DCD DMA2_Channel4_5_IRQHandler
__Vectors_End
__Vectors_Size EQU __Vectors_End - __Vectors
__Vectors 为向量表起始地址, __Vectors_End 为向量表结束地址,两个相减即可算出向量表大小。
向量表从 FLASH 的 0 地址开始放置,以 4 个字节为一个单位,地址 0 存放的是栈顶地址, 0X04 存放的是复位程序的地址,以此类推。从代码上看,向量表中存放的都是中断服务函数的函数名,可我们知道 C 语言中的函数名就是一个地址。
DCD:分配一个或者多个以字为单位的内存,以四字节对齐,并要求初始化这些内存。在向量表中, DCD 分配了一堆内存,并且以 ESR 的入口地址初始化它们。
复位程序
AREA |.text|, CODE, READONLY
定义一个名称为.text 的代码段,可读。
复位子程序是系统上电后第一个执行的程序,调用 SystemInit 函数初始化系统时钟,然后调用 C 库函数_mian,最终调用 main 函数去到 C 的世界。
WEAK:表示弱定义,如果外部文件优先定义了该标号则首先引用该标号,如果外部文件没有声明也不会出错。这里表示复位子程序可以由用户在其他文件重新实现,这里并不是唯一的。
IMPORT:表示该标号来自外部文件,跟 C 语言中的 EXTERN 关键字类似。这里表示 SystemInit 和__main 这两个函数均来自外部的文件。
SystemInit()是一个标准的库函数,在 system_stm32f10x.c 这个库文件中定义。主要作用是配置系统时钟,这里调用这个函数之后,单片机的系统时钟配被配置为 72M。__main 是一个标准的 C 库函数,主要作用是初始化用户堆栈,并在函数的最后调用main 函数去到 C 的世界。这就是为什么我们写的程序都有一个 main 函数的原因。
LDR、 BLX、 BX 是 CM4 内核的指令,可在《CM3 权威指南 CnR2》第四章-指令集里面查询到,具体作用见下表:
中断服务程序
在启动文件里面已经帮我们写好所有中断的中断服务函数,跟我们平时写的中断服务函数不一样的就是这些函数都是空的,真正的中断服务程序需要我们在外部的 C 文件里面重新实现,这里只是提前占了一个位置而已。
如果我们在使用某个外设的时候,开启了某个中断,但是又忘记编写配套的中断服务程序或者函数名写错,那当中断来临的时,程序就会跳转到启动文件预先写好的空的中断服务程序中,并且在这个空函数中无线循环,即程序就死在这里。
NMI_Handler PROC ;系统异常
EXPORT NMI_Handler [WEAK]
B .
ENDP
;限于篇幅,中间代码省略
SysTick_Handler PROC
EXPORT SysTick_Handler [WEAK]
B .
ENDP
Default_Handler PROC ;外部中断
EXPORT WWDG_IRQHandler [WEAK]
EXPORT PVD_IRQHandler [WEAK]
EXPORT TAMP_STAMP_IRQHandler [WEAK]
;限于篇幅,中间代码省略
LTDC_IRQHandler
LTDC_ER_IRQHandler
DMA2D_IRQHandler
B .
ENDP
B:跳转到一个标号。这里跳转到一个‘.’,即表示无线循环
用户堆栈初始化
ALIGN
ALIGN:对指令或者数据存放的地址进行对齐,后面会跟一个立即数。缺省表示 4 字节对齐。
;用户栈和堆初始化,由 C 库函数_main 来完成
IF :DEF:__MICROLIB ;这个宏在 KEIL 里面开启
EXPORT __initial_sp
EXPORT __heap_base
EXPORT __heap_limit
ELSE
IMPORT __use_two_region_memory ; 这个函数由用户自己实现
EXPORT __user_initial_stackheap
__user_initial_stackheap
LDR R0, = Heap_Mem
LDR R1, =(Stack_Mem + Stack_Size)
LDR R2, = (Heap_Mem + Heap_Size)
LDR R3, = Stack_Mem
BX LR
ALIGN
ENDIF
END
首先判断是否定义了__MICROLIB ,如果定义了这个宏则赋予标号__initial_sp(栈顶地址)、 __heap_base(堆起始地址)、 __heap_limit(堆结束地址)全局属性,可供外部文件调用。有关这个宏我们在 KEIL 里面配置,具体见图 15-2。然后堆栈的初始化就由 C 库函数_main 来完成。
如果没有定义__MICROLIB,则才用双段存储器模式,且声明标号__user_initial_stackheap 具有全局属性,让用户自己来初始化堆栈。
IF,ELSE,ENDIF:汇编的条件分支语句,跟 C 语言的 if ,else 类似
五、在Keil、IAR、CubeIDE软件中,变量不被初始化的方法
01 前言
有些时候在我们的应用过程中要求变量有连续性,或者现场保留,例如 Bootloader 跳转,某种原因的复位过程中我们有些关键变量不能被初始化,在不同的编译环境下有不同的设置,本文就这个操作做总结,分别介绍使用 Keil,IAR 和 CubeIDE 的操作方法,本文中所用芯片为STM32G431RBT6。
02 IAR 实现变量不初始化方法
IAR 实现相对简单,直接使用“__no_init”这个关键字即可,也就是在变量前面进行修饰:
为了验证是否执行成功,可以考虑周期性让系统复位,看变量的变化,比如下面的示例程序让系统周期复位,会发现每次 Test_NoInit 数据都是在上次数据基础上增加 10,而不是被初始化后的数据增加 10。
03 Keil 实现变量不被初始化方法
Keil 中没有像 IAR 里面的这个关键字,而且会有版本的区别,下面分别介绍:
为了防止未初始化的变量被初始化为 0,要将未初始化的变量放在一个特殊段内,这个段满足是 ZI 数据段(.bss),它的执行域(region)具有 UNINIT 属性。
3.1. Arm® Compiler 5 的操作
修改工程的 linker file 文件,*.sct 文件:
这边将 RAM 划分两个区间,其中 RW_IRAM2 就是我们要的变量不初始化区域,属性为UNINIT,定义一个 region 名字 NO_INIT.
变量定义到这个 section,这边 AC5 要用到 zero_init 这个修饰。
3.2. Arm® Compiler 6 的操作
在 AC6 上面需要加入.bss 这个 ZI 定义,如下的 sct 文件修改:
变量定义到 section 部分,AC5 和 AC6 也是有区别的,不再支持 zero_init 这个修饰,如下定义:
对于版本 AC5 和 AC6 具体区别可以参考 Keil 帮助文件中的描述:
04 CubeIDE 实现变量不初始化方法
CubeIDE 的实现和 Keil 有类似的操作,需要修改 linker file 文件*.ld。首先对 RAM 进行划分,划分出不初始化的 RAM 区域:
增加区域描述,并且加入区域名字:
定义变量到这个不初始化区域中:
另外,还提醒一点,有些 STM32 系列有专门针对特定 RAM 区复位后是否会被初始化的 Option 配置位。比方 STM32L4 系列,想让 SRAM2 变量不被初始化,得配置选项字节中的 SRAM2_RST位。如下图所示:
六、STM32呼吸灯的PWM原理
用定时器生成PWM波
PWM全称是Pulse Width Modulation,通过控制高频信号的占空比,眼睛当成低通滤波器,可以控制亮暗。再循环更改pwm的阈值,就弄出了呼吸的效果。
这里采用一个比较简单的方法生成PWM波:设置定时器中断然后根据阈值判断置高和置低。
void TIM3_IRQHandler(void)
{
TIM_ClearITPendingBit(TIM3,TIM_IT_Update);
if(counter==255)
counter = 0;
else
counter +=1;
if(mode == 0){
if(counter < pwm)
GPIO_SetBits(GPIOA,GPIO_Pin_0|GPIO_Pin_1);
else
GPIO_ResetBits(GPIOA,GPIO_Pin_0|GPIO_Pin_1);
}
if(mode == 1)
{
if(counter < pwm)
GPIO_SetBits(GPIOA,GPIO_Pin_1|GPIO_Pin_2);
else
GPIO_ResetBits(GPIOA,GPIO_Pin_1|GPIO_Pin_2);
}
if(mode ==2){
if(counter < pwm)
GPIO_SetBits(GPIOA,GPIO_Pin_2|GPIO_Pin_0);
else
GPIO_ResetBits(GPIOA,GPIO_Pin_2|GPIO_Pin_0);
}
}
程序流程
- 开启外设时钟(GPIO和TIM)
void RCC_Configuration(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC|RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4|RCC_APB1Periph_TIM3, ENABLE);
}
- 配置GPIO
- 配置时钟, 使能中断(计数阈值,预分频,时钟分频,计数模式)
void tim3() //配置TIM3为基本定时器模式 ,约10us触发一次,触发频率约100kHz
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; //定义格式为TIM_TimeBaseInitTypeDef的结构体的名字为TIM_TimeBaseStructure
TIM_TimeBaseStructure. TIM_Period =9; //配置计数阈值为9,超过时,自动清零,并触发中断
TIM_TimeBaseStructure.TIM_Prescaler =71; // 时钟预分频值,除以多少
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频倍数
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 计数方式为向上计数
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); // 初始化tim3
TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除TIM3溢出中断标志
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE); // 使能TIM3的溢出更新中断
TIM_Cmd(TIM3,ENABLE); // 使能TIM3
}
- 配置中断优先级
void nvic() //配置中断优先级
{
NVIC_InitTypeDef NVIC_InitStructure; // // 命名一优先级变量
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); // 将优先级分组方式配置为group1,有2个抢占(打断)优先级,8个响应优先级
NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn; //该中断为TIM4溢出更新中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;//打断优先级为1,在该组中为较低的,0优先级最高
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 响应优先级0,打断优先级一样时,0最高
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 设置使能
NVIC_Init(&NVIC_InitStructure); // 初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); //要用同一个Group
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; //TIM3 溢出更新中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;// 打断优先级为1,与上一个相同,不希望中断相互打断对方
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 响应优先级1,低于上一个,当两个中断同时来时,上一个先执行
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
- 写中断服务函数
代码实现
为了方便按键检测,除了TIM3配置PWM波之外,TIM4用来检测是否有输入。由于使用开漏输出,这里使用5V电源。
#include "stm32f10x.h"
#include "math.h"
#include "stdio.h"
u8 counter=0;
int pwm=100;
int flag=0;
int mode =0;
int velocity =0;
int turning=1;
void RCC_Configuration(void); //时钟初始化,开启外设时钟
void GPIO_Configuration(void); //IO口初始化,配置其功能
void tim3(void); //定时器tim4初始化配置
void tim4(void); //定时器tim4初始化配置
void nvic(void); //中断优先级等配置
void exti(void); //外部中断配置
void delay_nus(u32); //72M时钟下,约延时us
void delay_nms(u32); //72M时钟下,约延时ms
void breathing(int velocity){
switch(velocity){
case 0:
if(flag)
pwm +=1;
if(pwm>240) flag=0;
if(flag == 0){
pwm -=1;
if(pwm<10) flag=1;
}
break;
case 1:
if(flag)
pwm +=2;
if(pwm>240) flag=0;
if(flag == 0){
pwm -=2;
if(pwm<10) flag=1;
}
break;
case 2:
if(flag)
pwm +=3;
if(pwm>240) flag=0;
if(flag == 0){
pwm -=3;
if(pwm<10) flag=1;
}
break;
}
}
void assert_failed(uint8_t* file, uint32_t line)
{
printf("Wrong parameters value: file %s on line %d\r\n", file, line);
while(1);
}
void TIM4_IRQHandler(void) //TIM4的溢出更新中断响应函数 ,读取按键输入值,根据输入控制pwm波占空比
{
u8 key_in1=0x01,key_in2=0x01;
TIM_ClearITPendingBit(TIM4,TIM_IT_Update);// 清空TIM4溢出中断响应函数标志位
key_in1= GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_12); // 读PC12的状态
key_in2= GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_13);// 读PC13的状态
if(key_in1 && key_in2) turning =1;
breathing(velocity);
if(key_in1==0 && turning){
turning =0;
velocity = (velocity + 1) % 3;
}//调速度
if(key_in2==0 && turning){
turning =0;
mode = (mode + 1) % 3;
}//调颜色
}
void TIM3_IRQHandler(void) // //TIM3的溢出更新中断响应函数,产生pwm波
{
TIM_ClearITPendingBit(TIM3,TIM_IT_Update); // // 清空TIM3溢出中断响应函数标志位
if(counter==255) //counter 从0到255累加循环计数,每进一次中断,counter加一
counter = 0;
else
counter +=1;
if(mode == 0){
if(counter < pwm) //当counter值小于pwm值时,将IO口设为高;当counter值大于等于pwm时,将IO口置低
GPIO_SetBits(GPIOA,GPIO_Pin_0|GPIO_Pin_1); //将PC14 PC15置为高电平
else
GPIO_ResetBits(GPIOA,GPIO_Pin_0|GPIO_Pin_1); // 将PC14 PC15置为低电平
}
if(mode == 1)
{
if(counter < pwm) //当counter值小于pwm值时,将IO口设为高;当counter值大于等于pwm时,将IO口置低
GPIO_SetBits(GPIOA,GPIO_Pin_1|GPIO_Pin_2); //将PC14 PC15置为高电平
else
GPIO_ResetBits(GPIOA,GPIO_Pin_1|GPIO_Pin_2); // 将PC14 PC15置为低电平
}
if(mode ==2){
if(counter < pwm) //当counter值小于pwm值时,将IO口设为高;当counter值大于等于pwm时,将IO口置低
GPIO_SetBits(GPIOA,GPIO_Pin_2|GPIO_Pin_0); //将PC14 PC15置为高电平
else
GPIO_ResetBits(GPIOA,GPIO_Pin_2|GPIO_Pin_0); // 将PC14 PC15置为低电平
}
}
int main(void)
{
RCC_Configuration();
GPIO_Configuration();
tim4();
tim3();
nvic();
while(1)
{
}
}
void delay_nus(u32 n) //72M时钟下,约延时us
{
u8 i;
while(n--)
{
i=7;
while(i--);
}
}
void delay_nms(u32 n) //72M时钟下,约延时ms
{
while(n--)
delay_nus(1000);
}
void RCC_Configuration(void) //使用任何一个外设时,务必开启其相应的时钟
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC|RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO, ENABLE); //使能APB2控制外设的时钟,包括GPIOC, 功能复用时钟AFIO等,
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4|RCC_APB1Periph_TIM3, ENABLE); //使能APB1控制外设的时钟,定时器tim3、4,其他外设详见手册
}
void GPIO_Configuration(void) //使用某io口输入输出时,请务必对其初始化配置
{
GPIO_InitTypeDef GPIO_InitStructure; //定义格式为GPIO_InitTypeDef的结构体的名字为GPIO_InitStructure
//typedef struct { u16 GPIO_Pin; GPIOSpeed_TypeDef GPIO_Speed; GPIOMode_TypeDef GPIO_Mode; } GPIO_InitTypeDef;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //配置IO口的工作模式为上拉输入(该io口内部外接电阻到电源)
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //配置IO口最高的输出速率为50M
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12|GPIO_Pin_13; //配置被选中的管脚,|表示同时被选中
GPIO_Init(GPIOC, &GPIO_InitStructure); //初始化GPIOC的相应IO口为上述配置,用于按键检测
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; //配置IO口工作模式为 推挽输出(有较强的输出能力)
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //配置IO口最高的输出速率为50M
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2; //配置被选的管脚,|表示同时被选中
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化GPIOA的相应IO口为上述配置
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE); //失能STM32 JTAG烧写功能,只能用SWD模式烧写,解放出PA15和PB中部分IO口
}
void tim4() //配置TIM4为基本定时器模式,约10ms触发一次,触发频率约100Hz
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; //定义格式为TIM_TimeBaseInitTypeDef的结构体的名字为TIM_TimeBaseStructure
TIM_TimeBaseStructure. TIM_Period =9999; // 配置计数阈值为9999,超过时,自动清零,并触发中断
TIM_TimeBaseStructure.TIM_Prescaler =71; // 时钟预分频值,除以多少
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频倍数
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 计数方式为向上计数
TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure); // 初始化tim4
TIM_ClearITPendingBit(TIM4,TIM_IT_Update); //清除TIM4溢出中断标志
TIM_ITConfig(TIM4,TIM_IT_Update,ENABLE); // 使能TIM4的溢出更新中断
TIM_Cmd(TIM4,ENABLE); // 使能TIM4
}
void tim3() //配置TIM3为基本定时器模式 ,约10us触发一次,触发频率约100kHz
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; //定义格式为TIM_TimeBaseInitTypeDef的结构体的名字为TIM_TimeBaseStructure
TIM_TimeBaseStructure. TIM_Period =9; //配置计数阈值为9,超过时,自动清零,并触发中断
TIM_TimeBaseStructure.TIM_Prescaler =71; // 时钟预分频值,除以多少
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频倍数
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 计数方式为向上计数
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); // 初始化tim3
TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除TIM3溢出中断标志
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE); // 使能TIM3的溢出更新中断
TIM_Cmd(TIM3,ENABLE); // 使能TIM3
}
void nvic() //配置中断优先级
{
NVIC_InitTypeDef NVIC_InitStructure; // // 命名一优先级变量
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); // 将优先级分组方式配置为group1,有2个抢占(打断)优先级,8个响应优先级
NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn; //该中断为TIM4溢出更新中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;//打断优先级为1,在该组中为较低的,0优先级最高
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 响应优先级0,打断优先级一样时,0最高
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 设置使能
NVIC_Init(&NVIC_InitStructure); // 初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); //要用同一个Group
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; //TIM3 溢出更新中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;// 打断优先级为1,与上一个相同,不希望中断相互打断对方
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 响应优先级1,低于上一个,当两个中断同时来时,上一个先执行
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
七、SMT32的HEX文件里加入固件版本的方法
使用MDK编译器,让STM32程序HEX文件中加入固件版本信息。
代码
代码如下:
//------------------------------------------------------------------------------
#include <absacc.h>
//------------------------------------------------------------------------------
#define VERINFO_ADDR_BASE (0x8009F00) // 版本信息在FLASH中的存放地址
const char Hardware_Ver[] __attribute__((at(VERINFO_ADDR_BASE + 0x00))) = "Hardware: 1.0.0";
const char Firmware_Ver[] __attribute__((at(VERINFO_ADDR_BASE + 0x20))) = "Firmware: 1.0.0";
const char Compiler_Date[] __attribute__((at(VERINFO_ADDR_BASE + 0x40))) = "Date: "__DATE__;
const char Compiler_Time[] __attribute__((at(VERINFO_ADDR_BASE + 0x60))) = "Time: "__TIME__;
//------------------------------------------------------------------------------
写入到程序中:
选项配置中:Flash地址与大小不用做任何修改!
HEX文件:
串口打印输出:
上述方法的缺点
上述操作可行, 但是有一个缺点:就是生成的bin文件都是满flash大小的, 造成每次烧录都是整个flash读写。
其实这个可以把存放地址放到前面,比如偏移1K的地方,都不用改指定地址。
按照上述操作,程序末尾到VERINFO_ADDR_BASE地址这一段会被填充成0x00。根据需要可以修改VERINFO_ADDR_BASE减小地址,或者说不强制指定地址,由编译器自动分配,但这样就要去找相应的版本标识字符串了。
优化方法
不想前面这一段被大量填充0x00,让HEX文件体积小一点的话, 可以把选项配置中Flash的Size改小一点,把VERINFO_ADDR_BASE设置成从FlashSize后面的空间开始,这样生成的HEX文件就小了,且未用空间就不会被大量填充0x00了。
方法如下:
八、STM32时钟系统中的SysTick、FCLK、SYSCLK、PCLK和HCLK
时钟信号好比是单片机的脉搏,了解STM32时钟系统是必要的,下图是STM32F1xx用户手册中的时钟系统结构图。
在STM32F1xx中,有五个时钟源,分别为HSI、HSE、LSI、LSE、PLL。
实际上STM32时钟通过STM32CubeIDE可以一键配置
- HSI是高速内部时钟,RC振荡器,频率为8MHz
- HSE是高速外部时钟,可接石英/陶瓷谐振器或者接外部时钟源,频率范围为4MHz~16MHz
- LSI是低速内部时钟,RC振荡器,频率为40kHz
- LSE是低速外部时钟,接频率为32.768kHz的石英晶振
- PLL为锁相环倍频输出,其输出频率最大不得超过72MHz
SYSCLK
系统时钟SYSCLK最大频率为72MHz,它是供STM32中绝大部分部件工作的时钟源。系统时钟可由PLL、HSI或者HSE提供输出,并且它通过AHB分频器分频后送给各模块使用。
HCLK
HCLK为高性能总线AHB(advanced high-performance bus)提供时钟信号。由系统时钟SYSCLK分频得到,一般不分频时等于系统时钟,是给外设使用的。
FCLK
FCLK(free running clock)是自由运行时钟,为CPU内核提供时钟信号。我们所说的CPU主频为xxHz,指的就是这个时钟信号频率,CPU时钟周期就是1/FCLK。
“自由”表现在它不来自系统时钟HCLK,在系统时钟停止时FCLK也继续运行。FCLK用作采样中断或者为调试模块计时。在处理器休眠时,通过FCLK可以采样到中断和跟踪休眠事件。Cortex-M3内核的FCLK和HCLK互相同步、互相平衡,保证Cortex-M3的延迟相同。
PCLK
PCLK为高性能外设总线APB(advanced peripherals bus)提供时钟信号。
STM32365
单片机378
STM32 · 目录
上一篇学习STM32单片机,从菜鸟到牛人下一篇STM32基础实例-按键控制LED