入手STM32单片机的知识点总结

本文将以stm32f10x为例,对标准库开发进行概览。主要分为三块内容:
stm32系统结构
寄存器
通过点灯案例,详解如何基于标准库构建stm32工程
stm32系统结构
上图,stm32f10xxx系统结构。
内核ip
    从结构框图上看,cortex-m3内部有若干个总线接口,以使cm3能同时取址和访内(访问内存),它们是:指令存储区总线(两条)、系统总线、私有外设总线。有两条代码存储区总线负责对代码存储区(即 flash 外设)的访问,分别是 i-code 总线和 d-code 总线。
i-code用于取指,d-code用于查表等操作,它们按最佳执行速度进行优化。
系统总线(system)用于访问内存和外设,覆盖的区域包括sram,片上外设,片外ram,片外扩展设备,以及系统级存储区的部分空间。
私有外设总线负责一部分私有外设的访问,主要就是访问调试组件。它们也在系统级存储区。
    还有一个dma总线,从字面上看,dma是data memory access的意思,是一种连接内核和外设的桥梁,它可以访问外设、内存,传输不受cpu的控制,并且是双向通信。简而言之,这个家伙就是一个速度很快的且不受老大控制的数据搬运工。相关文章:详解stm32中的dma原理。
处理器外设(内核之外的外设)
    从结构框图上看,stm32的外设有串口、定时器、io口、fsmc、sdio、spi、i2c等,这些外设按照速度的不同,分别挂载到ahb、apb2、apb1这三条总线上。
寄存器
    什么是寄存器?寄存器是内置于各个ip外设中,是一种用于配置外设功能的存储器,并且有想对应的地址。一切库的封装始于映射。
是不是看的眼都花了,如果进行寄存器开发,就需要怼地址以及对寄存器进行字节赋值,不仅效率低而且容易出错。
库的存在就是为了解决这类问题,将代码语义化。语义化思想不仅仅是嵌入式有的,前端代码也在追求语义特性。
从点灯开始学习stm32
内核库文件分析
cor_cm3.h
    这个头文件实现了:
内核结构体寄存器定义。 
内核寄存器内存映射。 
内存寄存器位定义。跟处理器相关的头文件stm32f10x.h实现的功能一样,一个是针对内核的寄存器,一个是针对内核之外,即处理器的寄存器。
misc.h
    内核应用函数库头文件,对应stm32f10x_xxx.h。
misc.c
    内核应用函数库文件,对应stm32f10x_xxx.c。在cm3这个内核里面还有一些功能组件,如nvic、scb、itm、mpu、coredebug,cm3带有非常丰富的功能组件,但是芯片厂商在设计mcu的时候有一些并不是非要不可的,是可裁剪的,比如mpu、itm等在stm32里面就没有。
其中nvic在每一个cm3内核的单片机中都会有,但都会被裁剪,只能是cm3 nvic的一个子集。在nvic里面还有一个systick,是一个系统定时器,可以提供时基,一般为操作系统定时器所用。misc.h和mics.c这两个文件提供了操作这些组件的函数,并可以在cm3内核单片机直接移植。
处理器外设库文件分析
startup_stm32f10x_hd.s
    这个是由汇编编写的启动文件,是stm32上电启动的第一个程序,启动文件主要实现了
初始化堆栈指针 sp;
设置 pc 指针=reset_handler ;
设置向量表的地址,并 初始化向量表,向量表里面放的是 stm32 所有中断函数的入口地址
调用库函数 systeminit,把系统时钟配置成 72m,systeminit 在库文件 stytem_stm32f10x.c 中定义;
跳转到标号_main,最终去到 c 的世界。
system_stm32f10x.c
    这个文件的作用是里面实现了各种常用的系统时钟设置函数,有72m,56m,48, 36,24,8m,我们使用的是是把系统时钟设置成72m。
stm32f10x.h
    这个头文件非常重要,这个头文件实现了:
处理器外设寄存器的结构体定义。
处理器外设的内存映射。
处理器外设寄存器的位定义。
    关于 1 和 2 我们在用寄存器点亮 led 的时候有讲解。
其中 3:处理器外设寄存器的位定义,这个非常重要,具体是什么意思?
我们知道一个寄存器有很多个位,每个位写 1 或者写 0 的功能都是不一样的,处理器外设寄存器的位定义就是把外设的每个寄存器的每一个位写 1 的 16 进制数定义成一个宏,宏名即用该位的名称表示,如果我们操作寄存器要开启某一个功能的话,就不用自己亲自去算这个值是多少,可以直接到这个头文件里面找。相关文章:c语言操作寄存器的常见手法。
    我们以片上外设 adc 为例,假设我们要启动 adc 开始转换,根据手册我们知道是要控制 adc_cr2 寄存器的位 0:adon,即往位 0 写 1,即:
