1 概述% |% t) T7 s2 i7 S# W
1.1 资源概述) _! f( d w- ]& @# H0 O6 M
开发板:正点原子STM32F103 Nano开发板
9 l; T$ [2 q! d5 dCUBEMX版本:1.3.0
- e5 _ t D* ?1 P5 a: `5 _" |MDK版本:5.231 q8 L% L: f1 N5 Y) B8 t
主控芯片型号:STM32F103RBT6
" ^6 O8 P/ k" D1 t1 _ b; v# c+ } k
# m7 s. Q0 g$ \+ ~/ G5 m" s2 T! r
1 w# c/ Z1 A. N/ f( u x! l1 N9 {. E' k
1.2 代码移植
! ]8 q; h6 {+ c# {8 d3 ]8 J移植armfly安富莱的代码,代码名称为《V4-001_不一样的流水灯(软件定时器、状态机)(V1.0)》,开发板的主控芯片为STM32F103ZE,均属于M3内核芯片,但是ZE的外设资源多很多,总共144个引脚。但是很可惜,他们的开发板基本没有视频教程,不太方便新人学习。但是代码写的是真的好。非常规范和工整。选择这个程序进行移植时由于都是103芯片,时钟相同,外部晶振也是相同的。程序也相对比较简单。% l- f# @6 B/ D0 G0 m9 B- y
4 ^* p/ w) e, l: d
" n5 E8 c* N- W# w L2 ~
1 F$ n" ` F( c# K
1.3 实现功能, C6 i l2 p3 X
这个流水灯程序实现功能如下:3 }5 e4 |1 Q5 w
(1)上电时,LED1点亮,闪烁3次,闪烁频率为精确的1Hz。 — 状态0 (持续3秒)。! y/ ^# p" o- U1 k2 R, K
(2)依次点亮4个LED中的一个, 实现流水灯的效果。— 状态1 (持续5秒)。6 \7 Q6 a( E9 E, S7 W v8 H
(3)依次熄灭4个LED中的一个,实现第2种流水灯效果。 — 状态2 (持续5秒)。
4 \6 Q# k, { F9 I: U. F" b1 x: z(4)(状态0)–>(状态1)–>(状态2 ))–>(状态1)–>(状态2 )…4 L7 z2 B, B8 G# m/ E' |) D6 v( d
这个例子应用1个systick中断实现几个软件定时器,用来控制LED指示灯的闪烁时间。主程序采用了状态机编程方法。' z4 C$ T, }$ B/ r# y0 w9 g7 g A
+ z4 L6 V& k/ ]/ t2 软件实现8 z3 v% ^! {; I/ P* s1 U3 ^; w% Y# E
2.1工程修改
7 ~8 t' f) d$ u. _, t1 R1,修改.s启动文件为startup_stm32f10x_md.s。2 w5 E7 G1 @! r
7 n% x/ T, k- G
' `" a3 s% u2 b7 G7 C" ]3 n/ r
, x* o5 G: Z) x2 G" C& }) p+ U2,修改器件为STM32F103RB。
% Q& k. J! g6 D: F& u3 H
# h8 Q8 F1 N# B# o- ?5 B' ~/ V1 b" t& o4 } M
3,确认内存容量,当从大容量芯片变更为小容量芯片时,如果程序很大,则需要精简程序。
$ g# ?) f( ^( w0 p+ O7 G8 O# w+ K+ v
: X, n6 f- ?4 k8 d( S. R7 m* G5 E7 V$ R
4,修改Define,这里为MD,而不是HD,这里时全局宏定义。
3 E! n0 n# I& y3 B7 o _- Q, t( F
- c9 c3 j: y" x5 B
/ j) G V! A* H: i4 g, p |
9 `: B& ^: x5 O; Q% k/ s- Y8 Z4 f2 a% N5,修改下载器类型,这里选择STLINK
0 T' q& a! X+ s- ^% z
9 E! w. R/ Q O
: P' c* U3 r+ l+ j, o
- m) {9 L& ?# X* u' b; L( g
6,进入下载器设置,确认是否是中等容量flash4 M, [: F6 k- S9 O+ C% W
% Q' [' @* O3 _
4 C9 x+ H" A' P8 M8 M
7 b) F7 K" `2 {1 D7,修改代码外设,不仅仅时GPIO,还有定时器,通讯等,都要改为和目标板对应。如果时跨平台还要修改时钟等。安富莱对应LED灯部分原理图。1 G; | U/ W6 i5 a) z
2 u0 f. [ M* b
1 R, [8 O( s9 q' y" X4 ?, x. N. @8 d& s. y u$ J
安富莱对应程序代码,安富莱将硬件驱动整合成bsp文件,并在bsp文件下细分硬件分支。: B u! ?8 K8 C! h d5 Z9 n% ?
$ x' J, L! N9 e; Y6 g
- g7 z' @2 U- q
) l/ ] V( e( C7 j按照Nano开发板,改为如下
6 k9 l6 k4 ?0 {2 X, H: r3 m$ t' @9 h) J- x
$ f' d4 Z6 y2 M# E# o+ }& h
: W9 D3 k' L1 L# K$ `" i; L# f1 ]$ e8,删除多余外设资源。STM32F103RB只有四个定时器,没有TIM5,删除。1 }' F& i' B: ]! ]- \7 x }. r
j9 s5 Q" y" v' `9 w" }
: W( t, O5 o4 K( b
) P% Q0 N, O# y) i H$ }- b9 v- r9,编译下载,建议改代码时,改一块编译一次,否则错误太多不好查找原因更改。
% e; e) h: k* H' I8 m% W# X- o; |) Y+ I2 s
$ Y/ X( D8 L% F2 p7 I, M3 i. V* Q1 l' x" v9 W
2.2 main函数代码9 ?5 ?8 O+ F' [1 L* W2 |; V
代码非常规整,注释也很到位,非常值得我们学习。在后续研究中,我依据相关的状态机知识,重新改写了状态机部分的函数,定了了状态和事件两个枚举变量,并定义了对应的二维数组,使事件和状态对应起来。另外原来的代码,使用BEEP作为错误处理函数,但是在初始化中并未对BEEP进行声明,因此增加了BEEP的相关初始化,并在第一个状态中,将BEEP的状态与LED1的状态保持相同,实现相同的翻转功能。状态机的状态切换有点类似于RTOS系统中的任务切换,在此代码中,也采用了类似RTOS的时间片的概念,当事件到达时,退出当前的状态,切换到下一个状态。
& r& d" l7 f$ n5 W0 [% `6 N! u8 i- M3 X% ]; Y `
- #include "bsp.h" /* 底层硬件驱动 */
+ e+ c7 h7 [" `* r - 1 n$ _- ]4 A# J" y8 ] z5 c/ b' v
- /* 定义例程名和例程发布日期 */+ b. J. I6 f1 g: ^" E( P7 W
- #define EXAMPLE_NAME "V4-001_不一样的跑马灯(软件定时器、状态机)"
: R0 N, N, c, `# m# S0 f% r - #define EXAMPLE_DATE "2015-08-30"* g: r- R4 X2 t
- #define DEMO_VER "1.0"/ ` p2 s$ K8 t9 }) E% h# b
/ [- @2 S' I R! x* v- static void status_0(void);
9 T5 a; x0 K' \! Y( z - static void status_1(void);( l" R7 n/ U( o% M5 S/ R
- static void status_2(void);7 t1 e6 l7 g8 G
* U1 p' H$ z2 i9 n- #define MAX_STATUS 3: H5 Y3 w' O7 e
- #define MAX_EVENT 3! Q E& s3 P8 Y+ n% k2 ~
9 O$ T d' S1 f/ n% e/ h6 W5 r% _- enum status_led//定义状态* A; h5 ~9 K0 Y
- {
, m: J# g. J! d2 q- I0 N - status0 = 0,
) k; L! _& e- Q# ^! e: ?8 } - status1,
& E+ t% p! |9 a% _6 F - status2
1 A( X1 B% i0 m( [( `2 B - };+ f$ L2 x6 ]+ g, r% C
' n' v8 ]% O4 O) }! S% k- enum event_led//定义事件4 ~7 F* T; b% ^8 ?8 C% o
- {
1 c Y+ _; b3 t0 r - switch0 = 0,
[# z# Q! r7 R! J) I/ S/ p2 a - switch1,# i* i8 J3 f* Y Z( p2 d5 N
- switch2* O" c+ A+ _$ M" w) {+ z/ d
- }; j, y' E/ B- ]7 ] s
1 f) R7 j: I2 |2 H) E# N- int led_flow[MAX_STATUS][MAX_EVENT] = //状态机的二维数组,当前事件+触发事件=下一个状态. ]9 q+ y' }3 P7 O
- {: a& O4 B; m- b6 h2 ^4 ^
- [status0][switch0] = status1,/ V' h h! P# \
- [status1][switch1] = status2,
! F' B) [& F; u4 t9 W. k& x - [status2][switch2] = status1,' H, u/ X; k7 `
- };
2 A' M. }4 k2 ~2 I+ X; p5 D( @. a - * I' u/ p, u5 L- z3 b: k
- int led_next_status(int status,int switchled) //转态转移函数8 g6 }2 O" J0 C
- {+ p9 W' S% e: ]2 `: U
- return led_flow[status][switchled];( W8 B# n) f; n+ I s# S# Q' m
- }/ }8 q) b; Z& ~7 l3 L
- 6 U1 e' ?! j! S6 G3 _+ }
- int main(void)0 S1 p9 B' f! B8 K' x
- {# D7 K. L1 d$ _
- ( @& j; } P# w2 f
- int next_status = status0; //初始 状态
, b2 ^5 |. f5 z- c h/ [ - int event = switch0; /* 初始事件 */
% {9 L6 f; M7 U - 6 | }0 U9 H3 |) |9 _; o
- bsp_Init(); /* 硬件初始化 */" B1 a9 r- X l6 R: X
-
; H) p6 J0 I. `/ E - /* 状态机大循环 */
* a9 n: V) R d. [ - while (1)
+ P) b, v& _: B' @ - {
& k5 V, F' C, O - switch (next_status)
2 c8 ? N; C- M6 Y - {
; M" p1 W( g' T& [* W- m - case status0: /* 上电执行一次。LED1闪烁3次,每次间隔1秒。3次后状态机返回。*/) I9 K, m* B; X1 N. P3 W
- status_0(); 2 X+ b3 I- m- ?; K
- event = switch0; /* 触发新的事件0 */3 F1 I: e5 N/ t. f: ~. k' C
- break;
7 t/ J. _0 S$ m, G - : u( j& K6 `3 q$ n
- case status1: /* LED1 - LED8 依次流水显示。每次点亮1个LED。状态持续5秒后返回。 */
, }2 [, |/ l) p. d' M - status_1(); 7 m- M: b5 c k8 J6 u( A% s
- event = switch1; /* 触发新的事件1 */7 \4 Q: ?" p- {3 q' \* M
- break;
! Z0 |9 n+ m' D% @0 }! o' W - 7 I0 s* `# y% O* j0 f/ E9 J, S
- case status2:
! M" T" D' w8 ^- X+ R+ J, C' c+ H - status_2(); /* LED1 - LED8 依次流水显示。每次点亮3个LED, 熄灭1个。状态持续5秒后返回。*/4 x; _- G: p2 y. V% b4 N
- event = switch2; /* 触发新的事件2 */ O0 [! s2 Q* j
- break;
Q) v: H) R0 H& ] - } " T: ]0 \8 I0 r* E% H
- next_status = led_next_status(next_status,event);//状态转移
! F2 T {5 a' ]% \5 T8 _# t - }
$ ]$ K3 P' y6 Q - }3 V: U3 ~& v! T
2 n; v9 R6 S) q' ]" S& q, i- /*8 v; K- i" A7 q q- r
- *********************************************************************************************************
" d; O+ }8 {, w& B3 P6 o. k - * 函 数 名: status_0
. n# X* D+ ^4 b4 m - * 功能说明: 状态0 上电执行一次。LED1闪烁3次,每次间隔1秒。3次后状态机返回。
h" |, P; V; W - * 形 参:无
5 S- `+ H( y' x4 l! H% f - * 返 回 值: 无! G$ \0 L0 F% k2 d% W5 @
- *********************************************************************************************************
, t# m1 @% V! o; H, F8 h - */, I3 z9 N3 _( P# }7 f& E4 C
- static void status_0(void)) Z7 B B4 @1 o9 g+ w8 O, J! K( J
- {' @' M8 Z% J( a ~
- /* 关闭LED */" U0 j/ Y% ]& O+ d4 G5 _- r3 b: A5 y
- bsp_LedOff(1);, j5 `* M' {8 q0 I B/ l& s
- bsp_LedOff(2);
! C q% d0 B4 O( |' [ - bsp_LedOff(3);5 {* I2 j$ z- g8 c
- bsp_LedOff(4);) Z" [# h# X" N/ a0 @
- bsp_LedOff(5);
3 j* Y6 N+ A. g! b - bsp_LedOff(6);
' I; C4 m7 @. O$ ^# W - bsp_LedOff(7);% ~' l; O2 I6 f7 e, s* v( D& |! G9 r
- bsp_LedOff(8);
( W) A0 Z# `- P - /* 点亮 LED1 */
& K+ n7 {$ j G/ y4 j/ M2 P - bsp_LedOn(1);, N5 H4 \$ P) w, Q- _5 B m( B5 S
- bsp_BeepOn();! {; z. \0 x& u% \
- ! L4 }4 e; X1 v8 n* o2 P
- bsp_StartTimer(0, 3000); /* 定时器0是3000ms 单次定时器 */ ' E5 j+ W% b0 u3 x0 c4 C
- bsp_StartAutoTimer(1, 500); /* 定时器1是500ms 自动重装定时器, 控制LED1按1Hz频率翻转闪烁 */
9 ]4 }( P. t# U9 N - while (1)2 R. ~! ]& r5 _& s) c1 s2 Y
- { " x4 X& H, M7 F5 w* o7 ]
- bsp_Idle(); /* CPU空闲时执行的函数,在 bsp.c */( R$ m* o! }, ?
- 6 @' s( E( R- U9 z. I8 ?
- /* 这个地方可以插入其他任务 */
" B, g8 i8 X1 E1 l8 I3 I' w' p/ i -
' O' e; w. |3 D. o2 R - /* bsp_CheckTimer()检查定时器1时间是否到。函数形参表示软件定时器的ID, 值域0 - 3 */' F; |! t0 }" e; a. e8 U, i
- if (bsp_CheckTimer(1)) # ?4 P6 r4 ?, ] z* P* W: G! J
- {+ a& \1 S7 [9 ]+ f, ]
- bsp_LedToggle(1); /* 间隔500ms 翻转一次 LED1 */
3 s' f) _0 m& g0 t" j' U/ b - bsp_BeepToggle(); /* 间隔500ms 响一次BEEP */
& H) _6 O+ l) P6 L - }
1 ^# B- n: w% q2 x - 8 @0 `+ T; n8 `+ n
- /* 检查定时器0时间是否到 */
. N7 n1 [$ m$ V4 K9 w/ e - if (bsp_CheckTimer(0)); c+ \/ J- A4 C" M# u, K8 _& ~1 g
- {! c( U7 f& l% G4 y ?# t- S( @+ z
- /* 3秒定时到后退出本状态 */0 b5 h. P9 s. |) x
- break;
; a- e, K. C$ I1 c+ O& A- F3 x - }
" U6 A# ^( o+ S, f - }% c" c( x6 _ ^' ~: d3 _" Q7 ?
- 0 k l: `( D; E' o
- /* 任务结束时,应该关闭定时器,因为他们会占用后台的资源 */5 ]; v. \8 `( i( V( Z- t* M) h
- //bsp_StopTimer(0); 单次定时器如果超时到过一次后,可以不必关闭
. Y" T; e7 A" S9 a+ f( } - bsp_StopTimer(1);0 j4 |& ]% W6 e+ d
- }
' h+ q, |; }! R8 Z
8 H% f* A+ f( H6 }! v2 Z- /*
1 S8 g" h1 Y6 T' `; D - *********************************************************************************************************
; I. l5 H0 T. {) a. G; T# B - * 函 数 名: status_1. Q: P, ?3 a: q3 r: Q/ o! x
- * 功能说明: 状态1。 LED1 - LED4 依次流水显示。每次点亮1个LED。状态持续5秒后返回。" Q, h- t2 j4 f
- * 形 参:无. ~0 i5 w9 g6 A0 K# q; u
- * 返 回 值: 无
T( v; C* L5 b! R& P - *********************************************************************************************************
4 w% I0 a6 z6 Y1 b, j; i) u/ c Q3 m - */
0 U, _1 z+ g+ o" y9 Y4 T - static void status_1(void): s5 m$ U/ i$ q7 C/ Z- }8 C
- {
; ^- v% x8 H1 K - uint8_t led_no = 1; /* LED指示灯序号 1-4 */
; g' U" g2 V/ r5 s -
5 O4 K8 w6 q0 \% m6 W - bsp_StartTimer(0, 5000); /* 定时器0是5000ms 单次定时器 */9 H7 Q" d# M3 k0 D. X7 N: [
- bsp_StartAutoTimer(1, 200); /* 定时器1是500ms 自动重装定时器, 控制LED1按1Hz频率翻转闪烁 */
3 B8 H; U( a: A1 @6 U - bsp_LedOn(1); D8 b& K0 S" |" m( m$ K; P
- led_no = 1;
0 }# c) s7 r. C - while (1)# q3 P, Q' [- [5 n4 q
- {
: {4 }4 A$ i4 y: R- J2 k - bsp_Idle(); /* CPU空闲时执行的函数,在 bsp.c */
& p2 T4 I# r. k - 5 Y+ Q0 W' Y+ B4 [1 l
- /* 这个地方可以插入其他任务 */
% U; a& ^0 H3 t( | - 3 i' ]/ q) t4 E' `
- /* 检查定时器0时间是否到 */
! H, w/ Y& X) b2 f; F - if (bsp_CheckTimer(0))
/ @1 R" n% e2 I. L O - {) I1 C# }9 F# N. a" s
- break;& i( Y, u3 e3 A5 K) R% r N9 U
- }
& r) f. J1 t; c; c1 _ - 4 w2 C9 o5 Y8 ^7 x8 V! o
- if (bsp_CheckTimer(1)) /* 检查自动定时器2,间隔200ms翻转一次LED1 */
$ q/ k+ q0 [: l; _( i# ^" D/ M* p - {9 i; e5 X& t0 F/ y! r; r: t
- /* 先关闭所有的LED,然后在打开其中一个 */& L0 S/ X; v4 n5 \6 G6 N
- bsp_LedOff(1);! O: h7 f% R0 T, C
- bsp_LedOff(2);
& R! @7 f% D# z, T N& x7 d I - bsp_LedOff(3);
8 f+ Y, Z$ Z, d2 U - bsp_LedOff(4);
4 b$ b" R7 m$ Z2 O - bsp_LedOff(5);- O/ R! b9 F7 P$ e2 W# A9 G+ b
- bsp_LedOff(6);7 g$ P/ Y+ d) v- b
- bsp_LedOff(7);
$ r+ d3 X* z9 O6 H- \8 c- s# ^1 f - bsp_LedOff(8);
5 O" N1 z% U; h: d: m+ s - bsp_BeepOff();
. i( y/ I5 A" {% O; d; s - if (++led_no == 9); _4 s' z( z, s7 w% `- \
- {
6 o; U# {3 ?$ G i; ~" ~3 r' w$ H - led_no = 1;6 h2 j, v; G6 _( r
- }: g# Y3 e( p' J9 a. a. o! K
- ( k( O- l" N# m" L: Y* M
- bsp_LedOn(led_no); /* 点亮其中一个LED */ 7 Y# s8 l: A7 W' _
- }
' |# v/ _6 Q: k! D - }
) |+ a- G1 x4 A2 C* m4 _, M -
* r- X2 \8 G- `/ C: i: A - /* 任务结束时,应该关闭定时器,因为他们会占用后台的资源 */, m& g) P2 X6 a/ d0 i% m3 @8 r
- //bsp_StopTimer(0); 单次定时器如果超时到过一次后,可以不必关闭7 z8 L, L- w3 z4 f
- bsp_StopTimer(1);' R/ O& u7 N: i( H
- }8 C7 X7 C4 }& S( G2 g. k
7 ? f/ d+ s& K$ I( B# j* R# W- /*1 H3 E' O. [! R& E1 m
- *********************************************************************************************************. E3 I* @3 {* t, L0 S" Y+ o
- * 函 数 名: status_2
7 V9 M' C1 s3 q3 X# D8 T - * 功能说明: 状态2. LED1 - LED4 依次流水显示。每次点亮3个LED, 熄灭1个。状态持续5秒后返回。0 W, `* z5 Y7 L7 Q$ [3 N
- * 形 参:无' I1 k0 y. e G' Z* O3 g
- * 返 回 值: 无
6 N5 o3 p1 c7 s& A7 A. w* m - *********************************************************************************************************" ~' t$ R* {3 w+ J) v1 j* {
- */+ v8 E6 q2 E% Y$ @
- static void status_2(void)" l* a. c6 _9 x
- {* s! V A5 u l* T/ \4 w# P
- uint8_t led_no = 1; /* LED指示灯序号 1-4 */2 I& N9 p; x7 H% w" w* ?! }
-
2 M( F. `% c9 f9 S6 b% C8 T. T) W - bsp_StartTimer(0, 5000); /* 定时器0是5000ms 单次定时器 */9 M/ ]2 i% U" }
- bsp_StartAutoTimer(1, 200); /* 定时器1是500ms 自动重装定时器, 控制LED1按1Hz频率翻转闪烁 */; q1 f) K8 |0 T/ t
- bsp_LedOn(1);5 ?4 _- p; h9 R8 p
- led_no = 1;! `4 [/ d- x9 f
- while (1)
; b8 _% R }3 H - {
: B3 }, i3 z8 [1 O, N - bsp_Idle(); /* CPU空闲时执行的函数,在 bsp.c */
) C* ?! I# B. _1 [ -
5 a9 y# R* ^5 K7 ?# O( j* r - /* 这个地方可以插入其他任务 */
4 c- v1 q. z. o8 d - 1 K) J* p# g2 a5 H+ k! V
- /* 检查定时器0时间是否到 */
6 P2 S1 d' J' a) I - if (bsp_CheckTimer(0))
2 Y; {- n' M9 s: u7 d+ {# m - {& o/ \0 f/ M7 K4 D
- break;
& {/ b# m9 \" m - }. { q4 {* R* o8 T% `9 U1 J
- " Y/ ]- a& R- k6 I) ]# k: x) y
- if (bsp_CheckTimer(1)) /* 检查自动定时器2,间隔200ms翻转一次LED1 */8 J1 m7 H8 k7 b
- {
% U! P% y- r' B$ |# { - /* 先打开所有的LED,然后在关闭其中一个 */
, K% Z, Z1 L# {( M - bsp_LedOn(1);; ?& e8 C( A, r
- bsp_LedOn(2);
- I$ }% t0 N; f+ p/ {% Q - bsp_LedOn(3);5 A' k1 K: K8 a7 H. ^
- bsp_LedOn(4); ; x' [" k8 S; A4 v5 K4 G
- bsp_LedOn(5);0 d/ w$ b0 H+ |8 W
- bsp_LedOn(6);* P8 s M4 [: Z1 S4 @ y6 L0 a
- bsp_LedOn(7);! [/ c6 U" h5 Q6 d
- bsp_LedOn(8); : T5 G* x. A: O N7 }& j) l0 M
- bsp_BeepOff();
; X; a+ h& k8 [1 D. N4 @ - if (++led_no == 9), d% L" P9 M3 H; q9 W
- {
( Z- ~8 L6 l7 C. b* w |+ C - led_no = 1;
/ z+ [1 g5 @% h2 L( i5 _) ~ - }1 y& B5 k- }- H7 C s( K t8 L$ R* m
- " Q7 j$ ?7 R' ^# `+ x
- bsp_LedOff(led_no); /* 点亮其中一个LED */ & f0 y+ |+ K( z2 {5 E7 p& o
- } - ]# A) M% ] ?3 X
- }
6 \, Y6 N; c, w2 E% M6 s, X -
- o Z$ o; N' `, s - /* 任务结束时,应该关闭定时器,因为他们会占用后台的资源 */
; ?. ], \5 }- S! c, z( J- x% e - //bsp_StopTimer(0); 单次定时器如果超时过一次后,可以不必执行stop函数
% s8 l) U; r5 g5 j6 z7 ^( { - bsp_StopTimer(1);
4 ^4 P+ f% `8 K1 _* @9 i) ]3 R- h0 G1 m - }
复制代码 8 B3 A- A- v9 E+ w0 H: n; m
3 实验结果9 e0 [$ Q* k% l- j" M' ]- u* n! [
下载完程序之后,复位运行程序。观察开发板上的LED1-LED4 指示灯的状态。与设计预期一致。后续增加8个灯后,实现了8个灯的流水灯。
# ~4 N1 Y! g( g" e' N
) k* q" |& q4 Z- p( i
$ h+ B/ t9 T7 i! l/ F0 \5 q# C
* K4 h. N% Z2 v+ Z# C' b8 F0 ] K. N! Z) O/ I8 U) S
|