STM32CubeMX + HAL* t5 I, o# y" r. w+ q
一些说明
/ q' |: i; k8 `5 i3 ^底层配置
2 {* E+ Z+ t7 a" u3 W' j( L' @使用STM32CubeMX代码生成工具,不用关注底层配置的细节,真舒服。7 O" y3 \2 ^+ y" {, w k( e
使用教程:8 S4 q! `1 _+ A& K1 T _: I
https://sxf1024.lanzoui.com/b09rf2dwj 密码:bgvi
' k) e! R+ ]" z& Z2 k虽然Cube+HAL很舒服,但新手不建议用。最好还是先去学一下标准库怎么用,有个大致概念后,再来学这一套。
6 E& T6 F, I. c( TCube基本使用2 Z; X, a) s& K7 e& Y
新建工程# {5 s% s2 H- j4 i! [% q1 d3 D: B
选择芯片, E2 ~( b8 I- l. N$ O
Pinout&Configuration,选择RCC(HSE:Crystal/Ceramic Resonator)、SYS(Debug:Serial Wiire)
! @5 t* j4 C( I( F2 c/ T1 p7 y: L3 X ^Clock Configuration,配置时钟树
) L* ^4 |+ e `1 O$ a
/ c' a9 y0 s# V4 _$ V
Project Manager,配置工程输出项9 L1 Q/ f/ r4 |6 a
" W( Y1 F- b H- XPinout&Configuration,选择功能(若是选GPIO相关,可以直接在Pinout view选择;若是其他功能,可以在左边Categories打开,会自动配置引脚)、设置Parameter Settings/NVIC等7 M0 P2 r M' c9 ?9 A& l8 L
, J' B/ h( j) ]" pGENERATE CODE,生成工程,用KEIL打开编辑
6 t1 l/ P" d0 \, A' THAL库函数# O! Y6 Y9 B3 _; j5 Q( T% o
函数形式:均以HAL_开头
: u& B4 F& G9 h. W& u寻找过程:在驱动文件stm32f4xx_hal_XXX.c或其.h文件中找函数定义,一般在靠后位置4 m# O. K! O+ Q6 S6 M. W0 D
其他说明:
) A4 r4 i, z tHAL库并没有把所有的操作都封装成凼数。$ ^( V3 b+ b ]+ G- _1 n; d
对于底层的寄存器操作(如读取捕获/比较寄存器),还有修改外设的某个配置参数(如改变输入捕获的极性),HAL库会使用宏定义来实现。而且会用__HAL_作为这类宏定义的前缀。; D, y0 m; `8 k! v. @6 q- T& q
获取某个参数,宏定义中一般会有_GET;而设置某个参数的,宏定义中就会有_SET。& s6 P2 {$ g j: F+ q0 ]# B- ]
在开发过程中,如果遇到寄存器级别或者更小范围的操作时,可以到该外设的头文件中查找,一般都能找到相应的宏定义。
) M4 ~) `9 T+ e0 ]8 eHAL库函数第一个参数一般都是句柄(一个包含了当前对象绝大部分状态的结构体),虽然增加了开销,但是用起来便捷了非常多。7 d! S5 N' ?" B
中断回调函数
5 r# c, F- v4 _函数形式:HAL_XXX_XXXCallback()。0 r+ l9 V' X6 p8 C
寻找过程:中断文件stm32f4xx_it.c - > 中断函数XXX_IRQHandler(void) -> HAL库中断函数HAL_XXX_IRQHandler(GPIO_PIN_13) -> 回调函数HAL_XXX_XXXCallback(): n/ Z% }, E" x& A! b' V
外设对应时钟
9 i H9 `# ]/ }; @ K随便进入一个外设初始化函数,如MX_GPIO_Init() a" S! {0 I# N' o! r! C) y% z, }( w6 s
随便进入一个时钟使能函数,如__HAL_RCC_GPIOC_CLK_ENABLE()
7 U. L3 |( S8 M4 j$ t: U1 v随便进入一个RCC宏定义,如RCC_AHB1ENR_GPIOCEN
" i3 ?% n; ~# w3 K- J; Z或者直接进入stm32f429xx.h文件1 s/ C) |0 _8 G4 g
里面有所有外设与时钟对应关系,如RCC_AHB1ENR_DMA1EN
. F7 F. A0 J! X9 M5 ^& c
7 F1 d7 x1 O! Q: j1 _9 e }4 G# v+ G3 G/ T1 y: ?9 B
( u: _1 S X" y; n
m' u; A% ]2 }* f" g
配置示例6 y% e1 D& ~1 l! H7 Z o$ w" l
小编有话说& F( x& k4 f6 t3 s' B
例子源码:
. @, T: ~8 y0 i! M- B( p! L, mhttps://sxf1024.lanzoui.com/b09rf535a 密码:bf5q
3 n- M( u" e2 }# E/ P: }6 F9 X如果配置过程中,参数不知道怎么设置,可以去标准库例程(如野火、正点原子)中看对应的参数是什么Cube软件只是帮你配置了底层,一些初始化代码还是需要自己手动加的,如SDRAM充电初始化、读写函数等5 @* U; s' u3 g9 X; z2 d
RTC
! }% W; ]: o+ G, |
$ j# o& G4 n- S* l& m8 k5 Z
6 L+ P/ ` f/ @0 }7 \& f- x
- P1 S) w2 } V( o& }) O' a
' O' J/ y5 S9 {, y6 Q; f% s2 E
) ?# F" {: g D. A. E9 j+ x
- RTC_DateTypeDef sDate;
% k" P3 s* d, E7 U - RTC_TimeTypeDef sTime;8 `2 w$ N& w) E, T3 ?5 K. W
- uint8_t second_tmp = 0;+ r2 c$ W* P6 h' X
- ) I* h% L- M _- }6 {
- 3 F3 `0 N9 z# T+ K) ?2 y3 z; q
- HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN); // 读取时间0 e/ z9 G& q4 {; s/ g$ l
- HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN); // 读取日期/ h8 X& J) e5 z
- if(second_tmp != sTime.Seconds) { // 读取秒
9 `" I2 T g% }; c& s6 G* S - second_tmp = sTime.Seconds;
) e& B$ f; Y5 z: d2 _ - printf("20%d%d-%d%d-%d%d\r\n", 8 u0 M2 g- | }
- sDate.Year/10%10, sDate.Year%10, N8 O( v& B/ h6 j, E! y
- sDate.Month/10%10, sDate.Month%10, 7 q' q8 I3 w/ t, s
- sDate.Date/10%10, sDate.Date%10);) a2 v' E# r6 J0 ~* Q$ i; `
- printf("%d%d:%d%d:%d%d\r\n", + V9 \9 K% L1 Y, J+ C
- sTime.Hours/10%10, sTime.Hours%10, ) _' Y+ G& U& b3 g
- sTime.Minutes/10%10, sTime.Minutes%10,
6 E) I, M4 }9 c# Y/ `* f2 S - sTime.Seconds/10%10, sTime.Seconds%10);" k3 }2 B. s& ^* ^( u6 A, W
- }
复制代码 O. r. t- m; i& u5 n) E
5 F/ m; }, z! iSDIO + FATFS
* C4 ~4 f/ G' Q- K9 N/ `选择SDIO功能,Pinout&Clock Configuration,Connectivity -> SDIO -> Mode: SD 4bit Wide bus -> 勾选NVIC9 w/ i2 f, o8 o6 N9 m
) Z/ d0 e- D) Y配置SDIO时钟,Clock Configuration,SDIO模块输入要求为48MHz,系统提示可以自动设置时钟问题,选择Yes。SDIO时钟分频系数CLKDIV,计算公式为SDIO_CK=48MHz/(CLKDIV+2)也可手动修改时钟配置。
' {& O7 h9 M- r( S4 E1 Q" G
) I9 H9 K1 ? s5 x8 @. a3 F8 j( J启用文件系统中间件,Pinout&Clock Configuration,Middleware -> FATFS,模式选择SD卡,配置文件系统:8 H! t/ z0 i) @
如果要支持中文文件名,则配置CODE_PAGE为Simplified Chinese9 |9 T, P6 y: ]; Y/ v! k8 k
如果要支持长文件名,则要使能USE_LEN4 O, u1 }+ |% p! t$ H. K r
9 Q0 ~( w4 u+ b4 P- v6 a0 d+ T继续上面界面,Advanced Settings勾选Use dma template/ a' U; u/ w6 ~' X# d
& _/ X) e8 `* o& J1 x( s
设置DMA传输,Pinout&Clock Configuration,Connectivity -> SDIO-> DMA Settings
1 s2 p8 \2 q! B. z! b8 g, z
* J6 S9 M- E, P$ p
配置NVIC,Pinout&Clock Configuration,System Core -> NVIC。- V9 y4 L$ N( V( Q0 I
注意,SDIO中断优先级必须高于DMA2 stream3和DMA2 stream6的中断优先级' s8 V/ [4 G3 C7 T+ d1 H
8 |0 Q+ n: f% J" R" F3 L5 @3 ]
堆栈设置,Progect Manager -> Project -> Linker Settings,加大堆栈大小(注意:由于刚才设置长文件名动态缓存存储在堆中,故需要增大堆大小,如果不修改则程序运行时堆会生成溢出,程序进入硬件错误中断(HardFault),死循环)。1 b# I5 n: Y! r2 t/ l: W# v
@- R& G+ O1 k. ^0 |
生成工程,GENERATE CODE ,用KEIL打开' n1 t6 Z3 ]9 h: B
, f9 l A6 F% r
- UINT bw;
/ C" m* V5 d7 l$ Q# }4 d1 q( g - retSD = f_mount(&SDFatFS, SDPath, 0);# |' t6 e4 @8 V3 C: }3 g% _; I
- if(retSD != FR_OK) {
1 a" Y* x7 ~5 {; Z - printf("Mount Error :%d\r\n", retSD);+ [' q% m3 s8 w
- }
- `% \1 C* i8 R! R. n: U - retSD = f_open(&SDFile, "0:/test.txt", FA_CREATE_ALWAYS | FA_WRITE);
+ B6 J- z1 m# B6 q5 _; A - if(retSD != FR_OK){
4 e/ R# P& Q. A* I/ Q# G - printf("Open Error :%d\r\n", retSD);% e, F1 v" |( H7 i7 J. b
- }
4 [/ _0 X: d5 Z$ u/ n - retSD = f_write(&SDFile, "abcde", 5, &bw);' i0 D: F% ~6 g U3 X: {
- if(retSD != FR_OK){- z$ _* Z+ S) K) R8 X& X# y& k
- printf("Write Error :%d\r\n", retSD);
# [ o- A e5 ]' S - }2 r# F: p! F. V5 W" h
- f_close(&SDFile);* c/ ^/ R4 k. F- ^( v
- retSD = f_open(&SDFile, "0:/test.txt", FA_READ);9 ]9 \! c0 W5 E+ O
- char buff[10] = {0};. L) `% y0 L3 w$ p2 K; @
- retSD = f_read(&SDFile, buff, 5, &bw);
a, N7 E8 J7 E' b0 J/ E, W - if(retSD == FR_OK){* q4 }1 c7 p2 u8 `( E
- printf("%s\r\n", buff);
$ w. ]; U& ]4 n: @7 T' w - }4 h6 o* X+ D$ L- M
- f_close(&SDFile);
复制代码 + Q, e7 h: {: X
& C4 n9 e* x2 U0 `3 R: WDRAM' g8 R1 V4 L' P, i9 B) U% _7 T/ F
开启FMC功能,Pinout&Configuration ,Connectivity -> FMC -> SDRAM2% T) f1 V' A% B3 J! }* h2 H
SDRAM1的起始地址为0XC0000000,SDRAM2的起始地址为0XD0000000。# s0 C8 x# ]! f7 s" s3 S$ T( G
一般SDRAM都含有4个bank。3 s( {3 S; a8 A: g
Configuration中的参数可从SDRAM的数据手册上找到。
. e8 X" n$ p3 o! C; x4 n8 }" h3 g
' F& u3 l9 b5 L1 u各个选项的配置(只做解释,不对应上图):
5 }' ^0 _: e1 `3 p8 H) v5 R! FClock and chip enable:FMC_SDCKE0 和FMC_SDCLK0对应的存储区域1 的地址范围是0xC000 0000-0xCFFF FFFF;而FMC_SDCKE1 和FMC_SDCLK1 对应的存储区域2 的地址范围是0xD000 0000- 0xDFFF FFFF
c) r& N! ]- d; `+ H9 X' n! uBank由硬件连接决定需要选择SDRAM bank 2
3 B/ M! |! S: e) NColumn bit number表示列数,8位- J4 w4 n* O% k4 B7 P3 J
Row bit number表示行数,12位7 r( F `1 N f- p; A5 d
CAS latency表示CAS潜伏期,即上面说的CL,该配置需要与之后的SDRAM模式寄存器的配置相同,这里先配置为2 memory clock cycles(对于SDRAM时钟超过133MHz的,则需要配置为3 memory clock cycles)
" p1 W( E1 v, M7 V+ w, R) nWrite protection 表示写保护,一般配置为Disabled/ s3 C0 v, G( H8 l: Q6 b. X0 r, n1 x/ @
SDRAM common clock为SDRAM 时钟配置,可选HCLK的2分频\3分频\不使能SDCLK时钟。前面主频配置为216MHz,SDRAM common clock设置为2分频,那SDCLK时钟为108MHz,每个时钟周期为9.25ns2 u/ n7 l" S0 V5 W% f- ?' ]7 b4 f. u
SDRAM common burst read 表示突发读,这里选择使能
+ s* Y3 C8 E4 lSDRAM common read pipe delay 表示CAS潜伏期后延迟多少个时钟在进行读数据,这里选择0 HCLK clock cycle5 W6 N. S/ v) f" z
Load mode register to active delay 加载模式寄存器命令和激活或刷新命令之间的延迟,按存储器时钟周期计
3 t' {# Q7 c) h7 C% ^! D
' b* q1 {/ G* }2 G: |5 R, f/ m: i/ d; f& X; `' U$ f! e8 {' a& o
Exit self-refresh delay从发出自刷新命令到发出激活命令之间的延迟,按存储器时钟周期数计查数据手册知道其最小值为70ns,由于我们每个时钟周期为9.25ns,所以设为8 (70÷9.25,向上取整)
' s# b! N+ Z1 C6 L F
9 b) q( F+ A! P4 T5 Q7 w5 g
+ `# A" B+ a# n3 k% \* YSDRAM common row cycle delay刷新命令和激活命令之间的延迟,以及两个相邻刷新命令之间的延迟, 以存储器时钟周期数表示
' e* A4 [" A& A& t* `; s查数据手册知道其最小值为63ns,由于我们每个时钟周期为9.25ns,所以设为7 (63÷9.25,向上取整)" S+ Q1 [% d. s. ?$ g$ k$ h, B$ K
! ?6 c' X0 P& M5 i `9 O, Y
) C6 `& r7 S% ^5 u/ RWrite recovery time写命令和预充电命令之间的延迟,按存储器时钟周期数计
) ] q1 x* ?6 G' K: u- a$ y, U. d6 v& y
: Z n% n: A3 A7 [3 ~3 M3 MSDRAM common row precharge delay预充电命令与其它命令之间的延迟,按存储器时钟周期数计
# n' D7 _# U" `9 G# r查数据手册知道其最小值为15ns,由于我们每个时钟周期为9.25ns,所以设为2 (15÷9.25,向上取整) _' }3 U0 ~2 e) L
; @# h" T! q% ]# X% i- h9 Z; v; s' u
& \' Z& O( ^6 u; E- X( |Row to column delay激活命令与读/写命令之间的延迟,按存储器时钟周期数计: P& p9 c) _4 u5 M& A& c, z( Y
查数据手册知道其最小值为15ns,由于我们每个时钟周期为9.25ns,所以这里本应该设为2 (15÷9.25,向上取整)- Q5 {# a; c2 v' E
但要注意,时序必须满足以下式子:: j3 r; U$ n4 E. r1 _' D
TWR ≥ TRAS - TRCD
2 E) I, e* H L$ i" `! P/ xTWR ≥ TRC - TRCD - TRP
\( y! T% y9 I* S: M v) J# i其中:TWR = Write recovery time = 28 l1 l/ x7 c1 z8 b) D. n
TRAS = Self refresh time = 5. }0 B8 k4 q# t* u
TRC = SDRAM common row cycle delay = 77 c" S# s, \( \; q/ A. L" }* O$ d
TRP = SDRAM common row precharge delay = 2+ _8 _7 [ X8 r' [, g# p0 Y
TRCD = Row to column delay
/ D5 _4 j" f2 z2 O* c/ w所以这里Row to column delay应该取3
2 I4 Q; P0 c0 V6 z3 s3 Y$ j( B8 m% g, O
! @. }& z& _5 O$ h7 c! n. C生成代码,GENERATE CODE, 用KEIL打开
$ E, @% z4 Z' K- uint8_t temp[100]__attribute__((at(0xD0000000))); h: ^; V& f/ k/ ]+ r p
- for(int i=0;i<100;i++){
8 R8 C: L: a, }7 Q - temp[i] = i;$ X) n% ^4 @0 g/ l2 l
- }
; l' _3 s" b4 l" s, q - for(int i=0;i<100;i++){2 g2 V) `# J; ?
- printf("%d ", temp[i]);
7 R) W* d! \# h b a( O - }
复制代码
" c9 z! X+ f( a M# a- \. M; P7 y5 f* d& s) O3 P
到这里只是借助Cube完成了引脚配置,还需要SDRAM初始化操作和读写函数,可从官方例程里获取,路径:
0 e U" ?7 s+ [! M/ O" a! k) m, }C:\Users\10617\STM32Cube\Repository\STM32Cube_FW_F4_V1.25.0\Drivers\BSP\STM32F429I-Discovery\XXX: J( I" K4 ^: T6 u" x* h" I
2 [2 {/ @+ ~$ j9 q) V7 j5 b/ H; n: S3 O( }% j" N
- /*****************************SDRAM使能函数******************************/$ y9 b( z# ^1 k1 N1 H
- * L" H0 F9 v7 [. `9 J
- /**
- ~2 F" v3 ?4 R- K9 c1 @ - * @brief 对SDRAM芯片进行初始化配置# h+ s& J3 h. N
- * @param None. - y9 x7 D. g v& T. V% C. P' C. p; i4 V
- * @retval None.
8 I4 l" ?% ^9 T0 h: }2 z: Y! } L - */
! J" E# _( y! H3 k$ _( ?# J) ` - static void USER_SDRAM_ENABLE(void)3 i4 S0 [! ]) A: Z
- {
3 V" ]0 {* ?* O - FMC_SDRAM_CommandTypeDef Command;
. P# }, `5 C X - " w8 i" ^6 @! T& r' @/ J5 d" _
- __IO uint32_t tmpmrd =0;" _% J( V6 ?! {5 }
-
( m- D/ O$ p/ v) w' t7 L1 B - /* Step 1: Configure a clock configuration enable command */
" ]' ~0 ?; W- r* u - Command.CommandMode = FMC_SDRAM_CMD_CLK_ENABLE;
! T3 T& P+ [% b5 m; E# U8 o - Command.CommandTarget = FMC_SDRAM_CMD_TARGET_BANK2;
1 M4 k6 y$ Q! m - Command.AutoRefreshNumber = 1;
- t) \6 G, s9 ] p( r - Command.ModeRegisterDefinition = 0;, v, U5 u! n: M. j' B+ _$ O
- 0 \% f( `& \0 F& W
- /* Send the command */
, W7 w* J ?/ I; f - HAL_SDRAM_SendCommand(&hsdram1, &Command, SDRAM_TIMEOUT);
+ ~/ W6 m5 s7 u2 M; \: O, J -
0 u% V6 l5 P0 A: m( N3 f9 v, T - /* Step 2: Insert 100 us minimum delay */
; N, Y, m- F7 D6 L+ L - /* Inserted delay is equal to 1 ms due to systick time base unit (ms) */
/ C9 |4 ^9 a4 t. @& _2 X! c$ Z c - HAL_Delay(1);3 C$ u4 W3 \7 q" p" L9 j5 b4 p
-
8 R- e7 L& i" n& | B8 x5 ~6 P - /* Step 3: Configure a PALL (precharge all) command */ Z2 W, T7 S1 K
- Command.CommandMode = FMC_SDRAM_CMD_PALL;
! [2 v$ b1 n7 t7 v% b - Command.CommandTarget = FMC_SDRAM_CMD_TARGET_BANK2;) Y j, V' W0 A7 {
- Command.AutoRefreshNumber = 1;$ ^6 o# d5 U8 a& ]
- Command.ModeRegisterDefinition = 0;! y( N" q8 b* {9 K
-
4 n2 d* @& O) [, ?6 H - /* Send the command */. H Z( Z2 q, D6 x/ B& k% S; m
- HAL_SDRAM_SendCommand(&hsdram1, &Command, SDRAM_TIMEOUT);
1 B) l3 l8 G* n. n$ I -
. D% K) v- S9 L2 K0 X6 [ - /* Step 4: Configure an Auto Refresh command */
. R+ J7 Y6 ~( p/ t( r" n - Command.CommandMode = FMC_SDRAM_CMD_AUTOREFRESH_MODE;& u2 F/ a7 m) U- Y% Y. A% T
- Command.CommandTarget = FMC_SDRAM_CMD_TARGET_BANK2;+ Z4 Y' l: J$ U' p2 S X8 s
- Command.AutoRefreshNumber = 4;
0 H9 D* S5 o- b2 ?- i" i: W - Command.ModeRegisterDefinition = 0;
( m$ _" |1 [( v6 C7 p; T! T) z& \ - / e& T5 U6 c: R! X
- /* Send the command */ D, y' _4 w1 W' l; ~6 Z( |7 J
- HAL_SDRAM_SendCommand(&hsdram1, &Command, SDRAM_TIMEOUT);# O5 M8 q+ {- B" I4 x. g
- 6 ^3 e, C; H- A& o- p% \* M% L
- /* Step 5: Program the external memory mode register */8 i' y; \+ l0 y; f
- tmpmrd = (uint32_t)SDRAM_MODEREG_BURST_LENGTH_2 |
3 |1 Q+ y4 P7 \: G T2 L - SDRAM_MODEREG_BURST_TYPE_SEQUENTIAL |
6 |3 W9 q# D m' J( C* { - SDRAM_MODEREG_CAS_LATENCY_3 |
. x; }! I) v2 ?' U3 z$ ]* L# k' k - SDRAM_MODEREG_OPERATING_MODE_STANDARD |; L& O9 _ t( V: ?( y( q* d
- SDRAM_MODEREG_WRITEBURST_MODE_SINGLE;
& S4 X, U, K- c5 n -
7 _/ F. z' Y& p, d) J+ y5 W - Command.CommandMode = FMC_SDRAM_CMD_LOAD_MODE;2 H# b i4 R& z+ N2 X
- Command.CommandTarget = FMC_SDRAM_CMD_TARGET_BANK2;* [" {8 z" f' `& v$ R5 X. r
- Command.AutoRefreshNumber = 1;
% p' P' \5 V& ~: V - Command.ModeRegisterDefinition = tmpmrd;
4 I7 z- r+ A6 m( B - , p3 c! @6 Z% j# N
- /* Send the command */
3 K: v+ ~, X1 @ - HAL_SDRAM_SendCommand(&hsdram1, &Command, SDRAM_TIMEOUT);
- R Q0 _/ n2 y' s j) S3 U - , T8 h0 n4 A: W3 o' W
- /* Step 6: Set the refresh rate counter */6 _: |3 @* V% w" y0 }0 G
- /* Set the device refresh rate */, O3 R! S0 }7 x0 J; l T* S* z' f
- HAL_SDRAM_ProgramRefreshRate(&hsdram1, REFRESH_COUNT);
8 @; X. R# ~2 f4 l0 j) d: ]' E - }
" X2 Z0 N- J' O6 L$ P" ~, g- L - /*****************************使能函数结束******************************/
复制代码 0 V0 }6 E2 K1 F- K, X5 Y
6 H0 @* X9 ?4 S8 H
或者以我的野火STM32F429IGT6的版本,源码链接:
, s @8 I4 a- h& b0 w9 b: B" z, Lhttps://sxf1024.lanzoui.com/b09rf535a 密码:bf5q; Q8 L' L6 _! u- M9 J2 ^9 D
添加到工程Core路径下,然后在KEIL中初始化操作:
7 D W; A0 C7 m- #include "bsp_sdram.h"
, W- f( J& I# @ B - MX_FMC_Init();8 I, a. M! I+ y5 V; a D* s0 n9 }* n
- SDRAM_InitSequence();
复制代码 + m3 c& }' |# ^6 t* T+ H6 z& e7 G
7 S: C2 r6 i* x( U( ]5 x
5 l/ W) F' P' d
9 ?% ~0 h _; R% _. q8 n C
4 ]0 n/ @; u: m5 x- [- L- e: y9 f
l- \9 f, s6 P! ~6 \) d
( x% Y0 o" p$ E( M
. V/ c; A8 T. e/ L$ B
) D8 B j$ j# h" g/ \8 h! |
$ E1 C. C, h* d% t6 M0 s3 v2 o |