在实际的开发中需要使用各式各样的传感器,这些传感器可以采集周围的环境信息,比如温度、湿度、气压、光照等,这些信息本质上是物理的模拟信号,计算机处理信息时对数字信号比较敏感,因此把模拟信号转换为数字信号是有必要的。ADC就起到了这样的作用,本节将对ADC功能原理以及ADC数据采集时涉及到的DMA技术进行详解。
7 ]) l# o/ _" R
& {. |7 S* S R2 `9 M2 Z. jADC的概念:Analog-to-Digital Converter的缩写。指模/数转换器或者模拟/数字转换器。是指将连续变量的模拟信号转换为离散的数字信号的器件。1 ^% c. s) V( r+ B1 c
: x3 K7 C" ~3 G+ dADC的作用:采集传感器的数据,测量输入电压,检查电池电量剩余,监测温湿度等。典型的模拟数字转换器将模拟信号转换为表示一定比例电压值的数字信号。3 K7 ?: O* M* D" P8 [5 p
6 O1 j+ a0 }9 U" M( ^; N% ]
0 b. ^% N$ N# r& N3 D4 v
* C3 R' F+ q& C. \ADC的性能指标:
( P* k o$ N$ ^/ E0 u+ C+ [& g ]# s, v) ]/ E" Q
量程:能测量的电压范围,一般来收量程越大越好
4 g- L O5 p, ?5 j; P$ ]( @8 w: V/ A; S
分辨率:ADC的分辨率通常以输出二进制数的位数表示,位数越多,分辨率越高,精度越大,外部微小的变化都可以被显示。一般来说分辨率越高,转化时间越长。0 E8 |/ J6 h1 A
) |2 l2 a& D3 m$ U: v转化时间:模拟输入电压在允许的最大变化范围内,从转换开始到获得稳定的数字量输出所需要的时间称为转换时间,希望时间越短越好。1 L% E% j$ G5 L
5 Z+ o _; Q P. e a2 \- NSTM32F4的ADC配置:
! j ^2 g4 Q, {2 q4 a& {# T6 Y- o9 e8 @ j$ w, C g
STM32F40X有3个ADC,每个可配置 12 位、10 位、8 位或 6 位分辨率$ {- w7 B4 A& ]1 g4 r
9 N, W5 x3 Q, ^8 h4 P每个ADC 有16个外部通道。另外还有两个内部 ADC源(直接测量内部温度和电压信号) 和 V BAT 通道(测量电池的剩余电量)挂在 ADC1上% z# R0 N" `% b, ~5 Z5 q
% g9 I9 {* K, Y1 I% G# ]; w* z' R这些通道的 A/D 转换可以单次、连续、扫描或间断模式执行。
; @$ e7 A8 L2 _. l7 c' w* Q+ m+ q- b1 P
ADC具有独立模式(单个ADC工作)、双重模式(两个ADC配合工作完成采样)和三重模式(三个ADC配合工作完成采样),对于不同 AD转换要求几乎都有合适的模式可选9 i) y8 C* `3 i! G! d" \
- N' o7 u& S6 L g4 W
ADC 的结果可以左对齐或右对齐方式存储在 16 位数据寄存器中。采集到的数据精度最大为12位,不够16位,因此,需要选择合适的存储方式。* y* h; r4 s; O6 b
( @. L! E" O7 z2 c" eADC功能框图分析:6 ?( _ l3 `! O
9 b" v. u5 ] ~2 R
& {* i7 A; @8 ]2 W" x
& N1 P8 J1 `7 h4 q) ^
. @ K: \4 B7 Y R0 Y
1、电压输入范围:ADC 输入范围为:V REF- ≤ V IN ≤ V REF+ 。由 V REF- 、V REF+ 、V DDA 、V SSA 、这四个外部引脚决定。我们在设计原理图的时候一般把 V SSA 和 V REF- 接地,把 V REF+ 和 V DDA 接 3.3V,得到ADC 的输入电压范围为:0~3.3V。如果我们想让输入的电压范围变宽,去到可以测试负电压或者更高的正电压,我们可以在外部加一个电压调理电路(线性的转换关系,如根据一定的转换关系将-10V~10V转换为0~3.3V),把需要转换的电压抬升或者降压到 0~3.3V,这样 ADC 就可以测量了
5 w: M- r: e8 B$ H6 w
5 s: G9 |9 A* y: m$ _9 l: F# C2、输入通道:每个ADC 有16个外部通道。特别是ADC1还有两个内部 ADC 源 和 V BAT 通道挂在 ADC1上,V BAT 通道也属于内部通道,两个内部 ADC 源没有对应外部管脚,直接测量内部信息。一个连接内部温度传感器,主要测量CPU以及芯片内部的一些关键电路的温度。另一个连接VREFINT,测量电源模块的电压值,V BAT 通道用来测量备用电池的电池容量。6 k. c9 ^" w: z
. T' w% d% X. b4 Q, [# I8 a1 }
, d' m6 o# [: T, e- \
- G2 D( X1 w [% P8 X1 M: s; f3、转换顺序: N! v/ q8 z3 T7 o6 n
/ M" L% v7 j) _ ]- U, [" ?a.规则通道组:顾名思意,规则通道就是很规矩的意思,我们平时一般使用的就是这个通道。相当于正常运行的程序,最多16个通道,在使用过程中按顺序进行转换,按顺序采集完一个通道进行转换后再采集下一个通道进行转换。规则通道和它的转换顺序在ADC_SQRx寄存器中选择,规则组转换的总数应写入ADC_SQR1寄存器的L[3:0]中。
) _, e, S; @4 D1 ]5 \0 S) l s" u5 T" |& Y& b4 r3 \
2 \ w' Z# ]$ |- R( {% ? K% d# t ~. Y# [. k8 k5 l
. c6 b4 M1 {3 G0 W! S
& `: ]9 H6 L9 R- z
% z# E- R/ C; J/ y- o8 G& d A
, ?- A, j& z3 Y( uADC像这样的规则序列寄存器有3个,每个规则序列寄存器都能设定第n次转换的具体通道,共有16个外部通道,最多设置16次。如上图,规则序列寄存器3(ADC_SQR3)可以设定第一次到第六次顺序转换的通道,规则序列寄存器2(ADC_SQR2)可以设定第七次到第十二次顺序转换的通道,规则序列寄存器1(ADC_SQR1)可以设定第十三次到第十六次顺序转换的通道,且ADC_SQR1寄存器的L[3:0]中(20位到23位)用来设定要顺序转换的通道个数。例:有如下转换顺序要求,有4个通道,第一次转换通道2,第二次转换通道3,第三次转换通道4,第四次转换通道1,则ADC_SQR1寄存器的L[3:0]的值位0100,ADC_SQR3中SQ1的值设为2,SQ2的值设为3,SQ3的值设为4,SQ4的值设为1。具体的操作在Cube MX中可以配置
7 \* X. }8 F# D! g! b7 h2 [
1 |% u; U/ R/ f# i$ @& Nb.注入通道组:注入,可以理解为插入,插队的意思,是一种不安分的通道。相当于中断。最多4个通道。把某一通道配置位注入通道时,它的优先级就比规则通道高,暂停规则通道组的工作,优先转换注入通道组采集到的信号。注入组和它的转换顺序在ADC_JSQR寄存器中选择。注入组里转化的总数应写入ADC_JSQR寄存器的L[1:0]中。该通道组的配置方法于规则通道组相同。5 U5 I! ~) s0 i' K) c
+ T y4 M. R h4 {1 a4、转换时间:ADC 输入时钟 ADC_CLK 由 PCLK2 经过分频产生,最大值是 36MHz,即最大工作时钟不能超过36MHz,典型值为30MHz。对于 STM32F407我们一般设置PCLK2=HCLK/2=84MHz,最少需要4分频。所以程序一般使用 4分频或者 6分频。2 R. L. V2 b0 o, N" m" ~$ Z3 ]' F
0 s. z# _. S4 T# o3 Y
# U% D+ k5 w& v: t) r m* O& f" u) w9 |6 x
1 Y( m" K. r: J4 ^
ADC 的总转换时间Tconv = 采样时间 + 12个周期(转换时间),采样时间最少不小于3个ADC_CLK,转换时间一般就是12个ADC_CLK,如果对时间没有太过苛刻的要求,应当把采样时间设置的稍长一些,这样得到的结果才会更加准确。最小采样时间: T =3 + 112= 15个周期=0.42us(ADC时钟=36MHz下得到)。2 m7 s8 n* ?: e8 G9 r7 |! F7 S' p
; |6 c5 W; E3 k9 @ M! G9 E
) c$ d6 `3 @9 b
( B0 f, G& t) l) i; h
5、数据寄存器:规则数据寄存器 ADC_DR) l3 g; f; h. g. B6 R0 r' T
& j' W/ c: X& X' P7 u+ C9 @ADC_DR只有一个,是一个32 位的寄存器,只有低 16 位有效并且只是用于独立模式存放转换完成数据。因为 ADC 的最大精度是 12 位,ADC_DR 是16 位有效,这样允许 ADC存放数据时候选择左对齐或者右对齐,具体是以哪一种方式存放,由 ADC_CR2的 11 位 ALIGN 设置。对于规则通道组来说,所有的规则通道都使用这一个数据寄存器,实际采样点时候,如果开启了多个通道,每转换完成一个通道要及时把数据从寄存器中取走。如果不及时取走,下一次转换完成的数据就会覆盖原数据。而对于注入通道组来说,每一个注入通道都对应一个数据寄存器。
5 H% L% F# l( r2 S/ f
1 F2 m4 C% j" u8 \* D9 `& l
4 Q9 e6 d; w$ Z' g0 D
3 _% z! y( _5 q3 k: O使用 DMA:由于规则通道组只有一个数据寄存器,因此,对于多个规则通道的转换,使用 DMA 非常有帮助。这样可以避免丢失在下一次写入之前还未被读出的 ADC_DR 寄存器中的数据。在使能 DMA 模式的情况下(ADC_CR2 寄存器中的 DMA 位置 1),每完成规则通道组中的一个通道转换后,都会生成一个 DMA 请求。
5 ]* A) e6 \" Y2 r: ?* q6 r) h2 S u$ {+ n/ ^. h3 d
6、中断, U* b# @/ C N/ Q
9 l4 Y1 ?: ]* f0 s! G/ F+ ya.转换结束中断:规则通道和注入通道的数据转换结束后,都可以产生中断。通道转换完成后,ADC 状态寄存器 ADC_SR的EOC位会置1,可以不断轮询此位来判断是否触发中断,也可以使能中断,完成后就根据此位的状态触发中断。此中断的使用是最多的,可以在此中断处理程序中获取我们想要采集的数据。
) Q! E% K( f- b; M9 n
* \5 X4 ^$ O% G. W vb.模拟看门狗中断:当被 ADC 转换的模拟电压低于低阈值或者高于高阈值时,就会产生中断
+ t! x0 ]# C6 l5 [% y+ i5 m1 {. X, `2 f6 \) E& y" f2 {6 H# W
c.溢出中断:如果发生 DMA传输数据丢失,会置位 ADC 状态寄存器 ADC_SR的 OVR位,如果同时使能了溢出中断,那在转换结束后会产生一个溢出中断。6 I2 V- \( S$ V! g- v
/ g& \" `: t% ~9 j) j/ B: F# `( k' zd.DMA 请求:规则和注入通道转换结束后,除了产生中断外,还可以产生 DMA请求,把转换好的数据直接存储在内存里面。/ _9 G3 e# _7 M* M$ s
+ {9 _. G# _" Z
7、触发源
1 h0 e( G! \0 Y- x
" v- G- S0 l1 x( ra.软件触发:ADC转换可以由ADC 控制寄存器 2: ADC_CR2的 ADON这个位来控制,写 1的时候开始转换,写 0 的时候停止转换。此方法使用最多4 x8 T( b3 r; I7 T0 B9 t
s1 d' C2 U5 k# h- _& m# b0 V ?
b.外部事件触发:触发包括内部定时器触发和外部 IO触发。触发源有很多,具体选择哪一种触发源,由 ADC控制寄存器ADC_CR2的 EXTSEL[2:0]和 JEXTSEL[2:0]位来控制
% {; O* V, ~! q# h' |+ u0 h, y! M
3 i9 h# ]) V( E2 X% O( I, k/ q+ S2 Z
ADC的工作模式, i/ n% T1 z# n5 v. {6 R
$ v# q' S) U5 Z7 t" T" ^单次转换模式:ADC每次工作只转换一次,至于这一次转换几个通道并不关心,如果想要再次转换,必须重新启动ADC。0 K/ [# H% }$ I4 \; `. K3 w
& Q! ~: I; R5 g
连续转换模式:在连续转换模式下,ADC结束一个转换后立即启动一个新的转换,直到用户想要停止为止。(仅适用于规则通道)。
8 Z6 Y8 D* X2 V& m1 W& H
+ W2 f, l5 `( l- c9 m1 [扫描模式:该模式主要针对多个通道进行转换。例如使用单次转换模式又开启扫描模式,从第1个通道开始扫描,扫描第n个通道结束后不再扫描;如果使用单次转换模式而不开启扫描模式,这多个通道很有可能只转换第1个通道;使用连续转换模式又开启扫描模式,从第1个通道开始扫描,扫描第n个通道结束后再从第1个通道开始扫描,循环往复。
( I" U( l7 l g7 V) v
. o8 Z. P/ ]% Y5 \- _不连续采样模式:有多个通道需要转换,此模式下一次只转换其中的几个,这个值n需要用户自己设定,假设有0、1、2、3、6、7、9、10这几个通道需要转换,n设置为3,则7 D( t* ^" J9 d a; I6 W: F' o3 D
9 T% A9 r* ]" j" n1 G A' C
O& M, k. w# e$ s
: f1 r: Z: p8 P& M" H* L" jSTM32ADC单通道采集实例9 _6 z2 s" @" u' X
0 W( A0 H, z! E# p. _) X实验:利用ADC采集FSM4中电位器的数据- m3 q* ~$ K& b3 t7 E7 X5 U
* H) s; }+ I+ l$ k1 ^说明:电位器本质上就是一个滑动变阻器,通过调节电阻就可以输出不同的电压值,电压值通过PC3进入ADC的输入通道。
P8 f" z" i7 c$ f4 p ]8 {6 B o
; N( O( Y) b$ Y8 _) y. R; H/ {* D& K
步骤:( z0 \. a: v' H9 ]8 T$ ~2 D7 q
) F" w: N/ G H& j: ^9 u# I
1.配置RCC,ADC依赖于时钟APB2$ n7 H) ~) @; e1 ^4 w- j8 d
( _" c* T; L7 n" a8 ]2.配置PC3为ADC1的通道13
& l. x- ^% o3 f! u8 c$ q X
9 Q9 [) r1 P ]5 B
$ V; @+ c2 z* r4 H; f7 ^% v
3 a- O1 N$ V* _0 \$ g2 _6 c* |/ u/ a' U; Z8 f# `0 f0 _* }
3.配置ADC1. q- D6 z/ w+ ` q
P W: O5 F# {5 R- n
e. v/ L/ J! D# M( I
, O$ U n& e. n: J! D% z+ E' X
$ w% \& U i; I, ^2 N \1 ], P8 e
& v# C1 y- ^3 x) N- e, d( r( l4.使能中断,转换完成触发中断
2 P0 e, M# Q! l) f
) x# b7 d% S! j( j8 C
2 Y3 }, _6 C, p `3 J' H
- u. _" |/ g9 M, H& e3 E5.编写代码
% C2 X6 n, c$ x' Q3 N1 c- <font face="Tahoma"><font size="3">//main.c </font></font>
3 V& U4 T/ I8 q2 Z" w6 t - <font face="Tahoma"><font size="3">& i5 u, B* e: ?
- </font></font>
. M1 A# ?1 E# C% D4 ^ - <font face="Tahoma"><font size="3">uint32_t adc_value = 0;//全局变量外部才可以使用 </font></font>
- m% c, E9 G) E: M* D( c - <font face="Tahoma"><font size="3">int main(){ </font></font># b: u. a; f8 | [: p! D
- <font face="Tahoma"><font size="3"> HAL_Init(); </font></font>; w3 E2 w `/ {3 E
- <font face="Tahoma"><font size="3"> SystemClock_Config(); </font></font>8 f6 S" @% d0 C! z
- <font face="Tahoma"><font size="3"> MX_GPIO_Init(); </font></font>
+ k4 F! g7 |% j0 @7 { - <font face="Tahoma"><font size="3"> MX_ADC1_Init();//ADC1初始化配置,仍然采用结构体元素赋值的形式 </font></font>* E$ |. B/ | A5 t
- <font face="Tahoma"><font size="3"> MX_USART1_UART_Init(); </font></font>) l% q" x" d' P0 V& f2 B2 e0 E. Z
- <font face="Tahoma"><font size="3"> while(){ </font></font>
" d- c/ a6 p1 c6 n6 R - <font face="Tahoma"><font size="3"> /***方法一:轮询采集ADC的值,也就是轮询判断EOC是否置位***/ </font></font>9 `) z4 J9 `6 ]. f- {5 q, |
- <font face="Tahoma"><font size="3"> //置位说明转换完成就可以读值 </font></font>& i7 s1 g* v6 W, t: @( h
- <font face="Tahoma"><font size="3"> </font></font>
/ a2 W+ V5 X% w( f& \! R0 }" g5 V - <font face="Tahoma"><font size="3"> //启动ADC1,软件触发方式,将对应的启动位置位 </font></font>
j }% f B* E2 O9 e) A - <font face="Tahoma"><font size="3"> HAL_ADC_Start(&hadc1); </font></font># g) y6 I6 e, `* o6 y* w S
- <font face="Tahoma"><font size="3"> //HAL_ADC_PollForConversion(&hadc1, 100)该函数内部循环判断 </font></font>
! ]' {* i0 N1 E! R) s - <font face="Tahoma"><font size="3"> //EOC是否置位,没有置位就一直等待,直到超时 </font></font>
/ p0 N) }4 e2 E4 T - <font face="Tahoma"><font size="3"> if( HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK ){ </font></font>' D8 @/ N3 V' V2 w: T9 m0 F. Y4 m
- <font face="Tahoma"><font size="3"> adc_value = HAL_ADC_GetValue(&hadc1);//读取数据寄存器里的值 </font></font># G$ ~, c3 S' _4 ?& {2 r+ p
- <font face="Tahoma"><font size="3"> printf("adc_value = %d\n",adc_value); </font></font>
6 N# n6 R! o: |, J7 Q! W! Q C - <font face="Tahoma"><font size="3"> } </font></font>
- O: _% b( Y' { - <font face="Tahoma"><font size="3"> HAL_Delay(1000);//每过1s钟重新启动ADC </font></font>
5 z) v/ i. S% z l. @9 B5 n A$ F% ? - <font face="Tahoma"><font size="3"> </font></font>4 k: e+ A1 [1 F- W3 L! Q
- <font face="Tahoma"><font size="3"> </font></font>
; k5 b# v0 X+ Y& x' J6 U& | - <font face="Tahoma"><font size="3"> /***方法二:转换完成后触发中断,在中断中接收数值***/ </font></font>
9 I' _7 X1 [" S M0 d- G* s5 h2 s - <font face="Tahoma"><font size="3"> HAL_ADC_Start_IT(&hadc1);//启动ADC1并使能中断 </font></font>
: @5 |7 Z/ T8 W- K$ y; ? - <font face="Tahoma"><font size="3"> HAL_Delay(1000);//每过1s钟重新启动ADC </font></font>' w) N8 S5 ?9 A( E
- <font face="Tahoma"><font size="3"> } </font></font>
% n# T! s' J& b5 |- p! ] - <font face="Tahoma"><font size="3">}</font></font>
复制代码- <font face="Tahoma"><font size="3">//adc.c </font></font>- F/ e1 h' c/ { ^* ~+ T
- <font face="Tahoma"><font size="3">//重写转换结束中断回调函数,这里对应方法二 </font></font>
% G7 d; K, m1 P: Y8 w1 s, ] g/ _ - <font face="Tahoma"><font size="3">% u+ L3 D% @, Z) a- y) B5 N& m1 S
- </font></font>
1 S$ ~: \2 j9 a - <font face="Tahoma"><font size="3">extern uint32_t adc_value;//全局变量 </font></font>
/ w; q: ?' X7 k) n - <font face="Tahoma"><font size="3">void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { </font></font>
9 l2 I: _ W% |6 ~ - <font face="Tahoma"><font size="3"> if(hadc->Instance == ADC1) { </font></font>7 H$ A, P$ p( N& R) r
- <font face="Tahoma"><font size="3"> adc_value = HAL_ADC_GetValue(&hadc1);//读取数据寄存器里的值 </font></font>
7 t: a; _. @$ c; l2 q0 X# e. e - <font face="Tahoma"><font size="3"> printf("adc_value = %d\n",adc_value); </font></font>" U* {4 K' M; X- P& q: V2 r
- <font face="Tahoma"><font size="3"> } </font></font>
* c9 {0 {7 `6 ~1 C2 Z K - <font face="Tahoma"><font size="3">}</font></font>
复制代码
% r; D0 w( c, Q" CSTM32 ADC多通道采集实例
: u" v! D' t5 Q1 `6 j# L
j6 w: F2 o9 w0 @: Q G, `实验要求:利用ADC采集电位器及STM32内部温度传感器的数据。1 j, W+ Y2 K6 _1 n4 H1 e
7 P2 x0 |% K% }. `( w: M: E# a
说明:采集内部温度传感器使用的是内部专用通道,选择通道16或者通道18均可采集内部温度,需要注意的是,实际中多路通道的采集一般使用DMA来进行数据的搬移,因为规则通道的数据寄存器只有一个,如果不及时取走数据,上一次采集的数据可能被覆盖,DMA的知识将在后期进行讲解,因此本节仍采用轮询的方式查询标志位,及时将数据取出。& ~3 U$ I- `2 G, d5 |: r- v# g$ m+ J
" d @ i! S; d% x
) w `3 [& \7 j1 o2 E+ @: W2 h" m
$ l: q# m* K4 ^
2 _; [9 p" S" L6 z
步骤:
) ~) `0 x% j2 E5 T9 { u2 R
: L4 [& B* y$ ?# X% J! G, {9 M1.配置时钟RCC
|$ H. q' `: k6 Y+ c
, g. D! x- J7 a2.配置PC3位ADC1的通道13,配置内部温度传感器通道.
2 b, ]5 D+ g1 g8 X$ K* t3 e6 G& r3 N% s. }- s# l
% L. g) F% Z$ p; h4 {1 v* C0 G- k0 l. P
3.配置ADC
- [7 k& D& P J. d9 P
5 E, ]4 ^7 j9 \' I% M. _
|! {% a0 D; q& x- v# s
. j3 A) S; N- b; p6 J5 c' ~
! V$ `7 ^' g/ b8 ~4.编写代码5 v6 \; x! r9 K+ l: }0 s6 C, G
- <font face="Tahoma"><font size="3">//mian.c </font></font>8 G; {2 S1 ^0 {$ `% G8 B) J
- <font face="Tahoma"><font size="3">5 b. ~% w9 y2 L0 `0 k. p" ?
- </font></font>
2 v2 I" Z0 Q ?# ^ - <font face="Tahoma"><font size="3">int main(){ </font></font>5 K# k* v# c# v, h7 K: Y/ [
- <font face="Tahoma"><font size="3"> uint32_t adc_value = 0; </font></font>
, I4 @9 r& S$ X8 k5 C: O - <font face="Tahoma"><font size="3"> uint8_t i; </font></font>
5 U& H# U: Q' C K - <font face="Tahoma"><font size="3"> </font></font>
+ ?. d2 `8 i% U+ U: d& a8 l - <font face="Tahoma"><font size="3"> HAL_Init(); </font></font>
; Y4 H! J ^6 k/ O# O - <font face="Tahoma"><font size="3"> SystemClock_Config(); </font></font>
8 H# t9 U$ F# I2 e) W - <font face="Tahoma"><font size="3"> MX_GPIO_Init(); </font></font>
) z1 ~1 } M5 X; X& O - <font face="Tahoma"><font size="3"> MX_ADC1_Init(); </font></font>
0 e0 U( ]" p1 U5 V& \ - <font face="Tahoma"><font size="3"> MX_USART1_UART_Init(); </font></font>
% e" U0 @& t$ |8 \) x - <font face="Tahoma"><font size="3"> </font></font>
7 U3 V) N4 t, }9 }5 C+ q' O) T$ @+ ^ - <font face="Tahoma"><font size="3"> while(){ </font></font>
# s- X9 r7 r$ x, S' F6 c - <font face="Tahoma"><font size="3"> //启动ADC1,软件触发方式,将对应的启动位置位 </font></font>
3 S+ I- M2 D# d4 H - <font face="Tahoma"><font size="3"> HAL_ADC_Start(&hadc1); </font></font>
" p r7 u4 A9 {) _5 C9 `2 F8 K7 x' `4 R - <font face="Tahoma"><font size="3"> for(i=0;i<2;i++){ //使用了几个通道就应该轮询几次 </font></font>. S Q4 I5 K6 s1 ]
- <font face="Tahoma"><font size="3"> //首先采集外部通道电位器的数值,第二次采集内部温度传感器的值 </font></font>5 @4 X, J( J% ? [8 J; F" L. ?
- <font face="Tahoma"><font size="3"> //HAL_ADC_PollForConversion(&hadc1, 100)该函数内部循环判断 </font></font>
* n/ p( v) {( |2 Y& p - <font face="Tahoma"><font size="3"> //EOC是否置位,没有置位就一直等待,直到超时 </font></font>
2 c7 t J* o* P7 [ - <font face="Tahoma"><font size="3"> if( HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK ){ </font></font>' U1 r: w+ P$ n0 H3 r9 k7 T. c& v
- <font face="Tahoma"><font size="3"> adc_value = HAL_ADC_GetValue(&hadc1);//读取数据寄存器里的值 </font></font>2 _6 K* d' I" t* O
- <font face="Tahoma"><font size="3"> printf("adc_value = %d\n",adc_value); </font></font>/ K9 F" S8 y+ o/ u, v2 x
- <font face="Tahoma"><font size="3"> } </font></font> L7 ?( G2 M+ K$ z5 E
- <font face="Tahoma"><font size="3"> } </font></font># b, o& I, Y* A& L+ H9 [2 i4 c- `
- <font face="Tahoma"><font size="3"> HAL_Delay(1000);//每过1s钟重新启动ADC </font></font>
( b6 B4 a# m& y" [ - <font face="Tahoma"><font size="3"> } </font></font>( k$ l7 r4 L4 {& V6 }3 ^6 x- Q
- <font face="Tahoma"><font size="3">}</font></font>
, Q9 w) d% K4 T B+ i
复制代码
2 \7 b5 b) h' X' {STM32数据搬运工DMA专题讲解
. ]' b2 W' }- Y) j! X) X% p; t$ V8 z# G S0 U% U8 d+ v+ M& N7 Z
DMA专门用于数据搬运,在开发中经常会遇到外设与内存、内存与内存进行大量的数据交互的情况,这些情况一般都是由CPU进行控制的,如果数据交互非常频繁且数据量巨大,那么CPU就会被大量占用用于数据交互而无法完成其他工作,为了把CPU从这种单一繁琐的工作中释放出来,很多微控制器中都有专门用于数据交互的器件,就是DMA。
7 b, z: ~- O/ F* W6 O, H
, g$ q* t. @1 X8 C( vDMA,全称为:Direct Memory Access,即直接存储器访问。DMA 传输方式无需 CPU 直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为RAM 与 I/O 设备开辟一条直接传送数据的通路,无需CPU的参与,CPU将这条通路的控制权释放,转交由DMA控制器控制,能使 CPU 的效率大为提高。DMA控制器只用于数据交互,无其他功能。
! u& [, H3 W! z# s# X; O
3 m' k: R" X: v$ q
. }1 K5 V& F4 I2 {) O/ W
8 ^ \( v8 S1 L* BSTM32F4-DMA功能框图分析% Z! e; ]0 i/ }8 ^$ R
8 a; Q& Y7 I4 I
( j; I& x: j+ g: t" r' a
& Q* s- `' p, l①外设通道:STM32F4xx 系列资源丰富,具有两个 DMA 控制器,同时外设繁多,为实现正常传输,DMA需要通道选择控制。每个 DMA控制器具有 8个数据流,每个数据流对应 8个外设请求。
( o# g$ Q, e8 c2 f( f1 h3 X& q3 y3 n' X: G' k9 t L
外设通道选择要解决的主要问题是决定哪一个外设作为该数据流的源地址或者目标地址。宏观上一个数据流的所有通道可以同时工作,微观上通道异步前进进行工作,或者根据通道优先级进行工作/ w! n# C2 \) z7 z
8 x, s6 J: T5 V1 o% f; _" k2 F- A- i: g
4 P9 N0 j7 f, ^ J% @, X2 {; l
DMA1外设通道表如下:
0 ?; A4 j* D# B# }$ O& V9 T/ p( W; v7 ?; j; F: E4 V
$ ^* h& h' a1 M5 \
. |! h6 h, \5 j$ C
DMA2外设通道表如下:
. v/ T( Q1 X! w- E5 V: h7 ?
+ ?6 D( p* | v: ]* \7 B, h( L' C; R
S: _7 D, h, G
+ S" I+ W4 ^# E每个数据流都与一个 DMA 请求相关联,此 DMA 请求可以从 8 个可能的通道请求中选出。此选择由 DMA_SxCR 寄存器中的 CHSEL[2:0] 位控制,3位用来控制8个通道的选择。
5 }/ `2 {! V8 Z1 l. e
4 s. S( P5 m- u②数据流仲裁:一个 DMA控制器对应 8个数据流,数据流包含要传输数据的源地址、目标地址、数据长度等等信息。如果我们需要同时使用同一个DMA 控制器(DMA1 或 DMA2)多个外设请求时,那必然需要同时使用多个数据流,那究竟哪一个数据流具有优先传输的权利呢?这就需要仲裁器来管理判断2 I0 S6 E) |# `
9 Q" {, k4 I; I! o0 A每个数据流的优先级都可以通过配置 DMA_SxCR寄存器 PL[1:0]位,设置为非常高、高、中和低四个级别,由于8个数据流只有4个优先级,如果两个或以上数据流软件设置优先级一样,则他们优先级取决于数据流编号,编号越低越具有优先权。
0 n- A" q% _5 N& ]' p/ M7 X! [! A
2 f6 z) V: L$ T+ H( C/ b0 Z③数据FIFO:每个数据流都有一个独立的 4 字 FIFO(先进先出存储器缓冲区), DMA传输具有 FIFO模式和直接模式。
$ m! h. b/ P* H( l) t2 w. P0 c" Q3 Q! W( L c8 H/ Q
a.直接模式:每个 DMA 请求会立即启动对存储器的传输。当在直接模式(禁止 FIFO)下将 DMA请求配置为以存储器到外设模式传输数据时,DMA 仅会将一个数据从存储器预加载到内部 FIFO,从而确保一旦外设触发 DMA 请求时则立即传输数据。
& {% T, \2 P# a( o- Y! C" ^( _5 ~: J
b.FIFO模式:可通过控制寄存器 DMA_SxFCR 的 FTH[1:0]位来设置FIFO 阈值级别为 FIFO 大小的 1/4、1/2 或 3/4。如果数据存储量达到阈值级别时,FIFO 内容将传输到目标中。如果对传输速度要求不是很苛刻,建议使用此模式、直接模式传输效率太低,一次传输的数据量少而工作量大。
; G0 o, E; g+ _& s; I
$ y) l* P* u% Q# X: m. F/ h④存储器端口:DMA 控制器提供两个 AHB 主端口,分别为AHB 存储器端口(用于连接存储器)和 AHB 外设端口(用于连接外设)。存储器端口用来连接外部存储器,实现内存与外存的数据交互。% b! O) q6 \( \; T% B: d, k$ ]; E
8 l; f" t% u; o" e' l! S1 V9 E
⑤外设端口:用于连接外设,实现内存与外设的数据交互.
3 j' k/ w: P8 ]- r9 g& X+ v; Q8 K v0 X1 \* |
注意:DMA1 控制器 AHB 外设端口与 DMA2 控制器的情况不同,DMA1 控制器 AHB 外设端口没有连接到总线矩阵,因此,仅 DMA2 数据流能够执行存储器到存储器的传输,DMA1 不能实现存储器到存储器传输。2 c# P* ^- g% u q$ j# R! F, X) {
5 ?( Q, i% _$ h/ L/ K, FSTM32F4-DMA传输3 o/ V( M- |4 j: `+ G& i
1.传输模式:DMA1 只有外设到存储器和存储器到外设两种模式,DMA2 除前面两种传输模式外还支持存储器到存储器的传输模式。模式选择可以通过 DMA_SxCR 寄存器的 DIR[1:0]位(方向选择,规定谁是源,谁是目的)进行控制。
5 q# i o: X; o. n. E- v
2 A: \' @( l2 C" D4 c8 P5 t
5 }+ A1 d, X$ L( {# h, Y- T" P# b& a
& O! d+ Q3 M, \3 {' A2.DMA传输模式的源、目的、长度:4 |% D" f% R5 Q8 B
; U# t0 ^+ W5 u$ V) Oa.DMA_SxPAR寄存器: 设置外设寄存器地址8 X' F5 d. `# i% R4 t
. T0 O6 M0 V* D2 Q2 {9 j. v+ xb.DMA_SxM0AR寄存器:设置存储器地址
?, R M; Y1 `& {- q5 Z
' N+ O, @: l# Kc.DMA_SxCR 寄存器 :DIR[1:0]位配置数据的传输方向
' H+ {6 B; N% n' s% t) k0 G: S. K4 {1 X2 s
d.DMA_CNDTRx 寄存器:写入需要传输的数据量, (0 到 65535),注意是数据的一个量,单位并不是字节
' [# _6 M- h: D& E3 r7 g; Q
* h" Y: R: Z4 D; ~; De.DMA_SxCR 寄存器中的 PSIZE 和 MSIZE 位:设置源和目的的数据宽度,两边的位宽尽量保持一致,位宽只一次传输多大的位宽(如一次传输2字节,位宽16位),如果不能保持位宽,很有可能造成数据丢失,应当开启FIFO模式,保证数据不丢失。
5 @+ c& P: l( }) Z, b1 R' m6 A1 P, }4 L( c
3.DMA增量设置:
$ V! g( G# L" k$ O ]; |1 U/ f
; m: U% J5 ^' _5 e" Da.根据设置 DMA_SxCR 寄存器中 PINC 和 MINC 位的状态,外设和存储器指针在每次传输后可以自动向后递增或保持常量。+ N# v" [5 g# o# v3 w6 f; I
' ^$ _' h" ~, R
b.当设置为增量模式时,下一个要传输的地址将是前一个地址加上增量值,增量值取决与所选的数据宽度为1字节(8位) 、2 字节(16位)或 4字节(32位) 。如果不启动,每一次取很有可能只取首地址对应数据,而不会向后取值。
# v# S+ H4 g( O
6 W7 |, c% P* z6 I8 Hc.一般情况下外设并不需要递增,每一次都是同一个地址,要不要配置外设的增量模式依具体情况而定。
- e( G# M+ F( T1 r9 i4 d: B) E) T7 a" u8 i* n# H
+ A7 T+ d8 M9 w, I U& U
5 H5 |# Y- L Z2 Q) \
0 e& V3 i) `3 v, i$ V
: k. D' i1 j* x6 d8 c; A6 ^4.DMA循环模式:# g' n6 o* q) | i3 M8 o# t
. `. I9 H! P, h, a! F+ S: i0 za.循环模式用于处理循环缓冲区和连续的数据传输 ( 如 ADC 的连续循环扫描模式 ) 。可以使用 DMA_SxCR 寄存器中的 CIRC 位使能此特性。
) e4 n. d! D# S2 |. l/ d% [
; l5 [' \3 |" a3 ]% Qb.当启动了循环模式,一组的数据传输完成时,计数寄存器将会自动地被恢复成配置该通道时设置的初值, DMA 操作将会继续进行。
1 S2 n8 @9 X( A# Z
3 m0 \( x! _( A) N' a5.DMA单次传输和突发传输:, p8 u5 I# s: J; k _% I2 ]6 z6 D
3 \7 {( ~2 Z/ N0 h4 u$ h9 O; |a.DMA传输类型有单次(Single)传输和突发(Burst)传输。DMA 控制器可以产生单次传输或 4 个、8 个和 16 个节拍的增量突发传输。一个节拍对应着用户配置的一个数据宽度,一般突发传输要配合FIFO来使用,突发传输的数据量不可能超过FIFO的大小 Y( u6 A$ l/ C8 Q1 ^/ M9 F
( x& h- [; {4 i! A' ~+ B5 I! i
b.突发大小通过软件针对两个 AHB 端口独立配置,配置时使用 DMA_SxCR 寄存器中的MBURST[1:0] 和 PBURST[1:0] 位。
' _3 |. Y' {+ s0 h5 F5 a# ~# Q7 a Q
6 S; W, ^9 y" O. k- Qc.为确保数据一致性,形成突发的每一组传输都不可分割:在突发传输序列期间,AHB 传输会锁定,并且 AHB 总线矩阵的仲裁器不解除对 DMA 主总线的授权。7 I3 o6 w% D/ A5 y v
9 d7 o# }, ?' v7 p0 q
. x* p) w, p! ?. |$ |+ j
1 Q& W$ K, a r* c+ R3 e% L* ~/ Y
3 N; }" u$ j$ h6.DMA中断:对于每个 DMA 数据流,可在达到半传输、传输完成、传输错误、FIFO 错误(上溢、下溢或 FIFO 级别错误)、直接模式错误时产生中断。这些标志位都在中断状态寄存器DMA_xISR中设置。! Y2 N: {- S5 w8 l
3 r* N( O+ n# H! T
, A( s; S: B& j$ Z
) V% u6 a( M- n) k& r. vADC_DMA多路采集实例
+ Y. l! L) p/ q; b! X+ i" l# `1 O# U" V' W) Z
实验要求:利用ADC采集电位器以及芯片内部温度传感器的数据,在main函数中启动ADC转换以及DMA传输,最后在DMA完成中断中打印采集到的数据。) t) J1 ^8 x1 k# p6 D, I/ m
% j' V' ]8 E' @2 q# B; d1 z
说明:之前使用ADC采集两路数据是使用轮询的方式判断采集,需要CPU一直去判断,消耗CPU资源,显然这并不是ADC多路采集最好的办法,ADC多路采集一般使用DMA来进行传输,规则通道组采集数据时只有一个数据寄存器ADC_DR,读取数据不及时很有可能发生数据覆盖。3 C1 H) X" ~2 j3 u& W) p
7 L v: y+ w1 E, A2 D7 Z采用DMA传输,每当一路数据采集完成后都会产生一个DMA请求,及时把数据寄存器中的值搬移到内存中相应的buffer中去8 d; y$ Z2 U& A+ M
2 J- }7 f4 [' o' A8 ^
, ]5 k" u# J# T- t. D
步骤:, r) N! y4 c. G% }# z5 \
; Q! b& M+ \* u1 s$ ~/ G7 v' J1.配置RCC$ k* e% E8 g Y9 e8 O4 {. \& H' O
' I6 W$ S" V9 ~( [2 k
2.配置PC3位ADC1的通道13,配置内部温度传感器通道
5 @6 J! J0 S9 |% B8 r# `4 L" v: ^7 K( _2 J# l
; p: j6 l& S/ V+ q9 r$ _
: F0 i4 A: ]2 r* _
3.配置DMA" G `8 C1 h3 T% V L2 y
7 q: n3 j& j' m- m J
2 z7 m" m0 Z4 i `* R: T' B" c
1 k7 P4 w& p% ?6 S
4.配置ADC
9 _' H4 `& @4 B$ Y1 l: \7 @7 F! ^4 u
3 o8 ]) h; p9 n% s* Z8 R3 x5 o( R: A
* z# N4 u1 E3 S' x! U. U+ T; p: d! I6 M1 u
5.配置中断
` N. q2 }7 c! T+ \" G0 |+ Q& v, |: o# u
- g6 F; L" W2 s
( q) a5 |) s8 P3 m* |7 H5.编写代码
k2 F* R7 z# C3 X1 J- <font face="Tahoma"><font size="3">//main.c </font></font>) H- w+ p- J$ A
- <font face="Tahoma"><font size="3">- b$ B" C" b7 F( L
- </font></font>
0 o7 I, p5 r# I/ Y - <font face="Tahoma"><font size="3">uint16_t adc_value[2] = {0};//定义一个全局的接受区域,以便外部调用 </font></font>2 W, z; z) r K( ^
- <font face="Tahoma"><font size="3">int mian(){ </font></font>
( C6 N }- o# ~% {5 O; E - <font face="Tahoma"><font size="3"> HAL_Init(); </font></font>' W- a1 M( t/ L" ~
- <font face="Tahoma"><font size="3"> SystemClock_Config(); </font></font>
; y, c" u( x* h2 W6 s% u% ]" ` - <font face="Tahoma"><font size="3"> MX_GPIO_Init(); </font></font>0 _/ q+ I5 e) E1 I9 Q
- <font face="Tahoma"><font size="3"> MX_DMA_Init(); </font></font>! ~8 X: ~& R: i8 l
- <font face="Tahoma"><font size="3"> MX_USART1_UART_Init(); </font></font>
1 E" Y0 p7 B4 [% a6 A - <font face="Tahoma"><font size="3"> MX_ADC1_Init(); </font></font>
5 j' z0 i* y. g; P - <font face="Tahoma"><font size="3"> printf("this is adc dma test\n"); </font></font># Y; a# Y+ {9 v4 W1 W9 ?; P
- <font face="Tahoma"><font size="3"> while(1){ </font></font>. f" C: y& m Z% m3 w
- <font face="Tahoma"><font size="3"> //此函数既启动ADC,又使能DMA </font></font>" p3 v+ N) B( U; e5 q2 ~" [
- <font face="Tahoma"><font size="3"> HAL_ADC_Start_DMA(&hadc1, (uint32_t*) adc_value, 2); </font></font>
5 |. Q, H/ V2 B1 t; G% } - <font face="Tahoma"><font size="3"> //2表示传输了2个数据的量,一个量是我们设定的位宽 </font></font>! Q q9 r; [- {" m9 |9 p
- <font face="Tahoma"><font size="3"> //内部有一个专门的寄存器来记录这个量,每传输完成一次,寄存器的值减1 </font></font>4 R4 G9 P+ R* ]
- <font face="Tahoma"><font size="3"> //减到0就会触发DMA的传输完成中断 </font></font> U' T% G8 y7 p7 c- m% K( t9 N6 m/ x
- <font face="Tahoma"><font size="3"> HAL_Delay(1000); //每过1s启动一次 </font></font>- l4 [, w2 m3 C2 h8 [0 B
- <font face="Tahoma"><font size="3"> } </font></font>. C, ]" F; Y# _' q. F8 S
- <font face="Tahoma"><font size="3">}</font></font>
复制代码
: f% t4 B" i* A/ i& W4 D关于DMA传输完成中断处理函数是哪一个,先将函数HAL_ADC_Start_DMA(&hadc1, (uint32_t*) adc_value, 2);追进去可以看到一段代码设置了DMA传输完成的回调函数,将这个函数赋给了一个指针,这个指针是DMA_Handle下的一个指针,DMA可以用于很多的外设,不同的外设的转换完成函数是不一样的,DMA没有一个固定的处理函数,这个函数需要在外设配置的时候自己去初始化,DMA只提供一个接口,一个传输转换完成的函数指针,不同的外设对这个结构赋不同的处理函数的指针,这里对ADC来说自己封装了一个DMA传输完成处理中断的函数,当DMA传输完成中断触发的时候自己去调用这个接口下的对应的函数。ConvCplt的意思是ConverseComplete
+ d/ _: I1 F/ E- s+ ^
0 E. [. k2 E, ` d U/ t9 B2 _
% c1 _" S/ W8 ?
, E$ B3 o' m4 P# b M. |将void DMA2_Stream0_IRQHandler(void)这个中断处理函数入口追进去可以看到DMA有各种不同的中断处理请求,我们在传输完成中断***Transfer Complete Interrupt management***这一部分里可以看到hdma->XferCpltCallback(hdma);这样一个指针函数,但是用户无法使用这个指针函数,这个指针在HAL_ADC_Start_DMA(&hadc1, (uint32_t*) 这个函数中已经赋值,实际上这个函数最终进入了HAL_ADC_ConvCpltCallback函数,也就是ADC传输完成函数。. b* N& _. C, X$ r
( K$ K g! k5 Q% j: D; W
' c; c# C% O5 j8 u+ W1 l- <font face="Tahoma"><font size="3">//adc.c </font></font>
) s5 S7 z. g K# a: f - <font face="Tahoma"><font size="3">extern uint16_t adc_value[2] ; </font></font>
9 f7 a0 \) @! P9 ^ U8 J - <font face="Tahoma"><font size="3">void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { </font></font>
6 v/ R& Y i& K( O$ W' L( v - <font face="Tahoma"><font size="3"> //每一次采集完成后都会将数据传输到value中 </font></font>, I# A9 K7 [- h
- <font face="Tahoma"><font size="3"> if(hadc->Instance == ADC1) { </font></font>
( ?3 [" _5 {. l2 K$ B) g - <font face="Tahoma"><font size="3"> printf("adc_value[0]=%d, adc_value[1]=%d\n",adc_value[0],adc_value[1]); </font></font>! r$ k# ?" f9 g4 t: b
- <font face="Tahoma"><font size="3"> } </font></font>
& D. {- O1 J; B - <font face="Tahoma"><font size="3">}</font></font>
复制代码 ! K5 p1 s$ t# y+ _! n& l
至此,ARM开发初级知识已全部更新完毕!& W3 ?9 _4 V8 w5 r( D$ [$ ?
8 z5 w9 o0 j, a' R3 b" C6 s5 _1 I2 ^1 ]
转载自:骆驼听海
/ T* j) @6 n+ C" `7 I- ~) E" o0 n$ {; P" D
7 X( X/ g. a9 K2 {, V* I# Y2 `
3 ]7 @6 y5 | ^ v$ |# l {
) T+ W* M! l0 l4 g
|