Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 45

ADC, DMA, and I2C LCD: Reading Internal Temperature

Recommended resources

Hello! Welcome to the final lesson in our module on Data Acquisition and DMA.

In our last session, you mastered a powerful technique for safely processing continuous data streams using DMA interrupts and a ping-pong buffer. This allows the CPU to work on a stable set of data while the DMA simultaneously acquires new data, preventing race conditions.

Today, we'll put everything together in a practical mini-project that serves as a capstone for this module. This is a common task in professional firmware development: building a system health monitor. You will integrate several peripherals we've covered—ADC, DMA, Timers, and I2C—to create a functional device.

Your learning outcome is to build a project to read the internal temperature sensor via ADC and DMA, displaying the result on the I2C LCD.

1. Understanding the Internal Sensors

Most modern microcontrollers, including our STM32F103, contain internal sensors that are not connected to any external pins. These are crucial for monitoring the MCU's own state. For this project, we are interested in two:

  • Internal Temperature Sensor (TEMP_SENSOR): This sensor's output voltage changes predictably with the temperature of the silicon die itself. It's not designed for high-accuracy ambient temperature measurement but is excellent for detecting temperature trends, implementing thermal protection (e.g., shutting down a system if the MCU gets too hot), or for coarse temperature readings.
  • Internal Voltage Reference (VREFINT): This is a stable, temperature-compensated 1.2V (typical) internal voltage source.

The Importance of VREFINT for Accurate Measurements

You might wonder why we need to read VREFINT. The reason is that an ADC is a ratiometric device. The digital output it produces is a ratio of the input voltage (Vin) to the analog reference voltage (VDDA).

If the VDDA supply voltage fluctuates (due to changes in load, power supply noise, etc.), the ADC output will change even if the input voltage Vin is perfectly stable. This would make our temperature reading unreliable.

By also measuring the stable VREFINT with the same ADC, we can calculate the actual VDDA at any moment and use it to compensate our temperature reading. This is a fundamental technique for achieving accurate ADC results.

The process is:

  1. Read the raw ADC value for VREFINT.
  2. Since we know VREFINT is a stable ~1.2V, we can rearrange the formula to solve for the actual VDDA.
  3. Use this calculated VDDA to accurately convert the raw ADC value from the temperature sensor into a voltage (VSENSE).
  4. Finally, use a formula from the datasheet to convert VSENSE into degrees Celsius.

The article below provides an excellent overview of this process, including the specific datasheet values for our STM32F103.

STM32 Internal Temperature Sensor Reading - Example Code

Read the following article, 'STM32 Internal Temperature Sensor Reading', from DeepBlue Embedded. It provides the essential datasheet parameters and the formula for converting the sensor's voltage to a temperature reading.

Read the first section, 'STM32 Internal Temperature Sensor'. Pay close attention to the formula provided and the table listing the parameters for the STM32F103: 'Avg_Slope' and 'V25'. Also, note the required sampling time of at least 17µs.

From the article, the key formula is:
\text{Temperature (°C)} = \frac{(V_{25} - V_{SENSE})}{ \text{Avg_Slope}} + 25

Where for the STM32F103:

  • V25 is the sensor voltage at 25°C (typically 1.43V).
  • Avg_Slope is the change in voltage per degree Celsius (typically 4.3mV/°C).
  • VSENSE is the voltage we calculate from the temperature sensor's ADC reading.

2. Project Architecture and CubeMX Setup

We will now design a system that periodically reads the internal sensors and updates the LCD.

  1. A Timer (TIM3) will generate a trigger event once per second (1 Hz).
  2. The timer trigger will start a sequence of ADC conversions on two channels: VREFINT and TEMP_SENSOR.
  3. A DMA channel will automatically transfer the two conversion results into a memory buffer without CPU intervention.
  4. A DMA Transfer Complete interrupt will signal the CPU that a new set of data is ready.
  5. In the main loop, we'll process the data, calculate the temperature, and send it to the I2C LCD.

