mirror of
https://github.com/rfivet/stm32bringup.git
synced 2024-12-18 23:06:28 -05:00
372 lines
12 KiB
HTML
372 lines
12 KiB
HTML
|
<!DOCTYPE html>
|
|||
|
<html lang="en">
|
|||
|
<head>
|
|||
|
<meta charset="UTF-8">
|
|||
|
<title>3.4 Internal Voltage and Temperature</title>
|
|||
|
<link type="text/css" rel="stylesheet" href="style.css">
|
|||
|
</head>
|
|||
|
<body>
|
|||
|
<h1>3.4 Internal Voltage and Temperature</h1>
|
|||
|
|
|||
|
The STM32 chipsets have internal temperature sensor and voltage
|
|||
|
reference connected to two channels of the Analog to Digital Converter
|
|||
|
(ADC) peripheral. I will take some readings to see how that compare to
|
|||
|
other temperature sensors.
|
|||
|
<p>
|
|||
|
The STM32F030 family has a suffix 6 (STM32F030F4P<b>6</b>) for the ambient
|
|||
|
operating temperature: -40℃ to 85℃. It should be fine to use not only
|
|||
|
inside rooms but also outdoor (may be not on the dashboard of a car
|
|||
|
parked under direct tropical sunlight at noon in summer).
|
|||
|
|
|||
|
<h2>Analog to Digital Converter</h2>
|
|||
|
|
|||
|
The STM32 ADC is very versatile which also means it has many options. I
|
|||
|
am looking for a minimal setup to read both voltage and temperature by
|
|||
|
issuing an ADC conversion command for each value I need to read.
|
|||
|
<p>
|
|||
|
The initialization steps:
|
|||
|
<ul>
|
|||
|
<li> Enable ADC peripheral.
|
|||
|
|
|||
|
<li> Select a sampling clock.
|
|||
|
|
|||
|
<li> Recalibrate after a change of sampling clock.
|
|||
|
|
|||
|
<li> Enable command mode.
|
|||
|
|
|||
|
<li> Select Voltage reference and Temperature sensor as inputs.
|
|||
|
|
|||
|
<li> Configure acquisition direction and mode.
|
|||
|
</ul>
|
|||
|
<pre>
|
|||
|
static void adc_init( void) {
|
|||
|
/* Enable ADC peripheral */
|
|||
|
RCC_APB2ENR |= RCC_APB2ENR_ADCEN ;
|
|||
|
/* Setup ADC sampling clock */
|
|||
|
#ifdef HSI14
|
|||
|
RCC_CR2 |= RCC_CR2_HSI14ON ; /* Start HSI14 clock */
|
|||
|
do {} while( !( RCC_CR2 & RCC_CR2_HSI14RDY)) ; /* Wait for stable clock */
|
|||
|
/* Select HSI14 as sampling clock for ADC */
|
|||
|
// ADC_CFGR2 &= ~ADC_CFGR2_CKMODE ; /* Default 00 == HSI14 */
|
|||
|
#else
|
|||
|
/* Select PCLK/2 as sampling clock for ADC */
|
|||
|
ADC_CFGR2 |= ADC_CFGR2_PCLK2 ; /* 01 PCLK/2 Over default 00 */
|
|||
|
// ADC_CFGR2 |= ADC_CFGR2_PCLK4 ; /* 10 PCLK/4 Over default 00 */
|
|||
|
#endif
|
|||
|
|
|||
|
/* Calibration */
|
|||
|
ADC_CR |= ADC_CR_ADCAL ;
|
|||
|
do {} while( ADC_CR & ADC_CR_ADCAL) ; /* Wait end of calibration */
|
|||
|
|
|||
|
/* Enable Command (below Work Around from Errata necessary with PCLK/4) */
|
|||
|
do {
|
|||
|
ADC_CR |= ADC_CR_ADEN ;
|
|||
|
} while( !( ADC_ISR & ADC_ISR_ADRDY)) ;
|
|||
|
|
|||
|
/* Select inputs and precision */
|
|||
|
ADC_CHSELR = 3 << 16 ; /* Channel 16: temperature, Channel 17: Vrefint */
|
|||
|
ADC_CCR |= ADC_CCR_TSEN | ADC_CCR_VREFEN ;
|
|||
|
ADC_SMPR = 7 ;
|
|||
|
/* Select acquisition direction and mode */
|
|||
|
/* Default scan direction (00) is Temperature before Voltage */
|
|||
|
// ADC_CFGR1 &= ~ADC_CFGR1_SCANDIR ; /* Default 0 is low to high */
|
|||
|
ADC_CFGR1 |= ADC_CFGR1_DISCEN ; /* Enable Discontinuous mode */
|
|||
|
}
|
|||
|
</pre>
|
|||
|
|
|||
|
The ADC characteristics in the STM32F030 datasheet states that the
|
|||
|
maximum ADC sampling clock is 14MHz. It is possible to select either an
|
|||
|
internal 14 MHz clock (HSI14) or PCLK divided by 2 or 4. I want to check
|
|||
|
how the clock affects the readings (HSI14, 28/2, 24/2, 48/4). At first I
|
|||
|
couldn’t manage to make PCLK/4 work until I found the note on ADC
|
|||
|
calibration work around in the errata
|
|||
|
<a href="https://www.st.com/content/st_com/en/search.html#q=%20ES0219-t=resources-page=1"
|
|||
|
>ES0219 2.5.3</a>
|
|||
|
<p>
|
|||
|
After this initialization, I can trigger an ADC conversion by issuing a
|
|||
|
command, wait until completion and fetch the result in the ADC Data
|
|||
|
Register.
|
|||
|
|
|||
|
<pre>
|
|||
|
static unsigned adc_convert( void) {
|
|||
|
/* Either only one channel in sequence or Discontinuous mode ON */
|
|||
|
ADC_CR |= ADC_CR_ADSTART ; /* Start ADC conversion */
|
|||
|
do {} while( ADC_CR & ADC_CR_ADSTART) ; /* Wait for start command cleared */
|
|||
|
return ADC_DR ;
|
|||
|
}
|
|||
|
</pre>
|
|||
|
|
|||
|
There is two values to fetch, temperature and voltage in that order.
|
|||
|
|
|||
|
<pre>
|
|||
|
temperature = adc_convert() ;
|
|||
|
voltage = adc_convert() ;
|
|||
|
</pre>
|
|||
|
|
|||
|
The values resulting from an ADC conversion are raw, at the precision I
|
|||
|
selected during initialization (12 bits) it gives me a value between 0
|
|||
|
and 4095.
|
|||
|
|
|||
|
<h2>Factory Calibration</h2>
|
|||
|
|
|||
|
The raw data readings are relative to the analog operative voltage VDDA.
|
|||
|
<p>
|
|||
|
<b>Vmeasured = VDDA * VRAW / 4095</b>
|
|||
|
<p>
|
|||
|
This is why I measure both voltage and temperature. Ideally, VDDA should
|
|||
|
be 3.3V, in practice it’s highly dependent of the power supply.
|
|||
|
<p>
|
|||
|
Every chip is calibrated in factory during production, ADC conversion is
|
|||
|
done at 3.3V and 30℃ and the resulting values are stored in system
|
|||
|
memory.
|
|||
|
<p>
|
|||
|
<img alt="VREFINT_CAL" src="img/34_vrefint.png">
|
|||
|
<p>
|
|||
|
<img alt="TS_CAL1" src="img/34_tscal1.png">
|
|||
|
|
|||
|
<pre>
|
|||
|
/* STM32F030 calibration addresses (at 3.3V and 30C) */
|
|||
|
#define TS_CAL1 ((unsigned short *) 0x1FFFF7B8)
|
|||
|
#define VREFINT_CAL ((unsigned short *) 0x1FFFF7BA)
|
|||
|
</pre>
|
|||
|
|
|||
|
VREFINT is the embedded reference voltage measured on channel 17 of the
|
|||
|
ADC. According to the STM32F030 datasheet it is in the range 1.2V to
|
|||
|
1.25V. I have a chip whose VREFINT calibration value is 1526, that means
|
|||
|
the embedded reference voltage VREFINT is 3.3 * 1526 / 4095, close to
|
|||
|
the middle of the range and the given typical value of 1.23V.
|
|||
|
<p>
|
|||
|
I can read VREFINT raw data after ADC conversion and I know its factory
|
|||
|
calibrated value, so I can calculate VDDA.
|
|||
|
<p>
|
|||
|
<b>VREFINT = 3.3 * VCAL / 4095 = VDDA * VRAW / 4095</b>
|
|||
|
<p>
|
|||
|
<b>VDDA = 3.3 * VCAL / VRAW</b>
|
|||
|
<p>
|
|||
|
On my chip whose VCAL is 1526, if I measure VRAW at 1534, this will put
|
|||
|
VDDA at 3.28V. Less than 1% off the ideal 3.3V voltage.
|
|||
|
<p>
|
|||
|
There is only one temperature calibration value stored for STM32F030
|
|||
|
chips. You need two reference values to be able to calculate the
|
|||
|
temperature reliably, either the ADC readings taken at two temperatures
|
|||
|
(30℃ and 110℃ for other STM32 families) or one ADC reading plus the
|
|||
|
value of the temperature slope characteristic for that particular chip.
|
|||
|
<p>
|
|||
|
The STM32F030 datasheet gives the range 4~4.6 mV/℃ and a typical value
|
|||
|
of 4.3 mV/℃ for the average slope. The reference manual uses 4.3 mV/℃ in
|
|||
|
the temperature calculation code example.
|
|||
|
<p>
|
|||
|
When it comes to temperature measurement using the temperature sensor of
|
|||
|
STM32F030, you need to do your own two point calibration.
|
|||
|
|
|||
|
<h2>Voltage and Temperature Conversion API</h2>
|
|||
|
|
|||
|
I need to be able to
|
|||
|
<ul>
|
|||
|
<li> Initialize the ADC for voltage and temperature conversion and fetch
|
|||
|
the calibration values.
|
|||
|
|
|||
|
<li> Fetch the calibration values.
|
|||
|
|
|||
|
<li> Convert and read the raw ADC conversion values.
|
|||
|
|
|||
|
<li> Convert and read the calculated voltage and temperature.
|
|||
|
</ul>
|
|||
|
I add the following API to <b>system.h</b>
|
|||
|
|
|||
|
<pre>
|
|||
|
typedef enum {
|
|||
|
VNT_INIT,
|
|||
|
VNT_CAL,
|
|||
|
VNT_RAW,
|
|||
|
VNT_VNC
|
|||
|
} vnt_cmd_t ;
|
|||
|
|
|||
|
void adc_vnt( vnt_cmd_t cmd, short *ptrV, short *ptrC) ;
|
|||
|
</pre>
|
|||
|
|
|||
|
The voltage return value is in centiV (330 == 3.3V), the temperature
|
|||
|
return value is in deci℃ (300 == 30℃).
|
|||
|
<p>
|
|||
|
I make a copy of <b>gpioa.c</b> into <b>adc.c</b> adding the code I just
|
|||
|
explained for <code>adc_init()</code>, <code>adc_convert()</code>, the
|
|||
|
calibration value addresses and the following implementation of
|
|||
|
<code>adc_vnt()</code>.
|
|||
|
|
|||
|
<pre>
|
|||
|
void adc_vnt( vnt_cmd_t cmd, short *ptrV, short *ptrC) {
|
|||
|
if( cmd == VNT_INIT)
|
|||
|
adc_init() ;
|
|||
|
|
|||
|
if( cmd <= VNT_CAL) {
|
|||
|
/* Calibration Values */
|
|||
|
*ptrV = *VREFINT_CAL ;
|
|||
|
*ptrC = *TS_CAL1 ;
|
|||
|
return ;
|
|||
|
}
|
|||
|
|
|||
|
/* ADC Conversion */
|
|||
|
*ptrC = adc_convert() ;
|
|||
|
*ptrV = adc_convert() ;
|
|||
|
|
|||
|
if( cmd == VNT_VNC) {
|
|||
|
*ptrC = 300 + (*TS_CAL1 - *ptrC * *VREFINT_CAL / *ptrV) * 10000 / 5336 ;
|
|||
|
*ptrV = 330 * *VREFINT_CAL / *ptrV ;
|
|||
|
}
|
|||
|
}
|
|||
|
</pre>
|
|||
|
|
|||
|
The calculation for the temperature is based on the code example from
|
|||
|
the reference manual (RM0360 A.7.16).
|
|||
|
<p>
|
|||
|
The only thing missing is the description of the newly used registers
|
|||
|
and bitfields.
|
|||
|
<p>
|
|||
|
● The extra RCC bitfields and register for enabling the ADC peripheral and activating HSI14 clock.
|
|||
|
|
|||
|
<pre>
|
|||
|
#define RCC_APB2ENR_ADCEN 0x00000200 /* 9: ADC clock enable */
|
|||
|
|
|||
|
#define RCC_CR2 RCC[ 13]
|
|||
|
#define RCC_CR2_HSI14ON 0x00000001 /* 1: HSI14 clock enable */
|
|||
|
#define RCC_CR2_HSI14RDY 0x00000002 /* 2: HSI14 clock ready */
|
|||
|
</pre>
|
|||
|
|
|||
|
● The ADC registers and bitfields.
|
|||
|
|
|||
|
<pre>
|
|||
|
#define ADC ((volatile long *) 0x40012400)
|
|||
|
#define ADC_ISR ADC[ 0]
|
|||
|
#define ADC_ISR_ADRDY 1 /* 0: ADC Ready */
|
|||
|
#define ADC_ISR_EOC 4 /* 2: End Of Conversion flag */
|
|||
|
|
|||
|
#define ADC_CR ADC[ 2]
|
|||
|
#define ADC_CR_ADEN 1 /* 0: ADc ENable command */
|
|||
|
#define ADC_CR_ADSTART 4 /* 2: ADC Start Conversion command */
|
|||
|
#define ADC_CR_ADCAL (1 << 31) /* 31: ADC Start Calibration cmd */
|
|||
|
|
|||
|
#define ADC_CFGR1 ADC[ 3] /* Configuration Register 1 */
|
|||
|
#define ADC_CFGR1_SCANDIR 4 /* 2: Scan sequence direction */
|
|||
|
#define ADC_CFGR1_DISCEN (1 << 16) /* 16: Enable Discontinuous mode */
|
|||
|
|
|||
|
#define ADC_CFGR2 ADC[ 4] /* Configuration Register 2 */
|
|||
|
#define ADC_CFGR2_CKMODE (3 << 30) /* 31-30: Clock Mode Mask */
|
|||
|
/* 31-30: Default 00 HSI14 */
|
|||
|
#define ADC_CFGR2_PCLK2 (1 << 30) /* 31-30: PCLK/2 */
|
|||
|
#define ADC_CFGR2_PCLK4 (2 << 30) /* 31-30: PCLK/4 */
|
|||
|
|
|||
|
#define ADC_SMPR ADC[ 5] /* Sampling Time Register */
|
|||
|
#define ADC_CHSELR ADC[ 10] /* Channel Selection Register */
|
|||
|
#define ADC_DR ADC[ 16] /* Data Register */
|
|||
|
#define ADC_CCR ADC[ 194] /* Common Configuration Register */
|
|||
|
#define ADC_CCR_VREFEN (1 << 22) /* 22: Vrefint Enable */
|
|||
|
#define ADC_CCR_TSEN (1 << 23) /* 23: Temperature Sensor Enable */
|
|||
|
</pre>
|
|||
|
|
|||
|
<h2>Application</h2>
|
|||
|
|
|||
|
I create <b>adcmain.c</b> to take readings every second.
|
|||
|
|
|||
|
<pre>
|
|||
|
/* adcmain.c -- ADC reading of reference voltage and temperature sensor */
|
|||
|
|
|||
|
#include <stdio.h>
|
|||
|
#include "system.h"
|
|||
|
|
|||
|
#define RAW
|
|||
|
|
|||
|
int main( void) {
|
|||
|
unsigned last = 0 ;
|
|||
|
short calV, calC ;
|
|||
|
|
|||
|
/* Initialize ADC and fetch calibration values */
|
|||
|
adc_vnt( VNT_INIT, &calV, &calC) ;
|
|||
|
#ifdef RAW
|
|||
|
printf( "%u, %u\n", calV, calC) ;
|
|||
|
#endif
|
|||
|
|
|||
|
for( ;;)
|
|||
|
if( uptime == last)
|
|||
|
yield() ;
|
|||
|
else {
|
|||
|
short Vsample, Csample ;
|
|||
|
|
|||
|
last = uptime ;
|
|||
|
#ifdef RAW
|
|||
|
adc_vnt( VNT_RAW, &Vsample, &Csample) ;
|
|||
|
printf( "%i, %i, %i, %i, ", calV, Vsample, calC, Csample) ;
|
|||
|
Csample = 300 + (calC - (int) Csample * calV / Vsample)
|
|||
|
* 10000 / 5336 ;
|
|||
|
Vsample = 330 * calV / Vsample ;
|
|||
|
#else
|
|||
|
adc_vnt( VNT_VNC, &Vsample, &Csample) ;
|
|||
|
#endif
|
|||
|
printf( "%i.%i, %i.%i\n", Vsample / 100, Vsample % 100,
|
|||
|
Csample / 10, Csample % 10) ;
|
|||
|
}
|
|||
|
}
|
|||
|
</pre>
|
|||
|
|
|||
|
<h2>Build and test</h2>
|
|||
|
|
|||
|
I want to test using maximum ADC clock at 14 MHz provided by PLLHSI/2,
|
|||
|
as I am using a board with no external quartz.
|
|||
|
|
|||
|
<pre>
|
|||
|
/* No quartz, configure PLL at 28MHz */
|
|||
|
//#define HSE 8000000
|
|||
|
#define PLL 7
|
|||
|
#define BAUD 9600
|
|||
|
//#define HSI14 1
|
|||
|
</pre>
|
|||
|
|
|||
|
I add the composition in <b>Makefile</b>
|
|||
|
|
|||
|
<pre>SRCS = startup.txeie.c adc.c adcmain.c</pre>
|
|||
|
|
|||
|
The build gives some warning as 28MHz is not a perfect match for a
|
|||
|
baudrate of 9600 and the current implementation of <code>usleep()</code>. This
|
|||
|
will not affect my application.
|
|||
|
|
|||
|
<pre>
|
|||
|
$ make
|
|||
|
adc.c:155:3: warning: #warning baud rate not accurate at that clock frequency [-
|
|||
|
Wcpp]
|
|||
|
155 | # warning baud rate not accurate at that clock frequency
|
|||
|
| ^~~~~~~
|
|||
|
adc.c: In function 'usleep':
|
|||
|
adc.c:232:3: warning: #warning HCLK is not multiple of 8 MHz [-Wcpp]
|
|||
|
232 | # warning HCLK is not multiple of 8 MHz
|
|||
|
| ^~~~~~~
|
|||
|
f030f4.elf from startup.txeie.o adc.o adcmain.o
|
|||
|
text data bss dec hex filename
|
|||
|
2464 0 16 2480 9b0 f030f4.elf
|
|||
|
f030f4.hex
|
|||
|
f030f4.bin
|
|||
|
</pre>
|
|||
|
|
|||
|
Flashing the board and starting execution, I can see the results of the
|
|||
|
ADC conversion and the calculated values.
|
|||
|
<p>
|
|||
|
<img alt="Raw ADC conversion readings" src ="img/34_output.png">
|
|||
|
</p>
|
|||
|
The temperature readings are roughly 5℃ higher than room temperature.
|
|||
|
|
|||
|
<h2>Checkpoint</h2>
|
|||
|
|
|||
|
I have done a simple ADC settings to read the Voltage reference and the
|
|||
|
temperature sensor.
|
|||
|
<p>
|
|||
|
Using the factory calibration data I can convert the raw ADC measurement
|
|||
|
into actual Vref. This will help in adjusting the ADC readings. But for
|
|||
|
temperature, the provided calibration is insufficient, there is only one
|
|||
|
point measured in factory for the STM32F030 family members.
|
|||
|
<p>
|
|||
|
<a href="35_calibrate.html">Next</a>, I will do temperature calibration, which
|
|||
|
means taking two measurements as far apart as possible in the working range I
|
|||
|
want to use.
|
|||
|
|
|||
|
<hr>© 2020-2024 Renaud Fivet
|
|||
|
</body>
|
|||
|
</html>
|