DMA双缓冲是什么?
$ C+ o) M9 M- c$ Y5 R6 M' ^# P6 C在使用STM32的DMA时我们通常使用的是普通DMA传输,但STM32自带了一个双缓冲模式设置,这个模式有什么优点呢?
/ b( G. M# W m, L- y0 ]" C接下来我会在下面的介绍里详细说明:
; l: B; i! Y# X+ V+ L' R) w$ R& D9 G1 m5 x4 Z
DMA全称Direct Memory Access,即直接内存访问。* ?; H9 Q0 c+ K6 w0 w6 t/ J
普通DMA的使用是在DMA的数据流中进行的,设置好DMA的起点和终点以及传输的数据量即可开启DMA传输。, q* X* L% C) J5 G
. v0 e8 Y) L0 ]1 z8 I
& W7 _- @% ]: Y
; {* c6 B% X" a3 vDMA开启后就开始从起点将数据搬运至终点,每搬一次,传输数据量大小就减1,当传输数据量为0时,DMA停止搬运。5 ~& a- Z: t# _2 O8 \
: N) Q& h, n$ I. @
DMA的传输模式如果是普通模式,则当传输数据量为0时,需要程序中手动将传输数据量重新设置满才能开启下一次的DMA数据传输。
( K( v" J. L" W. O4 eDMA的传输模式如果是循环模式,则当传输数据量为0时,会自动将传输数据量设置为满的,这样数据就能不断的传输。
3 b3 E1 K& g( d& V8 u; `3 b6 v+ ~+ Z$ [- x- f4 |/ d4 ], a
; v' N1 g/ r, r6 |2 ]
% S; D8 f# H4 |6 S: F. j6 K那么,双缓冲是什么意思呢?& {) \/ f0 j; o
我们知道,普通DMA的目标数据储存区域只有一个,也就是如果当数据存满后,新的数据又传输过来了,那么旧的数据会被新的数据覆盖。(这就是普通DMA的缺点)% C& Z% T# A: ]. N* w2 g
) N+ t: X) d( N$ X. Q
5 d- P( o0 a; _/ E4 I ]( }* H u# `4 J
) X5 a# i3 x+ S4 Y3 [* O
而双缓冲模式下,我们DMA的目标数据储存区域有两个,也就是双缓冲,
) ]1 \4 N) m& K) [& f1 d, x C当一次完整的数据传输结束后(即Counter值从初始值变为0),会自动指向另一个内存区域。1 y, I% d* M3 [; I: n* r; p" U
% z1 ~$ O6 i9 _$ O8 X6 O7 _# \
6 c0 n% u8 Q. e/ ^: ^: \
% Q. {7 t9 L1 e& ]& Q
如何使用DMA双缓存?
$ f8 F( }( N0 T7 [. D! U, n& }那么我们在程序里怎么使用双缓冲呢?
$ A6 ~1 j; Q7 q/ y! C5 X2 p' x" H这里我们有两种方案使用双缓冲,一种是利用双缓冲自动跳转内存区域,一种是主动跳转内存区域。& L) ^$ d, N, o& ~# t$ o
3 |9 K" L* f% k$ Q9 j这里我们使用DMA的传输模式是循环模式) F; J, V6 @- b* {( T! a! l
: M. d0 V _+ r! l1 ]- DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;1 J& v1 T# z" X# I$ U- r0 i% v; F
复制代码 ( u+ d: { z# O% E: M3 u& r7 s* D
首先是第一种:
3 ~8 u4 a6 o/ L- s" s这里我们以大疆RoboMaster的遥控器使用为示例:
) @, l) K t0 M( r首先我们定义好双缓冲使用的两个内存区域
6 ^" X/ Z" O3 q( P; ~; ]- uint8_t sbus_rx_buffer[2][18u]; //double sbus rx buffer to save data5 m- [& k3 n# R: X7 V
复制代码 $ m" B, I( y% u# A3 X
这里我们设置长度为18,也就是一帧遥控器数据的长度。
8 b! h/ L' P0 H: G% x7 u2 ?6 u$ K0 Q" g. h" j1 E
- DMA_InitStruct.DMA_BufferSize = 18;0 p0 T- K5 R; g( s7 Q$ H1 [
复制代码
+ E3 K \2 i) B9 x* X* a然后配置好双缓冲的两个内存区域地址3 h \1 B, N& q; Z! u
4 ?4 ` z( f" ] B7 s5 K$ i/ w- DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)&sbus_rx_buffer[0][0];
( s8 g8 @0 o4 x' l
复制代码
S+ l* s! k5 C0 l! c这里配置内存区域2的地址,并把当前DMA指向内存区域1的地址
5 t5 \* h+ l$ Q K% ]6 C
7 u' d9 s. l8 W; t+ y" g- DMA_DoubleBufferModeConfig(DMA1_Stream1,(uint32_t)&sbus_rx_buffer[1][0],DMA_Memory_0); //first used memory configuration
8 ?; Q# x0 k3 J
复制代码 0 n) u6 x; _. `2 }9 e8 `
启动DMA双缓冲# ?' a) H( {/ y' X) h; D- d7 ^! L6 R
9 e; _: Q. n/ x& E! _
- DMA_DoubleBufferModeCmd(DMA1_Stream1, ENABLE);& [$ C- M% q0 p3 O
复制代码 ! A v9 ]8 g c( F
我们使用DMA时利用串口的空闲中断接收一帧一帧的数据。
1 R$ o4 _3 L! G$ [+ D$ Z0 V7 D5 L在串口中断处理函数中对数据进行处理
! X) k ?, m& p6 `& K& \* d' E7 F# E$ x$ }
- " ?& q- T& T' C' U
- uint16_t this_time_rx_len = 0; //当前剩余数据长度
- {$ G$ P5 k' \4 {, {# V - ( {" d7 h( |: f% {3 ?( v
- if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) //判断是否为空闲中断3 N3 y3 B8 ~* L r( E
- {% `% D1 w) q. `% _5 w2 o. f0 m# i
- USART_ReceiveData(USART3); //清除空闲中断标志位9 X% p5 w9 j1 h
- % Z/ F0 z$ p* G7 h9 C5 |
- if(DMA_GetCurrentMemoryTarget(DMA1_Stream1) == DMA_Memory_0) //获取当前目标内存是否为 DMA_Memory_01 I! k* S5 o6 s' f
- {6 f% l+ B+ a$ ?: U* |: S
- //重新设置DMA
& L/ a0 a. f8 U t. m( { - DMA_Cmd(DMA1_Stream1, DISABLE);
4 q6 k: a9 y' g - this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量; I: y Y Q; W1 }, R+ A
- 3 z4 x0 G7 |) m% j0 \. @
- DMA_SetCurrDataCounter(DMA1_Stream1, 18); //重新设置数据量
& Z0 j( W3 c. V) u6 ]0 A -
. l; y$ @4 f! q; [ h" N - DMA_Cmd(DMA1_Stream1, ENABLE);
' w. n: Z; \, _. F7 A( Q: O - if(this_time_rx_len == 18) //接收成功18个字节长度) y: n; q+ |3 H
- {
+ M$ m) k$ s& W/ B - //处理遥控器数据, Z e1 u& Y5 q" O# t
- RemoteDataProcess(sbus_rx_buffer[1]); //Memory_1
$ y8 O; A3 z4 v+ h - }+ r- _3 |+ x: f: j+ O5 ` Z5 O
- 4 m* x& n7 j x: t
- }. V6 m! E' D2 b) a- H
- else //获取当前目标内存是否为 DMA_Memory_14 ]3 V" U1 h0 J( w" E- b7 z0 s; i2 D$ V
- {2 K; F: y5 z$ f
- //重新设置DMA, i- t# P9 r. L4 J
- DMA_Cmd(DMA1_Stream1, DISABLE);- J( e; {1 F, m+ l6 g
- this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量
5 }; e$ M5 s8 r( F! F3 d& O
1 R' y, i }* n. f2 u8 N- DMA_SetCurrDataCounter(DMA1_Stream1, 18); //重新设置数据量8 ?1 h& [2 y7 @6 l; T S' B- ]5 K
- $ p1 J, ]* N8 c1 p0 k
- DMA_Cmd(DMA1_Stream1, ENABLE);
. F ]- e, V- H8 S - 1 E: L4 c+ ^3 L& y1 p/ _7 e
- if( this_time_rx_len== 18) //接收成功18个字节长度
6 }+ `' @1 c d: n r0 _0 E! t - {
7 H3 ^' h9 u: g - //处理遥控器数据7 R, q# T( ?1 @* ?4 x
- RemoteDataProcess(sbus_rx_buffer[0]); //Memory_0' {, r' j3 {. A {8 [
- }2 q8 _6 H8 v4 y6 Y. O5 M4 a
- $ x% c3 m( E) q1 W& J) w7 C
- }: l1 n( U* y8 T3 b# l% E& U
- }
( U; b5 l) X, f5 R
复制代码
}! T- ^: U/ L5 f2 x9 A/ B8 w) [该逻辑在程序复位后,当一帧遥控器数据发送过来后,Memory0区域被填满18个字节,DMA自动指向Memory1,所以第一次进入中断处理函数时,获取内存指向的时候得到的指向是Memory1。
2 o, Z" w6 P* u0 i& s3 V然后判断剩余数据大小是否为18,如果是则说明上一帧数据接收正常,可以对数据进行处理,如果不为18,则说明上一帧数据接收长度不正常,这时候就不对数据进行处理,当下一帧数据发送过来后对数据进行检验,正常后才对数据进行处理。- _" a* L0 a& W9 s# `
7 {/ r, d ~+ |+ E6 c
然后是第二种:' a& U" s! U- j4 g0 G0 o
这里我们将BufferSize改为比一帧数据长度大的值(比18大),这样可以在一帧数据传输完成后不会因Counter值变0导致DMA指向下一内存区域。0 A7 R4 Z% O2 x( ^$ s, F
3 B& x" f& L, A+ k% F) ]% Z
- DMA_InitStruct.DMA_BufferSize = 36; v R J8 B$ {3 N3 ^# M
复制代码- uint16_t this_time_rx_len = 0; //当前剩余数据长度- N0 b8 W! f" I
- 1 |% N1 d }4 l/ {* V0 u; H
- if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) //判断是否为空闲中断
9 T+ A" J0 ~1 X$ ~. m - {
( e' y* U( s/ g - USART_ReceiveData(USART3); //清除空闲中断标志位2 v" \- y M! @+ p. v& M' X- t4 L/ y
- # s* f: T2 q0 S' _9 Z
- if(DMA_GetCurrentMemoryTarget(DMA1_Stream1) == DMA_Memory_0) //获取当前目标内存是否为 DMA_Memory_0
/ A! Z% y/ W" |& j - {) ~* D/ e: T4 E- t# s" w3 K& p
- //重新设置DMA
Z# S2 ~( d: W) U3 e- {! l - DMA_Cmd(DMA1_Stream1, DISABLE);
* N! \3 v( T/ C. Y, R" K - this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量 接收18字节,则剩余数据量为36-18=18
/ Z% |* t# d. Q. N3 i4 B5 } -
3 w0 I( U y$ h% |5 o* L - DMA_SetCurrDataCounter(DMA1_Stream1, 36); //重新设置数据量* l6 e2 ]; l y- I$ R6 E {
- DMA1_Stream1->CR |= (uint32_t)(DMA_SxCR_CT); //将DMA指向Memory1
, @0 [% {! j( f6 `1 t7 v1 {9 f -
5 z: P9 k/ B" Z; ~- n - DMA_Cmd(DMA1_Stream1, ENABLE);
% {: F6 T, n# e6 q - if(this_time_rx_len == 18) //接收成功18个字节长度
) [: C. Z/ T% @1 G" h4 J' W - {% c) f& `/ u' t9 I: z4 Z4 S
- //处理遥控器数据
, I5 K I2 `- @ u( F - RemoteDataProcess(sbus_rx_buffer[0]); //Memory_0
$ y2 t, |. W8 l% X' N) N1 b/ ~ - }- P& I _# s1 o/ Y* y% \
% l N- q" m$ Y- }
1 w3 [8 r; }! u7 y! f - else //获取当前目标内存是否为 DMA_Memory_1
2 H5 y* j" G+ t) f! c& \" M5 w" w; r - {& k1 ~* H ?) p. X1 W/ c" L
- //重新设置DMA5 n- o9 a2 `: f3 q1 ^8 m% ~7 y
- DMA_Cmd(DMA1_Stream1, DISABLE);( v7 X/ _3 n/ r/ ^0 r6 m' e X
- this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量 接收18字节,则剩余数据量为36-18=18
R. U9 o$ d- H4 b
- v7 O0 l+ V; v v- DMA_SetCurrDataCounter(DMA1_Stream1, 36); //重新设置数据量
4 j% u7 L4 x% _" i8 ^0 ]. \, f - DMA1_Stream1->CR &= ~(uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0/ ?( x- J: y( G+ ^8 s# {) @* s
# L5 y1 N5 v4 }5 o- DMA_Cmd(DMA1_Stream1, ENABLE);$ }2 }! T* Y- [
) ~* V, D- O! z& d- if( this_time_rx_len == 18) //接收成功18个字节长度
; V; B- |* a2 A1 Q. e - {
5 f- \% i2 V. H9 l* {: c# v% H - //处理遥控器数据* e, E, o9 E, D9 m" _5 J5 h
- RemoteDataProcess(sbus_rx_buffer[1]); //Memory_1
. z x* I! d b( g, s - }/ V7 Z5 a H$ F, t
- ) n' f3 L2 N' l& H S
- }
. d; L$ V* ?" o' I5 W- o - }. f4 t: r# R3 ?8 Z/ C0 h5 o* F
复制代码
* y; `/ ^2 W; c4 E: v( W9 L7 n$ [两种DMA双缓冲使用方法的区别 M- m C* B3 W- ]" L* X
第二种方法和第一种方法的区别大同小异,这种方法由于将BufferSize设为比18大的值,也就是当第一帧数据传输完成后,Counter值不会自动填满,且内存区域还是指向Memory0,然后我们将剩余数据量保存下来,再将Counter值填满,接着把DMA指向Memory1,最后通过判断剩余数据量来决定是否对数据进行处理。) [& B6 q/ V6 f4 P
9 i: W# Z7 X* V9 q+ s
可以看到,在第一种方法下,我们并没有使用
- d' F0 _/ k: q5 `4 ]
$ I) O& j8 ]6 K: f0 P: F- DMA1_Stream1->CR &= ~(uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0
& G5 \3 [1 q. W R/ Z
复制代码- DMA1_Stream1->CR |= (uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0
复制代码 ! B7 ^7 n1 n4 Q0 ?3 A; b
% U6 j5 D; S( e- N9 p% p7 j这是因为当传输数据量和BufferSize大小一致时,完整接收完一帧数据后,DMA会自动指在向下一内存地址,因此在第二种方法中的手动改变内存指向就由DMA自动处理了。
! @. m* s i; U3 C; i/ w. F————————————————, p |; n! r$ @" H! |: d6 l! u6 G
版权声明:水木皆Ming
: Y1 N) N' h$ `4 S0 j$ y! v+ R( J* W7 e
/ z7 n1 a0 _ H; X3 c% C+ D0 X! l
|
请问一下,使用DMA双缓冲区进行数据的发送的时候,出现了重复发送数据的情况,请问该怎么解决呢?感觉主要是剩余文件数量达不到BUFFER_SIZE要求,带来的重复发送数据。