7 v+ S; Q) W* w) L* n, R( b
* b1 e' h! m0 X9 a4 ]" J' X# m& w) U& {; Z
今天来搞一下STM32MP157F-DK2,顺便写一下,我是参考官方的wiki,进行体验使用,并对相关过程做个记录。3 f+ N v2 y( U8 {
先来个全貌展示 + K M: {8 N& F" R
基础准备工作
% U% m6 W& F' z5 M: m系统环境1 T+ e. c/ x$ v0 U6 v8 v8 a i
下载安装Ubuntu Desktop(这里使用版本为20.04.4):https://ubuntu.com/download/desktop
' ^) t. k: K4 O; f. Q9 n! J/ ~$ Y' L- k% V8 `' W; k4 p! U7 x8 P
( B3 I$ |, F+ p1 u) f" S$ x安装完成后进行基础环境安装与设置:
0 v z* |5 e9 u! t- sudo apt update4 X% n5 i, r) v% N
- sudo apt install -y build-essential
复制代码 以下为应用与内核开发需要的环境安装与设置:
4 T& A7 U1 g- `- sudo apt install -y gawk git-core diffstat texinfo gcc-multilib chrpath socat cpio python3-pip python3-pexpect xz-utils debianutils iputils-ping python3-git python3-jinja2 libegl1-mesa libsdl1.2-dev pylint3 pylint xterm; k, V" g$ |1 G( w* W
- sudo apt install -y xsltproc docbook-utils fop dblatex xmlto
/ F- }" `& h _" W+ q7 K; n6 Q - sudo apt install -y libmpc-dev libgmp-dev
/ @ P- S+ _" T4 g) D - sudo apt install -y libncurses5 libncurses5-dev libncursesw5-dev libssl-dev linux-headers-generic u-boot-tools device-tree-compiler bison flex libyaml-dev libmpc-dev libgmp-dev
$ d; y+ k; A2 p" h" f - sudo apt install -y python-is-python3, h- h* Z3 h0 W, R* B8 @0 c& f
- ]% k: e( C" Q! y7 w) q0 a6 ~
- # 设置使MMC支持分为16个分区
9 ^0 d7 }0 a1 U' f7 J! K - echo 'options mmc_block perdev_minors=16' > /tmp/mmc_block.conf
7 B/ X3 |0 E# G9 \ - sudo mv /tmp/mmc_block.conf /etc/modprobe.d/mmc_block.conf
. T; z, a5 g1 y" M4 L8 V6 |; ?+ n
复制代码
1 i, t) h: u" V X2 I硬件连接1 A0 f( S# w9 w5 h) f Z
硬件连接如下图所示:
8 y4 v, J' U4 M2 t+ Q
/ ~- Y6 D1 T' }# m+ r
特别需要注意的是使用的电源(5V3A)、供电的USB线、通讯的USB线品质不能太差,不然可能系统镜像烧录过程中会出错。
3 B* i$ V$ u. O3 {2 i! l8 F6 C7 E) ^, v' S+ I% e
- y+ M7 ^1 F; \8 M3 D! v1 X) x
系统镜像烧录测试
1 t3 z: T$ U2 v" O" w2 t! g% lST官方提供了测试用的系统镜像,使用ST的烧录工具可以烧录镜像到存储器中。烧录工具有Linux、Windows、Mac版本的,选择合适的平台进行即可。: l4 h5 [2 Z1 e4 n5 S
0 t; ], z; E* @3 Q4 v2 m
! f! @1 \/ m0 R" F" _+ k/ y烧录是通过 USB OTG 那个口进行的(DFU模式)。需要注意的是烧录过程中设备会断开重新连接,如果使用虚拟机的话需要把这个USB设备设置为自动转到虚拟机或是在重新连接的过程中手动转到虚拟机。
. h. W/ k% R. \
0 @( V6 A b- @5 t, o" _% A9 Y: W* W
下面是Linux下系统镜像烧录测试过程:
: N: R: ^- G8 M. m# @
: \/ a$ b- B( Z0 m' x" y1 a4 y% M/ {, z5 s2 B* Z
STM32CubeProgrammer安装
2 J0 j; j" p2 {3 s* iSTM32CubeProgrammer用来将系统二进制文件烧录到TF卡或eMMC中,可以从下面选择合适的版本下载:
2 q! F4 h. p/ [- p9 Yhttps://www.st.com/en/development-tools/stm32cubeprog.html# j1 L# ?/ Q- s; i7 Z; |
我这里通过Ubuntu自带的浏览器下载,得到的 en.stm32cubeprg-lin_v2-10-0.zip 文件位于 ~/Downloads/ 目录下。" H0 e/ T+ c+ o% H `0 J
2 y& h1 G% L; w V3 d7 v1 _% h9 m9 y0 U/ z, D: ^
接下来进行安装:
" d& ?9 N( M2 u9 U2 X3 n- # 建立STM32CubeProgrammer安装目录" y* K; r6 G3 K( H# b# l" [
- mkdir -p ~/mp157/tools/prog P0 c0 h5 A, M8 Q* Z2 j& Q3 f
- # _. s1 ^1 l8 h, ~8 ~" Y
- # 解压与安装
}0 Z0 u- g* K' ?9 Y \/ w - cd ~/Downloads// l9 V" [" u$ \) h$ u/ Z! X
- unzip en.stm32cubeprg-lin_v2-10-0.zip
2 U W) W& ]' a/ a S - ./SetupSTM32CubeProgrammer-2.10.0.linux/ G9 D3 t. \+ M: E
- # 安装时选择上面建立的安装目录,其它默认即可
N9 b. Y: ^3 G1 g$ V1 v; L
复制代码 允许USB相关功能:
' ]- \4 ]- e l- # sudo apt install libusb-1.0-03 X; Y$ N% |* j0 M- Y, a) Y- ~! F
- cd ~/mp157/tools/prog/Drivers/rules/
' a: ~- T& d h - sudo cp *.* /etc/udev/rules.d/
复制代码 导出到环境变量方便使用:
+ D; F }# ~( d1 P+ a5 p2 A( T- # 下面方式是临时的,每次打开终端都需要重新设置6 k P! b5 y1 S# d+ Z9 {% A
- export PATH=$HOME/mp157/tools/prog/bin:$PATH( l. B& j4 D% R8 R
- # 设置成功的话使用 STM32_Programmer_CLI --h 可以看到软件信息
复制代码 系统镜像烧录, |# p' I4 W: @- B5 Y
从下面地址下载ST官方提供的系统镜像:
4 f% O. C8 i* t$ @$ ~3 Ghttps://www.st.com/content/st_com/en/products/embedded-software/mcu-mpu-embedded-software/stm32-embedded-software/stm32-mpu-openstlinux-distribution/stm32mp1starter.html
! N; P* Z. J7 |- Y1 o! X我这里通过Ubuntu自带的浏览器下载,得到的 en.FLASH-stm32mp1-openstlinux-5-10-dunfell-mp1-21-11-17_tar.xz 文件位于 ~/Downloads/ 目录下。
5 b) e( I4 |5 Q8 f: G4 R- mkdir -p ~/mp157/ecosystem/starter
) f1 ~: I4 M k3 k - cd ~/mp157/ecosystem/starter; J' g) {, \2 z; A" E* h: ^, R, f) j
- , v+ j9 V9 T" r. \4 l$ k8 o
- mv ~/Downloads/en.FLASH-stm32mp1-openstlinux-5-10-dunfell-mp1-21-11-17_tar.xz ./. \& ~8 R, D4 x7 a% Z
- tar xvf en.FLASH-stm32mp1-openstlinux-5-10-dunfell-mp1-21-11-17_tar.xz
! Q- w+ ?* U( d6 a8 L5 @, O - # 官方的系统固件与烧录配置等都在解压得到的目录中5 w: z$ C- x4 i) N0 ~* t9 i
复制代码 烧录前将开发板背面的两个拨码开关都拨到OFF位置,然后按正面的RESET按钮进行复位。接着启动烧录工具:. U& C4 g2 |3 k
连接设备,打开下面目录中对应的烧录配置文件(.tsv):6 K4 Z. u8 P; I, @
~/mp157/ecosystem/starter/stm32mp1-openstlinux-5.10-dunfell-mp1-21-11-17/images/stm32mp1/flashlayout_st-image-weston/
- W5 N% z" i. A6 {然后二进制文件路径选择下面这个:, J9 R* C$ |4 _7 M" P1 x
~/mp157/ecosystem/starter/stm32mp1-openstlinux-5.10-dunfell-mp1-21-11-17/images/stm32mp1/
/ W d2 U7 S, E+ C& \; P1 m最后点击 Download 进行烧录即可:7 K+ e5 V& h6 U0 F) b; q" p
1 x0 r1 ?* E$ S5 p烧录过程比较漫长,请耐心等待。
9 G( s8 k( F+ q3 D F1 `- J+ P F( F3 r# ^& x# e. i3 ^& E( k
0 A3 K6 x P, H, B' j烧录完成后将开发板背面的两个拨码开关都拨回ON位置,然后按正面的RESET按钮进行复位。
1 }2 ?- _2 x3 n" v4 z! z& [% V系统将正常启动显示开机画面,初次启动时接着将黑屏几分钟进行初始化,完成后会在屏幕上显示文章开头图片中画面。9 j8 q+ Q/ }/ k3 q5 N7 p
& _3 P1 K" Z* F5 v0 J3 x+ X# y/ w0 Q$ g; j M
如果STLink那个USB口使用数据线连接到电脑的话会在电脑上有一个串口,使用终端工具进行连接的话会在这里打印出启动日志。也可以通过这里与系统进行交互:
2 T ^- Y. l, m/ _' }
2 s; _& Y# N/ A+ h+ G" m
; O. m X. r6 B( H, v
" k/ N) G! c$ o; N- N% |: x: A
应用与内核开发
$ i# p2 g. N/ y4 e$ V) s$ U' qSDK安装8 I+ _! X* P' \, v" q* c1 _
从下面地址下载ST官方提供的SDK(SDK中包含编译工具链和库文件):
/ Y! n7 m- Y' S q% j0 k! {6 Xhttps://www.st.com/content/st_com/en/products/embedded-software/mcu-mpu-embedded-software/stm32-embedded-software/stm32-mpu-openstlinux-distribution/stm32mp1dev.html* `! M: _: c* F. b0 r6 e9 F
我这里通过Ubuntu自带的浏览器下载,得到的 en.SDK-x86_64-stm32mp1-openstlinux-5.10-dunfell-mp1-21-11-17.tar.xz 文件位于 ~/Downloads/ 目录下。
4 Q5 v' T9 D9 x: ~- cd ~/Downloads/
3 P7 k* ^/ {& _7 t - tar xvf en.SDK-x86_64-stm32mp1-openstlinux-5.10-dunfell-mp1-21-11-17.tar.xz4 e: h% y. T" B( w7 w$ I6 R, ?
- 1 N/ X3 A J! ?' ]
- # 建立SDK安装目录
" T7 }% i$ C9 s) l* g - mkdir -p ~/mp157/ecosystem/developer/SDK
z T7 l! Y: p Q( l9 _1 s! k - # 执行脚本安装SDK- m) u4 w( O2 q, J, L
- chmod +x stm32mp1-openstlinux-5.10-dunfell-mp1-21-11-17/sdk/st-image-weston-openstlinux-weston-stm32mp1-x86_64-toolchain-3.1.11-openstlinux-5.10-dunfell-mp1-21-11-17.sh" e# r: `8 h8 y# i
- ./stm32mp1-openstlinux-5.10-dunfell-mp1-21-11-17/sdk/st-image-weston-openstlinux-weston-stm32mp1-x86_64-toolchain-3.1.11-openstlinux-5.10-dunfell-mp1-21-11-17.sh -d $HOME/mp157/ecosystem/developer/SDK
3 M' y2 A/ v/ N8 Z: n; h - - ?' q3 p8 ^% D; W& q
- # 设置SDK
7 ~( P2 M% f$ \# V6 k/ v& X - # 下面方式是临时的,每次打开终端都需要重新设置
) x! J% y% c8 I+ _8 i1 S7 n- ` - cd ~/mp157/ecosystem/developer/
, ^6 C9 O& M u5 N - source SDK/environment-setup-cortexa7t2hf-neon-vfpv4-ostl-linux-gnueabi
. g& v t, D" G: G1 J6 p1 [ - # echo $ARCH
! p H" O0 k9 n9 c - # echo $CROSS_COMPILE
* H. X; z# G. A7 T- K0 \ - # $CC --version+ j7 b8 V* W5 o7 Y. X2 x
- # echo $OECORE_SDK_VERSION
* C* @* n( B+ |. K" S/ t- p
复制代码 应用开发( x1 K0 T! A6 H) g
- # 建立并进入工程目录1 b- j& A- o! ]9 M6 n( L
- mkdir -p ~/mp157/ecosystem/developer/application/gtk_hello_world_example
# Z8 V) @1 \- T. w* h - cd ~/mp157/ecosystem/developer/application/gtk_hello_world_example/9 N& U+ N/ N8 r7 u" Z3 G9 K
复制代码 建立 gtk_hello_world.c 文件(gedit gtk_hello_world.c),内容如下:
2 p& L* w+ `& w1 i: t- #include <gtk/gtk.h>8 E2 }8 y. D$ a4 t, B/ j
- 6 S$ O. g, S! U2 u4 E% H+ }
- static void3 `) i- |2 U8 Q$ O
- print_hello (GtkWidget *widget,
! a5 a- @5 q* O9 h4 z; O - gpointer data)3 I7 B0 M/ x. J0 j7 g5 p* B
- {% m5 I$ ?2 ]) V/ X) t" g0 Q
- g_print ("Hello World\n");9 M6 A' b4 ` [
- }; ]+ Z: |/ ]% }
o+ N: q' n% H- D- static void
u- e/ c% N! {. P- q# Q1 I - activate (GtkApplication *app,
3 @; z k7 b& o& E - gpointer user_data)6 C7 t, p. I2 o" s
- {* Z( B$ B4 L: @5 Q5 o7 W
- GtkWidget *window;
$ w# d- N: A4 b7 r9 h - GtkWidget *button;5 T& ~! D2 F5 s$ [8 l4 M* Q! H
- GtkWidget *button_box;
+ c8 Z' n! K4 v0 D - ! ]6 I+ {- H) H/ q7 b
- window = gtk_application_window_new (app);+ b$ P1 M1 S# B7 }- W
- gtk_window_set_title (GTK_WINDOW (window), "Window");7 z N5 ~% X2 Z8 V/ m+ I
- gtk_window_set_default_size (GTK_WINDOW (window), 200, 200);# V C% }% J4 S$ D' a) Z( |
- ; R2 S, V) B) Q
- button_box = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL);1 N6 A1 K$ O# g. v3 b
- gtk_container_add (GTK_CONTAINER (window), button_box);' N4 O+ ?) E1 y
% |: Q. Y' W( m2 {' g- button = gtk_button_new_with_label ("Hello World");4 [$ B* F1 P) E. v @- _- c+ I
- g_signal_connect (button, "clicked", G_CALLBACK (print_hello), NULL);' A' |& g" p1 W; C' m
- g_signal_connect_swapped (button, "clicked", G_CALLBACK (gtk_widget_destroy), window);- |" G. T4 C1 d! F2 I0 V
- gtk_container_add (GTK_CONTAINER (button_box), button);
: U) k8 z! P, g/ G
6 G/ m8 C g" n4 V* O- gtk_widget_show_all (window);% l9 Y5 x% b/ q, B% X
- }3 K% v) k! e0 a7 D [# R
- ; a- k$ p: H* t
- int
Y! X H8 B- S - main (int argc,
2 A3 b+ ?6 ?4 u( A - char **argv)
$ h) s: O: C; D& G; F - {
( m* E4 F2 r& L5 \1 [) K$ l+ A - GtkApplication *app;
3 U0 M' A9 N8 ]3 Z) m }+ p7 n - int status;
' Y8 S5 s6 k! l
$ q' H6 u% M+ A& ?2 ~! j" R( K* E% d- app = gtk_application_new ("org.gtk.example", G_APPLICATION_FLAGS_NONE);2 g# y2 { q8 f: Y1 {4 [
- g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
2 ]0 b; F T6 \ p - status = g_application_run (G_APPLICATION (app), argc, argv);7 J7 v O9 v3 O1 J1 b2 U, y: {
- g_object_unref (app);# {! M3 j% O9 u s# | f; J# d) P
- H% i0 o" E; h, v0 H4 x: k
- return status;4 X- m% T" E9 C5 C& w' u F
- }9 l( b. D& y. h4 v3 b; H! Z! W; v; @
复制代码 建立 Makefile 文件(gedit Makefile),内容如下:
9 v! U4 G2 U$ N. B3 A9 Y- PROG = gtk_hello_world: C2 d) } I6 p
- SRCS = gtk_hello_world.c& U7 r0 R6 B8 F/ _! w
' e ]0 w( S5 o1 S& r( A- CLEANFILES = $(PROG)
- H9 H0 r; h5 Z3 o9 { - - z. M+ p7 a6 }- W
- # Add / change option in CFLAGS and LDFLAGS$ H% O8 U" s* |2 Q
- CFLAGS += -Wall $(shell pkg-config --cflags gtk+-3.0)
+ }: c5 j L4 d( f - LDFLAGS += $(shell pkg-config --libs gtk+-3.0)
_+ `6 N0 {; S- L! l/ y
1 z9 V- F+ Q, w+ ~ Y- all: $(PROG)
; C- @: X- L; k; D8 G
! J: w! d) H0 T- $(PROG): $(SRCS): }( B I" y4 b J
- $(CC) -o $@ $^ $(CFLAGS) $(LDFLAGS)
8 `8 J% P. I9 {$ S1 r# e - + {& I5 b" D: z; J/ e
- clean:
8 }( g& c g+ H) q9 s9 p - rm -f $(CLEANFILES) $(patsubst %.c,%.o, $(SRCS))
9 E) @0 k( x, r8 W w+ Q, p
复制代码 使用 make 进行编译。编译得到的二进制文件可以通过网络传输到开发板中(比如使用 scp gtk_hello_world root@<board ip address>:/usr/local 方式),然后在开发板中运行(比如通过串口终端来运行)。运行运行上面程序会在开发板的屏幕上出现一个带有按钮的窗体:( P+ D ?3 t) F2 M! Z7 g6 O- V! p8 W
" o3 E& F" y' ^" x8 O* o% m
内核编译
6 _! R- I6 G/ j" D) |; W从下面地址下载ST官方提供的Linux内核源码开发包:开发包+ ?- [7 {3 U* s
, B& B! |+ s, R; K( L8 f
我这里通过Ubuntu自带的浏览器下载,得到的 en.SOURCES-kernel-stm32mp1-openstlinux-5.10-dunfell-mp1-21-11-17.tar.xz 文件位于 ~/Downloads/ 目录下。5 K6 f0 F2 K! O( q# N
- # 解压开发包内容
, a' v+ f$ Q$ r - cd ~/mp157/ecosystem/developer/
* C6 H, a) a, {% w6 l - cp ~/Downloads/en.SOURCES-kernel-stm32mp1-openstlinux-5.10-dunfell-mp1-21-11-17.tar.xz ./
- e5 {+ k! n6 ^# Z/ f7 n+ | - tar xvf en.SOURCES-kernel-stm32mp1-openstlinux-5.10-dunfell-mp1-21-11-17.tar.xz 6 n9 v! s% u6 |9 T2 M1 E
- |9 |6 P. A% D2 I6 q! C6 n
- # 解压内核,打上MP157补丁
4 G- T+ u! F/ A: g) E* [ - cd stm32mp1-openstlinux-5.10-dunfell-mp1-21-11-17/sources/arm-ostl-linux-gnueabi/linux-stm32mp-5.10.61-stm32mp-r2-r0/
1 X' T1 T6 L% u* Z% I# x" T - tar xvf linux-5.10.61.tar.xz$ t* J( d$ M$ W+ w4 h
- cd linux-5.10.614 G' F; e8 Q& f& S- A
- for p in `ls -1 ../*.patch`; do patch -p1 < $p; done D1 b+ _0 T/ R" B5 B
复制代码- # source $HOME/mp157/ecosystem/developer/SDK/environment-setup-cortexa7t2hf-neon-vfpv4-ostl-linux-gnueabi1 z9 T# g2 u u' R" L- [2 ^
- # 进行相关设置
9 y- l8 ^2 W9 D3 N# L' ~ - make ARCH=arm multi_v7_defconfig "fragment*.config"7 l6 z4 f; Y. Y4 c# R, T; n. e
- for f in `ls -1 ../fragment*.config`; do scripts/kconfig/merge_config.sh -m -r .config $f; done, Q' E! n1 f9 J
- yes '' | make ARCH=arm oldconfig/ ?6 |! {1 @2 A$ @- [
- 8 S' |0 G: V4 s; B5 S' j/ q
- # 编译内核和设备树
8 o# A3 r* D4 M' y2 e: M2 f - make ARCH=arm uImage vmlinux dtbs LOADADDR=0xC2000040
* z% j. H9 S1 o m6 P, W7 U/ k
& ] {" S' U- B3 ^- V- # 编译内核模块/ N( F9 V& w+ A5 y% E3 e$ m, \
- make ARCH=arm modules0 }( h* u5 Q: L" L3 Z# q& {8 C0 m, g/ n
- ( {; l, K( q# ^7 @) G
- mkdir -p $PWD/install_artifact/
/ R( o( }& b( f" n3 y! F7 i; f - make ARCH=arm INSTALL_MOD_PATH="$PWD/install_artifact" modules_install$ T, I; S/ v% Z# f- N! N
复制代码 编译完成后后可以使用网络将生成的内容更新到开发板上:' }+ [3 h% r8 U$ x* N8 \
- # 更新内核
; U/ q2 _9 o0 x/ u# e - # scp arch/arm/boot/uImage root@<board ip address>:/boot
+ }5 `$ L/ u' e
* i; N) A0 ] ~ m1 x- # 更新设备树! h; P/ k* U2 r. P$ J: Y% J9 R
- # scp arch/arm/boot/dts/stm32mp157*.dtb root@<board ip address>:/boot
) V$ g6 X; |: \" z" U% o1 P
: d$ j( I7 r t( A! m% u3 M( p- # 更新内核模块( h6 s! Y! O3 o: a+ v7 D1 J. X
- # rm install_artifact/lib/modules/5.10.61/build install_artifact/lib/modules/5.10.61/source
7 T$ r; n; ?+ m+ \1 T& ~8 ] - # find install_artifact/ -name "*.ko" | xargs $STRIP --strip-debug --remove-section=.comment --remove-section=.note --preserve-dates5 w$ p) g" l" b& Y0 M+ |9 Z
- # scp -r install_artifact/lib/modules/* root@<ip of board>:/lib/modules3 ]6 g1 {2 ~" W% G6 D
复制代码 以下操作在开发板终端中进行:
( m1 l, T6 o$ e" e) L+ I- # /sbin/depmod -a p- b; V3 O# W2 W8 o- j
- # sync
% S% k0 Q- N8 e
% I% R+ v& [+ C+ y, B: @- # 重启使所有更改生效 p, G" o% x, l! @) |2 \( a
- # reboot
复制代码 内核修改测试$ `# r- \5 {! k( K& a4 A7 {
- # cd $HOME/mp157/ecosystem/developer/stm32mp1-openstlinux-5.10-dunfell-mp1-21-11-17/sources/arm-ostl-linux-gnueabi/linux-stm32mp-5.10.61-stm32mp-r2-r0/linux-5.10.61/
) C% ]: O2 M8 h& L, C2 D
6 [" v; R( ?: f% z! `% T; P$ W% ~2 R- # 打开内核一个源码进行修改; y/ F1 j8 d( ^! e( s" e8 A
- gedit ./drivers/gpu/drm/stm/drv.c
% C4 O, C E$ T
复制代码 在打开的文件中添加红框中一条语句:" K. g6 a, O; O! Y6 I7 T/ x
; v! V4 ]) Z/ V7 j) ]0 Y; S$ f修改完成后保存推出,然后重新编译内核:
7 v9 p. | m y. ~5 Y4 G- make uImage LOADADDR=0xC20000405 n# F1 F" U6 V8 ~, h R
+ S$ [' q) B9 G% R$ [ {- # 编译完成后重新传输内核到开发板; v: D9 d( S1 Z
- scp arch/arm/boot/uImage root@<board ip address>:/boot
复制代码 以下操作在开发板终端中进行:
% B$ h' v/ s8 \; C2 ]5 k& s7 J- reboot' H' F0 [5 q$ x3 ^7 L; m5 d
- # 等待重启完成查看刚才修改的打印信息' p% K+ R$ @: m* A4 C$ h; }, m
- dmesg | grep -i stm_drm_platform_probe
复制代码
! D5 [7 c6 A& R0 Q, [
' L$ s$ F% z% z( | }7 ?* F9 s
5 u# B* P2 L, b4 R0 l) t' I |