自己的原文哦~https://blog.51cto.com/whaosoft/11897656
一、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)提供时钟信号。
九、STM32开发中的五大嵌入式系统
技术往往更新得非常快,并且总是让我们觉得学起来有难度而且有些迷茫。不过没有关系我们发烧友专注于在快乐中学习,要学习STM32,我们首先了解下五大嵌入式操作系统:μClinux、μC/OS-II、eCos、FreeRTOS和RT-thread。
μClinux
μClinux是一种优秀的嵌入式Linux版本,从字面意思看是指微控制Linux。同标准的Linux相比,μClinux的内核非常小,但是它仍然继承了Linux操作系统的主要特性,包括良好的稳定性和移植性、强大的网络功能、出色的文件系统支持、标准丰富的API,以及TCP/IP网络协议等。
μClinux操作系统的中断管理是将中断处理分为两部分:顶半处理和底半处理。在顶半处理中,必须关中断运行,且仅进行必要的、非常少、速度快的处理,其他处理交给底半处理;底半处理执行那些复杂、耗时的处理,而且接受中断。因为系统中存在有许多中断的底半处理,所以会引起系统中断处理的延时。
μClinux对文件系统支持良好,由于μClinux继承了Linux完善的文件系统性能,它支持ROMFS、NFS、ext2、MS-DOS、JFFS等文件系统。
μClinux最大特点在于针对无MMU处理器设计,这对于没有MMU功能的STM32F103来说是合适的,但移植此系统需要至少512KB的RAM空间,1MB的ROM/FLASH空间,而STM32F103拥有256K的FLASH,需要外接存储器,这就增加了硬件设计的成本。

μClinux结构复杂,移植相对困难,内核也较大,其实时性也差一些,若开发的嵌入式产品注重文件系统和与网络应用则μClinux是一个不错的选择。
μC/OS-II
μC/OS-II是在μC/OS的基础上发展起来的,是用C语言编写的一个结构小巧、抢占式的多任务实时内核。μC/OS-II能管理64个任务,并提供任务调度与管理、内存管理、任务间同步与通信、时间管理和中断服务等功能,具有执行效率高、占用空间小、实时性能优良和扩展性强等特点。

对于实时性的满足上,由于μC/OS-II内核是针对实时系统的要求设计实现的,所以只支持基于固定优先级抢占式调度;调度方法简单,可以满足较高的实时性要求。
μC/OS-II中断处理比较简单。一个中断向量上只能挂一个中断服务子程序ISR,而且用户代码必须都在ISR(中断服务程序)中完成。ISR需要做的事情越多,中断延时也就越长,内核所能支持的最大嵌套深度为255。
μC/OS-II是一个结构简单、功能完备和实时性很强的嵌入式操作系统内核,针对于没有MMU功能的CPU,它是非常合适的。它需要很少的内核代码空间和数据存储空间,拥有良好的实时性,良好的可扩展性能,并且是开源的,网上拥有很多的资料和实例,所以很适合向STM32F103这款CPU上移植。
eCos
eCos,即嵌入式可配置操作系统。它是一个源代码开放的可配置、可移植、面向深度嵌入式应用的实时操作系统。最大特点是配置灵活,采用模块化设计,核心部分由小同的组件构成,包括内核、C语言库和底层运行包等。每个组件可提供大量的配置选项(实时内核也可作为可选配置),使用eCos提供的配置工具可以很方便地配置,并通过不同的配置使得eCos能够满足不同的嵌入式应用要求。
eCos操作系统的可配置性非常强大,用户可以自己加入所需的文件系统。eCos操作系统同样支持当前流行的大部分嵌入式CPU,eCos操作系统可以在16位、32位和64位等不同体系结构之间移植。eCos由于本身内核就很小,经过裁剪后的代码最小可以为10 KB,所需的最小数据RAM空间为10 KB。

