15.1 关于串口3 @6 ?' p+ p- N4 E
15.1.1 串口理论知识
+ W* }9 z/ s* k- @% q' I; h2 @/ o4 S- C; ~6 T* V
说到串口,经常提到TTL、RS232、RS422、RS485,简单的说,就是为了适应不同的环境条件,使用了不同的电平标准。假如微处理器和板载的蓝牙透传模块通信时,一般就使用TTL电平,引脚直接连接即可。假如微处理器在工业现场,需要连接一个几十米外的装置,则应该考虑将TTL电平转为RS232、RS422、RS485。
+ Q! F, o! }' {+ v5 u
% G, L) D, s: }! `* M4 m' [: F7 G5 t. H5 J# M- A
" X2 M( H$ ?/ s% y: o; b8 \图 15.1.1 不同通信电平应用的场景 ! Y( |5 W0 ]$ H0 b. S+ ]2 w0 x) n
' V1 ?& y1 K5 C# z# g7 Q
6 }2 B3 [# y9 K' z; Z如下表 15.1.1 是几个通信接口标准总结。可以发现为了加大传输距离,人们依次尝试了增加电压、差分传输等方式。2 v9 s7 l9 j4 G$ u; U0 z! ?
# f i/ P- r- ]8 S6 N4 X
! h) S0 s* F1 o5 _* m0 B
表 15.1.1 常见通信接口标准 & z4 K. \% A1 Y! Z' D" m) a
7 \7 j4 T# {) Z$ @# R2 U
: F: C1 C% q7 x# P" j- [, f. z以TTL电平的接口为例讲解串口怎么传输数据,对于RS232/RS422/RS485等接口,仅仅是把TTL电平转换为不同的电平值,或者转换为差分信号。/ ~( F3 x% o' o8 D$ ], p
6 a9 [6 S1 U+ k; w8 W$ k
TTL接口的串口,硬件连接如图 15.1.2 所示。. X3 x1 C- f+ j3 h# w
; g0 u# U+ m1 R. X, n6 U
$ e8 I8 F( c/ u8 s; v4 d3 Z9 V8 t' E5 q
图 15.1.2 TTL电平的串口硬件连接示意图 ' z z: C- C& U& m, M+ ]
, |3 H: J1 Z5 _2 ?0 G' [2 q' P2 q4 q1 q. M
串口传输中的一些概念如下:
2 X- F; j( w4 g1 V( D# y( b" H6 w/ U7 m
波特率:一般选波特率都会有9600,19200,115200等选项。其实意思就是每秒传输这么多个比特位数(bit);5 g& ]8 }; C) I& ^; U2 n8 C* G
' V, }/ [, c# t% N* m P
起始位:先发出一个逻辑”0”的信号,表示传输数据的开始;4 `* k1 ^1 @' u
. j+ `+ |8 J! k( [$ o数据位:可以是5~8位逻辑”0”或”1”,先传输bit 0,再传输bit 1,依次类推;1 c$ Y7 k @" ^ L/ n, x
6 l% S7 ?- @ S* l1 {4 a$ E校验位:数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验),以此来校验数据传送的正确性。校验位是可选的,可以不传输;
1 K' W; p' h& C+ l2 l$ H) h& z0 T
: y4 G3 Z p( I' p" B9 I7 s停止位:它是一个字符数据的结束标志,数据线变回逻辑”1”;
# s# u: K7 U; B: H
0 f. i7 i2 r: Y
" V* i& _$ |' h* h2 z. v7 v5 O
: C" M4 }4 c9 u6 m8 J( ?怎么发送一字节数据,比如“A”?“A”的ASCII值是0x41,二进制就是01000001,怎样把这8位数据发送给对方呢?
2 j6 U$ H' ?) t& v, a W; j+ ^% R4 t3 T+ A5 P+ G, B
双方约定好波特率、数据格式(数据位个数、停止位个数、是否使用校验位、奇校验还是偶校验),假设数据位是8,停止位是1,校验位是1;
1 g& z7 Q0 Y$ [! q4 W# e$ O
8 s3 O, o: d1 X+ g初始电平为逻辑1;- n: x% B6 ^+ z2 M
7 [: x6 C* d ?, D; V发送方输出逻辑0,并保持1位的时间;接收方检测到逻辑0,就知道对方准备发送数据了;: n- E! v, u# c7 j
0 o0 F9 ~9 U* k# B: Q
发送方根据数据的bit 0设置引脚电平,并保持1位的时间;接收方读取引脚电平,得到bit 0;
0 r( j# ]6 O! ^' B
) \8 W9 Q& Y& X* B+ k发送方根据数据的bit 1设置引脚电平,并保持1位的时间;接收方读取引脚电平,得到bit 1;) ^6 u' k/ d( s2 T3 @
% B4 S$ u3 L$ O1 z5 c以此类推,发出8位数据;/ z$ R% `- S7 P6 F
' U( n8 e& ]# B) O/ S8 c
l发送方计算出校验值,设置引脚,并保持1位的时间;接收方读取引脚电平,得到校验值;注意,这步可以省略;+ Z9 G0 e8 a' H7 N7 Y6 [
4 V( [; k7 G( Q' I发送方输出逻辑1,并保持1位的时间;接收方读取引脚电平,直到数据传输结束;4 l4 A: n4 g, }, x+ N
7 x2 v# ^! E/ W" _! s, @+ U& j9 h 信号的波形图如图 15.1.3 所示。. O; ^3 d& ]7 A* X
, E& e+ w9 {$ d* ^6 R& Z
" t( q q1 A' F, ?
1 [5 H) ~1 M: ], t+ u0 i0 ?
图 15.1.3 串口信号传输波形图
$ |; t+ U: W+ s7 M }2 X! X
% g; n1 t- d; B0 w5 [9 a6 J6 O
- L6 G% J, s# `: y$ G) w8 M15.1.2 STM32的串口
, x3 h# m* r, `, t8 ~
2 d5 H& ^6 e/ S% q' q在嵌入式中,很多MCU和外设模块都集成有UART外设。STM32F103系列最多有3个通用同步异步收发器(Universal synchronous asynchronous receiver transmitter,USART),2个通用异步收发器(Universal asynchronous receiver transmitter,UART)。USART和UART的主要区别在于,USART支持同步通信,该模式有一根时钟线提供时钟。
7 ]& h- _% a* H$ r2 t, t7 w+ ^! u7 v
串口在嵌入式中经常使用,一般使用UART就足够了,常见的用途如下:% g1 M9 D- w1 F" Q+ _+ i/ _+ H
3 O: g3 v7 v" G
1. 作为调试口,打印程序运行的状态信息;
5 M+ W a; P q- D5 r/ i& b8 u" o8 E' t
2. 连接串口接口的模块(比如GPS模块),传输数据;
: c& e! l/ ~+ S5 `2 N( i" m8 X% T: y2 \: M7 B
3. 通过电平转换芯片变为RS232/RS485电平,连接工控设备;. N' [; g; i. L
6 H8 F8 b* n( E1 H
2 r' _: U1 {- @
STM32F103系列不同USART所支持的功能如表 15.1.2 所示,STM32F103C8T6只有USART1/2/3。
1 Y8 t" }1 o4 W5 K; S
8 r# p7 m+ S- Q/ F' B! G' Y \* F$ e
7 | `# [4 T0 @! t! C
表 15.1.2 STM32F103系列不同USART所支持功能 9 |. N+ G" U/ q6 s" w! o
USART内部结构的结构如图 15.1.4 所示。, R [* ^1 }' z' f- i& q
! _2 p: S4 E3 }, L. m) h' i
6 m- \7 j r( l
' Y% h) M ~& ]图 15.1.4 USART内部结构
) ~+ l T- _9 ], l2 R* I+ b. n
- `9 y6 k3 M+ _* V) J% [' i2 T1 V4 D1 `% r: ^* z0 L
可以把USART分成四部分:/ ]. h, S( X, l' @6 @0 u# L4 F! p4 L& `
/ _) X9 l1 a! j0 E& {6 Q6 H$ P7 S
①:USART引脚+ V7 g+ b& Q. ~- i
+ w7 q% v+ U% J) C- F! Z tTX:数据发送;- P5 V* O1 N" X$ a3 y
* ]! U* y# w4 T7 A7 N9 M
RX:数据接收;& w0 \: X0 {2 A3 x; {# O+ a i
7 v9 f b8 g$ q. l1 b1 m
SW_RX:在单线和智能卡模式下接收数据,属于内部引脚,没有具体外部引脚;
. b( e! e- g1 A5 \ }) K9 D% ^& L9 @8 } d- w! I8 z Z
RTS:在硬件流控制时,用于指示本设备准备好可接收数据,低电平说明本设备可以接收数据;. B# a5 w5 ^" j
7 [1 ]7 a3 X8 t# g, Y# k- o- z- E5 n
CTS:在硬件流控制时,用于指示本设备准备好可发送数据,低电平说明本设备可以发送数据;
{" \( D& K p U" [
# g: m4 f% N3 _0 o8 K. _. yCK:在同步模式时,用于输出时钟;8 x5 S2 e, @" n$ {
3 y& I, Y: U# l1 @, N, t②:波特率发生器
. D, A% \9 f9 Y/ N9 Q! q( R) K# \. | w5 `# l
通过设置USART_BRR寄存器的值,实现串口通信数据传输速率的设置。由《参考手册》可知计算公式为:
( [% i; M7 |% }6 g& ]( o$ M( h7 T# K q7 w4 B0 R1 |0 g
2 p) z5 e( d3 `% j8 j$ a8 V$ G5 {
5 X: y( G: m0 f7 u$ A/ ^# x5 q! V其中“Tx/Rx Baudrate”为波特率,“f_PCLK”为该外设USART的时钟频率,“USARTDIV”为USART_BRR寄存器的值。+ h1 n! Y) l+ a. Y1 R, `. G4 k
( d6 C' e- K H7 S2 F2 O9 @假设所需波特率为115200,当前USART时钟为72MHz,则USARTDIV=72000000/(115200*16)=39.0625。USART_BRR寄存器使用高12位[15:4]存放整数部分,低4位[3:0]存放小数部分,小数部分每一位对应1/24=0.0625。因此,整数39对应16进制为0x27,左移4位为0x270,小数0.0625,对应0x1,USART_BRR=0x271即可。1 M! }! F% o" Z4 o, o
& g: |% F6 r# u P" L/ U% z在利用寄存器配置USART的波特率的时候需要依据此公式计算USART_BRR的值,而在HAL库中无需计算,只需传入所需波特率,自动写USART_BRR寄存器值,但是我们仍然要学习这个波特率的计算公式,也许的开发调试过程中会使用到。
# M# j7 j5 y# O$ M/ d7 h8 Z: U' V; I
前面计算波特率需要知道外设时钟的值,由前面图 6.1.2 可知,USART1挂载APB1上,USART2/3和USART4/5挂载APB2上。由前面图 9.1.1 可知,APB1时钟最大为36MHz,APB2时钟最大为72MHz。因此,只有USART1的波特率计算中的能取最大系统时钟72MHz,而其它的USART/UART只能取36MHz。
: X3 E% M& `8 @; d- k7 f9 y
0 a# k' d/ `! g
3 E6 s0 d! D+ x7 L2 m" t. a* Y6 r: e
③:发送器/接收器控制单元" ^2 I3 z8 J {+ W( m, p
) O9 \. C$ M& `- n/ k通过向控制寄存器CR1、CR2、CR3和状态寄存器SR写入相应的位,可实现对USART数据的发送和接收控制。其中CR1主要用于配置USART的数据位、校验位和中断使能,CR2用于配置USART的停止位和SCLK时钟控制,CR3用于CTS硬件流控制、DMA多缓冲控制等。通过读取状态寄存器SR的值,可查询USART的状态。
2 h8 c( c: F2 \6 W e, E
" C# g# K" `. n$ B2 p9 A! r2 w; q3 \
1 T" E+ b) n- ]
④:数据收发寄存器单元; O& n" B; H3 R! E3 q
; c. u7 s% h' r9 ^$ f% P5 H该部分主要由发送数据寄存器(TDR)、发送移位寄存器、接收数据寄存器(RDR)、接收移位寄存器组成。发送移位寄存和接收移位寄存器,分别负责将发送数据并串转换和接收数据串并转换,从而实现数据在传输时,是一位一位的发送和接收。
y) r0 T: w* j5 A* r0 \4 \
2 |5 b& |9 A1 k5 @' ^15.2 硬件设计
2 K b5 B7 Z7 y1 F. v) d4 c9 g如图 15.2.1 为开发板USB串口部分的原理图,U2是UART转USB的芯片CH340G,可自动将UART信号转换为USB信号,右边是串口自动下载电路,在前面5.2.4 调试/下载电路有过分析,这里不再赘述。% M4 C) y- K, G6 h F j
7 G) `" C" i3 o J+ o9 Z# j. A$ s8 ]
MCU的USART1_TX(PA9)和USART1_RX(PA10)接到U2,U2收到数据后自动将UART信号转换成USB信号。; G* V" D7 d8 M
m# k, Q/ s8 C/ }' m8 C1 F如图 15.2.2 所示为开发板USB切换电路,拨码开关P2决定开关芯片U4、U5的6脚电平高低。如果6脚为低,开关芯片3脚与4脚接通,即串口的USB信号连接USB口;如果6脚为高,开关芯片1脚与4脚接通,即MCU的USB信号连接USB口,实现了USB口的两种状态。3 H5 l! u2 ?9 d& x( G- \
6 B" ?, x! l0 R9 \' q2 D
1 Q- S+ h4 \6 V- H1 A: y6 f9 [ S8 B! I* W6 H. [' s8 d
图 15.2.1 USB调试串口电路 6 `) d& P) |# ?3 Q- D \
- E9 I- }- X% L" m/ _8 y/ B
. K7 w$ \7 H" s( u; a( Q+ }, _6 S# Q: }- F6 c7 m
图 15.2.2 USB切换电路
4 D) Z$ }+ P1 @, d d( _9 c/ A0 H2 {# |. Q3 Q
& t- H& L- a2 V! H; Q+ Z9 t4 L; V15.3 软件设计; w1 ~/ J; x$ g; ?% w
15.3.1 软件设计思路
, l% V" a2 G$ ~1 Q A" c* D
5 D+ `& }* K( t1 O$ z0 _实验目的:本实验使用USART1向电脑发送打印信息。
. s# W# T; @4 _8 x7 }4 n1 w% X
' H/ ~2 m/ w( q, u! V, A8 V1) 初始化USART:设置波特率,收发选择,有效数据位等
: [- ^1 C2 ^' [( N( E, T1 L$ r1 v
2) 串口引脚初始化:USART使能、GPIO端口时钟使能、GPIO引脚设置为USART复用;9 o; _/ s% U# J9 x8 ]
0 X8 D2 K9 L0 U0 g$ n5 s4 w5 c, V' z
3) 重定向printf和scanf;
5 l/ E! E, ]$ p \/ D' Q( B
" l5 p$ C0 r& W0 ~, a9 S4 ?- G m$ Z4) 主函数调用USRAT初始化函数,使用printf打印输出,使用scanf获取输入;
# k7 G. t1 q* k, C! y7 g
) i" _. c( d! f2 u& h- y. k' g0 o本实验配套代码位于“5_程序源码\7_通信—调试串口\”。
+ X% [' Y' ]9 N2 {+ p) \. x, i# G8 O; Q
* \* M f$ T w4 o/ h& ^. w
7 L; ^* G3 a f# j/ z, K15.3.2 软件设计讲解
1 `0 Y* w3 m& J, l& a% z& k, B/ W
1) GPIO引脚选择与串口选择
0 V* H; W; O3 _0 x: a- P( y7 F+ f" q
代码段 15.3.1 引脚宏定义(driver_usart.h)8 }, w( n3 M# V' D
* [3 u b, Z; G2 l6 R2 j, S- i
- /*********************
( ?6 j5 ]0 R- \$ y- W8 m - * 引脚宏定义
( H2 o4 Z& K/ @3 O - **********************/
8 R9 n9 N& H0 R6 Q% I - #define USARTx USART1
* g% g. r: j5 Q6 y0 z - #define USARTx_TX_PIN GPIO_PIN_9
" C* v$ m) |2 U7 x - #define USARTx_RX_PIN GPIO_PIN_10
* i8 A; s3 N) G+ m) f0 W - #define USARTx_PORT GPIOA) F. m8 X1 J% W1 _% ~, ?% F
- #define USARTx_GPIO_CLK_EN() __HAL_RCC_GPIOA_CLK_ENABLE()
6 @8 v3 ~7 ^0 W9 ~( }6 l - #define USARTx_CLK_EN() __HAL_RCC_USART1_CLK_ENABLE()
1 h1 {1 C& _5 ]: U a7 z! n L - #define USARTx_CLK_DIS() __HAL_RCC_USART1_CLK_DISABLE()
复制代码
/ n, l- D$ l" E定义使用串口、对应GPIO、时钟使能,方便代码复用。* O1 o5 B1 A# P7 a, y
* z& @6 _* G1 `- F) S- J+ e4 [
# u9 |" J0 w! u& j- d; s6 C3 g- c* M1 y, x H7 o
2) 初始化USART
7 ], y# x3 h5 F8 q
& ^. A4 i# v" W# OUSART初始化包含两部分:协议部分和硬件部分。
) p( Y& A; I/ z% n+ ?$ A4 c+ A
& |. D7 {& O$ v8 X& X1 h协议部分与硬件无关,比如USART的波特率、奇偶校验、停止位等,通过“HAL_UART_Init()”设置。2 z- q9 [' J0 u$ E+ h: k. Q
5 W3 r5 b, f7 p& L
硬件部分指承载的硬件载体,比如串口所使用的发送、接收引脚的复用,通过“HAL_UART_MspInit()”设置。函数名中的Msp(MCU Specific Package,MCU特定软件包),就是指MCU相关的硬件初始化。
1 i- i; E" [' w* w! k" \5 q0 t+ l6 k0 G7 k2 Q5 e) H2 q
代码段 15.3.2 USART初始化(driver_usart.c)( C& A2 Z0 w0 G: Y. L6 f/ ?( ?$ M& @
0 B* b7 ` X) k- y5 {% C- /*+ ~9 @4 W( v3 Z$ ]8 o
- * 定义全局变量# r, c. c# X! u: g
- */
0 L; r0 C- ?5 L. k: h! n, D - UART_HandleTypeDef husart;
; F& t5 ~2 s+ O/ }0 T - 8 J/ y s% n" X+ r: q! ~% h% r
- /*
, ]# B) \9 z" q' L - * 函数名:void UsartInit(uint32_t baudrate)1 l2 U, D+ W `" a% i
- * 输入参数:baudrate-串口波特率
9 m3 L# h( M% P2 A/ T$ c" y/ s - * 输出参数:无/ J) R) @, Y# x" t1 c0 T
- * 返回值:无8 J5 {7 y" @! f$ G5 [. I2 A
- * 函数作用:初始化USART的波特率,收发选择,有效数据位等. \/ m" y- C3 e8 ?2 e) H) q! o3 o
- */, X) i$ ~( Q, _; W% V b% Z7 M
- void UsartInit(uint32_t baudrate)
1 s7 c5 v8 V+ ^ m" D4 H - {0 p& K. D5 t# @
- husart.Instance = USARTx; // 选择USART1
) u+ y; e/ F/ _7 A+ B6 r+ \ - husart.Init.BaudRate = baudrate; // 配置波特率
' j, W) ^8 D. \ - husart.Init.WordLength = USART_WORDLENGTH_8B; // 配置数据有效位为8bit& [% Q; o" o) ~4 w7 U
- husart.Init.StopBits = USART_STOPBITS_1; // 配置一位停止位& t) \& A! s" T+ T( z9 ]+ Z
- husart.Init.Parity = USART_PARITY_NONE; // 不设校验位- r+ S) [* { U" w# F
- husart.Init.Mode = USART_MODE_TX_RX; // 可收可发6 H* l3 B* M! G3 O3 g2 U
- husart.Init.HwFlowCtl = UART_HWCONTROL_NONE;
* [4 q' j/ `( o. s
, ]' x4 B2 H$ n0 Y7 v- // 使用库函数初始化USART1的参数
9 r" b a* _& `7 w! F. j - if (HAL_UART_Init(&husart) != HAL_OK)' ?# ^% N0 u5 f4 f4 O* o/ |6 m5 h' u
- {
6 i+ T' S6 e y - Error_Handler();
4 P: r% f! u5 ^" Y. n/ T- u1 m9 q - }) v/ m: y6 i, D# t/ k, K
- }
复制代码
) J# h+ }+ a8 c- _% z8 v/ B4行:定义一个“UART_HandleTypeDef”结构体(官方也称句柄)变量husart,用于保存串口参数设置;
; d0 M; H+ F: a) z+ D$ d
2 n6 `( r, M. D$ z+ [. h15~21行:设置串口参数,也就是husart:
* c6 M. ]* s$ ~% w6 {% [7 X$ z, j2 |4 e. f
Instance:USART寄存器及地址,即哪一个USART;
J6 P7 @! D. ` ~
" `4 Q; }5 C1 A# c u' \9 A8 jInit.BaudRate:“UART_HandleTypeDef”结构体里“UART_InitTypeDef”结构体成员,该属性为波特率;
: D2 K4 i [- _# [3 F {1 v$ W& @0 s g+ ^
Init.WordLength:每次发送的字节长度,通常设置为8bit;: _, v' p5 K: @4 y% A- W
- i4 d0 Q$ N6 t8 J tInit.StopBits:每次发送后停止位长度,通常设置为1bit;% e4 B/ X4 u; o4 `8 Z
. I' k P$ f" M0 y( p- U
Init.Parity:奇偶校验,通常设置为不校验;( q* q8 `+ [$ S. c2 f4 }" Y6 G+ Z
/ w0 ~, `1 M: ^5 K3 m% PInit.Mode:收发模式,通常设置为可收可发;1 V6 M( o, w( O2 R# c, v6 w
8 T* A* H6 U2 I5 R) GInit.HwFlowCtl:流控设置,没有用到(CTS/RTS),通常设置为None;0 H* o7 I3 H. f! s2 G
+ R; q3 `* U. x; m" q1 i24行:调用“HAL_UART_Init()”,传入设置的husart,初始化串口;
8 W/ V2 l6 y3 C F1 `& B6 U. I. n
" \, n( \# e" y“HAL_UART_Init()”会调用“HAL_UART_MspInit()”,从而实现对涉及的硬件初始化,用户需要覆写(HAL库提供函数名,函数内容需要自己编写)该函数,完成使能串口时钟、初始化TX/RX的引脚、设置USART1的中断优先级且使能中断等。
/ ~: F$ T$ d: L: i. w
+ W9 @1 V( y/ M( Q4 ^9 `代码段 15.3.3 USART MSP初始化(driver_usart.c)
" ?- C; a, n, r9 C0 u" D2 |& o' t9 t' |" c2 b' v
- /*( N; z, x' c7 l% ?, d
- * 函数名:void HAL_USART_MspInit(USART_HandleTypeDef* husart)
& o5 T2 h( h( P# h0 s - * 输入参数:husart-USART句柄
7 I: ^& T+ \" ~) H0 c' q4 B - * 输出参数:无& u9 S% d' i5 k; u
- * 返回值:无
! j% o+ E9 M! [7 p& e - * 函数作用:使能USART1的时钟,使能引脚时钟,并配置引脚的复用功能/ J- S# C6 J1 U! [
- */* P1 W: l% z' _) _$ v
- void HAL_UART_MspInit(UART_HandleTypeDef* husart). Q8 P/ ~9 `0 D7 y
- {9 u; k; }, N* R# b I8 X
- // 定义GPIO结构体对象$ L' z; C5 p: x9 ^2 u
- GPIO_InitTypeDef GPIO_InitStruct = {0};
' A; a2 T! X3 G! Y6 X, n( C - if(husart->Instance==USARTx)$ f# O! a; H/ |" f B$ p
- {
1 q$ p) x, J6 N- e! P - // 使能USART1的时钟1 g! K1 K% X* p8 N* A8 R, z
- USARTx_CLK_EN();
. c3 n* d1 {7 f4 i f: w - & B" I! r5 n6 E2 @; A
- // 使能USART1的输入输出引脚的时钟
' l7 ^. m9 r, Q& I! ^' J - USARTx_GPIO_CLK_EN();) J$ K( m8 D5 Z
- /**USART1 GPIO Configuration& |. {7 ?: E9 d
- PA9 ------> USART1_TX4 c6 e, b: o& R, L% f3 ~
- PA10 ------> USART1_RX9 R) f' J3 R9 |2 ]6 z2 B. p
- */# V3 e9 v0 g* ~$ U. M, v( F) X9 |
- GPIO_InitStruct.Pin = USARTx_TX_PIN; // 选择USART1的TX引脚
, o/ f) }* v* ^) P( b/ g - GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 配置为复用推挽功能
. t* H1 s' B5 N' `3 Y4 P - GPIO_InitStruct.Pull = GPIO_PULLUP;* s" s8 Q3 Y5 I1 C% V
- GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 引脚翻转速率快
3 h5 \# \2 I4 h" { - HAL_GPIO_Init(USARTx_PORT, &GPIO_InitStruct); // 初始化TX引脚& X6 X6 v2 c: F Z* ]1 r; u* }! v1 O* v
- . a3 W1 b$ @9 e" s
- GPIO_InitStruct.Pin = USARTx_RX_PIN; // 选择RX引脚
; m2 \" R4 G2 T( O - GPIO_InitStruct.Mode = GPIO_MODE_AF_INPUT; // 配置为输入
5 Z( u1 c. D: T! D4 t7 _3 h$ @ - HAL_GPIO_Init(USARTx_PORT, &GPIO_InitStruct); // 初始化RX引脚
6 y# \/ Y* n0 l: R8 C6 J - } a1 S6 v( c% @1 [. ?
- }
. R+ Q$ [! @/ \+ n
复制代码 - m# l! K* _2 q1 V( m- t% y
/ E. a6 Q+ T: J4 M
11行:定义一个“GPIO_InitTypeDef”结构体变量GPIO_InitStruct,用于保存GPIO参数设置;8 _8 t# e. W X6 c
8 p% m3 E. s$ o% j& V12行:当系统中用到多个串口时,都会调用“HAL_USART_MspInit”进行硬件初始化,因此需要判断是否为USARTx(宏定义对应USART1);- ], n2 R1 U' P6 w3 K
3 u& t( x9 q3 J! ~/ G V$ I( O
15行:使能USART1时钟;* {& U% i( X: ^5 M9 i+ |* O
Z y$ z) U" L3 ], [% K5 _7 H l4 q
18行:使能USART1_TX(PA9)和USART1_RX(PA10)所在组时钟;0 v5 ?1 g7 }( ~9 a
" O& u5 A( V# T/ C! b& G23~26行:设置USART的发送引脚:( ^% t$ |. X! K
: o+ ?. i0 z5 z# z9 R/ _+ c% \( cPin:指定引脚号;
6 V3 n2 R4 c' O/ K' _; a- Z+ ^; ^! ]# u5 u3 i' T j
Mode:配置为复用推挽功能;& B/ f& e" x( `* o
' Q" i4 Y( H- y, R- z
Pull:默认上拉即可;0 S1 W! f, U% f9 B8 S' Q2 D) R+ f
1 i% s* t+ J) ~3 v. z6 c1 r. FSpeed:作为通信信号引脚,设置为High;" n' L1 U/ `- ]$ ^, g k
6 ~7 d/ F, ^% K& d/ ~; v8 @27行:使用“HAL_GPIO_Init()”初始化该引脚;
, c- c9 h. n1 \; u7 ~" v4 j& X
" |' a7 I: Z2 E3 B3 ?29~31行:设置USART的接收引脚;- z! R% |+ M7 Q
# a9 Z- f" F8 V2 W
8 f: e3 ~/ @/ p
- p5 q$ a" V! _5 p, C. R( S3) 重定向打印函数
7 c& a0 z6 J' L& r5 M* t3 D; O; {' a/ d
以上初始化完成后,就可以使用HAL库提供的“HAL_UART_Transmit()”从串口发送数据,使用“HAL_UART_Receive()”接收数据,但这样使用不方便,需要自己处理数据类型。在学习C语言时,通常使用printf将数据格式化打印,比较方便。因此,这里需要重定向打印函数,在使用printf时调用“HAL_UART_Transmit()”打印。# h) B, X p3 K. y2 V! p
4 ?( b, g0 G3 ~4 m
代码段 15.3.4 重定向打印函数(driver_usart.c)$ `( A& U) d, q" c( G
! b$ W1 a" X% p0 ~- /*****************************************************8 x6 x- P. M4 r7 r1 f
- *function: 写字符文件函数
7 E& ]1 F0 H) J9 ~! C6 v8 }. n - *param1: 输出的字符
& F) q e- B" J# u* A - *param2: 文件指针
9 o* b+ X: g4 R; d- O5 p7 S - *return: 输出字符的ASCII码
. o9 W" K2 E) A) ]) ~& ? - ******************************************************/
$ H) f& F$ V* {- u5 p/ [ - int fputc(int ch, FILE *f)
& m* T, p; `5 {( m' s* f4 [ - {6 ~- l+ T( D0 B
- HAL_UART_Transmit(&husart, (uint8_t*)&ch, 1, 10);
7 x, w$ i; J2 \! U0 E - return ch;
; W. y% e7 e K$ G - } {5 U" e% R% }- h
- . [. }7 Q$ y6 m; x! d
- /*****************************************************
' v+ F4 T, x. D) t2 r6 g - *function: 读字符文件函数
, ~2 c0 _ R1 w. p8 q& g! J - *param1: 文件指针
$ ]( I7 t8 R# k$ g% Q - *return: 读取字符的ASCII码* \; W9 I, ^7 m6 N$ J$ F# ?
- ******************************************************/
) F$ k6 X' ~7 A* r - int fgetc(FILE *f)/ c# |8 V. Z, T0 g
- {0 r7 ^( v; b T- z6 [" X
- uint8_t ch = 0;$ ^ n0 P% c: c$ G: K. ~/ S* ]
- HAL_UART_Receive(&husart, (uint8_t*)&ch, 1, 10);
0 j3 M. ]: c7 h% h; r& D - return (int)ch;0 P4 b) Z; c' Y
- }
复制代码
# ]5 @% |1 k- @- g/ E$ w
. g1 C4 M" ~1 B( U' t \* `printf和scanf会分别调用“fputc()”和“fgetc()”,因此这里覆写这两个函数,使用HAL提供的函数实现收发数据。
+ O4 }) v% u5 m8 V! W8 I; O
c# j, M2 W. s0 O# B+ _/ G同时,还需要需要点击“ ”,打开工程选项界面,切换到“Target”标签,勾选上“Use MicroLIB”,如图 15.3.1 所示。
; [! R0 R/ E" G' w* }
) ?4 D1 [/ J2 {. j7 I7 }, P
2 S/ T: N% Q' f% l6 D" _: Y4 V4 Z6 B. f0 _ f8 d& i3 |
图 15.3.1 勾选“Use MicroLIB”
' d! [' l, F) P4 ]- l% l) k8 S* g/ c* f
3 z' }* W. T) r7 T
也可以添加如下代码,就不用勾选“Use MicroLIB”,两个方法二选其一即可。: C$ \9 Q" s) }& X% q. n
4 C! L) G# s1 {* P% T代码段 15.3.5 可选添加内容(driver_usart.c)+ U$ ~: G2 u2 A" T! ^) P$ \' h
" K% g* j x+ ~ e
- /*
9 J9 K" D# |. g1 ^% l. g - * 添加如下代码,可不在工程设置中勾选Use MicroLIB5 t2 K# m8 j# s1 K" L9 Z
- */* k: \* h' g5 a1 s6 I* r
" w5 ^+ b# v' w0 a4 O. u- #pragma import(__use_no_semihosting)1 `. x3 t. J& g0 \% q7 Q
- # s8 s3 v" u$ {8 ?
- struct __FILE( F4 o6 a( P- \: h% {* E, O3 k, V
- {
. L. O$ e2 D' h( \0 N# ? - int a;' X2 n6 O5 O" J0 O: L: K
- };+ y! ]" G V6 G
- 7 p' u, w9 J; L: B5 w
- FILE __stdout;' p4 \; }' N9 \3 q% z' J9 ~, Z
- FILE __stdin;6 t$ J+ N2 `$ S2 w# {9 `$ {+ m# S
- void _sys_exit(int x)
! z8 f6 @/ k; ]' N/ X6 m6 o+ D - {5 W6 K. g/ ~0 X8 N- E- A
- }
) i4 o" e2 r: Z0 G
复制代码
$ s. w$ h% A) q3 E4 \0 O4 S7 V- o
: T, b/ n" \ `2 s4) 主函数测试
2 V2 Y- ]& K, Z* g4 G) _$ O, ^. G2 m' V# V5 V
- // 初始化USART1,设置波特率为115200 bps$ B7 ^5 Z3 c# ]& U7 |0 P! r
- UsartInit(115200);
7 M6 {" Y/ N- J1 a - 2 F1 g! e1 J3 @7 d
- // 在windows下字符串\n\r表示回车
3 U# L0 ]' @& t# y' j( Z' l7 Z - // 如果工程在编译下面这句中文的时候报错,请在魔术棒“Option for target”->"C/C++"->"Misc Controls"添加“--locale=english”
' e! Q& s* z) x5 ^. K5 y0 j - printf("百问科技<a href="http://www.100ask.net" target="_blank">www.100ask.net</a>\n\r");8 x! X; q7 ~# F( }
- printf("UART实验\n\r");7 Y2 N; i, Y- v7 r2 |% X
- printf("test char = %c,%c\n\r", 'H', 'c'); i9 Z7 c, }# S
- printf("test string1 = %s\n\r", "www.100ask.net");
, x. e8 z. B- V - printf("test string2 = %s\n\r", "深圳百问网科技有限公司");3 R3 I# y2 ?5 h# K& x; h
- printf("test decimal1 number = %d\n\r", 123456);6 j$ [, L* B0 @. W( s9 X: O+ c% s
- printf("test decimal2 number = %d\n\r", -123456);
% g6 r+ l5 Z, y) f- K* Q - printf("test hex1 number = 0x%x\n\r", 0x123456);# w5 |) k0 v: ^. h! | X
- printf("test hex2 number = 0x%08x\n\r", 0x123456);
2 j0 ]; ] K( B* \1 W( I& A - printf("test float = %.5f\n\r", 3.1415);9 d/ h* R7 l; e) d2 x
- printf("test double = %.10lf\n\r", 3.141592653);4 d4 R$ R; A8 _& q; ^ X0 l" L
- 8 V9 G5 h" t/ Z
- 2 i! K' \% _6 S+ U- F! R
- printf("\r\n键盘输入‘C’或者‘c’控制串口打印‘Hello world’");
], g# ?$ ]) J' u - while(1)$ U! }( A2 K' h4 g; V" K
- {
. M2 o+ z# s6 b9 I - scanf("%c", &cmd);; n. V( A, ]3 @% `
- if(cmd=='C' || cmd=='c')
9 V n" ]+ w7 r& x% O0 t" T3 ? - {
& |: G- b, g6 U% H) D/ e8 s, ?) m - cmd = 0; M) l- R. b5 g0 [+ j+ p# E# @
- printf("\r\nHello World.");, E8 F$ h' j {0 P( X
- }
2 V0 o3 w+ i7 v* }) V- L* L - HAL_Delay(100);
. a& r1 R# t: C9 D - }
复制代码
8 O: g$ P8 B0 l, X0 c) I- Y主函数直接使用“printf()”便可调用USART1打印,使用“scanf()”便可调用USART1接收数据。% Z) \- [/ j$ l! D) q: d* K
; Z' r2 g* e4 }% D& R2行:初始化USART1,设置波特率为常用的115200;
9 @' b1 d' i5 p3 y# E6 e( W& [$ f8 U9 L$ m6 Y* U1 {/ ?' w
6~7行:中文打印测试;如果编译报错,需要点击“ ”,打开工程选项界面,切换到“C/C++”标签,在“Misc Controls”里加上“"--locale=english"”,如图 15.3.2 所示。
0 ~$ D# e( @& A3 B: {
8 K# H2 R5 [1 h8行:字符打印测试;
0 y5 w; L6 k# l# f8 w8 Y, j3 X
9~10行:字符串打印测试;
6 d9 ?8 q& T* Y& D& H& O
5 q. C* a" ]# q) ]8 `11~12行:正负整打印疑测试;
4 e/ \9 q$ q9 T; ]! a8 a v+ _9 ?* s$ Q4 H# H
13~14行:十六进制打印测试;; E. @1 v b! c$ k D
\: P1 ?8 Q. K- R3 {15~16行:浮点数打印测试;
+ Z% G1 }. n$ G; _2 d8 l
I6 S& @" I; |- k7 j+ S21~29行:通过“scanf()”函数获取输入,如果输入的指定内容,则打印指定内容;
+ \9 |" _, A% G7 s$ j* v T. k4 X3 \% G& R& d6 {
# [9 m* z1 y* m& A
; x( t j. F" V) l3 E- t: ~4 |图 15.3.2 指定本地编译环境 4 q4 x) a1 k0 J1 ^% _9 \
- ?6 q# b$ W/ u# }. d3 F5 l0 V6 I' O8 h" w# P4 L
15.4 实验效果
9 E, L, y# s3 `/ H% E本实验对应配套资料的“5_程序源码\7_通信—调试串口\”。首先如图 15.4.1 所示将开发板插入电脑USB口连接,注意拨码开关拨向非ON一端。打开MobaXterm,设置串口会话,如图 15.4.2 所示。
+ @% z2 r ]7 `: V- {2 N- ]6 K+ l0 c( F9 X5 c/ v
①点击做上角“Session”,创建会话;
6 o/ ]& U6 S# D8 W" U$ Z
9 p* |% G! w( \ p! p2 {7 X$ R, `②在中间弹出的窗口,选择“Serial”,即串口;
K; ` ~4 U/ l0 k- e5 I9 M H2 F- O- I+ x1 l8 l7 R
③下拉选择串口端口,我这里是COM6,读者可能编号不一样,但后面显示的芯片型号是一样的,选择“USB-SERIAL CH340 ”即可;
$ d/ E% X. O, D$ W( _+ M4 @$ u/ U0 M ?# i
④波特率选择115200,与主函数中串口初始化设置的波特率保持一致;/ z8 t4 j* g- V: s2 k3 Q
+ V. f2 O( f" a' Z. I
⑤以为串口是异步通信,需要双方统一传输规则,这里和代码段 15.3.2 中的设置保持一致。数据位8位,停止位1位,无校验,无流控;( i) M' q( j% L
7 f" l$ ^2 Y5 v⑥设置完后,点击“OK”;
/ }; ?5 [, n3 Z8 m$ E! K0 y+ g0 C2 ^: r
3 `1 T" } @1 \6 ~( \' |
$ k) ~+ b+ M4 U打开代码工程,使用Keil编译,下载程序,MobaXtrem显示如图 15.4.3 所示界面,所有打印正常显示。在电脑键盘输入“C”或“c”,即可控制串口打印“Hello World”。/ c! r. `; M' `& g: d5 n
9 s3 J: A9 z0 E3 \+ [; ?% `图 15.4.1 调试串口连接实物图
1 q6 x7 ?# ? y/ r, W! S
: r! y5 {0 [( E) X' U3 N. R7 I7 z5 p# ^
图 15.4.2 创建串口会话
* \$ Z; C+ P5 y( S4 J5 |0 J0 ]9 K- G" c, B' ^# v' l t1 y
# Y; t% W q" `1 o. l* K0 |
8 s- C( j- n4 W; P) M
图 15.4.3 串口打印效果
! U2 J ^' L' e" k3 j4 Q( t/ H( w3 \" y# Z4 ]
4 J* r6 }) {) [; L6 s/ g
作者:攻城狮子黄
4 Q8 Y D0 K/ y; i
# D3 |& Q0 G+ ~5 H; Y8 X5 G t8 ?. o' f3 o% G& M* W
|