DMA双缓冲是什么?
5 r; `6 x- G. V2 ~# b在使用STM32的DMA时我们通常使用的是普通DMA传输,但STM32自带了一个双缓冲模式设置,这个模式有什么优点呢?
, ~% f5 y2 _7 T- E1 n `- \0 H接下来我会在下面的介绍里详细说明:
$ J* k$ t+ Y0 d$ p" {& E
" P' g0 r! r6 H/ `' s n* dDMA全称Direct Memory Access,即直接内存访问。
( j0 T4 `) e& h' m& L w普通DMA的使用是在DMA的数据流中进行的,设置好DMA的起点和终点以及传输的数据量即可开启DMA传输。
- E7 l! f8 V. M6 H6 Q* H l0 ^- [+ _8 ]6 v- C8 K- X
" P5 R7 x$ S2 T4 e( ?8 | Y
3 o/ o7 C. D* P0 B! G2 ~. ~DMA开启后就开始从起点将数据搬运至终点,每搬一次,传输数据量大小就减1,当传输数据量为0时,DMA停止搬运。. z2 N8 x. g7 s \$ C
- N6 D, D0 F4 |. ADMA的传输模式如果是普通模式,则当传输数据量为0时,需要程序中手动将传输数据量重新设置满才能开启下一次的DMA数据传输。+ I/ }$ x$ T9 `) v+ o" z9 s+ E
DMA的传输模式如果是循环模式,则当传输数据量为0时,会自动将传输数据量设置为满的,这样数据就能不断的传输。$ R7 [9 k0 n3 l( ]- g. f
! B* W% @7 J6 l- \' N9 J, C9 m
+ g" D* ]$ [+ S8 I
* }% ~7 w/ `0 Y! s
那么,双缓冲是什么意思呢?
- T ~9 Z: b6 O2 b; @* Y我们知道,普通DMA的目标数据储存区域只有一个,也就是如果当数据存满后,新的数据又传输过来了,那么旧的数据会被新的数据覆盖。(这就是普通DMA的缺点)! U8 O2 W) J4 w# E* r
5 {+ n u/ M- p% c i' y
# \- |" ^1 `3 c. N
3 P* \' Q9 B8 {
% v% j Y |) S3 ~9 C
而双缓冲模式下,我们DMA的目标数据储存区域有两个,也就是双缓冲,3 V3 w1 u! f) ~; }+ V1 s
当一次完整的数据传输结束后(即Counter值从初始值变为0),会自动指向另一个内存区域。
1 W% A) a) O# E) Q! O8 \% v6 x2 q) n3 Q" _
" Q1 `0 F- t, F
. ?% H2 c* F" U, e如何使用DMA双缓存?& _/ v+ G8 V0 a
那么我们在程序里怎么使用双缓冲呢?$ F, `5 U% E. P# a2 e' w
这里我们有两种方案使用双缓冲,一种是利用双缓冲自动跳转内存区域,一种是主动跳转内存区域。
; ?$ f% e7 z$ S/ }8 l9 B, h8 t ?- V: j: \. l- V( i
这里我们使用DMA的传输模式是循环模式6 F. P! Y( ~% ^3 Z1 |/ f
! j3 L1 M/ E2 L' N+ u" {. v
- DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;4 G% w5 e. r1 ]& y7 V& C; F
复制代码
5 w( T% W/ A% g0 I- m/ H首先是第一种:
3 A; C4 D; m, v% w" u$ p6 M这里我们以大疆RoboMaster的遥控器使用为示例:0 I& r/ z/ e- f4 m
首先我们定义好双缓冲使用的两个内存区域$ ^& o3 C$ b; v+ \
- uint8_t sbus_rx_buffer[2][18u]; //double sbus rx buffer to save data
9 r5 u3 }7 k* l) b
复制代码 $ ~) U+ K8 w! C8 H/ N F
这里我们设置长度为18,也就是一帧遥控器数据的长度。" k0 n+ H% J( K5 u$ ]
2 {2 W% J9 J+ `' |3 y/ j- DMA_InitStruct.DMA_BufferSize = 18;
b- U; J0 ?7 \' v2 b" y
复制代码 0 O5 a& q/ u1 Z g
然后配置好双缓冲的两个内存区域地址
& w- d9 {$ p% B( O1 q" i1 y1 [
) g8 h1 Y; M1 ]0 Q5 V7 n- DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)&sbus_rx_buffer[0][0];
8 c+ a! D ^) S3 t5 j, T
复制代码
. g2 h. m2 K2 ^% s8 y+ d这里配置内存区域2的地址,并把当前DMA指向内存区域1的地址6 H6 o1 X# j4 p# l# ^% }9 V) V$ m% }
4 {* D0 F D5 k P5 S6 a
- DMA_DoubleBufferModeConfig(DMA1_Stream1,(uint32_t)&sbus_rx_buffer[1][0],DMA_Memory_0); //first used memory configuration$ x/ e) Z% M- ?0 b: w
复制代码 " z6 o) b& |# L4 [% p: ]
启动DMA双缓冲
9 F* J3 z' \/ s5 R) d2 l: ]; {0 J* M
- DMA_DoubleBufferModeCmd(DMA1_Stream1, ENABLE);
! b; S- u1 Q# e; A
复制代码
" z- u5 u; E, Y9 I我们使用DMA时利用串口的空闲中断接收一帧一帧的数据。6 L+ h7 `2 u7 r& c$ v. J* u
在串口中断处理函数中对数据进行处理$ D; v& g0 Y/ `
/ h1 ^2 I$ p3 ]2 w: Y-
6 |4 H5 z& P, t# i - uint16_t this_time_rx_len = 0; //当前剩余数据长度
5 S/ ^5 \) |) j7 b7 o/ Z2 m - / E \/ [- y+ o) ~
- if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) //判断是否为空闲中断
4 w: l0 b/ b3 D - {6 `0 E% l/ B! i4 f2 [* s
- USART_ReceiveData(USART3); //清除空闲中断标志位+ i2 M' F. z; N* t6 E" t
" a2 V, a4 {- \) d- if(DMA_GetCurrentMemoryTarget(DMA1_Stream1) == DMA_Memory_0) //获取当前目标内存是否为 DMA_Memory_00 ^: }4 S4 p! k- R) ]
- {
/ W1 y; ^4 u2 e - //重新设置DMA
- f5 [; W& k9 ?2 ~( o c' m - DMA_Cmd(DMA1_Stream1, DISABLE);9 s0 |- H2 A& D1 q+ M
- this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量. Q4 H$ S5 B' T# h% Z9 M- i6 E0 k
-
+ e4 {. R2 A* e A7 l' t - DMA_SetCurrDataCounter(DMA1_Stream1, 18); //重新设置数据量. k( U, p8 C. ~/ j7 j+ `9 L
- 4 S6 h. \5 @& \1 b+ _( _: M6 B
- DMA_Cmd(DMA1_Stream1, ENABLE);( P+ ^# l7 t3 [" K3 J
- if(this_time_rx_len == 18) //接收成功18个字节长度' c# a( i/ J, G! [$ g( n
- {
# ]* d, \ j( w. K& E3 z7 V. } - //处理遥控器数据
7 m" C3 u' h, g; X, R/ K; t/ k - RemoteDataProcess(sbus_rx_buffer[1]); //Memory_1: y5 j8 O1 v* c% t) z
- }
L# h5 G) q' L
' ?1 i T, Q+ p# a. h( P- }2 X: d0 `, q) _( ~2 v+ @
- else //获取当前目标内存是否为 DMA_Memory_1/ y8 e9 U% N- P S
- {
0 k O# D+ v# B+ n - //重新设置DMA
3 G7 T1 f; R6 U6 r& G( h - DMA_Cmd(DMA1_Stream1, DISABLE);
7 j1 u: J2 p/ C6 S- [; j - this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量
! {0 _! } _1 x0 T/ ^
+ X1 |/ [# g9 p9 M3 n7 u. h- DMA_SetCurrDataCounter(DMA1_Stream1, 18); //重新设置数据量. u: T. u, H3 \0 u& ]# H" ?) @
1 D4 \$ q' @5 F8 E+ v% P- DMA_Cmd(DMA1_Stream1, ENABLE);
. x" y5 f/ @7 j% Y3 |) a
/ z2 ~; Y) t( F8 B% }1 w' `- if( this_time_rx_len== 18) //接收成功18个字节长度' `& O+ e- F) r" v
- {' t/ e C4 F# n% ]
- //处理遥控器数据
3 `# F+ d- x" Z3 n6 G3 O1 Z - RemoteDataProcess(sbus_rx_buffer[0]); //Memory_0" y4 R' p* s+ l5 W
- }1 N* z3 v2 N( l2 d/ a
. Q& Q# C; l" r1 w+ J/ \& X- }
8 ]& ^3 @+ `- R/ Q+ x8 t( |. Y, C: m - }4 B- u. z) f8 R5 `
复制代码
) g& a: Z0 f* I% A4 q4 [该逻辑在程序复位后,当一帧遥控器数据发送过来后,Memory0区域被填满18个字节,DMA自动指向Memory1,所以第一次进入中断处理函数时,获取内存指向的时候得到的指向是Memory1。
! F9 m. \: S0 h. u4 O& o然后判断剩余数据大小是否为18,如果是则说明上一帧数据接收正常,可以对数据进行处理,如果不为18,则说明上一帧数据接收长度不正常,这时候就不对数据进行处理,当下一帧数据发送过来后对数据进行检验,正常后才对数据进行处理。
, k' w. ^+ d3 H+ V1 C7 d/ K% P- X9 B9 }" {
然后是第二种:, ~: n# ~. W" z, o/ s
这里我们将BufferSize改为比一帧数据长度大的值(比18大),这样可以在一帧数据传输完成后不会因Counter值变0导致DMA指向下一内存区域。. [ Z& n- C. D
& P9 B- l% Q$ S* C
- DMA_InitStruct.DMA_BufferSize = 36;$ i. r7 u; V9 J& y2 {; s$ l* D
复制代码- uint16_t this_time_rx_len = 0; //当前剩余数据长度) E4 v( C7 q! X, u! r7 p
' t" p) |4 j# `5 B2 v- if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) //判断是否为空闲中断
5 w+ S7 R7 }3 `/ F$ n7 f; P - {. [ y2 b3 `' x( t
- USART_ReceiveData(USART3); //清除空闲中断标志位
) [0 a: }$ a2 T - 4 D5 H8 A8 d* K a
- if(DMA_GetCurrentMemoryTarget(DMA1_Stream1) == DMA_Memory_0) //获取当前目标内存是否为 DMA_Memory_0+ w1 o+ C& S$ ?$ A) R" w+ M! e
- {' m8 o- l0 s* `% x: Y4 Y8 O2 G
- //重新设置DMA
2 W B7 f2 e! t* R! I# b+ ?' u8 N - DMA_Cmd(DMA1_Stream1, DISABLE);
" u( {2 w& e& d8 D; t - this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量 接收18字节,则剩余数据量为36-18=18
, f; I5 p2 h% r, m# d* f/ [ -
. m7 k) R2 i7 u. | - DMA_SetCurrDataCounter(DMA1_Stream1, 36); //重新设置数据量; p: e# z; L. I+ n+ q
- DMA1_Stream1->CR |= (uint32_t)(DMA_SxCR_CT); //将DMA指向Memory18 k. n" {4 x+ X4 D2 E8 {, n1 O- E
- ( p) w/ w. b' m J
- DMA_Cmd(DMA1_Stream1, ENABLE);# {9 a$ U! J; J+ f5 O" c
- if(this_time_rx_len == 18) //接收成功18个字节长度' A l! f" }, w3 p9 z- V j3 T' D
- {
w. P* c" l% _ - //处理遥控器数据) y! n' I; `# j: N
- RemoteDataProcess(sbus_rx_buffer[0]); //Memory_0( I+ Y5 w# O% x) Z
- }
# c7 J% d4 ?; E/ M6 u7 \+ W9 t! j6 ^ - " [( o7 m- a$ }5 H! g0 D
- }
4 `0 j/ L% h; ^5 ? - else //获取当前目标内存是否为 DMA_Memory_1
8 O4 f& k; Y4 e, {9 S) R - {
# U& ?/ T- T! S I; q$ z0 k - //重新设置DMA
3 [+ K6 q9 z0 ~# Y - DMA_Cmd(DMA1_Stream1, DISABLE);
- b G' L1 h2 y7 C - this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量 接收18字节,则剩余数据量为36-18=180 V6 T7 n0 f' I7 b6 i! l2 z
- : D: h6 z5 t, S1 T) N
- DMA_SetCurrDataCounter(DMA1_Stream1, 36); //重新设置数据量$ H7 Z/ d V' v, Y+ m& f
- DMA1_Stream1->CR &= ~(uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0
$ W8 d9 L |/ n9 o. n/ D - - ?4 z7 N2 k1 F8 N; d- ~
- DMA_Cmd(DMA1_Stream1, ENABLE);
, ~, _" y! f/ k" Q/ Z" E% M5 h. y
! v' K1 M( e5 t8 O7 `- if( this_time_rx_len == 18) //接收成功18个字节长度
0 t- U6 i% n6 e" W3 i9 W - {
! _% p9 R+ z8 U0 R- x; y" ~ - //处理遥控器数据! z" E! A0 Y n h3 v x. C- R. N
- RemoteDataProcess(sbus_rx_buffer[1]); //Memory_1
) u: a1 d$ v* M" l; t% b - }% n; _, z' R& G5 \ t
- % m- y" {0 g0 o/ k
- }
/ K. W6 I6 D- C3 U* y E - }# h0 v* A$ w# |- Q- @" Z
复制代码 0 f. N9 u5 ~! [ w
两种DMA双缓冲使用方法的区别* b( j( N5 R6 {& B3 D% C( F& T
第二种方法和第一种方法的区别大同小异,这种方法由于将BufferSize设为比18大的值,也就是当第一帧数据传输完成后,Counter值不会自动填满,且内存区域还是指向Memory0,然后我们将剩余数据量保存下来,再将Counter值填满,接着把DMA指向Memory1,最后通过判断剩余数据量来决定是否对数据进行处理。
9 I. t; Y, s3 c+ W& v& o8 J
8 {( ], {' F- ]- o0 Q5 r4 N: c6 A可以看到,在第一种方法下,我们并没有使用; J$ ^/ o- q( |( ]
/ N% U; @! W/ _- DMA1_Stream1->CR &= ~(uint32_t)(DMA_SxCR_CT); 将DMA指向Memory02 E3 Q* t0 J0 f$ ]8 q+ O3 i
复制代码- DMA1_Stream1->CR |= (uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0
复制代码 ' E8 e% U! ?! \
; h( u& ^! I0 c. J" D2 |这是因为当传输数据量和BufferSize大小一致时,完整接收完一帧数据后,DMA会自动指在向下一内存地址,因此在第二种方法中的手动改变内存指向就由DMA自动处理了。3 y9 B1 Z: D) d" M" O4 O
————————————————# ~* D- {# V+ v4 i
版权声明:水木皆Ming
* C3 M, k4 L0 L2 Y: M3 Z0 Q1 |& C+ |$ l
4 S8 }! w5 d9 {) Y. l+ R
|
请问一下,使用DMA双缓冲区进行数据的发送的时候,出现了重复发送数据的情况,请问该怎么解决呢?感觉主要是剩余文件数量达不到BUFFER_SIZE要求,带来的重复发送数据。