DMA双缓冲是什么?! V: P [" N! C {: ]2 M. Y
在使用STM32的DMA时我们通常使用的是普通DMA传输,但STM32自带了一个双缓冲模式设置,这个模式有什么优点呢?( p$ _7 u( `' w, c" n* t
接下来我会在下面的介绍里详细说明:
7 b9 n. b9 z" ^* d7 {
) m" h3 n1 z! O# o$ G0 }. Y/ G; hDMA全称Direct Memory Access,即直接内存访问。
0 \1 E3 m* Z& {& J; {; w普通DMA的使用是在DMA的数据流中进行的,设置好DMA的起点和终点以及传输的数据量即可开启DMA传输。
& b! k9 v& n" I3 w$ a1 c- A! S; N7 R- P! E8 ]- f9 g& T- N
4 L2 [7 n$ d, Z% P2 |+ o7 h, O" m8 |- E( N- e6 z
DMA开启后就开始从起点将数据搬运至终点,每搬一次,传输数据量大小就减1,当传输数据量为0时,DMA停止搬运。( j9 a7 G# u# q. R4 Y4 q
( k' }5 V3 i K! v. B0 k# H
DMA的传输模式如果是普通模式,则当传输数据量为0时,需要程序中手动将传输数据量重新设置满才能开启下一次的DMA数据传输。
: l' u$ E, ^% {" j9 |+ ?( }DMA的传输模式如果是循环模式,则当传输数据量为0时,会自动将传输数据量设置为满的,这样数据就能不断的传输。
* R. x" j, U' V. W7 F1 O9 ]- J: j* G% d- G9 z- ?
- e+ o- J: y4 T" A0 u2 R
2 C% ~+ A% k! N那么,双缓冲是什么意思呢?
( T! Z- O# L0 g# L' h. i我们知道,普通DMA的目标数据储存区域只有一个,也就是如果当数据存满后,新的数据又传输过来了,那么旧的数据会被新的数据覆盖。(这就是普通DMA的缺点)
) [1 c4 K/ O$ W5 Y; X: e; y" z" L" W( N$ D+ g, h
5 n; b( x1 v4 z
$ H9 D p# L; ?9 r( D+ N) w) Z: @9 w
而双缓冲模式下,我们DMA的目标数据储存区域有两个,也就是双缓冲,3 i) k$ x N+ r1 O
当一次完整的数据传输结束后(即Counter值从初始值变为0),会自动指向另一个内存区域。1 H/ b3 o) q; |1 M. m# U" q# g/ ]
9 O8 w" c. Q b9 i% v
. K' {: x8 G) f4 ^ W
/ S5 G0 T: a; Y+ l
如何使用DMA双缓存?( r9 a: C/ d4 h; c
那么我们在程序里怎么使用双缓冲呢?
+ S2 t% _; _2 W- `- H ^1 J3 G这里我们有两种方案使用双缓冲,一种是利用双缓冲自动跳转内存区域,一种是主动跳转内存区域。
1 j' k, _7 @' } A, U/ W v% ~# }+ |1 @0 M
这里我们使用DMA的传输模式是循环模式$ r8 P+ ?: V2 a" I
9 x% L. n" c* t8 X2 b
- DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;
$ e/ o; y1 U- k- N0 B! f8 i1 o
复制代码 4 Z) U0 L$ z e( f3 g) W) K/ q
首先是第一种:
7 i) Z0 ?& j# ^9 S; B这里我们以大疆RoboMaster的遥控器使用为示例:
3 P4 J6 J% `4 o1 Z' d: W- a首先我们定义好双缓冲使用的两个内存区域
1 u" s: n: v( x7 N' N- uint8_t sbus_rx_buffer[2][18u]; //double sbus rx buffer to save data
W+ y# m7 y' c
复制代码 1 u) j2 n# j- I& v/ G- v3 M
这里我们设置长度为18,也就是一帧遥控器数据的长度。
" C1 w4 _2 o; y# z0 x" o3 M, I" r( \( u$ d7 e0 Y& e
- DMA_InitStruct.DMA_BufferSize = 18;/ v1 Z+ G- [! \" q& T- B
复制代码
* w1 E& R2 g( v- j. Q然后配置好双缓冲的两个内存区域地址2 T: k. r6 S0 q" V
- S% a D5 r: t- j; P7 [2 M
- DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)&sbus_rx_buffer[0][0];
6 S) a* C; u+ }
复制代码 & Y- f4 O- e, U: s2 ^- Y: k R
这里配置内存区域2的地址,并把当前DMA指向内存区域1的地址
) r, e: {( ~8 ?3 o3 A( w) D* h0 K2 G* ]% k- J8 F( d! l! ~- t, ?
- DMA_DoubleBufferModeConfig(DMA1_Stream1,(uint32_t)&sbus_rx_buffer[1][0],DMA_Memory_0); //first used memory configuration5 j @' ?/ N4 [! T8 b
复制代码 ( D2 H0 j- Z- O
启动DMA双缓冲
" e5 a$ `! y5 T& H/ C4 d
) B! h/ N# V, u {" N- DMA_DoubleBufferModeCmd(DMA1_Stream1, ENABLE);/ X* u6 U+ Y, U7 T! i' i
复制代码 / C6 H) |( v3 ]
我们使用DMA时利用串口的空闲中断接收一帧一帧的数据。
, ~: H4 l$ {+ h( y5 {$ L* M在串口中断处理函数中对数据进行处理
: p% ]$ F1 y( c/ L# S
u( u5 j1 K8 k; w2 C, F: G- : ~0 [( o; ]. T
- uint16_t this_time_rx_len = 0; //当前剩余数据长度
: Z( }* x! k+ u7 U6 r( Y! D( m - : u" L0 F- f, z6 ]1 S
- if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) //判断是否为空闲中断
: y# D& \8 O, m+ u - {# B$ ]. C2 n# a- f- \1 `/ F
- USART_ReceiveData(USART3); //清除空闲中断标志位& b$ O" _- s' r+ B4 m6 j+ F$ B1 v
" _5 u$ ]3 o, b# {4 X, z `- if(DMA_GetCurrentMemoryTarget(DMA1_Stream1) == DMA_Memory_0) //获取当前目标内存是否为 DMA_Memory_0
3 h; U! f8 J. K5 h) n7 W6 g - {( {, S& {1 i( R/ E! X
- //重新设置DMA
, v+ L5 }3 j# P) _1 N y - DMA_Cmd(DMA1_Stream1, DISABLE);
5 S& o/ e9 D- c. v; ? - this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量, M2 @/ w3 t3 Z z* z& ?: D% y
-
+ I! K7 B) o, w2 A L5 k* o - DMA_SetCurrDataCounter(DMA1_Stream1, 18); //重新设置数据量
7 i' {4 \- }9 k1 V, ^ - ; g% Q; ~/ _6 U+ E% z% |
- DMA_Cmd(DMA1_Stream1, ENABLE);
! m3 n1 Q- J) t5 R - if(this_time_rx_len == 18) //接收成功18个字节长度# _$ F c2 I2 ~5 k; [
- {, N% \* \2 c: j% ^7 m" l0 r' E
- //处理遥控器数据
2 ~; Q& }" P+ C# y - RemoteDataProcess(sbus_rx_buffer[1]); //Memory_1
* Z( x. }6 P K% ]" Y - }2 ]! |" _( G* D; T2 [
( C3 A3 A) r9 ^( E- }8 X q7 k, D! n. q9 X8 G* t. |. B
- else //获取当前目标内存是否为 DMA_Memory_1
8 b3 r9 M6 D7 q - {
/ v; M" X) e8 h3 ?% H4 u9 o - //重新设置DMA" h* v; S( M7 |3 e ^
- DMA_Cmd(DMA1_Stream1, DISABLE);
1 @5 t9 E; F3 _& {* m+ s - this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量5 V7 |) M) F k4 I3 {" H
4 D9 A2 L) P. @" F4 g3 ?- DMA_SetCurrDataCounter(DMA1_Stream1, 18); //重新设置数据量/ |. p) o# N2 N% t* J3 N
! Z: C. M/ W" G* p7 j1 r- DMA_Cmd(DMA1_Stream1, ENABLE);
) f, N1 e' q3 T" I3 O- l' D+ A3 h
+ V6 Z8 E U/ z% [* e- if( this_time_rx_len== 18) //接收成功18个字节长度' p l* e, Q( W- Q8 ~: }2 b
- {
& J% j e8 C6 {3 g% n) @ - //处理遥控器数据) y; g9 b; e5 z& k$ y, D& ]
- RemoteDataProcess(sbus_rx_buffer[0]); //Memory_0
/ J+ l, I& G1 P+ b0 |% v - }
* W5 ^- e! |& X j5 x3 G! ], C
1 Z: g: y: \# q7 C6 i+ I- }
! s8 C& a3 ~# b! q3 y* c( x - }
) x; ?6 Z$ g: M; i
复制代码 # ?, ~# ~' T) b; x: S7 Q% I
该逻辑在程序复位后,当一帧遥控器数据发送过来后,Memory0区域被填满18个字节,DMA自动指向Memory1,所以第一次进入中断处理函数时,获取内存指向的时候得到的指向是Memory1。
7 J; d0 m. L# B& O" a8 f然后判断剩余数据大小是否为18,如果是则说明上一帧数据接收正常,可以对数据进行处理,如果不为18,则说明上一帧数据接收长度不正常,这时候就不对数据进行处理,当下一帧数据发送过来后对数据进行检验,正常后才对数据进行处理。
/ K1 X( d' n% i# A8 J$ R
1 H3 b0 I" Z$ _+ r然后是第二种:" Y5 \& w# V5 l% d1 B& W
这里我们将BufferSize改为比一帧数据长度大的值(比18大),这样可以在一帧数据传输完成后不会因Counter值变0导致DMA指向下一内存区域。 `% `$ i% y+ {+ S# {2 D/ Q6 w
0 S' a9 l# d- ^2 ?' u
- DMA_InitStruct.DMA_BufferSize = 36;5 V2 a: m2 b7 [( J1 [& B: a# ?
复制代码- uint16_t this_time_rx_len = 0; //当前剩余数据长度, Z1 c1 ?( g! g, v
- % O( w% _6 i# |7 P, B* @
- if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) //判断是否为空闲中断0 v+ s4 E) X" O
- {
) t$ }# Y- w( F8 g - USART_ReceiveData(USART3); //清除空闲中断标志位
$ L/ X/ i1 K( T' o& L: I' I6 F! g - 3 k% g/ |4 m- s6 t, x" k0 Q8 ~4 h
- if(DMA_GetCurrentMemoryTarget(DMA1_Stream1) == DMA_Memory_0) //获取当前目标内存是否为 DMA_Memory_0* O: {3 p+ c. e! b& B7 ?5 ~1 e# a
- {: T5 ^; k; L- t: h
- //重新设置DMA
2 w7 D. p( F% u) s" J' m - DMA_Cmd(DMA1_Stream1, DISABLE);
+ I5 W! {: R/ f/ W3 G - this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量 接收18字节,则剩余数据量为36-18=18# |4 d" B# ?; k& R" q
-
& Z7 }' e1 f9 b9 N, Z& m - DMA_SetCurrDataCounter(DMA1_Stream1, 36); //重新设置数据量
. }- ?3 Z7 ]8 |+ l% a0 u. n. l- ], } - DMA1_Stream1->CR |= (uint32_t)(DMA_SxCR_CT); //将DMA指向Memory1
/ u# g2 @2 | ?; y -
' n( y6 Q' ] P. A - DMA_Cmd(DMA1_Stream1, ENABLE);
7 c$ p3 c- d0 c" u9 ^7 S - if(this_time_rx_len == 18) //接收成功18个字节长度! q2 t9 r( S2 m3 Y7 Q- {' |
- {
3 _2 \9 h; H* B8 e* D6 [! Y - //处理遥控器数据; G8 T' G3 t0 J/ i
- RemoteDataProcess(sbus_rx_buffer[0]); //Memory_0
3 C) q: A. `# u* @* ? - }
. M" g/ ^( ?$ x6 v - " E( g: k# @2 M
- }
. X6 {4 G" O, t/ X/ Z2 ?3 t - else //获取当前目标内存是否为 DMA_Memory_1
- N/ o' q. o; l# A - {
) L$ d4 Q, o4 E# H- ?6 B+ V4 | - //重新设置DMA
8 V4 K N! u4 m* j% a: `( W+ \ - DMA_Cmd(DMA1_Stream1, DISABLE);4 T1 ]' r- R' H
- this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量 接收18字节,则剩余数据量为36-18=18
9 |9 W' O& s- w. h9 N$ D9 n/ R! P8 } - " K- y9 ^ i9 r: P) w! S
- DMA_SetCurrDataCounter(DMA1_Stream1, 36); //重新设置数据量
$ K3 r! {. \; R; i! f/ M - DMA1_Stream1->CR &= ~(uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0
' |. T+ Q$ V" F$ r# m2 m! Y
5 y" J1 A8 \4 g4 ^2 O4 }- DMA_Cmd(DMA1_Stream1, ENABLE);# M( x8 m8 ^2 t: {" b
- . d& h x4 ~/ E% w* |' e. U# l% U
- if( this_time_rx_len == 18) //接收成功18个字节长度1 O9 W; c( \" s5 R% s
- {
* ^4 b, H/ n( I. Z1 X+ J - //处理遥控器数据/ S( z- W, T2 L# k# `
- RemoteDataProcess(sbus_rx_buffer[1]); //Memory_1
2 o- J# [9 b5 a+ ]3 d& ^5 P5 X - }6 Y, C, H! h6 z, o" S7 |5 n
) v" C7 X ]" G+ N- }
! _5 a% F1 U0 _# A6 q - }6 w6 H+ |7 g1 m1 n4 i% ~9 i
复制代码
- i# Z; k( j% ?/ r6 G. ^两种DMA双缓冲使用方法的区别
' P; q) C- A' u第二种方法和第一种方法的区别大同小异,这种方法由于将BufferSize设为比18大的值,也就是当第一帧数据传输完成后,Counter值不会自动填满,且内存区域还是指向Memory0,然后我们将剩余数据量保存下来,再将Counter值填满,接着把DMA指向Memory1,最后通过判断剩余数据量来决定是否对数据进行处理。
+ M7 c B" \8 r( e+ i0 i5 V' o( g2 _
可以看到,在第一种方法下,我们并没有使用& g" o, u1 p5 F$ c Z6 ^" P
, A( y+ z+ B1 L) d- DMA1_Stream1->CR &= ~(uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0
' t- |* Z! Z: m* c- |! w
复制代码- DMA1_Stream1->CR |= (uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0
复制代码
* Q' l$ v: U7 l* W
/ W3 q9 b: [/ p" ?这是因为当传输数据量和BufferSize大小一致时,完整接收完一帧数据后,DMA会自动指在向下一内存地址,因此在第二种方法中的手动改变内存指向就由DMA自动处理了。
9 [/ ?3 a( e1 Y6 C————————————————
. Q, e; Z7 D% A/ b7 X版权声明:水木皆Ming. t! g" x5 X8 W6 ^& `/ {
# E0 P, g" y1 J- E" j5 C& K( X
: i! A9 ] d6 j2 q
|
请问一下,使用DMA双缓冲区进行数据的发送的时候,出现了重复发送数据的情况,请问该怎么解决呢?感觉主要是剩余文件数量达不到BUFFER_SIZE要求,带来的重复发送数据。