adc->cr2=0x00000001;  
    这是一般的操作方法。现在这个头文件里面有关于 adon 位的位定义:
#define adc_cr2_adon ((uint32_t)0x00000001)  
有了这个位定义,我们刚刚的代码就变成了:
adc->cr2=adc_cr2_adon  
stm32f10x_xxx.h
    外设 xxx 应用函数库头文件,这里面主要定义了实现外设某一功能的结构体,比如通用定时器有很多功能,有定时功能,有输出比较功能,有输入捕捉功能,而通用定时器有非常多的寄存器要实现某一个功能。
    比如定时功能,我们根本不知道具体要操作哪些寄存器,这个头文件就为我们打包好了要实现某一个功能的寄存器,是以机构体的形式定义的,比如通用定时器要实现一个定时的功能,我们只需要初始化 tim_timebaseinittypedef 这个结构体里面的成员即可,里面的成员就是定时所需要操作的寄存器。
    有了这个头文件,我们就知道要实现某个功能需要操作哪些寄存器,然后再回手册中精度这些寄存器的说明即可。
stm32f10x_xxx.c
    stm32f10x_xxx.c:外设 xxx 应用函数库,这里面写好了操作 xxx 外设的所有常用的函数,我们使用库编程的时候,使用的最多的就是这里的函数。
systeminit
    工程中新建main.c 。
    在此文件中编写main函数后直接编译会报错:
undefined symbol systeminit (referred from startup_stm32f10x_hd.o).
    错误提示说systeminit没有定义。从分析启动文件startup_stm32f10x_hd.s时我们知道。
;reset handlerreset_handler procexport reset_handler [weak]import __main;import systeminit;ldr r0, =systeminitblx r0ldr r0, =__mainbx r0endp  
汇编中;分号是注释的意思
    第五行第六行代码reset_handler调用了systeminit该函数用来初始化系统时钟,而该函数是在库文件system_stm32f10x.c中实现的。我们重新写一个这样的函数也可以,把功能完整实现一遍,但是为了简单起见,我们在main文件里面定义一个systeminit空函数,为的是骗过编译器,把这个错误去掉。
    关于配置系统时钟之后会出文章rcc时钟树详细介绍,主要配置时钟控制寄存器(rcc_cr)和时钟配置寄存器(rcc_cfgr)这两个寄存器,但最好是直接使用cubemx直接生成,因为它的配置过程有些冗长。
    如果我们用的是库,那么有个库函数systeminit,会帮我们把系统时钟设置成72m。
    现在我们没有使用库,那现在时钟是多少?答案是8m,当外部hse没有开启或者出现故障的时候,系统时钟由内部低速时钟lsi提供,现在我们是没有开启hse,所以系统默认的时钟是lsi=8m。
库封装层级
如图,达到第四层级便是我们所熟知的固件库或hal库的效果。当然库的编写还需要考虑许多问题,不止于这些内容。我们需要的是了解库封装的大概过程。
    将库封装等级分为四级来介绍是为了有层次感,就像打怪升级一样,进行认知理解的升级。
    我们都知道,操作gpio输出分三大步:
时钟控制:
    stm32 外设很多,为了降低功耗,每个外设都对应着一个时钟,在系统复位的时候这些时钟都是被关闭的,如果想要外设工作,必须把相应的时钟打开。
    stm32 的所有外设的时钟由一个专门的外设来管理,叫rcc(reset and clockcontrol),rcc 在stm32 参考手册的第六章。
    stm32 的外设因为速率的不同,分别挂载到三条总系上:ahb、apb2、apb1,ahb为高速总线,apb2 次之,apb1 再次之。所以的io 口都挂载到apb2 总线上,属于高速外设。
模式配置:
    这个由端口配置寄存器来控制。端口配置寄存器分为高低两个,每4bit 控制一个io 口,所以端口配置低寄存器:crl 控制这io 口的低8 位,端口配置高寄存器:crh控制这io 口的高8bit。
    在4 位一组的控制位中,cnfy[1:0] 用来控制端口的输入输出,modey[1:0]用来控制输出模式的速率,又称驱动电路的响应速度,注意此处速率与程序无关,gpio引脚速度、翻转速度、输出速度区别输入有4种模式,输出有4种模式,我们在控制led 的时候选择通用推挽输出。
    输出速率有三种模式:2m、10m、50m,这里我们选择2m。
