|
1 CAN发送邮箱 STM32共有三个CAN发送邮箱,在检测到总线空闲时交发送,但需要注意的是,有可能会发送失败,有可能因为仲裁失败从而导致失败,也有可能是其它错误,原则上bxCAN将自动重发,但bxCAN也可以配置不自动重发。正因为如此,发送邮箱中有可能同时存在多个需要发送的报文,一旦出现这种情况,那么发送邮箱中的多个报文又将是谁先发送谁后发送呢?有两种模式:ID模式和FIFO模式。ID模式由报文的ID值决定,即ID值越小,优先级越高,另一种FIFO模式,顾名思义,即为消息队列方式,谁先到谁先发送,此种模式下三个邮箱与接收FIFO类似。 $ B' M) C* E7 [1 P
下图是发送邮箱的状态图: % {" x: r: _# D3 E4 N7 N

图1 5 U: }- g' M! q; G/ [9 |7 [
由上图可知,发送邮箱共有四种状态,空状态,挂号状态,预定发送状态(scheduled),发送状态。 发送报文的流程为:应用程序选择1个空发送邮箱;设置标识符,数据长度和待发送数据;然后对CAN_TIxR寄存器的TXRQ位置’1’,来请求发送。TXRQ位置’1’后,邮箱就不再是空邮箱;而一旦邮箱不再为空,软件对邮箱寄存器就不再有写的权限。TXRQ位置1后,邮箱马上进入挂号状态,并等待成为最高优先级的邮箱,参见发送优先级。一旦邮箱成为最高优先级的邮箱,其状态就变为预定发送状态。一旦CAN总线进入空闲状态,预定发送邮箱中的报文就马上被发送(进入发送状态)。一旦邮箱中的报文被成功发送后,它马上变为空邮箱;硬件相应地对CAN_TSR寄存器的RQCP和TXOK位置1,来表明一次成功发送。9 u0 n- F4 L. g* n
如果发送失败,由于仲裁引起的就对CAN_TSR寄存器的ALST位置’1’,由于发送错误引起的就对TERR位置’1’。 + l$ V" }$ S2 o$ G' `6 S
) E, K, E9 `# v+ \! Y( {9 d2 发送优先级 如之前所述,如果三个邮箱中同时存在多个待发送的报文时,此时存在一个问题,即先送哪个邮箱中的报文好呢?此时,存在一个发送优先级的问题。此时,非空发送邮箱进入发送仲裁,发送仲裁有两种策略:ID模式和FIFO模式。 - ID模式:当有超过1个发送邮箱在挂号时,发送顺序由邮箱中报文的标识符决定。根据CAN协议,标识符数值最低的报文具有最高的优先级。如果标识符的值相等,那么邮箱号小的报文先被发送。此模式通过对CAN主控寄存器CAN_MCR的TXFP位清0来设置。
$ i( [" L1 }' m* s# b
& [$ F. q9 `8 v$ @' k
- FIFO模式:通过对CAN_MCR寄存器(CAN主控寄存器)的TXFP位置’1’,可以把发送邮箱配置为发送FIFO。在该模式下,发送的优先级由发送请求次序决定。该模式对分段发送很有用。
' s# A2 D5 K% f' y, @. H' }
$ z# Y% I3 v" j6 B. v, C$ u% J0 m* r ]" }6 B( Y r. O3 [
& J" j- p' d9 H
3 取消发送发送邮箱中待发送的报文在正常发送成功之前也可以中途取消,通过对CAN_TSR寄存器的ABRQ位置’1’,可以中止发送请求。 - 当发送邮箱处于挂号或预定状态时:发送请求马上就被中止了。
- 当发送邮箱处于发送状态时:
+ ]7 s$ D" z$ v7 d' y
3 y+ W5 n/ q3 @; ?' U 那么中止请求可能导致2种结果: 1:如果邮箱中的报文被成功发送,那么邮箱变为空邮箱,并且CAN_TSR寄存器(CAN发送状态寄存器)的TXOK位被硬件置’1’; 2:如果邮箱中的报文发送失败了,那么邮箱变为预定状态,然后发送请求被中止,邮箱变为空邮箱且TXOK位被硬件清’0’。 因此,不管如何,一旦取消发送,那么在发送操作结束后,邮箱都会变为空邮箱。 固件库中取消发送的接口为: ) p6 Y& k8 t: K7 O4 E3 ^
- - }- p9 v. z: u
- /**
9 C( |$ x5 o/ `% }! }8 y* y! P1 H - * @brief Cancels a transmit request. ?* G' B, X0 E* P
- * @param CANx: where x can be 1 or 2 to select the CAN peripheral.
$ y/ V& `8 ]1 f X! L - * @param Mailbox: Mailbox number.
' R4 |) |5 C& S" M8 N9 U - * @retval None
' ^$ u4 P, v* ]9 x - */
' E6 ?- ^0 |& s( }4 V - void CAN_CancelTransmit(CAN_TypeDef* CANx, uint8_t Mailbox);
4 f4 E" r$ D0 }) N% ^0 R" u( f
复制代码 T* v( N. _/ I9 v% J; I/ I
?2 u. K, t! J( y$ a3 h4 自动重传模式
/ `6 ~* Y1 c" x2 l该模式主要用于满足CAN标准中,时间触发通信选项的需求。通过对CAN_MCR寄存器的NART位置’1’,来让硬件工作在该模式(禁止自动重传)。
) i) Z# z+ T) r% v7 \; `9 Z- X在该模式下,发送操作只会执行一次。如果发送操作失败了,不管是由于仲裁丢失或出错,硬件都不会再自动发送该报文。
8 Q3 [4 Z# T4 A在一次发送操作结束后,硬件认为发送请求已经完成,从而对CAN_TSR寄存器的RQCP位置’1’,同时发送的结果反映在TXOK、ALST和TERR位上。& }' }% Q8 w4 R4 a
% s- a3 L$ F3 H+ S( k5 B- Z 5 发送邮箱的组成8 A1 h2 B/ k- S% Q1 z& X
说了那么多,那个三个发送邮箱的结构到底是怎么样的呢?与接收FIFO的邮箱类似,发送邮箱也是由四个寄存器组成:发送邮箱标识符寄存器(CAN_TIxR x=0..2),发送邮箱长度和时间戳寄存器(CAN_TDTxR x=0..2),发送邮箱低字节数据寄存器(CAN_TDLxR x=0..2),发送邮箱高字节寄存器(CAN_TDHxR x=0..2)。
+ t- Y6 Q k' Z 5.1 发送邮箱标识符寄存器 (CAN_TIxR) (x=0..2)3 L4 c7 A3 u2 Y4 c& q- w
地址偏移量: 0x180,0x190,0x1A0
! \# S% a) K" {) X8 x- u复位值: 0xXXXX XXXX,X=未定义位(除了第0位,复位时TXRQ=0)
% Q* s" E( Y) Y: w
注: 1 当其所属的邮箱处于等待发送的状态时,该寄存器是写保护的。 2 该寄存器实现了发送请求控制功能(第0位)-复位值为0 1 N& i# [3 J& B, B, D& Y& L& I

图2
* {0 O2 ]4 C2 U! E i
与接收FIFO的邮箱的发送邮箱标识符寄存器类似,各位定义如下:
) h L3 {' w! u4 I| 位31:21 | STID[10:0]: 标准标识符: F3 J2 W( g! S! r* @7 u- e
扩展身份标识的高字节。 | | 位20:3 | EXID[17:0]: 扩展标识符
! ?" W8 z1 G. P6 e4 C5 A: G" J+ a- a3 |扩展身份标识的低字节。 | | 位2 | IDE: 标识符选择' _# N% |+ n' K! F/ F$ a3 ~
该位决定发送邮箱中报文使用的标识符类型
* X+ w, I% g% o* y0: 使用标准标识符;
1 }" o+ e2 E0 k5 S$ N1: 使用扩展标识符。 | | 位1 | RTR: 远程发送请求 F0 F( K# G) J* v: F7 k. C# o* m
0: 数据帧;7 w, r% [2 \- J# l
1: 远程帧。 | | 位0 | TXRQ: 发送数据请求
6 \8 t* U6 O( d! p由软件对其置1,来请求发送其邮箱的数据。当数据发送完成,邮箱为空时,硬件对其清0。 | ) ?0 w5 V7 _) |) ~( U
) |4 Y; V) P6 c" m; N" ^5 V5.2 发送邮箱数据长度和时间戳寄存器 (CAN_TDTxR) (x=0..2)
( E1 a% l. y, L% v地址偏移量:0x184,0x194,0x1A4
: u: ]" L: b9 w4 N+ j$ b* h1 {" ~: L复位值:未定义( C: ]% u$ q2 h9 L+ h( m

图3
% I5 a3 D* M! C* o5 ?0 |% R4 R; u L, k% Z; m. J* E
| 位31:16 | TIME[15:0]: 报文时间戳
7 N2 J. y) c; O4 _/ y) B; X该域包含了,在发送该报文SOF的时刻,16位定时器的值。 | | 位15:9 | 保留位 | | 位8 | TGT: 发送时间戳0 Q7 @% f! Q' }+ I! s! {
只有在CAN处于时间触发通信模式,即CAN_MCR寄存器的TTCM位为1时,该位才有效。2 S6 v: N5 r5 o$ j8 T
0: 不发送时间戳;- B4 }3 L( m& Q" W9 T! h
1: 发送时间戳TIME[15:0]。在长度为8的报文中,时间戳TIME[15:0]是最后2个发送的字节:TIME[7:0]作为第7个字节,TIME[15:8]为第8个字节,它们替换了写入CAN_TDHxR[31:16]的数据(DATA6[7:0]和DATA7[7:0])。为了把时间戳的2个字节发送出去,DLC必须编程为8。 | | 位7:4 | 保留位。 | | 位3:0 | DLC[15:0]: 发送数据长度+ X f& `- r; N) l7 n4 t
该域指定了数据报文的数据长度或者远程帧请求的数据长度。1个报文包含0到8个字节数据,而这由DLC决定。 | / h: y, `9 r6 y! ]! f
5.3 发送邮箱低字节数据寄存器 (CAN_TDLxR) (x=0..2)0 `, w/ k1 h% h5 L8 ^
% J7 R% ^, k5 {. O! v, c6 O8 k. O# ]9 T
0 Y" J; R6 @. b2 C
地址偏移量:0x188,0x198,0x1A8
& j+ P& g! R w9 O4 ^/ D复位值:未定义位 W7 N% f3 S5 ?" Y8 ^
, W! k8 B% d; r8 i
当邮箱为空时,寄存器中的所有位为只读。
3 r1 [4 {2 i! Z! ?

图4
. T; b: u5 k7 y4 z( J| 位31:24 | DATA3[7:0] : 字节34 |' B, g2 J& t. I6 J) e4 k
报文的数据字节3。 | | 位23:16 | DATA2[7:0] : 字节2; a+ M1 @1 z. x' y n y
报文的数据字节2。 | | 位15:8 | DATA1[7:0] : 字节1
`! B C2 J2 v- D$ K报文的数据字节1。 | | 位7:0 | DATA0[7:0] : 字节0
" Z @# [' y/ B报文的数据字节0。
- m0 L" U/ B3 X4 v. f报文包含0到8个字节数据,且从字节0开始。 |
/ y+ y' _1 y! T: ^5.4 发送邮箱高字节数据寄存器 (CAN_TDHxR) (x=0..2)8 d6 J: m! q4 c6 F' f. ^$ f
地址偏移量:0x18C,0x19C,0x1AC
% A# h) Z8 C- W5 p1 C3 k$ f复位值:未定义位
% X* g& B8 ]9 D M/ t$ U当邮箱为空时,寄存器中的所有位为只读。
+ n* d4 n! E$ j( ?
+ I- r/ m6 ?: ?! F' M H8 i

图5 ! P. T) B6 K4 A9 T
| 位31:24 | DATA7[7:0] : 字节74 }- t( s R( g% T& E
报文的数据字节7
$ C" y: s/ c6 L8 p5 `注: 如果CAN_MCR寄存器的TTCM位为1,且该邮箱的TGT位也为1,那么DATA7和DATA6将被TIME时间戳代替。 | | 位23:16 | DATA6[7:0] : 字节6: R4 S: g' t6 f; j3 M7 G2 e; K
报文的数据字节6。 | | 位15:8 | DATA5[7:0] : 字节54 T1 I# h: a3 [4 L0 i' U3 d6 @1 I( I
报文的数据字节5。 | | 位7:0 | DATA4[7:0] : 字节4$ z- v6 }$ G. t: S& ^
报文的数据字节4。 | ( d! J ?4 d8 ~) \+ Z0 h
6 CAN发送状态寄存器 (CAN_TSR)) u. g+ a W3 [ |7 a
地址偏移量: 0x08 复位值: 0x1C00 0000
( F l% p; i2 e5 q+ B 单有发送邮箱还不行,不得有一个寄存器从整体上显示发送各邮箱的状态及控制,而CAN发送状态寄存器(CAN_TSR)即负责此工作的。
: _3 j3 }# O8 E1 e1 L

图6 ; U6 R/ `. T! q( U" y/ B: N
各定义如下: ; { c' a- z. K! @1 t% i# \ O7 y
| 位31 | LOW2: 邮箱2最低优先级标志- C6 f" _- T: a) @& u% E# B+ ~
当由多个邮箱在等待发送报文,且邮箱2的优先级最低时,硬件对该位置1。 | | 位30 | LOW1: 邮箱1最低优先级标志
4 D! R) e8 H# ?; v当由多个邮箱在等待发送报文,且邮箱1的优先级最低时,硬件对该位置1。 | | 位29 | LOW0: 邮箱0最低优先级标志
$ S# J3 U% [( `0 e" z当由多个邮箱在等待发送报文,且邮箱0的优先级最低时,硬件对该位置1。 | | 位28 | TME2: 发送邮箱2空
0 Q3 f# x+ P: g1 M% p( @1 O当邮箱2中没有等待发送的报文时,硬件对该位置1。 | | 位27 | TME1: 发送邮箱1空7 g( U! v9 D$ p, |/ X# H5 X' Q
当邮箱1中没有等待发送的报文时,硬件对该位置1。 | | 位26 | TME0: 发送邮箱0空 G- Q: m: o% l5 s# r; X3 ?% K
当邮箱0中没有等待发送的报文时,硬件对该位置1。 | | 位25:24 | CODE[1:0]: 邮箱号
; _3 {$ t" m+ k I, i' O当有至少1个发送邮箱为空时,邮箱号为下一个空的发送邮箱号。
6 V) h' r! B) q, N( ^! g: f4 p9 I: h当所有的发送邮箱都为空时,邮箱号为优先级最低的那个发送邮箱号。 | | 位23 | ABRQ2: 邮箱2中止发送
6 _* c) o* M: I2 R5 X& |; H; o软件对该位置1可以中止邮箱2的发送请求,当邮箱2的发送报文被清除时硬件对该位清0。
# z& ]3 b6 C# f0 N3 R, x% T \如果邮箱2中没有等待发送的报文,则对该位置1没有任何效果。 | | 位22:20 | 保留位,硬件强制其值为0 | | 位19 | TERR2: 邮箱2发送失败
1 M& T8 R! s* F8 _3 K. J当邮箱2因为出错而导致发送失败时,对该位置1。 | | 位18 | ALST2: 邮箱2仲裁丢失 G8 V& i* F! ~3 m6 b. @1 i. ]
当邮箱2因为仲裁丢失而导致发送失败时,对该位置1。 | | 位17 | TXOK2: 邮箱2发送成功
0 h% p% A0 o/ @- S; b每次在邮箱2进行发送尝试后,硬件对该位进行更新:
; h- r" v' t- U* \3 A7 I+ z% \0: 上次发送尝试失败;
1 C3 w6 Q1 D/ T( ~' \* [1: 上次发送尝试成功。
3 V$ e- p( Q* k3 f0 g# _当邮箱2的发送请求被成功完成后,硬件对该位置1。 | | 位16 | RQCP2: 邮箱2请求完成
6 R7 \3 I# B& F. q当上次对邮箱2的请求(发送或中止)完成后,硬件对该位置1。
. O9 C. g X; Y3 i* B B软件对该位写’1’可以对其清0;当硬件接收到发送请求时也对该位清0(CAN_TI2R 寄存器的TXRQ位被置1)。' T9 V+ \* F% t, v1 Z
该位被清0时,邮箱2的其它发送状态位(TXOK2, ALST2和TERR2)也被清0。 | | 位15 | ABRQ1: 邮箱1中止(发送)% e/ S9 v! {4 `4 X9 h" m
软件对该位置1可以中止邮箱1的发送请求,当邮箱1的发送报文被清除时硬件对该位清0。
* m( M/ {* B: B% S) Z( }如果邮箱1中没有等待发送的报文,则对该位置1没有任何效果。 | | 位14:12 | 保留位,硬件强制其值为0 | | 位11 | TERR1: 邮箱1发送失败' a6 |) J/ a, r% B$ z
当邮箱1因为出错而导致发送失败时,对该位置1。 | | 位10 | ALST1: 邮箱1仲裁丢失
2 B7 n5 X: }; K5 Z( B* f0 ~当邮箱1因为仲裁丢失而导致发送失败时,对该位置1。 | | 位9 | TXOK1: 邮箱1发送成功
! E( b' z: a5 X: b每次在邮箱1进行发送尝试后,硬件对该位进行更新:
. K* t6 T4 s5 Q0: 上次发送尝试失败;
' f; w6 A p. j# L, v" o; `1: 上次发送尝试成功。3 a" N6 W3 @- S
当邮箱1的发送请求被成功完成后,硬件对该位置1。 | | 位8 | RQCP1: 邮箱1请求完成
3 ^* j0 Q3 x/ c6 t j4 y5 p) X当上次对邮箱1的请求(发送或中止)完成后,硬件对该位置1。4 C* A/ Q9 |- d
软件对该位写’1’可以对其清0;当硬件接收到发送请求时也对该位清0(CAN_TI1R 寄存器的TXRQ位被置1)。2 _, r4 K' v) R) Y: ?
该位被清0时,邮箱1的其它发送状态位(TXOK1, ALST1和TERR1)也被清0。 | | 位7 | ABRQ0: 邮箱0中止(发送)3 v: L c5 [! z
软件对该位置1可以中止邮箱0的发送请求,当邮箱0的发送报文被清除时硬件对该位清0。, `- \$ B+ I: g0 O
如果邮箱0中没有等待发送的报文,则对该位置1没有任何效果。 | | 位6:4 | 保留位,硬件强制其值为0 | | 位3 | TERR0: 邮箱0发送失败
) K; j5 U! |. b当邮箱0因为出错而导致发送失败时,对该位置1。 | | 位2 | ALST0: 邮箱0仲裁丢失
; {0 D6 i7 [/ g+ h" G# U- E当邮箱0因为仲裁丢失而导致发送失败时,对该位置1。 | | 位1 | TXOK0: 邮箱0发送成功( C* \! @" A' c2 @6 c- r7 K
每次在邮箱0进行发送尝试后,硬件对该位进行更新:
% F/ I- b* R% ~& V0: 上次发送尝试失败;- x1 t" m% C. {, D/ f1 f
1: 上次发送尝试成功。
0 l( p' E( ^; c3 j! V8 }. Y当邮箱0的发送请求被成功完成后,硬件对该位置1。 | | 位0 | RQCP1: 邮箱0请求完成' [9 V5 t. a! a8 u) L# i* F7 ~
当上次对邮箱0的请求(发送或中止)完成后,硬件对该位置1。: k. Z, y% s u5 K/ b' I
软件对该位写’1’可以对其清0;当硬件接收到发送请求时也对该位清0(CAN_TI0R 寄存器的TXRQ位被置1)。
3 p1 m( W, K* D: H该位被清0时,邮箱0的其它发送状态位(TXOK0, ALST0和TERR0)也被清0。 | , G3 L* Q- Q& I: k0 W
这里值得注意的是位25~24,即下一个空的发送邮箱号。当所有的发送邮箱都为空时,邮箱号为优先级最低的那个发送邮箱号。通过此两位,STM32就知道下一条发送报文该存储到哪个邮箱了。
6 [, e" Y9 t p4 C$ l
8 r5 [3 @/ Z" J5 e ?# Z b8 B$ A# M5 e2 l) x
6 X% u' } S0 [7 与CAN发送有关的固件发送接口
& Q7 F; n5 D% m* I. N/ g发送接口如下:
- d) P0 D+ r; K$ G& t0 i
- /**
* N: }; h! a3 i5 C - * @brief Initiates and transmits a CAN frame message.
; Q7 T2 B5 w8 I8 F - * @param CANx: where x can be 1 or 2 to to select the CAN peripheral.8 N* Y- |# W- x k4 v
- * @param TxMessage: pointer to a structure which contains CAN Id, CAN DLC and CAN data.) f3 g5 G" E+ V0 _7 @
- * @retval The number of the mailbox that is used for transmission or) L! N* ~: p% J
- * CAN_TxStatus_NoMailBox if there is no empty mailbox.9 Z$ U: y' A5 K' z$ b9 x& c/ P
- */
- k% d; } C) B: @* p6 g8 s - uint8_t CAN_Transmit(CAN_TypeDef* CANx, CanTxMsg* TxMessage);
复制代码 ' s! i2 A' ]# g' V4 t: A' b, w" o
" N7 g7 s5 N" E+ ^2 G2 O1 [获取发送状态接口如下:5 N2 N& @3 O& v/ b* V4 H0 F! V
% C8 R0 E, i+ o6 w: N1 ]: G2 d! \9 F! f
- * M1 n$ W# u# Q! X, {) ^
- /**% P8 ?, x: ~3 T F/ c8 {, K3 {
- * @brief Checks the transmission status of a CAN Frame.
, C" B+ a9 s* d3 T$ i6 Z - * @param CANx: where x can be 1 or 2 to select the CAN peripheral." E7 x. v. j+ t9 p4 D" \
- * @param TransmitMailbox: the number of the mailbox that is used for transmission.
6 @. F" p0 H6 b) C! \: R O - * @retval CAN_TxStatus_Ok if the CAN driver transmits the message,
, ~9 M, f- D/ N. D+ _) P9 ^ - * CAN_TxStatus_Failed in an other case., a) t. e3 u! K ~+ U
- */ ^/ r3 P9 @ i0 e2 o
- uint8_t CAN_TransmitStatus(CAN_TypeDef* CANx, uint8_t TransmitMailbox);; L' b: @5 p* D ^
复制代码 D0 G) G' ~# V4 Q K ]# i2 G% B
( p2 K [" `) r- \5 t
8 一个示例5 _; m; V1 T; ^# [1 L
8 q, {, \! @# `( f* o$ i/ D- . E! Y& ~2 @6 b2 M( n- y* |
- CanTxMsg TxMessage;
) N# g% s9 S. i3 _! v1 Y - int i;! a5 v' K7 `1 V8 ?7 m
- ' ]. @0 g- Z3 G N/ O
- if(id_fmt == STD_ID)//如果是标准CAN ID
0 U' w. C' U) y1 [; j) W. G6 W - { 1 d- l7 Z9 u0 E, u2 C# M
- TxMessage.StdId = send_frame->id; //设置标准CAN ID
1 Q+ g, h% B) d" v/ A$ I - TxMessage.IDE = CAN_ID_STD; //设置IDE为标准CAN ID
. x, B r/ q& U4 y1 I& I7 m - }: Q6 ~: D& a) |3 G
- else* P/ [3 k$ f4 `. z3 V
- {
; }, y; P; E% x) b* j& h* H6 n: Y- I - TxMessage.StdId = (send_frame->- u7 p/ I+ c1 C
- id >>18) & 0x7FF; //设置扩展CAN ID的标准基本ID部分
& a' Y$ x" U! E7 d) H# T) r - TxMessage.ExtId = send_frame->id & 0x3FFFF; //设置扩展CAN ID的扩展ID部分
3 j6 Q2 i) ]2 n$ x7 Q - TxMessage.IDE = CAN_ID_EXT; //设置IDE为扩展CAN ID9 o$ M5 V! a6 Y; x: z4 G" c
- }1 O8 `) `* \4 @6 `
- ! u; e/ d$ y/ J' Q K
- TxMessage.RTR = CAN_RTR_DATA; //数据帧
/ ]( g5 l. F. D0 i+ [4 ^0 |( h - TxMessage.DLC = 8; //数据长度; @. a2 ^! u: n! z) {' q/ e' R
- for(i=0;i<8;i++)
8 r% p/ c6 w, r7 u* N" B5 i7 T - {3 h; u) @' Q" I) s
- TxMessage.Data[i] = send_frame->data_buff[i];
8 I- r5 T; w. d' x- _ - }
7 p A4 y6 Z8 B* {; F) j. c - ) |$ B9 ~( ?3 l4 }2 D% l( h2 @
- CAN_Transmit(CAN1,&TxMessage);) q& C* V$ `6 v5 x/ K
复制代码
( l4 I0 R& [2 f& }) ~9 z
2 U: a; ]1 f& s: @ |