$ e9 W2 a% a0 q3 n) Q% K
) A& Y" }: G8 B* ^2 b1 V4 h- H# _( M% I
很多STM32芯片里往往内置了专用的ADC通道,比方用来测量Vrefint,VBAT的分压或温度传感器的输出电压信号。不同系列所内置的模拟信号通道可能有差异。这里以STM32G4系列为例,它内置了对应于Vrefint,VBAT的三分之一分压和温度传感器的输出电压的专用模拟通道。
& L J; z P' F+ }7 K3 K# V4 R+ C5 o' R9 p& n
8 p) a1 s0 t* H7 F( S/ x8 |% M2 m) \, z* f |9 I' \9 n
# H z! ]( K" R0 W! B7 }4 i
7 s$ E, J5 S) c( s下面的示例就是针对上述3个通道进行ADC,并测量相关电压和片内温度,最终得到3个结果,分别是VRefint电压,VBAT的电压,片内温度。
0 j& |2 K( B- c, p; J. Y/ ]
8 M* ]! ^" v+ }* ?4 j y6 J4 C
1 @4 F, B0 c% ^. k. O' f2 N" R% ~& \3 i4 B
实现过程是这样的,大体分四步:【有点点麻雀虽小五脏俱全的味道图片】8 Z& }' M$ g0 i& L( ]- @
) [3 d' [* O# q, E" ]1 v
1、TIMER1 更新事件触发ADC的转换;1 B% m a3 L' I0 P7 X- n3 [
% m" \4 f' G% N1 f7 M! `4 ]5 {7 V
2、CPU基于EOC中断获取ADC结果;; @- K- t6 m* \: O/ ~3 l1 V
$ A0 G2 C) e6 G* K% L3、对ADC结果进行换算,得到电压值和温度值存放在特定内存位置;4 U/ i& s+ X% z
4 _! p9 D3 J0 j0 E! L, f9 P2 x/ ~& ^4、基于DMA传输通过UART将最终结果在串口终端显示;
1 O, d5 l3 I# q; n; U( G9 Q7 z$ | L H/ C7 {
其中,TIMER1的CH1输出PWM波形,其更新事件做ADC的转换启动信号。每次的TIMER更新事件触发ADC,3个通道扫描方式转换。这里的UART使用片内LPUART,使用它主要是考虑它跟板载虚拟串口直接相连,没有其它特别用意。; V3 d- J; t3 z, V* n
( i6 i2 V# H% V- e9 c/ m, ]! n
3 m7 h) b/ ~; W) Q% O- B8 ~
6 a' P) {6 {$ z. C1 ]我使用STM32G474Nucleo板来进行下面实验。其中VDD=3.3v,VBAT与VDD相连。另外,ADC模块的参考电压也是3.3v.( m* }9 G- X; k2 l. b
9 N8 T* s" v$ O9 o- {8 q/ z
使用CubeMx图形化工具进行配置,先看TIMER配置:
/ ]% s0 j' j2 w% U; x' M4 ]0 q
" z! `0 X+ a; U) R
8 f' }0 f8 @9 e! ]! z% `& ?; o% O- ?
再看看ADC的基本配置:0 B2 g% X# w& p. }% }! h1 J) F+ }7 X
4 V2 f+ ]. b2 W& y1 W& Q+ i; s
: }6 y: w2 I& r/ J, `3 y$ ^9 N
8 @& @' w; N! K
LPUART的基本配置:
8 F5 O" O4 z# C& x6 Q7 x, ^+ S: [4 i b
0 C6 S6 i6 n' U, W6 p9 U
* c6 ~3 e- h% I# o
因为要使用ADC中断和UART的DMA传输,记得做ADC的中断响应使能配置和LPUART的DMA配置,这里只使用UART的TX DMA功能。
6 A' y8 r1 z9 G. J7 |+ \# `; {# W: Y& q9 `5 {6 `0 }
$ ~1 V. |# A1 O- Z" g: q$ X2 l8 T
! _9 X% w/ o9 ^; x使用CubeMx主要配置主要是上面这些。+ E: Q2 j6 q1 E4 l: ?
; i: K0 {8 l7 g" S$ d5 O在组织用户代码前,先简单介绍下片内温度传感器的内容。该温度传感器针对不同温度有不同电压输出,其输出电压跟温度呈线性关系。ST公司针对片内温度传感器在两个特定温度【30℃和110℃或30℃和130℃】、基于特定参考电压【3v或3.3v,不同系列以数据手册为准】生成了1组校准值并存放于片内特定FLASH位置。0 w7 n( u. d+ H1 \. Y: ~$ [
) s& d! F6 y! z, a2 e8 tSTM32G4系列的校准值是在参考电压为3v,30℃和110℃条件下的两个值,在数据手册里还给出了校准值的片内存放地址。) Y, o" J1 H) \ x' d- m; i
5 c0 T9 E( k* H/ v$ c
5 X @! D. Z+ y& B
- L8 j; l% ~- ~; k0 H- X6 t$ u针对这个温度传感器的使用,ST公司在参考手册里还给出了计算公式。其实,有无这个公式无所谓,我们不难自行推理出来。【TS_DATA代表某时刻测得的传感器输出电压对应的转换值,TS_CAL1/TS_CAL2分别表示在30℃和110℃条件下基于传感器输出电压的转换值。】, b* z# d8 o" o6 B* P7 X
* `" I1 ]4 M* ^- y" P
: U* y: u) f5 Z: h; |# v
- d+ q# s2 q+ h8 C0 R+ n' `
另外,前面提过,ST公司在手册里给出了温度传感器的两个温度下的校准值,但要注意生成校准值的ADC模块所用参考电压跟我们实际应用时AD模块所用的参考基准电压可能不一致。如果不一致,就必须将ADC值换算成同一基准参考电压条件下的数据。目前在ST手册里也特别强调这点了。我把上面一副图再贴一遍于此【见黄色语句提醒】。
4 H9 W7 ]8 h" i0 Q# |$ V6 g J
! D- m" O3 z8 I8 }- e( I& Y
' \+ |: e0 j( m- [9 y. C4 e' ~) P
% z9 \: j+ A6 e2 u) r% L0 i关于这点,我们也不难理解。同一待测信号、同一ADC模块在不同基准参考电压下转换值往往是不一样的。见下面示意图加以理解。
\* w" x9 p+ g9 N" `
0 h" f0 u6 r+ {% D$ j) e/ Z
; u- ]0 A! C ?7 B
7 i3 A) I: L& G; [7 v' B, S" @完成各项配置后,创建软件工程。添加必需的用户代码:5 Y) F# c8 f$ H; C2 l1 a
/ ~9 G6 b! k. C- #define TX_Timeout (9999)
9 n1 h% h7 \5 R; E c' i
3 ~/ j; s5 K) {% \- #define TS_CAL1_ADDR (0x1FFF75A8) //用于计算温度传感器数据# S4 {& `4 N/ o& u; M1 @
- #define TS_CAL2_ADDR (0x1FFF75CA) //用于计算温度传感器数据3 h9 C& f7 X; I, O& m6 ]
( `1 a7 I4 `+ G0 l8 S- #define size1 (40)
9 k( H) `+ t$ a' x) V - ' y x$ P2 r" U' u9 a
- char WDVol[size1],BatVol[size1],InVol[size1];; C" ~& t( Q) O6 y. L" H1 t
- 1 _8 r# X6 `( B5 N2 |
- uint16_t ts_c30,ts_c110;' |0 q7 }8 Q' p2 r$ S
$ r5 L4 z. Y2 c* M! h, B- uint16_t ADCResult[3],convCNT;& Y& D! l( m' W! b( \
- . w4 y* m* f& u& Z
- volatile uint32_t Completed,EndofCon_Flag;
$ r' k+ j6 a7 g, Z+ E
8 e2 A% F8 }% |- float VBATVolt;//存放BBAT电压最终结果3 X0 z, v* u' U, J2 o& a4 E
- float VRefint; //存放Vrefint电压最终结果4 v6 P( f% R1 _; e
- float Temperature;//存放片内温度℃最终结果0 E. e. a9 w8 h
- d' e9 ~' l; ~, O
- int main(void)- A! K2 p& Z d$ J
- {$ B8 d. Z' e2 h2 i' U7 X
- /* USER CODE BEGIN 1 */& D* P. Z3 {% z# L, C
- t* ^" q, n* ^
- /* USER CODE END 1 */
6 x! P j, \8 G$ c7 t' w4 T5 ?
; i2 i/ C! \3 @( u/ W2 o- /* MCU Configuration--------------------------------------------------------*/
$ a( a5 t; f2 [* }/ d - 0 j7 l X# \/ `4 L8 \+ z# Y3 h$ O. t
- /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
+ b( I9 i3 ^3 b - HAL_Init();
/ ^) j' Z) V$ P: F: ^9 m
8 ~4 M: [5 H# ~/ j6 U- /* USER CODE BEGIN Init */
$ o5 [5 j, I, F# f
8 ^0 h H, d! _4 C3 m- /* USER CODE END Init */
l6 d4 T+ [" B& U# P9 F0 {. e - 1 f: d. }* D- J2 _7 I7 S
- /* Configure the system clock */( I5 L$ q. z! s
- SystemClock_Config();
. X+ n4 f' _2 A) o4 c - 3 `5 W4 a4 p# ]4 I
- /* USER CODE BEGIN SysInit */% W& r: @6 g( ?: Y: S
- * h6 c0 o7 g% E, I: U
- /* USER CODE END SysInit */
; i8 a. C/ p9 _( h+ B - , V8 F3 G8 {5 p1 {$ F
- /* Initialize all configured peripherals */
: N8 h# a y* e5 D" K; K7 {1 B - MX_GPIO_Init();
/ x' N. D1 s0 C - MX_DMA_Init();0 |* X3 i2 ]+ q, h% b: S% R& F& W( @7 _
- MX_ADC1_Init();3 Y1 c2 h! J+ a* ]% S5 K2 Q
- MX_LPUART1_UART_Init();- S( P7 \; X7 m$ h- s
- MX_TIM1_Init();
3 j# K6 F, g# r0 t6 _% K" x - /* USER CODE BEGIN 2 */7 ?4 z5 @' v) I
2 {3 w; q, D; Y# s& O' H3 }- ts_c30 = *(uint16_t *)(TS_CAL1_ADDR); //读取30℃时的ADC校准值- R& J G3 p' M; U6 W8 Y
# M' ?( x' d' [7 J; L- ts_c110 = *(uint16_t *)(TS_CAL2_ADDR);//读取110℃时的ADC校准值+ ~! h8 O# P1 L( l$ [9 X* j" D
. L! E9 B' }) R- HAL_ADCEx_Calibration_Start(&hadc1 , ADC_SINGLE_ENDED);//ADC校准0 j! D) M2 m- n3 b8 J4 L
- / M0 n4 ^! w* C) Q2 u: |3 M7 a/ |; t
- HAL_ADC_Start_IT(&hadc1);//启动ADC并开启转换中断# T# B/ p: a# n( m
- 8 z3 T+ n M3 D! B% r
- HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
7 W$ g P6 w7 F" I1 o3 k
+ L4 y8 z. V* W$ u1 J: u9 \( s- 8 x- p4 d4 w; [) y) I# P# y1 c
- /* USER CODE END 2 */- _# u e# f* }2 Z3 A* Y$ j
- , q# U7 ]$ `( z" j' y2 v
- /* Infinite loop */; {, c6 Z1 T/ I0 s/ t" S, v
- /* USER CODE BEGIN WHILE */
. H; u9 I$ s8 i7 P" m' e& t - while (1)
' O( ?1 i _$ { - {# G L/ y V; I" ^% u
- /* USER CODE END WHILE */
' q# ^* k. z: e - ! ?0 G) t; h8 I2 b% h& J' {. }% T
- /* USER CODE BEGIN 3 */. N6 ? Z* O8 n/ N
. g, ~9 p# C3 U$ `5 e' i- if (EndofCon_Flag!=0)1 P/ a% ?6 I9 |) G3 N4 b" p
- {3 C4 D2 x' Q8 E
- VBATVolt=(ADCResult[0]/4095.)* 3.3 * 3.;
5 f8 M: S( x# Q8 \
) z& n# f8 g% V9 B$ Q- \- VRefint=(ADCResult[1]/4095.) * 3.3;
8 D$ k L( a% \: F0 } - ) w2 M) s% T* z; X0 N' T
- Temperature = 30.+ (88.*(ADCResult[2]-((ts_c30/1.1))))/(ts_c110 - ts_c30);+ {5 ^7 k* i5 i" K: t
5 T8 t1 r& k. Y" V4 [% C5 S6 a- EndofCon_Flag=0;
: Z7 h$ ]. q* J
4 u7 W9 G" [2 R2 d6 l- //HAL_UART_Transmit(&hlpuart1, (uint8_t *)WDVol ,sizeof(WDVol), TX_Timeout);
2 S, r) o* ?. W - # J+ B0 R: M0 H b& k1 m( |1 _, d
- HAL_GPIO_WritePin( GPIOC,GPIO_PIN_3,GPIO_PIN_RESET); //for auxiliary test+ E, X$ [* G' x6 ^/ ?
* d( n9 B" ^% B S4 q. K- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)WDVol ,sizeof(WDVol));3 V2 a7 ?- x- U1 q. q$ ?
3 m ^, R0 ]+ j0 |4 s5 M) B d( L- while(Completed==0){}
+ P, A6 Y1 ~- v: D - Completed =0;
% x* x5 I1 Y7 P; L5 S q+ e
) l: N9 x: f5 G- g7 s2 x, u
# R* f, ]; a1 {( Z: y- //HAL_UART_Transmit(&hlpuart1, (uint8_t *)InVol ,sizeof(InVol), TX_Timeout);& H' `+ H7 ^/ ? h7 g
- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)InVol ,sizeof(InVol));
( r7 L; ]0 D! m) w2 G - " j) d* I9 z1 I7 E
- while(Completed==0) {}. ~0 A2 J$ }4 \" O0 {# A$ @
- Completed =0;8 H6 X5 s3 z" f- b6 f- J2 `/ C8 ]
9 _3 w5 L/ A# z& @3 x
4 X$ s5 s; {* y3 M X4 U, ^- //HAL_UART_Transmit(&hlpuart1, (uint8_t *)BatVol ,sizeof(BatVol), TX_Timeout);
9 O G6 Q# K( ` - HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)BatVol ,sizeof(BatVol));
8 k B& W2 P# f2 ^
4 k+ e# L: m7 P- while(Completed==0){}( d5 f y! `: j. C& F
- Completed =0;7 z6 u, K$ W1 R2 I2 K* x! r
- , K+ t- T) u4 H4 O d/ l
- HAL_GPIO_WritePin( GPIOC,GPIO_PIN_3,GPIO_PIN_SET); //for auxiliary test8 Z" v( X2 H% v* ]) v' ?/ \
2 @) }0 W! Z$ l7 |, m- }
3 t. M- _5 M9 W3 I% {& F; g6 d - - \9 k' F6 \+ O% ^# L) i
- }
/ K1 S( E& ?5 n+ {9 Y1 ] - /* USER CODE END 3 */
' @* Y; p/ }! H) N - }% V/ f! F! n& C. n( G( t( S
) ~7 o+ A( V) ?* o1 X9 P- //ADC EOC 中断回调处理函数
; n7 X+ [) } u - void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
2 g+ Y( q% Z$ b* B5 \1 t% Z! A# B( x/ G - {% E8 ]- Z* Y7 }' a
- ! Q9 Q% Z! ]: G2 `0 C! @* S
- ADCResult[convCNT]=HAL_ADC_GetValue(&hadc1); //获取转换结果并存入数组. A7 C. i7 u7 P; s5 u" }4 n" I
- 1 ~: v7 G% e4 l' P$ o; c
- convCNT++;: p! h& n, q7 d; V( S
1 J1 V0 p0 b) F5 s# w" V" j1 z- if(convCNT==3) $ z8 r; `; I( p8 V$ U
- 2 T Y B% D1 |! b$ U
- {
" z# I1 N: `' l6 {2 K, \* ^ - convCNT=0;7 h8 ^+ W3 F! T$ Y: i4 S
" @- Z' T& v! s, F* F W% o& b- EndofCon_Flag=0xff;
' c- Z" [" h6 ?* d
) Y0 [' k# |& w2 G, j- sprintf(WDVol,"Internal PN Temperature: %5.3f \r\n",Temperature); / r+ [5 p. ?3 _; s2 {
9 B& t; T I4 t+ {4 q9 u- sprintf(InVol,"Internal Reference Volt: %5.3f \r\n",VRefint);
8 w# R/ ~% W. t8 h/ a - * F# d* D( m- L# [2 {; G. \
- sprintf(BatVol,"Current Battery Volt: %5.3f \r\n\r\n",VBATVolt);3 [4 ], S2 n& X4 z) p! n5 L4 H
- / `4 ]7 U3 i) M0 H" k
- }: \( r6 Z z0 ?- i+ V; w$ |
8 K, e4 Q7 M7 x$ b* {- k$ M, G4 l- }
$ s$ x( E0 ~2 E$ X
) O+ `- ]1 B' P( B" | U- //UART DMA 传输完成中断回调函数9 @1 I5 F- D* \( K
% P- r) F* j! E9 h$ Q- void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
% f5 Z9 Z8 `. ` - {3 f/ d( a& Z( L/ E# E' m
# o! v" ~* a0 U1 H6 A( T- Completed=0xff;9 Q# ]5 ]" ~5 S( P' s8 e% j. C
- }
复制代码
; G$ K9 j [7 y1 }基于上面的配置和测试代码,我们就可以看到最终的结果了。定时器周期性地触发ADC,每得到3个ADC结果就进行数据处理,然后通过UART以DMA方式传输到串口终端。注意VBat电压是测量结果再乘以3得到的。. l, I v& Q7 k( ^
8 M* U. c+ x6 N5 h
4 q( H2 _8 l' Y! g7 \0 ]
; G8 Y6 H) w$ a" m& g% G% A7 S
# n* s( V% R1 r p0 b# a# m$ f" N, l( d
针对上面的应用演示,最后给几点相关应用提醒:
8 C$ U8 c: |6 R; c$ f) P( f, V) e
, S. F) ?- a. f1、针对温度传感器做测量时,校准时使用的参考电压与实际应用不一致时要做换算,换算成相同参考电压的数据后再做计算。这点前面也提过了。
) A% a q/ a& q5 v6 k! l+ b) I# x* z1 w" l7 M7 K+ F$ v. S7 c
2、使用TIMER的TRGO触发ADC,如果选择类似比较事件、更新事件来触发ADC时,此时ADC对触发极性的选择是无效的,或者说ADC的转换仅依赖于触发事件时间点。如果是选择TIMER的Ocref信号作为触发源,此时ADC的硬件触发的极性选择是有效的,可以是上沿或下沿触发,甚至是双沿触发。这时就得根据需要选择合适的触发沿。【可以进一步阅读本公众号文章《STM32定时器触发ADC的时序话题》】
# V2 k! m& @1 e% w5 B: k: I: H* W8 Y, o0 o
3、这里使用UART的DMA传输依次显示三个结果于串口终端,三个启动UART DMA传输的函数须保留适当时间间隔,即等上次传输完成后再启动下一次传输,因为这里每次传输使用的是同一DMA通道。否则没法全部正常输出。比如若把上面3次UART DMA传输的代码改成下面这样子:) s1 G" \% E! H. x: m; x
& O# x v; P3 P: l H/ ~ @8 o9 s
- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)WDVol ,sizeof(WDVol));
1 n, C1 B0 I5 }8 @% a* k - ! \1 Y, H' A/ j5 h
- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)InVol ,sizeof(InVol));
g7 w3 L4 `# G+ x$ k
' K- k1 X H& F/ K- G3 k- ~, }1 a: P& z- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)BatVol ,sizeof(BatVol))
复制代码
E7 I# ^" ]* s7 h5 Q这时输出结果会变成下面的情形,总是只能看到一个结果的输出,即第一次启动的DMA传输结果。
( X9 Z% \( p& M( A8 p, z5 G, ]6 R) O6 {$ l
9 o: X0 A2 Z7 Y3 X# W* h# i: U( i1 {4 m- x# E9 v, Z$ r+ y
如果想省事点,直接在相邻2次DMA传输间加上合适延时也行。我这里根据DMA传输完成事件来决定执行下一次发送。在DMA传输完成中断里设置Completed变量为非0值表示当前一轮DMA传输完成。$ V) m- f6 {/ o. A+ `
& z4 w( R( O7 ^! [: W* V4、对于那些在中断和主程序里都会被访问的变量,记得将它们冠以volatile。
) |) W. w& T5 d$ j! b t9 b U: a6 x( [) E+ I) g9 [
下图的三路波形是我调试时辅助使用的。4 F$ Z/ u5 ~ {' Z, Q3 Q/ c2 r
5 D6 o0 k2 o+ u6 E0 d, k- z第一路表示计数器的计数变化,显然是单向向上计数模式。) y- }. @6 m! A3 z( t0 i
4 K3 g" \: X% ?, S+ Y, a% f第二路是TIMER1通道1的PWM输出波形。 r, g3 I4 _3 a4 Y
. b, ]* T+ Q0 v' r) b
第三路是我每次基于DMA实现UART发送时拉高拉低的波形。平常管脚电平为高,在实现DMA传输过程中拉低。. T+ C, C+ j5 f4 c$ k
! Y+ w: |, T- P/ T6 E8 J; g
0 P' A: P4 i8 B& a* e2 r7 f
; s' J( E% ?3 n- [
如有侵权请联系删除
9 V3 }9 O& F- Z/ B8 q1 E转载自: 茶话MCU
( [ H( S p- C0 o) |5 ~- c
" a; h" V a1 g# F1 U2 ? & j( x8 j7 ^& L9 ~5 I
& u3 Q; O) j6 M) m7 P$ k3 @/ l
|