STM32CubeMX + HAL" t: E# `# m% l) n0 S
一些说明
v4 l v2 a. g, J8 L) p- @底层配置
: G/ b- R0 d# u1 ], g" I0 J+ X使用STM32CubeMX代码生成工具,不用关注底层配置的细节,真舒服。* v7 E& n- W& ^. C o W8 F' ?# ?
使用教程:2 I- @8 `; {6 R' }! w0 R7 y
https://sxf1024.lanzoui.com/b09rf2dwj 密码:bgvi
. s9 ` o% Q) b2 P虽然Cube+HAL很舒服,但新手不建议用。最好还是先去学一下标准库怎么用,有个大致概念后,再来学这一套。
' ]( ^5 b4 k: M/ O* k6 @' b1 e) y: QCube基本使用9 a" v, I. d, w3 e+ L2 `
新建工程
7 f2 T. P' G/ { \3 B) m- s' ]$ A选择芯片
' \$ W( h7 @1 i9 Q. _( \Pinout&Configuration,选择RCC(HSE:Crystal/Ceramic Resonator)、SYS(Debug:Serial Wiire)
+ V6 x a) f( B+ T' P7 x0 ZClock Configuration,配置时钟树
! h0 y# [! X |+ T
: Y* h0 X6 S, w! a
Project Manager,配置工程输出项
. w4 K4 {2 p% F1 n) n# }7 E
' c2 |, d- M8 ^0 WPinout&Configuration,选择功能(若是选GPIO相关,可以直接在Pinout view选择;若是其他功能,可以在左边Categories打开,会自动配置引脚)、设置Parameter Settings/NVIC等2 b9 M, a7 }( k: I. w
( |- d: A" h1 g, D, UGENERATE CODE,生成工程,用KEIL打开编辑. P3 p7 Y+ `4 r. z5 p+ X
HAL库函数
' O7 v; W- ^* ?函数形式:均以HAL_开头$ `2 S: ]6 W$ S
寻找过程:在驱动文件stm32f4xx_hal_XXX.c或其.h文件中找函数定义,一般在靠后位置4 C, Q/ i, W# o8 G+ \
其他说明:
T: Y& |" o( U% K7 zHAL库并没有把所有的操作都封装成凼数。2 n& b Y7 M; G
对于底层的寄存器操作(如读取捕获/比较寄存器),还有修改外设的某个配置参数(如改变输入捕获的极性),HAL库会使用宏定义来实现。而且会用__HAL_作为这类宏定义的前缀。# q3 j% I, L8 N) S
获取某个参数,宏定义中一般会有_GET;而设置某个参数的,宏定义中就会有_SET。
, A d) R9 c+ H; v* h/ z, i在开发过程中,如果遇到寄存器级别或者更小范围的操作时,可以到该外设的头文件中查找,一般都能找到相应的宏定义。
/ b9 n9 e: G6 ]' H; p2 ]HAL库函数第一个参数一般都是句柄(一个包含了当前对象绝大部分状态的结构体),虽然增加了开销,但是用起来便捷了非常多。
8 s1 |, M; L2 y7 z/ {- Y A$ S1 j中断回调函数# n2 A( ]. H, J6 B* a/ O
函数形式:HAL_XXX_XXXCallback()。
* D& V# b) \/ p+ P7 \( y$ y" _寻找过程:中断文件stm32f4xx_it.c - > 中断函数XXX_IRQHandler(void) -> HAL库中断函数HAL_XXX_IRQHandler(GPIO_PIN_13) -> 回调函数HAL_XXX_XXXCallback()( e9 C' ?2 B4 _5 x5 h* ^
外设对应时钟
6 @; W8 Z, J- r. y) P随便进入一个外设初始化函数,如MX_GPIO_Init()* {6 j! u7 {" ?4 _( b; G4 C
随便进入一个时钟使能函数,如__HAL_RCC_GPIOC_CLK_ENABLE()
. c8 x$ I2 s5 {4 K' t9 }随便进入一个RCC宏定义,如RCC_AHB1ENR_GPIOCEN1 a0 X6 H/ q3 }2 M" _: }
或者直接进入stm32f429xx.h文件
. W) w D2 w+ N% b: G: w( |里面有所有外设与时钟对应关系,如RCC_AHB1ENR_DMA1EN9 m# E4 b# k6 i3 y* c
& p4 W0 m0 }1 \# _0 C4 ?1 q
# B4 I2 x$ ?* y4 M+ O) [- l0 I2 D' D0 k7 s3 ^0 n- g
. w5 Z! ^* P4 X5 t1 ~$ i1 B/ I配置示例# S$ `/ V* w1 {
小编有话说
& a7 c: l2 m* ]/ T例子源码:4 ]% M5 T5 ]% X7 i7 @ l* F1 ^4 m
https://sxf1024.lanzoui.com/b09rf535a 密码:bf5q3 l1 {% q6 p# s! a
如果配置过程中,参数不知道怎么设置,可以去标准库例程(如野火、正点原子)中看对应的参数是什么Cube软件只是帮你配置了底层,一些初始化代码还是需要自己手动加的,如SDRAM充电初始化、读写函数等! d* V9 P+ a8 R1 t0 _3 |* x# A
RTC e0 _8 p" M4 A- I7 P
* U$ x4 s; L+ f4 G3 ?$ l
, j# q$ G' j( C0 k/ v& l
, }: ]5 T8 s. q2 M8 h& _
) `2 Y/ ]) _" k ~) t6 h ^
" Z9 u* ]% ?/ o) N* C0 V9 i( m- RTC_DateTypeDef sDate;
$ h; v! q* a5 u* h6 p - RTC_TimeTypeDef sTime;
3 `6 A# ?& `0 a$ A# } - uint8_t second_tmp = 0;' G s& H1 A3 ~' M4 c$ r
- . O6 Z; O$ u* E( a: q
-
; x5 h$ l- g0 y' e! n/ ~5 E* i2 z - HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN); // 读取时间% r8 S) \ `4 y3 `+ k; w3 Y
- HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN); // 读取日期$ q" O/ W3 t E/ n% E4 N
- if(second_tmp != sTime.Seconds) { // 读取秒) [' F, }$ t( t
- second_tmp = sTime.Seconds;
$ U7 O6 D. g" y( U* t1 [9 Q; I) o - printf("20%d%d-%d%d-%d%d\r\n", + q4 o7 N% B: L( f
- sDate.Year/10%10, sDate.Year%10,
7 c9 o5 x% b0 E - sDate.Month/10%10, sDate.Month%10, : p- x, u0 V* P8 s% }2 o& a
- sDate.Date/10%10, sDate.Date%10);$ B7 V& b( H0 d1 l
- printf("%d%d:%d%d:%d%d\r\n",
& v# `- q8 G6 J3 d9 d2 E - sTime.Hours/10%10, sTime.Hours%10, . r% T) J7 S# H% U* h" o
- sTime.Minutes/10%10, sTime.Minutes%10,
7 y# }( P" G: o- j1 d - sTime.Seconds/10%10, sTime.Seconds%10);
# h% g7 L- u R$ n" v: T* d - }
复制代码
/ r: f, A3 G, U/ j' u3 r- S+ [' }8 T5 x- d2 k
SDIO + FATFS0 k8 j3 }8 {( Y. ~& `2 g
选择SDIO功能,Pinout&Clock Configuration,Connectivity -> SDIO -> Mode: SD 4bit Wide bus -> 勾选NVIC
; {( O, J0 Z& |4 U
# o$ ?: b6 a3 P: k& d; G% L% P
配置SDIO时钟,Clock Configuration,SDIO模块输入要求为48MHz,系统提示可以自动设置时钟问题,选择Yes。SDIO时钟分频系数CLKDIV,计算公式为SDIO_CK=48MHz/(CLKDIV+2)也可手动修改时钟配置。
6 r# \1 d3 U$ q6 C+ j5 A# V2 s
' |% l( U* ?8 R: A. a- `6 u启用文件系统中间件,Pinout&Clock Configuration,Middleware -> FATFS,模式选择SD卡,配置文件系统:
6 c; k+ z. K: v+ u0 R# w6 m如果要支持中文文件名,则配置CODE_PAGE为Simplified Chinese0 i4 U) m# I3 z J9 E; x/ _
如果要支持长文件名,则要使能USE_LEN
! ]6 N% G! G! K* h
4 ]" Z/ p: c# t
继续上面界面,Advanced Settings勾选Use dma template3 v$ c3 N% v C; r E- q
8 n% `+ l y" b0 f: ?9 A* O9 y% D设置DMA传输,Pinout&Clock Configuration,Connectivity -> SDIO-> DMA Settings
: ^; |/ W! c' G2 }; V/ Z
" U/ J8 R8 |8 p' c# A9 @* Y( W
配置NVIC,Pinout&Clock Configuration,System Core -> NVIC。
) K/ E4 C- R5 m. A6 q注意,SDIO中断优先级必须高于DMA2 stream3和DMA2 stream6的中断优先级
& ~% H2 P- n2 Q/ F9 C, I4 w
4 e3 w5 K$ S& d, x8 f
堆栈设置,Progect Manager -> Project -> Linker Settings,加大堆栈大小(注意:由于刚才设置长文件名动态缓存存储在堆中,故需要增大堆大小,如果不修改则程序运行时堆会生成溢出,程序进入硬件错误中断(HardFault),死循环)。2 l! }2 Z' J& \1 C( \/ }) \6 i
4 n* D" ?- F' g
生成工程,GENERATE CODE ,用KEIL打开* h7 p4 D4 a( _" r! F
* E0 ^* N& s4 L8 L' s( F: r! m- UINT bw;; v3 Q) r4 u, b1 o% q) z. ]6 Y
- retSD = f_mount(&SDFatFS, SDPath, 0);
9 t& T" c* z* d6 _7 W - if(retSD != FR_OK) {
6 ?$ {5 ]8 M' `2 i0 U6 k - printf("Mount Error :%d\r\n", retSD);! R! ^. m+ B. @9 A
- }
% W D; l0 G) \ - retSD = f_open(&SDFile, "0:/test.txt", FA_CREATE_ALWAYS | FA_WRITE);$ X" r5 V: l( V# U7 u( A
- if(retSD != FR_OK){' i' j6 y$ p& y& S( `# P
- printf("Open Error :%d\r\n", retSD);
9 N$ P# j9 g+ x% w5 ^! M# z( D7 B - }
2 v: z# m# [- J o) y - retSD = f_write(&SDFile, "abcde", 5, &bw);0 D: i, I: Z, w+ V' b4 L! N- I
- if(retSD != FR_OK){3 ?3 F: u+ G3 M) K
- printf("Write Error :%d\r\n", retSD);
0 N4 [$ r! n' V1 x. C+ e7 G0 { - }
( w% X+ q, @" d4 k - f_close(&SDFile);
* k- P. G. H) r! q$ H1 ~ - retSD = f_open(&SDFile, "0:/test.txt", FA_READ);
( G$ z, q7 [2 ~# c) Z - char buff[10] = {0};! r z, c% l; Z- T7 h* t
- retSD = f_read(&SDFile, buff, 5, &bw);
. t8 N2 b9 u0 u' H1 h* v" [/ i$ l - if(retSD == FR_OK){6 z) u7 L" r( x2 G! P
- printf("%s\r\n", buff);
, {9 T! [* F, }2 x A3 B: j( u - }' j8 g! v. Q( W
- f_close(&SDFile);
复制代码
, c+ |: M0 c0 F1 F$ `# i
& z: L, V, V: P5 K5 m! c1 dDRAM/ C# e- X, W" h0 J6 _! D$ t
开启FMC功能,Pinout&Configuration ,Connectivity -> FMC -> SDRAM26 q8 j4 v2 s; l# }# C1 m& n D
SDRAM1的起始地址为0XC0000000,SDRAM2的起始地址为0XD0000000。
3 q! Y. T' h9 v0 z# o* J6 H3 I一般SDRAM都含有4个bank。6 D' c8 a$ T3 H, z! _4 {
Configuration中的参数可从SDRAM的数据手册上找到。$ _6 B3 C# u5 E6 z! o V
H# K# A. [; T: I8 y
( C* U8 H7 i/ y; @# u各个选项的配置(只做解释,不对应上图):
& `8 S1 B$ I. k* _Clock and chip enable:FMC_SDCKE0 和FMC_SDCLK0对应的存储区域1 的地址范围是0xC000 0000-0xCFFF FFFF;而FMC_SDCKE1 和FMC_SDCLK1 对应的存储区域2 的地址范围是0xD000 0000- 0xDFFF FFFF: v% }+ | x+ b( _4 }, Y' P: W0 S
Bank由硬件连接决定需要选择SDRAM bank 2& O+ I! Y: N9 N( G! M
Column bit number表示列数,8位) \/ Z4 l) x z4 m$ _) B% A& J
Row bit number表示行数,12位
6 ~4 ^7 Y8 D3 u7 d, ?CAS latency表示CAS潜伏期,即上面说的CL,该配置需要与之后的SDRAM模式寄存器的配置相同,这里先配置为2 memory clock cycles(对于SDRAM时钟超过133MHz的,则需要配置为3 memory clock cycles)4 H" E8 C s$ U2 z9 |" O. I
Write protection 表示写保护,一般配置为Disabled' ~9 Z5 i# k2 x B i- D2 B
SDRAM common clock为SDRAM 时钟配置,可选HCLK的2分频\3分频\不使能SDCLK时钟。前面主频配置为216MHz,SDRAM common clock设置为2分频,那SDCLK时钟为108MHz,每个时钟周期为9.25ns/ f2 U6 R) y) w/ n" B
SDRAM common burst read 表示突发读,这里选择使能
( P+ Y7 O" [- g8 b, Q( }SDRAM common read pipe delay 表示CAS潜伏期后延迟多少个时钟在进行读数据,这里选择0 HCLK clock cycle
0 o4 k: J) P" r8 ZLoad mode register to active delay 加载模式寄存器命令和激活或刷新命令之间的延迟,按存储器时钟周期计
# e, `3 P' D! [+ z7 q# ~, X" ^* W( u6 } v: |; s
' o/ Q F% e& S: u( f/ r- n
Exit self-refresh delay从发出自刷新命令到发出激活命令之间的延迟,按存储器时钟周期数计查数据手册知道其最小值为70ns,由于我们每个时钟周期为9.25ns,所以设为8 (70÷9.25,向上取整)1 Y6 b) j% q/ Q
4 Q% ?* j+ G6 V. E. a+ Y8 k
( [9 Z- F! Q# _4 ^+ P8 _& _3 ^
SDRAM common row cycle delay刷新命令和激活命令之间的延迟,以及两个相邻刷新命令之间的延迟, 以存储器时钟周期数表示
! y r( O4 n; L查数据手册知道其最小值为63ns,由于我们每个时钟周期为9.25ns,所以设为7 (63÷9.25,向上取整)! p% W5 x# A% T
+ l( S; p4 k, f' o
/ C& w# ^ g" f; w) e' j6 v
Write recovery time写命令和预充电命令之间的延迟,按存储器时钟周期数计/ j/ @1 B% T ?7 C1 F5 d, r/ v
% E- i' \4 p D# z# z$ d
% x% l* k9 _( ?3 c9 Z& \SDRAM common row precharge delay预充电命令与其它命令之间的延迟,按存储器时钟周期数计/ a) L/ V8 r0 I, D7 H
查数据手册知道其最小值为15ns,由于我们每个时钟周期为9.25ns,所以设为2 (15÷9.25,向上取整)
9 ~' _" R% w3 L+ z' p# Z9 B ]5 p( a
. B T2 v" V+ ~" V# I9 h L
Row to column delay激活命令与读/写命令之间的延迟,按存储器时钟周期数计4 ?9 M( o0 _( e! [- J! e
查数据手册知道其最小值为15ns,由于我们每个时钟周期为9.25ns,所以这里本应该设为2 (15÷9.25,向上取整)
1 @& [% n! |0 N; q但要注意,时序必须满足以下式子:
. K. e, S: D5 W+ X% ~ [8 }3 [TWR ≥ TRAS - TRCD9 C. D( }5 Q5 P! Q, q; p1 S* \- N
TWR ≥ TRC - TRCD - TRP5 f* E5 q$ m" _2 {4 x$ ^8 ^
其中:TWR = Write recovery time = 2
4 a+ m9 F" _" K+ C+ i( N. TTRAS = Self refresh time = 5
. W, l- G/ w/ K- D' S* d# ^& QTRC = SDRAM common row cycle delay = 7
- P, ?7 B9 q- i6 y! Q- R* GTRP = SDRAM common row precharge delay = 29 I: N5 i/ a3 E) L- e, x! t# ^4 i
TRCD = Row to column delay
5 u* P$ q* @2 _+ i3 _/ A所以这里Row to column delay应该取3
, O2 s& v4 e6 |) [+ b5 H0 V4 T4 i+ R
. J8 s* \3 K7 X
生成代码,GENERATE CODE, 用KEIL打开
8 [0 y- s6 j' d Y$ n: W3 J# `2 l' ~" q- uint8_t temp[100]__attribute__((at(0xD0000000))); 6 ?, T7 H( N# Z# U$ G
- for(int i=0;i<100;i++){
' i3 ~' P4 S& l - temp[i] = i;8 a3 q s! Z/ N
- }
( F# J/ @+ l( Y+ T3 O S+ o2 K - for(int i=0;i<100;i++){
% r8 {4 I9 Z* u6 J3 { - printf("%d ", temp[i]);* a3 u' v* s& b: D# B; v
- }
复制代码
* G" D" S( [) L& B6 A0 P
9 ]2 `% Q$ v, ~. U" K到这里只是借助Cube完成了引脚配置,还需要SDRAM初始化操作和读写函数,可从官方例程里获取,路径:
0 ]# i" m3 S" w2 V' S& q/ YC:\Users\10617\STM32Cube\Repository\STM32Cube_FW_F4_V1.25.0\Drivers\BSP\STM32F429I-Discovery\XXX
/ A, e+ A$ {. U. q0 J0 ~
6 `5 T4 W% m8 E, v e; J+ [# Q# j+ {, \
- /*****************************SDRAM使能函数******************************/
4 C7 e. o* _2 m$ K/ \( G/ } - 0 i6 f1 ?) j1 f, O, H
- /**
1 z- P( c+ i4 R1 K# ^! f - * @brief 对SDRAM芯片进行初始化配置9 T3 J4 i/ L( Y h2 Z5 s( E; S
- * @param None. 4 E7 _6 N, ]5 k+ d* \- X
- * @retval None.. W5 C+ G4 N" s3 c* N* h! z
- */
3 y$ ?8 o- E" x5 v - static void USER_SDRAM_ENABLE(void)' @$ j+ ?# ^% u# v7 {& u4 i' O$ O0 ^
- {
" f- X& t4 X; k O - FMC_SDRAM_CommandTypeDef Command;
_; {- s( H, D+ B8 b% C4 k - # t: K4 l2 Z: E2 @
- __IO uint32_t tmpmrd =0;
% }: K( `+ D& k% D% }* V) w -
9 c) I# |& e, `3 R- G3 X - /* Step 1: Configure a clock configuration enable command */1 p3 e$ I1 j0 M. j7 s
- Command.CommandMode = FMC_SDRAM_CMD_CLK_ENABLE;
8 ?* p; F' b4 ?0 u% @ - Command.CommandTarget = FMC_SDRAM_CMD_TARGET_BANK2;$ t7 T$ V; D& T+ t% i# I
- Command.AutoRefreshNumber = 1;
' V* C' C$ P% ]. f7 {7 r - Command.ModeRegisterDefinition = 0;
1 r/ C; D% Z6 G' n* A) j -
# ^- F! {/ w! Q/ e: Y1 q - /* Send the command */
. j$ t# }- [* ~4 ^7 ~8 a6 K: X - HAL_SDRAM_SendCommand(&hsdram1, &Command, SDRAM_TIMEOUT);% X+ s5 d! Y, m9 c+ p% W
-
- z) m4 B5 T+ h+ ]5 x5 e - /* Step 2: Insert 100 us minimum delay */ 1 w" ^& q) V* R
- /* Inserted delay is equal to 1 ms due to systick time base unit (ms) */( m. y$ |& o9 R+ ]: R
- HAL_Delay(1);
: W9 P" a: R% N/ C, F" I -
I9 O5 J5 V0 a8 x" ?+ ] - /* Step 3: Configure a PALL (precharge all) command */
. W' V/ M: a6 q! }8 P% [' v - Command.CommandMode = FMC_SDRAM_CMD_PALL;( {/ w7 ? f0 [/ `
- Command.CommandTarget = FMC_SDRAM_CMD_TARGET_BANK2;
7 J6 B) ^% p6 s2 ~2 l2 B _) b$ G9 ? - Command.AutoRefreshNumber = 1;& w" e. Y& T- j! j' l
- Command.ModeRegisterDefinition = 0;
" |! J' k; f7 p) {' _6 i% Z4 A2 G+ @ - * H* l! u' h8 C/ V( o
- /* Send the command */# o( \6 B/ c5 A7 N2 Q |
- HAL_SDRAM_SendCommand(&hsdram1, &Command, SDRAM_TIMEOUT); b/ Y: R! M/ l N/ I# T4 K9 D
-
# F$ X4 g& m( H5 O - /* Step 4: Configure an Auto Refresh command */
: Q2 e+ g9 e( k) U7 I - Command.CommandMode = FMC_SDRAM_CMD_AUTOREFRESH_MODE;
6 R3 B+ n' x- |' m/ w, i - Command.CommandTarget = FMC_SDRAM_CMD_TARGET_BANK2;2 u- m* @$ Q4 l' B& ?
- Command.AutoRefreshNumber = 4;
! ^7 S, C& V9 K& F+ b& n- l - Command.ModeRegisterDefinition = 0;
/ |" o( ]3 n% a8 p4 I - # J: z& @ M. Z9 x
- /* Send the command */
C; P' x1 U+ n5 W" l h# u - HAL_SDRAM_SendCommand(&hsdram1, &Command, SDRAM_TIMEOUT);) W( I8 U. O2 U4 `+ z. C
-
' k, V7 q# J0 l+ M( i1 a - /* Step 5: Program the external memory mode register */: G; z S/ B) r- R6 F
- tmpmrd = (uint32_t)SDRAM_MODEREG_BURST_LENGTH_2 |4 n* L( D* ?- f
- SDRAM_MODEREG_BURST_TYPE_SEQUENTIAL |9 X! t, u) E2 N; c0 a* J* Q- c
- SDRAM_MODEREG_CAS_LATENCY_3 |4 O6 E; _2 j) I
- SDRAM_MODEREG_OPERATING_MODE_STANDARD |8 J% z7 n: B7 C7 u8 @3 w
- SDRAM_MODEREG_WRITEBURST_MODE_SINGLE;) c& u3 X( m# i0 z+ Q F# l7 I/ Z
- % w: u' b7 Q& r# Y$ c/ t
- Command.CommandMode = FMC_SDRAM_CMD_LOAD_MODE;# a: {; D# S( H" I9 ?* U
- Command.CommandTarget = FMC_SDRAM_CMD_TARGET_BANK2;) X' V% L' n+ w5 T
- Command.AutoRefreshNumber = 1;
- [6 p: B) Y( c8 |% M( c2 V1 G8 r - Command.ModeRegisterDefinition = tmpmrd;
! K) _+ a0 H1 l6 h0 r) [+ j - 5 ^6 N* B X3 K# u* _& m }
- /* Send the command */
: Z7 }+ f g1 U3 q' t% H5 [( J - HAL_SDRAM_SendCommand(&hsdram1, &Command, SDRAM_TIMEOUT);
. f2 x( m, ]: U; j9 x! ]) X -
! W% Z Q' q1 `8 r' j K - /* Step 6: Set the refresh rate counter */" q, e( T, U2 ?4 i! E' B" ]
- /* Set the device refresh rate */+ p) ? D2 H% k6 f
- HAL_SDRAM_ProgramRefreshRate(&hsdram1, REFRESH_COUNT); / o. v" F4 s, ?1 r! a2 R; t
- }
% H9 u3 q4 `% u; \( E - /*****************************使能函数结束******************************/
复制代码
- ~1 [5 w' @! y* ]
7 \- @! s7 R( j# Y# H' c" M5 L/ D或者以我的野火STM32F429IGT6的版本,源码链接:
/ j( @) L# O! }% @https://sxf1024.lanzoui.com/b09rf535a 密码:bf5q! X0 I' ^& y; P
添加到工程Core路径下,然后在KEIL中初始化操作:# w) @" e% h& G* h0 G; u
- #include "bsp_sdram.h"
: A4 @+ P4 W, L; k+ g4 |& z/ r H' G - MX_FMC_Init();
3 {3 n4 z5 `1 Q! n1 c - SDRAM_InitSequence();
复制代码
9 v0 D% r/ o1 F: y/ t d. E
. Z- E) k: r u0 Y- M" Z) B" _: ^& _5 A
7 r4 q& k; s$ X8 z# Y; N
_5 a5 T* a) \: X8 E1 t
! |$ \4 X* P j; s5 a. f" x- Z2 k; d; O6 E9 P. f H$ z
; g& X+ @. y) n3 n0 Y
. e( c% I {! z+ E' f
: C# D6 E3 l( U' W" T6 B' H |