STM32的三种开发方式! s' l8 m1 U2 y( Z0 U }
通常新手在入门STM32的时候,首先都要先选择一种要用的开发方式,不同的开发方式会导致你编程的架构是完全不一样的。一般大多数都会选用标准库和HAL库,而极少部分人会通过直接配置寄存器进行开发。
0 H+ l0 J: `; u4 ~" `9 ]0 i' b* O3 {3 `
: i s* b# C0 a i! t; h) L! R4 F网上关于标准库、HAL库的描述相信是数不胜数。可是一个对于很多刚入门的朋友还是没法很直观的去真正了解这些不同开发发方式彼此之间的区别,所以笔者想以一种非常直白的方式,用自己的理解去将这些东西表述出来,如果有描述的不对的地方或者是不同意见的也可以大家提出。6 A+ l9 [4 M. F9 I8 N* S
! j/ e3 B& p) {: L
: y* f8 v" O& m+ J4 ]
1、直接配置寄存器) u( Q& v& _9 m
不少先学了51的朋友可能会知道,会有一小部分人或是教程是通过汇编语言直接操作寄存器实现功能的,这种方法到了STM32就变得不太容易行得通了,因为STM32的寄存器数量是51单片机的十数倍,如此多的寄存器根本无法全部记忆,开发时需要经常的翻查芯片的数据手册,此时直接操作寄存器就变得非常的费力了。但还是会有很小一部分人,喜欢去直接操作寄存器,因为这样更接近原理,知其然也知其所以然。7 @6 S# V6 A% Q' Z$ _- Q
2 \2 z+ A5 Z& T
6 ?3 K: R) m9 E+ B0 ?! a9 Q2、标准库& ]3 Y, w2 Z8 \1 T/ e
上面也提到了,STM32有非常多的寄存器,而导致了开发困难,所以为此ST公司就为每款芯片都编写了一份库文件,也就是工程文件里stm32F1xx…之类的。在这些 .c .h文件中,包括一些常用量的宏定义,把一些外设也通过结构体变量封装起来,如GPIO口时钟等。所以我们只需要配置结构体变量成员就可以修改外设的配置寄存器,从而选择不同的功能。也是目前最多人使用的方式,也是学习STM32接触最多的一种开发方式,我也就不多阐述了。: I" p! D6 V2 k8 P( M7 O; v
9 t2 ?' w# j7 v/ J$ h, L, s. `
/ }, P( z1 ]5 h7 H. X- u
3、HAL库2 B7 P$ e4 w; ?$ v5 l; W% N& S
HAL库是ST公司目前主力推的开发方式,全称就是Hardware Abstraction Layer(抽象印象层)。库如其名,很抽象,一眼看上去不太容易知道他的作用是什么。9 V) u4 f$ [/ y& ^
$ Y8 ^" }- y/ q0 E! T) P( t% R% X5 J8 K2 O5 C5 z- E
它的出现比标准库要晚,但其实和标准库一样,都是为了节省程序开发的时期,而且HAL库尤其的有效,如果说标准库把实现功能需要配置的寄存器集成了,那么HAL库的一些函数甚至可以做到某些特定功能的集成。也就是说,同样的功能,标准库可能要用几句话,HAL库只需用一句话就够了。
6 Q4 o) l" D* Z$ F9 e! ^6 f1 L7 u: i3 A* d( O9 R$ Z8 K
! I6 J5 i$ ~; d/ d: i; W0 }
并且HAL库也很好的解决了程序移植的问题,不同型号的stm32芯片它的标准库是不一样的,例如在F4上开发的程序移植到F3上是不能通用的,而使用HAL库,只要使用的是相通的外设,程序基本可以完全复制粘贴,注意是相通外设,意思也就是不能无中生有,例如F7比F3要多几个定时器,不能明明没有这个定时器却非要配置,但其实这种情况不多,绝大多数都可以直接复制粘贴。2 u( H* Z5 W0 D# I- B& l
e: ^8 W: _; T
+ s5 w( z o" a/ s( r是而且使用ST公司研发的STMcube软件,可以通过图形化的配置功能,直接生成整个使用HAL库的工程文件,可以说是方便至极,但是方便的同时也造成了它执行效率的低下,在各种论坛帖子真的是被吐槽的数不胜数。8 B& \! C8 P j
2 m' T7 u, c1 E# w$ _
0 W" t5 P+ f' r$ r( _( IHAL库固件库安装与 用户手册0 E$ z- A' K: C- d4 O/ S J& V7 ~
1、首先设置让Cube可以自动联网下载相关固件库选择updater Settings+ `$ [1 ]- J2 G% E1 u! v
设置如下:
8 m( ]) d. G# y1 S5 e0 e" ^
4 n% ~! P+ {. M0 P1 f2 b0 v" [/ e2、根据芯片选择所需固件
8 k/ c8 ]5 T, Z7 g' E版本是向下兼容的,可以直接选择最新版。但如果觉得最新版太大,可以阅读下面的Main Changes.能够支持你目前的芯片就好。5 F' T- S6 Z$ i$ d) G6 i0 }
选好了,点击Install Now就行,过程可能有点长。建议直接官网下载到本地,再安装文件会被下载到如下位置,建议更改此目录,不要选在C盘!!!# B1 m3 \2 H. N# s5 c; j" r
3、寻找用户帮助手册
! j9 ?2 }* U. Q: z进入固件所在文件夹,里面包含很多内容。1 K9 X4 G' p, c4 j# V8 k
比如说 官方提供的开发板程序,每个型号下面都有对应功能的实现,用户手册就在Drivers文件夹下面。
$ w2 Q- f5 Q+ v5 J) c0 M- M( p JSTM32 HAL库与标准库的区别
% [5 ?/ a& _( V* k/ d6 k1、句柄
, a/ A. O5 L1 V# F/ V句柄(handle),有多种意义,其中第一种是指程序设计,第二种是指Windows编程。现在大部分都是指程序设计/程序开发这类。 h0 u# b6 e1 H, E- W+ i, b" H
. c8 e; \1 m/ H! l' X. g
: [3 J, n7 w2 w3 h3 q* z- f第一种解释:句柄是一种特殊的智能指针 。当一个应用程序要引用其他系统(如数据库、操作系统)所管理的内存块或对象时,就要使用句柄。6 c7 O! a+ b, c. ~9 f
J* e% ^& U% }# r0 @
0 L9 R0 ?% Q0 ?" W4 I) ]
第二种解释:整个Windows编程的基础。一个句柄是指使用的一个唯一的整数值,即一个4字节(64位程序中为8字节)长的数值,来标识应用程序中的不同对象和同类中的不同的实例,诸如,一个窗口,按钮,图标,滚动条,输出设备,控件或者文件等。应用程序能够通过句柄访问相应的对象的信息,但是句柄不是指针,程序不能利用句柄来直接阅读文件中的信息。如果句柄不在I/O文件中,它是毫无用处的。句柄是Windows用来标志应用程序中建立的或是使用的唯一整数,Windows大量使用了句柄来标识对象。
% w2 p$ Y$ W% m% }7 l$ C6 |: [0 n" \! O0 {* K+ _' ?2 F
) ], U* |) K$ P: E$ y% U2 x) MSTM32的标准库中,句柄是一种特殊的指针,通常指向结构体!
) t% s J1 \- m! L$ c7 \( @8 U9 ~# W3 k
7 d, p* j+ m5 s* P- G% ]
在STM32的标准库中,假设我们要初始化一个外设(这里以USART为例),我们首先要初始化他们的各个寄存器。在标准库中,这些操作都是利用固件库结构体变量+固件库Init函数实现的:
/ [1 A- N! }' Z9 Y- USART_InitTypeDef USART_InitStructure;
3 T3 Q) ?! |) K3 T: k" p4 H
' d) h! {! q/ z7 l6 e G- USART_InitStructure.USART_BaudRate = bound;//串口波特率$ [ O. B$ v! }
- USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式8 j' |3 H$ ^, k$ a
- USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
- @6 K7 L( X& _$ ~: X - USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
6 G7 y8 |+ u& i) ?' f3 u& \6 K - USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制* d8 t8 u8 B' s! j+ G4 e( f: r
- USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
f1 F% o# i0 [ _
6 e2 K0 s. x8 _) R2 i' P; W- USART_Init(USART3, &USART_InitStructure); //初始化串口1
* g2 e" G. p# Y; f9 Y4 A/ w7 z3 D
复制代码 可以看到,要初始化一个串口,需要:
' a7 ]( O9 G6 ^% [0 l- [, M9 V, ?0 A2 y9 t3 b
$ @5 h% X3 ]% L Z" ?, W9 I
1、对六个位置进行赋值
" l1 F# g- N/ S- g; y5 t( L9 E: v+ G2、然后引用Init函数! D8 [" T3 G0 X/ O4 g
USART_InitStructure并不是一个全局结构体变量,而是只在函数内部的局部变量,初始化完成之后,USART_InitStructure就失去了作用。而在HAL库中,同样是USART初始化结构体变量,我们要定义为全局变量。3 x; s( u& p5 |8 S: x
- UART_HandleTypeDef UART1_Handler;
复制代码 右键查看结构体成员
" _8 n7 [2 _7 o3 L) h4 \- typedef struct
5 o+ z* y0 h5 [& P) l - {" B& U' k- I. S9 s/ z
- USART_TypeDef *Instance; /*!< UART registers base address */8 f8 v: W* C& D& R7 Z+ z
- UART_InitTypeDef Init; /*!< UART communication parameters */
+ E. Y" ] |8 H - uint8_t *pTxBuffPtr; /*!< Pointer to UART Tx transfer Buffer */7 c7 p( \9 k6 X" V% J
- uint16_t TxXferSize; /*!< UART Tx Transfer size */
, s, b) J# o5 I8 r2 M" z - uint16_t TxXferCount; /*!< UART Tx Transfer Counter */& N% t' W9 f S& [7 c) C/ n& j/ c
- uint8_t *pRxBuffPtr; /*!< Pointer to UART Rx transfer Buffer */6 g$ m6 G' r$ `. [5 r+ ^
- uint16_t RxXferSize; /*!< UART Rx Transfer size */
% D2 b. U" W/ I5 G' o! ^4 ~ - uint16_t RxXferCount; /*!< UART Rx Transfer Counter */
2 R3 P) H, i0 _7 {7 n$ n - DMA_HandleTypeDef *hdmatx; /*!< UART Tx DMA Handle parameters */
9 h/ G; o8 i k9 l8 M - DMA_HandleTypeDef *hdmarx; /*!< UART Rx DMA Handle parameters */
! I2 B/ j0 X0 R: \ - HAL_LockTypeDef Lock; /*!< Locking object */
6 R, _" Q& z a* M - __IO HAL_UART_StateTypeDef State; /*!< UART communication state */
0 l+ a) z3 G8 O( X* l1 n - __IO uint32_t ErrorCode; /*!< UART Error code */1 b- X: a$ q% |3 ^$ P* k7 u
- }UART_HandleTypeDef;$ G4 z2 }# h' A- G
复制代码 我们发现,与标准库不同的是,该成员不仅:
4 v7 I4 _/ ]. V. y& i) y- s( [ w, `9 ^8 M2 G! y/ i1 g9 E
$ S7 C0 P" l# z! r- e8 U% I7 F9 `1、包含了之前标准库就有的六个成员(波特率,数据格式等),1 f6 s; f. K) C: ~
; k& k9 T8 o' v3 i4 |/ c! b4 Z
8 @6 Q8 M, y. o2、还包含过采样、(发送或接收的)数据缓存、数据指针、串口 DMA 相关的变量、各种标志位等等要在整个项目流程中都要设置的各个成员。
" o* f$ M4 J& B, b; F$ q6 u- V: N1 B3 W% k- S
5 d- G3 s6 K* |+ ^
该 UART1_Handler就被称为串口的句柄,它被贯穿整个USART收发的流程,比如开启中断:
+ f; H) S/ v9 M. o3 Y. I2 w: @- HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, RXBUFFERSIZE);
复制代码 比如后面要讲到的MSP与Callback回调函数:8 G$ v- M8 ^1 }! t [$ S- g Y
- void HAL_UART_MspInit(UART_HandleTypeDef *huart);
9 c: n* U- ^* {+ d( ~ - void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
复制代码 在这些函数中,只需要调用初始化时定义的句柄UART1_Handler就好。, ?. P) j1 j& _* d' g, P' U
! l9 w- Z/ W6 Y8 p1 t0 m7 k( N9 A8 x% f [" |
2、MSP函数
+ P. }( k; f+ X, n% MMSP: MCU Specific Package 单片机的具体方案
% W) a+ E: {; c' j6 Q
. g& y9 c5 c; A. q
, J4 E' {( T- c/ L" V- j4 pMSP是指和MCU相关的初始化,引用一下正点原子的解释,个人觉得说的很明白:' R- Q2 {; N/ ^8 [
* y. c( y( t$ O5 b+ x& a5 X0 N c
# @, l7 G& p# n, j: u我们要初始化一个串口,首先要设置和 MCU 无关的东西,例如波特率,奇偶校验,停止位等,这些参数设置和 MCU 没有任何关系,可以使用 STM32F1,也可以是 STM32F2/F3/F4/F7上的串口。而一个串口设备它需要一个 MCU 来承载,例如用 STM32F4 来做承载,PA9 做为发送,PA10 做为接收,MSP 就是要初始化 STM32F4 的 PA9,PA10,配置这两个引脚。所以 HAL驱动方式的初始化流程就是:7 E( V' A4 O8 B" A8 O1 J% k
0 e2 K! Q. e. ~! s
: p/ f, l, x' u4 g
HAL_USART_Init()—>HAL_USART_MspInit() ,先初始化与 MCU无关的串口协议,再初始化与 MCU 相关的串口引脚。% h9 ?6 `! O1 B' g' _
- }4 {9 A) S# y8 f p- f$ y* q9 ?) E. C/ }- Q, o: X* H7 u" j3 I
在 STM32 的 HAL 驱动中HAL_PPP_MspInit()作为回调,被 HAL_PPP_Init()函数所调用。当我们需要移植程序到 STM32F1平台的时候,我们只需要修改 HAL_PPP_MspInit 函数内容而不需要修改 HAL_PPP_Init 入口参数内容。4 v& T) ?6 o: Q- `
- G1 ?6 I3 D$ y5 S8 I+ w, U& R9 d6 e! _- L! M
在HAL库中,几乎每初始化一个外设就需要设置该外设与单片机之间的联系,比如IO口,是否复用等等,可见,HAL库相对于标准库多了MSP函数之后,移植性非常强,但与此同时却增加了代码量和代码的嵌套层级。可以说各有利弊。
8 g! O5 H) o j3 Q* l) S, ]+ E0 X% Y3 X( n/ r- y. ~4 ~
; Y* M4 r w, C# d# ]* w
同样,MSP函数又可以配合句柄,达到非常强的移植性:0 t) s! I6 K3 n/ \0 z2 [
- void HAL_UART_MspInit(UART_HandleTypeDef *huart);
复制代码 3、Callback函数% v/ t8 f& u; h
类似于MSP函数,个人认为Callback函数主要帮助用户应用层的代码编写。
+ d1 I, f) g7 C. r ~ X/ D3 S
, y3 N+ K* m/ g% L0 S C
还是以USART为例,在标准库中,串口中断了以后,我们要先在中断中判断是否是接收中断,然后读出数据,顺便清除中断标志位,然后再是对数据的处理,这样如果我们在一个中断函数中写这么多代码,就会显得很混乱:* J8 w4 O( c# Z! Y
- void USART3_IRQHandler(void) //串口1中断服务程序
* s T" C% {$ G* @. U# H! V - { n) c: j' Z, {! m9 X! p
- u8 Res;
# m* ]; e Z" S* G' {/ C - if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾)
. h, b4 o- R9 j2 ~" X4 H) p7 T. S2 e - {& A5 c, n' [ X: A6 z, X& _7 X" ^! V
- Res =USART_ReceiveData(USART3); //读取接收到的数据8 u. k! `; p5 C. y& X6 L
- /*数据处理区*/' c+ P/ X2 K/ X( F" `) g; V9 D, Q
- } , \% J- r' z7 X, Z3 B [- O
- } ; |8 U2 C3 ^) C: d0 N
- } 7 B2 m4 j$ n3 I5 [' n
复制代码 而在HAL库中,进入串口中断后,直接由HAL库中断函数进行托管:$ Y+ E1 U3 D% ]3 j
- void USART1_IRQHandler(void)
& F+ n/ g& ?) Y- g- ?" r$ U - ) J1 G$ p- g3 F- _
- {
8 C. f2 j4 f8 {
' b5 ^9 o. z: ]1 x- HAL_UART_IRQHandler(&UART1_Handler); //调用HAL库中断处理公用函数
% ]. b1 y. o: j' J% n- h - " w; L2 `! T+ u, ~3 r& y% a
- /***************省略无关代码****************/ 4 Q- B$ x+ j8 o2 h/ `) k
- 4 L0 S! k( r9 d1 I% \; [9 i* U
- }
复制代码 HAL_UART_IRQHandler这个函数完成了判断是哪个中断(接收?发送?或者其他?),然后读出数据,保存至缓存区,顺便清除中断标志位等等操作。
2 r6 @- _9 f; T6 C
/ _, p# n. p( K/ G: w$ q Q5 a& Q$ }/ H
比如我提前设置了,串口每接收五个字节,我就要对这五个字节进行处理。在一开始我定义了一个串口接收缓存区:
6 N2 Y, L: ^( `: Y( I" a
& C% k, |* b- ^
5 C4 O4 m! e4 v0 [9 O/ ]! k/ n/ w/*HAL库使用的串口接收缓冲,处理逻辑由HAL库控制,接收完这个数组就会调用HAL_UART_RxCpltCallback进行处理这个数组*/1 J5 e& L/ A. t4 [- J/ X0 T
/*RXBUFFERSIZE=5*// \' s' v6 Q( y: q3 r! a ]- ~
u8 aRxBuffer[RXBUFFERSIZE];
4 H" o5 @& g. V3 X4 m4 J0 d: q在初始化中,我在句柄里设置好了缓存区的地址,缓存大小(五个字节)# C" {# _4 T) G1 i- V
7 z* n) K9 Y9 N6 r( ~- v. F$ O
, F8 i5 Z( o6 o2 s/*该代码在HAL_UART_Receive_IT函数中,初始化时会引用*/( ?1 @, V( o" L$ I* H" \
- huart->pRxBuffPtr = pData;//aRxBuffer% u: k2 P% |. _7 K1 ^' @' ^( D
1 Y- R5 p: j) |9 X; L4 H, K4 R- huart->RxXferSize = Size;//RXBUFFERSIZE
, B! j$ [, L* c7 |: F* U' Q
& W3 Y0 w+ L% { g5 P B# a- huart->RxXferCount = Size;//RXBUFFERSIZE
复制代码 0 w% Y' g4 N: {, W }
则在接收数据中,每接收完五个字节,HAL_UART_IRQHandler才会执行一次Callback函数:2 O. F1 g8 P0 b' Y& W( Y
; b7 M8 j" X* D
~& V2 u- o/ D3 U3 H, _
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);9 }* x ~- w. }1 v8 ]% h( z- T
在这个Callback回调函数中,我们只需要对这接收到的五个字节(保存在aRxBuffer[]中)进行处理就好了,完全不用再去手动清除标志位等操作。1 v a7 {* U8 S+ N- S3 i2 t
2 k _1 ]3 U4 Q$ l! h% H6 \$ g* I) Z
所以说Callback函数是一个应用层代码的函数,我们在一开始只设置句柄里面的各个参数,然后就等着HAL库把自己安排好的代码送到手中就可以了~) h7 _8 N6 b k2 Z3 {
! C- h- h& H! c5 |; ?% j& c3 O
; x. t4 N% I- |9 y- D! z综上,就是HAL库的三个与标准库不同的地方之个人见解。个人觉得从这三个小点就可以看出HAL库的可移植性之强大,并且用户可以完全不去理会底层各个寄存器的操作,代码也更有逻辑性。但与此带来的是复杂的代码量,极慢的编译速度,略微低下的效率。看怎么取舍了。! s/ L* T* D- B. G
, I( g2 e2 X" l# C" `
9 B6 U" r9 ]5 x
STM32 HAL库结构- k" |7 f8 d& {" q; @ q
说到STM32的HAL库,就不得不提STM32CubeMX,其作为一个可视化的配置工具,对于开发者来说,确实大大节省了开发时间。STM32CubeMX就是以HAL库为基础的,且目前仅支持HAL库及LL库!首先看一下,官方给出的HAL库的包含结构:, E, l' g' v; L( B- K( c4 u
1、stm32f4xx.h主要包含STM32同系列芯片的不同具体型号的定义,是否使用HAL库等的定义,接着,其会根据定义的芯片信号包含具体的芯片型号的头文件:
5 {* t& k# y' C' z+ K; B6 d- #if defined(STM32F405xx)
9 u) D8 ^% l1 v/ \0 a' _ - - q1 B, d/ P' n% P' x
- #include "stm32f405xx.h"4 s1 |9 a6 J+ [6 \3 z* ~7 d
: M' m6 E4 S: r5 O# U" e- #elif defined(STM32F415xx)
' \: v3 Y5 F( t- o0 ^3 w
3 F8 M- H: k. F9 o- #include "stm32f415xx.h"
4 ?+ `2 i- ~8 }3 e7 j - ! ]$ Z8 |9 E6 k# u6 ?! m
- #elif defined(STM32F407xx)
* g. X. l+ F5 b" r3 c
0 W; L0 \, R3 I- #include "stm32f407xx.h"1 [6 B5 A1 D& I8 a7 d# M7 y: Y& T
- 9 m. ^7 J- U0 X6 {/ u7 I
- #elif defined(STM32F417xx)8 L8 ]8 b3 f9 D1 A
/ g. I. G& O9 y* p7 c- #include "stm32f417xx.h"
/ `) }( z& X7 @3 f' w1 z
/ Y+ A' ?+ Q9 C, T/ T- #else
. i' Q8 V9 f5 b6 _) A7 K
4 e2 ~3 p$ t9 E8 D" b. L- #error "Please select first the target STM32F4xx device used in your application (in stm32f2xx.h file)"2 q. c& ^$ S& ]" I
* l1 [' V/ P5 J, A3 A' {- #endif
复制代码 紧接着,其会包含stm32f4xx_hal.h。4 w4 |" j- X1 X' N o9 ~
}8 `, y) c: |, k9 Z
. U! T" c9 _3 T: T$ m2、stm32f4xx_hal.h:stm32f4xx_hal.c/h 主要实现HAL库的初始化、系统滴答相关函数、及CPU的调试模式配置( N' E' z1 ]6 K9 c
, {* H- r, z# F) N9 l( h7 r2 Y( `4 t0 m
3、stm32f4xx_hal_conf.h :该文件是一个用户级别的配置文件,用来实现对HAL库的裁剪,其位于用户文件目录,不要放在库目录中。
' r4 ?* y8 s9 H
( U6 `) E" Y. N
+ Q1 b N0 O Z. q接下来对于HAL库的源码文件进行一下说明,HAL库文件名均以stm32f4xx_hal开头,后面加上_外设或者模块名(如:stm32f4xx_hal_adc.c):
8 Y M ]( @+ S' Q3 T ]+ }3 ^7 v. w0 \4 ?2 |
* \! n. r' i' c. f1 V
4、库文件:stm32f4xx_hal_ppp.c/.h // 主要的外设或者模块的驱动源文件,包含了该外设的通用API
7 ?" \2 @* T5 _& V2 q O$ P
M$ ^- W$ N$ C# g3 Y0 V
. h( T2 [& k C! C/ v1 ostm32f4xx_hal_ppp_ex.c/.h // 外围设备或模块驱动程序的扩展文件。这组文件中包含特定型号或者系列的芯片的特殊API。以及如果该特定的芯片内部有不同的实现方式,则该文件中的特殊API将覆盖_ppp中的通用API。
4 u5 _8 w; [6 `
# U% {; A/ y: K7 F
! h9 y2 v( u: fstm32f4xx_hal.c/.h // 此文件用于HAL初始化,并且包含DBGMCU、重映射和基于systick的时间延迟等相关的API
; G/ ^& k' L) Q; [! m G% @0 G, i5 M
% {; G! t0 E/ O, p C
5、其他库文件
- v4 L7 P6 o3 H2 j0 i5 {$ Q6 A2 j9 l' x
4 k- u+ x5 C4 Z# j0 `. z- Z, I用户级别文件:6 y8 l) }7 T' a
6 C, C6 |$ W% I
# W- N. `# I$ Istm32f4xx_hal_msp_template.c // 只有.c没有.h。它包含用户应用程序中使用的外设的MSP初始化和反初始化(主程序和回调函数)。使用者复制到自己目录下使用模板。5 ^$ |7 \8 U* i6 Z, U: ]( ^1 w; K
4 {! ^& ], o% t/ y% `# n2 S
" Y6 k9 y2 n0 P6 ?/ bstm32f4xx_hal_conf_template.h // 用户级别的库配置文件模板。使用者复制到自己目录下使用8 _: {* J" S8 T, U5 J3 s/ {
5 W, ?+ I+ t# {- u$ I1 q
6 F1 ]9 o8 c% M0 ?system_stm32f4xx.c // 此文件主要包含SystemInit()函数,该函数在刚复位及跳到main之前的启动过程中被调用。它不在启动时配置系统时钟(与标准库相反)。时钟的配置在用户文件中使用HAL API来完成。startup_stm32f4xx.s // 芯片启动文件,主要包含堆栈定义,终端向量表等 stm32f4xx_it.c/.h // 中断处理函数的相关实现
# X' c8 }3 b# y( C
7 T, W: p7 p3 T6 k0 q4 w! D W$ o2 D9 Q, ~) `7 K, T
6 main.c/.h //$ _1 ^ B: J3 }8 m, F; e: n4 U
0 f% `- i% X2 X/ g% P1 X {
- \5 J! d( z, h9 b* [: |根据HAL库的命名规则,其API可以分为以下三大类:
( k q1 ?4 A* `) b6 B8 @, ?6 e0 E
$ E* V* e) h( n: _ J, ?& Z
. t& t3 I, }6 ^; e2 V) g初始化/反初始化函数:
0 n I g' n: p! M' `( m$ U HAL_PPP_Init(), HAL_PPP_DeInit()7 h% @0 \- P6 t, ?2 I( s% R
IO 操作函数:- c) s+ i( w& F' A: t
- HAL_PPP_Read(),2 _" j$ q: b5 ^& b
- HAL_PPP_Write(),
0 p: R6 _, o& v: v h2 @- v, C - HAL_PPP_Transmit(), - G9 B# k: s8 ~* U. _/ J" ~- z H' I. ^
- HAL_PPP_Receive()
复制代码 # ~% B7 T5 Q/ a0 {
控制函数:
; k/ z x! e8 R1 ^- j1 R3 Q$ ~- HAL_PPP_Set (),
]5 Y- ` k9 G9 n# b* k - ! ?* M o( b; k* D
- HAL_PPP_Get ().
复制代码 状态和错误:
: Z2 [, f- w- R7 j4 f% d- ** HAL_PPP_GetState (), 9 g/ k8 z X9 [& T
- n2 Y+ i+ `3 _4 i h6 M
- HAL_PPP_GetError ().
复制代码 / r% s# b& [1 }$ j& @9 z
注意:
5 h, N; Q: W9 f, u3 G) t目前LL库是和HAL库捆绑发布的,所以在HAL库源码中,还有一些名为 stm32f2xx_ll_ppp的源码文件,这些文件就是新增的LL库文件。使用CubeMX生产项目时,可以选择LL库。
# R G8 z3 R3 {" n, x/ M" G# E, ^! L* F, ?0 R8 F
6 n& A: C. {4 `' Q6 L* w" [ u
HAL库最大的特点就是对底层进行了抽象。在此结构下,用户代码的处理主要分为三部分:
2 k8 \# z) m( t
2 ~: c8 l- C& U! a6 X
8 D1 m7 Y, j: E2 ]处理外设句柄(实现用户功能)
9 e) w3 \& \5 c( g2 ] L# }处理MSP
( t; i, a4 p8 f1 S, N处理各种回调函数9 Y# u: g+ B2 Z: J" R6 D8 g
相关知识如下:
. C* d. s. i5 M( o& h* v. R" u" c& s( |2 j l
+ \1 }4 ~9 v3 z3 f* p; g
1、外设句柄定义$ H8 z3 t4 p$ `" `
用户代码的第一大部分:对于外设句柄的处理。HAL库在结构上,对每个外设抽象成了一个称为ppp_HandleTypeDef的结构体,其中ppp就是每个外设的名字。*所有的函数都是工作在ppp_HandleTypeDef指针之下。- r" U# M! q% f1 @) r$ {: N: H9 i
A0 J# ~* Q; [! g3 \- E/ a _& P6 B
多实例支持:每个外设/模块实例都有自己的句柄。因此,实例资源是独立的
7 H8 ^' H' N( R8 r' V5 I! A; H6 q% o下面,以ADC为例
! I! k# Y# r5 a5 ^! j0 b' G2 m) n
9 x' w- Q$ \4 r& g9 d0 ? p
外围进程相互通信:该句柄用于管理进程例程之间的共享数据资源。5 ?- x3 P6 K m# C# t2 k4 n; o
- /** 9 ]$ O& ]! N% @, N( S" T+ e, E
- T7 A7 R% @: M$ g V8 m- * @brief ADC handle Structure definition, `2 D5 v+ T6 y0 f( }
- 4 E& B5 V/ N' Q
- */
- P Y; h/ ~% u - typedef struct: T3 H* N4 U H+ _2 _- q, b) i
- {; d) |5 Q d( H4 c; K
- ' s8 t) s$ a# _0 b0 b& X! O
- ADC_TypeDef *Instance; /*!< Register base address */
6 a- p( L* b7 C* u5 ?; W) H% Q7 {/ r1 K
/ U" z7 b4 s$ u9 p- ADC_InitTypeDef Init; /*!< ADC required parameters */% z, o. b( e3 A' Q! H7 g& b8 s# r
- 8 C' n' G- M$ j. g6 f6 B% J6 Q
- __IO uint32_t NbrOfCurrentConversionRank; /*!< ADC number of current conversion rank */
9 g# Y3 J0 ~1 x7 k' S
4 C( J$ n( `4 q8 b8 D. w- DMA_HandleTypeDef *DMA_Handle; /*!< Pointer DMA Handler */3 Z& E8 j6 ] c9 \/ e/ @4 o
+ q1 V& B, T* f9 M7 f. F! K# k- HAL_LockTypeDef Lock; /*!< ADC locking object */ {7 f* o, l E3 g) }2 H
- / B+ X0 f; G( S& w( N' k1 u
- __IO uint32_t State; /*!< ADC communication state */
1 [/ f0 W# r( I2 N - % M" {* d4 ^, ]# f- U
- __IO uint32_t ErrorCode; /*!< ADC Error code */
# M- X2 }# v7 a) _7 H - 3 B3 o- g- A1 G( p
- }ADC_HandleTypeDef;
复制代码 + P' w" K8 J- g5 C7 y0 d
) Q4 l/ k8 \+ Q; O; {7 u
6 l( p5 @$ N0 {9 y% C. L7 L从上面的定义可以看出,ADC_HandleTypeDef中包含了ADC可能出现的所有定义,对于用户想要使用ADC只要定义一个ADC_HandleTypeDef的变量,给每个变量赋好值,对应的外设就抽象完了。接下来就是具体使用了。2 P) y4 C* q5 q& F0 u$ G3 V4 h; g
+ z' @& Z: U$ l# ~: N9 V# D
7 j2 d& I9 |. G- \$ Z/ B: E
当然,对于那些共享型外设或者说系统外设来说,他们不需要进行以上这样的抽象,这些部分与原来的标准外设库函数基本一样。例如以下外设:; Q/ I! @# y0 I
GPIO
+ E1 `7 `( |1 G% `& T& BSYSTICK
8 O) w, o4 j# g4 B+ a" YNVIC7 z. k' R7 h. a& t
RCC
) ?" B, a! {1 B5 d" LFLASH: C& t# A1 d r/ s' g; V' p
( J7 V; I0 w8 h
# c: \+ z7 i& O2 r U2 z/ ^以GPIO为例,对于HAL_GPIO_Init() 函数,其只需要GPIO 地址以及其初始化参数即可。6 ]+ G+ \, H4 Q5 F. j4 i
5 h0 b) Y ?0 {( v% \
0 J- w% Y( E a; O+ K
2、 三种编程方式
* z0 ~% @( d% ]/ SHAL库对所有的函数模型也进行了统一。在HAL库中,支持三种编程模式:轮询模式、中断模式、DMA模式(如果外设支持)。其分别对应如下三种类型的函数(以ADC为例):& X; o# ?5 n' J, x, _4 Z
- HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef* hadc);
6 ^$ ~6 T; X7 b x: J r3 q: A - + G. F: l4 X; `
- HAL_StatusTypeDef HAL_ADC_Stop(ADC_HandleTypeDef* hadc);" U Z, W& K% F* V8 ]
- & f5 ?3 x& s! ^; _
: e. N( S' n% J4 F% N- ' O; r' R. u" e' e( u9 O! m/ ~
- HAL_StatusTypeDef HAL_ADC_Start_IT(ADC_HandleTypeDef* hadc);* d! U+ [$ k+ H+ r! V" f
- : L% w) D7 o- l9 z: M6 i
- HAL_StatusTypeDef HAL_ADC_Stop_IT(ADC_HandleTypeDef* hadc);
1 t) S+ u- Y; Y9 b! T+ N z - " f$ \6 \% @! c6 w% T% x, c
- 0 ?1 \" f4 g- d' j9 m
- 2 u/ z5 k5 v; F! R
- HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc, uint32_t* pData, uint32_t Length);
& s" S3 i7 B6 `3 F - 1 r; f1 x1 q& H; Q
- HAL_StatusTypeDef HAL_ADC_Stop_DMA(ADC_HandleTypeDef* hadc);
复制代码 3 b* `" D2 o9 `8 I0 W
其中,带_IT的表示工作在中断模式下;带_DMA的工作在DMA模式下(注意:DMA模式下也是开中断的);什么都没带的就是轮询模式(没有开启中断的)。至于使用者使用何种方式,就看自己的选择了。' L6 g" H$ ]0 z( B
4 j( ~9 e+ v, K& h
, h7 a3 B9 Z% ^ `6 g8 |此外,新的HAL库架构下统一采用宏的形式对各种中断等进行配置(原来标准外设库一般都是各种函数)。针对每种外设主要由以下宏:
$ l2 \1 J: Q7 N4 r0 b- __HAL_PPP_ENABLE_IT(HANDLE, INTERRUPT): 使能一个指定的外设中断7 F& \1 ?9 d# ]( `0 z( M
- 1 [6 W5 O, |% A& a3 x" R, Q
- __HAL_PPP_DISABLE_IT(HANDLE, INTERRUPT):失能一个指定的外设中断
# z0 }/ E+ X: y - ! }- K$ ?$ \0 W+ E% T" t2 o( H1 a7 b
- __HAL_PPP_GET_IT (HANDLE, __ INTERRUPT __):获得一个指定的外设中断状态* m3 v6 B; Q8 ?% m6 Q6 W, F7 E( ]
% t' v8 h' b7 M$ ~$ o/ r5 Z x- __HAL_PPP_CLEAR_IT (HANDLE, __ INTERRUPT __):清除一个指定的外设的中断状态
6 w$ m C6 r T2 E/ V& e - , O; l6 ^) K5 m% w; ~) }+ k1 H9 J
- __HAL_PPP_GET_FLAG (HANDLE, FLAG):获取一个指定的外设的标志状态
* h8 D/ e s5 ? - 1 m. }3 U0 T0 M. I/ Y# Y% Z8 ]
- __HAL_PPP_CLEAR_FLAG (HANDLE, FLAG):清除一个指定的外设的标志状态. s* ?+ |- J$ }3 W0 `7 f
# X' I; \/ C' O, p2 }( F- __HAL_PPP_ENABLE(HANDLE) :使能外设1 u- s; N4 U Q# I \
- / ?2 Q$ W$ I- u# {4 u0 D
- __HAL_PPP_DISABLE(HANDLE) :失能外设4 D1 p, E3 ~) w
- ' ]) [% N. M6 x6 w/ z; J
- __HAL_PPP_XXXX (HANDLE, PARAM) :指定外设的宏定义
5 `3 S L/ k: e; m8 ^' V. q; u
* w2 [1 V$ R1 W- _HAL_PPP_GET IT_SOURCE (HANDLE, __ INTERRUPT __):检查中断源
复制代码 ) n/ Y% N2 k1 F* B, X( e/ o
3、 三大回调函数
* e2 Y1 j& `0 c' \' _2 s+ o7 i在HAL库的源码中,到处可见一些以__weak开头的函数,而且这些函数,有些已经被实现了,比如:' }+ s5 i8 J& T7 Q1 e5 {. H
- __weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority); O: ]' P! M) W7 [
- # B! ]) [7 T# A1 Z2 M* f: j
- {3 p+ x/ i* v. ~. E( d
- , N2 ]4 l) Q1 b& |, G
- /*Configure the SysTick to have interrupt in 1ms time basis*/
( V8 V6 n$ ]( M( w5 L6 D - 0 h8 Q ?0 {. I8 N& w! A
- HAL_SYSTICK_Config(SystemCoreClock/1000U);
' u6 _# y: p# q$ F: N7 t
% X* v7 K. p. o Q8 A- /*Configure the SysTick IRQ priority */
3 m$ N7 @, Q# d8 D: L/ g - * _. A |% K3 b4 X6 T
- HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority ,0U);$ b: c& e* Y* ^. Q7 A/ z8 |7 t2 p
- 9 ]- A8 o% t1 H7 {6 u
- /* Return function status */; c) g' o0 Q) y
- % ^+ r5 R" P) H* B- _% b8 W4 v2 i9 J
- return HAL_OK;
8 r9 s6 G' Z4 U/ P. X
- D4 y; A% o: n" t( q* T! K5 r* ]- }
复制代码 / E' `+ f+ `5 ~* C# N1 g% N
有些则没有被实现,例如:
0 Z) o _. h! ^7 s- __weak void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
, {. L8 a! g) x2 H
% }4 |+ ~0 E8 V T! c! k- {
0 I0 l" F6 W, j1 `8 z/ R( E2 R
: p8 I- m- a6 d; u* q- /* Prevent unused argument(s) compilation warning */
7 w5 T5 y0 R6 P/ e& a0 X - 7 h2 g( i- D: e! `3 ~* w
- UNUSED(hspi);: D$ v# F3 p% d8 h- m4 L0 i
+ I0 Y0 J I1 z% I0 p4 u- /* NOTE : This function should not be modified, when the callback is needed,the HAL_SPI_TxCpltCallback should be implemented in the user file
3 m7 x: @; L- T+ i! z. ?
( ^. H2 y& X$ ^- */
$ F7 ?8 i( g+ i6 v8 g
8 k& w# C& K# i- }
复制代码 " U$ b, j( @( u4 W# h
所有带有__weak关键字的函数表示,就可以由用户自己来实现。如果出现了同名函数,且不带__weak关键字,那么连接器就会采用外部实现的同名函数。
. v6 n, T R& d# m/ A! M
/ b6 o; z( b4 M- g2 _! P4 ^* p; ?. ?. a# R
通常来说,HAL库负责整个处理和MCU外设的处理逻辑,并将必要部分以回调函数的形式给出到用户,用户只需要在对应的回调函数中做修改即可。HAL库包含如下三种用户级别回调函数(PPP为外设名):
X8 u3 O% l4 u' v) _& F0 G
+ G3 [5 Y3 H- ^$ a
/ H6 @! G6 }4 f8 R6 I# {$ V1、外设系统级初始化/解除初始化回调函数(用户代码的第二大部分:对于MSP的处理):
; C& G0 I& }+ R8 s+ _/ X- HAL_PPP_MspInit()和 HAL_PPP_MspDeInit**
复制代码 例如:+ G( g4 Y7 s( D' V
- __weak void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)。
复制代码 在HAL_PPP_Init() 函数中被调用,用来初始化底层相关的设备(GPIOs, clock, DMA, interrupt)7 n. C U3 b6 G: S- O1 p: H
! ]; G1 E, J9 g
" a* G- y) R2 B; P2 [* C+ ]2、处理完成回调函数:HAL_PPP_ProcessCpltCallback*(Process指具体某种处理,如UART的Tx),9 c& r# t' |, i: s3 ]" r
例如:/ b+ i* A7 I5 M O2 _/ Z) M
- __weak void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)
复制代码 当外设或者DMA工作完成后时,触发中断,该回调函数会在外设中断处理函数或者DMA的中断处理函数中被调用错误处理回调函数:
# w5 ?# U6 j2 G* z" y8 C a. h$ C: H. N- J' t- {
- \ m% r6 Y% P' m0 J+ M
HAL_PPP_ErrorCallback
6 b" b1 r) n1 q4 b$ c: e" f& |
. S# \( K# d: a% F1 D6 `4 w9 |# b h5 c4 `) A
例如:
$ A& m( I/ |( a" _2 N; e5 Z- __weak void HAL_SPI_ErrorCallback(SPI_HandleTypeDef hspi)*
复制代码 3、当外设或者DMA出现错误时,触发终端,该回调函数会在外设中断处理函数或者DMA的中断处理函数中被调用
4 p" M# A4 z/ S1 ^; x/ A) C错误处理回调函数:# ~7 Q) C9 F! { Z
. B) [. ?; \- M0 G
! V. z# s5 X3 C3 N9 U) eHAL_PPP_ErrorCallback6 Q7 q: N& m+ A) x. r, e) W( Y
7 C- T/ T" b ]: }' t% y- e4 {
1 [% o' w1 S+ ]2 E$ f. e
例如:
# e/ d9 g8 w7 ?3 l( F5 p- __weak void HAL_SPI_ErrorCallback(SPI_HandleTypeDef hspi)*
复制代码 当外设或者DMA出现错误时,触发终端,该回调函数会在外设中断处理函数或者DMA的中断处理函数中被调用。
* Q7 U, o: t6 r5 S. w& ]3 c; `* o: G0 ]- I
) F9 N3 Y0 I7 S/ }' k) K绝大多数用户代码均在以上三大回调函数中实现。
( G1 `( q F" g- O0 Z5 }9 Z
& ?4 Z7 Y1 e" ~! b$ k
1 X- y& G* Z+ N0 o* t9 l' j0 a8 @HAL库结构中,在每次初始化前(尤其是在多次调用初始化前),先调用对应的反初始化(DeInit)函数是非常有必要的。
* z4 k1 p% j/ L( x
u7 n, c( |# }: P- h6 @" T- u+ l9 s0 C; w9 o
某些外设多次初始化时不调用返回会导致初始化失败。完成回调函数有多中,例如串口的完成回调函数有7 C7 j* J9 ^- H9 ?3 J' N
7 O6 J0 o4 y9 g* f1 f7 P
8 V2 E8 u$ z* ?HAL_UART_TxCpltCallback
1 o! r, [% f: L( rHAL_UART_TxHalfCpltCallback
2 |6 Z6 E' `- A(用户代码的第三大部分:对于上面第二点和第三点的各种回调函数的处理)在实际使用中,发现HAL仍有不少问题,例如在使用USB时,其库配置存在问题。) r% i) J$ H0 k! C
|0 ?& u2 a& _0 X. g( r
! b: q% |4 n" d2 lHAL库移植使用- S8 _9 F# m4 E- u: ?
基本步骤:: x5 y, c8 I$ O' l" c) z5 Q
5 w6 G+ [6 f+ ~* U& ]
% r* W/ Q* c$ N
1、复制stm32f2xx_hal_msp_template.c,参照该模板,依次实现用到的外设的HAL_PPP_MspInit()和 HAL_PPP_MspDeInit。
7 q. e% t$ a) r$ m7 A/ O( p
, h$ x: l! H3 m/ N2 R8 N$ C; n+ v$ ?/ O) D, X2 Q+ q' j/ f
2、复制stm32f2xx_hal_conf_template.h,用户可以在此文件中自由裁剪,配置HAL库。
! L; O# d" r6 c2 B- X: D, _. ]; I U( H L5 ^
- B0 E! G4 Q0 m3、在使用HAL库时,必须先调用函数:HAL_StatusTypeDef HAL_Init(void)(该函数在stm32f2xx_hal.c中定义,也就意味着第一点中,必须首先实现HAL_MspInit(void)和HAL_MspDeInit(void))* `1 t+ B! Y/ n k1 f0 `, B
! y+ P# {% u; v6 n
( h4 N& o: e( j; e5 D3 p
4、HAL库与STD库不同,HAL库使用RCC中的函数来配置系统时钟,用户需要单独写时钟配置函数(STD库默认在system_stm32f2xx.c中)
1 N7 y% j& D, O& L4 r/ B# k) O# P
- ?3 m( F; X @2 S) q2 `
5、关于中断,HAL提供了中断处理函数,只需要调用HAL提供的中断处理函数。用户自己的代码,不建议先写到中断中,而应该写到HAL提供的回调函数中。
, g7 C9 v% I# `, z: M# B
$ W' y y5 c2 R# ]; R
3 P' k- Q4 r% w1 O6、对于每一个外设,HAL都提供了回调函数,回调函数用来实现用户自己的代码。整个调用结构由HAL库自己完成。
2 a. n. Z7 |4 R# B2 V* f# }9 D, x
/ i& F4 {' C W% e2 N1 r) ]
例如:- K V! y, |& G; f3 m: p+ f
Uart中,HAL提供了
8 M8 z! L+ H2 B% w M [- void HAL_UART_IRQHandler(UART_HandleTypeDef *huart);
复制代码 ! L8 J6 O" C4 h k, X' y A
: s0 n5 Z' r- c0 L& j3 I1 I; V7 v# D
函数,用户只需要触发中断后,用户只需要调用该函数即可,同时,自己的代码写在对应的回调函数中即可!如下:
b& y% Z L ~( D2 k4 U/ O- l- <p><font face="微软雅黑" size="4">void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);</font></p><p><font face="微软雅黑" size="4">void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);</font></p><p><font face="微软雅黑" size="4">void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);</font></p><p><font face="微软雅黑" size="4">void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);</font></p><p><font face="微软雅黑" size="4">void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart);</font></p>
复制代码
0 k! H! {4 t0 W; O) U' M" I: Z使用了哪种就用哪个回调函数即可!2 W9 m8 j9 c2 K7 X
* s8 R# u7 J+ U* C5 @$ o1 G) p
& G. ~9 g, P! w6 v, J基本结构3 M- E7 p; t7 E* ?+ l1 c V/ h
# j+ U6 B8 P; s4 G: S4 x6 o d% ^ u; z5 u
综上所述,使用HAL库编写程序(针对某个外设)的基本结构(以串口为例)如下:4 u! T2 e4 C4 R1 C+ W2 ? p1 O
_0 O9 i1 E4 [( n2 ^& p
( l5 A, z2 O1 A, f' F/ h+ [1、 配置外设句柄 例如,建立UartConfig.c,在其中定义串口句柄 UART_HandleTypeDef huart;接着使用初始化句柄(HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef huart))7 N7 t7 r. d( `( N# O
& C. q8 I& X# e, H! F; o. X2 f
* ]' K6 i9 N% Q0 n( I6 ^
2、编写Msp 例如,建立UartMsp.c,在其中实现void HAL_UART_MspInit(UART_HandleTypeDef huart) 和 void HAL_UART_MspDeInit(UART_HandleTypeDef* huart)
/ q: G$ y$ g- j+ ^7 G* a6 s1 f+ x+ i# A" Q- o z1 y
9 Q. H& `( z0 v. F: n5 c, Y
0 J2 b: d" I$ F) H5 j* n, y3、实现对应的回调函数 例如,建立UartCallBack.c,在其中实现上文所说明的三大回调函数中的完成回调函数和错误回调函数5 G; r x4 X- T" `& H7 ^" C
1 ?5 M3 l+ ~% Z3 \7 H8 j. i9 t A% T
e4 q5 E5 {) F9 w3 p; p |