项目背景& z6 S* @* Q8 e1 {: t1 w$ D
笔者在进行不少项目开发时,都遇到了需要多通道多次采样的需求。由于STM32片上12位ADC的精度不少很高,通常需要对每个通道多次采样,然后计算平均值作为采样的结果。如果采用常规的读取ADC数值并计算平均值的方法,会占用大量CPU时间,极大地增加了MCU的负担(如6个通道采样20个数据计算平均值,需要采样120次才能刷新一次数据)。而采用DMA传输可以使ADC采样和数据刷新放在“后台”自动进行,可以节省大量的单片机运行时间。+ r! h$ y) A4 [& ^1 u% {
. [% v% z" R& w5 n m& E* Z _" @
笔者将以STM32F103为例,简单介绍一下采用DMA传输使用ADC的一般方法。3 K2 x2 P/ D \3 ^
% f5 d8 H# V9 `( W2 X. |9 y" P$ C7 R% YDMA简介" X& q: c- \1 |
DMA(Direct Memory Access)——直接存储器存取,是单片机的一个外设,它的主要功能是用来搬数据,但是不需要占用 CPU,即在传输数据的时候,CPU 可以干其他的事情,好像是多线程一样。数据传输支持从外设到存储器或者存储器到存储器,这里的存储器可以是 SRAM 或者是 FLASH。DMA 控制器包含了 DMA1 和 DMA2,其中 DMA1 有 7 个通道,DMA2 有 5 个通道,这里的通道可以理解为传输数据的一种管道。要注意的是 DMA2 只存在于大容量的单片机中。下图所示是STM32F103的DMA结构框图:& C' C1 u0 v. G. p* q
# V! Y# W: ?) k7 ~/ R8 H
' N& U4 y9 Q+ z2 O$ E" EDMA 控制器独立于内核,属于一个单独的外设,结构比较简单。从应用的角度来说,它主要有以下三点值得注意的地方:" {' O$ Z* C, j' s) I
, d) @, Q6 D' F+ F& ^. U7 a: i
DMA请求
$ ?+ ` @0 s) Z如果外设要想通过 DMA 来传输数据,必须先给 DMA控制器发送 DMA请求,DMA收到请求信号之后,控制器会给外设一个应答信号,当外设应答后且 DMA 控制器收到应答信号之后,就会启动 DMA 的传输,直到传输完毕。1 I& d, u8 H5 w5 V: b, E
DMA 有 DMA1 和 DMA2 两个控制器,DMA1 有 7 个通道,DMA2 有 5 个通道,不同的 DMA 控制器的通道对应着不同的外设请求,这决定了我们在软件编程上该怎么设置。
. g# K) s8 q* t6 a
. {9 R$ q8 }/ IDMA通道* M$ p) Z5 c- P9 \
DMA 具有 12 个独立可编程的通道,其中 DMA1 有 7 个通道,DMA2 有 5 个通道,每
$ M: }3 t0 y6 a4 T/ V个通道对应不同的外设的 DMA 请求。虽然每个通道可以接收多个外设的请求,但是同一) h% {: I# j2 K3 F
时间只能接收一个,不能同时接收多个。0 \" f( o- r+ H2 l* h, K4 G
% Q1 z: J" d3 p$ i. u下图所示为DMA1请求映射表:
! w/ T( O e/ s* g9 k# @" \# n8 j# ^1 U4 Z _- k
& z6 g/ X2 a* d L8 q1 J
3 G5 y( C y& M1 f2 }6 |下图所示为DMA2请求映射表:5 q6 N9 D4 T* M# j
% O( d3 ~# @2 g( ` N. {7 Z+ I
" E/ W( S4 V7 ^( m7 k. }
4 s- I8 t3 H7 D6 k, aDMA仲裁
' h {1 \& e. N/ b当发生多个 DMA 通道请求时,就意味着有先后响应处理的顺序问题,这个就由仲裁器也管理。仲裁器管理 DMA 通道请求分为两个阶段。第一阶段属于软件阶段,可以在DMA_CCRx 寄存器中设置,有 4 个等级:非常高、高、中和低四个优先级。第二阶段属于硬件阶段,如果两个或以上的 DMA 通道请求设置的优先级一样,则他们优先级取决于通道编号,编号越低优先权高,比如通道 0 高于通道 1。在大容量产品和互联型产品中,DMA1 控制器拥有高于 DMA2 控制器的优先级。
5 w! i/ ^9 @: E9 A4 T
4 z. g" [: B2 s& X5 K* v+ k3 ~DMA配置+ @2 a6 ~7 t% P
使用 DMA,最核心就是配置要传输的数据,包括数据从哪里来,要到哪里去,传输的. A& H2 Y) @7 j6 p! z( D
数据的单位是什么,要传多少数据,是一次传输还是循环传输等等。
" Z$ I6 T* l- M5 D
8 y* i5 `1 y# r! e/ }& L从哪里来到哪里去; E7 w; T5 x/ ]7 p" U
DMA 传输数据的方向有三个:从外设到存储器,从存储器到外设,从存储器到存储器。
F. n% X9 C6 P0 h, M3 X# [4 x: j+ P. m" b
外设到存储器
4 B) m$ @- i3 V/ b% a( f当我们使用从外设到存储器传输时,以 ADC 采集为例。DMA 外设寄存器的地址对应的就是 ADC 数据寄存器的地址,DMA 存储器的地址就是我们自定义的变量(用来接收存储 AD 采集的数据)的地址。方向我们设置外设为源地址。( O* J" ^& m7 H5 t$ a5 P
& S& ~9 \- l! Y; M: C e2 C
存储器到外设
. k& w! N8 e) O) _当我们使用从存储器到外设传输时,以串口向电脑端发送数据为例。DMA 外设寄存器的地址对应的就是串口数据寄存器的地址,DMA 存储器的地址就是我们自定义的变量(相当于一个缓冲区,用来存储通过串口发送到电脑的数据)的地址。方向我们设置外设为目标地址。' Z w O0 ^) j- D7 m& S; R
3 }. E! h. n) D8 M3 T存储器到存储器" ]3 Z+ d7 o4 b) i
当我们使用从存储器到存储器传输时,以内部 FLASH 向内部 SRAM 复制数据为例。DMA 外设寄存器的地址对应的就是内部 FLASH(我们这里把内部 FALSH 当作一个外设来看)的地址,DMA 存储器的地址就是我们自定义的变量(相当于一个缓冲区,用来存储来自内部 FLASH 的数据)的地址。方向我们设置外设(即内部 FLASH)为源地址。 F8 ]& `3 C% V# V9 I9 h5 h
& @" T. x# l5 W8 q0 J, l
要传多少,单位是什么) F9 |# Z) t7 ~4 |/ A
当我们配置好数据要从哪里来到哪里去之后,我们还需要知道我们要传输的数据是多少,数据的单位是什么。以串口向电脑发送数据为例,我们可以一次性给电脑发送很多数据,具体多少由DMA_CNDTR 配置,这是一个 32 位的寄存器,一次最多只能传输 65535 个数据。要想数据传输正确,源和目标地址存储的数据宽度还必须一致,串口数据寄存器是 8 位的,所以我们定义的要发送的数据也必须是 8 位。外设的数据宽度由 DMA_CCRx 的PSIZE[1:0]配置,可以是8/16/32位,存储器的数据宽度由DMA_CCRx的MSIZE[1:0]配置,可以是8/16/32位。
R' Q$ b0 {$ w2 _; N. l
, t9 W" a- \+ y" q& u9 c! j/ P在 DMA 控制器的控制下,数据要想有条不紊的从一个地方搬到另外一个地方,还必须正确设置两边数据指针的增量模式。外设的地址指针由 DMA_CCRx 的 PINC 配置,存储器的地址指针由 MINC 配置。以串口向电脑发送数据为例,要发送的数据很多,每发送完一个,那么存储器的地址指针就应该加 1,而串口数据寄存器只有一个,那么外设的地址指针就固定不变。具体的数据指针的增量模式由实际情况决定。
O7 F& k8 R2 A: q: X1 e# j8 A; ]; s2 `: k- \
什么时候传输完成
' c$ \: H7 L9 V1 B0 s- C' U$ Y% _数据什么时候传输完成,我们可以通过查询标志位或者通过中断的方式来鉴别。每个DMA 通道在 DMA 传输过半、传输完成和传输错误时都会有相应的标志位,如果使能了该类型的中断后,则会产生中断。有关各个标志位的详细描述请参考 DMA 中断状态寄存器DMA_ISR 的详细描述。
3 Q$ v$ F+ X5 g! o6 @- K
1 N4 `9 I3 K# w3 m; B7 K% {1 @传输完成还分两种模式,是一次传输还是循环传输,一次传输很好理解,即是传输一次之后就停止,要想再传输的话,必须关断 DMA 使能后再重新配置后才能继续传输。循环传输则是一次传输完成之后又恢复第一次传输时的配置循环传输,不断的重复。具体的由 DMA_CCRx 寄存器的 CIRC 循环模式位控制。2 x: m) G9 n6 h1 q; o1 o
+ z7 ]9 V" c' ^9 k0 d! n1 a代码示例
' b7 T0 E2 A2 w在正确配置后,DMA传输与ADC采样会启动。笔者的代码中使能了6个ADC通道进行采样,存储深度为20组。由于设置了循环传输模式,二维数组 m) Z8 N7 m5 j* C, a$ B
ADC_DMA_Value[ADC_DMA_CHANNEL_DEEPTH][ADC_DMA_CHANNEL_NUM]
# a: o( s3 ?6 ~& \9 r5 ?/ A; c1 H+ p中的数据会被不断的刷新。
$ H% P; o; p+ e* W4 k/ G9 _7 I5 p
通过函数uint16_t ADC_GetData_DMA(uint8_t channel)可以读取某个通道的采样数值。
8 K H/ i2 m! ]3 D通过函数uint16_t ADC_GetAverageData_DMA(uint8_t channel)可以读取以深度20为单位计算出来的平均采样数值。" T1 n" j: h8 u- I3 f
1 |4 N# \ e& d1 N6 R' P. E! T配置GPIO; F3 o- G# A7 N _3 r5 x# `
- void GPIO_Config()
8 L0 w' h f# r( k - {
) ~+ Q' ~! T( t9 i3 h1 ? - GPIO_InitTypeDef GPIO_InitStructure;
# N) ]" Y% }; a0 t: u3 h - RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);3 J& Q% @3 e+ o
- RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);# R. S/ Q2 u- V8 y3 m
- # G Y; A$ e: G% x4 V
- GPIO_AFIODeInit();
0 J7 O' F$ K$ m2 q$ e( g) Z - /*JTAG-DP Disabled and SW-DP Enabled, use PB3, PB4 as GPIO*/# _* E0 C. l* P+ e6 a2 M X
- GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);2 H# o6 H1 \, R* P
: e* c( g: Z9 \" |6 f2 p' h- ///ADC GPIO - PA3, PA4, PA5, PA6/ D2 [7 d( x! N# K+ l
- GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6;
/ h M, N, @) o$ | - GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
& T3 R- o* F, w f- E; g6 h - GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;7 X( p$ Q# b1 B- C: O4 e$ \( T
- 9 V; a( h0 z* ` [& K4 k/ G
- GPIO_Init(GPIOA, &GPIO_InitStructure);) u# h; g3 W4 v% H
- }
复制代码
0 a- U4 d. v1 G: Z3 L$ ]相关变量定义
# ?" M7 B% I q% N, @( [- #define ADC_DMA_CHANNEL_NUM 6* F6 m, Q* h+ j. h- P
- #define ADC_DMA_CHANNEL_DEEPTH 20" ]3 Z: s- J( \5 ~! }5 R# D
- #define ADC_DMA_BUFFER_SIZE (ADC_DMA_CHANNEL_NUM * ADC_DMA_CHANNEL_DEEPTH)
; d/ H. R& T: Q* ^' b% ^; b) c
+ h8 q% u4 O/ F+ \' {- volatile uint16_t ADC_Data[20];
) {# U9 {8 o2 }; H8 j - volatile uint16_t ADC_DMA_Value[ADC_DMA_CHANNEL_DEEPTH][ADC_DMA_CHANNEL_NUM];
复制代码
" q R x+ w# y7 {配置ADC a3 O8 s" K# j4 [! ?
- void ADC_Config()
0 U6 P' z V5 W/ U. w - {- @9 I5 O2 \$ T/ [* G& T$ I6 ?
- /**6 k' `* t' Z, H g9 ^
- * Configuration of ADC
N6 [% D4 a0 G# [ - */# B8 V; G# e: u2 k" E
- ADC_InitTypeDef ADC_InitStructure;1 d% h0 X4 j. p) }
- , e( J; T. z( P! @* d4 k
- RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //使能ADC1通道时钟
! G3 W# k8 \5 e8 v - RCC_ADCCLKConfig(RCC_PCLK2_Div6); //设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M4 P% k8 u V" J2 j+ h# f0 ^
- ADC_DeInit(ADC1); //复位ADC1
3 R( z: b" h" m1 M {7 Z& H+ D
# @: M/ T% X. t: V& d- ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
" ^, x! p+ x7 ` - ADC_InitStructure.ADC_ScanConvMode = ENABLE; //通道扫描( P- u( _8 X" Y
- ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //连续转换! r+ l( u( \! i9 x& f7 s9 U
- ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
% k5 h8 n8 G7 E - ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;. C$ B W2 s5 Z( e a+ W4 K
- ADC_InitStructure.ADC_NbrOfChannel = 6;
( I( i# n$ r" I8 r {0 O - ADC_Init(ADC1, &ADC_InitStructure);" U0 U+ d- U: H: d6 T0 P
- / M) X( L9 q" @( ? }
- ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5); //通道1转换结果保存到ADCConvertedValue[0~10][0]. n! E0 }" F+ z9 O
- ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 2, ADC_SampleTime_239Cycles5); //通道2转换结果保存到ADCConvertedValue[0~10][1]# _) N% Q0 M0 ?* u* W
- ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 3, ADC_SampleTime_239Cycles5); //通道3转换结果保存到ADCConvertedValue[0~10][2]
$ }0 ?8 p& V2 _3 Y$ z5 \ - ADC_RegularChannelConfig(ADC1, ADC_Channel_4, 4, ADC_SampleTime_239Cycles5); //通道4转换结果保存到ADCConvertedValue[0~10][3]0 V6 s# g6 g# s( A x8 M* c0 X
- ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 5, ADC_SampleTime_239Cycles5); //通道5转换结果保存到ADCConvertedValue[0~10][4]
8 A. c4 g! A% ~1 b- L' m - ADC_RegularChannelConfig(ADC1, ADC_Channel_6, 6, ADC_SampleTime_239Cycles5); //通道6转换结果保存到ADCConvertedValue[0~10][5]( p6 N: `6 o' z8 [7 K6 i8 u2 ]/ E
- 3 N. S a' a- L1 A! v6 v- d: d4 y
- ADC_DMACmd(ADC1, ENABLE); //开启ADC的DMA支持, x# _6 J/ S9 j7 d9 _
- ADC_Cmd(ADC1, ENABLE);
1 |& |4 ~- ~) j% Q7 ? - ADC_ResetCalibration(ADC1);! t- M" C+ ?% x* D
- while (ADC_GetResetCalibrationStatus(ADC1))$ J& m% Q' C' M/ W* w
- ;$ z' a; t# _3 @2 v* f: @
- ADC_StartCalibration(ADC1);
$ K) z, g( u9 T2 y - while (ADC_GetCalibrationStatus(ADC1))
( Q" Y! X$ M; a7 z( a - ;2 l; z! ?9 J% i: L$ f. p7 C- }4 A
- ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的ADC1的软件转换启动功能
2 ?; R7 N# j, i( r - }
复制代码
% W1 @# {% B6 e* m4 D0 L+ v6 T, c配置DMA
$ V C) y1 i% o5 w1 ]# s- void DMA_Config()
' ^& `/ e. Q, \8 | - {% Y2 Q9 R* B! A
- /**
! T' @- T' z8 g T2 ? - * Configuration of DMA
$ `( y( S- J! ^0 G1 S - */( h2 L. A# I& D7 R4 D
- DMA_InitTypeDef DMA_InitStructure;
% E2 j* t" s$ ~+ p -
2 ]# l2 C, u! r' D - RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能时钟2 s0 ~6 n5 d I* N
- DMA_DeInit(DMA1_Channel1); //将通道一寄存器设为默认值$ }7 S4 A+ V ~+ p3 S7 t
- 0 m5 H5 m: K4 ?
- DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t) &(ADC1->DR); //该参数用以定义DMA外设基地址( S+ U! f$ s2 @
- DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t) &ADC_DMA_Value; //该参数用以定义DMA内存基地址(转换结果保存的地址)$ C+ u$ W' S1 L
- DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //该参数规定了外设是作为数据传输的目的地还是来源,此处是作为来源! ^* k8 v, f$ B/ p3 U6 m
- DMA_InitStructure.DMA_BufferSize = ADC_DMA_BUFFER_SIZE; //定义指定DMA通道的DMA缓存的大小,单位为数据单位。这里也就是ADC_DMA_Value的大小
$ T5 L# Q9 B7 P# x' c - DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //设定外设地址寄存器递增与否,此处设为不变 Disable) ?/ m7 L! h, A, S
- DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //用来设定内存地址寄存器递增与否,此处设为递增,Enable6 F# U8 n; G7 K. a2 z3 R" _
- DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //数据宽度为16位
' o: I* Q% R# F8 C* G% B5 c1 ~$ ~ - DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //数据宽度为16位
8 C) h1 w) b- E E2 N. P - DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //工作在循环缓存模式
1 s" v3 t" X# n R5 ~ - DMA_InitStructure.DMA_Priority = DMA_Priority_High; //DMA通道拥有高优先级 分别4个等级 低、中、高、非常高 Y' B$ z: _3 T- ^% G3 K# m, j
- DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //使能DMA通道的内存到内存传输
1 A; u; @3 @' t9 W0 W- _ - : S: \/ [# X% ?0 C# q6 \' C) J
- DMA_Init(DMA1_Channel1, &DMA_InitStructure); //根据DMA_InitStruct中指定的参数初始化DMA的通道+ Y1 ]( H# m5 a' b1 ~& G% K
- // DMA_ITConfig(DMA1_Channel1,DMA_IT_TC, ENABLE);
6 b- y) K" Y8 O% w! A X - DMA_Cmd(DMA1_Channel1, ENABLE); //启动DMA通道一
6 q4 |1 P' B' K9 r; y6 s2 u2 \ - }' L, [" F" P: c3 O3 }7 @! ]# t& L
复制代码
( F/ b$ T3 k: V' d/ V, d- G" N获取采样值, H G- u, \' o- h# K
- uint16_t ADC_GetData_DMA(uint8_t channel)8 @) w: I2 p" X4 n& {- a' n- d
- {
3 T! @' [' i0 P. k% B - return ADC_DMA_Value[0][channel];
: Y. [( x3 t, @& z" u# o4 N) ~! | - }% x5 f; v& L9 Q- H% i- ?) x
, r+ ^6 t% f/ Y" x u- uint16_t ADC_GetAverageData_DMA(uint8_t channel)
7 [+ H1 ~! u3 q$ C% S7 Z2 }" H4 z - {
/ c" b$ z& I( y A - uint16_t sum = 0;' Y9 X4 K$ g, E, R* Q5 K& K
- for (uint8_t j = 0; j < ADC_DMA_CHANNEL_DEEPTH; j++)
% i; I$ H: l7 m. B, K8 Q2 f i - {9 Y9 Z# G/ R8 r/ ?0 `! S4 Y
- sum += ADC_DMA_Value[j][channel];* V2 m3 t& m- p, v7 G
- }) }8 u5 ^9 l8 ]" \" d/ _
- return sum / ADC_DMA_CHANNEL_DEEPTH; //求平均值并转换成电压值
" G& I- F$ \0 Z7 l! h! F+ U - }
$ {: p# {! p% b# r- v. }
复制代码
; o7 C9 f8 e( @0 M
3 }! N# F3 H0 u8 P3 }) R% J8 x- e
|