你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

【经验分享】入手STM32单片机的知识点总结

[复制链接]
STMCU小助手 发布时间:2022-6-7 21:25
   本文将以STM32F10x为例,对标准库开发进行概览。主要分为三块内容:
0 P: R6 U' `4 y, h
  • STM32系统结构
  • 寄存器
  • 通过点灯案例,详解如何基于标准库构建STM32工程# ]/ `/ s  S$ ]% r+ H2 D
STM32系统结构
O3%8JFD{%MTH1)DPIMHL%(1.png
    上图,STM32f10xxx系统结构。
内核IP
    从结构框图上看,Cortex-M3内部有若干个总线接口,以使CM3能同时取址和访内(访问内存),它们是:指令存储区总线(两条)、系统总线、私有外设总线。有两条代码存储区总线负责对代码存储区(即 FLASH 外设)的访问,分别是 I-Code 总线和 D-Code 总线。
    I-Code用于取指,D-Code用于查表等操作,它们按最佳执行速度进行优化。
    系统总线(System)用于访问内存和外设,覆盖的区域包括SRAM,片上外设,片外RAM,片外扩展设备,以及系统级存储区的部分空间。
    私有外设总线负责一部分私有外设的访问,主要就是访问调试组件。它们也在系统级存储区。
    还有一个DMA总线,从字面上看,DMA是data memory access的意思,是一种连接内核和外设的桥梁,它可以访问外设、内存,传输不受CPU的控制,并且是双向通信。简而言之,这个家伙就是一个速度很快的且不受老大控制的数据搬运工。

9 y  n# h  M. t0 V
处理器外设(内核之外的外设)
    从结构框图上看,STM32的外设有串口、定时器、IO口、FSMC、SDIO、SPI、I2C等,这些外设按照速度的不同,分别挂载到AHB、APB2、APB1这三条总线上。

( ^, ?& A' P  Z8 s; K) w( T
寄存器
    什么是寄存器?寄存器是内置于各个IP外设中,是一种用于配置外设功能的存储器,并且有想对应的地址。一切库的封装始于映射。
TDYMT$}J1GGFWO500VK71MJ.png
    是不是看的眼都花了,如果进行寄存器开发,就需要怼地址以及对寄存器进行字节赋值,不仅效率低而且容易出错。
    库的存在就是为了解决这类问题,将代码语义化。语义化思想不仅仅是嵌入式有的,前端代码也在追求语义特性。

) |& G/ C+ r2 a! `  J) P
从点灯开始学习STM32
U(5G)(D$PW0R4DZ]HGW8]Z6.png
- S3 h$ \. p+ i. d
内核库文件分析
1 ?' F/ R: u* f. c8 ~; w) g
cor_cm3.h
    这个头文件实现了:
  • 内核结构体寄存器定义。
  • 内核寄存器内存映射。
  • 内存寄存器位定义。跟处理器相关的头文件stm32f10x.h实现的功能一样,一个是针对内核的寄存器,一个是针对内核之外,即处理器的寄存器。
    - E% ?0 ?6 T7 M( u% `* t
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内核单片机直接移植。