Let's configure this in STM32CubeMX.

  1. RCC: Configure for HSE and set the system clock to 72 MHz.
  2. I2C1: Enable the I2C peripheral. Use the same settings as in Module 5. The pins are PB6 (SCL) and PB7 (SDA).
  3. TIM3:
    • Set Clock Source to Internal Clock.
    • Calculate Prescaler (PSC) and Counter Period (ARR) to get a 1 Hz update rate. At 72 MHz, a PSC of 7199 and an ARR of 9999 works well. (72,000,000 / (7199+1) / (9999+1) = 1 Hz).
    • Set Trigger Event Selection (TRGO) to Update Event.
  4. ADC1:
    • Enable both the VREFINT Channel and Temperature Sensor Channel.
    • In Parameter Settings:
      • Enable Scan Conversion Mode.
      • Set Number Of Conversion to 2.
      • Set External Trigger Conversion Source to Timer 3 Trigger Out event.
      • For the rank corresponding to the Temperature Sensor, set the Sampling Time to 239.5 Cycles. At a 12MHz ADC clock, this gives a sampling time of ~20µs, safely above the required 17µs minimum. The VREFINT channel can use a shorter sampling time.
    • Go to the DMA Settings tab and Add a DMA channel for ADC1. Set the Mode to Circular and ensure Memory Increment is enabled.
  5. NVIC: Enable the DMA1 channel 1 global interrupt.

The following image shows the ADC1 configuration panel with the correct settings for the two internal channels.

STM32CubeMX ADC Configuration for Internal Temperature Sensor
STM32CubeMX view showing ADC1 configured for the internal Temperature Sensor and Vrefint channels, with Scan Mode enabled, 2 conversions, and Timer 3 as the trigger.

Finally, remember to wire your I2C LCD to the Blue Pill board.

STM32 Blue Pill to I2C LCD Wiring Diagram
Wiring diagram showing the 5V, GND, SCL (PB6), and SDA (PB7) connections between the STM32 Blue Pill and the I2C LCD module.

3. Firmware Implementation and Calculation

Now let's write the code to bring this system to life. The following video provides a complete walkthrough of a very similar project, covering CubeMX setup, HAL code, and debugging. It's an excellent guide to follow.

STM32 Multi ADC DMA method

Watch this video from PR TechTalk to see a demonstration of reading multiple ADC channels, including the internal temperature sensor, using DMA and a timer trigger.

Watch the video, focusing on these key parts: 2:38 - 9:28: The CubeMX configuration for ADC, Timer, and DMA. This will reinforce the steps we just discussed. 9:28 - 12:00: The C code for setting up variables, starting the timer, and starting the ADC with DMA. 12:00 - 14:00: The implementation of the DMA complete callback to set a flag, signaling that data is ready. 20:39 - 24:37: The crucial part where the raw ADC values are converted into meaningful physical units (millivolts and degrees Celsius). Pay close attention to the helper functions used.

Let's implement the code in our main.c file.

Step 1: Includes and Variable Definitions

First, make sure you have the I2C LCD driver files (i2c-lcd.h and i2c-lcd.c) from Module 5 in your project's Core/Inc and Core/Src folders respectively.

In main.c, add the necessary includes and define your global variables.

/* USER CODE BEGIN Includes */



#include "i2c-lcd.h"
#include <stdio.h>
/* USER CODE END Includes */

/* USER CODE BEGIN PV */
// Datasheet values for STM32F103C8



#define AVG_SLOPE_MV_PER_C  4.3f
#define V_AT_25C_MV         1430.0f
#define VREFINT_V           1.2f

volatile uint16_t adc_buf[2]; // Buffer for VREFINT and TEMP_SENSOR
volatile uint8_t conversion_complete_flag = 0;

float temperature_C = 0.0f;
char lcd_buffer[20];
/* USER CODE END PV */

