01、概述# r/ F% q7 \8 s) D
0 i# g) {0 w- K. `
在串口讲解的文章中,示例代码采用中断方式接收和发送数据,中断的好处在于可以及时响应,快速接收到数据,但缺点也很明显,那就是频繁中断,接收1000个字节需要中断1000次,频繁中断就意味着会打断其他代码的执行,对一些应用场景是不允许的。这个时候,使用DMA+串口的组合就可以很好解决这个问题。% o7 N* D/ ^9 m! H9 a) w% m
, H" M4 z* D9 } D- u4 x) ^
2 p! [& F$ Y' q, I; T. x z1 ~+ c
% F+ b" M- V$ P
DMA每个数据流有8个通道,每个通道映射到不同外设,这有利于针对不同的产品配置不同的DMA外设请求。
7 c* O7 B O* M2 i8 t3 ]
) O# Z: ]4 y7 _* P1 @4 f( A1 f每个数据流只能配置为映射到一个通道,无法配置为映射到多个通道。即,与数据流不同,每个DMA控制器可以同时配置多个数据流(因为有仲裁器),但每个数据流不能同时配置多个通道(因为只有选择器)。; V) X" e0 d+ Q7 c$ I) m1 @8 H5 r
4 Q+ ~6 p' D) z4 t3 w* k
我们使用USART1串口外设,从数据手册中可以查到,USART1的发送和接收都是支持DMA的,使用的是DMA2., l' |) m/ ~1 R# @2 ~6 i9 ~/ d
$ j9 F" { q& N
* R* [4 |' A3 m) b( I& o
( x/ H7 T: J' f$ M+ K( w0 z* M接下来我们循序渐进了解DMA在串口中的应用
" ~2 Y/ N) C; e4 D" s. ?/ f7 W: A! N! D& K0 G2 F
02、DMA接收/ Q1 O! O% v0 y$ d4 f6 z
我们先配置DMA,将DMA外设和串口联动起来。首先需要配置DMA。6 x- b, x8 Y* C
0 z7 H+ j. q* I7 U7 ^- void DMA_Config(void)
7 [+ b2 ^, O9 s7 T3 g! a5 g - {" X6 K a7 u4 y$ k/ Q
- DMA_InitTypeDef DMA_InitStructure;4 Z6 x( S; |1 }2 G7 X; i' I0 f
; i# x1 j% {2 f! p; A3 `9 j- /* Enable DMA clock */6 y3 U; q: b$ ?4 z+ G
- RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);' @1 M" C8 ~, k9 q
# l7 @7 U' k8 F: ~: q- /* Reset DMA Stream registers (for debug purpose) */& n+ x; E# R2 B8 x) P% v- f
- DMA_DeInit(DMA2_Stream2);! L& R4 G N1 `) s5 p5 \
- ; M6 w$ P5 J+ r7 b
- /* Check if the DMA Stream is disabled before enabling it.
: {) A7 }+ ^( o: r* H% _ - Note that this step is useful when the same Stream is used multiple times:
: `5 L- P2 ~; j7 j: W7 | - enabled, then disabled then re-enabled... In this case, the DMA Stream disable
. W' i( O9 j4 X( V% \& z6 L. M* N4 X - will be effective only at the end of the ongoing data transfer and it will 3 E2 g2 b' h A4 D: E
- not be possible to re-configure it before making sure that the Enable bit : R3 C8 G0 f* u. ^4 e
- has been cleared by hardware. If the Stream is used only once, this step might , M) E6 P3 C) q9 F" }5 J' @
- be bypassed. */7 P* ]; Z Y, J( @, V
- while (DMA_GetCmdStatus(DMA2_Stream2) != DISABLE)
6 H* m# ~1 @1 S. Q% l. U) p- C5 Z* E - {2 N; K7 Z e" m, O$ P: v
- }
' |9 w7 T( g9 m; H - : X+ ]( J4 R- s6 w8 L% j
- /* Configure DMA Stream */
) F' k( s0 b8 c" a5 J - DMA_InitStructure.DMA_Channel = DMA_Channel_4; //DMA请求发出通道$ o3 x8 z+ ~* `2 j1 Z
- DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;//配置外设地址/ l/ Z% A3 V0 J/ G/ x( j. k8 c9 w1 p2 o
- DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)UART_Buffer;//配置存储器地址$ ^" q, T* _8 z3 u& Z( @ v& A% ~7 o& p
- DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;//传输方向配置
% k* `2 O+ n$ g/ O1 p i/ V - DMA_InitStructure.DMA_BufferSize = (uint32_t)32;//传输大小: G' q8 |7 v6 [ ~. B' _. H4 \
- DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设地址不变0 R4 s1 g0 I. U) ]
- DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//memory地址自增. c' q- n$ O0 ?% Q
- DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设地址数据单位8 k7 w& n+ c( c
- DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//memory地址数据单位
( [8 e6 q! V1 |2 ?5 i - DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;//DMA模式:正常模式
1 q; J4 ~! j! g; R, m - DMA_InitStructure.DMA_Priority = DMA_Priority_High;//优先级:高
: W( Q# k& A1 W5 `6 i - DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;//FIFO 模式不使能. & o2 s) y$ c. m' L4 @( @' a
- DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;// FIFO 阈值选择7 G' E1 G9 ^& ~8 k/ e2 ~: `
- DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;//存储器突发模式选择,可选单次模式、 4 节拍的增量突发模式、 8 节拍的增量突发模式或 16 节拍的增量突发模式。
8 s' q( y6 x; A9 j' I) [. t - DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;//外设突发模式选择,可选单次模式、 4 节拍的增量突发模式、 8 节拍的增量突发模式或 16 节拍的增量突发模式。
& ~- w9 M2 ]/ q' A6 k - DMA_Init(DMA2_Stream2, &DMA_InitStructure); 7 N1 G! y, h5 ^
- # t. |6 V$ P2 a& {! H5 G# H3 W
- /* DMA Stream enable */
9 J7 C0 K: K E - DMA_Cmd(DMA2_Stream2, ENABLE); T: Y( E) O2 w/ m) S4 h( U8 ]
- }
复制代码 0 V! C" R) e! S# u, \% Y
s0 _7 {/ H: X. h! ~; D
除了配置DMA外设外,我们还需要配置串口对应的DMA配置,在手册有一小章节讲解到。
" e5 E) L$ n2 l0 p' I: O
- x7 S, N9 j) g5 L- K0 p9 ^. z
- F+ }# ^. C' F* O% z5 w. _( Z
需要配置的寄存器是USART_CR3寄存器。1 z" d$ _: t9 t( y( w: r
. M) S0 c2 h4 Y! a# k
9 |( G3 P8 W0 y) b& _: y
- }8 G6 a @9 ?2 c0 B我们可以通过配置USART_CR3寄存器的bit6和bit7使能串口发送和接收DMA。ST的标准外设库同样提供了对应的外设库。
+ v A' M+ J9 S" l% x1 F5 Z) N) V' }) M
- void USART_DMACmd(USART_TypeDef* USARTx, uint16_t USART_DMAReq, FunctionalState NewState)
复制代码
% s9 B3 z3 N* d2 u通过上面接口可以配置串口的DMA配置如下:
9 `6 T" a+ S; A' m6 c/ x' E; s! s4 r4 N
- /*使能串口DMA接收*/6 v3 R. z* M0 t, u2 T4 C% ] V
- USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
复制代码
6 I. i V, J8 Y( }% w v3 D1 l& ?% ]03、中断) @, [. {# V3 e- ?* T# {: e
我们使用DMA+串口解决了频繁中断的问题,但现在有一个问题,我们还需要及时将接收的数据信息通知CPU,以便达到数据的及时性。我们使用DMA和串口两个外设,他们都有自己的中断。
9 n! y' G) y9 ^- w! |1 W- ?3 z( j" a7 i3 |2 d' z( T) q
使用DMA中断,如下配置8 M, t/ X3 c# ?, u3 q7 g& E' e
. X& @4 N2 z- H. G7 ]
- /* Enable DMA Stream Transfer Complete interrupt */4 z* W6 \+ @9 R8 d+ h$ Y5 d0 [
- DMA_ITConfig(DMA2_Stream2, DMA_IT_TC, ENABLE);
0 x+ z- Q; {. d0 s# o, u' R5 V9 } J - * {. {2 X7 A: w* Y) M( V
- /* Enable the DMA Stream IRQ Channel */1 q- L' p- M/ k# W# Z
- NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream2_IRQn;
: k4 n! A9 j- i- j: S4 w2 X - NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
3 J+ }% ]1 `" Q( q E, J - NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;. b. A. U; D' M) o) o
- NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;) K: r; e9 K8 l" t# v4 o$ ]
- NVIC_Init(&NVIC_InitStructure);
复制代码
4 p, Y; d1 K( w" s/ E) e' `5 ~当DMA接收完毕时,会产生中断通知CPU取数据。
/ K& O( g- P' ~( Y$ [
( `4 c! b0 M. O$ J但这有个明显的缺陷:串口接收一包数据,长度如果小于DMA的缓冲长度,那么久不能触发中断,只能等DMA接收满数据才会产生中断,如果下一包数据迟迟不来,那么这一包就不能被及时响应。
9 J+ z1 g" H$ B" \% ~! d" ?" _3 k* |
那么我们采用串口中断是一个不错的方案。串口提供了一个空闲中断,“似乎”就是为了DMA专门使用的。
% v3 j r( w) V& P2 m5 X3 T) `! a% }. x- |% L
/ |1 B a- N3 c# E
; F1 z+ y c6 l( T ^) M当串口接收一包数据,接收完最后一个字节,没有数据接收时,会产生一个中断,这个时候,CPU就可以取数据。
8 t1 H4 P$ b Q" J* w2 p1 ?/ F9 s, a: ?7 V' Q
串口的配置知识不再讲解,不太懂的同学请看《STM32串口详解》,串口空闲中断配置如下
. }! F$ ?3 h4 y# E8 s# G( e3 S
6 s2 B3 e* |. X1 W, |! \! T- USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);/ z' F) f; V: c+ a2 P3 J
- 8 A: E5 ~0 x- x5 Y$ m
- /* Enable the USARTx Interrupt */0 E# `. [ U. y
- NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
, }+ I, Q4 f0 w8 Q3 j% Y - NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority =0;, D* P3 ^+ N4 }+ D
- NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
! Z0 P, C* J2 M( D - NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;. n. W/ b- _' E s) t( j# n
- NVIC_Init(&NVIC_InitStructure);
复制代码
6 }3 S5 ~0 H( u; V' d% m串口中断代码如下
: A% |1 F' h) {5 w6 d
1 e' y9 p. b# Y% M; {% v- void USART1_IRQHandler(void)# z+ X) V7 i4 W
- {- i( Z) y8 [8 Z
- uint8_t temp;' A, k9 j& L. z: b
- if(USART_GetFlagStatus(USART1, USART_FLAG_IDLE) == SET)
! [# E) H% n" E3 }/ D - {; A% }0 M# a# w2 l7 B# u
- DealWith_UartData();' F% m: z& X. d, l1 h& [
- // USART_ClearFlag(USART1, USART_FLAG_IDLE);" d; e# W) K( E, _
- temp = USART1->SR;
) z& j. i9 o% }9 E- g5 ` - temp = USART1->DR; //清USART_IT_IDLE标志 $ w3 V1 n. t! |# H
- }/ Z" W( x1 t; [' F
- }
复制代码 / V7 [+ J: d% n3 c" I ?
重点:这里有一个坑!!!' T- R# ]& a% D L
) L! I" Z$ F% x7 h9 L, R2 q清除空闲中断位的代码是
' g/ w; R* e8 f7 N/ Q: ^5 m' P3 V S, w. A
- temp = USART1->SR;
* e% m3 B+ e0 |* K9 {, S - temp = USART1->DR; //清USART_IT_IDLE标志
复制代码 9 z2 J7 C$ i; w8 r: i2 s
证据如下
0 ?. ~ R* E+ z4 S2 c `4 Q0 L8 _3 w1 ^3 G5 {" Z
" H1 q: P5 P- W: Z- @
3 W# C7 d8 b; ~7 a7 B5 z* e
这一点很坑人,注意。& i0 O* A2 t) I$ r0 X' _
& m, y$ ~ G3 ]$ }$ _ D7 H
04、代码7 B9 N) Q* X8 I. K* s) @
DMA+串口接收的工程代码是开源的,Keil和IAR的工程都有
8 G' l. |* l, q% \
! t* q0 X$ U$ j4 ]( n& J& [; |
" s0 N+ q, ]! i" ?4 _# L
0 K' a$ n/ [* [' i( m f, Y
33-USART-DMA-Receive DMA串口接收(没有使用中断)
! s6 ?% n) ?& O2 X& ^0 o9 E4 H* a% q" {, D3 n% W( ~
34-USART-Receive-DMAInterrupt DMA串口接收(DMA中断)9 Q$ G& h, w: J7 i
Z8 ^. [, ^% g/ O7 V }
35-USART-DMA-Receive-Interrupt DMA串口接收(串口空闲中断), I' \5 Y, ^0 Z
2 v, g7 Y( E0 ^) z4 x
" a o* W: D _! V$ X; b( d |
好诶!
学习