在系统移植方面 eCos操作系统的可移植性很好,要比μC/OS-II和μClinux容易。
eCos最大特点是配置灵活,并且支持无MMU的CPU的移植,开源且具有很好的移植性,也比较合适于移植到STM32平台的CPU上。但eCOS的应用还不是太广泛,还没有像μC/OS-II那样普遍,并且资料也没有μC/OS-II多。eCos适合用于一些商业级或工业级对成本敏感的嵌入式系统,例如消费电子领域中的一些应用。
FreeRTOS
由于RTOS需占用一定的系统资源(尤其是RAM资源),只有μC/OS-II、embOS、salvo、FreeRTOS等少数实时操作系统能在小RAM单片机上运行。相对于μC/OS-II、 embOS等商业操作系统,FreeRTOS操作系统是完全免费的操作系统,具有源码公开、可移植、可裁减、调度策略灵活的特点,可以方便地移植到各种单片机上运行,其最新版本为6.0版。
作为一个轻量级的操作系统,FreeRTOS提供的功能包括:任务管理、时间管理、信号量、消息队列、内存管理、记录功能等,可基本满足较小系统的需要。
FreeRTOS内核支持优先级调度算法,每个任务可根据重要程度的不同被赋予一定的优先级,CPU总是让处于就绪态的、优先级最高的任务先运行。FreeRT0S内核同时支持轮换调度算法,系统允许不同的任务使用相同的优先级,在没有更高优先级任务就绪的情况下,同一优先级的任务共享CPU的使用 时间。
相对于常见的μC/OS-II操作系统,FreeRTOS操作系统既有优点也存在不足。其不足之处, 一方面体现在系统的服务功能上,如FreeRTOS只提供了消息队列和信号量的实现,无法以后进先出的顺序向消息队列发送消息。
另一方 面,FreeRTOS只是一个操作系统内核,需外扩第三方的GUI(图形用户界面)、TCP/IP协议栈、FS(文件系统)等才能实现一个较复杂的系统, 不像μC/OS-II可以和μC/GUI、μC/FS、μC/TCP-IP等无缝结合。
RT-thread
RT-Thread 是一款主要由中国开源社区主导开发的开源实时操作系统(许可证GPLv2)。实时线程操作系统不仅仅是一个单一的实时操作系统内核,它也是一个完整的应用系统,包含了实时、嵌入式系统相关的各个组件:TCP/IP协议栈,文件系统,libc接口,图形用户界面等。

十、STM32芯片的内部架构
STM32芯片主要由内核和片上外设组成,STM32F103采用的是Cortex-M3内核,内核由ARM公司设计。STM32的芯片生产厂商ST,负责在内核之外设计部件并生产整个芯片。这些内核之外的部件被称为核外外设或片上外设,如 GPIO、USART(串口)、I2C、SPI 等。

芯片内部架构示意图
芯片内核与外设之间通过各种总线连接,其中驱动单元有 4 个,被动单元也有 4 个,具体如上图所示。可以把驱动单元理解成是内核部分,被动单元都理解成外设。
ICode 总线
ICode总线是专门用来取指令的,其中的I表示Instruction(指令),指令的意思。写好的程序编译之后都是一条条指令,存放在 FLASH中,内核通过ICode总线读取这些指令来执行程序。
DCode总线
DCode这条总线是用来取数的,其中的D表示Data(数据)。在写程序的时候,数据有常量和变量两种。常量就是固定不变的,用C语言中的const关键字修饰,放到内部FLASH当中。变量是可变的,不管是全局变量还是局部变量都放在内部的SRAM。
系统System总线
我们通常说的寄存器编程,即读写寄存器都是通过系统总线来完成的,系统总线主要是用来访问外设的寄存器。
DMA总线
DMA总线也主要是用来传输数据,这个数据可以是在某个外设的数据寄存器,可以在SRAM,可以在内部FLASH。
因为数据可以被Dcode总线,也可以被DMA总线访问,为了避免访问冲突,在取数的时候需要经过一个总线矩阵来仲裁,决定哪个总线在取数。
内部的闪存存储器Flash
内部的闪存存储器即FLASH,编写好的程序就放在这个地方。内核通过ICode总线来取里面的指令。
内部的SRAM
内部的SRAM,是通常所说的内存,程序中的变量、堆栈等的开销都是基于内部SRAM,内核通过DCode总线来访问它。
FSMC
FSMC的英文全称是Flexible static memory controller(灵活的静态的存储器控制器)。通过FSMC可以扩展内存,如外部的SRAM、NAND-FLASH和NORFLASH。但FSMC只能扩展静态的内存,不能是动态的内存,比如就不能用来扩展SDRAM。
AHB
从AHB总线延伸出来的两条APB2和APB1总线是最常见的总线,GPIO、串口、I2C、SPI 这些外设就挂载在这两条总线上。这个是学习STM32的重点,要学会对这些外设编程,去驱动外部的各种设备。
十一、GuiLite,一个优秀的单片机图形库
本文给大家推荐一个很不错的Gui库:GuiLite,非常好用,希望对你有所帮助。
GuiLite介绍
GuiLite是一个开源的Gui框架,只依赖于一个单一的头文件库(GuiLite.h),不需要很复杂的文件管理,代码量平易近人,GuiLite由4千行C++代码编写,单片机上也能流畅运行,其最低的硬件运行要求如下:

