介绍 w" C" f J$ g6 P- n, O2 ]3 v
串口(UART通用异步收发器,TTL)通讯是一种设备间的串行全双工通讯方式。由于UART是异步传输,没有传输同步时钟,为了保证数据的正确性,UART采用16倍数据波特率的时钟进行采样。因为它简便捷,因此大部分电子设备都支持该通讯方式工程师在调试设备时也经常使用该方式输出调试信息。
3 x! w7 r0 ^+ b本文详细的介绍如何来编写一个串口收发程序,我们采用常用的收发逻辑,发送直接编写函数进行实现,而接收使用中断进行完成。接收中断使用接收到一个字节和一帧数据两种中断触发方式。
: x# C) H3 r% v2 u6 ^9 T; A& d# W4 k0 u/ I' E( S" d$ R
USART中断
+ ? r4 v' E8 h1 ^. R9 ?" }; c. |USART 有多个中断请求事件。
) W: ?" z( j d6 U% f- \
( [. ~* w5 S, w
, B* e2 a# f$ Q9 J
之所以介绍这个USART中断请求,是因为很多人在初学阶段,对串口怎么判断串口中断的状态不太了解,所以我这里重点来介绍一下。/ U6 G( {( g# h
一般在我们开始和配置完串口中断后,进入串口中断处理程序的情况会有很多,我们也可以自己选择打开哪些串口中断情况。一般情况下,我们在接受时主要使用的中断事件标志是RXNE和IDLE。/ b0 _0 g z7 m' [$ ^
RXNE是接收中断,每接收一个字节都会出发这个中断,也是我们用的最频繁的中断请求。+ Z, }( ` ~; }, r; {
IDLE 是空闲中断,每接收完一帧数据,总线就会暂时空闲,就会触发这个中断。" c* C& Z3 {# N) m0 d
' q% d& G$ j1 P8 d: b$ q. D串口状态% @8 P( u* a0 i" b- i0 k
串口的状态可以通过状态寄存器 USART_SR 读取。USART_SR 的各位描述如下:# ?' M7 k4 D2 {. Q
2 b- K, z7 }3 _) O% K0 e& l: V
% I4 }4 t+ m& y. K* I. g
' i$ f* `2 g4 {; v
这里我们关注一下两个位,第 5、6 位 RXNE 和 TC。
8 _3 j% s8 C( R& i, `) KRXNE(读数据寄存器非空),当该位被置 1 的时候,就是提示已经有数据被接收到了,且可以读出来了。这时候我们要做的就是尽快去读取 USART_DR,通过读 USART_DR 可以该位清零,也可以向该位写 0,直接清除。& l# U0 a% M( ~) _
TC(发送完成),当该位被置位的时候,表示 USART_DR 内的数据已经被发送完成了。果设置了这个位的中断,则会产生中断。该位也有两种清零方式:
' h' L; _' G3 G2 Q5 e1.读 USART_SR,USART_DR。
, n. m$ q- R' v* P! ]2.直接向该位写 0。
1 F# k. A+ N* u [, n6 R1 K5 s7 o' J3 j( N% a8 J' G0 P
实例; b) W! F5 h( b1 D- I9 `, x9 k
需求分析: C* |& \% }' v& H) x2 K. l
本项目主要编写一个串口收发的实例。使用STM32F103C8T6充当MCU,在PC上使用串口调试助手充当上位机。每次PC向MCU下发一帧数据, MCU每接收一个字节数据,检查一下数据中是否有指令0x23,当接收到指令0x23的时候,MCU向上位机发送“PC”。当一帧数据接收完毕后,MCU向上位机发送“Receive a frame data”.
! D0 [7 x) p9 G, _. Y9 Y0 E) n
( D9 A( s( t+ @; @1 \/ S串口初始化
' [( H. z; v% o4 w. p3 m- f5 \串口初始化的一般步骤可以总结为如下几个步骤:
) o6 R1 S+ G4 z' e6 j4 h1.串口时钟使能,GPIO 时钟使能。
# C- J9 q" B! w3 k: p2.设置引脚复用器映射/ B: n/ L, S+ f
3.GPIO 初始化设置:要设置模式为复用功能。
$ U% ]0 l( V1 D% ^( Z4.串口参数初始化:设置波特率,字长,奇偶校验等参数。
; Z6 z( s/ d6 K7 C. u4 ~6 ? m* u) F& E5.开启中断并且初始化 NVIC,使能中断(如果需要开启中断才需要这个步骤)。
/ a0 o" s4 A% O9 J# a+ D9 W' j, @8 S6.使能串口。
9 ]. q) O: ^/ V& [0 g/ ^
* V0 D: H0 k, W+ J) A; \% l* {2 F- NVIC_InitTypeDef NVIC_InitStructure;
2 R$ U" H4 Y/ `+ }+ k - GPIO_InitTypeDef GPIO_InitStructure;
7 Z8 z7 ~: A5 _- g' x( ~' g# q% ~ - USART_InitTypeDef USART_InitStructure
' b4 ]7 l3 Y4 `1 O2 U3 {1 N) { - RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);
) i( E" f$ _! }( g - RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
* |, [7 R; N" ?% s7 R: Y5 ? - RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能DMA传输 n+ V u; F! I0 n
- //USART3_TX GPIOB.10
+ G j4 ]4 [* W* |8 F - GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
. @) L0 ?+ d! V2 i - GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
% M4 T6 N- N) b0 ]3 q% B; i4 t - GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
# o9 x7 [- G' J. n4 o. Q - GPIO_Init(GPIOB, &GPIO_InitStructure)
7 l! t% C9 _# Y9 H7 h - //USART3_RX GPIOB.11初始化; n# y8 s% s. {8 m
- GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
$ @& b( ?, _( Y; t" \% d0 Z4 p - GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
! v8 ?% A9 Q* O2 ] - GPIO_Init(GPIOB, &GPIO_InitStructure)
D4 o2 s/ f# B8 m! Z) R3 | - // 配置波特率5 l- I P# \" g7 K9 q
- USART_InitStructure.USART_BaudRate = bound;
/ ?, ]+ P9 |- m* {5 T$ j - // 配置 针数据字长
# C8 m& a6 p/ W- e! v- t1 q - USART_InitStructure.USART_WordLength = USART_WordLength_8b;# C/ R8 \/ }2 J% @+ r+ R; }5 q
- // 配置停止位" f- |( R7 ]: H- D
- USART_InitStructure.USART_StopBits = USART_StopBits_1;' l j D s# _) K8 T7 Q+ q) J% Y
- // 配置校验位
; \5 j$ c# o- ]9 i - USART_InitStructure.USART_Parity = USART_Parity_No ;8 D) C: f v* I6 |
- // 配置硬件流控制
( X* v% K& r& e$ `3 E - USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;* n7 g3 n- J- \/ \ E, t
- // 配置工作模式,收发一起2 T' E$ \! j- h8 T4 {, Q- B3 V% |
- USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
* T3 T+ ^# B( b& K/ T$ m' y - // 完成串口的初始化配置8 G% G# ]$ G s& F5 r
- USART_Init(USART3, &USART_InitStructure);
. _ Q; g& o/ R) A% f - // 使能串口接收中断
: B' T4 y% Q# u4 U$ v - USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);" _" M, c/ z0 C4 l
- USART_ITConfig(USART3, USART_IT_IDLE, ENABLE);
# _- W1 w9 k* ?" q8 Q$ W! I - USART_Cmd(USART3, ENABLE);* U9 k" d, W! ^* H2 n0 b% U
- // 清除发送完成标志( U% ~9 s+ h9 S, W8 p
- NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn;2 A; A% V {9 C4 n8 ^/ F
- NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0X06;+ x7 G/ Q! O1 j# F8 M6 W
- NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0X00; X! e6 x$ Y$ J6 \) r, G4 b
- NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;5 \$ d+ m; K! A) Y* b( _
- NVIC_Init(&NVIC_InitStructure);
复制代码
, q' n" J1 D3 n我们平时需要改的其实就是串口的一些参数配置。% i2 Z3 ?6 C0 Y/ v! e
BaudRate:波特率, C" e+ S" {6 H" Y0 [; J# y
WordLength;:字长! g& l+ |" h) m1 P
StopBits:停止位
# {1 S% Y) ^4 _' k# mParity:奇偶校验
0 O* p, Q$ ~- y1 ^6 k# ?Mode:收/发模式设置 @6 p: r" V1 x; D- W, F
HwFlowCtl:硬件流设置& M- V: y; u0 h; r
OverSampling:过采样设置
. Y; C$ ~+ I, {
6 O k8 P7 }2 K( J: \) t4 R串口发送
: e- R$ R, {' S' ~! o串口发送这里使用的非中断发送方式。# k" a1 R) j7 a) V$ {' y
# }( s ~) j/ }1 W+ e1 U! F- /*******************************************************************************: [6 v) v h. l( Y1 F, d! p
- * @函数名称 USART_Send
0 P( r; F) S5 \/ \1 f - * @函数说明 发送信息* A% y6 R0 k) g# w1 s8 ~* {
- * @输入参数 _UART:串口号
6 q# U9 P' d! a+ {# B# H- q* O - data:要发送的信息的首地址
% \' f6 t: F0 Z: m0 D; @- @ - len: 发送的长度4 Z, g: B6 o4 b3 n8 o
- * @输出参数 无
, |* o# u4 A/ W7 k. P8 E - * @返回参数 无- l2 V7 a4 e7 m8 l9 j% \- ]
- *******************************************************************************/
Z1 g3 p0 i- u; @% B0 N - void USART_Send(USART_TypeDef *_UART,u8 *data, u8 len). F5 C2 v' V6 Y4 \4 z5 x) U$ }
- {/ x, B: H* x+ `7 G) b" U0 {
- for(int i = 0; i < len; i++)
5 Z5 K' g# R B! y/ Y# X2 r9 R5 k - {; i: c# P& l: R
- USART_SendData(_UART, data<i>); //向串口发送数据
% ^+ n! k+ ~, H$ l N - while(USART_GetFlagStatus(_UART, USART_FLAG_TXE) == RESET); //等待发送结束: P2 e R, @4 P1 f) J
- }
1 J- C3 I; U% O W - }</i>
复制代码 0 \2 B6 F9 f/ u' P) O1 u. M# W
主要使用的是USART_SendData(_UART, data)函数,USART_SendData函数是标准库中自带的函数。; z ]4 T4 ~3 A
_UART:串口号5 i5 O3 A8 O1 \$ s8 U
data:发送的数据
/ M# A# ?$ Z% |& v: d v/ R4 D. ?每次发送一个字节的数据,但我们要注意,当发送多个字节的数据时,可能会造成前一个数据还没有发送完,后一个数据就已经要开始发送了,解决这个问题的方法就是使用USART_GetFlagStatus(_UART, USART_FLAG_TXE) == RESET判断是否发送完。
; Z3 b+ P- j: p7 |$ n" R8 \. W( G
: S4 k/ M- K7 n串口接收
" R! B- a8 b1 g这里串口接收使用的是中断的方式。2 v1 a8 u1 B3 q. x% C: G
中断的类别在文章的最上边已经介绍过。我们在初始化时设定触发中断的类型。本文中设置的# x4 z5 V, {2 G# e
' r- s; W" n" B; T7 v1 H/ P- USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);7 W+ G1 {/ p( N# z8 T
- USART_ITConfig(USART3, USART_IT_IDLE, ENABLE);
复制代码 Z$ M9 R$ L; n6 ?3 E7 Q
代表只有接收数据和空闲中断会触发。8 L$ N3 T- D$ c* g/ C( M' g
在stm32f1xx_it.c中有我们的串口中断处理函数。我们将这个函数进行重构。8 C/ d; _4 g9 L/ C1 L
1 J1 q( ~1 f# z
- /*******************************************************************************1 L% q8 y# B( y: T, R
- * @函数名称 USART3_IRQHandler- p0 V" Y" a& e4 ~6 l
- * @函数说明 串口3中断服务程序
7 q% B, {; \. B* c& U& T5 B - * @输入参数 无4 X5 e/ I% {4 z( g, o: Y
- * @输出参数 无
4 U7 E9 r: M- k. D1 E- L/ l# d - * @返回参数 无9 k3 r# ^! U }
- *******************************************************************************/
5 \$ F( v% o5 @ - void USART3_IRQHandler(void)
# X$ D$ ~$ j& V; j - {0 O' ^% O# F/ H! h+ @" H1 ?
- u8 Res;
) C+ ~2 @* S1 x( X9 ? - if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)( p) ?& ]" l0 |" T; A9 M' i
- {& i& Y; R+ B; v. L$ i; x6 s
- Res = USART_ReceiveData(USART3); //读取接收到的数据 - d5 o% A& m" h4 _2 J
- if(Res==0x23)$ g1 V5 o! t# K7 X
- printf("PC");
* t9 w, u; y3 R7 {5 O5 y2 I: o - }$ ~8 v! M- A' h9 a
- else if(USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) //接收中断" G+ F4 P3 e8 d
- {
# K) o) J: d9 Z% b/ I- ] - USART3->SR;//先读SR寄存器, H3 y+ T2 c5 S
- USART3->DR; //再读DR寄存器6 I* e# W8 d; z* z1 i" {) }
- printf("Receive a frame data. ");2 N \) u$ N* r" \4 Z/ D/ d
- }; F% A* f4 f/ M
- }
复制代码
8 R* B. R r t$ O4 r# J4 {这里面的几个重点,我们来一一介绍。/ [0 S' J( x& `' k% ?& H; |. O' j3 S
首先是判断标志位,我们使用标准库中的USART_GetITStatus()函数,里面有两个参数,前者是串口号,后者是具体哪个标志位。
' a7 o/ k: A- I' Tif(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)用来检测是否检测到有单个字节的中断。5 r' e* V& g/ c% ?( t# m( G* m- n/ n
if(USART_GetITStatus(USART3, USART_IT_IDLE) != RESET)用来检测是否有空闲中断(代表这一帧数据传输完了)。& l7 B' {/ o2 v! B6 x
$ ?7 X1 u% u0 p/ b: L
重定向printf和scanf
: F6 a2 X9 C H, v( `6 [* E R8 k0 ^还有一点需要注意的,使用 fput 和 fgetc 函数达到重定向 C 语言标准库输入输出函数必须在 MDK 的工程选项把“Use MicroLIB”勾选上, MicoroLIB 是缺省 C 库的备选库,它对标准 C 库进行了高度优化使代码更少,占用更少资源
F r+ _% A. U; V4 L/ P1 _9 l为使用 printf、 scanf 函数需要在文件中包含 stdio.h 头文件。8 V& W* n3 T3 {& e
- B/ @9 y4 D8 b6 r! J/ W
- #pragma import(__use_no_semihosting)
3 y; G$ K) e v" R9 Z - //标准库需要的支持函数) w0 U4 r% H7 \9 D; l. R
- struct __FILE
2 W. Z3 E8 Y/ \9 i" l - {0 H3 \5 t- R! A( w
- int handle;) j+ X4 C5 E& h
! J, p- p! O$ S& {% ?+ H- };
% ]$ x4 T# W( [" a) ] - 2 p# y" P$ ~/ A2 U! v0 \ v0 {6 l5 i
- FILE __stdout;
( p& v2 E+ b; g- i$ I* a" _/ G - //定义_sys_exit()以避免使用半主机模式
* S) t- z8 w% g$ {. F, G - void _sys_exit(int x)
8 y9 l8 h( }: _0 }0 A* b0 R& R - {# D9 ~% L/ A3 v1 Q# X3 M
- x = x;
& b& t+ Y6 M+ d9 n, p5 V - }
& f& x& L& K v: G' r& h - //重定义fputc函数% ~8 ^% t$ y" n+ s
- int fputc(int ch, FILE *f)/ e7 @- o% D8 Z: S* z
- {# {% e& J( a( |4 M0 L
- while((USART3->SR & 0X40) == 0); //循环发送,直到发送完毕
: T* A2 \* \. L) g N
3 P8 B4 h. ~ q( Y- USART3->DR = (u8) ch;
7 N. A1 c; u! R. V( p. h# Z/ x - return ch;5 b) b+ C4 Y/ i1 u. A5 v4 \+ o
- }" y1 t+ L8 J$ [ ]+ {: o
复制代码 * Z. S! P5 @' V! K
效果
% z& @) W6 `# [. ?, `6 g: \1.PC下发:11 22 33 44
& N! }8 t/ e7 G) G
, T0 O5 V( z. r. [5 k `# T# p
% b1 S) g4 [* ^$ a# g
2 M+ H ]2 b* G: N+ x- E8 D2.PC下发:12 23 34 450 e4 B" ]; F+ T6 e' [
1 }" t6 l1 |2 z {; c# c" V
: i, C% O5 S, {* \转载自: 跋扈洋7 S5 X( A- P% G( W
- s9 m( \8 ~* ]; `1 }' f
! m2 w2 e0 s; s' C; h |