一、STM32的启动模式配置与应用

三种BOOT模式

    所谓启动,一般来说就是指我们下好程序后,重启芯片时,SYSCLK的第4个上升沿,BOOT引脚的值将被锁存。用户可以通过设置BOOT1和BOOT0引脚的状态,来选择在复位后的启动模式 . whaosoft的stm32合集

51c嵌入式~单片机合集1_嵌入式硬件

  • 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__;


//------------------------------------------------------------------------------

    写入到程序中:

51c嵌入式~单片机合集1_嵌入式硬件_02

    选项配置中:Flash地址与大小不用做任何修改!

51c嵌入式~单片机合集1_嵌入式硬件_03

    HEX文件:

51c嵌入式~单片机合集1_嵌入式硬件_04

    串口打印输出:

51c嵌入式~单片机合集1_嵌入式硬件_05

上述方法的缺点

    上述操作可行, 但是有一个缺点:就是生成的bin文件都是满Flash大小的, 造成每次烧录都是整个Flash读写。

    其实这个可以把存放地址放到前面,比如偏移1K的地方,都不用改指定地址。

    按照上述操作,程序末尾到VERINFO_ADDR_BASE地址这一段会被填充成0x00。根据需要可以修改VERINFO_ADDR_BASE减小地址,或者说不强制指定地址,由编译器自动分配,但这样就要去找相应的版本标识字符串了。

优化方法

    不想前面这一段被大量填充0x00,让HEX文件体积小一点的话, 可以把选项配置中Flash的Size改小一点,把VERINFO_ADDR_BASE设置成从FlashSize后面的空间开始,这样生成的HEX文件就小了,且未用空间就不会被大量填充0x00了。

    方法如下:

51c嵌入式~单片机合集1_嵌入式硬件_06




三、获取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);
    }     
}

    示波器的观察结果:

51c嵌入式~单片机合集1_嵌入式硬件_07

51c嵌入式~单片机合集1_嵌入式硬件_08

    可见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);
    }     
}

    示波器的观察结果:

51c嵌入式~单片机合集1_嵌入式硬件_09

51c嵌入式~单片机合集1_嵌入式硬件_10

    可见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的执行时间:

51c嵌入式~单片机合集1_嵌入式硬件_11

    怎么去看检测结果呢?用调试的办法,打开调试界面后,将Time变量添加到Watch一栏中。然后全速运行程序,既可以看到Time中保存变量的变化情况,其中TimeWidthAvrage就是最终的结果。

51c嵌入式~单片机合集1_嵌入式硬件_12

    可以看到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汇编指令汇总

51c嵌入式~单片机合集1_嵌入式硬件_13

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——堆

51c嵌入式~单片机合集1_嵌入式硬件_14

    开辟堆的大小为 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的向量表。

51c嵌入式~单片机合集1_嵌入式硬件_15

__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 的代码段,可读。

51c嵌入式~单片机合集1_嵌入式硬件_16

    复位子程序是系统上电后第一个执行的程序,调用 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》第四章-指令集里面查询到,具体作用见下表:

51c嵌入式~单片机合集1_嵌入式硬件_17

 中断服务程序

    在启动文件里面已经帮我们写好所有中断的中断服务函数,跟我们平时写的中断服务函数不一样的就是这些函数都是空的,真正的中断服务程序需要我们在外部的 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 来完成。

51c嵌入式~单片机合集1_嵌入式硬件_18

    如果没有定义__MICROLIB,则才用双段存储器模式,且声明标号__user_initial_stackheap 具有全局属性,让用户自己来初始化堆栈。

    IF,ELSE,ENDIF:汇编的条件分支语句,跟 C 语言的 if ,else 类似




五、在Keil、IAR、CubeIDE软件中,变量不被初始化的方法

01 前言

有些时候在我们的应用过程中要求变量有连续性,或者现场保留,例如 Bootloader 跳转,某种原因的复位过程中我们有些关键变量不能被初始化,在不同的编译环境下有不同的设置,本文就这个操作做总结,分别介绍使用 Keil,IAR 和 CubeIDE 的操作方法,本文中所用芯片为STM32G431RBT6。

02 IAR 实现变量不初始化方法

IAR 实现相对简单,直接使用“__no_init”这个关键字即可,也就是在变量前面进行修饰:

51c嵌入式~单片机合集1_嵌入式硬件_19