同时GuiLite具有很强的跨平台特性:
- 支持的操作系统:iOS/macOS/WatchOS,Android,Linux(ARM/x86-64),Windows(包含VR),RTOS... 甚至无操作系统的单片机
- 支持的开发语言:C/C++, Swift, Java, Javascript, C#, Golang...
- 支持的第3方库:Qt, MFC, Winforms, CoCoa...
除此之外,GuiLite 提供一系列辅助开发工具:
- ☁️完美的"云" + "物联网"解决方案:让你轻松驾驭全球IoT业务
- 支持多语言,采用 UTF-8 编码;支持视频播放
- 资源制作工具为你定制自己的字体/图片资源
- 所见即所得的GUI布局工具
- 编译活跃度统计,及实时分析
- 支持3D & Web
- 支持Docker,一条命令启动。
Gui移植结果
下载完成后程序复位,可以在OLED上看到Demo的示例动画。

一些其他的演示效果:









十二、单片机基础概念:指令、数位、字节、存储器、总线
执行指令
我们来思考一个问题,当我们在编程器中把一条指令写进单片机内部,然后取下单片机,单片机就可以执行这条指令。
那么这条指令一定保存在单片机的某个地方,并且这个地方在单片机掉电后依然可以保持这条指令不会丢失,这是个什么地方呢?这个地方就是单片机内部的只读存储器即ROM(READ ONLY MEMORY)。
为什么称它为只读存储器呢?刚才我们不是明明把两个数字写进去了吗?原来在89C51中的ROM是一种电可擦除的ROM,称为FLASH ROM,相x关推荐:EEPROM和Flash这样讲,我早就懂了。刚才我们是用的编程器,在特殊的条件下由外部设备对ROM进行写的操作,在单片机正常工作条件下,只能从那面读,不能把数据写进去,所以我们还是把它称为ROM。
数的本质和物理现象
我们知道,计算机可以进行数学运算,这令我们非常难以理解,它们只是一些电子元器件,怎么可以进行数学运算呢?
我们人类做数学题如37+45是这样做的,先在纸上写37,然后在下面写45,然后大脑运算最后写出结果,运算的原材料是37和45,结果是82都是写在纸上的,计算机中又是放在什么地方呢?
为了解决这个问题,先让我们做一个实验:这里有一盏灯,我们知道灯要么亮,要么不亮,就有两种状态,我们可以用'0'和'1'来代替这两种状态:规定亮为'1'、不亮为'0'。
现在放上三盏灯,一共有几种状态呢?我们列表来看一下:000 / 001 / 010 / 011 / 100 / 101 / 110 / 111。我们来看,这个000 / 001 / 101 不就是我们学过的的二进制数吗?本来,灯的亮和灭只是一种物理现象,可当我们把它们按一定的顺序排好后,灯的亮和灭就代表了数字了。让我们再抽象一步,灯为什么会亮呢?是因为输出电路输出高电平,给灯通了电。因此,灯亮和灭就可以用电路的输出是高电平还是低电平来替代了。这样,数字就和电平的高、低联系上了。
数位的含义
通过上面的实验我们已经知道:一盏灯亮或者说一根线的电平的高低,可以代表两种状态:0和1,实际上这就是一个二进制位。
因此我们就把一根线称之为一"位",用BIT表示。
一根线可以表示0和1,两根线可以表达00 / 01 / 10 / 11四种状态,也就是可以表达0~3,而三根可以表达0~7,计算机中通常用8根线放在一起,同时计数,就可以表示0~255一共256种状态。
这8根线或者8位就称之为一个字节(BYTE)。
存储器的构造
存储器就是用来存放数据的地方。它是利用电平的高低来存放数据的,也就是说,它存放的实际上是电平的高、低,而不是我们所习惯认为的1234这样的数字,这样,我们的一个谜团就解开了。
一个存储器就象一个个的小抽屉,一个小抽屉里有八个小格子,每个小格子就是用来存放"电荷"的,电荷通过与它相连的电线传进来或释放掉。至于电荷在小格子里是怎样存的,就不用我们操心了,你可以把电线想象成水管,小格子里的电荷就象是水,那就好理解了。存储器中的每个小抽屉就是一个放数据的地方,我们称之为一个"单元"。
有了这么一个构造,我们就可以开始存放数据了,想要放进一个数据12,也就是00001100,我们只要把第二号和第三号小格子里存满电荷,而其它小格子里的电荷给放掉就行了。
可问题出来了,一个存储器有好多单元,线是并联的,在放入电荷的时候,会将电荷放入所有的单元中,而释放电荷的时候,会把每个单元中的电荷都放掉。这样的话,不管存储器有多少个单元,都只能放同一个数,这当然不是我们所希望的。因此,要在结构上稍作变化。
需要在每个单元上有个控制线,想要把数据放进哪个单元,就把一个信号给这个单元的控制线,这个控制线就把开关打开,这样电荷就可以自由流动了。而其它单元控制线上没有信号,所以开关不打开,不会受到影响。
这样,只要控制不同单元的控制线,就可以向各单元写入不同的数据了。同样,如果要从某个单元中取数据,也只要打开相应的控制开关就行了。
存储器的译码
那么,我们怎样来控制各个单元的控制线呢?这个还不简单,把每个单元的控制线都引到集成电路的外面不就行了吗?
事情可没那么简单,一片27512存储器中有65536个单元,把每根线都引出来,这个集成电路就得有6万多个脚?不行,怎么办?要想法减少线的数量。
有一种方法称这为译码,简单介绍一下:一根线可以代表2种状态,2根线可以代表4种状态,3根线可以代表8种,256种状态又需要几根线代表?8根线,所以65536种状态我们只需要16根线就可以代表了。
存储器的选片概念
至此,译码的问题解决了,让我们再来关注另外一个问题。送入每个单元的八根线是用从什么地方来的呢?它就是从计算机上接过来的,一般地,这八根线除了接一个存储器之外,还要接其它的器件。
这样问题就出来了,这八根线既然不是存储器和计算机之间专用的,如果总是将某个单元接在这八根线上,就有问题出现了:比如这个存储器单元中的数值是0FFH另一个存储器的单元是00H,那么这根线到底是处于高电平,还是低电平?怎样分辨?
办法很简单,当外面的线接到集成电路的引脚进来后,不直接接到各单元去,中间再加一组开关就行了。平时我们让开关打开着,如果确实是要向这个存储器中写入数据,或要从存储器中读出数据,再让开关接通就行了。
这组开关由三根引线选择:读控制端、写控制端和片选端。
要将数据写入片中,先选中该片,然后发出写信号,开关就合上了,并将传过来的数据(电荷)写入片中。如果要读,先选中该片,然后发出读信号,开关合上,数据就被送出去了。
读和写信号同时还接入到另一个存储器,但是由于片选端不同,所以虽有读或写信号,但没有片选信号,所以另一个存储器不会"误会"而开门,造成冲突。那么会不同时选中两片芯片呢?
只要是设计好的系统就不会,因为它是由计算控制的,而不是我们人来控制的,如果真的出现同时出现选中两片的情况,那就是电路出了故障了,这不在我们的讨论之列。
总线概念
从上面的介绍中我们已经看到,用来传递数据的八根线并不是专用的,而是很多器件大家共用的。
所以我们称之为数据总线,总线相x关文章:UART、I2C、SPI、TTL、RS232、RS422、RS485、CAN、USB、SD卡、1-WIRE、Ethernet。总线英文名为BUS,总即公交车道,谁也可以走。而十六根地址线也是连在一起的,称之为地址总线。