% Q/ X$ y  D6 _, s$ P# w
处理器外设库文件分析startup_stm32f10x_hd.s
    这个是由汇编编写的启动文件,是STM32上电启动的第一个程序,启动文件主要实现了
  • 初始化堆栈指针 SP;
  • 设置 PC 指针=Reset_Handler ;
  • 设置向量表的地址,并 初始化向量表,向量表里面放的是 STM32 所有中断函数的入口地址
  • 调用库函数 SystemInit,把系统时钟配置成 72M,SystemInit 在库文件 stytem_stm32f10x.c 中定义;
  • 跳转到标号_main,最终去到 C 的世界。( s% b& u# P5 e6 N/ }
system_stm32f10x.c
    这个文件的作用是里面实现了各种常用的系统时钟设置函数,有72M,56M,48, 36,24,8M,我们使用的是是把系统时钟设置成72M。
Stm32f10x.h
    这个头文件非常重要,这个头文件实现了:
  • 处理器外设寄存器的结构体定义。
  • 处理器外设的内存映射。
  • 处理器外设寄存器的位定义。

      M3 v. v' Y+ y, G* R
    关于 1 和 2 我们在用寄存器点亮 LED 的时候有讲解。
    其中 3:处理器外设寄存器的位定义,这个非常重要,具体是什么意思?
    我们知道一个寄存器有很多个位,每个位写 1 或者写 0 的功能都是不一样的,处理器外设寄存器的位定义就是把外设的每个寄存器的每一个位写 1 的 16 进制数定义成一个宏,宏名即用该位的名称表示,如果我们操作寄存器要开启某一个功能的话,就不用自己亲自去算这个值是多少,可以直接到这个头文件里面找。
    我们以片上外设 ADC 为例,假设我们要启动 ADC 开始转换,根据手册我们知道是要控制 ADC_CR2 寄存器的位 0:ADON,即往位 0 写 1,即:
  1. ADC->CR2=0x00000001;
复制代码
$ E  B* Z' a* K6 Z5 j
    这是一般的操作方法。现在这个头文件里面有关于 ADON 位的位定义:
  1. #define ADC_CR2_ADON ((uint32_t)0x00000001)
复制代码

2 d0 m  [7 h* i3 n! c
有了这个位定义,我们刚刚的代码就变成了:
  1. <font face="Tahoma"><font size="3"><font color="#000000">ADC->CR2=ADC_CR2_ADON</font></font></font><b><b><font face="Tahoma"><font size="3"><font color="#000000">stm32f10x_xxx.h</font></font></font></b></b>
复制代码
( z0 N$ J% ?0 f* X5 E
    外设 xxx 应用函数库头文件,这里面主要定义了实现外设某一功能的结构体,比如通用定时器有很多功能,有定时功能,有输出比较功能,有输入捕捉功能,而通用定时器有非常多的寄存器要实现某一个功能。
    比如定时功能,我们根本不知道具体要操作哪些寄存器,这个头文件就为我们打包好了要实现某一个功能的寄存器,是以机构体的形式定义的,比如通用定时器要实现一个定时的功能,我们只需要初始化 TIM_TimeBaseInitTypeDef 这个结构体里面的成员即可,里面的成员就是定时所需要操作的寄存器。
    有了这个头文件,我们就知道要实现某个功能需要操作哪些寄存器,然后再回手册中精度这些寄存器的说明即可。

  S1 x2 ^1 B; x, ~0 W( F
stm32f10x_xxx.c
    stm32f10x_xxx.c:外设 xxx 应用函数库,这里面写好了操作 xxx 外设的所有常用的函数,我们使用库编程的时候,使用的最多的就是这里的函数。

3 @6 @- H* `9 q) V4 l, j
SystemInit
    工程中新建main.c 。
    在此文件中编写main函数后直接编译会报错:
Undefined symbol SystemInit (referred from startup_stm32f10x_hd.o).
% r' z0 _, t. F/ w3 ^! N
    错误提示说SystemInit没有定义。从分析启动文件startup_stm32f10x_hd.s时我们知道。
  1. 1 ?4 a& ]" c' i' J% F
  2. ;Reset handler% y7 r4 W  ^/ I# p
  3. Reset_Handler PROC: J$ w, R7 J  Q& z  i4 {0 Q
  4. EXPORT Reset_Handler [WEAK]
    ! E# z* v+ c- V% d9 u6 y7 q
  5. IMPORT __main% r) u6 w; ~5 s. v) @
  6. ;IMPORT SystemInit) h0 w, j" f) z0 N
  7. ;LDR R0, =SystemInit5 {  y/ e5 b* h4 D$ Q$ W
  8. BLX R03 u- w9 e$ y7 r% C( p1 e
  9. LDR R0, =__main
    ( q$ k# g, D% e1 w4 R
  10. BX R0" k4 d5 d; B- }% ^" C3 Z
  11. ENDP
复制代码

8 y+ _1 K  o" d) W7 W
汇编中;分号是注释的意思
    第五行第六行代码Reset_Handler调用了SystemInit该函数用来初始化系统时钟,而该函数是在库文件system_stm32f10x.c中实现的。我们重新写一个这样的函数也可以,把功能完整实现一遍,但是为了简单起见,我们在main文件里面定义一个SystemInit空函数,为的是骗过编译器,把这个错误去掉。
    关于配置系统时钟之后会出文章RCC时钟树详细介绍,主要配置时钟控制寄存器(RCC_CR)和时钟配置寄存器(RCC_CFGR)这两个寄存器,但最好是直接使用CubeMX直接生成,因为它的配置过程有些冗长。
    如果我们用的是库,那么有个库函数SystemInit,会帮我们把系统时钟设置成72M。
    现在我们没有使用库,那现在时钟是多少?答案是8M,当外部HSE没有开启或者出现故障的时候,系统时钟由内部低速时钟LSI提供,现在我们是没有开启HSE,所以系统默认的时钟是LSI=8M。
; X0 X% g& s% ^: C3 [* b8 u1 U* B
库封装层级
% a7 u; G( w! y4 @3 u2 p; B3 T
~TQS80YXQW3M0AIM4(ZCC)A.png
! B$ C  G  r  B0 C
    如图,达到第四层级便是我们所熟知的固件库或HAL库的效果。当然库的编写还需要考虑许多问题,不止于这些内容。我们需要的是了解库封装的大概过程。
    将库封装等级分为四级来介绍是为了有层次感,就像打怪升级一样,进行认知理解的升级。
    我们都知道,操作GPIO输出分三大步:

% x' y  I, h8 d( {
时钟控制:
    STM32 外设很多,为了降低功耗,每个外设都对应着一个时钟,在系统复位的时候这些时钟都是被关闭的,如果想要外设工作,必须把相应的时钟打开。
    STM32 的所有外设的时钟由一个专门的外设来管理,叫RCC(reset and clockcontrol),RCC 在STM32 参考手册的第六章。
    STM32 的外设因为速率的不同,分别挂载到三条总系上:AHB、APB2、APB1,AHB为高速总线,APB2 次之,APB1 再次之。所以的IO 口都挂载到APB2 总线上,属于高速外设。
+ N" k2 _9 ]1 y, {! N
模式配置:
    这个由端口配置寄存器来控制。端口配置寄存器分为高低两个,每4bit 控制一个IO 口,所以端口配置低寄存器:CRL 控制这IO 口的低8 位,端口配置高寄存器:CRH控制这IO 口的高8bit。
    在4 位一组的控制位中,CNFy[1:0] 用来控制端口的输入输出,MODEy[1:0]用来控制输出模式的速率,又称驱动电路的响应速度,注意此处速率与程序无关,GPIO引脚速度、翻转速度、输出速度区别输入有4种模式,输出有4种模式,我们在控制LED 的时候选择通用推挽输出。
    输出速率有三种模式:2M、10M、50M,这里我们选择2M。
* ?3 d1 \, F, Z
电平控制:
    STM32的IO口比较复杂,如果要输出1和0,则要通过控制:端口输出数据寄存器ODR来实现,ODR 是:Output data register的简写,在STM32里面,其寄存器的命名名称都是英文的简写,很容易记住。
    从手册上我们知道ODR是一个32位的寄存器,低16位有效,高16位保留。低16位对应着IO0~IO16,只要往相应的位置写入0或者1就可以输出低或者高电平。
    第一层级:基地址宏定义
(KTF`~D3Z6X3@M{S88Z[$OI.png
    时钟控制:
_PGC1)UI@1{L~N}$HJIN(3D.png
    在STM32中,每个外设都有一个起始地址,叫做外设基地址,外设的寄存器就以这个基地址为标准按照顺序排列,且每个寄存器32位,(后面作为结构体里面的成员正好内存对齐)。
查表看到时钟由APB2外设时钟使能寄存器(RCC_APB2ENR)来控制,其中PB端口的时钟由该寄存器的位3写1使能。我们可以通过基地址+偏移量0x18,算出RCC_APB2ENR的地址为:0x40021018。那么使能PB口的时钟代码则如下所示:
  1. #define RCC_APB2ENR *(volatile unsigned long *)0x40021018
    0 A+ S4 H0 A( g/ }2 ]8 ]
  2. ; \  D+ d8 g9 d# y& K% ^
  3. // 开启端口B 时钟+ `+ T5 x! z7 \$ t% f+ o  C$ n
  4. RCC_APB2ENR |= 1<<3;
复制代码
3 L& {" Y# ^' r
模式配置:
[S(U0X}$${DF%LX%ELPOUAR.png
    同RCC_APB2ENR一样,GPIOB的起始地址是:0X4001 0C00,我们也可以算出GPIO_CRL的地址为:0x40010C00。那么设置PB0为通用推挽输出,输出速率为2M的代码则如下所示:
+ W4 C7 x& j0 k; h# n6 U4 D1 Q9 a
$PV%O~4%T7SU)]%{I@B0QIF.png

$ M0 |7 e; s, O) r  V" I. p- S( m0 W
    同上,从手册中我们看到ODR寄存器的地址偏移是:0CH,可以算出GPIOB_ODR寄存器的地址是:0X4001 0C00 + 0X0C = 0X4001 0C0C。现在我们就可以定义GPIOB_ODR这个寄存器了,代码如下:
  1. #define GPIOB_ODR *(volatile unsigned long *)0x40010C0C+ |' ^5 I/ u8 K. X- L9 ]0 b) c4 B

  2. 8 h+ O/ `- P9 _  u
  3. //PB0 输出低电平6 v7 h2 W4 e$ r$ a  p
  4. GPIOB_ODR = 0<<0;
复制代码
  X# a% f$ T* C6 ~4 p: K
    第一层级:基地址宏定义完成用STM32控制一个LED的完整代码:
  1. #define RCC_APB2ENR *(volatile unsigned long *)0x40021018- Z' ~4 O( P% X: H% r" T: E
  2. #define GPIOB_CRL *(volatile unsigned long *)0x40010C00
    & s$ ^/ T! B3 c0 P5 u" t% P
  3. #define GPIOB_ODR *(volatile unsigned long *)0x40010C0C) D7 J! K! R" S7 q
  4. : y$ W, L/ Z% d; C0 n' l7 D
  5. int main(void)0 t  O4 |; U2 S7 S& M+ n' |
  6. {9 x/ t! z9 W1 k7 s0 S: I( H$ R8 O. o
  7. // 开启端口B 的时钟
    . d, v* L5 V2 L7 U/ T
  8. RCC_APB2ENR |= 1<<3;( a- c/ i$ `" n2 d: M. G9 ~

  9. 5 a" T8 r- \- y6 I) V* A6 L4 k1 j$ Z
  10. // 配置PB0 为通用推挽输出模式,速率为2M
    # B* m; F0 k+ {) J( g" h+ H
  11. GPIOB_CRL = (2<<0) | (0<<2);0 Q7 e1 b& _5 Y- V0 _

  12. ' ~2 @8 h, Q! D8 G. E
  13. // PB0 输出低电平,点亮LED) t5 ]# e9 ]2 L; U/ A; Z( K
  14. GPIOB_ODR = 0<<0;% h* j+ H- R  x( F& k
  15. }) p, p6 O$ h, R6 W9 K

  16. ! Z/ t4 `# m  T) k
  17. void SystemInit(void)" c; |  m$ B* B6 F. a+ \5 k5 {( z+ X1 E& k
  18. {% [& f/ M/ D+ d2 @+ ]3 _4 x
  19. }
复制代码

' A3 ^: V3 h6 m" k8 Z+ X
    第二层级:基地址宏定义+结构体封装
外设寄存器结构体封装
    上面我们在操作寄存器的时候,操作的是寄存器的绝对地址,如果每个寄存器都这样操作,那将非常麻烦。我们考虑到外设寄存器的地址都是基于外设基地址的偏移地址,都是在外设基地址上逐个连续递增的,每个寄存器占32个或者16个字节,这种方式跟结构体里面的成员类似。
    所以我们可以定义一种外设结构体,结构体的地址等于外设的基地址,结构体的成员等于寄存器,成员的排列顺序跟寄存器的顺序一样。这样我们操作寄存器的时候就不用每次都找到绝对地址,只要知道外设的基地址就可以操作外设的全部寄存器,即操作结构体的成员即可。
    下面我们先定义一个GPIO寄存器结构体,结构体里面的成员是GPIO的寄存器,成员的顺序按照寄存器的偏移地址从低到高排列,成员类型跟寄存器类型一样。
  1. typedef struct
    - O! i0 D; K! \2 `) N8 j/ q* W
  2. {
    # F; J) c3 U. q( a6 E, c
  3. volatile uint32_t CRL;% e5 h  i+ f; ?3 R
  4. volatile uint32_t CRH;
    ' {) {6 \- Q& j" W0 q% q
  5. volatile uint32_t IDR;- C& }6 R: L( a# Q9 d% ?. q
  6. volatile uint32_t ODR;
    6 t& l& {2 u/ P0 f7 A
  7. volatile uint32_t BSRR;5 Q, R2 y, `/ N, ?) J! h
  8. volatile uint32_t BRR;
    1 }9 j1 g6 n% _" W. ?# A7 z( ]
  9. volatile uint32_t LCKR;
    5 q" z4 I4 H! h
  10. } GPIO_TypeDef;
复制代码

7 X+ @  s- e6 a" P; `$ z% [6 a
    在《STM32 中文参考手册》8.2 寄存器描述章节,我们可以找到结构体里面的7个寄存器描述。在点亮LED的时候我们只用了CRL和ODR这两个寄存器,至于其他寄存器的功能大家可以自行看手册了解。
    在GPIO结构体里面我们用了两个数据类型,一个是uint32_t,表示无符号的32位整型,因为GPIO的寄存器都是32位的。这个类型声明在标准头文件stdint.h 里面使用typedef对unsigned int重命名,我们在程序上只要包含这个头文件即可。
    另外一个是volatile作用就是告诉编译器这里的变量会变化不因优化而省略此指令,必须每次都直接读写其值,这样就能确保每次读或者写寄存器都真正执行到位。
外设封装- r! s; v/ y) Z1 [
    STM32F1系列的GPIO端口分A~G,即GPIOA、GPIOB。。。。。。GPIOG。每个端口都含有GPIO_TypeDef结构体里面的寄存器,我们可以根据手册各个端口的基地址把GPIO的各个端口定义成一个GPIO_TypeDef类型指针,然后我们就可以根据端口名(实际上现在是结构体指针了)来操作各个端口的寄存器,代码实现如下:

) I% Q) I$ N& ?( J: [
  1. #define GPIOA ((GPIO_TypeDef *) 0X4001 0800)6 W, ]6 h3 }) y: y
  2. #define GPIOB ((GPIO_TypeDef *) 0X4001 0C00)& g! u2 y) k) h! H5 v& Y5 g
  3. #define GPIOC ((GPIO_TypeDef *) 0X4001 1000)
    , B( [8 D7 y4 |- W. b- H2 w, \
  4. #define GPIOD ((GPIO_TypeDef *) 0X4001 1400)7 J* U0 W  G* H; S
  5. #define GPIOE ((GPIO_TypeDef *) 0X4001 1800)! N# W- j( Y3 ^, f- i$ J2 @
  6. #define GPIOF ((GPIO_TypeDef *) 0X4001 1C00)2 B& J& _0 I% ~( d  B$ \6 X8 ^6 t
  7. #define GPIOG ((GPIO_TypeDef *) 0X4001 2000)
复制代码
7 C1 Z7 f6 B9 e0 ]
/ @9 {4 G1 M" ]8 [* B
外设内存映射
    讲到基地址的时候我们再引人一个知识点:Cortex-M3存储器系统,这个知识点在《Cortex-M3权威指南》第5章里面讲到。CM3的地址空间是4GB,如下图所示:
3 E3 d$ R! W  t; L+ t2 B
    我们这里要讲的是片上外设,就是我们所说的寄存器的根据地,其大小总共有512MB,512MB是其极限空间,并不是每个单片机都用得完,实际上各个MCU厂商都只是用了一部分而已。STM32F1系列用到了:0x4000 0000 ~0x5003 FFFF。现在我们说的STM32的寄存器就是位于这个区域。

& }2 C- P3 w" L1 b3 r3 Y0 E% Y
APB1、APB2、AHB 总线基地址
    现在我们说的STM32的寄存器就是位于这个区域,这里面ST设计了三条总线:AHB、APB2和APB1,其中AHB和APB2是高速总线,APB1是低速总线。不同的外设根据速度不同分别挂载到这三条总线上。
    从下往上依次是:APB1、APB2、AHB,每个总线对应的地址分别是:APB1:0x40000000,APB2:0x4001 0000,AHB:0x4001 8000。
    这三条总线的基地址我们是从《STM32 中文参考手册》2.3小节—存储器映像得到的:APB1的基地址是TIM2定时器的起始地址,APB2的基地址是AFIO的起始地址,AHB的基地址是SDIO的起始地址。其中APB1地址又叫做外设基地址,是所有外设的基地址,叫做PERIPH_BASE。
    现在我们把这三条总线地址用宏定义出来,以后我们在定义其他外设基地址的时候,只需要在这三条总线的基址上加上偏移地址即可,代码如下:
  h* Z/ x5 m% v* Z9 M- z( u
  1. #define PERIPH_BASE ((uint32_t)0x40000000)
    7 s' u: T5 @( I$ Q. v! u* i: x+ G: W
  2. #define APB1PERIPH_BASE PERIPH_BASE
    . z7 K% j7 \) S# y: m. ]! J
  3. #define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
    ; F* M& n( M* V% W
  4. #define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)
复制代码
) v6 H, e6 g7 A, ^' {8 f, J

7 g7 e; x  A, V9 _GPIO 端口基地址
    因为GPIO挂载到APB2总线上,那么现在我们就可以根据APB2的基址算出各个GPIO端口的基地址,用宏定义实现代码如下:
  1. #define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)) i, x3 R& w9 w8 o) ]2 K& g
  2. #define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
    5 |! v) ?( ^4 |0 H: X
  3. #define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)$ a; j% F4 k" W9 \1 v
  4. #define GPIOD_BASE (APB2PERIPH_BASE + 0x1400)
    7 f4 D$ J  k3 R/ f
  5. #define GPIOE_BASE (APB2PERIPH_BASE + 0x1800)) A6 l( }: _7 b& n. p' c- U& L/ V$ E
  6. #define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00)
    0 y9 ~( @+ \: q( N* n* L
  7. #define GPIOG_BASE (APB2PERIPH_BASE + 0x2000)
复制代码

! c: [; y3 o! u8 p
    第二层级:基地址宏定义+结构体封装完成用STM32控制一个LED的完整代码:
  1. #include <stdint.h>: }& O9 b4 o0 M' _
  2. #define __IO volatile
    " \# |: Y9 r! m9 \7 W

  3. ' T, \$ F% v  c, s6 l
  4. typedef struct
    # B' `  K0 t. i: e5 f9 g) Z0 s
  5. {0 v  e. A# l$ A: i  l
  6. __IO uint32_t CRL;
    / K+ i$ |! X( w8 x& b
  7. __IO uint32_t CRH;
    ) ~* O! f5 s. [9 C
  8. __IO uint32_t IDR;( Q' P8 y$ }: @# y) I$ _# o
  9. __IO uint32_t ODR;
    / y* S+ b8 A! E! Y
  10. __IO uint32_t BSRR;1 J- i% i, M  N3 U
  11. __IO uint32_t BRR;" F7 m. o: ~- \
  12. __IO uint32_t LCKR;6 r2 o5 l3 ~1 n: I3 \( Q8 X2 ?. H6 e
  13. } GPIO_TypeDef;
    ( P6 C: F/ t7 ^; E8 b: H; J
  14. * ~7 G! N6 y8 y' Q! w2 ?/ C  G
  15. typedef struct
    ' T$ O& i6 ~8 z/ C" |5 P
  16. {
    / N9 P+ W+ N- `  K
  17. __IO uint32_t CR;
    4 ^- I7 \$ r. R  S! Q
  18. __IO uint32_t CFGR;
    4 s/ S. d2 a: [# R7 v, M
  19. __IO uint32_t CIR;
    ' V9 c, Z. q% Y- i7 R
  20. __IO uint32_t APB2RSTR;
    + w2 s' R  i4 B$ R2 F. M
  21. __IO uint32_t APB1RSTR;
    4 Q( g# k8 D9 R2 ?& {
  22. __IO uint32_t AHBENR;
    1 A' {& x* x9 G3 n: B) u
  23. __IO uint32_t APB2ENR;  n9 s! K- g9 I$ p* I# d  H
  24. __IO uint32_t APB1ENR;
    / D4 S0 L: ]$ ~" V8 R8 e
  25. __IO uint32_t BDCR;
    2 n/ g/ `  @9 p7 q$ r
  26. __IO uint32_t CSR;. W4 x; _0 p3 I9 W" }- e
  27. } RCC_TypeDef;( i4 m6 q1 Z) l( V8 ?: L; M; ^4 [
  28. 0 H5 r( |1 Z. Y- v6 j
  29. #define PERIPH_BASE ((uint32_t)0x40000000)
    ; U* M, z8 M- r$ j9 I6 M1 Z

  30. 7 F0 u0 x) F, O
  31. #define APB1PERIPH_BASE PERIPH_BASE
    ) W4 k- E5 s  d9 y
  32. #define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
    9 ]. ^9 F; ^. ^! i/ y* w. `; O, v
  33. #define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)
    . \4 D' L% f* [% L1 M8 \  V
  34. ( G0 c! [$ e# Y0 i: e' \! }) e4 B' {# n
  35. #define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
    3 _/ E% e# v0 o+ n! b. @
  36. #define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
    8 ^% O9 z$ Z6 e
  37. #define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)2 J) N8 e6 I% z, F" \
  38. #define GPIOD_BASE (APB2PERIPH_BASE + 0x1400)
    6 P3 f9 K# j2 p5 b4 s' \; j
  39. #define GPIOE_BASE (APB2PERIPH_BASE + 0x1800)* V7 ?  w/ G2 Q. T8 c
  40. #define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00)
    " k' @6 f7 @. n
  41. #define GPIOG_BASE (APB2PERIPH_BASE + 0x2000)) W+ |$ \' z) h1 v
  42. #define RCC_BASE (AHBPERIPH_BASE + 0x1000)
    ! n1 \/ ~5 j6 c& H
  43. ' h& ~- w9 z8 I, Y+ p9 I& F1 W& w; j
  44. #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)) m2 S2 H3 O& x- O9 V. M
  45. #define GPIOB ((GPIO_TypeDef *) GPIOB_BASE). S! c! \, L" r( e# S
  46. #define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)& Y( ^* o, ]' c/ L; g
  47. #define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
    % g# Z  M9 q+ i) B$ H0 Q& {2 z+ M
  48. #define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
    % L; |" ^; i+ ?& K* t; D! o
  49. #define GPIOF ((GPIO_TypeDef *) GPIOF_BASE), e# A. O# B8 S- c9 r: _) W
  50. #define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
    - H2 s; N, S  r1 J  H; d
  51. #define RCC ((RCC_TypeDef *) RCC_BASE)
    7 m$ l. B& _9 U/ W; d5 c
  52. , X- l" T; s( c
  53. 8 a! o( `: C/ ?0 l* r9 p
  54. #define RCC_APB2ENR *(volatile unsigned long *)0x40021018" v* F; W2 Y+ e* {) I
  55. #define GPIOB_CRL *(volatile unsigned long *)0x40010C00
    : U  k6 s& C- }) ~
  56. #define GPIOB_ODR *(volatile unsigned long *)0x40010C0C
    , F# P* I- Q& L  j

  57. , K0 }8 H- Y4 W- L1 G+ l. T
  58. int main(void)
    ! ]# W1 P7 z* }1 W9 _
  59. {
    / ~3 |1 M) b2 S5 M0 P5 q8 f
  60. // 开启端口B 的时钟4 A# H' o  w1 A5 U: |4 X+ M" G- D. d, x
  61. RCC->APB2ENR |= 1<<3;! Z1 _4 q7 C. E/ Z8 R  |

  62. ' x, O( W, o5 F
  63. // 配置PB0 为通用推挽输出模式,速率为2M1 z5 |6 i$ u; A1 C% M  y
  64. GPIOB->CRL = (2<<0) | (0<<2);9 K8 g# Q. v( u8 ^+ @: m1 s
  65. 7 P' B$ z( t" p3 |' @' }) r0 |
  66. // PB0 输出低电平,点亮LED  o# u% l+ I# S; X5 s" K
  67. GPIOB->ODR = 0<<0;
    % i" ]: \7 I3 p% J
  68. }0 O0 t. N4 z% r  l4 `+ X3 Z3 ~
  69. ' L; z+ S/ b+ }3 K/ e3 n% Q
  70. void SystemInit(void)
    4 P1 f! A: P3 I( B9 c
  71. {
    6 Z. C% k# `) ?7 A
  72. }
复制代码

% U+ D5 n. Y( d3 M/ N
    第二层级变化:
    ①、定义一个外设(GPIO)寄存器结构体,结构体的成员包含该外设的所有寄存器,成员的排列顺序跟寄存器偏移地址一样,成员的数据类型跟寄存器的一样。
    ②外设内存映射,即把地址跟外设建立起一一对应的关系。
    ③外设声明,即把外设的名字定义成一个外设寄存器结构体类型的指针。
    ④通过结构体操作寄存器,实现点亮LED。
    第三层级:基地址宏定义+结构体封装+“位封装”(每一位的对应字节封装)
上面我们在控制GPIO输出内容的时候控制的是ODR(Output data register)寄存器,ODR是一个16位的寄存器,必须以字的形式控制其实我们还可以控制BSRR和BRR这两个寄存器来控制IO的电平,下面我们简单介绍下BRR寄存器的功能,BSRR自行看手册研究。
LTIP`4F@0$(DAY[AS3{[1KR.png
    位清除寄存器BRR只能实现位清0操作,是一个32位寄存器,低16位有效,写0没影响,写1清0。现在我们要使PB0输出低电平,点亮LED,则只要往BRR的BR0位写1即可,其他位为0,代码如下:
  1. GPIOB->BRR = 0X0001;
复制代码

8 L5 x! F2 @9 Q/ C8 ?4 O! T
    这时PB0就输出了低电平,LED就被点亮了。
    如果要PB2输出低电平,则是:
  1. GPIOB->BRR = 0X0004;
复制代码

8 M9 m2 K( v7 O  {, x! F0 L! ^
    如果要PB3/4/5/6。。。。。。这些IO输出低电平呢?
    道理是一样的,只要往BRR的相应位置赋不同的值即可。因为BRR是一个16位的寄存器,位数比较多,赋值的时候容易出错,而且从赋值的16进制数字我们很难清楚的知道控制的是哪个IO。
    这时,我们是否可以把BRR的每个位置1都用宏定义来实现,如GPIO_Pin_0就表示0X0001,GPIO_Pin_2就表示0X0004。只要我们定义一次,以后都可以使用,而且还见名知意。“位封装”(每一位的对应字节封装) 代码如下:
  1. #define GPIO_Pin_0 ((uint16_t)0x0001) /*!< Pin 0 selected */" q  U0 J! M* K9 o$ A& s
  2. #define GPIO_Pin_1 ((uint16_t)0x0002) /*!< Pin 1 selected */( @, Q6 ?8 v7 O$ b0 R; q# P- x
  3. #define GPIO_Pin_2 ((uint16_t)0x0004) /*!< Pin 2 selected */
    3 }3 s5 m$ t( K5 H  c1 p
  4. #define GPIO_Pin_3 ((uint16_t)0x0008) /*!< Pin 3 selected */0 X7 A( x. Q* o7 d7 l. }2 l- G# {$ {
  5. #define GPIO_Pin_4 ((uint16_t)0x0010) /*!< Pin 4 selected */
    7 `. u& d2 ]1 Z/ n6 ?- J
  6. #define GPIO_Pin_5 ((uint16_t)0x0020) /*!< Pin 5 selected */+ K" x  F/ V" P" @3 s, a& k' r
  7. #define GPIO_Pin_6 ((uint16_t)0x0040) /*!< Pin 6 selected */
    4 z1 W/ H. J& t5 m
  8. #define GPIO_Pin_7 ((uint16_t)0x0080) /*!< Pin 7 selected */
    % Z/ O" `! k; x, q
  9. #define GPIO_Pin_8 ((uint16_t)0x0100) /*!< Pin 8 selected */' Y' u2 L( e3 o; m' G, Q4 V- e7 M; |6 x' M
  10. #define GPIO_Pin_9 ((uint16_t)0x0200) /*!< Pin 9 selected */
    3 [* b5 L& c4 ^& ?) |" d9 \
  11. #define GPIO_Pin_10 ((uint16_t)0x0400) /*!< Pin 10 selected */
    , u7 [" \7 `  W. e/ D
  12. #define GPIO_Pin_11 ((uint16_t)0x0800) /*!< Pin 11 selected */
    0 R+ J1 q' O4 f4 ~9 X
  13. #define GPIO_Pin_12 ((uint16_t)0x1000) /*!< Pin 12 selected */
    , j! e; d5 M6 Z% U* \8 k, U5 A
  14. #define GPIO_Pin_13 ((uint16_t)0x2000) /*!< Pin 13 selected */
    9 @% x& Z' m- d
  15. #define GPIO_Pin_14 ((uint16_t)0x4000) /*!< Pin 14 selected */
    * ]3 I% O, b8 ~  W; R+ \! d0 e
  16. #define GPIO_Pin_15 ((uint16_t)0x8000) /*!< Pin 15 selected */
    & |. H5 a3 F0 J. }7 v& B
  17. #define GPIO_Pin_All ((uint16_t)0xFFFF) /*!< All pins selected */
复制代码

8 y+ U5 c4 X" @# N* d, M$ i
    这时PB0就输出了低电平的代码就变成了:
  1. GPIOB->BRR = GPIO_Pin_0;
复制代码

6 D" ]% ?9 s  x+ ~& W% B$ M
    如果同时让PB0/PB15输出低电平,用或运算,代码:
  1. GPIOB->BRR = GPIO_Pin_0|GPIO_Pin_15;
复制代码

, g  f1 Q' [- r  ~. K! x. J
    为了不使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寄存器,代码如下:
  1. void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
    + ]. _* Z) @6 c- S; @7 O6 l- w( p
  2. {
    ) _& r. G* U8 S5 U  W9 B; u- D
  3. GPIOx->BRR = GPIO_Pin;; G; w* Z; n8 q; o
  4. }
复制代码
9 W6 f- A) y8 x* A' B
    这时,PB0输出低电平,点亮LED的代码就变成了:
  1. GPIO_ResetBits(GPIOB,GPIO_Pin_0);
复制代码

7 n& z" }, y, V) ]4 [& k" s, s
    同理, 我们可以控制BSRR这个寄存器来实现关闭LED,代码如下:
  1. // GPIO 端口置位函数
    , N5 @! q4 Y8 N: H  w
  2. void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
    / Q% s4 a9 A. }  p
  3. {# Q4 O+ z. c" p  f9 @2 R# ?
  4. GPIOx->BSRR = GPIO_Pin;4 B3 E$ G4 R# c0 t1 ?
  5. }
复制代码
8 V5 G, X$ E, ]; w8 l" g. V& _
    这时,PB0输出高电平,关闭LED的代码就变成了:
  1. GPIO_SetBits(GPIOB,GPIO_Pin_0);
