DMA双缓冲是什么?
! z8 F7 [7 B2 }$ z% T+ t/ \" N7 @在使用STM32的DMA时我们通常使用的是普通DMA传输,但STM32自带了一个双缓冲模式设置,这个模式有什么优点呢?5 x" W d5 h8 C3 a0 J
接下来我会在下面的介绍里详细说明:3 _$ K* C5 F+ B" T
I- e" ~" a9 T- Z
DMA全称Direct Memory Access,即直接内存访问。% ^7 W5 S& X9 e) i
普通DMA的使用是在DMA的数据流中进行的,设置好DMA的起点和终点以及传输的数据量即可开启DMA传输。
j2 P0 q' {% R% }* q! F" H! e. w/ |' b+ o( @( J8 x( I
7 C+ s2 y- v/ D2 l5 _! F
9 x2 \4 }0 x. T! q" Y3 }& R
DMA开启后就开始从起点将数据搬运至终点,每搬一次,传输数据量大小就减1,当传输数据量为0时,DMA停止搬运。
+ Y! l- j+ S8 F8 C6 Y E& H' _$ m% q5 s3 C% ?0 D( X0 N
DMA的传输模式如果是普通模式,则当传输数据量为0时,需要程序中手动将传输数据量重新设置满才能开启下一次的DMA数据传输。
2 b; w, e+ V; C4 H8 x" O5 ], lDMA的传输模式如果是循环模式,则当传输数据量为0时,会自动将传输数据量设置为满的,这样数据就能不断的传输。0 C& i0 E% O7 I; m7 d5 R1 ^1 ^
0 g0 t1 K4 [! P: F
% }; ^' \/ ^) |! F# }' b) D9 x: g/ O7 u! ]' r; ~' T
那么,双缓冲是什么意思呢?
7 r+ b+ Q0 w- y+ m5 o: }我们知道,普通DMA的目标数据储存区域只有一个,也就是如果当数据存满后,新的数据又传输过来了,那么旧的数据会被新的数据覆盖。(这就是普通DMA的缺点)
- _6 o+ t) _! k" N, L8 p% |( e4 g$ H$ b4 T0 f2 Y
8 h+ F, `, q& t6 c" n. ^- R; A
' Q0 M* I2 [# j' A
) p0 P( U# a. W* x, ~) o2 k而双缓冲模式下,我们DMA的目标数据储存区域有两个,也就是双缓冲,
k- z3 ~, n" x# u `6 u# i当一次完整的数据传输结束后(即Counter值从初始值变为0),会自动指向另一个内存区域。3 L" F5 ~- d# g; I( h. A
/ r8 B0 @6 p( l% K4 c. X
( T* U, T# `2 ~/ a) ^
+ g/ n7 q4 a% X, q如何使用DMA双缓存?
: h" @+ t& t) m3 c7 U) |' F$ S那么我们在程序里怎么使用双缓冲呢?4 |$ m# C: t2 X, Q7 l% F8 a) b
这里我们有两种方案使用双缓冲,一种是利用双缓冲自动跳转内存区域,一种是主动跳转内存区域。" j/ x) [ ?* _) D2 M+ [
+ u! S5 w) t! L" X s这里我们使用DMA的传输模式是循环模式
( c) J- J; t+ h5 w ?* A
3 A, @! X# E7 _4 I9 w2 ]; q$ Y- DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;) C2 d4 N' x a( O% g- |. T
复制代码 9 ]' l. P T+ h0 D; r) {& o
首先是第一种:
6 v* w( s( A$ H8 g' H- }4 V( o, a这里我们以大疆RoboMaster的遥控器使用为示例:" g& C# T3 Q0 { h: o- E2 q
首先我们定义好双缓冲使用的两个内存区域
9 Z B* l3 `) |: g9 c) ^- uint8_t sbus_rx_buffer[2][18u]; //double sbus rx buffer to save data
' J/ C1 Q+ j9 j" o- R
复制代码
5 V2 x) a! `3 j) y) W这里我们设置长度为18,也就是一帧遥控器数据的长度。
3 T z$ p8 }( r) T5 @& V- ^+ h5 y* ]# e' Q7 ?+ g+ m
- DMA_InitStruct.DMA_BufferSize = 18;
& B% y2 ^- A- Z, p5 q# M
复制代码 5 w* n) Y0 s% i8 b/ u
然后配置好双缓冲的两个内存区域地址 m$ [' Y& [& V$ A
" G! n3 x; P; p% @% m" p
- DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)&sbus_rx_buffer[0][0];
5 M" w) [6 |! \% E+ u$ f/ R( M# ^3 Q
复制代码
( h$ s% z. J" c' _这里配置内存区域2的地址,并把当前DMA指向内存区域1的地址- k s# J$ x; E
6 m* F8 E q* K* p! p- x- DMA_DoubleBufferModeConfig(DMA1_Stream1,(uint32_t)&sbus_rx_buffer[1][0],DMA_Memory_0); //first used memory configuration
' h6 E" y2 T |$ N4 S
复制代码 * {3 T3 N8 h* Y" x6 L4 @3 B
启动DMA双缓冲
& W4 x9 S% |8 n0 H& a9 z+ X
9 d6 v/ l$ f' m. i" K- \. [- DMA_DoubleBufferModeCmd(DMA1_Stream1, ENABLE);
Z9 ~" ?8 o9 V" u4 R
复制代码 0 D0 C$ [4 S$ t- K; N8 F% q
我们使用DMA时利用串口的空闲中断接收一帧一帧的数据。7 H. W2 J$ N9 O7 L! d
在串口中断处理函数中对数据进行处理
. u* Q( I6 O ]9 t! N! f9 r: r
) ~6 C0 X1 h# D/ O) y' K4 ]6 r-
+ L! p+ y% C, O- d; S1 z1 b9 J - uint16_t this_time_rx_len = 0; //当前剩余数据长度5 ^0 [( _( m- R8 u
6 x6 v8 h' ~! j7 e; {! l- if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) //判断是否为空闲中断
. E7 i7 g: p( H, l# |5 P: z. ^ - {
8 e/ |) n$ O; ^ - USART_ReceiveData(USART3); //清除空闲中断标志位* e$ J9 c9 y5 a2 K
- 5 I( M0 {4 N' N# r
- if(DMA_GetCurrentMemoryTarget(DMA1_Stream1) == DMA_Memory_0) //获取当前目标内存是否为 DMA_Memory_0. }4 }' O( L9 h8 ]1 L
- {, x! b! R4 D' b( S6 I9 V
- //重新设置DMA; H, J0 I( D: B, {
- DMA_Cmd(DMA1_Stream1, DISABLE);
6 z! K8 [+ t$ Q - this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量
6 F+ {) Y! l, ]" T8 m -
0 V5 @* p* D% l - DMA_SetCurrDataCounter(DMA1_Stream1, 18); //重新设置数据量& |9 \: ^- J3 U1 h( o. ^
- 0 Y8 N- }6 c' v. U+ @- ]
- DMA_Cmd(DMA1_Stream1, ENABLE);
# R7 O. P! U8 k - if(this_time_rx_len == 18) //接收成功18个字节长度
% ~4 ]* H3 Y$ l% X# a; Z+ X0 W - {
) {3 Y) ?/ Z% i# p7 w% _. V - //处理遥控器数据
2 g2 ~" L+ ]5 y' ]5 X: o& `5 _ - RemoteDataProcess(sbus_rx_buffer[1]); //Memory_1
2 y1 P( V: w* a7 V3 K7 ` - }' Q% @: T) F$ m% U
- 8 x$ E: |5 N- c7 D8 \
- }6 g% w( N: H2 i6 `
- else //获取当前目标内存是否为 DMA_Memory_1
- H: M. O- N/ |( v" e; U - {
: N7 \$ H! O+ ^# L/ t# f* w - //重新设置DMA4 U- b; i9 n! G0 D. Z' b. S
- DMA_Cmd(DMA1_Stream1, DISABLE);
! x- @- M6 C1 S, U( F' A7 _8 |$ x - this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量
1 i j1 b( O, {" k - 9 M" \# @- u& B. N
- DMA_SetCurrDataCounter(DMA1_Stream1, 18); //重新设置数据量4 y+ t# J8 u4 E, N$ ?
- + C& Z/ u0 F) y; ~1 b5 p
- DMA_Cmd(DMA1_Stream1, ENABLE);* d2 M; W9 s# t# C9 L
, P3 p7 r* |1 V$ {$ L( ^- if( this_time_rx_len== 18) //接收成功18个字节长度
1 N# A' W9 m. b) X - {
9 _$ L U. P: L7 f; L8 a1 K - //处理遥控器数据& Y( w7 g' R( t; r( |
- RemoteDataProcess(sbus_rx_buffer[0]); //Memory_0: ~( Z* A6 ]& a
- }
& f8 x4 ~* l3 H5 s/ R
1 e5 F1 e$ x5 Q. c% @* L- }! G! f/ G' {% R1 s4 s. Z9 z" q
- }- P& h$ o: ?$ x# k7 P3 ^: `; b
复制代码 5 X# x6 `+ K0 d3 p8 S. I) Y) m
该逻辑在程序复位后,当一帧遥控器数据发送过来后,Memory0区域被填满18个字节,DMA自动指向Memory1,所以第一次进入中断处理函数时,获取内存指向的时候得到的指向是Memory1。
4 D2 r+ I! \( e$ D/ ?+ q然后判断剩余数据大小是否为18,如果是则说明上一帧数据接收正常,可以对数据进行处理,如果不为18,则说明上一帧数据接收长度不正常,这时候就不对数据进行处理,当下一帧数据发送过来后对数据进行检验,正常后才对数据进行处理。
% k: E3 z- V6 ]& h5 f
6 p* Z! {' A3 W) q' A然后是第二种:7 j* }9 t) V* N; o0 s3 K! Z
这里我们将BufferSize改为比一帧数据长度大的值(比18大),这样可以在一帧数据传输完成后不会因Counter值变0导致DMA指向下一内存区域。; k2 F0 M, f# C; W7 ^
) W& Y; x( _! K. C) _- DMA_InitStruct.DMA_BufferSize = 36;
6 E+ U& H! w' _: K, ^3 W) E; x
复制代码- uint16_t this_time_rx_len = 0; //当前剩余数据长度% c u5 C! N) w. J& L) [5 _2 `2 H& J0 N
^# g! E5 Z- K# ~- if (USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) //判断是否为空闲中断& [" B* x8 ^/ ~2 [5 x+ l* P& x u
- { z1 i* F1 d0 Q& K4 y1 Y5 s' g
- USART_ReceiveData(USART3); //清除空闲中断标志位, c3 `- Y' k3 V0 d& {
$ Q; Z$ o+ O0 s3 h' o- if(DMA_GetCurrentMemoryTarget(DMA1_Stream1) == DMA_Memory_0) //获取当前目标内存是否为 DMA_Memory_09 j- \9 `1 B7 i/ n
- {
+ l* i2 x% a$ F8 V8 q/ ~6 A - //重新设置DMA: U2 Y: J& D! o
- DMA_Cmd(DMA1_Stream1, DISABLE);# T/ e" ^6 o t3 W" t- m/ U
- this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量 接收18字节,则剩余数据量为36-18=18) X, z' y2 {/ p7 g- }' A; J6 N
- & A2 L. c! S1 k. V0 L/ y/ ]5 ~, j) O
- DMA_SetCurrDataCounter(DMA1_Stream1, 36); //重新设置数据量
7 J2 L( Z4 u! W. h. \0 L - DMA1_Stream1->CR |= (uint32_t)(DMA_SxCR_CT); //将DMA指向Memory1
2 a& n* E5 \' X -
/ a" h. L9 e: R' d+ x' f. i, b) p - DMA_Cmd(DMA1_Stream1, ENABLE);* {. S/ r. M/ p& g4 V
- if(this_time_rx_len == 18) //接收成功18个字节长度, Z# g8 @/ B. G9 m8 q# ]
- {
$ F( i g+ ^- ~% Z# H% B, | - //处理遥控器数据
( C, L- s4 p! S s0 l$ x" X - RemoteDataProcess(sbus_rx_buffer[0]); //Memory_0( g8 p+ r$ i# f7 o9 C
- }
$ v& d' o; y1 X) n
& A% p7 _' m9 p1 G# x+ I/ j- }1 G) s7 L9 }( h3 {. l6 K- }" h6 e8 q
- else //获取当前目标内存是否为 DMA_Memory_1
$ j# k8 s7 o7 \) X0 h - {
& j" p/ _% f+ H- f4 _: ^ - //重新设置DMA) L& \& c9 E3 B( m1 G0 ^; c
- DMA_Cmd(DMA1_Stream1, DISABLE);
5 n: u7 w9 [) h e - this_time_rx_len = DMA_GetCurrDataCounter(DMA1_Stream1); //获取当前剩余数据量 接收18字节,则剩余数据量为36-18=187 F& s; ?1 A5 f0 i; A, K0 q
- # U- {# W* C$ A$ n- u
- DMA_SetCurrDataCounter(DMA1_Stream1, 36); //重新设置数据量
1 W b7 U8 ~5 m+ g - DMA1_Stream1->CR &= ~(uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0# ^1 {* b0 z8 U% z2 ?* @) T* I
, p1 {) U t- j7 G- K, {! R8 S- DMA_Cmd(DMA1_Stream1, ENABLE);
/ z( `+ y- o9 _9 g6 E) X9 b2 b! ]
& _: z0 ?+ u/ E( Q- if( this_time_rx_len == 18) //接收成功18个字节长度
4 t( O0 J* ?$ e" ~) y+ t; A8 c. A: B - {
; H/ D1 L( h1 T; O8 t - //处理遥控器数据4 l) U+ ^3 g' y2 l& F0 x8 b
- RemoteDataProcess(sbus_rx_buffer[1]); //Memory_1' O7 y8 e4 j# O B4 c, r, C
- }
' t2 X, @. q! b! }* Y9 h - / B' K/ S u) L2 v% @+ D
- }. \( z8 f! B/ M. P/ x8 r
- }
! t. ~; u! H- h8 D* D1 W6 r G
复制代码
0 X- |$ R; h$ j7 c% m两种DMA双缓冲使用方法的区别
6 P( A5 t L: w% r/ q( j/ Z0 W第二种方法和第一种方法的区别大同小异,这种方法由于将BufferSize设为比18大的值,也就是当第一帧数据传输完成后,Counter值不会自动填满,且内存区域还是指向Memory0,然后我们将剩余数据量保存下来,再将Counter值填满,接着把DMA指向Memory1,最后通过判断剩余数据量来决定是否对数据进行处理。4 o1 S% ~' t9 A n& z9 i$ L' a7 N
& d7 @5 q4 m% o: F& Q; D: n9 X可以看到,在第一种方法下,我们并没有使用# x% g J8 S* n
1 H) ]# m: D0 M( z5 B' H' c
- DMA1_Stream1->CR &= ~(uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0' N3 k' E) W% [3 _+ U# O+ }% I$ S
复制代码- DMA1_Stream1->CR |= (uint32_t)(DMA_SxCR_CT); 将DMA指向Memory0
复制代码 7 f* T) G) \, @, g: N; V
! w5 v8 O' K3 y Q
这是因为当传输数据量和BufferSize大小一致时,完整接收完一帧数据后,DMA会自动指在向下一内存地址,因此在第二种方法中的手动改变内存指向就由DMA自动处理了。' Y% J, O! B; k h7 q- A
————————————————$ n2 ]1 Z1 S+ F8 W0 S t9 M3 P
版权声明:水木皆Ming
! l' \, r' [( K. |( p3 z5 }; Z: P/ h8 T ^
C9 {4 C8 Z2 K# j
|
请问一下,使用DMA双缓冲区进行数据的发送的时候,出现了重复发送数据的情况,请问该怎么解决呢?感觉主要是剩余文件数量达不到BUFFER_SIZE要求,带来的重复发送数据。