3. Cache 一致性问题
2 y+ j) @( d; ]$ @$ W3.1 什么是 cache 一致性问题
$ `9 o/ D) u( j' ?$ T; o+ W所谓的 Cache 一致性问题, 主要指的是由于 D-cache 存在时,表现在有多个 Host(典型的如 MCU 的 Core, DMA 等)访问同一块内存时, 由于数据会缓存在 D-cache 中而没有更新实际的物理内存。
1 x: C! X W( p7 ^ B; U$ s
7 F1 q- G7 r0 n, Y在实际应用中,有以下两种情况:
' y/ d( q; D3 G+ I# G9 V4 B
- u' l7 n8 H1 T2 |: r$ G, a% _第一种情况是当有写物理内存的指令时,Core 会先去更新相应的 cache-line(Write-back 策略),在没有 clean 的情况下,会导致其对应的实际物理内存中的数据并没有被更新,如果这个时候有其它的 Host(如 DMA)访问这段内存时,就会出现问题(由于实际物理内存并未被更新,和 D-cache 中的不一致),这就是所谓的 cache 一致性的问题。
' v# o& a8 g" T$ @) S6 F) i1 y* D v E+ l! w( d
& I! U$ N' d, V7 ^" C
4 b9 G8 v9 a D% t Q7 K4 O图3.1 Cache 一致性问题 第1种情况
# F: P! i; _" r1 R [6 N6 P# S: S- F# ]
第二种情况是 DMA 更新了某段物理内存(DMA 和 cache 直接没有直接通道),而这个时候 Core 再读取这段内存的时候,由于相对应地址的 cache-line 没有被 invalidate,导致 Core 读到的是 cache-line 中的数据,而非被 DMA 更新过的实际物理内存的数据。
3 d* \8 W* C& c& w& m' t# l. G& o8 F" O1 Q, D2 i' u7 F; k
" d0 }: D6 b" k% K" H. S
% @* V8 B3 A3 z$ H0 D: q3 \6 Z$ q图3.2 Cache 一致性问题 第2种情况 5 `) e% t, y* s/ e, u; ~; r, M
& S; z( U# |- }6 v3 f4 D; y( X+ X" S
3.2 如何处理 cache 一致性问题
- g" p- ~1 }6 |' x我们知道,Cache 机制是为了提高存储系统的平均读写性能而设计的,但是这种机制带来了数据一致性问题,然而,却没有对一致性的硬件支持。
% A6 L/ F/ Z3 i C W* `4 _# S
. c8 _2 e$ P2 b6 z4 v) e- T因此为了解决一致性问题,一个办法就是禁用 Cache(cache 都禁用了,肯定不会有 cache 一致性的问题啦~)。但是如果你选择使用 STM32F7 这样高性能的微控制器,又不使用其带来的高性能特性,那你为什么要用 F7 呢,用 F3、F4 不就得了么?所以为了提高性能,还是使能 cache,并积极解决 cache 一致性问题吧。
' G' V0 @; v* K# f' J% V( ~, u0 E2 P2 R6 g Q1 M6 a$ i) y! q" k
好吧,解决 STM32F7 的 cache 一致性问题,有两种可选方案:
8 F2 [. O1 K& A0 D( L5 l8 h( o- s+ ~5 B
所有的共享存储器都定义为共享属性. e' i8 c1 p' n2 \4 q* K0 O
, H) p% F5 t5 A2 y# u0 `
这些区域将默认不被缓存到 D-Cache。
8 H8 ^9 u* p; D0 U4 n, Q所有的操作都直接针对二级存储器(内部Flash,外部存储器),性能降低。
! t# E" ]* `% S因为缓存对这些区域是透明的,写软件更容易。/ F/ J; q) K/ c6 W- R4 X r
6 \* l5 ^$ z t0 w2 ^
通过软件进行cache的维护7 G; _8 j, g( v9 t+ l3 |
(1)Cortex-M7 的写操作要是全局可见的
" o/ [" Q( H! R! N* S6 d) e% M1 |; {. W5 A
使用透写属性(通过 MPU 设置)。
7 y- H! r4 T+ l; [8 k使用 SIWT@CACR(Shared = Write Through)。
' F$ u. e; d3 f- u通过指令清 D-cache,然后所有更新位置禁止 D-Cache操作。: O4 I8 b3 l" w/ l( p
(2)其他主设备的写操作要对 Cortex-M7 可见
9 c) Z0 D9 P" f7 q! m% p
7 J* j2 e. A- u" |6 d; J" K2 o比如作废 Cortex-M7 Dache 中数据。, J+ @- t( {9 C! s; r, M
3.3 示例
2 F* q+ ~3 z3 K3.3.1 程序描述/ Q) c [9 l3 D/ n' i
(1)首先将地址 0x20020000(SRAM1)处开始的 128 字节初始化为 0x55。7 j' u7 j: a3 E+ Q/ M5 b2 Z
(2)将 Flash 中的 128 字节的常量数组 aSRC_Const_Buffer 拷贝到 SRAM1 地址 0x20020000(pBuffer)。
0 y) ^4 }9 L2 e, E6 I4 Y5 X(3)配置并使能 DMA,通过 DMA 将数据从 SRAM1 的地址 0x20020000 处拷贝到 DTCM RAM 中的数组 aDST_Buffer 中。% m @, O, o- Q
(4)将 Flash 中的数组 aSRC_Const_Buffer 与 DMA 读出的数组 aDST_Buffer 进行比较。: I8 ?5 F' o: M g9 \( ~2 h
) q' C$ `! Z. A4 Z3 @显然,这个例子中的 cache 一致性问题, 展示的是上面(图3.1)的第一种情况。也就是在 Write-back 策略下,CPU 先去更新相应的 cache-line,然后 DMA 去访问对应的内存,从而导致数据不一致的现象。
! g) P' h/ H- b* u0 L5 N, w+ @5 F" T! i9 x$ @
程数据的传输流程和路径如下图所示:8 W+ C9 R [ b4 z4 D
# R; z& k% o) K/ v2 C+ v; }" U
+ ~6 v- U6 Q% k; o0 x3 a
+ a& J m. b/ R( a图3.3 Cache 示例数据传输框图 % S/ @6 Q$ G6 ], L/ c7 x
# S6 ~2 `# m) E' w6 A% K+ l3.3.2 复现 cache 一致性问题& C; ~( ^4 ~, K$ O6 r, p
我们先来按照示例要求编写代码,复现 cache 一致性问题。有些人可能会疑惑,变量数据怎么放到 Flash、SRAM1、DTCM?实际上,可以通过一些相关的配置文件进行设置,比如 icf 文件、scatter 文件等,当然,这跟所使用开发环境和编译工具链有关。& w! T2 \3 M. H+ X+ p
. v* H/ Y2 M( B z) K4 x本文所使用的环境是 IAR,其链接文件 *.icf 如下:
; ]( a) ~3 a; `1 U9 q
0 J& J; m2 ?% [! J3 s
$ I5 H% l! X" ~. j) [. D" a. m7 \7 s& L! z& f9 L9 i4 Y
然后将 aSRC_Const_Buffer 数组定义为常量,即可分配到 RO 区域,aDST_Buffer 定义为普通的全局变量或静态变量即可,因为内存区域从 0x20000000 开始,也就是 DTCM RAM。
+ g' H, |& f8 {$ z# ^& P+ Y; v, }& p" F6 _- h
好了,代码主体部分如下:
+ I. k9 h) D+ l% Q0 a
* B+ s8 q2 @. u' |- #define SRAM1_ADDRESS_START (0x20020000UL)" P% R+ D, {/ {& C" M" I& Z
+ m5 _, c3 A/ Q5 H/ ?. i- static const uint32_t aSRC_Const_Buffer[BUFFER_SIZE] =( D3 e( k$ ], H- J9 t, O
- {
. W( a# `7 e8 G+ q3 G - 0x01020304, 0x05060708, 0x090A0B0C, 0x0D0E0F10,/ [6 d8 T1 r' B* j5 v# f3 o1 X! ?
- 0x11121314, 0x15161718, 0x191A1B1C, 0x1D1E1F20,
A. G7 e5 I7 h% z- x - 0x21222324, 0x25262728, 0x292A2B2C, 0x2D2E2F30,; [% _6 q$ {, H' q7 o7 H. q
- 0x31323334, 0x35363738, 0x393A3B3C, 0x3D3E3F40,2 W" X f% |, Z1 u) N& ~
- 0x41424344, 0x45464748, 0x494A4B4C, 0x4D4E4F50,
' x7 C' k6 w5 ]# \( d - 0x51525354, 0x55565758, 0x595A5B5C, 0x5D5E5F60,
2 V7 P; \3 t. _5 C+ m" ]. G. A - 0x61626364, 0x65666768, 0x696A6B6C, 0x6D6E6F70,
8 g2 e% U+ _. K- c$ ~, O - 0x71727374, 0x75767778, 0x797A7B7C, 0x7D7E7F80
" w! ?* Q) b+ F# F: N - };
7 E& [" k) J! H+ ]9 K+ j - & U X k- G9 i4 T, n1 u$ }( t
- static uint32_t aDST_Buffer[BUFFER_SIZE];. j* Q; h' P; g; }: v- y3 h
" r6 x l: y& Z5 V0 {% U4 {9 g8 I- int main(void)" z X9 ~2 w4 h5 P% y
- {
* q! N! w) E) z# O0 p. s! @1 l# e - uint32_t counter = 0;1 a4 z5 B/ K' P
- uint32_t *pBuffer = (uint32_t*)SRAM1_ADDRESS_START;
- Q' y3 P2 L( F5 {0 K. [ M1 N - 3 t) ]; K6 P: A1 `, S+ v' J% _+ S0 L
- if (HAL_Init() != HAL_OK)
# N. i2 k- L: N$ F( h - {
2 y, y9 y- m+ d - Error_Handler();9 v S! ^2 X' O( f5 Q( W" V; @, ?: c! j
- }# y. F2 l( B# a6 S
! e2 k' Y# @+ f9 D* n, S' U6 M- /* Initialize LEDs */
2 n9 t) f/ u8 E4 L C7 z7 i - BSP_LED_Init(LED1);+ F; ^& r3 `6 d7 g, C- u( E
2 \! a! t% X, [: n3 u e8 N; F- /* Configure the system clock to 216 MHz */6 K) y- }( b: J/ i5 ?. e
- SystemClock_Config();
% ?9 F! d2 {) R8 X* ] - BSP_LCD_Config();
$ K1 [4 q0 ]7 n - 1 C5 R+ j) f8 F5 c6 V/ x5 Y& ^, m
- /* Set to 1 if an transfer error is detected */
- ~0 ?+ B( ]" f7 |" Q- q. i9 y4 X - transferErrorDetected = 0;
+ F0 \8 P* P1 `4 `# N9 L1 ? - ' w. R* S' }* m! j3 S1 r# g5 N
- /* Fill 128 bytes with 0x55 pattern */
$ e7 e$ `% D& b0 s( ~1 R - memset((uint8_t*)SRAM1_ADDRESS_START, 0x55, sizeof(aSRC_Const_Buffer));* p5 D. T0 ~3 ^
& k% ]6 B$ d3 d2 ]- /* TODO:Enable MPU and change SRAM region attribute 8 S( R. w4 t1 K8 s
- * set write-back policy on SRAM */
. B. A$ C% R& g1 [: x: L, c4 `1 x - MPU_Config();, O$ |6 Q b" K: F: U6 E8 j C
- ; u/ \- N- @( {9 o% G1 ^! K. k
- /* Enable Data cache */
O7 N6 A- @. |& ^) B$ H - SCB_EnableDCache();2 d. L5 f4 v; y$ Q5 o2 G
`5 a0 m. e/ L h! U+ |: [5 e- /* Copy data from Flash to SRAM by CPU */
+ ~* b' ]; P- Y' I, D - for (counter = 0; counter < (sizeof(aSRC_Const_Buffer)/4); counter++)
: T6 V8 z, t+ k- E6 Y+ s - {
0 \+ X; i w3 o3 A. X( S) F - *pBuffer++ = aSRC_Const_Buffer[counter];
( k' t, ]& S, g/ i( G# [ - }
6 i; H% \+ V& x1 k' c+ Y - 1 u% L$ B! }2 J7 Q1 E7 ]7 l4 A% D
- //* Configure and enable the DMA stream for Memory to Memory transfer */
$ A) M' W% e. S* l% j4 }* ?; w - DMA_Config();
, a1 v8 G9 ?5 \: |0 j - 5 h _' g: L9 G+ |0 K% s
- /* Wait for DMA end-of-transfer */
% h I6 a7 H- ]+ ^$ m: \& u, T) D s - " E4 }4 k t. p# x- P
- while(TransferCompleteFlag == RESET)
! M6 t. P. ^: P/ l - {1 z0 S0 v$ [' c! s3 y+ g3 H( g
- }( A, o2 n/ P1 y: c7 r6 s; T
. n& r' @8 k$ Y" W' t/ \- /* Check data integrity*/
1 r! m) `; r. D) j H+ p \, |0 t( J - pBuffer = (uint32_t*)&aDST_Buffer;
% h6 N& W4 [; X: A - for(counter = 0; counter <(sizeof(aSRC_Const_Buffer)/4); counter++)
' A1 E; L6 O' S& O6 S+ Z - {1 y" D7 ?- J$ X( h
- ) H* Z/ M+ q6 |! D
- if(aSRC_Const_Buffer[counter] != *pBuffer)$ S ]. V3 B7 e/ j! |) f. V
- {! p+ C( W; m6 V
- compareErrorDetected++;
J, G; j2 y4 N - }
1 @; u2 V" [0 M+ i( B - pBuffer++;- s4 ?7 D" Y+ E4 W2 A5 I' |. g
- }9 w% @ B$ v- u
-
5 X' [. b$ n. {9 D3 X- V& ~ - if (compareErrorDetected != 0)
& p7 D& D2 E/ Y - {+ J Q7 c ~ c1 h) |
- /* Toggle LED1 */
3 C' d7 b$ O+ }/ l% ^ - BSP_LED_Off(LED1);* K" f+ M+ a$ h$ d* A
- compareErrorDetected = 0;
% P4 F. f* v6 l2 w0 a/ H. N. n - BSP_LCD_DisplayStringAtLine(10, (uint8_t *)" Data comparation failed! ");: t2 S: g6 b9 E" X5 h& W1 y
- }
4 u. F% v* n1 ]) L% K, m3 N% \ - else
% n8 Z2 G" ^# T0 C - {4 i; W* P1 w$ O
- /* Turn LED1 on */5 ?) A) r+ |' A( G) H2 k+ y
- BSP_LED_On(LED1);. Y" t: A2 H' H6 t3 ]$ f
- BSP_LCD_DisplayStringAtLine(10, (uint8_t *)" Data comparation success! ");2 a: g$ ~# A8 ~( {- v
- }: l' ?! D/ r& E9 N8 b/ \
+ {; F7 Q! F, j! e/ [+ C; J: x6 }* T- while (1)
$ t9 I$ |4 A9 k: r7 `$ u |. ^ - {
7 Y! s- R7 N% `7 ] M- @" P
, p+ X* e& h9 I# T6 P- }& V! Y. ?/ P; e/ D/ M4 a
- }
w5 R$ E! D* ~" g - V8 l; N, c1 j7 H G; o9 U# h: v, L
- static void MPU_Config(void)# E" S; f3 `0 `4 h! |! F& B
- {
" e. H2 e" p" y7 h a/ r - /* Disable MPU */
4 g, ~! o; o) d0 E: q - MPU->CTRL &= ~MPU_CTRL_ENABLE_Msk;! G1 P( |8 w" H0 r4 X& @! @3 {
- - V1 {) s3 Y/ r6 X5 ?$ a2 } a2 W& N
- /* Configure RAM region as Region N°0, 256kB of size and R/W region */
+ R" M+ y7 p4 w! B - MPU->RNR = SRAM1_REGION_NUMBER;
. @9 M& l% e- ~ - MPU->RBAR = SRAM1_ADDRESS_START;
/ y# B% H! Y' R' a/ h+ t3 q Q& e -
0 d& x7 x8 p+ ?) e7 j- l - /* Write-Back policy */! ^# W8 K: S9 e5 ^+ n
- MPU->RASR = SRAM1_SIZE | MPU_RASR_C_Msk | MPU_RASR_B_Msk | SRAM1_ACCESS_PERMISSION | 1<<MPU_RASR_TEX_Pos;
8 T% j1 z# m$ ?3 v
# _- o* e8 L: g9 S5 q2 {& F- /* Enable MPU */& Z7 o3 G0 p, G3 E
- MPU->CTRL |= MPU_CTRL_PRIVDEFENA_Msk | MPU_CTRL_ENABLE_Msk;9 ]/ s5 y U' T1 }( l
- }
6 @8 {/ y6 Y8 M: E
复制代码 ; J) s4 P& O8 P" g2 f/ F
为了确保 aSRC_Const_Buffer 在 Flash,aDST_Buffer 在 DTCM,我们可以在编译完之后查看 *.map 文件,如下:
) V7 E- e2 S* P8 H: I5 N ?5 y1 j7 b: p: e8 m# \9 ~
) F, x; g* z# J
) B" }# G1 h. u* R: _" [+ l图3.4 检查常量和变量分配情况 & X& A( b9 h6 V* z8 E# ]
4 E5 M# {0 ~0 N下载到 STM32F769I-DISCO 板子上,显然,由于此时开启了 D-Cache,会出现数据不一致的现象,执行结果如下所示:: r2 s# a3 @$ J8 [
3 V \% V# ^8 f' ]$ a2 e9 q
6 q% Z# T/ s$ d5 O& K2 D8 p" G/ q% w3 _" \
图3.5 Cache 数据不一致 % I6 {# C( [- d7 n3 Z' _
9 _9 A' M) ^ T' d& l$ ^. O9 W0 o+ ?! ]
3.3.3 解决方案
" n& p# i/ t6 O- B A% T Z2 D' Q w(1)不启动 D-Cache
9 k" _( k' c( n' f( w* I; J1 o; {/ @- ?- O
注释掉 SCB_EnableDCache();
' V% o1 O: j' S1 G8 W+ p8 x& Q3 X9 }( G' B5 @# p% n
不启动 D-Cache,当然也就没有了 Cache 数据不一致的问题啦~4 Y0 b9 {) C5 C1 Y5 O% _: ]. O+ w
& x) R- i5 G: g' G6 i(2)将 SRAM1 相应区域设置为 shareable
. o3 M4 e& m- u& K) p4 T0 K, n& x: E0 P/ _
通过 MPU 将 SRAM1 相应区域设置为 shareable,MPU_Config() 函数处理如下:
! K* W+ s0 |2 `4 M7 A- static void MPU_Config(void)
/ D* g% e# _. s% j c0 I - {; h% Q' Y3 c' T( w: n! x
- /* Disable MPU */; T' h1 X* R2 ?9 F( S; m% Z
- MPU->CTRL &= ~MPU_CTRL_ENABLE_Msk;
! A B4 r) F8 b/ X* x5 K# q
2 b& I; ]" y9 i. ]2 B5 I* }- /* Configure RAM region as Region N°0, 256kB of size and R/W region */
g7 w% y: B5 U - MPU->RNR = SRAM1_REGION_NUMBER;
3 y! ^: V# `6 O1 K+ q1 Y - MPU->RBAR = SRAM1_ADDRESS_START;' P2 t/ P1 u; |6 ~" r
- : O1 |! U$ c: x9 U
- /* Shareable */* \. u& j6 d# I0 `& ~( C3 V
- MPU->RASR = SRAM1_SIZE | MPU_RASR_S_Msk | SRAM1_ACCESS_PERMISSION;
; l. i7 h G$ v8 ], \4 s% c
$ A7 b" V9 `' N# | Y B- /* Enable MPU */6 z+ j) ~. m5 L* B6 w
- MPU->CTRL |= MPU_CTRL_PRIVDEFENA_Msk | MPU_CTRL_ENABLE_Msk;
! ?; W4 s. a1 E; s - }
复制代码
" g; v q4 V+ _0 I(3)DMA 访问 SRAM1 前先 Clean cache# f5 N& Q' I G' |( F
% }' b6 F0 F! C! ]% ^" N! q
在启动 DMA 访问之前,程序员需要在合适的地方将 D-Cache 数据回写到主内存中,也就是 Clean 的操作。
) [) ?/ K. T; J; w# ?6 I' v( E7 t8 A! H2 B$ c
在本示例中,可以在 DMA_Config(); 前调用:9 N. M7 n U5 `0 I% m9 w, ?
或者. {3 c8 U1 P2 A( }
- SCB_CleanDCache_by_Addr((uint32_t*)SRAM1_ADDRESS_START, sizeof(aSRC_Const_Buffer));
复制代码 0 C1 l1 b. y: b ^4 ^8 ^$ [
(4)将 SRAM1 相应区域设置为 Write-through 策略3 [; M6 J# W* Q: s2 {* V7 ]
4 L, i7 B! b g! X# e4 s' o
通过 MPU 将 SRAM1 相应区域设置为透写模式(Write-through),MPU_Config() 函数处理如下:
?2 v: r% G( T0 M* }9 ^, p
/ [% s- O* k7 |8 F1 v, v1 z& u- static void MPU_Config(void); a/ g# ^; |( L6 [" _2 u5 c; u% a
- {0 @) y7 T% P: W8 v# w; @% O
- /* Disable MPU */8 I* a- R$ F# n) x5 j2 H- \0 T
- MPU->CTRL &= ~MPU_CTRL_ENABLE_Msk;
5 t) }6 m0 d# l5 F6 _& x - $ Y: A8 t: q- R2 P6 F1 d% l' ]
- /* Configure RAM region as Region N°0, 256kB of size and R/W region */7 ] i& u( l u$ u! W, R
- MPU->RNR = SRAM1_REGION_NUMBER;5 a% \9 j1 z# K6 m# w- _
- MPU->RBAR = SRAM1_ADDRESS_START;
; I# A* o% b$ }" U1 w4 q+ B - , z' Q5 D9 i) F" w2 f
- /*Write Through policy*/& g5 X9 Q8 E# V; a1 l
- MPU->RASR = SRAM1_SIZE | MPU_RASR_C_Msk | SRAM1_ACCESS_PERMISSION;
* q9 ]) d/ G/ w5 z) X9 F
. V! B: L( ^' h2 \7 ?* K! ]2 t- /* Enable MPU */- C @( r5 `1 l9 {7 g
- MPU->CTRL |= MPU_CTRL_PRIVDEFENA_Msk | MPU_CTRL_ENABLE_Msk;8 d u* h2 u) f+ y% \
- }
复制代码 7 q7 o) d: B- ~8 @) A6 A/ w
(5)将所有 cacheable 的空间全部强制 Write-though
9 |9 _; G4 E) A/ v, y( f# h$ x, d2 r( F( z( O/ b
通过 cache 控制寄存器,将所有 cacheable 的空间全部强制 Write-though 模式。- o8 K8 w$ }8 ]) x
* T/ `" ~4 W3 h! p! M5 b+ D
2 l/ ?; @1 C* j0 c$ h6 o
; R% B1 G, ^. |; t0 G图3.6 CACR 寄存器(来自 PM0253) - a" a G" C, _0 N0 V
5 f: s7 }8 C4 l: E }在初始化的时候进行设置:
3 E' c) d' u( ?6 C% p: n9 k, K! b$ j
宏定义为:1 v# h8 z: S5 x5 c5 H
- #define __FORCE_WRITE_THROUGH() *(__IO uint32_t *)0xE000EF9C = 1UL<<2
复制代码
% h0 O5 E3 i2 C& }6 q以上这是都是较为常用的方法,在实际的开发过程中,为了提高性能,一般都会开启 cache,同时将其配置为 WB 策略,这就需要开发者在使用时特别小心!
- ~4 A: E0 g1 O- P9 {( ?& Q
4 v; \+ j- e6 h; a1 U. m值得一提的是:对于第二种情况(图3.2),就不是 clean 操作了,而是 invalidate。需要先调用 SCB_InvalidateDCache() 或 SCB_InvalidateDCache_by_Addr() 去 invalidate 相应的 cache-line, 这样当 CPU 在读取时,会忽略 D-cache 中的内容,去真实的物理地址读取对应的数据。
^% \5 { v) ?$ y; |( r7 a5 h" v1 T& {* x8 `+ N8 ^! H( p
* p' a5 d3 w: c( ^1 y" F v7 F: O+ n6 P6 b. O/ T
图3.7 Cache 数据一致 ; G) B9 f5 P7 M. {5 v* ~. M9 D m
, z9 w/ V1 w2 g$ L6 I/ V! t9 A
好啦,通过上述几种方法,就可以解决 cache 数据一致性问题。当然,除了我这里提供的,还有其他方案,各种方案各有利弊,要根据实际应用场景去衡量,这就是嵌入式程序员展示才华的时候啦~
" ~( J0 J5 ~- D: z* ?( g
. ~; z1 {) Q! ^6 {% }6 A' Q! c9 v5 f0 `/ |9 {
8 X% U* {$ _4 p/ l8 J |