为了验证是否执行成功,可以考虑周期性让系统复位,看变量的变化,比如下面的示例程序让系统周期复位,会发现每次 Test_NoInit 数据都是在上次数据基础上增加 10,而不是被初始化后的数据增加 10。

51c嵌入式~单片机合集1_嵌入式硬件_20

03 Keil 实现变量不被初始化方法

Keil 中没有像 IAR 里面的这个关键字,而且会有版本的区别,下面分别介绍:

51c嵌入式~单片机合集1_嵌入式硬件_21

为了防止未初始化的变量被初始化为 0,要将未初始化的变量放在一个特殊段内,这个段满足是 ZI 数据段(.bss),它的执行域(region)具有 UNINIT 属性。

3.1. Arm® Compiler 5 的操作

修改工程的 linker file 文件,*.sct 文件:

51c嵌入式~单片机合集1_嵌入式硬件_22

这边将 RAM 划分两个区间,其中 RW_IRAM2 就是我们要的变量不初始化区域,属性为UNINIT,定义一个 region 名字 NO_INIT.





51c嵌入式~单片机合集1_嵌入式硬件_23

变量定义到这个 section,这边 AC5 要用到 zero_init 这个修饰。

51c嵌入式~单片机合集1_嵌入式硬件_24

3.2. Arm® Compiler 6 的操作

在 AC6 上面需要加入.bss 这个 ZI 定义,如下的 sct 文件修改:

51c嵌入式~单片机合集1_嵌入式硬件_25

变量定义到 section 部分,AC5 和 AC6 也是有区别的,不再支持 zero_init 这个修饰,如下定义:

51c嵌入式~单片机合集1_嵌入式硬件_26

对于版本 AC5 和 AC6 具体区别可以参考 Keil 帮助文件中的描述:

51c嵌入式~单片机合集1_嵌入式硬件_27

04 CubeIDE 实现变量不初始化方法

CubeIDE 的实现和 Keil 有类似的操作,需要修改 linker file 文件*.ld。首先对 RAM 进行划分,划分出不初始化的 RAM 区域:

51c嵌入式~单片机合集1_嵌入式硬件_28

增加区域描述,并且加入区域名字:

51c嵌入式~单片机合集1_嵌入式硬件_29

定义变量到这个不初始化区域中:

51c嵌入式~单片机合集1_嵌入式硬件_30

另外,还提醒一点,有些 STM32 系列有专门针对特定 RAM 区复位后是否会被初始化的 Option 配置位。比方 STM32L4 系列,想让 SRAM2 变量不被初始化,得配置选项字节中的 SRAM2_RST位。如下图所示:

51c嵌入式~单片机合集1_嵌入式硬件_31




六、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__;


//------------------------------------------------------------------------------

    写入到程序中:

51c嵌入式~单片机合集1_嵌入式硬件_32

    选项配置中:Flash地址与大小不用做任何修改!

51c嵌入式~单片机合集1_嵌入式硬件_33

    HEX文件:

51c嵌入式~单片机合集1_嵌入式硬件_34

    串口打印输出:

51c嵌入式~单片机合集1_嵌入式硬件_35

上述方法的缺点

    上述操作可行, 但是有一个缺点:就是生成的bin文件都是满flash大小的, 造成每次烧录都是整个flash读写。

    其实这个可以把存放地址放到前面,比如偏移1K的地方,都不用改指定地址。

    按照上述操作,程序末尾到VERINFO_ADDR_BASE地址这一段会被填充成0x00。根据需要可以修改VERINFO_ADDR_BASE减小地址,或者说不强制指定地址,由编译器自动分配,但这样就要去找相应的版本标识字符串了。

优化方法

    不想前面这一段被大量填充0x00,让HEX文件体积小一点的话, 可以把选项配置中Flash的Size改小一点,把VERINFO_ADDR_BASE设置成从FlashSize后面的空间开始,这样生成的HEX文件就小了,且未用空间就不会被大量填充0x00了。

    方法如下:

51c嵌入式~单片机合集1_嵌入式硬件_36







八、STM32时钟系统中的SysTick、FCLK、SYSCLK、PCLK和HCLK

时钟信号好比是单片机的脉搏,了解STM32时钟系统是必要的,下图是STM32F1xx用户手册中的时钟系统结构图。

51c嵌入式~单片机合集1_嵌入式硬件_37

    在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)提供时钟信号。

STM32 · 目录

上一篇学习STM32单片机,从菜鸟到牛人下一篇STM32基础实例-按键控制LED