1 B/ H E; G' Q7 Z, z3 m* S. f% a# a" e
5 Y( L7 E4 N5 O+ _" j
很多STM32芯片里往往内置了专用的ADC通道,比方用来测量Vrefint,VBAT的分压或温度传感器的输出电压信号。不同系列所内置的模拟信号通道可能有差异。这里以STM32G4系列为例,它内置了对应于Vrefint,VBAT的三分之一分压和温度传感器的输出电压的专用模拟通道。) `& `$ g2 l" P7 e+ h
' u! c7 l3 S2 v4 N! e8 W' Q; |
+ Y0 x9 L; }6 x2 s
* \3 _& d' G" x/ H( ^
( y ?+ P3 ~+ F4 Q1 l/ o
0 T* L# s8 U4 k# [8 x* `: Z下面的示例就是针对上述3个通道进行ADC,并测量相关电压和片内温度,最终得到3个结果,分别是VRefint电压,VBAT的电压,片内温度。
0 X' U$ |" h4 @. o. B$ i
* c! k/ @9 V% I+ J" C! T9 J2 ~; ^$ e; {3 n2 s+ W) U$ _
. ]& u6 q" E' T. C实现过程是这样的,大体分四步:【有点点麻雀虽小五脏俱全的味道图片】
; m w" w+ A* ?7 ]3 ^# R+ R9 J: u! g! E+ T7 e; M' @5 i- k E
1、TIMER1 更新事件触发ADC的转换;
% ^7 g; W' `7 }. v$ e. B+ Q6 }7 }9 p$ R& m9 Q, e# x( g3 l
2、CPU基于EOC中断获取ADC结果;. ]) x" `3 r/ b ~
. c* C! d1 h8 N: Q6 I! \3、对ADC结果进行换算,得到电压值和温度值存放在特定内存位置;
# @9 i6 e; N& p8 f9 U, b7 m8 n# h" F- C$ p5 ?, P
4、基于DMA传输通过UART将最终结果在串口终端显示;
+ L {4 c' i/ _ T
) D8 k4 c' F" `9 P3 F8 V) U( F其中,TIMER1的CH1输出PWM波形,其更新事件做ADC的转换启动信号。每次的TIMER更新事件触发ADC,3个通道扫描方式转换。这里的UART使用片内LPUART,使用它主要是考虑它跟板载虚拟串口直接相连,没有其它特别用意。
! r" j9 d0 Q+ f6 z' `% u9 X& m* C! {7 i) @
; ]3 c1 v5 h7 u- c1 ~/ @, I, |2 x3 M) U, k1 H
我使用STM32G474Nucleo板来进行下面实验。其中VDD=3.3v,VBAT与VDD相连。另外,ADC模块的参考电压也是3.3v.
" J2 P7 ^1 ~' d2 t" |
+ z k5 P7 z3 }& h! w1 ]使用CubeMx图形化工具进行配置,先看TIMER配置:1 o* _$ G: b& m: t0 L' u
9 R# J) e# M- [: a- u, ~$ C
& h. H7 L5 r- U- n: [3 j) b2 u1 b2 k3 i3 _8 y6 h" Q
再看看ADC的基本配置:" j8 A! S$ W* m. i' z! D
3 { ^' @* S3 g5 V8 `
7 \3 e* ]* n, ^3 W r
& x T0 B8 B" _3 Z3 q5 u! }LPUART的基本配置:
) e9 i5 g' ^" F Z! G% ]/ |/ m
7 u/ ]8 h+ Y$ n, |
5 x; b7 n3 \2 j, _6 o7 z
/ h* q. ]4 W2 x( J0 f因为要使用ADC中断和UART的DMA传输,记得做ADC的中断响应使能配置和LPUART的DMA配置,这里只使用UART的TX DMA功能。3 q, X: |4 N( T. `. Z0 e6 n6 z
8 j; j! k4 s2 R9 ?9 L/ X1 U5 H
# }) O/ z# I) |* W7 ^5 Z" R) m: ]( ?. X
使用CubeMx主要配置主要是上面这些。0 Q: m- O) h7 I4 Y/ s/ D4 I
! l! _. ]' h! Z0 y
在组织用户代码前,先简单介绍下片内温度传感器的内容。该温度传感器针对不同温度有不同电压输出,其输出电压跟温度呈线性关系。ST公司针对片内温度传感器在两个特定温度【30℃和110℃或30℃和130℃】、基于特定参考电压【3v或3.3v,不同系列以数据手册为准】生成了1组校准值并存放于片内特定FLASH位置。7 y! C& Y% j: I8 I, M# W
6 Z) Z- v* J& P( J4 D" g, `
STM32G4系列的校准值是在参考电压为3v,30℃和110℃条件下的两个值,在数据手册里还给出了校准值的片内存放地址。
( J/ k" a0 F1 y6 T7 V# R1 W3 b7 @6 j8 E. w# g( A
; ^& r4 k0 m! L% e. M# T/ f8 ]7 z( S2 W/ {3 i- W" w [8 R
针对这个温度传感器的使用,ST公司在参考手册里还给出了计算公式。其实,有无这个公式无所谓,我们不难自行推理出来。【TS_DATA代表某时刻测得的传感器输出电压对应的转换值,TS_CAL1/TS_CAL2分别表示在30℃和110℃条件下基于传感器输出电压的转换值。】- T/ r x9 E2 _, p0 k4 R: u
# }5 ]$ D1 v& g$ s0 Y, g( m- x4 g3 q& }3 i2 k. j1 {$ K2 n
$ K7 F- f+ | ~8 e' ^另外,前面提过,ST公司在手册里给出了温度传感器的两个温度下的校准值,但要注意生成校准值的ADC模块所用参考电压跟我们实际应用时AD模块所用的参考基准电压可能不一致。如果不一致,就必须将ADC值换算成同一基准参考电压条件下的数据。目前在ST手册里也特别强调这点了。我把上面一副图再贴一遍于此【见黄色语句提醒】。5 r# H* W9 @4 W0 p ~. `( r
4 |, h2 e. m9 m. j! u1 V
3 Q& J; q3 Z0 I% p+ b2 W8 V) @7 u6 O4 \. b& D
关于这点,我们也不难理解。同一待测信号、同一ADC模块在不同基准参考电压下转换值往往是不一样的。见下面示意图加以理解。5 D0 Y" M) ~; X2 @5 s! @
& B9 k1 S$ q/ Y% j6 R) h
7 _+ \& E+ `* \$ g- |9 }
0 M, H$ y* E% a5 f
完成各项配置后,创建软件工程。添加必需的用户代码:
o. r! K+ Z) p/ j* ^6 d
4 x) G q, c. u! E- #define TX_Timeout (9999)
6 }6 k& `' f4 F, f& s2 z - + u' ~/ V+ X2 W, J2 T* D, L/ E
- #define TS_CAL1_ADDR (0x1FFF75A8) //用于计算温度传感器数据% q8 a# `( ~: s0 `
- #define TS_CAL2_ADDR (0x1FFF75CA) //用于计算温度传感器数据. H4 m$ G4 k. d$ E; u
( c- I0 |& c- b1 m: {. F: n2 B6 f- #define size1 (40)
3 a) a3 I4 u) V
0 ~, I! C' d" m8 I+ `- R$ y- char WDVol[size1],BatVol[size1],InVol[size1];# U& P' N9 W6 O4 \/ B6 D
5 D, M, R3 ]5 P; k/ K2 Q- uint16_t ts_c30,ts_c110;) k V1 Y3 U/ H8 ~; W* [
- . s D1 v5 f1 I/ S6 s2 @, X h/ w3 |
- uint16_t ADCResult[3],convCNT;
1 I* d/ v- P7 ]% Y2 p9 r - ( m1 F; E( Z" m- ]3 B9 b8 l; X
- volatile uint32_t Completed,EndofCon_Flag;/ z6 _. X* F2 K& D) A2 k
5 W7 y% v$ \; I: P C8 j; U! v- float VBATVolt;//存放BBAT电压最终结果+ d/ Q9 @6 ]$ W5 @( U; U( F, q9 J
- float VRefint; //存放Vrefint电压最终结果1 R& h+ K* b; A4 N
- float Temperature;//存放片内温度℃最终结果
0 g" c' k% Y) H: a
; `: j: J' e/ L0 ^- int main(void); I1 o+ L+ C# }- g
- {& E1 r2 q" P' s" h. @
- /* USER CODE BEGIN 1 */7 g! e- J4 Q1 p- b- S
' _( c" H9 L* W5 o; Z: Y" m# X5 t- /* USER CODE END 1 */
& H4 t( I7 |$ r6 e, I$ O0 x3 J& ` - 8 g+ c) R4 l# @" f% S
- /* MCU Configuration--------------------------------------------------------*/1 X6 ]! b1 e# V& C8 r% F
- / A* U7 `9 i* @3 j6 a7 z) L
- /* Reset of all peripherals, Initializes the Flash interface and the Systick. */0 A% L$ V0 v7 s5 I$ X; {% P
- HAL_Init();% X. E, V" ~) S8 j
- & ?2 s/ |8 b' h- Q( ]: w
- /* USER CODE BEGIN Init */+ \' q- ^# u/ a( J& ^! v9 u+ i, A
- 3 P. J6 g3 B9 g- s3 ^4 z
- /* USER CODE END Init */. J, X) U5 O: ?6 J- t6 [$ q: W6 C
e0 Y: ~* ]# o$ f A8 ?& e" S- /* Configure the system clock */
/ j, B9 C9 i! n, W% m, P - SystemClock_Config();
- i6 h. M& r/ r5 J6 {* u
0 ^; a: a( J9 i$ n- /* USER CODE BEGIN SysInit */
4 l! g0 d* Z W8 D8 L8 m
$ R ~; U3 s A2 j, b- |- /* USER CODE END SysInit */, ^: e$ u/ o7 t, S3 I4 K2 t
2 }% g; u0 b- x( X- /* Initialize all configured peripherals *// P$ b* H$ X: q4 _0 I) V3 o
- MX_GPIO_Init();. l4 O$ n: k' ]) o
- MX_DMA_Init();
' E1 I6 |4 ^7 \9 s" g - MX_ADC1_Init();
% e5 c1 K4 @% [2 M3 Q" V - MX_LPUART1_UART_Init();
0 [! K9 l% B3 k( ]$ L; N% h - MX_TIM1_Init();
7 B" l( |1 i& X$ a# O1 j - /* USER CODE BEGIN 2 */0 Z7 \. ?0 j! k/ I- l8 F
- x9 d; b: v- n6 E5 R9 `* f3 p* e; v
- ts_c30 = *(uint16_t *)(TS_CAL1_ADDR); //读取30℃时的ADC校准值
' |) _% k( `/ Q2 y7 k. V - - K2 k0 \; g4 y) I8 C* z: ^1 w
- ts_c110 = *(uint16_t *)(TS_CAL2_ADDR);//读取110℃时的ADC校准值
M4 C5 I( z7 `" f5 C* i4 m5 s! x
5 o/ I) }2 [2 x4 A" J6 e- HAL_ADCEx_Calibration_Start(&hadc1 , ADC_SINGLE_ENDED);//ADC校准
P3 d" S& Q- a3 S: J% B1 ?
. D' |+ a. v" G- HAL_ADC_Start_IT(&hadc1);//启动ADC并开启转换中断
2 U }9 p) D& b
) v+ Z! Z- d4 M5 A, p' ]4 Q: n6 S- HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
1 Y: x8 r& R, @ - # h" A) c ]3 u' y2 m
- ) Q/ q, C& |6 y b M/ z) ]" z
- /* USER CODE END 2 */
+ {: v( t1 F2 g. Y
- e( p; _0 p5 O$ z A- /* Infinite loop */
0 @$ J( v- u& v - /* USER CODE BEGIN WHILE */
! h5 |" u. z5 w - while (1) r7 C6 v( J% m! q2 s# B
- {) c) W6 w+ Z% i- o6 v, {! u
- /* USER CODE END WHILE */- ]$ O3 f `* g# F) R+ _
- 8 L- ^+ [7 U& K
- /* USER CODE BEGIN 3 */
. \2 y6 x- e# ~: {9 | - 6 T8 L5 I- ?3 q0 W3 U9 M/ r8 t
- if (EndofCon_Flag!=0)
' ~ b% C6 _) W% h5 R - {! z" X: W+ c1 O4 n) d
- VBATVolt=(ADCResult[0]/4095.)* 3.3 * 3.; 6 i( }; H2 _4 n
, i6 B; N6 n* k8 q* d5 C- VRefint=(ADCResult[1]/4095.) * 3.3;
5 P4 H$ D3 W' w; L
5 w) G% U- o4 l! W7 R2 {4 m- Temperature = 30.+ (88.*(ADCResult[2]-((ts_c30/1.1))))/(ts_c110 - ts_c30);$ C! G; g3 u, G, r1 j/ g/ p
5 Z7 G( w1 G/ Q2 Y3 C3 b" ^9 {7 s- EndofCon_Flag=0;9 k5 k+ c5 w# H; i2 k
- - Q8 `3 H4 Z/ z
- //HAL_UART_Transmit(&hlpuart1, (uint8_t *)WDVol ,sizeof(WDVol), TX_Timeout);8 K& `# D9 E' c' i8 b4 d
0 }) H8 F0 f' P$ Q4 p3 j' |- HAL_GPIO_WritePin( GPIOC,GPIO_PIN_3,GPIO_PIN_RESET); //for auxiliary test( k8 e+ T/ S$ C4 C, o- t
- * `7 h {2 }$ r4 v1 x; k) U
- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)WDVol ,sizeof(WDVol));
9 c9 M( Z! W H! m
, }& o: C8 Y$ U1 }8 f+ e- while(Completed==0){}# i: w( R' ~8 t% J! M
- Completed =0;/ B; D7 y1 M5 @8 p# O S/ H
- / [7 x4 d0 E' I7 d; e; [
- 3 z0 U- j# H2 Y1 { ~5 N2 r) x
- //HAL_UART_Transmit(&hlpuart1, (uint8_t *)InVol ,sizeof(InVol), TX_Timeout);( z7 }; X9 V: e8 ^( D
- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)InVol ,sizeof(InVol));
; z( z8 G# S% M! M/ P- @- |) k V$ K
1 y/ T3 @9 x2 G- while(Completed==0) {}
+ g. x& j: ~' E - Completed =0;6 R! A" a, U0 i& j, @' a$ ?: X
/ _# H8 l; k% ^! G* f" x
+ Q0 G' B. ^$ F3 @3 z. B* B- //HAL_UART_Transmit(&hlpuart1, (uint8_t *)BatVol ,sizeof(BatVol), TX_Timeout);6 Z. X( S- [. T" L
- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)BatVol ,sizeof(BatVol));
: J n# M* d, B, W/ e' `
) D. z' ]; m; y! E- while(Completed==0){}+ s/ X9 K- {7 V% O! I
- Completed =0;' i7 x2 B9 J( C. H% p6 t( c
) k' r7 C$ \" }( {7 J- HAL_GPIO_WritePin( GPIOC,GPIO_PIN_3,GPIO_PIN_SET); //for auxiliary test
" b: ]5 a/ t2 W( M8 T
& T2 f* A5 O3 ?2 M- }
& d( D6 \) r, ?0 @
# S+ i& y& n+ b( F- }6 o; Z2 q) m: Z1 P) _9 |8 N
- /* USER CODE END 3 */
, n- w7 V) ^( S8 }+ W& M! T - }4 b# Q* j& t4 G$ d% z
" C" j! z6 q7 {8 E9 ]4 B3 G- //ADC EOC 中断回调处理函数# D/ h+ |& O/ v- t
- void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)0 p5 [6 F. f7 [. T
- {
2 w3 F2 {! ^$ X7 z2 Z- S# J% [
8 h, K( ]% v5 W- ADCResult[convCNT]=HAL_ADC_GetValue(&hadc1); //获取转换结果并存入数组$ p5 M* L- S$ A" ?! L* a
- 1 @+ ]9 w/ C' u8 `
- convCNT++;5 g2 z" h9 [ X [ W9 N
# P ]5 x$ R$ n3 v" j- if(convCNT==3)
+ R& X3 P( r5 f
; R9 w5 y; T6 \5 _6 I+ k) H- {
7 | ^7 N: q% y+ y e$ i8 U - convCNT=0;4 Q/ G) k8 a4 @: n
- + ?* R! k) I+ e V* M
- EndofCon_Flag=0xff;
. ?! G. T; F9 o2 E. Y - , V; ?( F- B- C @4 t1 P
- sprintf(WDVol,"Internal PN Temperature: %5.3f \r\n",Temperature);
3 ] U' x; ?4 D' r3 \
g' z, _0 Y8 }' _0 x& T+ o/ u, }3 i6 h0 U- sprintf(InVol,"Internal Reference Volt: %5.3f \r\n",VRefint); $ o# {$ W1 v& b4 l
0 R. v k* ^! c9 V' l, K1 ?- sprintf(BatVol,"Current Battery Volt: %5.3f \r\n\r\n",VBATVolt);7 p, N& {' w* f5 ^4 v, m2 f& ^
- " n. Q6 G9 e, A9 `( @
- }3 X( S) v/ [* B
- 3 Q0 k) y) m9 N4 o, ~3 `1 b) G
- }
& E# G3 E+ `1 L
4 z3 K3 i6 \0 G6 x* l- //UART DMA 传输完成中断回调函数
* M) }+ F7 n6 r j - 8 E# v# ~% J8 P
- void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
# O9 U0 w. c3 `$ T4 S# y1 c8 O1 { - {0 P: V6 w. h6 { Z5 w7 g
; ~5 |7 M5 P/ \- Completed=0xff;
, w, k" ?; I8 j9 X# E& T9 S; m - }
复制代码
8 L, h' Q$ F5 e* k基于上面的配置和测试代码,我们就可以看到最终的结果了。定时器周期性地触发ADC,每得到3个ADC结果就进行数据处理,然后通过UART以DMA方式传输到串口终端。注意VBat电压是测量结果再乘以3得到的。. B' c0 _/ v* |$ g( j, k) r
4 V! B3 v4 g( l! m) U/ {% X
/ h: S7 U6 \6 w! t1 _3 G
f( C$ e- e, P& v1 y. G
0 o/ y6 O) \: B; t
( d. S8 N$ b9 }. l5 W针对上面的应用演示,最后给几点相关应用提醒:
) d1 x& r1 I) i0 C0 m* x p6 p* P2 P( z% w6 V
1、针对温度传感器做测量时,校准时使用的参考电压与实际应用不一致时要做换算,换算成相同参考电压的数据后再做计算。这点前面也提过了。" Z: ^( N4 x" O' Z1 L
$ T$ R% h: L2 z- B3 ]
2、使用TIMER的TRGO触发ADC,如果选择类似比较事件、更新事件来触发ADC时,此时ADC对触发极性的选择是无效的,或者说ADC的转换仅依赖于触发事件时间点。如果是选择TIMER的Ocref信号作为触发源,此时ADC的硬件触发的极性选择是有效的,可以是上沿或下沿触发,甚至是双沿触发。这时就得根据需要选择合适的触发沿。【可以进一步阅读本公众号文章《STM32定时器触发ADC的时序话题》】$ p7 y: i6 E+ w0 ~
- D' ?1 R! ?* |+ t: p% K
3、这里使用UART的DMA传输依次显示三个结果于串口终端,三个启动UART DMA传输的函数须保留适当时间间隔,即等上次传输完成后再启动下一次传输,因为这里每次传输使用的是同一DMA通道。否则没法全部正常输出。比如若把上面3次UART DMA传输的代码改成下面这样子: V4 [7 V: o2 Z+ f
% d, K8 k1 n: z4 B) ?" l( f; {
- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)WDVol ,sizeof(WDVol));/ Y i0 Q9 O, e" |9 [+ J3 Z
; N$ g) u' ^# k9 U/ }$ y6 y }+ F" w6 O3 @- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)InVol ,sizeof(InVol));* G7 j8 _1 [8 v0 p# S# Z
% b" F/ h2 l+ M5 ]3 }0 a- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)BatVol ,sizeof(BatVol))
复制代码 ! p3 t8 v6 b1 v% Z. o( V4 ]2 d
这时输出结果会变成下面的情形,总是只能看到一个结果的输出,即第一次启动的DMA传输结果。
2 W' ~) E2 ~8 I# W& p5 P$ i8 I( G1 r5 v8 w2 j; H- C
1 e( l( A; o p; K0 ? P
9 D1 n1 f: Y/ }4 u* y( @如果想省事点,直接在相邻2次DMA传输间加上合适延时也行。我这里根据DMA传输完成事件来决定执行下一次发送。在DMA传输完成中断里设置Completed变量为非0值表示当前一轮DMA传输完成。
' X% F( h, h. V+ J8 v( x
0 I' c0 G; c2 W5 T5 }7 L# }9 h5 W* n* A4、对于那些在中断和主程序里都会被访问的变量,记得将它们冠以volatile。
* g2 w- [" q+ s+ f( X* m- m
5 a/ w1 c) r$ n下图的三路波形是我调试时辅助使用的。# }* A* J, L) u
7 g* i% m1 d9 |0 L第一路表示计数器的计数变化,显然是单向向上计数模式。: {( w. ~: _, s! `7 v" I
1 U8 v; a' O0 t/ f$ l! ~9 `! s$ S3 Y
第二路是TIMER1通道1的PWM输出波形。
/ j% C! h6 |: S) X+ h; a' J
2 @) k( J8 s( ]2 J' w/ H3 g2 Z第三路是我每次基于DMA实现UART发送时拉高拉低的波形。平常管脚电平为高,在实现DMA传输过程中拉低。: j# `' e. d; k/ U( d" p
1 P6 ]' L( V6 p+ u8 c% R$ _5 k# w
1 P- B% i4 e" G' K
; ], a8 z' J- o; S8 d如有侵权请联系删除+ l8 Z. k0 a+ E, e5 `8 f5 Y O, |
转载自: 茶话MCU* W$ Y1 A; ^9 F( M9 e- r7 _
, L# a l% R5 U6 I: {9 i
+ ]! K* l! B6 a
$ D" K7 G1 `8 D% W2 @
|