Note: The order of VREFINT and TEMP_SENSOR in adc_buf depends on the Rank you configured in CubeMX. Rank 1 will be at adc_buf[0], and Rank 2 at adc_buf[1]. Let's assume Rank 1 is VREFINT and Rank 2 is TEMP_SENSOR.

Step 2: Initialization

In your main() function, after the peripheral MX_..._Init() calls, initialize the LCD and start the timer and ADC/DMA process.

/* USER CODE BEGIN 2 */
lcd_init();

// Calibrate the ADC
HAL_ADCEx_Calibration_Start(&hadc1);

// Start the timer that triggers the ADC
HAL_TIM_Base_Start(&htim3);

// Start the ADC in DMA mode
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, 2);
/* USER CODE END 2 */

Step 3: Implement the DMA Callback

In the USER CODE BEGIN 4 section, implement the HAL_ADC_ConvCpltCallback function. Its only job is to set a flag to notify the main loop.

/* USER CODE BEGIN 4 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
  conversion_complete_flag = 1;
}
/* USER CODE END 4 */

Step 4: Main Loop for Processing and Display

The while(1) loop will now be very clean. It just waits for the flag, and when set, performs the calculations and updates the display.

/* USER CODE BEGIN WHILE */
while (1)
{
    if (conversion_complete_flag)
    {
        // Reset the flag
        conversion_complete_flag = 0;

        // Separate the raw ADC values
        uint16_t raw_vrefint = adc_buf[0];
        uint16_t raw_vsense = adc_buf[1];

        // --- Perform the calculations ---
        
        // 1. Calculate the actual Vdda (in millivolts)
        float vdda_mv = (VREFINT_V * 1000 * 4095) / raw_vrefint;

        // 2. Convert the raw temperature sensor reading to millivolts
        float vsense_mv = (raw_vsense * vdda_mv) / 4095.0f;
        
        // 3. Calculate the temperature using the datasheet formula
        temperature_C = ((V_AT_25C_MV - vsense_mv) / AVG_SLOPE_MV_PER_C) + 25.0f;
        
        // --- Update the LCD Display ---
        lcd_clear();
        lcd_put_cur(0, 0); // Row 0, Col 0
        sprintf(lcd_buffer, "Temp: %.1f C", temperature_C);
        lcd_send_string(lcd_buffer);

        lcd_put_cur(1, 0); // Row 1, Col 0
        sprintf(lcd_buffer, "Vdda: %.2f V", vdda_mv / 1000.0f);
        lcd_send_string(lcd_buffer);
    }
    /* USER CODE END WHILE */
}

Compile, flash your board, and you should see the temperature and calculated VDDA updating on your LCD once per second! Try touching the MCU chip with your finger; you should see the temperature reading slowly rise.


Conclusion

Congratulations on completing the capstone project for this module! You have successfully built a complete, event-driven system that integrates multiple peripherals (ADC, DMA, Timers, I2C) to perform a real-world task. This project reinforces many of the skills you've acquired throughout this course.

Key Takeaways:

  • Internal sensors like the TEMP_SENSOR and VREFINT are vital for system health monitoring and achieving accurate measurements.
  • Voltage compensation using VREFINT is a professional best practice to ensure your ADC readings are stable even if the power supply fluctuates.
  • The architecture of using a timer to trigger an ADC, which then uses DMA to transfer data, is a highly efficient and common pattern in embedded systems.
  • Combining peripheral drivers (ADC, I2C, Timers) is a core part of firmware development.

Preview of the Next Lesson:
Our application is growing in complexity. We now have sensor processing, display updates, and interrupt handling. If we wanted to add another independent task, like listening for commands on the UART, managing everything with flags and callbacks would quickly become difficult and error-prone. This is where a Real-Time Operating System (RTOS) becomes essential. In our next module, we will introduce the core concepts of an RTOS, which provides a powerful framework for managing multiple tasks concurrently, making your applications more robust, scalable, and easier to maintain.