DMA双缓冲是什么?
+ U- n( j: B/ m, A3 ^5 k在使用STM32的DMA时我们通常使用的是普通DMA传输,但STM32自带了一个双缓冲模式设置,这个模式有什么优点呢?+ M" S+ {1 V* K
接下来我会在下面的介绍里详细说明:
- X! f7 q5 i. K9 p- O y3 M9 V1 G& a; l1 B8 {
DMA全称Direct Memory Access,即直接内存访问。+ \2 E7 J2 {9 D4 x) t
普通DMA的使用是在DMA的数据流中进行的,设置好DMA的起点和终点以及传输的数据量即可开启DMA传输。
' ]$ c6 u% R2 L+ y7 G2 M, q, C% f, _& P7 U7 [( j& W5 Q6 T' X
* t ^" s6 w8 K# d6 B$ v
G3 `, N" ?# x, A9 H2 KDMA开启后就开始从起点将数据搬运至终点,每搬一次,传输数据量大小就减1,当传输数据量为0时,DMA停止搬运。* J$ r' J1 `3 ]7 G% F3 O& ]
4 \% ]5 w8 O+ k' m* L7 ]; }
DMA的传输模式如果是普通模式,则当传输数据量为0时,需要程序中手动将传输数据量重新设置满才能开启下一次的DMA数据传输。
: N4 p9 ^: f7 O" h& w; }DMA的传输模式如果是循环模式,则当传输数据量为0时,会自动将传输数据量设置为满的,这样数据就能不断的传输。2 }' f* [- C. V" h
" a' ]2 B. n% E8 W/ r" T
5 B5 u/ R; S' m L' \/ r: ^- d2 D/ E1 D
那么,双缓冲是什么意思呢?
6 e" A4 j6 D( w) Z& b: n$ r, ]我们知道,普通DMA的目标数据储存区域只有一个,也就是如果当数据存满后,新的数据又传输过来了,那么旧的数据会被新的数据覆盖。(这就是普通DMA的缺点)/ j5 o K0 a9 G. Q) Y8 F
4 \) B( ]8 s6 q, X
/ [- t- h$ E& p1 s- y4 F& n
+ A1 w1 q7 p4 K/ a+ v) N6 E& G/ ^5 o7 i. p
而双缓冲模式下,我们DMA的目标数据储存区域有两个,也就是双缓冲,
1 C& F% R, @" S( n* Z9 b- Q当一次完整的数据传输结束后(即Counter值从初始值变为0),会自动指向另一个内存区域。
: I( \+ N v* P# i% e; \
& ?* M% q6 z0 ]; [% c @, u' P6 v
+ Z3 C2 ~/ I- i
5 G- }) P, c* i+ v: i/ w# f0 l! d如何使用DMA双缓存?
: d! s% [+ O R- w那么我们在程序里怎么使用双缓冲呢?
. o0 @" t% [1 u6 \这里我们有两种方案使用双缓冲,一种是利用双缓冲自动跳转内存区域,一种是主动跳转内存区域。* v2 _1 }! }# R5 V# a1 x
7 l- O% A4 M! a6 G1 y, H
这里我们使用DMA的传输模式是循环模式
! B0 D2 J/ j7 N* T5 G Y3 R2 S/ C3 h+ f1 u/ \6 F/ L v4 O
- DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;) h, z/ |- F# P% _* k& K) `) o
复制代码 9 F; g" O) s& @2 A J
首先是第一种:
2 Z8 L5 G) J" _" d# M/ S G3 I这里我们以大疆RoboMaster的遥控器使用为示例:
$ G; c- X3 U& k. `) D+ ~ }0 ^3 v0 |( \首先我们定义好双缓冲使用的两个内存区域; K) a5 F, o6 U' B* {8 n2 S1 o
- uint8_t sbus_rx_buffer[2][18u]; //double sbus rx buffer to save data
3 `: }: {5 x: S5 N" L% P
复制代码 & o' y- j! J I% P7 J, X) L6 B8 p. j
这里我们设置长度为18,也就是一帧遥控器数据的长度。
# o$ n* O8 B( n- V) k9 D
# |. b( {9 s1 {+ `( J4 O- DMA_InitStruct.DMA_BufferSize = 18;" Q8 o. Q/ X8 I) C! Z
复制代码 2 d% R7 k5 d- }' F8 r3 E- Q6 a7 A
然后配置好双缓冲的两个内存区域地址' x! O: P5 W2 N. ^% U
, N! A" g! T8 P m- DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)&sbus_rx_buffer[0][0];
, @ Z% i% i+ \: h) `* h
复制代码 9 ]0 ^* M% S1 G
这里配置内存区域2的地址,并把当前DMA指向内存区域1的地址
& @+ v3 p, V0 o. x
J7 {/ u' b! F( d: n4 o- DMA_DoubleBufferModeConfig(DMA1_Stream1,(uint32_t)&sbus_rx_buffer[1][0],DMA_Memory_0); //first used memory configuration. w4 \$ N+ |% Y, S# L q
复制代码 ' u" ?8 n0 e# \5 r ~6 A
启动DMA双缓冲
9 ^1 i. ^) r P0 L- W6 }& q/ `3 X% X& b
- DMA_DoubleBufferModeCmd(DMA1_Stream1, ENABLE);
! S1 m2 Y' G$ P1 M. m; }! C8 [$ ^
复制代码 9 ^0 e3 _, ?& n8 I2 Z6 Z" m
我们使用DMA时利用串口的空闲中断接收一帧一帧的数据。
: _9 H/ |! A5 {' H/ F7 G0 J在串口中断处理函数中对数据进行处理7 S; ^( o, f7 V" O: W# v) i
; e" T$ n4 j% d, T8 ~/ B! f-
3 y) V, | k: E$ Q* | - uint16_t this_time_rx_len = 0; //当前剩余数据长度; h7 ~& ]' C! C* v+ x
5 q2 {* Q1 \9 f7 U R5 `- if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) //判断是否为空闲中断0 R+ I% A1 n" P; C) I# z& W
- {& b: f8 [3 I n9 r& W8 O6 n6 w: G; \. L
- USART_ReceiveData(USART3); //清除空闲中断标志位
5 D/ `3 q& ?: {% X
7 `. R9 j* r2 o W, l9 A- if(DMA_GetCurrentMemoryTarget(DMA1_Stream1) == DMA_Memory_0) //获取当前目标内存是否为 DMA_Memory_05 u3 P8 q* _" e- L( L3 I4 g
- {
1 I R& h! U, O( b m Z' Q" ^& F - //重新设置DMA( `$ \0 Q2 }1 k/ y& N
- DMA_Cmd(DMA1_Stream1, DISABLE);8 u1 f6 m% }8 x/ J3 Z2 R6 M
- this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量* b6 M( Y& `4 A( H) t, B$ k
-
$ U- z+ B- h( V - DMA_SetCurrDataCounter(DMA1_Stream1, 18); //重新设置数据量
' F: C( U( p/ a. T) S i1 G - 3 }+ U% t: H5 b4 _& N; y$ w7 ?
- DMA_Cmd(DMA1_Stream1, ENABLE);
2 q& a1 \$ c$ x& K3 e - if(this_time_rx_len == 18) //接收成功18个字节长度7 q& J0 \! U0 m% }9 ]+ }5 i. W
- {8 h) L/ C) S: K( a0 g- X
- //处理遥控器数据2 F% V d# j9 ?
- RemoteDataProcess(sbus_rx_buffer[1]); //Memory_1$ c5 X( r* p# k7 q& W F
- }% g. Q- } K- q
- ! s, ~9 O0 ]5 A: c
- }
/ ~! @3 b- G" J: B+ N - else //获取当前目标内存是否为 DMA_Memory_1
1 F( E/ X( z! x, O - {
$ C6 j2 J8 G6 {2 |$ v; B) w - //重新设置DMA
/ ^) V' B7 L, L; {, z) p - DMA_Cmd(DMA1_Stream1, DISABLE);
4 Z$ K5 q. |' p) Y) M. T - this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量8 P4 F1 {5 i& u
1 y' M ^- q1 e: E2 J A) L4 x, I- DMA_SetCurrDataCounter(DMA1_Stream1, 18); //重新设置数据量
r- |/ Z& ~/ t T1 V, L! U9 n! _- c - 2 E6 \ E- }/ \: u- t! y/ ^0 C
- DMA_Cmd(DMA1_Stream1, ENABLE);9 L4 d P$ z, a' F
- + ~# B, N. q5 B4 s4 r+ r E H3 ~
- if( this_time_rx_len== 18) //接收成功18个字节长度
, R2 M( T; |4 H! g+ [- ? - {: k# p2 x' I9 F5 W
- //处理遥控器数据7 u6 S& X9 Q4 a' o8 h+ w$ a5 N
- RemoteDataProcess(sbus_rx_buffer[0]); //Memory_0
! T$ L* o5 U/ c$ o8 @3 Y4 ~ - }' q- ^, {1 ^% c j4 @
- - D' g2 O# f6 }* [9 u
- }
2 W: o4 Z n. I9 Z - }
, l* d8 y# l, T" Z; @" K1 `
复制代码 : n: o/ X6 H0 {. y: R* X, H
该逻辑在程序复位后,当一帧遥控器数据发送过来后,Memory0区域被填满18个字节,DMA自动指向Memory1,所以第一次进入中断处理函数时,获取内存指向的时候得到的指向是Memory1。
; @& s. k8 P* F" L [然后判断剩余数据大小是否为18,如果是则说明上一帧数据接收正常,可以对数据进行处理,如果不为18,则说明上一帧数据接收长度不正常,这时候就不对数据进行处理,当下一帧数据发送过来后对数据进行检验,正常后才对数据进行处理。
. }4 d% |9 a' C6 O- p2 [. U$ Q( P3 U, C0 J( Z3 z1 h4 R
然后是第二种:* i1 W) p ?) |! U; G# C0 a
这里我们将BufferSize改为比一帧数据长度大的值(比18大),这样可以在一帧数据传输完成后不会因Counter值变0导致DMA指向下一内存区域。6 N- C% l9 { D( y1 S
' F) S1 s2 i3 e2 _ u4 V- DMA_InitStruct.DMA_BufferSize = 36;0 M% J4 b" Z# ~, r# T; m
复制代码- uint16_t this_time_rx_len = 0; //当前剩余数据长度
' R4 G# X2 u& t' D. l9 v* R- H - * T0 s# g1 i0 F* Q
- if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) //判断是否为空闲中断7 o1 a6 j. {4 Q' t( c
- {/ I- [& {1 m+ V! u7 I$ ^
- USART_ReceiveData(USART3); //清除空闲中断标志位
' _5 ^* p; A2 G, }
7 S' s1 [! I# |% @- if(DMA_GetCurrentMemoryTarget(DMA1_Stream1) == DMA_Memory_0) //获取当前目标内存是否为 DMA_Memory_0: j6 n1 j0 U# B9 G) d3 D2 U
- {
0 M) ]; ^, h) _. z! ~0 d - //重新设置DMA4 X) k4 a7 w. V: a2 t
- DMA_Cmd(DMA1_Stream1, DISABLE);. Y* A- H7 m$ z( \4 ^ M% B
- this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量 接收18字节,则剩余数据量为36-18=18
6 l# k% y( g- Z3 X$ G: Z -
* l! B# c; I- V: N2 L8 J/ [ - DMA_SetCurrDataCounter(DMA1_Stream1, 36); //重新设置数据量9 a. Q# `: D8 q; c0 w
- DMA1_Stream1->CR |= (uint32_t)(DMA_SxCR_CT); //将DMA指向Memory1
4 [: F" }1 |4 }% t1 ^( N3 { -
0 J+ t; e8 r5 b3 G% z i - DMA_Cmd(DMA1_Stream1, ENABLE);
& w; p9 e! }, G. R; j) y) i2 t - if(this_time_rx_len == 18) //接收成功18个字节长度
4 a: b/ i* F/ G - {
! [1 k0 K7 x8 L - //处理遥控器数据+ m% u4 O. d& T; G8 C
- RemoteDataProcess(sbus_rx_buffer[0]); //Memory_0
. g5 C9 L5 h0 [/ P4 a- a - }. j) }5 A" v i3 i7 s2 q
8 u# q, T2 y4 G; O( v- }
- A6 e0 m* d7 l8 u# z2 w3 K1 p) P( t - else //获取当前目标内存是否为 DMA_Memory_11 h& E, n8 R1 R! [
- {5 Q6 I6 F% D- i. h
- //重新设置DMA
$ u% M& E1 K! \5 N: b& _ - DMA_Cmd(DMA1_Stream1, DISABLE);* b+ D0 A; m$ r& V+ q2 C
- this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量 接收18字节,则剩余数据量为36-18=18
1 j8 n4 b' b, k9 z* P( @ - 8 t& Q4 s! y* |3 Y. X& Q2 B
- DMA_SetCurrDataCounter(DMA1_Stream1, 36); //重新设置数据量/ |: M, W8 [, d
- DMA1_Stream1->CR &= ~(uint32_t)(DMA_SxCR_CT); 将DMA指向Memory03 X+ U- y) E, y' X0 H$ T% y
+ f7 a6 j& s+ i/ v: W+ u- G- DMA_Cmd(DMA1_Stream1, ENABLE);
$ I" A( r$ ^8 i. I: ~2 Q
+ W# q1 e% V) U( g3 N; o* X3 `; o% X- if( this_time_rx_len == 18) //接收成功18个字节长度3 [! U4 }' q( A- N3 N; L3 F& P
- {* R$ I5 F* o: X) \
- //处理遥控器数据) s+ `$ C3 @% ^) @' U+ y
- RemoteDataProcess(sbus_rx_buffer[1]); //Memory_1- U V. N. f9 X8 Z' K2 v A! w
- }3 f, N: e( X5 z: a. t9 m& Y0 W
- s3 A/ g# T$ Z* Y/ F( M- }3 ?' x" |# s0 n
- }
8 t/ |. r( l7 G+ q" |" q" v0 w
复制代码 - @" t Y4 ^, H* b$ D
两种DMA双缓冲使用方法的区别* `+ d/ R* H! N! @- S
第二种方法和第一种方法的区别大同小异,这种方法由于将BufferSize设为比18大的值,也就是当第一帧数据传输完成后,Counter值不会自动填满,且内存区域还是指向Memory0,然后我们将剩余数据量保存下来,再将Counter值填满,接着把DMA指向Memory1,最后通过判断剩余数据量来决定是否对数据进行处理。
3 D4 X/ n/ q% m' x+ i6 U+ a: ]8 ~, h$ r9 ?- `
可以看到,在第一种方法下,我们并没有使用7 U/ q8 [- |! J( o
- B6 i5 C' O0 g& w9 v% t- DMA1_Stream1->CR &= ~(uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0* I( M: w) c6 Q
复制代码- DMA1_Stream1->CR |= (uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0
复制代码
8 J/ z1 Z9 W9 m& R0 i" _5 a
& V. Y4 J5 Z6 {( N' Z! g) }这是因为当传输数据量和BufferSize大小一致时,完整接收完一帧数据后,DMA会自动指在向下一内存地址,因此在第二种方法中的手动改变内存指向就由DMA自动处理了。; Y7 @" g' D1 v0 r3 S! ?
————————————————5 c' W; j- q+ p1 M
版权声明:水木皆Ming
- v4 O' z& v$ d% K0 T6 H
2 a! U/ }: x7 }& @3 ]5 B
0 |+ r, a/ k( s& [ |
请问一下,使用DMA双缓冲区进行数据的发送的时候,出现了重复发送数据的情况,请问该怎么解决呢?感觉主要是剩余文件数量达不到BUFFER_SIZE要求,带来的重复发送数据。