* }& |% j Z. L4 L. A% `7 J
8 m' F0 m* a/ G! O4 F, A9 g9 x7 W F5 x+ N. d
很多STM32芯片里往往内置了专用的ADC通道,比方用来测量Vrefint,VBAT的分压或温度传感器的输出电压信号。不同系列所内置的模拟信号通道可能有差异。这里以STM32G4系列为例,它内置了对应于Vrefint,VBAT的三分之一分压和温度传感器的输出电压的专用模拟通道。
; f; V- {% ?* n9 s2 Q" }% o
$ v2 R4 J* E0 P0 {) W [* F8 ^/ j# p
6 _. _+ e# P/ u6 P) ?+ H
) R! R4 M- \/ \" @4 `+ `' B. N2 W$ E. l, W8 D9 Q3 e8 t) S
下面的示例就是针对上述3个通道进行ADC,并测量相关电压和片内温度,最终得到3个结果,分别是VRefint电压,VBAT的电压,片内温度。
U) _' r4 C. n) r. i, r; O; Q& P( l* m. n2 ]
# V: N$ v) g5 ^4 u$ \5 q/ [2 n! B
4 ~8 T+ A% x! ~; g5 z; Q$ U( o0 e实现过程是这样的,大体分四步:【有点点麻雀虽小五脏俱全的味道图片】
2 {" I2 E# r; V+ }# n6 D1 C9 ]# g2 C, ~" Y. g' S8 D# U
1、TIMER1 更新事件触发ADC的转换;; W2 o/ d/ R$ c+ W
0 }- n0 S9 e5 o/ G2、CPU基于EOC中断获取ADC结果;
8 G0 k4 N# O) r+ s G* \+ b& q9 ~5 h7 B- @
3、对ADC结果进行换算,得到电压值和温度值存放在特定内存位置;
) t; u6 O+ @+ \: T5 Y4 @( k4 g* H1 g$ n! ?$ c; P4 `
4、基于DMA传输通过UART将最终结果在串口终端显示;/ n3 }0 Y ?& N7 x& F% k
; @+ {$ A/ J* [% k& }6 X% x
其中,TIMER1的CH1输出PWM波形,其更新事件做ADC的转换启动信号。每次的TIMER更新事件触发ADC,3个通道扫描方式转换。这里的UART使用片内LPUART,使用它主要是考虑它跟板载虚拟串口直接相连,没有其它特别用意。% f4 o7 {* ^; O& N+ W4 Q. \
9 W1 F0 U# h& f) g+ p4 Z/ H. q" ` y- }! j3 W9 N
: i1 f" Z( W; @% g5 R. P我使用STM32G474Nucleo板来进行下面实验。其中VDD=3.3v,VBAT与VDD相连。另外,ADC模块的参考电压也是3.3v.2 w0 J- u' R9 U6 O6 F! `# S1 ^0 X1 h
% W& q" a& q+ \$ T
使用CubeMx图形化工具进行配置,先看TIMER配置:7 N* Q/ S" u( v
, O; @ y b. f! }1 {7 `* X* w; S
+ F: A2 V- U1 J# P( y7 z
再看看ADC的基本配置:* q% N$ P0 b) y o6 G0 |
. y: P2 h! S" n( i, u5 |7 Z: M% H! @
6 L6 [. v2 [ U7 _( q
LPUART的基本配置:6 v" ?' j! q# N& p
& Y9 S0 S( K/ X* s1 j& G$ o7 \7 e
, l. F. \+ f, Q, t
# Y* A/ X& u) W9 Q3 q: W因为要使用ADC中断和UART的DMA传输,记得做ADC的中断响应使能配置和LPUART的DMA配置,这里只使用UART的TX DMA功能。9 e$ a _+ j3 q( x: l8 v
6 f; r6 S2 e5 z% s$ b9 u3 F
+ s) H) P0 P, N1 H% |: i
( g" o+ i, A7 g! B
使用CubeMx主要配置主要是上面这些。2 u4 u6 \0 o t3 d9 _; L# p) G( S! _
s% g) G2 r( J2 A在组织用户代码前,先简单介绍下片内温度传感器的内容。该温度传感器针对不同温度有不同电压输出,其输出电压跟温度呈线性关系。ST公司针对片内温度传感器在两个特定温度【30℃和110℃或30℃和130℃】、基于特定参考电压【3v或3.3v,不同系列以数据手册为准】生成了1组校准值并存放于片内特定FLASH位置。& B6 D. Q# o) ~0 R2 q
2 u' z/ L4 D% d! O, i9 r
STM32G4系列的校准值是在参考电压为3v,30℃和110℃条件下的两个值,在数据手册里还给出了校准值的片内存放地址。
0 j2 A( Y" g( f. l2 q, G/ I0 Z. c, G. r: p9 v
8 g. _/ V+ L* U. B( [6 q
% s) s" u+ G8 n% P8 v" g
针对这个温度传感器的使用,ST公司在参考手册里还给出了计算公式。其实,有无这个公式无所谓,我们不难自行推理出来。【TS_DATA代表某时刻测得的传感器输出电压对应的转换值,TS_CAL1/TS_CAL2分别表示在30℃和110℃条件下基于传感器输出电压的转换值。】
* X$ O+ c9 x: h7 ^2 k
. T3 ]1 q \. ?4 b l! J- v, i, H
7 l! }- ~2 z$ J另外,前面提过,ST公司在手册里给出了温度传感器的两个温度下的校准值,但要注意生成校准值的ADC模块所用参考电压跟我们实际应用时AD模块所用的参考基准电压可能不一致。如果不一致,就必须将ADC值换算成同一基准参考电压条件下的数据。目前在ST手册里也特别强调这点了。我把上面一副图再贴一遍于此【见黄色语句提醒】。4 i: V' c1 G* y" v
* i4 }8 I, z4 i2 q3 X* q0 X
/ p* F7 F# q4 W, w5 R3 q p* m: N" K0 f5 _
关于这点,我们也不难理解。同一待测信号、同一ADC模块在不同基准参考电压下转换值往往是不一样的。见下面示意图加以理解。" `+ h, j" p# S1 O% m
$ q: g, R! c# w e! D2 T
\6 l; L/ q8 \, h. e
! ]( l. f" B4 s1 [' R+ `完成各项配置后,创建软件工程。添加必需的用户代码:
. x5 D6 O3 e# H& d: o6 W) ^2 |- n& u, Z4 k" g% A1 l0 `7 h
- #define TX_Timeout (9999)0 L. m! h! m% ^1 V& K% T
- 4 X$ M& w1 R0 ^
- #define TS_CAL1_ADDR (0x1FFF75A8) //用于计算温度传感器数据& O7 F: a# F& {3 C
- #define TS_CAL2_ADDR (0x1FFF75CA) //用于计算温度传感器数据
' w3 O9 P" q( U0 j' b - 1 Z8 w6 Y) n3 }0 }& H0 T
- #define size1 (40)
% l( U: E+ `' d9 x' @ - # D3 N8 V% e; _# u8 D
- char WDVol[size1],BatVol[size1],InVol[size1];
4 t/ j: E; e: F1 z* t8 t; ]' H
' \1 d; l% n; Y- uint16_t ts_c30,ts_c110;4 ?. A5 A [" I
- H9 `* M+ e Z; ]" {! W0 _
- uint16_t ADCResult[3],convCNT;+ Q# ~4 {" a1 }. ^
- . {& F3 S) U# {; }8 U5 p! m
- volatile uint32_t Completed,EndofCon_Flag;. ~5 [( b! ?- W5 a+ u
- 4 |0 G0 W$ \4 E* x
- float VBATVolt;//存放BBAT电压最终结果$ U4 r& Q5 P0 c# l; v, V, R a
- float VRefint; //存放Vrefint电压最终结果$ w& l+ l6 i3 A- D, Y6 `- q: I
- float Temperature;//存放片内温度℃最终结果5 X7 o0 H# t$ q8 E. Y/ M
/ e; q+ F. n4 f# h; z9 V: Z- int main(void)
* p; d5 R# G, G9 P B8 d: M - {. v) w4 m4 i& p' i0 V. V) C4 r
- /* USER CODE BEGIN 1 */
9 [/ L4 e, J* x5 ?% `' U
# z! X% G5 E5 r1 K6 n6 N3 u" ^- /* USER CODE END 1 */
+ o. r1 o0 j( N$ N U i! ]% \
/ a' H% j3 a# k- /* MCU Configuration--------------------------------------------------------*/. C9 W* P# \" c# ?
- y1 S U) v! Q+ w5 }7 p& Q* E! o- /* Reset of all peripherals, Initializes the Flash interface and the Systick. */5 o. T# F, y( U3 D! l4 m& B
- HAL_Init();
. h( @! p& O4 H: D$ V4 F - / X3 ~8 L! J0 o, r$ O
- /* USER CODE BEGIN Init */2 s+ ~& [. r% n% X- [
- * Z8 g( y, j9 H, J
- /* USER CODE END Init */
4 i: l( V5 `* g, q# u' i3 C
- d* Y- X U _0 v; u7 {% F- /* Configure the system clock */0 k, R, d% t" k
- SystemClock_Config();
" M, ~6 h" n- p# Y* X
7 M ]; @& R/ w6 A p- /* USER CODE BEGIN SysInit */' u: X9 e, `% K. a4 g* z2 @9 y ?
, [4 f0 \' s' ?+ n- /* USER CODE END SysInit */
+ j# U0 }) k$ _% K3 B - % H4 R A, U: [' x) L6 U' K
- /* Initialize all configured peripherals */
$ U% J9 N% s5 D* b+ S2 C! g- c: i1 W - MX_GPIO_Init();7 c/ ^% s }( x# h! Q
- MX_DMA_Init();; k, x- S/ V9 k
- MX_ADC1_Init();- ?- Q) A% y7 t
- MX_LPUART1_UART_Init();
3 x8 f* Q% W. x9 a3 A5 {! p) { - MX_TIM1_Init();
2 c4 L" J" W: K4 C) u - /* USER CODE BEGIN 2 */0 {! T2 k, l& R' U6 w/ j6 R
- 0 h- Z9 ^9 `" R; @9 b
- ts_c30 = *(uint16_t *)(TS_CAL1_ADDR); //读取30℃时的ADC校准值
3 ^, Z' G3 ?) [ - 5 D }, c/ k6 u& x, S
- ts_c110 = *(uint16_t *)(TS_CAL2_ADDR);//读取110℃时的ADC校准值2 ?$ B7 n! N( |* j% O
7 C. ], n$ l& U5 r# P7 f$ Z- HAL_ADCEx_Calibration_Start(&hadc1 , ADC_SINGLE_ENDED);//ADC校准
1 k" n8 ~- N2 l& l" B6 C) p
- r& ?3 e7 l5 ]4 y' W, G& a' w- HAL_ADC_Start_IT(&hadc1);//启动ADC并开启转换中断/ U1 ]4 b$ J x& m1 |& w+ @8 q
8 @4 ^% v" B2 X' f4 t0 N" Z- HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
" c7 C$ V" g: w D
( v7 [: ?# E& `+ s- % Q Q0 U) C9 m. W5 P: {, f! `" _
- /* USER CODE END 2 */& J1 T1 d7 I$ z! d6 y* U1 Z
- & _ ?; T& n1 N" R4 g
- /* Infinite loop */+ X& c( b& ]: W8 P' |
- /* USER CODE BEGIN WHILE */
2 B# q5 F% s. ^% M) J - while (1)$ T6 U; b; n9 Q: ]
- {7 K, v3 v# e1 g* \0 Y, G
- /* USER CODE END WHILE */
( s" f" ]9 G) x; \
% d* G0 q+ H: X; r; l+ H {, M- /* USER CODE BEGIN 3 */2 z1 S U% d: |4 i
& u) j0 e8 o4 l/ S- if (EndofCon_Flag!=0)+ U" B: x5 P. V( s* l& B' ^ o8 B
- {
5 R3 w( C! J/ x+ @1 n; \ - VBATVolt=(ADCResult[0]/4095.)* 3.3 * 3.;
9 U& P( h7 I2 t" B Q9 ~
7 I) i/ f5 v; Z- VRefint=(ADCResult[1]/4095.) * 3.3; + W5 W3 u6 Y# s, z, U( c' y* w. V
- ! c' u$ }! q: o0 G7 r) F1 L
- Temperature = 30.+ (88.*(ADCResult[2]-((ts_c30/1.1))))/(ts_c110 - ts_c30);
5 B0 q' |) M ^- Q6 @; j
1 r5 y2 K( @7 J8 |- EndofCon_Flag=0;
% |4 I; l( Y- ^1 D) g, V1 Q( t - . Y4 s4 ^3 j" X6 \. [! Q
- //HAL_UART_Transmit(&hlpuart1, (uint8_t *)WDVol ,sizeof(WDVol), TX_Timeout);
1 x$ l" l& p( {
9 f+ C b# ^ C- HAL_GPIO_WritePin( GPIOC,GPIO_PIN_3,GPIO_PIN_RESET); //for auxiliary test; ?& _ h: f1 ~. m
- 4 ]8 }2 Z o* ?3 M6 P
- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)WDVol ,sizeof(WDVol));
$ v: J: J7 j4 N, _0 |; r! R - ) a4 C3 L' U R3 k, C
- while(Completed==0){}
4 q2 y3 s7 p2 q7 ` - Completed =0;
^8 V. c' Q3 _3 e9 A) @
, D- J" r* n& x) }1 i- ! }- D @8 T+ k( w+ e& `
- //HAL_UART_Transmit(&hlpuart1, (uint8_t *)InVol ,sizeof(InVol), TX_Timeout);) ^; Q5 m; G: j4 p4 a X# L
- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)InVol ,sizeof(InVol));
' Y% z& n( m- p8 a2 u6 W- \# f* o1 |
: ?8 t- F |! F- while(Completed==0) {}
2 `) S" T1 r# n! j" C$ p: |7 a - Completed =0;
2 b/ C- Z9 E$ G4 N0 M+ k
, h0 e8 m: k/ g& b- " S: V) R( c8 T E# @
- //HAL_UART_Transmit(&hlpuart1, (uint8_t *)BatVol ,sizeof(BatVol), TX_Timeout);2 ^3 O$ O8 _ |
- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)BatVol ,sizeof(BatVol));
$ H" ^$ U+ n: N7 r! b# H: o" E - . j/ A: ~7 D" z/ N3 I# D0 L
- while(Completed==0){}) ^7 P5 |+ }6 S4 _8 v3 e e
- Completed =0;
) F: I+ Z! Y6 ~1 C( n1 b
& o! z( P e! l- HAL_GPIO_WritePin( GPIOC,GPIO_PIN_3,GPIO_PIN_SET); //for auxiliary test# Z4 J1 T' S v$ ^9 q' c3 l
' s$ v5 }3 O* y- }
- m; E2 S ]4 [ A' H, t
( y9 J1 z5 D7 p$ o% K- }
: V' f( L3 M* c8 k - /* USER CODE END 3 */$ y) ^+ q w# ^+ e0 J& r
- }
. z( z. t% H: D' e, x
Z3 t% X0 i9 n+ ]3 r0 X- //ADC EOC 中断回调处理函数
9 L$ ?- _4 t) k4 ]2 T, j, U( k( T - void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
- t) x5 w' c o9 M1 F - {
# b5 S. n& d1 i8 l- U - $ D2 Z. O! @: W+ _4 O* C
- ADCResult[convCNT]=HAL_ADC_GetValue(&hadc1); //获取转换结果并存入数组
C' ]: R2 p+ M. ^/ [$ l+ B6 q
m8 z" _9 N4 A/ o' q& r- convCNT++;
! `# x1 G9 N* g0 U5 c, y2 v1 p# Q - & \8 U' L% F( `* `, f: F
- if(convCNT==3)
1 E$ o" q& Y" ^# ?! }, `5 [' t
4 h1 I; F3 {. e/ C- {
9 |8 V/ E" f9 w1 L1 h! P - convCNT=0;
1 J1 m. X$ n1 i4 p
4 r# O: h. S M2 A. R0 O& z- EndofCon_Flag=0xff;
: a8 z' |$ o D6 g3 E
4 ]/ Q2 _. `1 t' W& e* w" v: K- sprintf(WDVol,"Internal PN Temperature: %5.3f \r\n",Temperature);
9 y' E/ Q0 U$ o! | - * Q6 a5 d- Y& B' G7 Y7 M
- sprintf(InVol,"Internal Reference Volt: %5.3f \r\n",VRefint);
5 E t ~# T) m5 d# j7 |- x - $ G$ s! ~- ]7 L) w7 H
- sprintf(BatVol,"Current Battery Volt: %5.3f \r\n\r\n",VBATVolt);
% n& @3 u4 M' M5 ^; f6 u
( `. w9 P+ s; `; B/ B( |. O- }
. F6 K& z" M1 A4 C
) E& }! K8 D; R1 Z0 z1 \7 O- }) ~1 R" }' p, i4 {- b- b7 f
- 2 j- b! I. ^4 j; B: K5 C
- //UART DMA 传输完成中断回调函数
8 E' |# m; l |6 M2 \1 \ - , {3 r% t3 Q9 w3 Q% A
- void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
1 ~# K4 }) ^- w - {, h# H5 i# P' |4 n3 L2 r+ J
F6 I8 R/ |5 u- F9 _2 P- Completed=0xff;# n3 `, F3 R6 n f3 E% K
- }
复制代码 0 f/ U5 B+ I" g4 R
基于上面的配置和测试代码,我们就可以看到最终的结果了。定时器周期性地触发ADC,每得到3个ADC结果就进行数据处理,然后通过UART以DMA方式传输到串口终端。注意VBat电压是测量结果再乘以3得到的。
: A) h0 G) M: h6 k5 O8 g# t: f m2 G4 [: \" c, p
0 h4 J$ J( g9 U; Y# }' @# T5 K
1 o- C& K2 i7 w) H
0 m5 v: H0 e7 y# K8 _4 z- |" i: z; `& `7 h% h
针对上面的应用演示,最后给几点相关应用提醒:' [, h2 Q* h- ?) g$ J2 N0 E
1 @7 m* u5 Q) m; I' U3 n- C
1、针对温度传感器做测量时,校准时使用的参考电压与实际应用不一致时要做换算,换算成相同参考电压的数据后再做计算。这点前面也提过了。
% f1 O3 m- D7 q9 [
+ U+ x0 b+ ?+ x3 v5 Q' c/ [2、使用TIMER的TRGO触发ADC,如果选择类似比较事件、更新事件来触发ADC时,此时ADC对触发极性的选择是无效的,或者说ADC的转换仅依赖于触发事件时间点。如果是选择TIMER的Ocref信号作为触发源,此时ADC的硬件触发的极性选择是有效的,可以是上沿或下沿触发,甚至是双沿触发。这时就得根据需要选择合适的触发沿。【可以进一步阅读本公众号文章《STM32定时器触发ADC的时序话题》】2 d- @; \+ ?. h0 x
- M9 b" q t# ?7 {7 M9 x4 ]
3、这里使用UART的DMA传输依次显示三个结果于串口终端,三个启动UART DMA传输的函数须保留适当时间间隔,即等上次传输完成后再启动下一次传输,因为这里每次传输使用的是同一DMA通道。否则没法全部正常输出。比如若把上面3次UART DMA传输的代码改成下面这样子:
' s6 r3 m6 I, d( q7 p3 G! b S4 @, \; W% x, t
- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)WDVol ,sizeof(WDVol));+ v, p: {6 e! d5 D, R; R0 |
; o$ B5 S% W" s, \5 D+ M' h- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)InVol ,sizeof(InVol));
9 v- F0 r* v3 q7 O8 r - 1 Y2 E" [- H& j \! S) u
- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)BatVol ,sizeof(BatVol))
复制代码 2 j# `( C' I* `5 l, O$ A6 X1 \/ ?
这时输出结果会变成下面的情形,总是只能看到一个结果的输出,即第一次启动的DMA传输结果。0 ^$ X5 f# o p3 x$ Q, q% S; e
0 o9 W+ g' E: u- j4 e3 V: x
( l8 X3 S; R5 j2 e6 L% ?2 T- D0 s
$ ^5 c$ a2 v8 a$ Z, L! n4 O如果想省事点,直接在相邻2次DMA传输间加上合适延时也行。我这里根据DMA传输完成事件来决定执行下一次发送。在DMA传输完成中断里设置Completed变量为非0值表示当前一轮DMA传输完成。
+ ?# I& G2 ^0 d% Z) [
' D1 s' }. O4 U. H/ u9 W f4、对于那些在中断和主程序里都会被访问的变量,记得将它们冠以volatile。+ P$ r9 C9 r. g5 L% p
+ m6 E% v4 ]' R9 i' `3 e, W0 H3 g
下图的三路波形是我调试时辅助使用的。
8 f. {6 F3 P. [3 e! Q0 E
, g8 r( i% b" v8 t% x, v1 Z8 r第一路表示计数器的计数变化,显然是单向向上计数模式。, `$ B( s( N; J
% @: r5 v, e0 n8 j! |* j第二路是TIMER1通道1的PWM输出波形。
0 Q; b+ _! I! C1 H
, |. j8 v, Y- p7 j5 A! c9 d; @: ~第三路是我每次基于DMA实现UART发送时拉高拉低的波形。平常管脚电平为高,在实现DMA传输过程中拉低。$ H8 q' V. Q2 `8 { _4 n
m8 I- _9 y; e- c/ b) A. u, Y$ }* Z
' B1 l7 o1 }- m" E4 q4 n; m
如有侵权请联系删除
% ~! C/ u1 a! B; {转载自: 茶话MCU' N0 W3 y+ z" Z; c% {. q& i
/ V) u J5 I! n & ]# q. D$ @1 R6 Z, l2 S& b e
a u& n; W7 ]: }) i7 _ |