电平控制:
    stm32的io口比较复杂,如果要输出1和0,则要通过控制:端口输出数据寄存器odr来实现,odr 是:output data register的简写,在stm32里面,其寄存器的命名名称都是英文的简写,很容易记住。
    从手册上我们知道odr是一个32位的寄存器,低16位有效,高16位保留。低16位对应着io0~io16,只要往相应的位置写入0或者1就可以输出低或者高电平。
    第一层级:基地址宏定义
时钟控制:
在stm32中,每个外设都有一个起始地址,叫做外设基地址,外设的寄存器就以这个基地址为标准按照顺序排列,且每个寄存器32位,(后面作为结构体里面的成员正好内存对齐)。
查表看到时钟由apb2外设时钟使能寄存器(rcc_apb2enr)来控制,其中pb端口的时钟由该寄存器的位3写1使能。我们可以通过基地址+偏移量0x18,算出rcc_apb2enr的地址为:0x40021018。那么使能pb口的时钟代码则如下所示:
#define rcc_apb2enr *(volatile unsigned long *)0x40021018 // 开启端口b 时钟 rcc_apb2enr |= 1<<3;  
    模式配置:
同rcc_apb2enr一样,gpiob的起始地址是:0x4001 0c00,我们也可以算出gpio_crl的地址为:0x40010c00。那么设置pb0为通用推挽输出,输出速率为2m的代码则如下所示:
同上,从手册中我们看到odr寄存器的地址偏移是:0ch,可以算出gpiob_odr寄存器的地址是:0x4001 0c00 + 0x0c = 0x4001 0c0c。现在我们就可以定义gpiob_odr这个寄存器了,代码如下:
#define gpiob_odr *(volatile unsigned long *)0x40010c0c//pb0 输出低电平gpiob_odr = 0<<0;  
    第一层级:基地址宏定义完成用stm32控制一个led的完整代码:
#define rcc_apb2enr *(volatile unsigned long *)0x40021018#define gpiob_crl *(volatile unsigned long *)0x40010c00#define gpiob_odr *(volatile unsigned long *)0x40010c0cint main(void){ // 开启端口b 的时钟 rcc_apb2enr |= 1<<3; // 配置pb0 为通用推挽输出模式,速率为2m gpiob_crl = (2<<0) | (0<<2); // pb0 输出低电平,点亮led gpiob_odr = 0     这时pb0就输出了低电平,led就被点亮了。
    如果要pb2输出低电平,则是:
gpiob->brr = 0x0004;  
    如果要pb3/4/5/6。。。。。。这些io输出低电平呢?
    道理是一样的,只要往brr的相应位置赋不同的值即可。因为brr是一个16位的寄存器,位数比较多,赋值的时候容易出错,而且从赋值的16进制数字我们很难清楚的知道控制的是哪个io。
    这时,我们是否可以把brr的每个位置1都用宏定义来实现,如gpio_pin_0就表示0x0001,gpio_pin_2就表示0x0004。只要我们定义一次,以后都可以使用,而且还见名知意。“位封装”(每一位的对应字节封装) 代码如下:
#define gpio_pin_0 ((uint16_t)0x0001) /*!< pin 0 selected */#define gpio_pin_1 ((uint16_t)0x0002) /*!< pin 1 selected */#define gpio_pin_2 ((uint16_t)0x0004) /*!< pin 2 selected */#define gpio_pin_3 ((uint16_t)0x0008) /*!< pin 3 selected */#define gpio_pin_4 ((uint16_t)0x0010) /*!< pin 4 selected */#define gpio_pin_5 ((uint16_t)0x0020) /*!< pin 5 selected */#define gpio_pin_6 ((uint16_t)0x0040) /*!< pin 6 selected */#define gpio_pin_7 ((uint16_t)0x0080) /*!< pin 7 selected */#define gpio_pin_8 ((uint16_t)0x0100) /*!< pin 8 selected */#define gpio_pin_9 ((uint16_t)0x0200) /*!< pin 9 selected */#define gpio_pin_10 ((uint16_t)0x0400) /*!< pin 10 selected */#define gpio_pin_11 ((uint16_t)0x0800) /*!< pin 11 selected */#define gpio_pin_12 ((uint16_t)0x1000) /*!< pin 12 selected */#define gpio_pin_13 ((uint16_t)0x2000) /*!< pin 13 selected */#define gpio_pin_14 ((uint16_t)0x4000) /*!< pin 14 selected */#define gpio_pin_15 ((uint16_t)0x8000) /*!< pin 15 selected */#define gpio_pin_all ((uint16_t)0xffff) /*!brr = gpio_pin_0;  
    如果同时让pb0/pb15输出低电平,用或运算,代码:
