DMA双缓冲是什么?. U3 ~$ Z( a O
在使用STM32的DMA时我们通常使用的是普通DMA传输,但STM32自带了一个双缓冲模式设置,这个模式有什么优点呢?& k2 P9 v, a7 r1 Z/ c
接下来我会在下面的介绍里详细说明:1 f# _; q7 ~' v) ^ X8 @ |4 W
9 W! _7 [# w( D- b4 L
DMA全称Direct Memory Access,即直接内存访问。
" G8 y: y" _ V. ?- j$ q普通DMA的使用是在DMA的数据流中进行的,设置好DMA的起点和终点以及传输的数据量即可开启DMA传输。
5 u3 `# u& N; u* t2 a; d; B' }. z# ^7 S! O! W' b/ v1 i
( J4 G9 u: U: \- \- D9 ?
. j, Z- m& [. B- ~
DMA开启后就开始从起点将数据搬运至终点,每搬一次,传输数据量大小就减1,当传输数据量为0时,DMA停止搬运。1 }5 {. E# v! m
& \- V1 j6 j" b
DMA的传输模式如果是普通模式,则当传输数据量为0时,需要程序中手动将传输数据量重新设置满才能开启下一次的DMA数据传输。; K( X# r& x; ~
DMA的传输模式如果是循环模式,则当传输数据量为0时,会自动将传输数据量设置为满的,这样数据就能不断的传输。! Y# D$ ], |: p# Y
& u; e: k; ~6 P% |+ ]1 v
9 t; C( u$ J- e8 P0 j/ [4 f( K
2 I% d8 }* H" y/ i; [, G; k) k那么,双缓冲是什么意思呢?9 Q' S' Q7 A, ]3 [" i6 U% _. I, e+ y
我们知道,普通DMA的目标数据储存区域只有一个,也就是如果当数据存满后,新的数据又传输过来了,那么旧的数据会被新的数据覆盖。(这就是普通DMA的缺点)1 }& r0 E) b9 Z2 l* B2 m$ |
# K" ~$ }" `5 P7 P& b7 s7 `$ X6 x
8 i- B7 A/ \( {0 K
; @- N* P, `9 z0 a# m
, i) ]* P1 h+ l/ S v而双缓冲模式下,我们DMA的目标数据储存区域有两个,也就是双缓冲,$ b5 d4 }; ~! j. o
当一次完整的数据传输结束后(即Counter值从初始值变为0),会自动指向另一个内存区域。
0 r. A5 {6 N5 k0 e/ H/ S' P0 P% O. x, A9 G$ }1 h, t
7 V) Y) I2 Y$ @0 q' o
: I* q5 M( C- q6 U* `( A# I6 R
如何使用DMA双缓存?+ ]! s) m2 i( w1 x; i- H
那么我们在程序里怎么使用双缓冲呢?) B z/ P" _! h
这里我们有两种方案使用双缓冲,一种是利用双缓冲自动跳转内存区域,一种是主动跳转内存区域。
4 k; e' |( p6 v( j* Y1 ~# D- g0 X0 d3 c, E* h( {& o3 T
这里我们使用DMA的传输模式是循环模式
5 G! J, |( s7 u6 F6 L% m( R* \' H
- DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;/ j6 t& ]6 k- Y
复制代码 / w) C3 t2 W L: P
首先是第一种:7 D8 M/ X: V# p! {2 X% K
这里我们以大疆RoboMaster的遥控器使用为示例:
; B3 n) S2 ?- Z% x3 h! k( `- }首先我们定义好双缓冲使用的两个内存区域
1 L" W! X+ e8 F: u+ C1 q6 K- uint8_t sbus_rx_buffer[2][18u]; //double sbus rx buffer to save data$ I2 `3 ?+ c8 _4 }$ V
复制代码
0 b M7 {7 V% x* u这里我们设置长度为18,也就是一帧遥控器数据的长度。3 n5 i8 k% d# h6 @
, p* W" g4 U0 D% V- i/ b2 k- DMA_InitStruct.DMA_BufferSize = 18;
/ h# \1 y; Y7 k- R& W9 o ?3 Q, ^
复制代码 ! O; [% n1 L& O2 c
然后配置好双缓冲的两个内存区域地址) g* ?" g, d& J c+ U! e
+ P: W! E3 c5 k- DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)&sbus_rx_buffer[0][0];4 Y6 }% |! r$ s- d5 a" \9 r; J7 |1 q
复制代码
0 [8 Q' q/ ^+ Z" y这里配置内存区域2的地址,并把当前DMA指向内存区域1的地址
( L; c" T- C% E$ A1 B2 E9 d+ B/ w: a( B1 L% s
- DMA_DoubleBufferModeConfig(DMA1_Stream1,(uint32_t)&sbus_rx_buffer[1][0],DMA_Memory_0); //first used memory configuration
% u/ y' C$ e( s/ ]- |
复制代码 . L/ ]/ P3 I# b* ]3 [% X2 R8 v4 |4 }
启动DMA双缓冲& I7 a) c; ^$ c$ g: P$ _. o
) M" p# k4 M0 ^- DMA_DoubleBufferModeCmd(DMA1_Stream1, ENABLE);
; m2 B" `8 O6 G8 J& q
复制代码
7 @# R3 S$ t- b$ R! F/ {我们使用DMA时利用串口的空闲中断接收一帧一帧的数据。
/ s* M( b. w8 u' y c: E在串口中断处理函数中对数据进行处理
# O! B) Y: s( Y8 ]8 R# @/ |+ c: Z8 O9 H {' y
- 9 {, p. Z" | L/ N0 |: a# B
- uint16_t this_time_rx_len = 0; //当前剩余数据长度
4 w# r: l5 F# V2 d' C2 d - 5 G7 J$ d" e* Y/ `
- if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) //判断是否为空闲中断* q) F; ?( P8 `7 U
- {1 I: ]9 {1 \5 k8 E
- USART_ReceiveData(USART3); //清除空闲中断标志位1 v$ R3 K# X1 }2 S7 s2 G) M+ e9 g
- ( A/ M/ A( j8 d; U
- if(DMA_GetCurrentMemoryTarget(DMA1_Stream1) == DMA_Memory_0) //获取当前目标内存是否为 DMA_Memory_0' O, m$ V8 p( g3 x
- {
9 D6 L! B: `$ [# s! A' h - //重新设置DMA
! i$ A2 [, i% i' M5 F9 `. k - DMA_Cmd(DMA1_Stream1, DISABLE);
6 x! s) a: B7 v: M( A - this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量4 k3 T- b/ i j
- % V% P3 {! u# v& w' y
- DMA_SetCurrDataCounter(DMA1_Stream1, 18); //重新设置数据量# a* J0 Z( v T3 v2 A* P: M
-
9 K4 H# M5 q2 W; d6 O - DMA_Cmd(DMA1_Stream1, ENABLE);
( b) N5 G. Z S - if(this_time_rx_len == 18) //接收成功18个字节长度; ~( A) B% G. _
- { V2 J1 I. f6 ?% p$ b9 F
- //处理遥控器数据' W: K; I) i3 {6 ~2 {. n- m; j
- RemoteDataProcess(sbus_rx_buffer[1]); //Memory_1( S4 q8 E8 V" l" X+ d
- }
8 P* @- b y; M$ {* L0 j
" a$ J) q" @ |6 \- }$ S! j- J. B6 b2 W, |- m6 @/ ?
- else //获取当前目标内存是否为 DMA_Memory_1
# f% M8 M, x& v [ - {
7 u* Z) p& o1 B+ J - //重新设置DMA
" h- F' M* A( J0 k - DMA_Cmd(DMA1_Stream1, DISABLE);0 R/ V9 f$ D/ A
- this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量- H0 g! U" y: [0 t% t3 `
- $ }3 ?* \+ [* y! ]# _# j
- DMA_SetCurrDataCounter(DMA1_Stream1, 18); //重新设置数据量) U. ^5 y- u) \) A& V8 M
6 {3 ?/ S& c) ]9 ?- DMA_Cmd(DMA1_Stream1, ENABLE);/ B% f4 A' q4 A5 w- n. [9 v6 w
5 M6 M: k6 n# z0 {$ Z4 Z4 {- if( this_time_rx_len== 18) //接收成功18个字节长度
, W% H h7 b( j( e - {7 P& F8 Q+ J( I
- //处理遥控器数据
" G% I6 [% s% Z' g, g8 B! y1 U - RemoteDataProcess(sbus_rx_buffer[0]); //Memory_05 A1 \: V" ^: R" e1 ]* B" [) a* i
- }4 q2 ^7 \3 s$ ^0 a$ @% H. M
- 4 {; F+ B4 v5 J( z5 _# z6 l* t
- }3 c) @2 |+ Q" x) i- u$ m
- }3 D; h9 ~" e5 B! ^( j4 i/ J9 H I
复制代码 V" i& H* S* A& {4 c# `
该逻辑在程序复位后,当一帧遥控器数据发送过来后,Memory0区域被填满18个字节,DMA自动指向Memory1,所以第一次进入中断处理函数时,获取内存指向的时候得到的指向是Memory1。
0 c" z6 c2 I- A8 c2 G7 o1 i9 x然后判断剩余数据大小是否为18,如果是则说明上一帧数据接收正常,可以对数据进行处理,如果不为18,则说明上一帧数据接收长度不正常,这时候就不对数据进行处理,当下一帧数据发送过来后对数据进行检验,正常后才对数据进行处理。
T6 n: {/ k: @8 Q8 T7 J1 V* y) d/ V9 C
然后是第二种:
9 ?3 X$ ]: {4 D1 [. M这里我们将BufferSize改为比一帧数据长度大的值(比18大),这样可以在一帧数据传输完成后不会因Counter值变0导致DMA指向下一内存区域。: I# U4 ^) s2 w) ?3 K' Z' X3 m
# ~8 j3 C2 j+ k5 ~' Z9 K
- DMA_InitStruct.DMA_BufferSize = 36;$ I3 }- t, ^# o! O! Z2 ?; T4 n
复制代码- uint16_t this_time_rx_len = 0; //当前剩余数据长度* a' L. l+ D- S5 G8 y
- : Y! H$ S8 Q T. H# {+ V/ a
- if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) //判断是否为空闲中断
; ~$ Z; }" X! K+ P' j- |2 Q- |2 x - {6 y; ]) |- {' \$ M
- USART_ReceiveData(USART3); //清除空闲中断标志位
8 _8 v& I8 I3 F/ ^: s1 f. Y
5 Q% q- C' u3 L' E- if(DMA_GetCurrentMemoryTarget(DMA1_Stream1) == DMA_Memory_0) //获取当前目标内存是否为 DMA_Memory_0
, v2 b2 l* i. a: a% I) X2 ]5 f - {
# ^& p6 `% G9 P4 {- I* ?3 H - //重新设置DMA
, _4 Z, }$ U: v" m1 L% u. q4 b - DMA_Cmd(DMA1_Stream1, DISABLE);
5 d% R7 ^$ x) C1 b - this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量 接收18字节,则剩余数据量为36-18=18& K! f! C" }. n) T4 v& O
-
; u* ]( W, x" C( o' ~; _3 @ - DMA_SetCurrDataCounter(DMA1_Stream1, 36); //重新设置数据量- {) ` K, @3 ^% o0 X
- DMA1_Stream1->CR |= (uint32_t)(DMA_SxCR_CT); //将DMA指向Memory1& t6 l" k2 z" Z
-
: }7 a7 s J; z7 f. J, g+ x3 Q - DMA_Cmd(DMA1_Stream1, ENABLE);0 B+ x5 G. t) K+ y+ l
- if(this_time_rx_len == 18) //接收成功18个字节长度
5 z( ?- |- m5 K: v9 O - {
. e+ h4 ~9 C- \, I4 b4 z7 j, P - //处理遥控器数据
4 k- h' d& d: i% D; Y6 d1 W - RemoteDataProcess(sbus_rx_buffer[0]); //Memory_08 Q5 M) q- {& Q6 }( C- T
- }3 A5 M% u" A: o9 H4 H) s9 s! y
- n- z' A" C3 Z& [0 D- }
1 x1 U( \% T3 Y7 J$ @3 ^3 d - else //获取当前目标内存是否为 DMA_Memory_1
. c! s" ~+ b# S. A; M/ X - {
: _: S# r2 |* e0 o8 i - //重新设置DMA
: @: E* Y- X% N: |# I - DMA_Cmd(DMA1_Stream1, DISABLE);; u0 E: K! y& \ t4 r
- this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量 接收18字节,则剩余数据量为36-18=189 K: m' u/ R; r3 S( i! Q
- % G: k Z/ c4 R
- DMA_SetCurrDataCounter(DMA1_Stream1, 36); //重新设置数据量; M; [: v+ c; q7 f6 T
- DMA1_Stream1->CR &= ~(uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0, ?* Y5 o$ j+ z6 v2 H8 T& t9 d& S
- . t3 [, M8 q6 a, Q
- DMA_Cmd(DMA1_Stream1, ENABLE);: l8 w+ G4 m- M: m( }
- . |3 ~4 c; H8 P6 S) q
- if( this_time_rx_len == 18) //接收成功18个字节长度( H) E4 m+ y3 S7 i$ |1 p4 _
- {
4 I( G H% U, }! S8 @ - //处理遥控器数据) d% g5 x, ~/ ~, D9 P4 c+ e; v+ f
- RemoteDataProcess(sbus_rx_buffer[1]); //Memory_1
& G8 S7 q. h- m' M7 C% c- K W1 L - }' v9 l6 M! N+ B& S/ m' k
) d4 Y. C g$ Y- U; X) ]2 r1 B5 d2 g- }
2 P$ P, w! A) _- t/ M3 a5 i5 U - }
1 ~# h8 V% s* T% O. i* m
复制代码
: E! L ]5 [ r两种DMA双缓冲使用方法的区别: p% J% R, B) {" Q6 L: `; ~( ^
第二种方法和第一种方法的区别大同小异,这种方法由于将BufferSize设为比18大的值,也就是当第一帧数据传输完成后,Counter值不会自动填满,且内存区域还是指向Memory0,然后我们将剩余数据量保存下来,再将Counter值填满,接着把DMA指向Memory1,最后通过判断剩余数据量来决定是否对数据进行处理。2 [$ a: X: ?; F
9 w4 b" W0 g, ^$ \, A
可以看到,在第一种方法下,我们并没有使用
: [9 N& c- y- s9 G! q4 \& z2 b; \
( l0 u$ t$ I( e- DMA1_Stream1->CR &= ~(uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0, L/ {: R! v2 F3 E) t0 s# r3 G$ ]) A
复制代码- DMA1_Stream1->CR |= (uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0
复制代码
# Z& M( F/ {) |; f
/ c" ?% b7 V8 b这是因为当传输数据量和BufferSize大小一致时,完整接收完一帧数据后,DMA会自动指在向下一内存地址,因此在第二种方法中的手动改变内存指向就由DMA自动处理了。
. Q, T( v/ z7 N4 \————————————————" V% E. J5 t! }2 w3 u/ c
版权声明:水木皆Ming
1 [# I1 E& \% N8 x9 r; ~0 B# d4 a2 J
' h6 T. U/ |" j! Z" m0 {: M |
请问一下,使用DMA双缓冲区进行数据的发送的时候,出现了重复发送数据的情况,请问该怎么解决呢?感觉主要是剩余文件数量达不到BUFFER_SIZE要求,带来的重复发送数据。