5 q( m' I0 K2 q& C' i; `
2 [, i1 A/ w) D) w* C9 R/ p# w' X6 b& L1 v$ X
很多STM32芯片里往往内置了专用的ADC通道,比方用来测量Vrefint,VBAT的分压或温度传感器的输出电压信号。不同系列所内置的模拟信号通道可能有差异。这里以STM32G4系列为例,它内置了对应于Vrefint,VBAT的三分之一分压和温度传感器的输出电压的专用模拟通道。
9 }; }6 X. [9 s6 y. J# ~1 t6 @' j$ L5 r/ n5 e7 I2 E- a5 O
- D4 L4 b# O0 z; E( d" L$ S3 V
% i! V R. H8 ^) f, m/ D5 [2 {4 ?: F" Y- ~, |
5 V4 U) p [, [5 t; X/ ?
下面的示例就是针对上述3个通道进行ADC,并测量相关电压和片内温度,最终得到3个结果,分别是VRefint电压,VBAT的电压,片内温度。
) x1 [& ^+ F% J4 b. T* j: A' ?* }% j
( W; x; ?5 e3 n5 v9 x3 U8 ~2 ^3 L( z, i! ^
- i0 P1 v! `1 P8 Q# o* f v2 M实现过程是这样的,大体分四步:【有点点麻雀虽小五脏俱全的味道图片】5 D# d3 k6 w) [* d+ Q2 M
6 i2 q" X. G3 \8 a% v! a( i" E5 R
1、TIMER1 更新事件触发ADC的转换;& K" m* z1 z3 _ Q. M
4 I8 r& B/ h6 @8 N" C( B2、CPU基于EOC中断获取ADC结果;( j. j6 j! k/ Y2 B& |
% k" m2 N. c9 ~3、对ADC结果进行换算,得到电压值和温度值存放在特定内存位置;: `. B M. r7 T7 p: V! K: T
: Z% [/ I" k% Z: U, V$ \7 _; [4、基于DMA传输通过UART将最终结果在串口终端显示;! i/ ]0 u9 P7 B$ P1 o( {1 ]6 e
# M! ~. L9 g1 n3 f其中,TIMER1的CH1输出PWM波形,其更新事件做ADC的转换启动信号。每次的TIMER更新事件触发ADC,3个通道扫描方式转换。这里的UART使用片内LPUART,使用它主要是考虑它跟板载虚拟串口直接相连,没有其它特别用意。
6 z2 C0 H+ |8 q1 u) W0 x5 W8 |. k# b' T
3 k1 D- X0 j7 M0 w% D
& c) R7 g, v, n* m7 r0 b
我使用STM32G474Nucleo板来进行下面实验。其中VDD=3.3v,VBAT与VDD相连。另外,ADC模块的参考电压也是3.3v.
G) U/ y8 K0 C# t! C
a7 c/ y, U; b' l4 x使用CubeMx图形化工具进行配置,先看TIMER配置:# w# k, X+ f( {8 X$ ~& P
! n% r% b# Y* C) e4 S J
# d( t, i/ c% A# D4 N& m
1 E A: h' U/ h% {3 w3 b再看看ADC的基本配置:
/ Y+ l v4 x% v s/ \" W3 u3 F) Q6 {
c! D, C6 z1 R" ?! D( ]3 x
* e* I6 [* p4 {* `4 D& `& VLPUART的基本配置:% `. K7 [" m8 J7 q2 S- @. Y7 z9 Z4 H
5 g D- f# `- |* @" _0 Z
# i! @) L3 W' k: s- R" F, J1 b& F8 k' I: ^
因为要使用ADC中断和UART的DMA传输,记得做ADC的中断响应使能配置和LPUART的DMA配置,这里只使用UART的TX DMA功能。
" Q, F1 Y' Q8 x- c: I' J; ~% o- S4 h/ X
; \3 Z. u6 c! z/ i( @( g2 |6 ~8 u6 C. w3 M! k! K+ C
使用CubeMx主要配置主要是上面这些。& d% p" V F; D
8 I& }9 R& G, q8 U/ y2 _在组织用户代码前,先简单介绍下片内温度传感器的内容。该温度传感器针对不同温度有不同电压输出,其输出电压跟温度呈线性关系。ST公司针对片内温度传感器在两个特定温度【30℃和110℃或30℃和130℃】、基于特定参考电压【3v或3.3v,不同系列以数据手册为准】生成了1组校准值并存放于片内特定FLASH位置。9 n9 }2 Q2 D+ v( z" h. x+ s% l/ O
; R5 d; d6 l: g0 T3 A9 R% y2 N* HSTM32G4系列的校准值是在参考电压为3v,30℃和110℃条件下的两个值,在数据手册里还给出了校准值的片内存放地址。
. z, r' X" _; x; l+ _
! G3 q# S8 c8 u
6 c. H& E2 F ]
' ^$ B9 L: Z w# i, O针对这个温度传感器的使用,ST公司在参考手册里还给出了计算公式。其实,有无这个公式无所谓,我们不难自行推理出来。【TS_DATA代表某时刻测得的传感器输出电压对应的转换值,TS_CAL1/TS_CAL2分别表示在30℃和110℃条件下基于传感器输出电压的转换值。】( W$ T+ M3 N' P4 M9 A$ f0 k
9 d2 w5 ?/ x) r* h0 L! d
- `: ?6 P' }3 s/ W1 J4 h7 q
' L" a; s' a; O( ^) ]3 L另外,前面提过,ST公司在手册里给出了温度传感器的两个温度下的校准值,但要注意生成校准值的ADC模块所用参考电压跟我们实际应用时AD模块所用的参考基准电压可能不一致。如果不一致,就必须将ADC值换算成同一基准参考电压条件下的数据。目前在ST手册里也特别强调这点了。我把上面一副图再贴一遍于此【见黄色语句提醒】。 A3 a3 V- e4 K" Y. j+ ~
/ K4 N4 X/ M( ?! E) V# F: i
. A5 \4 @2 q0 H0 e6 u4 e. L- D
7 {/ e! V" P5 J0 ^8 b& v6 M
关于这点,我们也不难理解。同一待测信号、同一ADC模块在不同基准参考电压下转换值往往是不一样的。见下面示意图加以理解。
3 }; Y9 n1 I7 p
0 x- R8 R" B% Q- R' V0 ?
c7 E& z9 g& `3 h
: T9 J% q- v. R8 H; \: p5 q完成各项配置后,创建软件工程。添加必需的用户代码:; B+ G& Z. [0 z( U
1 y9 w) F; k" e9 k- #define TX_Timeout (9999)
2 G1 ~0 g1 K& g, u0 v% Q% b" I5 |
# b: l+ C/ \4 }1 F% \" j4 ^( _- #define TS_CAL1_ADDR (0x1FFF75A8) //用于计算温度传感器数据3 S9 L$ C1 A' m6 A$ v1 {4 m8 W
- #define TS_CAL2_ADDR (0x1FFF75CA) //用于计算温度传感器数据" I) h p3 p; g9 Y9 }5 G: A) \6 W
5 F! n. [7 z0 c0 d; E& @ ~& k" S- #define size1 (40)5 h5 ~7 j3 g8 w8 B+ v {$ [0 b
- $ @$ N7 F7 |4 d; K: x. @% t
- char WDVol[size1],BatVol[size1],InVol[size1];
; U8 s% p6 }5 z! |: ]
, U7 ^. A. V) i/ o9 f/ V- uint16_t ts_c30,ts_c110;
4 O: Q8 b! Z6 h2 A. U/ e7 H
: X+ \; j1 M- M! O: z" d- uint16_t ADCResult[3],convCNT;: k: D8 d! N1 h7 c' f
- 0 ]: n: W9 o$ ^) S6 ~% m
- volatile uint32_t Completed,EndofCon_Flag;
- a' l9 E1 E! h7 p
; _9 s% \# x$ w& @- float VBATVolt;//存放BBAT电压最终结果
. \: x& r0 O. }6 s: @/ o, q; ? - float VRefint; //存放Vrefint电压最终结果
! Q% m" w% @3 Y9 |& |; O - float Temperature;//存放片内温度℃最终结果) w6 y) X4 Z/ s& [
- & h7 K& j r2 | d4 }
- int main(void)
8 F6 G6 D- M3 \6 b - {
4 b4 R8 q5 @2 r B) X$ ^4 [3 y/ F - /* USER CODE BEGIN 1 */
: x1 f' U: \! q4 _- P: i1 v - i$ B2 L$ l" W8 n
- /* USER CODE END 1 */
) ]5 J j& I0 K0 g+ Y7 s' h
; Z3 h, r" {, V- /* MCU Configuration--------------------------------------------------------*/% w8 I' ~) m& U1 V- }6 Z& L! S/ I
- 4 l( \; a/ I( l
- /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
g n: n$ a9 S0 q6 R& Y# X- v - HAL_Init();% u: J# y- r& X. k" B7 N" v
- $ w8 c6 j4 A; }! B% j+ p" O
- /* USER CODE BEGIN Init */
! l9 p1 e" P6 F& s' F' z+ ]
1 l B6 k; S3 b; g- K0 l% v7 N2 p- /* USER CODE END Init */" v+ C r4 m% \8 |. h5 o
/ M. K; z4 L j0 b/ M/ W0 |1 h- /* Configure the system clock */
5 c! T9 O- t- Q- Z* |& D - SystemClock_Config();. L/ e4 l: P4 _0 N" R
- - n2 s' ^& _7 Q R" u
- /* USER CODE BEGIN SysInit */
& k3 D& Q# t; B) I; q( r6 `- [ - 2 @/ R/ x6 h m n, y
- /* USER CODE END SysInit */& R9 f# |* B0 B: R- `
1 {/ h4 ^ _( p- /* Initialize all configured peripherals */$ j. ?$ H# Z9 ^4 l/ F3 @# r& B- \
- MX_GPIO_Init();9 @/ P# h5 c& ~- o3 h8 r6 \
- MX_DMA_Init();
7 H# ]1 @4 V! `$ A - MX_ADC1_Init(); q1 h8 L) M9 ^+ G, d, n! J" }
- MX_LPUART1_UART_Init();
2 F- f+ q: J3 @ - MX_TIM1_Init();
4 [& R7 w6 D1 f% \* x - /* USER CODE BEGIN 2 */ l+ s; v# ]0 K! z0 E- E
- ( ~: P( k1 D8 T$ P s' {3 [: D' w) J
- ts_c30 = *(uint16_t *)(TS_CAL1_ADDR); //读取30℃时的ADC校准值
" u) G! c. S! A0 A3 d
0 o- ]5 S5 |4 C1 E; K- ts_c110 = *(uint16_t *)(TS_CAL2_ADDR);//读取110℃时的ADC校准值4 |& ^% o: V/ ~# Z. V$ l. }
- f0 r* A* d9 N- q1 ~5 C
- HAL_ADCEx_Calibration_Start(&hadc1 , ADC_SINGLE_ENDED);//ADC校准
9 P/ ^& c3 [2 E4 a1 u9 W
0 X! }9 p* N- l- HAL_ADC_Start_IT(&hadc1);//启动ADC并开启转换中断
7 b2 G2 `4 [& | ` - 8 \+ e: d( [% P/ k$ J* u# u
- HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
' v3 A# C" O, ?9 ?& a, C& [
' @0 Z( w/ d# D5 \) _% K- , W/ H8 u2 Q* k) _# m9 g
- /* USER CODE END 2 */' H6 J; R7 Z2 _1 p! J, L8 a
3 q% c( z; ]9 x1 m- /* Infinite loop */
9 V9 o: ]7 u2 v w - /* USER CODE BEGIN WHILE */3 c; |' r) }1 g/ }( U
- while (1)
- ~% {8 q% T) B8 u- e - {
" i0 U9 ^2 a' [- y" E5 [7 p - /* USER CODE END WHILE */# x) m, a' F9 [( M0 ^, H
- . N# w! w q. O/ h* P
- /* USER CODE BEGIN 3 */" a& J7 ]% P1 A( L0 X( m/ q
- 1 ]: z* j w& l6 H5 k, E9 p
- if (EndofCon_Flag!=0)
6 a- Z1 H7 H! p' K9 ~ - {
! W/ F6 d* N' j4 N6 z# o5 C - VBATVolt=(ADCResult[0]/4095.)* 3.3 * 3.; 9 p% E: y. Q2 B9 {' T# @2 C4 ]
' g2 j: P3 s* [6 k/ E# k- VRefint=(ADCResult[1]/4095.) * 3.3;
# p% k) I# k$ s7 R1 _
; r& q2 I) y9 }* l8 S# b* b- Temperature = 30.+ (88.*(ADCResult[2]-((ts_c30/1.1))))/(ts_c110 - ts_c30);
0 w# U/ F; r9 ] - / u: c2 G1 v( j$ L8 b7 p, {, S
- EndofCon_Flag=0;: ]1 c7 z2 w( @2 B- L: o# {
- * R9 S2 ^* E5 | M' Y/ ~
- //HAL_UART_Transmit(&hlpuart1, (uint8_t *)WDVol ,sizeof(WDVol), TX_Timeout);
. A& ~7 ^. k* a: a! A+ n - 3 Z6 ~+ Z: l* D) t G
- HAL_GPIO_WritePin( GPIOC,GPIO_PIN_3,GPIO_PIN_RESET); //for auxiliary test7 s: |2 l9 B( E* f) G3 E& y
- - u6 O3 _$ \( R; ~8 T+ A
- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)WDVol ,sizeof(WDVol));' O# P8 B9 i! U [
- 6 a+ l9 `) E4 v8 j$ y2 ?
- while(Completed==0){}
+ v; P0 y: Y+ Q" S) N+ m' [( T - Completed =0;/ |$ Q4 a; B% R. w
- # D& R o& c2 ~& F- |+ J
9 J2 ^' _/ l6 ?# V% j4 Q- //HAL_UART_Transmit(&hlpuart1, (uint8_t *)InVol ,sizeof(InVol), TX_Timeout);8 Z% }6 w, f. \
- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)InVol ,sizeof(InVol));
8 N2 I7 W/ v* R2 ~+ y5 A - $ [( N/ ~ t8 S
- while(Completed==0) {}9 k+ _) x0 `0 a6 `
- Completed =0;
& m% ]/ a: z1 y; J- Z# v
) f0 J; r; c; p- r# k5 g
, w/ V2 N1 g- {% z; q; O- //HAL_UART_Transmit(&hlpuart1, (uint8_t *)BatVol ,sizeof(BatVol), TX_Timeout);
# M/ g. e* `1 \: P" V/ U8 b0 |4 Q - HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)BatVol ,sizeof(BatVol));
: {9 I- |- c5 ]& I$ |
N! E7 y& Q% d/ V- while(Completed==0){}' }, M l4 `* v6 J
- Completed =0;
$ w w2 J- ?' U' o
+ ~) i" \& F' f6 i/ s3 y- HAL_GPIO_WritePin( GPIOC,GPIO_PIN_3,GPIO_PIN_SET); //for auxiliary test ?* K; y% R o) C. r! `6 U% y
- , V* h7 }" d; T- D' u, W/ Z
- } & A3 B# K$ W' J3 V y
: O0 x1 u" _- z; T9 A- }
3 p& }: U) ~2 U, n$ M6 S - /* USER CODE END 3 */
2 e; V8 ^. }5 u( T& f - }
# Q) ^- X6 u0 }" [2 M - % Z$ P# S4 U0 ^) i4 t( w# H4 n
- //ADC EOC 中断回调处理函数
- w- R$ D6 p" f: @( a - void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)' {- ^$ P6 T& F5 q' w
- {
4 R! T7 ~, _" X9 Q4 M% \3 Z7 K* q
1 V& A7 |5 t+ k5 b( q5 X6 B6 h- ADCResult[convCNT]=HAL_ADC_GetValue(&hadc1); //获取转换结果并存入数组' S y9 f/ f' G8 P. L# G: Y
+ R5 p# l: Z5 C* U& g# `; |9 W- convCNT++;/ R) ~; K! ]: W& z
3 @; {% S q9 O6 y3 V. l0 B- if(convCNT==3)
6 `- c* `% b ?8 }5 A/ g
8 x' N _. v0 z! W' M- {9 o6 f" _7 P! \
- convCNT=0;
, @' E* A3 A1 F8 ?4 _ - . X$ J6 }" Q) m$ t' P
- EndofCon_Flag=0xff;
0 k7 W8 n9 s; W7 Z - " g3 @7 U/ m7 x& [
- sprintf(WDVol,"Internal PN Temperature: %5.3f \r\n",Temperature);
; k" h% W& Q8 d# ^4 A3 h0 h: w8 e
$ L- e# `: R9 g/ X3 j/ C- sprintf(InVol,"Internal Reference Volt: %5.3f \r\n",VRefint); $ Q/ e, @1 A+ g$ z
- & R; ?8 z* R2 y0 S' B9 e
- sprintf(BatVol,"Current Battery Volt: %5.3f \r\n\r\n",VBATVolt);; z0 P* Z5 P6 Y' P4 F7 @
9 U9 v! W5 V# D! F3 m U- }( K( m k7 g R1 O
- % z' O/ l4 _3 ]) V* H1 p
- }- a7 G2 g# b8 P9 M6 P, J1 v# z. x
- & Y1 f$ _4 p9 O. y0 O
- //UART DMA 传输完成中断回调函数
" G7 H( V$ m% i - ' [. f$ h0 ~: d- T% o, A
- void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
9 F& N- b7 R! d5 k/ A' ?. @) F - {6 M0 L% I, s/ |% I; x" E. u2 j
- 6 o, a. |6 A+ d3 J2 d4 ?: d" f2 D
- Completed=0xff; q) x$ M/ |9 |7 A- m2 }" }
- }
复制代码 9 u. l9 a# k5 I9 V& U- V ]
基于上面的配置和测试代码,我们就可以看到最终的结果了。定时器周期性地触发ADC,每得到3个ADC结果就进行数据处理,然后通过UART以DMA方式传输到串口终端。注意VBat电压是测量结果再乘以3得到的。
O8 p4 ? @4 z3 y# m
2 \# ~1 S7 Z9 l% ]8 E
0 V7 o# _ R6 j/ ~' J
7 @6 b1 Q0 L$ c( z9 x
9 b# Z2 P* \ c: P5 k8 t2 X7 ^' R8 k9 F) S4 ^1 q
针对上面的应用演示,最后给几点相关应用提醒:2 t) @) }2 M1 i. u4 i' B
% o- u0 k) Y4 E7 |2 e$ `1、针对温度传感器做测量时,校准时使用的参考电压与实际应用不一致时要做换算,换算成相同参考电压的数据后再做计算。这点前面也提过了。- e; e X) y8 J% L0 M% k3 ]& c7 v
9 D; k* L1 F# }* {: t9 z
2、使用TIMER的TRGO触发ADC,如果选择类似比较事件、更新事件来触发ADC时,此时ADC对触发极性的选择是无效的,或者说ADC的转换仅依赖于触发事件时间点。如果是选择TIMER的Ocref信号作为触发源,此时ADC的硬件触发的极性选择是有效的,可以是上沿或下沿触发,甚至是双沿触发。这时就得根据需要选择合适的触发沿。【可以进一步阅读本公众号文章《STM32定时器触发ADC的时序话题》】: K, |# c% ?- V- u
? B$ g* i$ k5 O0 X: `
3、这里使用UART的DMA传输依次显示三个结果于串口终端,三个启动UART DMA传输的函数须保留适当时间间隔,即等上次传输完成后再启动下一次传输,因为这里每次传输使用的是同一DMA通道。否则没法全部正常输出。比如若把上面3次UART DMA传输的代码改成下面这样子:9 i! H6 R: U- f `: E
- k6 L* R# X! T: _- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)WDVol ,sizeof(WDVol));
: X% `6 M+ T' I B) i6 y. B - # N$ b/ ?! U4 g+ Q( k* W$ w
- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)InVol ,sizeof(InVol));. a3 u: B" n! V7 `! z2 M/ d
3 H3 e3 { C3 a( o/ ]2 y- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)BatVol ,sizeof(BatVol))
复制代码
$ q" T" K5 S3 G, B! m/ y* v这时输出结果会变成下面的情形,总是只能看到一个结果的输出,即第一次启动的DMA传输结果。
5 Z/ y+ i1 Q) a( n% M6 F5 I e' }" ^
! _& c! g* Q: I! r6 w' x- Y& ^ }- ^ g5 R$ F: p
如果想省事点,直接在相邻2次DMA传输间加上合适延时也行。我这里根据DMA传输完成事件来决定执行下一次发送。在DMA传输完成中断里设置Completed变量为非0值表示当前一轮DMA传输完成。' y/ j- J% i3 |" m7 E$ H R3 ?
4 x" j3 B% n' B9 P- |% E3 j4、对于那些在中断和主程序里都会被访问的变量,记得将它们冠以volatile。
q2 x; _+ o6 V! I+ K% ?3 { ? X% `4 g7 h$ \7 R8 F5 g/ }
下图的三路波形是我调试时辅助使用的。% B! ?- t) f- i: R, V/ i R
, i$ l3 J9 O8 `第一路表示计数器的计数变化,显然是单向向上计数模式。1 v" d/ g( ]2 R: K# p! o
# f G) p, D1 v3 y0 N. f6 r) H
第二路是TIMER1通道1的PWM输出波形。
. K0 Z N" t5 v/ T: H! m# |" }- \, s- m, v
第三路是我每次基于DMA实现UART发送时拉高拉低的波形。平常管脚电平为高,在实现DMA传输过程中拉低。( \; q7 ?" d2 t9 Y
. u' B. c' g) o, }
1 O- H. }* H# ~, h" a
7 U- k3 B1 h& X
如有侵权请联系删除" Q; U6 R! E1 a7 Q2 U
转载自: 茶话MCU/ D6 m) ]7 ~- d+ K$ ^
* P4 F* U7 q0 p% T4 f- J3 L
0 _% v8 h0 c4 ^6 V" j0 v* k: y/ J( c0 h7 S# M z6 [& t
|