gpiob->brr = gpio_pin_0|gpio_pin_15;  
    为了不使main函数看起来冗余,上述库封装 的代码不应该放在main里面,因为其是跟gpio相关的,我们可以把这些宏放在一个单独的头文件里面。
    在工程目录下新建stm32f10x_gpio.h,把封装代码放里面,然后把这个文件添加到工程里面。这时我们只需要在main.c里面包含这个头文件即可。
    第四层级:基地址宏定义+结构体封装+“位封装”+函数封装
    我们点亮led的时候,控制的是pb0这个io,如果led接到的是其他io,我们就需要把gpiob修改成其他的端口,其实这样修改起来也很快很方便。
    但是为了提高程序的可读性和可移植性,我们是否可以编写一个专门的函数用来复位gpio的某个位,这个函数有两个形参,一个是gpiox(x=a...g),另外一个是gpio_pin(0...15),函数的主体则是根据形参gpiox 和gpio_pin来控制brr寄存器,代码如下:
void gpio_resetbits(gpio_typedef* gpiox, uint16_t gpio_pin){ gpiox->brr = gpio_pin;}  
    这时,pb0输出低电平,点亮led的代码就变成了:
gpio_resetbits(gpiob,gpio_pin_0);  
    同理, 我们可以控制bsrr这个寄存器来实现关闭led,代码如下:
// gpio 端口置位函数void gpio_setbits(gpio_typedef* gpiox, uint16_t gpio_pin){ gpiox->bsrr = gpio_pin;}  
    这时,pb0输出高电平,关闭led的代码就变成了:
gpio_setbits(gpiob,gpio_pin_0);  
    同样,因为这个函数是控制gpio的函数,我们可以新建一个专门的文件来放跟gpio有关的函数。相关文章:stm32中gpio工作原理详解。
    在工程目录下新建stm32f10x_gpio.c,把gpio相关的函数放里面。这时我们是否发现刚刚新建了一个头文件stm32f10x_gpio.h,这两个文件存放的都是跟外设gpio相关的。
    c文件里面的函数会用到h头文件里面的定义,这两个文件是相辅相成的,故我们在stm32f10x_gpio.c 文件中也包含stm32f10x_gpio.h这个头文件。别忘了把stm32f10x.h这个头文件也包含进去,因为有关寄存器的所有定义都在这个头文件里面。
    如果我们写其他外设的函数,我们也应该跟gpio一样,新建两个文件专门来存函数,比如rcc这个外设我们可以新建stm32f10x_rcc.c和stm32f10x_rcc.h。其他外依葫芦画瓢即可。
实例编写
    以上,是对库封住过程的概述,下面我们正在地使用库函数编写led程序。
①管理库的头文件
    当我们开始调用库函数写代码的时候,有些库我们不需要,在编译的时候可以不编译,可以通过一个总的头文件stm32f10x_conf.h来控制,该头文件主要代码如下:
这里面包含了全部外设的头文件,点亮一个led我们只需要rcc和gpio 这两个外设的库函数即可,其中rcc控制的是时钟,gpio控制的具体的io口。所以其他外设库函数的头文件我们注释掉,当我们需要的时候就把相应头文件的注释去掉即可。
    stm32f10x_conf.h这个头文件在stm32f10x.h这个头文件的最后面被包含,在第8296行:
#ifdef use_stdperiph_driver#include stm32f10x_conf.h#endif  
    代码的意思是,如果定义了use_stdperiph_driver这个宏的话,就包含stm32f10x_conf.h这个头文件。
    我们在新建工程的时候,在魔术棒选项卡c/c++中,我们定义了use_stdperiph_driver 这个宏,所以stm32f10x_conf.h 这个头文件就被stm32f10x.h包含了,我们在写程序的时候只需要调用一个头文件:stm32f10x.h即可。
②编写led初始化函数
    经过寄存器点亮led的操作,我们知道操作一个gpio输出的编程要点大概如下:
1、开启gpio的端口时钟
2、选择要具体控制的io口,即pin
3、选择io口输出的速率,即speed
4、选择io口输出的模式,即mode
5、输出高/低电平
    stm32的时钟功能非常丰富,配置灵活,为了降低功耗,每个外设的时钟都可以独自的关闭和开启。stm32中跟时钟有关的功能都由rcc这个外设控制,rcc中有三个寄存器控制着所以外设时钟的开启和关闭:rcc_aphenr、rcc_apb2enr和rcc_apb1enr,ahb、apb2和apb1代表着三条总线,所有的外设都是挂载到这三条总线上,gpio属于高速的外设,挂载到apb2总线上,所以其时钟有rcc_apb2enr控制。