复制代码

, Y6 ~2 Y7 n2 k: N$ ?
    同样,因为这个函数是控制GPIO的函数,我们可以新建一个专门的文件来放跟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来控制,该头文件主要代码如下:

( Z' g4 I" |# i3 [  |* d
~LPPLQ5J1ITFXM{@`][$LK3.png

! V/ @! o! w% b# M8 P
    这里面包含了全部外设的头文件,点亮一个LED我们只需要RCC和GPIO 这两个外设的库函数即可,其中RCC控制的是时钟,GPIO控制的具体的IO口。所以其他外设库函数的头文件我们注释掉,当我们需要的时候就把相应头文件的注释去掉即可。
    stm32f10x_conf.h这个头文件在stm32f10x.h这个头文件的最后面被包含,在第8296行:
  1. #ifdef USE_STDPERIPH_DRIVER
    7 b: F  {4 Z1 A( ?0 Q$ ?  \
  2. #include "stm32f10x_conf.h"
    0 t1 Y1 i- Z) d% k
  3. #endif
复制代码
' h  t; R: t4 u; `# S* Y
    代码的意思是,如果定义了USE_STDPERIPH_DRIVER这个宏的话,就包含stm32f10x_conf.h这个头文件。
    我们在新建工程的时候,在魔术棒选项卡C/C++中,我们定义了USE_STDPERIPH_DRIVER 这个宏,所以stm32f10x_conf.h 这个头文件就被stm32f10x.h包含了,我们在写程序的时候只需要调用一个头文件:stm32f10x.h即可。

# K. K, e- Z  r% x/ W& Q' Y. N# a0 X
②编写LED初始化函数
    经过寄存器点亮LED的操作,我们知道操作一个GPIO输出的编程要点大概如下:
1、开启GPIO的端口时钟
2、选择要具体控制的IO口,即pin
3、选择IO口输出的速率,即speed
4、选择IO口输出的模式,即mode
5、输出高/低电平

; O1 C# Z+ t8 N, a
    STM32的时钟功能非常丰富,配置灵活,为了降低功耗,每个外设的时钟都可以独自的关闭和开启。STM32中跟时钟有关的功能都由RCC这个外设控制,RCC中有三个寄存器控制着所以外设时钟的开启和关闭:RCC_APHENR、RCC_APB2ENR和RCC_APB1ENR,AHB、APB2和APB1代表着三条总线,所有的外设都是挂载到这三条总线上,GPIO属于高速的外设,挂载到APB2总线上,所以其时钟有RCC_APB2ENR控制。
8 \" L/ K* E# W) r1 F4 C' ]
GPIO 时钟控制
    固件库函数:RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE)函数的原型为:
  1. void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph,2 v8 g( v, z8 g" ~1 M- j
  2.                               FunctionalState NewState)
    ; p3 p+ X+ _* l
  3. {
    / f. B' I' }1 V4 w1 T7 r
  4. /* Check the parameters */
    ) l. G( o- D* H* Q& ?. q/ |
  5. assert_param(IS_RCC_APB2_PERIPH(RCC_APB2Periph));
    4 v4 _3 }4 @  Y5 Q* ~' g
  6. assert_param(IS_FUNCTIONAL_STATE(NewState));
    # t- @2 a0 G  W7 u  x$ C6 d
  7. if (NewState != DISABLE)
    * s4 ^( R8 g& S  m8 D4 \3 Y# j
  8. {
    3 V1 W7 I8 Z8 s
  9.   RCC->APB2ENR |= RCC_APB2Periph;, p3 m! z3 u) `$ |6 ]
  10. } . ^: r$ |8 Q  Y
  11. else
    & c1 ^8 S  k- t1 K( u3 q
  12. {
    % N6 m% {+ P) V7 U* p
  13.   RCC->APB2ENR &= ~RCC_APB2Periph;% M! q; b  t3 ?1 M' V/ Y* G
  14. }
    & V$ K  J$ X- j0 c" O) J
  15. }
复制代码

; S+ |' B4 E% C' r6 H% {8 C! d% `4 A
    当程序编译一次之后,把光标定位到函数/变量/宏定义处,按键盘的F12或鼠标右键的Go to definition of,就可以找到原型。固件库的底层操作的就是RCC外设的APB2ENR这个寄存器,宏RCC_APB2Periph_GPIOB的原型是:0x00000008,即(1<<3),还原成存器操作就是:RCC->APB2ENR |= 1<<<3。相比固件库操作,寄存器操作的代码可读性就很差,只有才查阅寄存器配置才知道具体代码的功能,而固件库操作恰好相反,见名知意。

( N  u# p% y6 h
GPIO 端口配置
    GPIO的pin,速度,模式,都由GPIO的端口配置寄存器来控制,其中IO0~IO7由端口配置低寄存器CRL控制,IO8~IO15由端口配置高寄存器CRH配置。固件库把端口配置的pin,速度和模式封装成一个结构体:
  1. typedef struct
    5 N/ y% j8 N6 U+ ^6 n6 d
  2. {
    0 q$ q" g2 q+ o7 F) H
  3. uint16_t GPIO_Pin;
    1 _/ |* _. `" ]1 W9 _
  4. GPIOSpeed_TypeDef GPIO_Speed;8 a1 O! b; N6 _- A& @& T
  5. GPIOMode_TypeDef GPIO_Mode;
    * C7 d8 a) g- ]" F
  6. } GPIO_InitTypeDef;
复制代码
2 N, ~" s4 G8 S8 O7 i* X
    pin可以是GPIO_Pin_0~GPIO_Pin_15或者是GPIO_Pin_All,这些都是库预先定义好的宏。speed也被封装成一个结构体:
  1. typedef enum
    / p1 `& J: v5 X! W# O3 \; r/ m
  2. {' G1 U3 G3 X1 a9 t7 |4 N1 F
  3. GPIO_Speed_10MHz = 1," y8 \/ c; b! ~; C4 h
  4. GPIO_Speed_2MHz,
    + D" V. i( T* q2 l8 {4 A9 R
  5. GPIO_Speed_50MHz. \! N5 Q; k; v* J8 N- P
  6. } GPIOSpeed_TypeDef;
复制代码
7 k# a7 K+ U, q' Z. l" U, ]
    速度可以是10M,2M或者50M,这个由端口配置寄存器的MODE位控制,速度是针对IO口输出的时候而言,在输入的时候可以不用设置。mode也被封装成一个结构体:
  1. typedef enum
    ) B' Q' q- P5 H9 @& {
  2. {
    & }' c0 _% {0 k1 f# N; }. R
  3. GPIO_Mode_AIN = 0x0, // 模拟输入
    ' u9 E9 p* P( V3 G
  4. GPIO_Mode_IN_FLOATING = 0x04, // 浮空输入(复位后的状态): L* U& A9 Z3 T$ L. F! ?7 k
  5. GPIO_Mode_IPD = 0x28, // 下拉输入/ M! X. h6 X# \8 `
  6. GPIO_Mode_IPU = 0x48, // 上拉输入- C( B% k& F' t
  7. GPIO_Mode_Out_OD = 0x14, // 通用开漏输出' W, C9 G: K- Y& O/ i* u7 b
  8. GPIO_Mode_Out_PP = 0x10, // 通用推挽输出
    4 K; N& P* x- F6 m" ]0 `% q
  9. GPIO_Mode_AF_OD = 0x1C, // 复用开漏输出
    ( I9 ]# x4 _" k& o2 ?2 K! r& I
  10. GPIO_Mode_AF_PP = 0x18 // 复用推挽输出6 T' W# r* \& n
  11. } GPIOMode_TypeDef;
复制代码

9 V, t3 x5 U5 ^0 `$ x: ^2 h
* E4 v% r& _& Q7 O1 ]! x' T
    IO口的模式有8种,输入输出各4种,由端口配置寄存器的CNF配置。平时用的最多的就是通用推挽输出,可以输出高低电平,驱动能力大,一般用于接数字器件。
    最终用固件库实现就变成这样:
  1. // 定义一个GPIO_InitTypeDef 类型的结构体; Q/ q8 h) h$ |/ Z5 F2 D& j
  2. GPIO_InitTypeDef GPIO_InitStructure;
    & t- I& n( F6 t, a, j8 M+ ]

  3. ) [- C6 i4 d6 A
  4. // 选择要控制的IO 口
    6 T9 H) k1 s5 l7 k5 X8 S! y
  5. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    9 |& [/ r/ h1 T

  6. $ ^/ e" o: R2 u8 U8 X0 f# _4 Y6 h
  7. // 设置引脚为推挽输出
    0 H, w: B" T0 _
  8. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;6 O% R7 p, g4 j+ T7 J: M* ~/ E

  9. $ Z' C# D8 ?7 U( o& Q
  10. // 设置引脚速率为50MHz- n' p  b3 X& u+ {" {
  11. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
    + A# `  i& X  z% T9 H; r

  12. / y3 d) b/ {$ S# m- l; J: y
  13. /*调用库函数,初始化GPIOB0*/7 e& D6 r) j$ X/ m. i0 I
  14. GPIO_Init(GPIOB, &GPIO_InitStructure);
复制代码

# T: P9 I# u. x; j$ ]% c* c) {
    倘若同一端口下不同引脚有不同的模式配置,每次对每个引脚配置完成后都要调用GPIO初始化函数,代码如下:
- l/ p* o$ Z. {2 h: H
  1. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;  }1 l6 Y6 T8 R1 S. B
  2. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15 ;                     
    7 z- C  {* N' S( {3 g4 S: f) i
  3. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;          //上拉输入: a: a  i! O# o4 m5 x7 B
  4. GPIO_Init(GPIOB, &GPIO_InitStructure);
    9 L$ p" g9 ]' y: d& Y0 u1 _
  5. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 ;                     4 `: F& \; N) Y; H4 E8 R2 _0 }9 Z+ z
  6. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;       //推挽输出
    5 l/ ]6 N6 I: w: M4 N$ L
  7. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;( U7 y+ j. s0 ~8 A4 A# D  I4 v# \. W
  8. GPIO_Init(GPIOB, &GPIO_InitStructure);
复制代码

. k/ [* v; z# S  G& @
6 P8 J$ c/ T1 x4 l5 m
GPIO 输出控制
    GPIO输出控制,可以通过端口数据输出寄存器ODR、端口位设置/清除寄存器BSRR和端口位清除寄存器BRR这三个来控制。端口输出寄存器ODR是一个32位的寄存器,低16位有效,对应着IO0~IO15,只能以字的形式操作,一般使用寄存器操作。
  1. // PB0 输出高电平,点亮LED
    " f% j" J4 \  g3 B+ k3 D
  2. GPIOB->ODR = 1<<0;
复制代码
- ^( A) q3 I$ Y* {
    端口位清除寄存器BRR是一个32位的寄存器,低十六位有效,对应着IO0~IO15,只能以字的形式操作,可以单独对某一个位操作,写1清0。
  1. // PB0 输出低电平,点亮LED3 `% Q5 q# p* i. u% o
  2. GPIO_ResetBits(GPIOB, GPIO_Pin_0);
复制代码
: ^% T: b" h& S1 J5 M& A5 M; g: z
    BSRR是一个32位的寄存器,低16位用于置位,写1有效,高16位用于复位,写1有效,相当于BRR寄存器。高16位我们一般不用,而是操作BRR这个寄存器,所以BSRR这个寄存器一般用来置位操作。
  1. // PB0 输出高电平,熄灭LED1 K- _0 {2 ^2 F! \# f) n+ T
  2. GPIO_SetBits(GPIOB, GPIO_Pin_0);
复制代码
! b( w% S( j" \3 z( O1 p- }. p
    综上:固件库LED GPIO初始化函数。

& a  E2 ?* S3 {& _3 W% H
  1. void LED_GPIO_Config(void), j& y3 ^- w! E* f. j, a$ d
  2. {
    " w) e* M# K( r8 O: _  L
  3. // 定义一个GPIO_InitTypeDef 类型的结构体; n! W: M& ^7 D# i. G$ d  }
  4. GPIO_InitTypeDef GPIO_InitStructure;( j4 K: V7 V  s. v& }2 S5 k
  5. - i2 A- o/ j  ~* F5 }8 ^
  6. // 开启GPIOB 的时钟
    7 j4 S+ x6 H6 G: q* a- V* D- B( a
  7. RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE);
    . L4 ?' p8 q6 Z) W; q: {. l' c

  8. ! v. T" a. k/ z/ w; V: v2 F
  9. // 选择要控制的IO 口  x3 |" t" I6 v# G2 q
  10. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    ; u! o9 ~# R' n' T+ N; R% R
  11. 5 X$ P& I" T7 m7 u; S8 [6 E1 Z! J5 e
  12. // 设置引脚为推挽输出1 c. g& J# b/ A$ \" l4 h8 b' H
  13. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;/ c. y# ?* ~  P* v% Q: u

  14. 9 M) f# L: D6 E% x- H! |
  15. // 设置引脚速率为50MHz
    3 \+ u! i  ?, \; t6 ~2 s$ P( e
  16. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;  `) ?0 ]- E9 R8 A# j+ ]9 y
  17. & ~, I! d* M/ s& S& v4 i7 a
  18. /*调用库函数,初始化GPIOB0*/
    " i" h, J8 q$ r) @. Z, E) B
  19. GPIO_Init(GPIOB, &GPIO_InitStructure);
    : m" o* i1 O- a1 ?! b

  20. # }( K9 g4 C: B: [2 X+ m9 c
  21. // 关闭LED& d/ N. ?2 z) \# x# W7 v
  22. GPIO_SetBits(GPIOB, GPIO_Pin_0);
    * H( h/ \* B. g% h6 Y+ F" P
  23. }
复制代码

6 ^& {% Q! |4 x
主函数" V7 a2 f/ l0 b/ v' q9 K, P' m
  1. #include "stm32f10x.h"! z9 X% G- z) [8 B( a/ J6 w5 C/ J, N
  2. ' K  m; O& [/ F8 Q
  3. ! I' ^( |" n* t  x$ z7 j+ N5 R: C
  4. void SOFT_Delay(__IO uint32_t nCount);" K* N: x* U  }
  5. void LED_GPIO_Config(void);
    5 {8 ~$ z, f/ Q, i- H; L9 w! ?

  6. 6 D5 m3 D4 S1 f1 z: B
  7. int main(void)0 G' |3 T' }# m  k1 I) ?
  8. {. `" T6 G+ T/ o$ N: U
  9. // 程序来到main 函数之前,启动文件:statup_stm32f10x_hd.s 已经调用7 n' h& p6 ?1 z
  10. // SystemInit()函数把系统时钟初始化成72MHZ
    ! a; v* f1 z! Y+ `, E5 A% x
  11. // SystemInit()在system_stm32f10x.c 中定义
    ' X9 a0 q% ]( f
  12. // 如果用户想修改系统时钟,可自行编写程序修改( i. a4 Z( t1 j% \

  13. 2 ^8 x! W1 e0 l/ p, d
  14. LED_GPIO_Config();
    6 E( d. c0 W$ C" n& C

  15. - w1 |( r8 x; ^' C
  16. while ( 1 ) 6 N! ?$ ]2 X- d2 p7 t" {# D- H1 ?
  17. {. b8 I  `* E! X7 v+ M4 @: \# v
  18. // 点亮LED
    ' S) s5 m6 Q& f" S1 s. z+ \
  19.   GPIO_ResetBits(GPIOB, GPIO_Pin_0);
    4 O( Y; }; b, N1 G: E3 h
  20.   Time_Delay(0x0FFFFF);5 F5 Q4 f  Y$ B, ^3 `
  21. % n( D1 G) x, |; v/ l
  22. // 熄灭LED
    2 [9 {) x" [+ q6 o
  23.   GPIO_SetBits(GPIOB, GPIO_Pin_0);
    , H  {+ H5 L* P& G2 r
  24.   Time_Delay(0x0FFFFF);: q2 g4 s6 a  X2 M3 Q' {! `
  25. }
    ( X7 Y7 I0 |. A! ]" H
  26. }
    # k3 X/ Z& ]1 i7 m  @2 L
  27. // 简陋的软件延时函数
    / z2 T; H9 s3 i; R
  28. void Time_Delay(volatile uint32_t Count)
    / K- c  H$ d. W1 I
  29. {" `8 M; @2 i1 g
  30. for (; Count != 0; Count--);0 J0 w0 A$ a8 @
  31. }
复制代码

# l% o! {! l( G, h
) c* \( ]) a3 `; s
    注意void Time_Delay(volatile uint32_t Count)只是一个简陋的软件延时函数,如果小伙伴们有兴趣可以看一看MultiTimer,它是一个软件定时器扩展模块,可无限扩展所需的定时器任务,取代传统的标志位判断方式, 更优雅更便捷地管理程序的时间触发时序。
8 ?7 ]. C( L. l0 Y" w8 L% @1 \
$ u4 T9 }8 j5 I! K7 q9 C7 _
收藏 评论0 发布时间:2022-6-7 21:25

举报

0个回答

所属标签

相似分享

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版