! i" G+ v1 C% F$ ?7 Z$ o- [$ y* d) x, U9 n2 X. d2 ]0 Z/ n
1 H- |7 I, m- G! P, S很多STM32芯片里往往内置了专用的ADC通道,比方用来测量Vrefint,VBAT的分压或温度传感器的输出电压信号。不同系列所内置的模拟信号通道可能有差异。这里以STM32G4系列为例,它内置了对应于Vrefint,VBAT的三分之一分压和温度传感器的输出电压的专用模拟通道。
" R) j9 f: [; W' x k! ?5 W) |1 O+ y
g S4 |' ]4 R9 [6 g
( Y" L2 U, W0 n L& _% @0 u; Q4 S A9 D& J: Q
- U" z, S! q5 G' R/ E! c3 b
% Z e) q' U* q( X V( T5 O- }3 g( `
下面的示例就是针对上述3个通道进行ADC,并测量相关电压和片内温度,最终得到3个结果,分别是VRefint电压,VBAT的电压,片内温度。
0 j3 k% @: X" ]* {4 J, f; G% X: ^% y& G4 z) N0 j7 l
1 F" Q- I& u5 M9 D3 p
* t+ c. k y) ]8 t
实现过程是这样的,大体分四步:【有点点麻雀虽小五脏俱全的味道图片】
4 t7 C0 ~4 w, Y0 n- m' A2 j
% |* `: G- U9 p; S1、TIMER1 更新事件触发ADC的转换;
3 s8 g0 G+ t+ d- o0 W8 @" N% b3 P# x* P# k1 G8 X! Z" C
2、CPU基于EOC中断获取ADC结果;6 F8 K1 f( g/ u: i5 C5 \- q- H2 r# n
; d4 b1 U) T- l
3、对ADC结果进行换算,得到电压值和温度值存放在特定内存位置;4 g) k# ~1 J7 X4 M9 W
2 X; {; ~$ R( T$ M
4、基于DMA传输通过UART将最终结果在串口终端显示;7 q' a ^- V, R
* @1 q8 @- N& X8 V4 b N6 N! U其中,TIMER1的CH1输出PWM波形,其更新事件做ADC的转换启动信号。每次的TIMER更新事件触发ADC,3个通道扫描方式转换。这里的UART使用片内LPUART,使用它主要是考虑它跟板载虚拟串口直接相连,没有其它特别用意。
, d% a4 G5 \. ?: ?7 r/ `3 x z8 I& d( [1 q
3 R& o/ J5 P$ y8 I1 [' O; H! f& b- P
我使用STM32G474Nucleo板来进行下面实验。其中VDD=3.3v,VBAT与VDD相连。另外,ADC模块的参考电压也是3.3v., H- h# x, l1 g( o. y
) R9 y* W2 B5 M W% f使用CubeMx图形化工具进行配置,先看TIMER配置:
: ^# F+ L9 T7 l
) F4 l/ k, P3 \' z0 p1 k: N# J
# n- i0 l! j0 I' j, u% A
5 W; k; u4 n5 F8 p; `& Q1 Y2 F( _再看看ADC的基本配置:
3 I. @9 d: O5 Z* f: b
& q: U' ?6 x/ O( ^+ ?* H
0 |* S3 h, X7 h0 E4 b( X8 L# X! z6 U- K, V& \) B
LPUART的基本配置:2 L- L/ M6 t" `9 ^2 l# R7 H! p
2 ?$ G5 L. j& M# L3 p n7 r+ A3 `; k$ ?9 _/ \
4 \9 @5 K; \* B$ T因为要使用ADC中断和UART的DMA传输,记得做ADC的中断响应使能配置和LPUART的DMA配置,这里只使用UART的TX DMA功能。
/ [/ `! Y: c2 ~. p, D- n1 o$ g9 j' f7 l! ~' C- N+ `/ F0 n
/ p( l$ M( u# g$ y e0 F* D9 M& D
; h1 k0 O1 s) G0 Y F使用CubeMx主要配置主要是上面这些。) H s. h5 ^: u2 R6 L: T8 J0 e7 p9 `
- q) g, `3 r5 p- f$ I2 u
在组织用户代码前,先简单介绍下片内温度传感器的内容。该温度传感器针对不同温度有不同电压输出,其输出电压跟温度呈线性关系。ST公司针对片内温度传感器在两个特定温度【30℃和110℃或30℃和130℃】、基于特定参考电压【3v或3.3v,不同系列以数据手册为准】生成了1组校准值并存放于片内特定FLASH位置。
0 W5 K! W: w7 @" f" e y# _* ]2 S. m$ |5 c) ^4 m0 A) J
STM32G4系列的校准值是在参考电压为3v,30℃和110℃条件下的两个值,在数据手册里还给出了校准值的片内存放地址。
# T4 b- K' x8 P2 p l1 h
' _* E" b$ [3 F
; W2 ?$ h5 b% H! i+ V. \* N6 b8 s2 T! \1 e7 n+ j* d
针对这个温度传感器的使用,ST公司在参考手册里还给出了计算公式。其实,有无这个公式无所谓,我们不难自行推理出来。【TS_DATA代表某时刻测得的传感器输出电压对应的转换值,TS_CAL1/TS_CAL2分别表示在30℃和110℃条件下基于传感器输出电压的转换值。】# h9 D9 F/ F8 u& v% Z. m7 _) S# A
% |% m* w7 W& y3 D. g! D$ G/ k
1 [! G; b6 N+ L1 ]$ R" L) h! Q* e3 |
. S3 T& H9 [, d* `另外,前面提过,ST公司在手册里给出了温度传感器的两个温度下的校准值,但要注意生成校准值的ADC模块所用参考电压跟我们实际应用时AD模块所用的参考基准电压可能不一致。如果不一致,就必须将ADC值换算成同一基准参考电压条件下的数据。目前在ST手册里也特别强调这点了。我把上面一副图再贴一遍于此【见黄色语句提醒】。
0 f/ b; i" J ^5 u3 b# l
/ F* ^" H5 M/ \4 M$ }
: D+ w+ Z Z! N+ i z5 D- y
1 y" S6 i7 x; M* Y9 d关于这点,我们也不难理解。同一待测信号、同一ADC模块在不同基准参考电压下转换值往往是不一样的。见下面示意图加以理解。* C+ A5 i. P2 U6 e; M
! I& S0 l8 o$ a1 ^3 G: `
K8 S! P" |: o) i
" u; s8 x- a) J, ^完成各项配置后,创建软件工程。添加必需的用户代码:1 N3 N9 x8 ?7 e2 O0 O
0 b0 e) H n) \$ d/ `+ T' R) p- #define TX_Timeout (9999)
! u1 E/ H- q5 g% v% X* s, d - 0 g7 m/ k& | R( v
- #define TS_CAL1_ADDR (0x1FFF75A8) //用于计算温度传感器数据
' f! A& `( V* g% \. P- b+ u - #define TS_CAL2_ADDR (0x1FFF75CA) //用于计算温度传感器数据' D. b2 z2 R/ Y! g$ z# J
- P% q6 U' v. x- #define size1 (40)
1 E: j( ^) I ~0 ?% U - 2 D* {( B/ B- }; d
- char WDVol[size1],BatVol[size1],InVol[size1];
2 [$ E2 M5 X" b. g - 9 C+ ?+ ]! l4 a4 C5 [5 r
- uint16_t ts_c30,ts_c110; Z) s: G) x' S/ [# A
0 g& ]/ D8 ~! |! o/ B2 W- uint16_t ADCResult[3],convCNT;
# v- T# p7 W+ c' M; b6 B - ( Y k8 \ x0 Q3 v. Z0 \, H
- volatile uint32_t Completed,EndofCon_Flag;
% N" H4 u# p: Y- ^* ? - . c9 ^) q# L2 r E. @% Q: L
- float VBATVolt;//存放BBAT电压最终结果
4 I2 y5 p. A3 j; @ - float VRefint; //存放Vrefint电压最终结果! A% u9 P! ?- _! M- F% U# O
- float Temperature;//存放片内温度℃最终结果$ w& g I% P$ ?3 q; Q- i( x2 c) `
- 3 h D' b d& U- Y8 i& |5 `( j
- int main(void)/ f+ j8 S* T3 K; t- H% Z
- {+ E3 E5 Z% m1 w
- /* USER CODE BEGIN 1 */
0 ?* v W+ Y ^' F
5 h% q+ i, C4 g. _" u, v- W* e- X- /* USER CODE END 1 */) \ {! I7 _4 l' v* E
- 9 f* Y8 g. T/ b6 Y
- /* MCU Configuration--------------------------------------------------------*/8 V. k7 o6 c [; Y7 N
, G! g4 B/ X w" x! p( t8 j( D- /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
" h/ [" p# _" L. w: R5 o1 X- i/ N - HAL_Init();
$ \! T9 x( Q& O( C' r - 7 `2 ?+ }0 o. s" E x
- /* USER CODE BEGIN Init */4 h( P' a" w0 `1 M( a9 C
6 T. {. n3 U$ l u7 Y) T' y3 A- /* USER CODE END Init */
7 q! @" U0 n) V( |! E
2 @) e$ r; p3 i- /* Configure the system clock */
- F: d2 ~( z' j: P ~7 W$ _7 C - SystemClock_Config();) I$ p* W# i6 c) C# m' r* m7 p
- & k9 o3 `) D% c
- /* USER CODE BEGIN SysInit */
- P8 l8 l6 a1 p9 R0 g. Q
9 f$ R- l5 v7 r2 Q" `- /* USER CODE END SysInit */
: t# D+ A1 b6 z: ]) |- W% J- ~ - 4 @, P/ [% `6 P
- /* Initialize all configured peripherals */, m- H) s& G5 j# R9 ?7 ~, p& x
- MX_GPIO_Init();
# |" P( w4 I; {) Q' G - MX_DMA_Init();. D! {: e; d* y7 @. ? b
- MX_ADC1_Init();
+ G* }/ k: [& r: b* c- h - MX_LPUART1_UART_Init();5 w1 u1 h9 u0 ?$ f }- S7 O3 T* k
- MX_TIM1_Init();$ b$ g b$ I# n$ F
- /* USER CODE BEGIN 2 */
- i$ U" _9 H8 b2 a/ a( d, |* W! [
) x% x; N" R1 {" ^9 a) @- ts_c30 = *(uint16_t *)(TS_CAL1_ADDR); //读取30℃时的ADC校准值
/ H! J* |" b4 s - ! a# u4 i8 @% a' U+ h; `2 T* H
- ts_c110 = *(uint16_t *)(TS_CAL2_ADDR);//读取110℃时的ADC校准值
6 ]0 r' d- j! C; o' a
\; s% p* i/ K6 y0 |* d6 O- HAL_ADCEx_Calibration_Start(&hadc1 , ADC_SINGLE_ENDED);//ADC校准' W8 I$ c2 }2 w. `
- 3 ~3 r9 l. o% q) I* d2 Y* H& q
- HAL_ADC_Start_IT(&hadc1);//启动ADC并开启转换中断% S$ t3 o, d( Q& y
' r8 F1 i$ \( t3 d- HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);, E/ m- S( [" b
- 9 e7 H/ C3 ^) Q b: E: E$ L
: J) c# H% q% Y/ p. m" a( B ~7 R5 B- /* USER CODE END 2 */
8 H) M: t/ h. ?9 n, R% T2 G: F$ L) Z
9 e2 @3 Y9 w& A) M& o( ~- /* Infinite loop */
& K! P! T$ ?' y - /* USER CODE BEGIN WHILE */
* q& d. m3 `9 N- D - while (1)
5 _/ V0 X& u: v I: g5 g - {9 ]+ L! a! q5 h, h8 y
- /* USER CODE END WHILE *// f6 W1 m/ K7 {. n5 q6 A' E% T
- % H3 L0 A/ r+ U; G
- /* USER CODE BEGIN 3 */+ t5 B ?+ F1 G
- , R4 U9 A; Q* j# P
- if (EndofCon_Flag!=0)
2 P- i# ]( _; Q; k0 }# H - {' V. z: t. J X% H' @
- VBATVolt=(ADCResult[0]/4095.)* 3.3 * 3.;
) X) n! Y( Z4 m5 ]" i - ( M6 v( w7 d, p6 G
- VRefint=(ADCResult[1]/4095.) * 3.3;
+ x, _( U& x7 e+ M4 B - 4 D$ n% R$ e5 _, |- J
- Temperature = 30.+ (88.*(ADCResult[2]-((ts_c30/1.1))))/(ts_c110 - ts_c30);
A& m6 x0 p* r5 h# K7 O: | - + e5 D. W7 F$ [9 a, c- \9 m
- EndofCon_Flag=0;
" K8 @ S. [2 w4 z# Y
) E7 Z+ l, y; N/ `% G0 i$ s, H- //HAL_UART_Transmit(&hlpuart1, (uint8_t *)WDVol ,sizeof(WDVol), TX_Timeout);+ ^6 O" h7 ?& q) }# L8 ~
# v* h: l/ F! `; s- HAL_GPIO_WritePin( GPIOC,GPIO_PIN_3,GPIO_PIN_RESET); //for auxiliary test3 }9 z. u0 ?; o: [/ \
- 0 `3 @9 S% @- D4 @5 y
- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)WDVol ,sizeof(WDVol));; J( }4 d( ?; T% A7 D$ N
- 8 K" c7 u& E g7 E3 ]" Y
- while(Completed==0){}
3 h8 L6 {1 ^% b - Completed =0;
9 J& k: ?7 Z7 X - N! `- @: \8 C% K
# F1 X. K0 p& v5 ~4 s8 G- //HAL_UART_Transmit(&hlpuart1, (uint8_t *)InVol ,sizeof(InVol), TX_Timeout);3 M. \- {3 j1 o+ H" J; T
- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)InVol ,sizeof(InVol));" G5 L9 A2 A% n
- ( J c" h5 c/ @. u: Y3 V$ L
- while(Completed==0) {}- b/ M) e3 ~$ F! I) j
- Completed =0;
. W5 T: p3 e' z$ ^
, Y6 {- q$ G0 [3 f- " r! l/ l6 [% _
- //HAL_UART_Transmit(&hlpuart1, (uint8_t *)BatVol ,sizeof(BatVol), TX_Timeout);
0 t/ m1 o( x" Z7 q6 Y# m - HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)BatVol ,sizeof(BatVol));+ E* [- k! \) ]6 X
/ y/ Q; C0 R7 _# J' J# [, Z8 |) }% D- while(Completed==0){}& J9 L& U. `: ?; g
- Completed =0;5 v* E0 }0 v! F" E
+ h" T# [% m; G1 E' [% k- HAL_GPIO_WritePin( GPIOC,GPIO_PIN_3,GPIO_PIN_SET); //for auxiliary test# h% K0 R x+ s7 \8 S
8 ?" T" S6 e# i) @0 R- } 4 k% H0 R( k" i1 B
- , Y! Y @- p6 V9 t! g/ M
- }+ D% U2 V( Z) E1 T3 ?9 u( x+ m
- /* USER CODE END 3 */
, x4 }3 H; ?+ h6 c- Z+ e# c# j2 O - }: V5 e4 y. C) G/ [
3 q( s) R' r0 t7 ]- //ADC EOC 中断回调处理函数0 f* e, D s9 J# ~* f5 |3 O
- void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)! [8 A% l. V/ u2 M3 W9 r
- {
% k- C# q( `& |5 ?
5 x; E3 N. u5 P% r- ADCResult[convCNT]=HAL_ADC_GetValue(&hadc1); //获取转换结果并存入数组
2 n8 j' n% [5 A8 Z; [& \ - . w( P' s) U* f5 T( m, \2 f# k
- convCNT++;
1 E" \/ {1 k2 `$ Y' }0 ` - 7 G/ u+ `! H `, h5 Y
- if(convCNT==3)
6 Y1 e( Y1 T$ L9 N* d. O - 4 g# v3 i! q4 ^( L# B- u3 o
- {
1 o$ \7 i) a5 y8 f, a9 d1 u - convCNT=0;& O0 R, H& a( a6 n2 M7 v; T N
- 3 Z. Z, z4 x5 B
- EndofCon_Flag=0xff;# j+ @9 R9 l- w* o% J! M1 [
- 0 A/ {/ L5 ^: b) ^- |
- sprintf(WDVol,"Internal PN Temperature: %5.3f \r\n",Temperature);
" ? y7 o2 t; J - : G$ t ]. @0 e1 c4 H ?
- sprintf(InVol,"Internal Reference Volt: %5.3f \r\n",VRefint);
9 i: i+ b. V% z9 |8 w; P) W5 f
7 @# j/ c* k1 q% q* R- sprintf(BatVol,"Current Battery Volt: %5.3f \r\n\r\n",VBATVolt);! z$ o9 q; V9 o, K4 G% f, t
- ( a( ] U' Z$ U2 _9 }
- }
! e% e' S! K; H% b8 f - ( Q8 |8 Y( b1 ~4 y3 n7 k: _
- }. {0 H. r+ ?, a* E% Y1 E
+ o4 J& J6 [" B- W- //UART DMA 传输完成中断回调函数
: w& V6 ^8 d1 l' \" t - ; S8 ~& K4 X) l, ^( y: r! A
- void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
, q4 A! U- J, `! t+ Y - {" Q' |2 A( a( h
- : }0 s3 [" m% U! [* j1 [
- Completed=0xff;
4 u, K/ Y- t+ }" D/ D/ c - }
复制代码 ' g% R4 }8 r- a5 N! t9 E N: L7 z
基于上面的配置和测试代码,我们就可以看到最终的结果了。定时器周期性地触发ADC,每得到3个ADC结果就进行数据处理,然后通过UART以DMA方式传输到串口终端。注意VBat电压是测量结果再乘以3得到的。/ q9 O' h7 [* x7 L6 U3 m
* C' p, F* L/ V
+ M7 [1 b6 {, I- \9 h/ \7 G% T) k5 C! U
3 G8 t) I( f1 J$ F3 f
3 U, }2 w4 M, D+ M; l针对上面的应用演示,最后给几点相关应用提醒:4 l3 ~) F% O, p6 H# c' S5 G$ g) |- Z4 L
. ?+ x4 E) J# [! k2 ` _2 h1、针对温度传感器做测量时,校准时使用的参考电压与实际应用不一致时要做换算,换算成相同参考电压的数据后再做计算。这点前面也提过了。. P7 `+ J3 J: ^& w9 V/ l# M* p
+ I: n/ L: M Y# ?) ]' n7 {
2、使用TIMER的TRGO触发ADC,如果选择类似比较事件、更新事件来触发ADC时,此时ADC对触发极性的选择是无效的,或者说ADC的转换仅依赖于触发事件时间点。如果是选择TIMER的Ocref信号作为触发源,此时ADC的硬件触发的极性选择是有效的,可以是上沿或下沿触发,甚至是双沿触发。这时就得根据需要选择合适的触发沿。【可以进一步阅读本公众号文章《STM32定时器触发ADC的时序话题》】
$ r9 U3 O a! i) ]- Y6 r7 C) {' i, f9 I, R W1 B6 q8 ^" n
3、这里使用UART的DMA传输依次显示三个结果于串口终端,三个启动UART DMA传输的函数须保留适当时间间隔,即等上次传输完成后再启动下一次传输,因为这里每次传输使用的是同一DMA通道。否则没法全部正常输出。比如若把上面3次UART DMA传输的代码改成下面这样子:8 N' V5 \8 _( F, m5 ^0 I! O. X8 g
( ~, D( K X7 M9 f" D& b
- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)WDVol ,sizeof(WDVol));' J2 Y. D; S+ o) B0 ^3 q5 F
2 E* T F8 i' S( G% P- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)InVol ,sizeof(InVol));- k; h: Q- f3 n r
" F" Q) ~6 U! `. e& [# X- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)BatVol ,sizeof(BatVol))
复制代码 , e: z* y6 G( m* h
这时输出结果会变成下面的情形,总是只能看到一个结果的输出,即第一次启动的DMA传输结果。3 Y: s# M* s0 Q1 j
1 [0 u" d* x" v% [- E' f f1 }2 @7 }; G" h
: r2 _+ l T9 R- F1 v如果想省事点,直接在相邻2次DMA传输间加上合适延时也行。我这里根据DMA传输完成事件来决定执行下一次发送。在DMA传输完成中断里设置Completed变量为非0值表示当前一轮DMA传输完成。
% V8 N! K* z) A0 o$ o4 j8 T, R% _- _* q4 d* A$ L
4、对于那些在中断和主程序里都会被访问的变量,记得将它们冠以volatile。
: Z6 c% h9 J8 ?. H7 l ~9 ?0 ^+ b
9 e6 u# Y3 u3 y' q( A( {) r! t下图的三路波形是我调试时辅助使用的。
" ?, }- U( P# u
5 \. L. [# K& k* \7 \- v- Q第一路表示计数器的计数变化,显然是单向向上计数模式。
4 @/ X$ l2 P$ I1 r! X& b. _) A* {3 m; e! a- d; o0 P8 {
第二路是TIMER1通道1的PWM输出波形。5 U2 n$ P. U6 R8 c
8 }; A k( u6 B0 Q
第三路是我每次基于DMA实现UART发送时拉高拉低的波形。平常管脚电平为高,在实现DMA传输过程中拉低。: t3 P& a( u5 U3 Z5 z
; q! r; n a F1 S" m" ?, r
' C, D, `% k) P# o$ z6 I) K* o
+ G0 k E Z; U5 N4 e' r. k
如有侵权请联系删除
: D' \* r. x5 B2 d9 M1 Z9 T* m3 A转载自: 茶话MCU
! a1 M$ e% y1 n; l) a) O* z
5 `( \+ {% j/ F6 M1 G
7 s1 B$ d" s6 V( f& u8 |
4 k/ _+ J% B7 c+ g4 D9 n |