gpio 时钟控制
    固件库函数:rcc_apb2periphclockcmd( rcc_apb2periph_gpiob, enable)函数的原型为:
void rcc_apb2periphclockcmd(uint32_t rcc_apb2periph, functionalstate newstate){/* check the parameters */ assert_param(is_rcc_apb2_periph(rcc_apb2periph)); assert_param(is_functional_state(newstate));if (newstate != disable) { rcc->apb2enr |= rcc_apb2periph; } else { rcc->apb2enr &= ~rcc_apb2periph; }}  
    当程序编译一次之后,把光标定位到函数/变量/宏定义处,按键盘的f12或鼠标右键的go to definition of,就可以找到原型。固件库的底层操作的就是rcc外设的apb2enr这个寄存器,宏rcc_apb2periph_gpiob的原型是:0x00000008,即(1     端口位清除寄存器brr是一个32位的寄存器,低十六位有效,对应着io0~io15,只能以字的形式操作,可以单独对某一个位操作,写1清0。
// pb0 输出低电平,点亮ledgpio_resetbits(gpiob, gpio_pin_0);  
    bsrr是一个32位的寄存器,低16位用于置位,写1有效,高16位用于复位,写1有效,相当于brr寄存器。高16位我们一般不用,而是操作brr这个寄存器,所以bsrr这个寄存器一般用来置位操作。
// pb0 输出高电平,熄灭ledgpio_setbits(gpiob, gpio_pin_0);  
    综上:固件库led gpio初始化函数。
void led_gpio_config(void){// 定义一个gpio_inittypedef 类型的结构体 gpio_inittypedef gpio_initstructure;// 开启gpiob 的时钟 rcc_apb2periphclockcmd( rcc_apb2periph_gpiob, enable);// 选择要控制的io 口 gpio_initstructure.gpio_pin = gpio_pin_0;// 设置引脚为推挽输出 gpio_initstructure.gpio_mode = gpio_mode_out_pp;// 设置引脚速率为50mhz gpio_initstructure.gpio_speed = gpio_speed_50mhz;/*调用库函数,初始化gpiob0*/ gpio_init(gpiob, &gpio_initstructure);// 关闭led gpio_setbits(gpiob, gpio_pin_0);}  
主函数
#include stm32f10x.hvoid soft_delay(__io uint32_t ncount);void led_gpio_config(void);int main(void){// 程序来到main 函数之前,启动文件:statup_stm32f10x_hd.s 已经调用// systeminit()函数把系统时钟初始化成72mhz// systeminit()在system_stm32f10x.c 中定义// 如果用户想修改系统时钟,可自行编写程序修改 led_gpio_config();while ( 1 ) {// 点亮led gpio_resetbits(gpiob, gpio_pin_0); time_delay(0x0fffff);// 熄灭led gpio_setbits(gpiob, gpio_pin_0); time_delay(0x0fffff); }}// 简陋的软件延时函数void time_delay(volatile uint32_t count){for (; count != 0; count--);}  
    注意void time_delay(volatile uint32_t count)只是一个简陋的软件延时函数,如果小伙伴们有兴趣可以看一看multitimer,它是一个软件定时器扩展模块,可无限扩展所需的定时器任务,取代传统的标志位判断方式, 更优雅更便捷地管理程序的时间触发时序。


易灵思Programmer工具的配置模式过程分析
电烙铁通电后不发热的原因是什么
lcr数字电桥的校准与检定方法
电子芯闻早报:可穿戴市场将达顶峰 一加3开卖
高性能VLS-II系列燃料电池量产,可提供每升4.2kW的高功率密度
入手STM32单片机的知识点总结
springboot自动配置的原理介绍
巴西电信表示未来网络发展需要考虑社会经济和服务的统一化因素
LED光源实现植物生长动态补光控制的几大要点
FPGA设计案例:数据缓存模块设计与验证实验
智联安高精定位低速RedCap芯片MK8520荣获本次IOTE 2023创新产品金奖
STM32G0开发笔记:多通道ADC与DMA的使用
WiSA E开发套件开始向全球发货,旨在统一非Sonos音频市场
液体特性对微孔压电超声雾化效果影响
中国光伏企业的未来,筹码是国外新兴市场?
2K+骁龙835+8G+全面屏,一加5即将发布碾压小米6
小米上下折叠手机专利被曝光 配备翻转式镜头
Ripple推出的XRP代币将成为下一个比特币
爱特梅尔推出低功耗8位微控制器ATtiny 10/20/40
瑞萨电子扩展“云实验室”,重塑远程设计