" h3 j3 j9 a9 O! G, O5 U0 D; @0 N! `& C/ n: k
" _, p: v2 Q. I9 w. U" R
很多STM32芯片里往往内置了专用的ADC通道,比方用来测量Vrefint,VBAT的分压或温度传感器的输出电压信号。不同系列所内置的模拟信号通道可能有差异。这里以STM32G4系列为例,它内置了对应于Vrefint,VBAT的三分之一分压和温度传感器的输出电压的专用模拟通道。
( h4 P& H& L' l2 ]) n. ]( B" v7 A
+ \( r" j9 {- l3 Q6 v1 o
% i5 B& N5 R1 F/ k0 s9 H7 Y, j, {0 w
0 h `7 j A0 L" [% J! D& H
. \' ^; K6 V/ N/ T* o3 F; m4 ^下面的示例就是针对上述3个通道进行ADC,并测量相关电压和片内温度,最终得到3个结果,分别是VRefint电压,VBAT的电压,片内温度。- Z+ Y0 {* [$ J }
8 I3 q$ h) {" N+ F; u4 E p
! W' Y5 X; h* U& b9 A3 ]8 ]/ _& J) f& l2 k7 z9 p! y p
实现过程是这样的,大体分四步:【有点点麻雀虽小五脏俱全的味道图片】
$ @- l7 z% M* Q% z1 E8 Y$ S0 a( w* V/ q0 p
1、TIMER1 更新事件触发ADC的转换;! Z9 u/ w5 X6 E% v7 n* [& {
# T( [ d3 s6 e4 D2、CPU基于EOC中断获取ADC结果;9 R$ j; n# J8 E
/ I# j. x) N. t) q/ G3、对ADC结果进行换算,得到电压值和温度值存放在特定内存位置;5 @& {8 R" r9 Z9 f4 r
) l2 b/ n1 }% o" q$ g7 Y" @
4、基于DMA传输通过UART将最终结果在串口终端显示;
7 M7 c$ d5 o' C7 r4 w* J4 `1 a. [9 w/ W6 o7 `% H5 K( Q8 G+ l
其中,TIMER1的CH1输出PWM波形,其更新事件做ADC的转换启动信号。每次的TIMER更新事件触发ADC,3个通道扫描方式转换。这里的UART使用片内LPUART,使用它主要是考虑它跟板载虚拟串口直接相连,没有其它特别用意。
5 T+ b" m! k+ j( h, N3 K
' T7 D6 A( m) |: i2 r6 s1 }5 p% c( [
- Z P2 `( B7 o3 P8 X( k1 A) J
; G' I: z9 `$ \9 H4 A% R我使用STM32G474Nucleo板来进行下面实验。其中VDD=3.3v,VBAT与VDD相连。另外,ADC模块的参考电压也是3.3v.
/ s, E7 w3 m5 b6 n
$ _+ J9 ^" A2 b使用CubeMx图形化工具进行配置,先看TIMER配置:4 A. m- g. z# l3 O9 I
/ l- l* {8 ~0 z# A6 _ u
4 S6 {7 b, s- O
0 n0 j' z; y0 S' T& n* U
再看看ADC的基本配置:" v e G( k( K3 Q* g* y9 N
" s; n5 `+ D5 _6 Y6 f5 c: S- h
9 T. f% U. I$ M. A- a
& q ^" r) ?6 r1 NLPUART的基本配置:" r# t( p- @! w. B' J6 L3 O# o/ y
9 n/ s' } O/ \6 H8 N0 V
. n7 X2 p+ @( m, F; i4 s% c' S
3 w# n% ^+ j0 C' K- _因为要使用ADC中断和UART的DMA传输,记得做ADC的中断响应使能配置和LPUART的DMA配置,这里只使用UART的TX DMA功能。$ z( o" e' X: t- u
; F6 b8 A4 w4 O& a: e+ F
. }* z4 |: d- [# q, K$ [# a, Z2 o I9 }, k
使用CubeMx主要配置主要是上面这些。* P5 H$ m4 Q k! [& p( M
- P" @9 l' X; M6 P5 u在组织用户代码前,先简单介绍下片内温度传感器的内容。该温度传感器针对不同温度有不同电压输出,其输出电压跟温度呈线性关系。ST公司针对片内温度传感器在两个特定温度【30℃和110℃或30℃和130℃】、基于特定参考电压【3v或3.3v,不同系列以数据手册为准】生成了1组校准值并存放于片内特定FLASH位置。
2 Y9 N/ {# h+ `" e7 I1 L
8 @; @( w% }( Q+ _/ g3 h% T( bSTM32G4系列的校准值是在参考电压为3v,30℃和110℃条件下的两个值,在数据手册里还给出了校准值的片内存放地址。8 r$ M0 U8 x' L* j
% G# {! z! m) o7 u" W& W
2 h1 w2 S, g; r( y" b
% X4 o& {: ?5 a0 X7 I7 q0 c/ o" m针对这个温度传感器的使用,ST公司在参考手册里还给出了计算公式。其实,有无这个公式无所谓,我们不难自行推理出来。【TS_DATA代表某时刻测得的传感器输出电压对应的转换值,TS_CAL1/TS_CAL2分别表示在30℃和110℃条件下基于传感器输出电压的转换值。】
" k2 A+ D6 W# {, H% n& C" P& |! C6 s2 b: {
/ F) ]; J, p. Z1 ] |/ q
3 p7 s7 R" f0 U9 u- d
另外,前面提过,ST公司在手册里给出了温度传感器的两个温度下的校准值,但要注意生成校准值的ADC模块所用参考电压跟我们实际应用时AD模块所用的参考基准电压可能不一致。如果不一致,就必须将ADC值换算成同一基准参考电压条件下的数据。目前在ST手册里也特别强调这点了。我把上面一副图再贴一遍于此【见黄色语句提醒】。
: v* k+ i! i7 {
) E" M1 p! P# W% b
; L$ d) ^- C1 ?+ P# n, f7 R' p' F. Y# n( I; M+ s0 F
关于这点,我们也不难理解。同一待测信号、同一ADC模块在不同基准参考电压下转换值往往是不一样的。见下面示意图加以理解。
9 `) H3 P* E2 x: z, n2 z" i
' w+ ]+ f x4 f4 b& B& ?3 w. B
) t8 V3 Y4 j! K' s D& W; U- F, m4 S( c7 J: w
完成各项配置后,创建软件工程。添加必需的用户代码:$ B# d/ Y2 ]: J
/ I( O' w* P3 Z6 o' R1 t0 ?! }- #define TX_Timeout (9999)
8 M) I e+ l U! N! p/ A+ d' y1 m
: [2 o6 w; v: x2 Q- #define TS_CAL1_ADDR (0x1FFF75A8) //用于计算温度传感器数据 I+ F- i( Z# G. H5 p2 J: u
- #define TS_CAL2_ADDR (0x1FFF75CA) //用于计算温度传感器数据
/ P* g2 u) a2 [0 s8 [, F a
1 x* o/ g! Q2 m: i) N0 `- #define size1 (40)
- E3 B, |7 U& A. P - " l+ o9 ^0 V- j9 b ^9 t! H- C
- char WDVol[size1],BatVol[size1],InVol[size1];
" H0 d& h$ v$ n - ; @, u) K, }# b9 W5 o
- uint16_t ts_c30,ts_c110;, y7 z* f. U! y& ? o' k
- r2 ]' h. [& m) }; R9 j- uint16_t ADCResult[3],convCNT;* L9 s8 d9 E# P0 X: R7 O
& b# N) C# S2 U& W- volatile uint32_t Completed,EndofCon_Flag;( \; I7 x: G" }) o; |2 b/ S+ w
- : v& Z2 F" Y3 |* E7 L
- float VBATVolt;//存放BBAT电压最终结果
& l: g# n# J$ {1 z; v - float VRefint; //存放Vrefint电压最终结果1 c* x2 y' l4 g
- float Temperature;//存放片内温度℃最终结果
/ ]/ t( R/ |- ]6 g) K - ' p1 V J: Y6 K S
- int main(void)
" x6 E% y- j5 W+ B7 O - {- u7 X% {& b% X5 C, Q5 b0 @
- /* USER CODE BEGIN 1 */% H1 D; K1 f. m% c* I& z
- " {0 Q% R6 n# a% Q9 N- k
- /* USER CODE END 1 */9 M& }; Y+ H' i6 w- [# e
- * D; a; S+ e0 T( y) J- Y
- /* MCU Configuration--------------------------------------------------------*/0 c5 ^6 a( ^8 f' H- w! P
- ; J; u A" n/ C l
- /* Reset of all peripherals, Initializes the Flash interface and the Systick. */. \$ Z. K I9 h% `$ @- n
- HAL_Init();, P+ p' J* \6 b! N# s/ a* u2 x: C) D
; O5 Q# {, S4 [ }* S1 B- /* USER CODE BEGIN Init */. }' y( ], K; r- f% l+ s; J% U) v
- * E, l0 w5 ^: G
- /* USER CODE END Init */$ `2 S# B/ Y; [/ l
- , d3 L1 R# q, _! t2 H
- /* Configure the system clock */
* F# ?3 q9 w9 G- Z& T - SystemClock_Config();# g( p( d- Y4 `7 A- ~2 b
- " E7 I7 m; I' R& n* M7 _6 U
- /* USER CODE BEGIN SysInit */
) H" |; x+ R2 b7 ^
) X5 q) C& i8 Y% {- /* USER CODE END SysInit */" @; r! M x8 \$ E8 b
- : `- U: [1 O. r8 P" C5 H8 ]
- /* Initialize all configured peripherals */$ d& m; b2 l: K0 U$ u: b
- MX_GPIO_Init();& b/ K9 W; H. y
- MX_DMA_Init();
# V( t7 S4 R) a# ]8 r& ~) z, k - MX_ADC1_Init();
7 w8 O4 g( N, P/ o - MX_LPUART1_UART_Init();' c1 D6 @. l6 d3 [" t
- MX_TIM1_Init();3 \7 U- F* s, T/ ?
- /* USER CODE BEGIN 2 */; W7 S+ M& i1 r$ C2 L& }+ T
- 9 S3 D4 L1 ^/ f$ p, U
- ts_c30 = *(uint16_t *)(TS_CAL1_ADDR); //读取30℃时的ADC校准值# i5 P N5 @0 S3 h% ^
4 g, U* J4 E% Y( `: a. q6 b; \- ts_c110 = *(uint16_t *)(TS_CAL2_ADDR);//读取110℃时的ADC校准值; T u' ~' \: e+ [7 z7 u
- " M% ^! m; ^/ ]$ ?0 C7 p, e
- HAL_ADCEx_Calibration_Start(&hadc1 , ADC_SINGLE_ENDED);//ADC校准% Y* X6 ^* ^8 F5 ^$ x& x/ i- `; X r# e: O
- f4 L, q/ E5 {5 Q
- HAL_ADC_Start_IT(&hadc1);//启动ADC并开启转换中断
a) R# Q( C7 L4 H- m3 A$ B
* ^# z9 x2 p! k/ U9 X( l5 P; H9 U* C, I- HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);& y+ v: k, m% q1 Z) g
- ; h! B' b! N& K, N8 p
- 4 f8 Z) ^. t. @, q1 ^4 p0 ~
- /* USER CODE END 2 */
% i6 @2 m" n7 o3 ^2 R
c( {% k" I1 o- /* Infinite loop */# t- q# s0 k; u5 A0 k
- /* USER CODE BEGIN WHILE */
9 d+ R" C* H$ }9 z3 | - while (1)
6 |) a6 i L' ]: D+ c; W" O% e - {
# I( ?- H6 {' R+ l! K: V* ? - /* USER CODE END WHILE */( x! a8 V9 N) ~ H1 u( r; l4 [5 u4 y
- : N! ~2 C5 V- i: d
- /* USER CODE BEGIN 3 */
. ~4 A3 t7 i% m
8 a0 g5 ?) w& R4 t/ n! o) |- if (EndofCon_Flag!=0)
( o: t1 `! g- C - {
) ]0 Z, b- l& r+ J0 B$ b3 e - VBATVolt=(ADCResult[0]/4095.)* 3.3 * 3.;
" c' C) e9 F3 q" Y! o - ) h x# |1 c, f, a h
- VRefint=(ADCResult[1]/4095.) * 3.3; 3 {% n+ B3 M4 p' m7 e
2 S, p: ^$ Q m2 w. D- Temperature = 30.+ (88.*(ADCResult[2]-((ts_c30/1.1))))/(ts_c110 - ts_c30);
- D g! Z9 ]/ a) Q( I0 A
5 V; \9 j0 D3 ^- EndofCon_Flag=0;
9 T* p0 d2 y5 I K5 o" x - 7 P6 \7 ^2 o) _, l) N- z& A
- //HAL_UART_Transmit(&hlpuart1, (uint8_t *)WDVol ,sizeof(WDVol), TX_Timeout);$ Z$ O3 y& ^) @. T
' U3 L% L5 _' e& S. u- HAL_GPIO_WritePin( GPIOC,GPIO_PIN_3,GPIO_PIN_RESET); //for auxiliary test
8 h0 D3 H! Q+ g) I! @. ?2 m2 e
: z3 x) J+ r( p9 G \2 I* I- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)WDVol ,sizeof(WDVol));
7 [, q$ ~5 ~2 y% u& M5 g- m - 4 }; e2 w3 w9 u7 ~. M: p2 i7 P6 ~
- while(Completed==0){}
0 ~. G0 @. E5 O$ V# v) C* ` - Completed =0;& Z) P' k6 q) f8 j- H! C0 C; i
- / }4 B; a7 @0 H
) `( p1 o! `! I: w- //HAL_UART_Transmit(&hlpuart1, (uint8_t *)InVol ,sizeof(InVol), TX_Timeout);
0 \6 j: }; X8 j. ^1 [ - HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)InVol ,sizeof(InVol));
7 m2 E/ y+ U9 j6 W j( p - " h/ f& o! r6 e3 z5 N
- while(Completed==0) {}
* j* m5 ~5 c7 f, M6 B+ }% \0 I/ x. L - Completed =0;
+ R: N! r0 s3 X0 e - , L8 L4 g% [" q% J. D+ h
/ J5 o& b9 ^' h, b6 j' [- //HAL_UART_Transmit(&hlpuart1, (uint8_t *)BatVol ,sizeof(BatVol), TX_Timeout);
& U' R4 L" q9 }; t, P - HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)BatVol ,sizeof(BatVol));
* b, V- Z3 z& b: e( |0 w/ K - # ^7 k* `7 F: ?1 y, r
- while(Completed==0){}' Q5 S9 @7 j M9 d- U% q0 m
- Completed =0;; j6 p+ A$ d: m2 O) x g7 ^! r
; r ?! D1 q9 g" p9 @" A- HAL_GPIO_WritePin( GPIOC,GPIO_PIN_3,GPIO_PIN_SET); //for auxiliary test
! p. s# O! V4 t1 p% u& n* W J! S/ ^3 S - - Q$ g- G/ _" c' S8 p" m
- }
6 f a3 p( o/ n6 Q
) V5 \0 L/ v- q# ]+ P7 u- }
" x1 I6 |8 ?9 p: \2 g4 ]- C - /* USER CODE END 3 */$ A9 L& R+ `# |4 R) k
- }
, W# p2 b P2 D$ N# ^8 s - 1 H7 f5 V; q% T1 {3 l0 G; v* M
- //ADC EOC 中断回调处理函数
: t; y, E3 Q- y3 b- C% U% t - void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc), ^2 `3 t; A. j# g0 ~( A& `
- {" M1 [# |- s/ c4 ?2 e
, X/ ?( j2 v! f9 P% o& F- ADCResult[convCNT]=HAL_ADC_GetValue(&hadc1); //获取转换结果并存入数组2 l( c/ Y1 @, V; L9 J
9 k$ L k: L3 y3 Y- convCNT++;
: ~. D3 `& |% F
0 j/ `' G4 E5 d# a) H( K- if(convCNT==3) % ]* g% W7 ~) K' _2 q- v: r) W
- ) C& L$ ]5 ^, [0 ]
- {: l% {' A6 S' h9 d
- convCNT=0;
9 \6 x3 O* P) J) z - ( c- T' H, _$ P. ?1 V% d
- EndofCon_Flag=0xff;% \* l! M* ]- |9 I. f/ c3 {
- ( C' { g: O4 ~1 S8 ~
- sprintf(WDVol,"Internal PN Temperature: %5.3f \r\n",Temperature); * b' `! S& @. K5 B- B
- 0 u9 n, V- z3 f4 [. v
- sprintf(InVol,"Internal Reference Volt: %5.3f \r\n",VRefint); . o$ t! `4 S" P* v4 s4 j. j2 ?
- 6 c$ x |- f1 g: ?
- sprintf(BatVol,"Current Battery Volt: %5.3f \r\n\r\n",VBATVolt);
- u g' V# A' N2 F* p
2 B7 {8 o* T- N6 ^$ n7 u' c- }
! H$ E/ s6 S4 f
& e: J0 V/ m9 N4 C- }* |7 z, M; x: X
- 2 n+ N0 h" }, X+ ^% X5 t. b: ^
- //UART DMA 传输完成中断回调函数 g0 D! j* n9 x" C! u
* _2 h& J8 \! K7 Y. l- void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
6 P A6 `2 f$ m& D0 p" t) [, E - {! Z5 O" o3 U5 b
4 u+ N9 ^8 P+ o. w c+ _4 h- Completed=0xff;8 S$ n! A) K/ `
- }
复制代码
" _$ L3 z/ t* ]% P6 S基于上面的配置和测试代码,我们就可以看到最终的结果了。定时器周期性地触发ADC,每得到3个ADC结果就进行数据处理,然后通过UART以DMA方式传输到串口终端。注意VBat电压是测量结果再乘以3得到的。 {" [" u6 m+ s8 ^; y6 E7 A+ [7 a# D7 I
+ M3 k* h, s4 y! `& S# K9 J6 b. E$ `
& G/ U' s0 J% n' x% `2 j$ ~; h: w5 e" ^
4 l* H- N+ E/ i) R. x: I* q& C1 O5 T+ O5 }0 l
针对上面的应用演示,最后给几点相关应用提醒:
; L9 n. ? w) @* y
4 [9 U9 g9 Y( _8 |& {$ s- o1、针对温度传感器做测量时,校准时使用的参考电压与实际应用不一致时要做换算,换算成相同参考电压的数据后再做计算。这点前面也提过了。# ~# n7 |* \( q; W9 I7 F% t, I5 S
+ j# w4 X* @5 F* V4 \2、使用TIMER的TRGO触发ADC,如果选择类似比较事件、更新事件来触发ADC时,此时ADC对触发极性的选择是无效的,或者说ADC的转换仅依赖于触发事件时间点。如果是选择TIMER的Ocref信号作为触发源,此时ADC的硬件触发的极性选择是有效的,可以是上沿或下沿触发,甚至是双沿触发。这时就得根据需要选择合适的触发沿。【可以进一步阅读本公众号文章《STM32定时器触发ADC的时序话题》】# R, E6 K- G3 \2 D9 X% A' [ b
+ Y7 a5 e' |& ~* w/ U2 A$ n
3、这里使用UART的DMA传输依次显示三个结果于串口终端,三个启动UART DMA传输的函数须保留适当时间间隔,即等上次传输完成后再启动下一次传输,因为这里每次传输使用的是同一DMA通道。否则没法全部正常输出。比如若把上面3次UART DMA传输的代码改成下面这样子:
/ c0 |( ~. W) O8 E6 t7 \: D+ u
, j( W8 ^4 u1 R( a) i/ R8 Q" S- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)WDVol ,sizeof(WDVol));
" D+ L2 _" {7 B - 6 e1 K$ S6 W, v# S% D7 ?. f) F- N4 ~
- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)InVol ,sizeof(InVol));( {" U) H3 l8 T9 V7 f6 H6 P9 z9 O7 K
- 6 x5 v$ b' r2 r6 I# m8 b
- HAL_UART_Transmit_DMA(&hlpuart1, (uint8_t *)BatVol ,sizeof(BatVol))
复制代码 ) G8 Q; p& [! E ^- q% M; x5 o
这时输出结果会变成下面的情形,总是只能看到一个结果的输出,即第一次启动的DMA传输结果。
4 l8 e! ]# Y4 @. [6 t/ s: k) \
, _+ a9 ^5 J: J& I t5 _& _" S8 U' ]! P, u- [7 f- J4 H
7 | s' x- Q0 M; |# w
如果想省事点,直接在相邻2次DMA传输间加上合适延时也行。我这里根据DMA传输完成事件来决定执行下一次发送。在DMA传输完成中断里设置Completed变量为非0值表示当前一轮DMA传输完成。
0 x+ t% K2 B6 u5 c
' W0 R, k9 l) I% s* ^9 z2 p4、对于那些在中断和主程序里都会被访问的变量,记得将它们冠以volatile。# B' u7 ^4 i* q" g: |: C1 y
$ i9 ~( ]3 c& I* C% ]8 _; W+ {( n: n
下图的三路波形是我调试时辅助使用的。
( k+ F; k( ~3 ^/ u; @2 Q) a5 g r6 l4 g6 j7 a' V7 s
第一路表示计数器的计数变化,显然是单向向上计数模式。# i: ?6 b+ v5 v+ H) G( b, v
5 O5 O; o$ o- l- F! u
第二路是TIMER1通道1的PWM输出波形。! Y$ `, ]! a1 X$ {: m, o0 M3 h( `
9 A4 V h/ d; J* N( P第三路是我每次基于DMA实现UART发送时拉高拉低的波形。平常管脚电平为高,在实现DMA传输过程中拉低。& l" t3 [9 O% L6 N; N6 g: W
% C* E2 c6 _( a6 q- `
$ _ ?7 D( ?$ N) [
* x1 y, T, [+ f& M
如有侵权请联系删除4 w* H& T! }; M; u% p! I$ b
转载自: 茶话MCU2 R" Y. u+ B. o' [" W8 N1 J
5 c& _! D4 V! w1 ?/ x% N & H8 q5 P% o. ~ {8 Q( M* C8 m
' D9 Z/ |- P1 N; [) P# O1 W5 c
|