DMA双缓冲是什么?
2 {/ X1 t0 h& E6 [+ e/ X! O" }( {在使用STM32的DMA时我们通常使用的是普通DMA传输,但STM32自带了一个双缓冲模式设置,这个模式有什么优点呢?) K' e/ v: n5 w8 Q$ P
接下来我会在下面的介绍里详细说明:
/ s" q$ Y* X1 R7 b* ?/ F8 s
8 D8 G; D c2 P& G- a1 a3 n3 aDMA全称Direct Memory Access,即直接内存访问。5 [) R( O9 g; l9 {$ b
普通DMA的使用是在DMA的数据流中进行的,设置好DMA的起点和终点以及传输的数据量即可开启DMA传输。2 o4 `5 s ^0 I a8 m1 R, l, m) k
. `5 v) a( P0 G9 J* L+ }
# j$ Q$ a1 e# C2 c
/ @7 a' Z3 Z- ?% ~% eDMA开启后就开始从起点将数据搬运至终点,每搬一次,传输数据量大小就减1,当传输数据量为0时,DMA停止搬运。
( {6 m' Z: T: E, f, J; I" O+ r( y
DMA的传输模式如果是普通模式,则当传输数据量为0时,需要程序中手动将传输数据量重新设置满才能开启下一次的DMA数据传输。
) C/ l) }" t$ d5 `8 bDMA的传输模式如果是循环模式,则当传输数据量为0时,会自动将传输数据量设置为满的,这样数据就能不断的传输。
( P% o; C; Y, H! D3 ^4 y' {4 X8 a a5 E9 H" d
- }+ T0 D: d. h8 h8 k3 \: C2 {; N, V C) ~* k
那么,双缓冲是什么意思呢?
# ?# _6 L2 f' Y3 g- {! h我们知道,普通DMA的目标数据储存区域只有一个,也就是如果当数据存满后,新的数据又传输过来了,那么旧的数据会被新的数据覆盖。(这就是普通DMA的缺点)# u1 E% Y$ ]) ^! i7 @/ @
) p+ z5 x' ~# }0 S4 j, c$ W
$ C; ] ?8 L, R& l3 b8 ]5 ^+ u
- H% t9 c2 ^* N, n$ @1 y0 h8 Z L$ f" v# C- E
而双缓冲模式下,我们DMA的目标数据储存区域有两个,也就是双缓冲,7 }$ F4 F" K" X# r
当一次完整的数据传输结束后(即Counter值从初始值变为0),会自动指向另一个内存区域。
" W9 f7 L8 K" p e
. h7 y6 l- a. K+ g
9 a5 D/ k/ {" V- P8 g* t' x) K- v; L( i$ k+ [+ \/ b- P
如何使用DMA双缓存?
. n4 m5 y; i) H' B- x3 Q' }那么我们在程序里怎么使用双缓冲呢?
( ]' O1 Y1 S& O- k; N7 i4 E这里我们有两种方案使用双缓冲,一种是利用双缓冲自动跳转内存区域,一种是主动跳转内存区域。
4 l" `8 O9 `) d8 ?4 m5 {) y! b
' ^$ i2 t" U' Q! l3 `9 @这里我们使用DMA的传输模式是循环模式
- |3 O) r: w* {( c- `# m) E- w: S; P# F# F; e0 v
- DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;+ Z1 v- a3 I5 C& g
复制代码 0 T- O! Z% Z; J% Q7 A) u
首先是第一种:% u3 b3 n" [+ g5 a5 x0 `
这里我们以大疆RoboMaster的遥控器使用为示例:/ K; a4 M. j7 U1 a% U9 S; D/ v
首先我们定义好双缓冲使用的两个内存区域9 ~9 l6 r* b7 i
- uint8_t sbus_rx_buffer[2][18u]; //double sbus rx buffer to save data6 h8 P* s3 B4 s4 ~
复制代码
; q9 s0 @- a6 C4 s这里我们设置长度为18,也就是一帧遥控器数据的长度。
' J: d5 g+ M4 t# b* k3 w2 y' T% v% z, m* e5 T K
- DMA_InitStruct.DMA_BufferSize = 18;# E% B, f/ [% U" w
复制代码 * ?) g3 S4 |" j3 {
然后配置好双缓冲的两个内存区域地址, H4 r! t L( Y
6 N7 k4 P8 b1 g* S
- DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)&sbus_rx_buffer[0][0];$ [' _1 O; F% |+ f; h& |' X$ G
复制代码
! L: O+ P/ w2 B* d这里配置内存区域2的地址,并把当前DMA指向内存区域1的地址! ^( H* G2 N$ C* S! R8 ?, k
, T& v( q& P2 W$ @( l& R- DMA_DoubleBufferModeConfig(DMA1_Stream1,(uint32_t)&sbus_rx_buffer[1][0],DMA_Memory_0); //first used memory configuration
, m) `4 U6 Z4 `- C% x% y3 q: N9 n
复制代码 ' c; R, p7 q, J: T5 ~
启动DMA双缓冲5 m N! X# Y. E Z- Q9 O
" k% N9 S- A2 }1 h& L- DMA_DoubleBufferModeCmd(DMA1_Stream1, ENABLE);
5 g$ Q6 _) X4 A$ S9 z
复制代码
" v" J' F6 ?5 J' _9 C6 L/ U我们使用DMA时利用串口的空闲中断接收一帧一帧的数据。
4 Z, o( W+ i/ C# e; l b9 k* ]5 ?在串口中断处理函数中对数据进行处理
, W2 W4 E/ }. f' e& ^
$ E) V+ d/ Q2 i) P- + U [: L3 O: q+ L# U+ P
- uint16_t this_time_rx_len = 0; //当前剩余数据长度( P. l9 a; P: J$ h! s! E3 E+ W
5 n/ ~5 r. B# _- if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) //判断是否为空闲中断
& U, v, D1 z- |2 M' ]! v - {
2 H" X% I. p2 }% r. i7 o - USART_ReceiveData(USART3); //清除空闲中断标志位
1 y4 W) {% D5 D s. T9 v6 q7 O$ |
' j9 L8 {* z' ~- if(DMA_GetCurrentMemoryTarget(DMA1_Stream1) == DMA_Memory_0) //获取当前目标内存是否为 DMA_Memory_0
3 U0 |7 U Y- R9 j) Y g1 y! z - {0 G' M. r& q4 p/ R
- //重新设置DMA
( z" r/ p4 m3 j. z( j: X. d, X- x - DMA_Cmd(DMA1_Stream1, DISABLE);+ g, O! G/ A! Z
- this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量. Z) n' Z7 G7 r; \7 H
-
4 ~) v# K3 Q* X' P; l L T - DMA_SetCurrDataCounter(DMA1_Stream1, 18); //重新设置数据量2 s3 r0 y( b L5 M# `7 j
- . B' _* y6 [! ]& ]8 c% ^ o
- DMA_Cmd(DMA1_Stream1, ENABLE);7 }6 m" I! J3 {& g: q
- if(this_time_rx_len == 18) //接收成功18个字节长度
' a- T5 d0 N8 G$ a$ U) I2 F- Z - {5 D! V$ Y }* j, u6 F% K' n
- //处理遥控器数据$ p% |& o) g: o( ~
- RemoteDataProcess(sbus_rx_buffer[1]); //Memory_1- n; ^: d+ E, Z/ n. Q2 E" P u
- }
* X5 J& `$ K: u4 K. s+ p9 | - 6 c. s4 n3 [5 K( {: p
- }* @- L" X- N5 ?
- else //获取当前目标内存是否为 DMA_Memory_1
2 L8 j' X' N, r ]" ` - {4 z8 \7 U! g m( G% e& Z
- //重新设置DMA
2 E4 P5 A- Z1 d - DMA_Cmd(DMA1_Stream1, DISABLE);
- o9 y9 C$ c3 J$ W2 k; j - this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量
" S3 }) f/ W' c
" ^, j" h; h X6 x% x4 o; W, G- DMA_SetCurrDataCounter(DMA1_Stream1, 18); //重新设置数据量
y- \9 n, ?& ]5 p( I3 W* @2 g" b - , _3 N4 ^% @" C: U2 [9 b( ]
- DMA_Cmd(DMA1_Stream1, ENABLE);3 e1 i# T6 P- B8 _4 \ ^) G3 w
- - b' p. k, C# q" }: t6 Q
- if( this_time_rx_len== 18) //接收成功18个字节长度6 G3 S8 _7 ^" _0 r/ K
- {
, C0 {2 `9 u& Z6 Y0 \ - //处理遥控器数据
* x5 W, @, `; s6 J2 z - RemoteDataProcess(sbus_rx_buffer[0]); //Memory_0
# Y( J( ~4 v3 K+ x' P - }
- [; x! [, D. @* I# [& {2 {4 ?' a4 W2 L
: j* s, S+ t {8 ~- }2 O& C) H3 i1 l: |9 p1 Y8 I
- }! L! a; S+ V" l# r$ k& ]6 A
复制代码
/ I: Q* f/ M) F# t7 K该逻辑在程序复位后,当一帧遥控器数据发送过来后,Memory0区域被填满18个字节,DMA自动指向Memory1,所以第一次进入中断处理函数时,获取内存指向的时候得到的指向是Memory1。$ l9 i% k2 o3 L' T4 [) M7 ?
然后判断剩余数据大小是否为18,如果是则说明上一帧数据接收正常,可以对数据进行处理,如果不为18,则说明上一帧数据接收长度不正常,这时候就不对数据进行处理,当下一帧数据发送过来后对数据进行检验,正常后才对数据进行处理。
2 J1 A* m8 L3 ]8 E7 ^8 r& s. b/ W* B0 h% u5 @% k
然后是第二种:
3 {1 |7 ~# Q- `/ Z9 F0 R这里我们将BufferSize改为比一帧数据长度大的值(比18大),这样可以在一帧数据传输完成后不会因Counter值变0导致DMA指向下一内存区域。
5 O! E6 _0 B, r8 w! T, \) m, p3 t3 H+ z
- DMA_InitStruct.DMA_BufferSize = 36;
2 {! K9 i8 ~1 `( ~
复制代码- uint16_t this_time_rx_len = 0; //当前剩余数据长度
- [2 R0 C. j" J( q% o2 l0 A- P/ ~. @1 t
2 m( W- O0 K* Z9 x5 t* f ~- if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) //判断是否为空闲中断
% a' @, y. i) `/ X# [+ r8 N - {+ w1 G4 a5 @, V" \ I! x
- USART_ReceiveData(USART3); //清除空闲中断标志位& j3 i' ]$ `2 {9 ~# b
( d, z; D; m& C4 ^4 N( A- if(DMA_GetCurrentMemoryTarget(DMA1_Stream1) == DMA_Memory_0) //获取当前目标内存是否为 DMA_Memory_0
& \( x) m3 P. f- {- v - {) ^: b( m9 L1 G$ b e
- //重新设置DMA5 F$ |% V0 m$ @) _+ @
- DMA_Cmd(DMA1_Stream1, DISABLE);( Y( g8 @; }, e- o
- this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量 接收18字节,则剩余数据量为36-18=18
7 z" v6 K: S0 K -
) z7 e/ f4 H, Q! j0 R5 I - DMA_SetCurrDataCounter(DMA1_Stream1, 36); //重新设置数据量
; ?! S6 t% e+ t" c1 I - DMA1_Stream1->CR |= (uint32_t)(DMA_SxCR_CT); //将DMA指向Memory1. @$ M! w3 `' ?& @# f
- * x& M5 Y% h! e# C+ b+ R
- DMA_Cmd(DMA1_Stream1, ENABLE);
" E$ L2 p) f$ o* D - if(this_time_rx_len == 18) //接收成功18个字节长度: c- p' d& w/ C) H+ x3 l. z
- {1 I! H: g% B3 c% a& C- I, U
- //处理遥控器数据
$ B" j/ v8 `7 m% |. z' } - RemoteDataProcess(sbus_rx_buffer[0]); //Memory_0
% Y; \* e( e* V" c5 U# F; [$ ~ - }/ v6 n' G l6 z
- R$ Y) e+ W/ r5 f) |) M( H% D% G- }
7 W$ |$ y9 d/ @( A- [7 x - else //获取当前目标内存是否为 DMA_Memory_1" N2 i1 ?/ x) E8 C8 a: Q4 A) l
- {& S, l8 b9 \1 T! N$ ]
- //重新设置DMA6 b5 ~1 w( S; r( M+ H
- DMA_Cmd(DMA1_Stream1, DISABLE);
/ f( ~. q( K& Y, i' j - this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量 接收18字节,则剩余数据量为36-18=18# O" _, J6 J4 I$ C8 ?8 I7 J& s
- 6 `2 r- a. ~5 e) I) V/ m% Z6 b
- DMA_SetCurrDataCounter(DMA1_Stream1, 36); //重新设置数据量% ~2 @! G# y. ?5 E7 Z0 [
- DMA1_Stream1->CR &= ~(uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0. v: ?4 P0 @* o7 ?/ ]( C
- 5 W! }* _6 K* V G! z$ w
- DMA_Cmd(DMA1_Stream1, ENABLE);: v6 A/ R) R3 o% Y" ^2 C* K6 I. F
- $ U, y1 b) @* p- H8 f5 e/ s
- if( this_time_rx_len == 18) //接收成功18个字节长度4 y5 s4 k1 |5 |
- {* y" }( q% [8 H' S' k% W
- //处理遥控器数据
. w% {9 u( H7 _, R% A: F* m5 C, k - RemoteDataProcess(sbus_rx_buffer[1]); //Memory_1. B+ R* w/ S5 v1 X' k4 d5 n; n9 _
- }
0 A; N' `. s) F; z3 d
. `$ D* K6 J/ ^4 G/ `- }
8 H8 ^8 {, s4 p - }
; x9 p( B& p( N$ F
复制代码
4 p ~5 ]: q4 N8 Q! j两种DMA双缓冲使用方法的区别
( R7 |. U: ~6 p3 j7 k第二种方法和第一种方法的区别大同小异,这种方法由于将BufferSize设为比18大的值,也就是当第一帧数据传输完成后,Counter值不会自动填满,且内存区域还是指向Memory0,然后我们将剩余数据量保存下来,再将Counter值填满,接着把DMA指向Memory1,最后通过判断剩余数据量来决定是否对数据进行处理。. J U6 J# B, Y% y
5 j; o% j" m' V( p+ g' V4 B8 @! U可以看到,在第一种方法下,我们并没有使用
0 U# u- `, T9 F; n
; A& W2 F4 L; k2 J$ O' l- P* O& F- DMA1_Stream1->CR &= ~(uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0
! e+ C) r% d% r; K
复制代码- DMA1_Stream1->CR |= (uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0
复制代码
9 G' D1 j, a2 ~% h( T- m1 y- i7 V
" Z b4 Q$ R# L0 z这是因为当传输数据量和BufferSize大小一致时,完整接收完一帧数据后,DMA会自动指在向下一内存地址,因此在第二种方法中的手动改变内存指向就由DMA自动处理了。
' v5 u4 N# V4 t, W' N————————————————5 r# {6 K9 P, U3 m' _6 |
版权声明:水木皆Ming, h# X5 y% {; `$ `1 M5 ^/ d5 |
8 S0 `) o; b3 d+ L; L6 ^
! H1 N: G$ K3 u( }0 b8 z |
请问一下,使用DMA双缓冲区进行数据的发送的时候,出现了重复发送数据的情况,请问该怎么解决呢?感觉主要是剩余文件数量达不到BUFFER_SIZE要求,带来的重复发送数据。