1
0
mirror of https://github.com/rfivet/stm32bringup.git synced 2024-12-18 23:06:28 -05:00
stm32bringup/docs/34_adcvnt.html

372 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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
couldnt 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 its 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 &lt;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>