串口通讯(Serial Communication)是一种设备间非常常用的串行通讯方式,因为它简单便捷,因此大部分电子设备都支持该通讯方式,其通讯协议可分层为协议层和物理层。物理层规定通信协议中具有机械、电子功能的特性,从而确保原始数据在物理媒体的传播;协议层主要规定通讯逻辑,统一双方的数据打包、解包标准。通俗的讲物理层规定我们用嘴巴还是肢体交流,协议层规定我们用中文还是英文交流。下面分析一下串口通讯协议的物理层和协议层。
( @) p3 H# O& _3 l8 X
' m, l) M, x1 `! `2 Y7 B4 w- L- A物理层
" H0 j- F4 v( n% L9 [ G1.通讯结构; C& Y! Y: Z" ]9 g$ M8 T
串口通讯的物理层的主要标准是RS-232标准,其规定了信号的用途、通讯接口及信号的电平标准,其通讯结构如下:- c$ v. Q. {5 F. u/ a# R
* J0 O! F; I. A2 r1 ?0 c
+ l5 c6 ^4 Y) y6 @( a5 [* n
1 V) ?" p9 D. o4 v6 c. v
在设备内部信号是以TTL电平标准传输的,设备之间是通过RS-232电平标准传输的,而且TTL电平需要经过电平转换芯片才能转化为RS-232电平,RS-232电平转TTL电平也是如此。3 U& b. O( I8 y" s% a" n( Z
2.电平标准
6 q* m* x$ B) l* @( \. m) W9 J7 T根据使用的电平标准不同,串口通讯可分为 RS-232标准 及TTL标准,具体标准如下:
& m- B6 S7 [4 V6 Y0 J; W* O9 e" p- P
2 d: B" a& I0 c" b4 b/ I7 ^! { X0 M6 E) L
在电子电路中常使用TTL的电平标准,但其抗干扰能力较弱,为了增加串口的通讯距离及抗干扰能力,使用RS-232电平标准在设备之间传输信息,经常使用MA3232芯片对TTL电平及RS-232电平进行相互转换。
) j. V0 K6 Z9 g8 Q a( ?
8 [' |- V9 g& n% D i k: y2 T- P2 ?8 ]. Y5 t' e
协议层% {- M. \, [' c/ I* f6 ]
1.数据包: a5 w! R" N7 @: U! K
串口通讯的数据包由发送设备通过自身的TXD接口传输到接收设备得RXD接口,在协议层中规定了数据包的内容,具体包括起始位、主体数据(8位或9位)、校验位以及停止位,通讯的双方必须将数据包的格式约定一致才能正常收发数据。) N7 ^8 X1 T/ O1 q+ ]7 @
; _* D6 s U' ~& V7 r6 Z
" [6 L: U6 _% `7 f& p
/ z/ B9 E% l, R2 a) |3 R+ k7 ?; h3 `6 ]
2.波特率
5 k! y) x1 J; w由于异步通信中没有时钟信号,所以接收双方要约定好波特率,即每秒传输的码元个数,以便对信号进行解码,常见的波特率有4800、9600、115200等。STM32中波特率的设置通过串口初始化结构体来实现。! ~5 r" t4 W! P7 v( B8 s
& s* w8 t+ f# a. t
3.起始和停止信号. v+ F, H( M3 E# D( |; T K
数据包的首尾分别是起始位和停止位,数据包的起始信号由一个逻辑0的数据位表示,停止位信号可由0.5、1、1.5、2个逻辑1的数据位表示,双方需约定一致。STM32中起始和停止信号的设置也是通过串口初始化结构体来实现。" J6 c" N9 M8 f; a; C% W
4 c- e3 h% B7 g* ~+ w
4.有效数据" x0 L3 _* |0 }7 ]3 N
有效数据规定了主题数据的长度,一般为8或9位,其在STM32中也是通过串口初始化结构体来实现的。; \3 J# k3 w, U8 }; c: m6 j; H
! I' V: z5 f9 F4 c
5.数据校验* S/ B* i7 }1 E. [
在有效数据之后,有一个可选的数据校验位。由于数据通信相对更容易受到外部干扰导致传输数据出现偏差,可以在传输过程加上校验位来解决这个问题。校验方法有奇校验(odd)、偶校验(even)、 0 校验(space)、 1 校验(mark)以及无(noparity)。这些也都可以在串口初始化结构体中实现的。
+ a4 ~ B; f7 E1 ]9 m9 ?2 A
7 E9 x: ]1 b8 vUSART简介2 ?* F0 U) w/ c* E5 R* V' t
USART(通用同步异步收发器)是一个串行通信设备,可以灵活地与外部设备进行全双工数据交换。有别于 USART 还有一个UART,它是在 USART 基础上裁剪掉了同步通信功能,只有异步通信。简单区分同步和异步就是看通信时需不需要对外提供时钟输出,我们平时用的串口通信基本都是 UART。USART 在 STM32 应用最多莫过于“打印”程序信息,一般在硬件设计时都会预留一USART 通信接口连接电脑,用于在调试程序是可以把一些调试信息“打印”在电脑端的串口调试助手工具上,从而了解程序运行是否正确、如果出错哪具体哪里出错等等。/ S3 R* S7 X6 z- y+ c0 v
5 n* J: e9 v% C3 O$ w
STM32中一共有5个USART,如示:& d- L( Y3 D4 n1 `( n+ X8 m7 Z
) Z V' b, B+ n* a
' r; s6 x8 @% u0 J
1 C# A# ^# U! ~" h1 S: q3 fUSART的USB转串口原理图如下:
" l% T( _: b8 R% y: q' j8 Y+ J" j; ~ R, m, ]! a
. I' f! S9 L8 n6 {4 H* G( ~% ~: U* S* N4 g' |
USART1的发送和接收端口是事先连接好的,如果要使用其他USART只需要将相应的发送接收端口按图连接好即可。
# _' a4 ^7 g/ s5 m+ c, G* h4 s6 d- h4 C& h, x
; y" T6 |8 w& o1 ]1 M: TUSART有多个中断请求事件:
g* z% q5 i" R0 k6 f! e( q( X3 r
. N e; {- F& ]- r U O. p
' n# N9 L5 o! H# E7 ~开发板与上位机的连接, j1 `: Q) e/ T* |+ h! b! d
开发板与上位机之间通过USB线连接,所以在上位机上要配置一个USB转串口 的驱动,以便把USB传输过来的电平转换为TTL电平,TTL电平才能与串口调试助手建立联系。一般使用CH341驱动作为win10下的USB转串口,驱动安装成功的情况下接入USB会在计算机的设备管理器的端口中发现串口:
8 u. `* W- C1 H r% d6 I8 l- `4 ]6 ]7 x! w
+ U* E* @: Z8 ~; I" [5 o* C1 n9 z- }! E
(win7系统一般选择CH340作为USB转串口驱动。)
5 Z' t$ @* U4 h+ V: U0 D: G' A+ ?
2 N0 w9 I! c3 j/ I' ?; G
6 k# ~2 i/ G1 e代码讲解:: s. A. ^+ b" I5 a8 I
固件库编程的一大好处就是我们可以根据固件库函数来学习外设的相关知识,而且固件库函数的编写都是建立在对底层寄存器操作上的,所以通过讲解代码可以更好理解串口通讯相关知识。
! B; j) X8 g y q# N, t g1 J/ \9 ]4 t1 @3 v$ v
一.初始化结构体
+ ]# X* h% {2 j. R, o- typedef struct {5 L% M Y' ?1 G. w* m9 u
- uint32_t USART_BaudRate; // 波特率2 }% K# M! A2 G3 B: O$ G6 E
- uint16_t USART_WordLength; // 字长3 \. @& b E4 {, _" U2 l' y
- uint16_t USART_StopBits; // 停止位
! l, z# I/ y3 a7 U! c - uint16_t USART_Parity; // 校验位. w' C& v* H1 b4 R5 t
- uint16_t USART_Mode; // USART 模式, C0 d' v; c X1 q7 _7 X; ?, ]
- uint16_t USART_HardwareFlowControl; // 硬件流控制
: @, V8 u1 S+ x" k - } USART_InitTypeDef;
& T& `! G9 u8 }9 a4 j: a
复制代码
1 k5 ?) X# M( y' t/ ]1 K5 e& GUSART初始化结构体中的相应变量都对应着数据包中的相对内容。$ L1 r0 _) ~7 ]. C: G+ C1 X
$ s5 K8 o0 m5 \. M
1 L* Y: @; b8 ^) a/ d1 a9 M
二.NVIC配置中断优先级
* X5 a% J k$ x1 C- ?) j2 N0 |我们在串口接收信息时采用了触发中断事件,所以要配置一下串口中断的优先级:. e7 [ C! x, t" Q2 i
- NVIC_Configuration(void)
: J3 q' \$ \0 v8 ]9 n- q: D - {8 N. ^5 u: H- _6 t/ R" Z' K! b. O
- NVIC_InitTypeDef NVIC_InitStructure;, J3 ~4 I7 x7 t; h* f
- 8 h) U4 \* t1 g5 X
- /* 嵌套向量中断控制器组选择 */8 u/ n0 Z& ?8 n* Z" |: z. A" i5 n
- NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
5 A) G; D8 ]( [) v- M - " q0 _! L3 {5 D/ B6 f, K8 t# r
- /* 配置USART为中断源 */
( B- x1 s6 U; @3 u# U - NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ;
; }* o5 o( V: g; L4 t# s) t - /* 抢断优先级*/0 x! S" e) }8 ~
- NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
9 x' L* F' u% J# E% l0 v4 }% ` - /* 子优先级 */2 t X' L$ k2 m7 B
- NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
2 y$ G7 Z$ F4 F - /* 使能中断 */, b/ K) B$ d' C4 q
- NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
# J) Y+ x$ c/ P, B8 p; ` - /* 初始化配置NVIC */6 d2 A: o# N% M$ ] E" y/ n
- NVIC_Init(&NVIC_InitStructure);
7 ?/ U: @4 B- c% n0 t - }* @+ _4 a) N5 C
复制代码 * J7 h/ `+ H! k8 b4 a# Y
三.USART配置函数讲解
$ l* w1 q y3 N) u3 F0 L1 ^USART配置函数的主要作用是打开串口与相应的GPIO引脚,配置好相应串口信息与GPIO引脚的工作模式,以便信息的传输与接收。8 v7 e: E l4 `7 U
- void DEBUG_USART_Config(void), M0 U; K" o8 D1 @0 P% g; b8 q
- {
+ S1 G* p! d2 e0 m# H" N& v3 ^ - GPIO_InitTypeDef GPIO_InitStructure;3 N2 b3 V* T7 }4 X8 z8 \
- USART_InitTypeDef USART_InitStructure;
6 w. t. ~/ D) X, H1 u0 g0 `- K -
1 N* {/ U0 b- n+ h" x! M6 }5 D& q - /* 第一步:初始化GPIO */
" W" y7 g8 H ~ e0 T! n - // 打开串口GPIO的时钟
) I4 X6 K- l- u7 ~ - DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);
% o _" Z& z( s" ^/ J2 F8 a5 z6 n2 T - // 将USART Tx的GPIO配置为推挽复用模式
9 ^( v" V4 C* `( Z5 P a - GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
5 A# C2 D c. @2 M( p5 I - GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;- \ Z' m0 M6 F
- GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
6 @5 @/ s( V9 d/ G9 K - GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);1 J( d7 S! K" _. k
- } k: s& A6 Y9 h4 G6 S+ p
- // 将USART Rx的GPIO配置为浮空输入模式
& h: k3 D: O7 D- G$ U$ s* y+ |: y - GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
9 y e5 t0 ]/ F- m( \! b( r# { - GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
- b# f1 W* V; s, l; C& p - GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure); 7 c! N9 o1 F! p0 K1 b
-
\, D1 u8 ~+ K u - /* 第二步:配置串口的初始化结构体 */$ Q* q$ w, t8 e+ _4 m+ m% ]
- // 打开串口外设的时钟
7 n) Z" b6 ~6 z) }7 k; I0 ] - DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);
# Z3 L, N- F" _* } - // 配置串口的工作参数% X1 b1 ~# M) T4 g! G3 K" D5 W
- // 配置波特率
% x% K5 Q. z( a/ }1 c - USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;" ^4 W3 y3 G; x' z3 ]' A1 _' h
- // 配置 针数据字长
' f5 B% D! T2 ?& m! k# B! V9 A - USART_InitStructure.USART_WordLength = USART_WordLength_8b;# z/ W' \: v: P; H, i9 ]
- // 配置停止位( r) Y% a: b" `9 D& m# g$ G
- USART_InitStructure.USART_StopBits = USART_StopBits_1;
4 d) ?2 n$ m/ C3 v2 v - // 配置校验位" z! V+ w) D% `
- USART_InitStructure.USART_Parity = USART_Parity_No ;: j: h# p {: u% q9 T5 L6 t
- // 配置硬件流控制
" B) o1 i* ~% Y! m% o) W' s1 u8 { - USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
# }0 |$ J9 I) n- }' d* O2 ]% g - // 配置工作模式,收发一起
9 v5 \- x0 ~/ P0 p/ Q3 Z2 ` - USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
2 Q% m2 z! K" J" n( b/ z# B- t - // 完成串口的初始化配置
6 v' V/ c6 _# O9 a4 M* e - USART_Init(DEBUG_USARTx, &USART_InitStructure);. F( P4 `; N+ j
& N$ }4 N& Y5 P" c" a8 f1 t7 ?- /*--------------------------------------------------------*/- E/ w7 O8 g) U5 g* w
- // 串口中断优先级配置2 p6 Q, s K2 {: ] t# s' j
- NVIC_Configuration();
2 {" P7 I6 h4 o5 P - 5 M/ c3 t2 c$ k7 B7 W% Z2 s. g
- // 使能串口接收中断
$ v, i% G6 E3 L4 e1 B - USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE);
7 ^1 x7 B/ T1 E4 F* R; s0 j - /*--------------------------------------------------------*/
1 i- E' z2 I. f) P3 ^- j* ? - . a, @, Y3 j+ F/ r
- /* 第三步:使能串口 */
/ J; |+ O* R m# `( p4 L - // 使能串口4 k# { y$ h, \
- USART_Cmd(DEBUG_USARTx, ENABLE);
2 r3 J' E4 r2 E( }& Z4 [ - }
+ r/ S8 ~0 s6 |0 Z# }( I
复制代码
8 S9 M# m$ K+ F0 ?; B3 `第一步:打开了GPIO的时钟,设置发送和接收引脚的信息,将Tx(发送引脚)配置为推挽复用模式用来发送数据,Rx(接收引脚)配置为浮空输入模式用来接收数据。 R p( A' [. o5 o
0 ^7 P1 L: \" e& |& Q5 o" k
第二步:首先打开USART1 的时钟,根据USART初始化结构体成员配置相关的信息,之后利用初始化函数将初始化结构体中的信息写入相应寄存器中,然后的话就是引用NVIC_Configuration()函数配置串口中断优先级,打开相应的串口接收中断,中断接收函数的参数如下:
- P" d" v3 t- W+ H5 a- V" F6 V0 P, \& ^6 [) E$ s0 m
# R T9 Z' {0 o8 R y e
X/ d4 r6 P/ ? T5 c0 s第三步 :最后相当于打开总电源——使能串口
7 w% L( n3 `. l7 B% i0 C$ MUSART配置函数完成后代表,USART1 的接收和发送准备工作已经准备就绪,接下来就是,串口与上位机之间的信息传递了,信息的发送和接收都有相对于的函数。
" H' P3 D; l5 F; Y: f
8 ]) Z' T9 B! ?$ F; T. T: \( [) g( H* ] r0 s5 p2 ^
四.传输数据的函数:
~& m6 y @; `2 V0 C' C开发板与上位机之间的数据传输可以有多种方法,下面一一介绍:
4 l5 y7 R D/ R+ N; H
8 B: k0 E4 c( ^1 z1.发送一个字节
; t* L: h- a3 e( z( V4 e0 B9 ]4 {以USART_SendData(pUSARTx,ch); 函数为基础建立的函数可以向上位机发送一个字节的数据,利用FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG) 读取发送数据寄存器的状态来 等待发送寄存器将数据成功发送。) k, A* g& p8 I! b; @0 C
- void Usart_SendByte( USART_TypeDef * pUSARTx, uint8_t ch)
) _0 E) N, q3 T* | - {% R6 H& m4 o1 ^- K
- /* 发送一个字节数据到USART */
! t7 E1 q) t" E - USART_SendData(pUSARTx,ch);
. j2 w D& S) U8 c l, f -
- h% |- W+ J. W8 ? - /* 等待发送数据寄存器为空 */
( o0 w! o- i" |* d - while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET); # g) `% @8 G! Y; W2 J
- }7 n7 C; n, ~0 ~% \* V
复制代码 5 M& l1 C) p2 |: d4 f7 a0 x
2.发送字符串
& b5 P! a# Q* a9 R' C) R) w5 K本质是利用上面的字节发送函数逐位发送字符串中的内容, X$ _( [* }7 |( T
- void USART_SendString(USART_TypeDef * pUSARTx, char *str)2 k( G2 L/ i/ g% x" ?8 l
- {
i8 F+ e: B- E3 q% e1 @( a - unsigned int k=0;
1 @( _8 Z E8 {3 Q9 R" F - while(*(str+k)!='\0')0 `8 R9 \* x& q5 ?/ o- u9 {
- {
" ~: w& T# i6 f M - USART_SendData(pUSARTx, *(str+k));7 @ v8 u7 u% |4 g( v0 ^5 L
- /* 等待发送数据寄存器为空 */$ O3 Z; n+ e8 p, F
- while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);0 N& X' _& @ d
- k++;
% ?& I* ~% ]1 i+ i: T - }: T& c0 k% a& C, }4 @( f
- while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TC)==RESET); /* TC:传输完成标志 */
4 B! M8 c+ O- U) C8 | P - }
, d+ e, F0 ?0 z3 f) \. w( `
复制代码 8 d" V( C0 I+ E9 R; V
3.重定向printf函数发送字符串: L: k r* { i' u: H. [
关于重定向的知识之前总结过,链接:重定向知识。重定向后的printf()函数功能强大,具有向串口调试助手打印数据的功能,使用方法和c语言时一样,比如printf("欢迎来到小全全的串口实验\n");就可以将“欢迎来到小全全的串口实验”这句话发送到上位机中,而且换行符“\n”还具有换行作用。
6 n9 W2 L8 `0 ?' _7 @& ]* s, h/ w- /* 重定向printf函数 */
8 y. R7 @7 n1 r - int fputc(int ch, FILE *f)4 a1 J0 ?% H1 h0 T# E
- {# E$ F p' g2 v- d7 l! w5 T
- USART_SendData( DEBUG_USARTx, (uint8_t) ch);" ~9 X0 o% `& @0 d
- /* 等待发送完毕 */
! ~+ D) T6 M- K" R A, `/ L - while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE) == RESET); $ ~* I6 M* i9 c7 H p: {
- return ch;
# x5 e. U8 D, Q+ t$ C; M0 G - }! f0 R N1 }5 I
复制代码 ! r$ e! s6 a* a+ g3 J) |$ o( o) ~
4.重定向getchar函数接收字符
a; I8 K+ w d) r' U0 n8 y具体操作与重定向后的printf函数类似,比如可以通过如下代码向上位机发送已经接收到的数据:
/ C. q: W% }5 j- c$ k, a- x=getchar();
: A) ?/ J1 o" V% `; t/ y* _! z9 w - printf("接收到的字符是:%c\n",x);% |$ V! C. f4 o6 f; [2 |
复制代码 9 k; `7 @; q$ F; [" c2 v
重定义如下:
( G v# w: S" l7 d, N- ///重定向c库函数scanf到串口,重写向后可使用scanf、getchar等函数
, ^4 ?$ l" K5 h. J - int fgetc(FILE *f)
( n" Z3 s9 Z! r# _5 k; e6 X - {
! S9 F3 S: t2 m2 v" W" X - /* 等待串口输入数据 */
$ p* Q8 N- y6 T, |$ Q& T. P3 o - while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_RXNE) == RESET);$ e0 ?( l' W7 H- ]0 _$ f
- " m$ J2 T0 |0 Z+ f" ?& n$ t4 I
- return (int)USART_ReceiveData(DEBUG_USARTx);- e2 t. i9 i& z2 W! `2 T/ }
- }
! j2 `5 c/ T% }( f' p4 {$ q
复制代码 , B! q% R: T }/ `% Z
在使用此函数作为接收数据时记得关闭串口得接收中断!!!
, O2 S8 l3 ~: H
+ F! ?/ y, u0 |3 e) a6 T% D! u
: C: S& I" P# o; C" {4 |3 E5.通过中断接收% V) T+ @; k" x% `9 D$ c5 w2 D
在stm32f10x_it.c中编写USART1中断源相对应得中断函数,利用了固件库函数中的
. @- e# I8 A+ F/ V7 a# UUSART_ReceiveData(DEBUG_USARTx);接收函数
1 t9 c3 q+ @% c: I2 R3 L0 G" N$ q; ]USART_SendData(DEBUG_USARTx, x);发送函数% V- ]: G( V" w
USART_GetITStatus(DEBUG_USARTx, USART_IT_RXNE);判断标志位函数
+ `2 W. @5 y" Z3 S- /* #define DEBUG_USART_IRQn USART1_IRQn
3 D! k& i4 T0 G' b+ [+ W - #define DEBUG_USART_IRQHandler USART1_IRQHandler */, ]: M1 V9 [# Y
- void DEBUG_USART_IRQHandler(void)
2 K, b1 N0 R% V" X; P - {
) g3 F+ ] {5 R# G+ P* Q - uint16_t x;' n, D k' n9 ~" u9 ^
- /* 判断是否收到中断信号 */
. v8 q2 u" P+ D& Z2 r. W' K1 | - if(USART_GetITStatus(DEBUG_USARTx, USART_IT_RXNE) == SET)# w8 k1 u! j; h9 H: x3 s
- {
) j' Y# ^3 e/ ]- Y9 e9 H8 |9 Z - x = USART_ReceiveData(DEBUG_USARTx);, y& H6 h- m( y+ M/ T
- USART_SendData(DEBUG_USARTx, x);
' `" R3 s! [' d& t! @8 N - }9 k' I3 b, M, f7 L3 U( v. ]
, }; ], d5 R3 P- }# M' x4 ^- v* v" W4 F
复制代码
0 f3 B( q" f! G结语. Y- q3 [" S& b8 d- }
以固件库函数编程的思路讲解,未能顾及到众多寄存器的讲解,我认为进行固件库编程本身就是学习操作寄存器的过程,很多时候我们不需要知道如何操作寄存器,只要了解如何操作固件库函数即可。" S2 U- L5 I( E) Y- \! S
————————————————) m+ z5 x U: T/ K
版权声明:Aspirant-GQ/ `7 {4 y& F m/ J" ?! _( w m3 S
7 |: e- G& U- P- ~
+ v- @% ?1 i/ B+ P0 ]) g |