01、概述2 a4 \" L0 z: i/ o' |9 |9 j) A
2 g3 w1 _2 ?6 ^9 Z. }$ f* c在串口讲解的文章中,示例代码采用中断方式接收和发送数据,中断的好处在于可以及时响应,快速接收到数据,但缺点也很明显,那就是频繁中断,接收1000个字节需要中断1000次,频繁中断就意味着会打断其他代码的执行,对一些应用场景是不允许的。这个时候,使用DMA+串口的组合就可以很好解决这个问题。% a- i' z$ W" P/ }
$ t; R0 j% o0 u+ O1 L9 |& c- G' R
; g7 }8 i8 n& V0 O, c' i+ ~2 d
$ f" Z0 U) n+ o' x9 I+ O, R0 X
DMA每个数据流有8个通道,每个通道映射到不同外设,这有利于针对不同的产品配置不同的DMA外设请求。. `2 _" s1 ?0 b4 }2 [
+ K4 ^" a. N/ C3 s6 M, F2 S每个数据流只能配置为映射到一个通道,无法配置为映射到多个通道。即,与数据流不同,每个DMA控制器可以同时配置多个数据流(因为有仲裁器),但每个数据流不能同时配置多个通道(因为只有选择器)。
7 J8 {! }- j# B$ ]: I8 B' z3 h
* U1 T1 d" d( u* ~/ k! k0 J3 n我们使用USART1串口外设,从数据手册中可以查到,USART1的发送和接收都是支持DMA的,使用的是DMA2.
8 { g T9 Z8 J! _% H" D
- _! y2 l# f e3 }( z4 ?1 i
i3 {$ S3 `, j; d2 p4 @" Q2 l4 D# U' E5 ^7 b/ p
接下来我们循序渐进了解DMA在串口中的应用
6 \# Z" O1 C0 r K
0 k+ O* K8 {! Q% }3 M02、DMA接收5 @/ R& F) I* p5 B( h
我们先配置DMA,将DMA外设和串口联动起来。首先需要配置DMA。
& B/ P: C1 Y! V; H8 y. K" B) P/ ]9 t, e1 h1 G% ]
- void DMA_Config(void)
2 O' {( n3 {) A; } - {
" s6 f2 \6 z) E1 i3 Y1 v6 b - DMA_InitTypeDef DMA_InitStructure;
) ]# d! l% ]9 d' ^& k1 U$ \' q
$ K9 O1 a/ v; L) F, }! e6 Y) p6 \- /* Enable DMA clock */* L% [9 f$ l4 y( a( A2 C7 k
- RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
7 j0 o% [( O. K3 K* W. F - 5 o$ x" u- S) K
- /* Reset DMA Stream registers (for debug purpose) */
, g5 S& ]. U7 ^, {5 ` F b0 U& F - DMA_DeInit(DMA2_Stream2);- E ]; Q$ d/ |1 y
/ K2 l# E! Z: Y( R' k( C- /* Check if the DMA Stream is disabled before enabling it.
. d! X1 L. s7 b( H$ t - Note that this step is useful when the same Stream is used multiple times:
3 Q" i0 P3 n# H - enabled, then disabled then re-enabled... In this case, the DMA Stream disable
+ k3 I2 G W- m- ? - will be effective only at the end of the ongoing data transfer and it will
4 c$ Y) Q7 _( T _ - not be possible to re-configure it before making sure that the Enable bit
6 q2 }2 Y, e' n/ k - has been cleared by hardware. If the Stream is used only once, this step might 7 u6 U1 }. }2 k/ g& }+ L. ?# Z) f
- be bypassed. */. e! t/ X; U- V! s: c
- while (DMA_GetCmdStatus(DMA2_Stream2) != DISABLE)( | U: \. K. I+ I0 B& j
- {
* C% O! h1 R$ O& W9 k - }
6 [* W2 R/ x& _2 v( i9 ]
p7 O4 W+ d/ G: I' c- /* Configure DMA Stream */% t" B$ B- r% b# @- H9 ~
- DMA_InitStructure.DMA_Channel = DMA_Channel_4; //DMA请求发出通道
1 w0 [8 e1 K6 p: E$ Y8 c! K - DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;//配置外设地址+ z' _" D |3 \/ n* P+ s
- DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)UART_Buffer;//配置存储器地址, V2 B9 B( y$ z& k" i
- DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;//传输方向配置8 l1 ]5 Q, z0 j2 K$ v; V
- DMA_InitStructure.DMA_BufferSize = (uint32_t)32;//传输大小
% z5 W; P. t1 a' w D7 C+ ? - DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设地址不变8 V- @% V8 Q! e9 w6 U
- DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//memory地址自增0 b$ s5 w) C f/ W7 O
- DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设地址数据单位9 `* w% [. Q: z w# g' Y
- DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//memory地址数据单位+ b& I' Z) i% P8 k9 F
- DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;//DMA模式:正常模式
& G! x# k! ]+ [' H' H - DMA_InitStructure.DMA_Priority = DMA_Priority_High;//优先级:高: j% ?3 u% l+ }
- DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;//FIFO 模式不使能. 7 s( j5 E% _& U. L# E O
- DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;// FIFO 阈值选择
) Z! V% j) f. A4 M% d9 B# P) B - DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;//存储器突发模式选择,可选单次模式、 4 节拍的增量突发模式、 8 节拍的增量突发模式或 16 节拍的增量突发模式。% v) y8 v6 j0 P+ r9 w( L3 y& ]& g
- DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;//外设突发模式选择,可选单次模式、 4 节拍的增量突发模式、 8 节拍的增量突发模式或 16 节拍的增量突发模式。
) p3 h. R7 a& h# @+ y, ` - DMA_Init(DMA2_Stream2, &DMA_InitStructure);
# R5 E: d0 U# E- ?
+ N O$ Y$ w: o/ D0 n- /* DMA Stream enable */" v* {7 ]! L. l* c
- DMA_Cmd(DMA2_Stream2, ENABLE);& F5 t; k/ L3 R1 s
- }
复制代码
, ~. z2 S/ m2 ^; Y$ z8 V2 e: i) w6 C% f
除了配置DMA外设外,我们还需要配置串口对应的DMA配置,在手册有一小章节讲解到。8 P' ~6 A( _ f4 ?
& `# O7 n1 H1 i" @ z% g
: Z- T! |2 v2 {: v/ H; f2 }; c! @( j3 L: d
需要配置的寄存器是USART_CR3寄存器。6 z' u4 a& c9 l- d; H3 i
+ ^: E9 X+ b% l: ~
* L/ h- k5 i- {3 ^6 r) \1 j
. f4 K2 g/ u! f8 O* I* E我们可以通过配置USART_CR3寄存器的bit6和bit7使能串口发送和接收DMA。ST的标准外设库同样提供了对应的外设库。9 H+ R) N2 \2 _ @) z1 F! l# x" x
U O0 _/ V% \- void USART_DMACmd(USART_TypeDef* USARTx, uint16_t USART_DMAReq, FunctionalState NewState)
复制代码 ' X+ o5 r; B& s" l1 |6 r
通过上面接口可以配置串口的DMA配置如下:2 Z. J" [$ l2 E1 |
; x( Y! J) { K: `+ E+ Z& \$ L
- /*使能串口DMA接收*/& t: D- l4 U4 K& J/ v9 O1 w: D5 D
- USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
复制代码
$ A" l/ Z T k03、中断& [: Q |5 H) v, l8 F' b
我们使用DMA+串口解决了频繁中断的问题,但现在有一个问题,我们还需要及时将接收的数据信息通知CPU,以便达到数据的及时性。我们使用DMA和串口两个外设,他们都有自己的中断。, c" D/ i1 `9 \( A. W6 f& i- F
3 s! l* C5 w' j% ~6 ~
使用DMA中断,如下配置
# Q( L+ G3 Z9 n+ {( |) i; d( o( ?6 T. _- F2 D1 M
- /* Enable DMA Stream Transfer Complete interrupt */9 ]* [3 M0 X: s K
- DMA_ITConfig(DMA2_Stream2, DMA_IT_TC, ENABLE);
, @/ L( o# D: v; F2 m/ ~ Z0 \
7 n& i1 l- |6 J- /* Enable the DMA Stream IRQ Channel */8 D9 V# _* G7 W/ _( D0 q
- NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream2_IRQn;: U8 {3 G4 c/ m9 ]# C. ]8 e
- NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;, B1 `. c3 |5 s8 }0 S( s- @
- NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;- r; w, g' D1 w1 C. P
- NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;+ M! {; k# y N2 K5 }' z
- NVIC_Init(&NVIC_InitStructure);
复制代码 6 _& H8 F$ s" r6 N
当DMA接收完毕时,会产生中断通知CPU取数据。- e; y" R5 e$ b; K. ^3 W; B" x
5 W6 F# X3 Y& s$ V7 G但这有个明显的缺陷:串口接收一包数据,长度如果小于DMA的缓冲长度,那么久不能触发中断,只能等DMA接收满数据才会产生中断,如果下一包数据迟迟不来,那么这一包就不能被及时响应。
* p% B- u, F* |8 w. ~6 P- ]/ T+ V8 l/ U3 q
那么我们采用串口中断是一个不错的方案。串口提供了一个空闲中断,“似乎”就是为了DMA专门使用的。6 x/ P( w( n6 @" v0 a' R5 [
6 v6 [# U) g, r9 I
7 v6 q6 R! W$ q/ \+ {
1 ^# G! S' u S* @3 d9 _当串口接收一包数据,接收完最后一个字节,没有数据接收时,会产生一个中断,这个时候,CPU就可以取数据。
3 {4 I8 \8 x9 h8 u' ?
, g: K% @( ?+ p; D& l串口的配置知识不再讲解,不太懂的同学请看《STM32串口详解》,串口空闲中断配置如下
! x$ }2 m$ w# B7 h! t# h$ I& R) O; _# l2 v2 ]( f, n2 M7 i
- USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
+ M9 k! x/ D( |" p' @$ J - - z6 D3 ^+ B$ J3 d- U2 | x& M; X7 U
- /* Enable the USARTx Interrupt */; ~& D! d! O1 ~8 M+ h
- NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
# y& ]% s# x/ L# n4 o - NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority =0;
/ i0 j/ M# {/ _ - NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;7 X4 g2 l% U1 w+ P+ r
- NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
! X2 k# D( _1 M! q' Q6 r/ h - NVIC_Init(&NVIC_InitStructure);
复制代码 ' E8 }9 k" P7 | L+ @$ L, {
串口中断代码如下" k( I% Q8 @3 }& l8 Q
9 I6 @& A/ ?/ m0 B3 p" F- void USART1_IRQHandler(void)" U+ A4 G* V1 y* \% V7 V
- {) w) Z. K" z7 U9 I
- uint8_t temp;" {3 b9 Z6 g. A' s* ^4 }8 C
- if(USART_GetFlagStatus(USART1, USART_FLAG_IDLE) == SET). N# S- c1 B M8 }% }$ ?) @ C
- {
5 R" p3 F6 F1 J$ v T. b# a8 l, v - DealWith_UartData();* J9 G+ P( V( f% { x7 _4 V/ v& n. O
- // USART_ClearFlag(USART1, USART_FLAG_IDLE);
% o% r: A$ k8 h! q" ` - temp = USART1->SR;
% q& Y2 p4 D! E+ N6 i- C - temp = USART1->DR; //清USART_IT_IDLE标志
% S! A O- k$ O1 i - }1 N! J1 M/ ~1 G. S. u) n' y4 R5 t
- }
复制代码 G9 e D$ w7 k7 W# A+ b; d
重点:这里有一个坑!!!
/ N4 o* n: v' b- W& \. G4 }7 |3 k, M: s! d; J3 x
清除空闲中断位的代码是
4 Z: Z9 T3 k& Z6 `/ V
9 C6 w, P8 `& @! ~3 k+ z5 E8 g9 J- temp = USART1->SR;
7 c, _5 I2 v0 G# L - temp = USART1->DR; //清USART_IT_IDLE标志
复制代码
" f7 v8 r% h( {4 i% D, K8 ~证据如下# Y& h1 V- S+ p3 u
* I- u7 \1 `% D" a
6 E: J# L+ M6 l/ j6 A
+ N5 f% M8 j) ~7 D" [6 u8 c& [这一点很坑人,注意。
5 R0 q4 P$ J1 W3 S, l$ |/ f, e% S o( e3 m% k M; u3 _0 P
04、代码5 I4 ^* A; [& M# N9 L
DMA+串口接收的工程代码是开源的,Keil和IAR的工程都有
V1 z& U8 x8 ~( k/ X% J/ i' ]) q, T s5 s9 g
( b: O! O/ q* x
0 s5 g8 S$ G! `) A {3 I! h
33-USART-DMA-Receive DMA串口接收(没有使用中断)
3 y# z, h4 n! R/ K3 l
( q: F/ t/ M/ E34-USART-Receive-DMAInterrupt DMA串口接收(DMA中断)
4 ^5 P6 g9 c8 R1 ]0 N! N
- C- ~) `" z, y' }5 G! g8 q35-USART-DMA-Receive-Interrupt DMA串口接收(串口空闲中断)
/ w3 h l$ w4 f; A, K' y; v. B8 W
- G5 Y8 }+ v G3 \# @- `' t5 n |
好诶!
学习