1.第一个工程 翻转引脚 上一篇文章我们详细介绍了 STM32F030 从复位时取得复位向量,系统初始化,然后跳转到 main( ) 函数的过程。下面我们结合一个最简单的例子,对 Cube 库的使用做一个简单的介绍。 我们用 Keil 打开下面这个工程: 1 s( X3 X3 f3 Q, e STM32Cube_FW_F0_V1.11.0\Projects\STM32F030R8-Nucleo\Examples\GPIO\GPIO_IOToggle\MDK-ARM\Project.uvprojx9 z& i6 |( ]' L C ~ 3 M5 Y6 V( y& `- F. K 编译下载运行此代码,会看到一个 LED灯(连至MCU的 PA5引脚)不停地闪烁。为了完成这个简单的功能,我们看到这个工程里包含了不少文件:2 {: a+ }% Y1 @0 P* C1 G4 Y 3 m" W# x9 `! ^1 g# x( j2 B9 y 如果是初次用这种库的方式做开发,乍一看还真感觉有点乱。不过让我们一个一个看一下这些文件,理清它们的关系后就会体会到这种方式的巨大优点。' E7 \- ]/ `, D& I S+ B, d+ b' @; f1 F9 u% I 2.文件分类解释4 n* S" `( z' u1 @ 工程里的文件分为五大类:启动代码,M0内核初始化,驱动,板级支持包(BSP),用户代码。一般来说我们开发应用程序,主要关注用户代码文件就行了。如果硬件电路板做了改动,则修改BSP里的内容。# H1 x. Y k- G s/ J$ h9 W o* ` A 7 v. W f3 K4 L4 q& W4 E, D 在早期的单片机开发中,芯片内资源很少,通常的情况是一个工程师就从硬件到软件编程都做了,是没有 BSP(Board Support Package)这种概念的。BSP概念来源于较复杂的CPU系统的开发,一般是厂家设计主板,并提供 BSP(包含启动代码,驱动,Bootloader等)。我们这里的 BSP 概念稍有不同,它是指对某一块儿以 MCU 为核心的电路板的支持代码包。启动代码,内核初始化和驱动,没有包含在内。BSP会调用驱动层的代码。 对于 STM32 Nucleo 这块儿开发板来说,板上资源很少,所以BSP只提供了相应的按键(BUTTON)和指示灯(LED)支持代码。里面的ADC,SPI,LCD等代码是支持其它板子的,可以先忽略。 ; G$ `+ x7 e% ^- D5 ^. W 启动代码 为理解汇编代码,我们先熟悉一下这些伪指令: 1 X3 k8 @% U- X1 _7 H* l ALIGN 变量或代码对齐。如:! j c# F! _( h6 v0 i0 u ALIGN = 3 以8(2的3次方)字节对齐。, {/ R @3 a& o EQU 给标号赋值。如: , [# a# H) E" Q3 V2 X n: E" r; ] Stack_Size EQU 0x400; 9 ?+ G8 z2 u: s- O9 E. A DCD 分配1个或多个字(words)的内存空间。如:& h5 w( X4 S' S* h& H$ E 5 \9 n+ f& s$ ] Data DCD 1,5,8; 定义3个字并赋值为 1,5 和 8。 % V* S" Q. O. P8 ]1 w AREA 定义一个代码或数据段(section),命名并指定属性。如:& k) H( G7 y( | 5 e) r% e% F$ p/ M AREA Func01, CODE, READONLY;1 C4 r9 Z1 W: r9 R5 r 定义了一个名字为 Func01 的只读代码段。 2 y9 e: Z. P0 b+ @) F9 d4 S SPACE 保留一段空间并初始化为 0。如: Data SPACE 100; 为 Data 保留 100个字节初始化为 0 的内存空间。 : @/ s( N# {9 L8 M- u IMPORT 导入其它文件中的标号,以在当前文件中引用。如: x( @" v2 _6 \ IMPORT SystemInit LDR R0, =SystemInit; Q4 _4 D. w6 j9 x, B BLX R0 ; ?2 _- D& P, ]3 b: P8 Y0 S 从文件 system_stm32f0xx.c 中导入 SystemInit 这个函数并调用。. s, z5 Y2 G( q0 R; E ! W5 ~ p/ {: |$ l+ g EXPORT 导出能被连接器(Linker)识别的标号。从ASM文件导出的标号可以在C中引用。6 {( E# w6 ]/ p# M' W; ]) L1 k 5 C8 |# c7 w; H) X8 O [WEAK] 如果在其它地方定义了相同的标号,则此处定义被覆盖。 $ m# Q% h3 k* |/ [8 v3 v( A& S PROC 定义一个函数的起始地址。 , K# ]$ h) q/ T( D5 ^7 x- _6 z ENDP 标志当前函数结束。 例子: SysTick_Handler PROC5 P4 {2 ], c4 e/ s4 T. H; d) z EXPORT SysTick_Handler [WEAK]# }) D! [1 V% c/ U' N R% _* e 2 w- ], E! a5 N" n! B- }+ @5 E, q B.1 q$ @" k, ^& n! |7 B/ b9 } V/ u' | 2 b/ U2 L( i3 Q ENDP ) I* n' R, q& l9 _% h+ M! D6 T 导出 SysTick_Handler 这个中断处理函数。如果在其它地方定义了一个新的 SysTick_Handler 函数,那么新函数将覆盖此处定义的这个陷阱函数。汇编语句 B.为在当前语句死循环。+ X j2 @6 [% \8 a7 b 下面我们看一下启动文件 startup_stm32f030x8.s 定义堆和栈: ) w# V+ Y% f8 j9 J8 M5 y( F2 y3 E 中断向量表:& V) L; O$ \$ c: u& `3 D9 Q0 } ) ?' P3 D4 L% {" D* ]( j5 f" u' B 现在这个工程用到的只有绿线框中的几个向量:# M0 n0 [* L0 F; Q! d3 ^ __initial_sp 初始堆栈指针1 Z; Y6 t* ^+ U) N Reset_Handler 复位向量,我们在上一篇文章已经讲到如何从复位向量一步一步执行到用户代码中的主程序main( )。$ y/ Z3 @2 G0 q" T! d7 g SysTick_Handler 系统时钟中断向量。此程序每 1ms 产生一次中断。2 Z% q& T" s" n 需要注意的是 SysTick_Handler 这个中断处理函数在用户代码文件stm32f0xx_it.c$ g7 i6 ^( Z3 b" }3 S 中进行了重定义,所以当 SysTick 中断发生时,实际会跳转到用户代码的中断处理函数,而不是跳到下图所示的汇编代码中断处理函数进入死循环。1 U# U2 K& G) P, R7 o8 I, E ! r# x7 M) W5 A/ K% y ( {( x3 B: Z+ {, K) M- d3 q9 j9 w1 j 再往下可以看到,对所有芯片级中断定义了一个共享的陷阱函数。用户在实际使用到某一个中断的时候,要在中断处理文件 stm32f0xx_it.c 中用相同的函数名定义,从而在中断发生时跳转到实际的中断处理函数。 6 J u% H7 Q6 s! o# N 在此文件的最下面的代码,是用来传递堆栈信息给库的: : e& \* ~, K. @4 i! j' y' n ) E: [ y4 ~4 ~" o# h4 i$ ?0 w: K 在芯片资源比较少时,可以通过选中 Options for Target->Target->Use MicroLIB 选项,使用简化版的库来实现 printf 等操作。若资源充足时使用标准库,库将调用下面的 __user_initial_stackheap 函数来获得堆栈信息。 7 I. ?' s4 g5 w4 q! l$ `6 o2 `+ M ' |' V( L8 H ?& ?) ]0 ] M0 内核初始化 2 r. T/ v" d( |6 _ system_stm32f0xx.c4 \9 \% ]) B# G# L3 {: o 此文件只有两个函数:3 H5 j0 G5 t/ o* B- }$ n / t4 q- ^ X# a4 Z SystemInit( ),在启动代码中调用,把系统时钟复位到初始默认状态(8MHz的高频内部时钟 HSI)。 9 o8 q }. N9 \' S& s1 r) n" O2 D SystemCoreClockUpdate( ), 在用户调用库函数更改时钟配置后,需要调用此函数以更新全局系统时钟变量 SystemCoreClock。其它模块基于此时钟的计算才会正确。一般来说更改时钟配置的 HAL函数已经包含此函数的调用,如 HAL_RCC_ClockConfig( ), 无需用户再次调用。# {$ S4 |' k; i: N' p 驱动 " Z* i' W- k F3 ~ stm32f0xx_hal_cortex.c ; E7 j# g' [4 o/ y4 S2 |) l 包含 Cortex 内核中两个重要模块的驱动: 可嵌套中断向量控制器 NVIC(Nested Vectored Interrupt Controller),2 g3 T, ]9 o1 } V, M1 D O0 Z' V7 k( j @ 系统滴答时钟 SYSTICK。 3 q! v0 @6 A4 k, j- u9 q1 b stm32f0xx_hal.c 7 B; h2 p0 ^- k5 G) h# j/ } 此文件包含用户程序必须首先调用的 HAL_Init( ),它会使能数据和指令缓存,预取指令队列;配置系统滴答时钟产生 1ms 中断;调用 HAL_MspInit( )回调函数。, _1 {" i7 e/ C1 r+ O / ~1 J& L9 X- P, [1 R' A/ W HAL_MspInit( )函数用来做系统级的初始化,配置某一模块相关的 时钟,引脚,DMA,中断等资源,但是在所有的例程中都没有实际用到此函数。可以先忽略。 stm32f0xx_hal_rcc.c- ~: F6 S6 ^% ~" J4 P( c- ^1 t / |; ^8 {1 L$ D, F; r stm32f0xx_hal_rcc_ex.c ( I' y" z3 B3 F4 ~ RCC(Reset and Clock Controller)模块的驱动。一个模块为什么要两个驱动文件呢?前一个文件提供了基本的通用的功能驱动,后一个文件是扩展功能驱动,通常针对某一特定型号的芯片。如同我们吃饭需要餐具,_rcc.c 提供碗筷等常用必备工具,_rcc_ex 可能提供的就是酒杯,烛台等这些东西。 ( K" r, K+ ]( v8 I* R% }' D! ]9 v stm32f0xx_hal_gpio.c6 w5 a3 v" W% C; O( F& d # ^2 h* w) h* K7 S$ F. t! f GPIO 模块的驱动。 BSP 板级支持包 stm32f0xx_nucleo.c ; ], e @( {* r9 Q/ K. X$ x m6 V 针对 STM32 Nucleo 开发板的类型,宏定义,支持代码。 / j; v3 X& K) h Y9 C5 l1 q 用户代码 main.c 主程序: Q* L* n" h1 M ' o7 ~5 g) q) B# ~ stm32f0xx_it.c 中断处理9 _+ D- e# T1 B4 r; e @% T 前面介绍了一大堆文件,主要是为了清除系统的工作流程。在开发中使用库还是很简单的。在主程序中调用库,只需要通过 main.h 包含下面这个头文件:# X3 W3 e d& [4 f) m7 g stm32f0xx_hal.h 如果有 BSP 则包含 BSP 的头文件,在本工程是: 7 m% C8 S& v$ q& O' k; ^1 X3 C stm32f0xx_nucleo.h* `, O. M! J+ Y/ m z ' x R7 _0 H( ?" Z( }& ? j 使用到哪个模块就在配置文件中打开使能该模块的宏定义。 stm32f0xx_hal_conf.h, w9 f3 f: X P& ], z# U+ H" c% R3 m 然后第一步必须调用 HAL_Init( )。4 t D% L" `; M" O6 @- R$ ? 第二步,如果希望系统时钟工作在默认内部时钟(8MHz HIS)以外的频率,则需要调用 SystemClock_Config( )。此函数又调用 HAL_RCC_ClockConfig( ) 完成新配置。 下面是应用代码: , P6 Y3 S; P5 {) S% z/ i 所有模块一般都是这三个步骤:使能模块的时钟,初始化模块,使用模块的功能。 stm32f0xx_it.c 中的中断处理函数 SysTick_Handler( ) 很简单,每次进入就对滴答计时变量 uwTick 加1,其它 HAL 函数可以基于此变量计时。 3 q9 h. C% y6 G, i |