DMA双缓冲是什么?) g4 d; J! A" i
在使用STM32的DMA时我们通常使用的是普通DMA传输,但STM32自带了一个双缓冲模式设置,这个模式有什么优点呢?
6 I% r/ e: Y+ Y$ n" l! e接下来我会在下面的介绍里详细说明:
8 _$ h# W" Q: f
! |0 a f' k+ i! vDMA全称Direct Memory Access,即直接内存访问。
" v9 h1 v0 M5 ]5 W2 x, W0 Q4 ^普通DMA的使用是在DMA的数据流中进行的,设置好DMA的起点和终点以及传输的数据量即可开启DMA传输。
5 R2 b1 J0 A& S) V& Z+ F
) Z: Y! b! S) p
% ^: v' h+ x: {: ^5 t$ Q
- K, o# Y! C& w8 v0 @
DMA开启后就开始从起点将数据搬运至终点,每搬一次,传输数据量大小就减1,当传输数据量为0时,DMA停止搬运。
: z/ @: l8 V$ \7 J$ [7 V8 T; F1 g* Q5 C
DMA的传输模式如果是普通模式,则当传输数据量为0时,需要程序中手动将传输数据量重新设置满才能开启下一次的DMA数据传输。
3 N% c' d6 y% E0 U' d4 X+ X8 o8 `$ ]' w5 qDMA的传输模式如果是循环模式,则当传输数据量为0时,会自动将传输数据量设置为满的,这样数据就能不断的传输。( I5 Y1 Z3 o4 k E" h3 a% V% G
+ M/ ^, [/ h/ g2 c: g$ a; P/ L
) h; k) f, h; g0 L. u) ]
% j( X1 g5 f6 ]% G4 l Y) q
那么,双缓冲是什么意思呢?
I. A* }5 B5 c. O0 @我们知道,普通DMA的目标数据储存区域只有一个,也就是如果当数据存满后,新的数据又传输过来了,那么旧的数据会被新的数据覆盖。(这就是普通DMA的缺点)
$ `9 L d/ z% l {" t' T9 q" ~0 G# Q" E- A0 ~/ o
, P. ]1 \% I+ @9 Q* @. K3 p
6 v9 n1 C0 Y: q8 G3 H1 A) S6 m" f6 m$ I1 r' M/ {3 u
而双缓冲模式下,我们DMA的目标数据储存区域有两个,也就是双缓冲,
, [0 F L6 V! @- f当一次完整的数据传输结束后(即Counter值从初始值变为0),会自动指向另一个内存区域。
3 |! a, \, P j8 z6 A
3 u7 [0 m6 c3 ]7 [9 }
: K! f" x9 ^5 W7 t( ]$ E6 a8 U2 L9 J. d7 Y
如何使用DMA双缓存?
( P+ W1 P! c. f4 d那么我们在程序里怎么使用双缓冲呢?7 Z6 S1 }$ X, _* c
这里我们有两种方案使用双缓冲,一种是利用双缓冲自动跳转内存区域,一种是主动跳转内存区域。 P( z/ e( w4 v; P3 o
* i$ C' N3 o& h7 z& k$ B
这里我们使用DMA的传输模式是循环模式8 G( q8 l0 N2 X3 [& N: U9 x/ ^
2 q( R7 r. O3 T7 N1 v3 x2 C3 k8 k- DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;
$ ^( N7 A4 R; E, ]! L
复制代码
4 i2 K: k/ ~! s6 R/ I首先是第一种:1 F( K4 t, L0 z# |1 Y* ]
这里我们以大疆RoboMaster的遥控器使用为示例:+ g0 `4 {0 j8 c% k5 Y
首先我们定义好双缓冲使用的两个内存区域* E5 X1 g! w ~6 I* R
- uint8_t sbus_rx_buffer[2][18u]; //double sbus rx buffer to save data
% `2 J9 D, A8 a! M# d- K
复制代码
+ P, s( G( M; Z! p P9 ]这里我们设置长度为18,也就是一帧遥控器数据的长度。" r9 @3 z. h) M
( @+ ]$ c2 j+ Z7 E+ h) D" N9 @' K- DMA_InitStruct.DMA_BufferSize = 18;; o7 L- K# H. O, t8 a+ T- k
复制代码
. A& j/ r# W" a( n4 g7 ]' J3 v! }然后配置好双缓冲的两个内存区域地址
* Z7 ]7 | `+ ^3 X
) [9 t% i# a0 M L, ?( l3 |; P- DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)&sbus_rx_buffer[0][0];
( l# j2 r: e1 J
复制代码 4 v9 B; R$ m# p' H7 s
这里配置内存区域2的地址,并把当前DMA指向内存区域1的地址# `7 i% |' Y1 s# V
' ^9 x: ?7 p! v
- DMA_DoubleBufferModeConfig(DMA1_Stream1,(uint32_t)&sbus_rx_buffer[1][0],DMA_Memory_0); //first used memory configuration
- ?6 A6 k. K+ o* S# w. T! r. v
复制代码 ! H5 |) Q+ W. [
启动DMA双缓冲$ _; T" Z. F2 A/ p: l4 Y( Q& C
/ n! F: y. ^4 R5 o8 g
- DMA_DoubleBufferModeCmd(DMA1_Stream1, ENABLE);1 f$ Z+ K$ R& S) Z0 Y9 N B
复制代码 / [* J9 c9 r& [/ _, S7 n6 S
我们使用DMA时利用串口的空闲中断接收一帧一帧的数据。
3 H* _; ? w4 S% x, @$ a% T在串口中断处理函数中对数据进行处理
+ Z! |3 k3 a/ G$ {! l0 ^
) I& z& M7 n' h0 N$ d- 1 f9 P! F( W9 x) g9 J
- uint16_t this_time_rx_len = 0; //当前剩余数据长度
% g$ v/ w* ?5 m2 Y - 7 t% f) k) `) L. D, s8 q
- if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) //判断是否为空闲中断
5 y" ]9 p/ j8 F0 ] - {
' a: a/ j5 Y$ l4 c. r - USART_ReceiveData(USART3); //清除空闲中断标志位
$ f# i5 R Q& A" _ - 3 j5 I" Q7 ^5 f4 W% {
- if(DMA_GetCurrentMemoryTarget(DMA1_Stream1) == DMA_Memory_0) //获取当前目标内存是否为 DMA_Memory_0
$ P0 s v2 o/ a: i6 S/ r) R - {
# V2 L+ j$ x) n, e1 O; M - //重新设置DMA
0 `# a, o. o% y" S - DMA_Cmd(DMA1_Stream1, DISABLE);
6 G* |; s H" |3 H. W& n - this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量% Y6 U N9 \. {# ?+ b# L+ U
- ' @, G- W& c# f
- DMA_SetCurrDataCounter(DMA1_Stream1, 18); //重新设置数据量& Q/ K" w6 [% U6 x, J3 E8 S
-
2 W, g& _! j9 S- P, d H - DMA_Cmd(DMA1_Stream1, ENABLE);
0 n! O3 y6 ^& F; c1 L* [ - if(this_time_rx_len == 18) //接收成功18个字节长度
: B+ \2 P& \, r7 R' p - {
% _4 [' R$ \( l8 r3 x2 \0 c8 q - //处理遥控器数据; C( A. O' G* d. e2 Q
- RemoteDataProcess(sbus_rx_buffer[1]); //Memory_1
2 r7 \! _$ T+ E$ r( r9 C4 ~ - }
+ A+ F$ m$ }/ j# O# |( `0 N5 M - : c+ r' _9 R% b! c
- }
; k; D: D: n" x! ~% C' Q - else //获取当前目标内存是否为 DMA_Memory_1. g6 l W1 ~1 d$ G( O# E
- {' T9 |+ ?$ U" F( W2 W& K; Z
- //重新设置DMA
& g9 k% {! Z4 H - DMA_Cmd(DMA1_Stream1, DISABLE);3 j2 t. j$ m# N6 r @
- this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量7 a7 s' O9 m8 C- F
- ; Y7 {. l/ \1 z8 e/ ^2 o
- DMA_SetCurrDataCounter(DMA1_Stream1, 18); //重新设置数据量
* T1 ~. n# |. K7 Y# v - " A& M W: i% ]
- DMA_Cmd(DMA1_Stream1, ENABLE);& L4 {- b7 r T7 ^2 L
5 Z& G( w. l5 h6 H( e- if( this_time_rx_len== 18) //接收成功18个字节长度
7 O/ k; I9 @% T' M. O g2 l* u9 @ - {
! I- B0 I h# {, [" S+ s- l7 ^ - //处理遥控器数据* g2 u/ h) g; T& ^
- RemoteDataProcess(sbus_rx_buffer[0]); //Memory_0' K( Z& ~# V% v, h3 ~
- }% J; F5 U4 t6 k6 c6 e, s
- + i) y! O& ~9 ^6 s3 v8 [ |. g0 z
- }
' @1 @9 \' Y) v8 S3 d- y - }" N" |- x6 u) m4 N
复制代码 0 W- H) z1 r* O( l0 i
该逻辑在程序复位后,当一帧遥控器数据发送过来后,Memory0区域被填满18个字节,DMA自动指向Memory1,所以第一次进入中断处理函数时,获取内存指向的时候得到的指向是Memory1。
+ _) H/ F% C+ I$ J6 z然后判断剩余数据大小是否为18,如果是则说明上一帧数据接收正常,可以对数据进行处理,如果不为18,则说明上一帧数据接收长度不正常,这时候就不对数据进行处理,当下一帧数据发送过来后对数据进行检验,正常后才对数据进行处理。, {1 j( Y" i. Z& C0 {
5 w C# Q1 P/ }& E. b/ \8 h然后是第二种:; Z4 e9 Q9 F% X; E8 B- T' ~/ |
这里我们将BufferSize改为比一帧数据长度大的值(比18大),这样可以在一帧数据传输完成后不会因Counter值变0导致DMA指向下一内存区域。
8 K9 ~; U' Z( N- U# y' L
, p) ~* m! O! W2 F! i) X0 E* z- DMA_InitStruct.DMA_BufferSize = 36;
( f3 S1 V& k( g8 |1 [+ a
复制代码- uint16_t this_time_rx_len = 0; //当前剩余数据长度
3 N! s- e, ~$ I% _3 S4 ?
- ~* Y5 Y# [. S- if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) //判断是否为空闲中断
6 ~4 k4 h$ g" G6 q. s3 t - {
& l5 y% q: e6 M0 F8 ^+ { - USART_ReceiveData(USART3); //清除空闲中断标志位) \& v% U+ a8 r* i; A% N0 F
+ o8 q" H2 K' ?9 y! E- if(DMA_GetCurrentMemoryTarget(DMA1_Stream1) == DMA_Memory_0) //获取当前目标内存是否为 DMA_Memory_0
9 H! s9 X9 [) q4 t* E2 ], }! r - {
- j; G+ U! u1 M' [( ` - //重新设置DMA
4 C$ P5 [/ b; i& H/ H5 T4 V - DMA_Cmd(DMA1_Stream1, DISABLE);
2 ?7 l* M* X& _& m - this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量 接收18字节,则剩余数据量为36-18=18
9 |- P# F" Q2 p/ }/ i - + O! T6 P& o/ S, E6 o
- DMA_SetCurrDataCounter(DMA1_Stream1, 36); //重新设置数据量' Z5 y- Z. \ \
- DMA1_Stream1->CR |= (uint32_t)(DMA_SxCR_CT); //将DMA指向Memory1
, z: p1 ]# I( H+ f -
+ H& E* i Q0 Q" U, w7 o% Q% G; e8 H - DMA_Cmd(DMA1_Stream1, ENABLE);
1 Z' b# z* x: P, H4 W - if(this_time_rx_len == 18) //接收成功18个字节长度/ b: i: Q5 B7 G7 V* M/ e
- {( H {# Z3 [- x% E0 w& D& K, A
- //处理遥控器数据
! q' q8 z( G! X* v. {8 L6 ~ - RemoteDataProcess(sbus_rx_buffer[0]); //Memory_0
2 a# c+ |1 Z9 N- g9 H% B - }2 K9 n3 {3 G" }! D
- ) A) @7 p1 |5 y/ }8 Q$ s9 j* z
- }4 J2 ]; p4 }4 y, c' y7 c
- else //获取当前目标内存是否为 DMA_Memory_1% M2 ~. L! A9 F# ]
- {
1 a9 {3 ]3 x3 n8 j - //重新设置DMA
, y' ~/ H# T! ^8 P, f - DMA_Cmd(DMA1_Stream1, DISABLE);
`9 F. H, i# M* u - this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量 接收18字节,则剩余数据量为36-18=18
' C; q2 i# N6 y' O& ?. d - : o1 I& ^+ W* s5 m7 u
- DMA_SetCurrDataCounter(DMA1_Stream1, 36); //重新设置数据量; }# N( B& o8 }3 g" I0 H
- DMA1_Stream1->CR &= ~(uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0
/ |) y& \- n' T
& u# A7 V# G) R5 q- DMA_Cmd(DMA1_Stream1, ENABLE);
3 l' }" d3 D3 u
4 }2 J' A5 G$ f1 I1 V" g6 H' S- if( this_time_rx_len == 18) //接收成功18个字节长度* G, o& ^: w J% C6 W; E3 b
- {; `2 H$ W4 s0 e: r# o
- //处理遥控器数据1 j, S8 P- \ F4 `: M# Z
- RemoteDataProcess(sbus_rx_buffer[1]); //Memory_1
( l- p4 w/ P) D5 O4 c; G0 m; l - }
" \ ]8 Y* B @
. V* J. }+ T5 Q5 |: R- }
p( m% D( t+ h. g - }* R- ~( U5 d2 `; }) o
复制代码 ! F- \7 r- R/ l: g
两种DMA双缓冲使用方法的区别
% d O M1 s; K/ `2 L, ?; h% S ]第二种方法和第一种方法的区别大同小异,这种方法由于将BufferSize设为比18大的值,也就是当第一帧数据传输完成后,Counter值不会自动填满,且内存区域还是指向Memory0,然后我们将剩余数据量保存下来,再将Counter值填满,接着把DMA指向Memory1,最后通过判断剩余数据量来决定是否对数据进行处理。4 Y: E; _* _* y8 Z! O- W
: n- F, |. i" x7 z! R7 A$ `; T可以看到,在第一种方法下,我们并没有使用 }) }. |( |: E" g! _% O
# Z, A u( G% q2 ]7 X% b2 O- DMA1_Stream1->CR &= ~(uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0
4 n3 ?. Z, z6 x" J2 d, p$ T
复制代码- DMA1_Stream1->CR |= (uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0
复制代码
+ E8 w/ u% V- o3 s" U2 c6 J$ Y6 ]) {, g! a p' }0 o9 A6 K
这是因为当传输数据量和BufferSize大小一致时,完整接收完一帧数据后,DMA会自动指在向下一内存地址,因此在第二种方法中的手动改变内存指向就由DMA自动处理了。# V* @( W3 |! G$ g5 m: v" Y
————————————————1 u T: A& B }* H9 O; s
版权声明:水木皆Ming9 H# q( P |' l! j9 G- e: {2 @
% w+ ?8 I9 D" r% |+ s) e! A8 J* t2 M1 ~5 X
|
请问一下,使用DMA双缓冲区进行数据的发送的时候,出现了重复发送数据的情况,请问该怎么解决呢?感觉主要是剩余文件数量达不到BUFFER_SIZE要求,带来的重复发送数据。