FB pixel

Renesas RA - 19. Making a Thermocouple-based Thermometer using 24-bit Sigma-Delta ADC, Temperature Sensor and TimerNew

Published


Hello again! Today we will keep learning the analog modules of the RA2A1 family. And this time, we will discover a unique module, present only in this family of the RA MCUs. I’m talking about the 24-bit Sigma-Delta ADC (SDADC).

The Sigma-Delta type of ADC is quite complex, even for me. I understand the basics of its operation but not that deep. If you want, you can read more about it here, where its operation is described quite clearly. Otherwise, take it for granted that there is such a type of ADC that has higher accuracy and resolution but much lower conversion speed than SAR ADC, which we discussed previously.

As for the task for the current tutorial, it took a very long time for me to find a good one, and I was already desperate when Josh gave me a good idea which I finally implemented in this tutorial. I will talk about my struggles later when we consider the SDADC module, and now let me just present to you the current task.

We will create a thermometer based on the K-type thermocouple, which can measure the temperature in the range of -250 to 1200 Centigrades (much wider than the DS18B20 sensor we usually use for home appliances). Why precisely this thermocouple? Because it’s the most widely available. If you have a multimeter that can measure the temperature, most likely, you have such a sensor inside the box (Figure 1).

Figure 1 - K-type thermocouple
Figure 1 - K-type thermocouple

It’s usually marked as TP-01A in the relatively cheap multimeters, though you may have one with another marking. You can notice these markings in Figure 1 (TP-01 to TP-04).

Despite the marking, all these thermocouples are K-type. They only have different measurement limits - TP-01(A) is used in the range of -50 to 400 Centigrades, while others are used in the range of -50 to 750 Centigrades. I assume this difference is conditioned by the assembly quality and used materials.

In this tutorial, I will focus on the thermocouples’ operation principles. You can read about them on Wikipedia or Circuitbread. In Wikipedia, though, there are more details, and a short description of each thermocouple type is given.

As I’ve said, in this tutorial, I will use the K-type, made of two alloys - chromel and alumel. It has an accuracy of 1-2 Centigrades and an almost linear characteristic function. Also, it’s pretty cheap, unlike the ones based on platinum or gold alloys.

If you have any other thermocouple, don’t worry, I will tell you what to change in the program to work with any type.

But before proceeding further, let’s consider the new MCU modules that will be used in this project.

Introduction to 24-bit Sigma-Delta Analog to Digital Converter (SDADC24)

It’s hard to tell which ADC is more complex - ADC16, which we have learned about in the previous tutorial, or the current one.

The RA2A1 family has one SDADC module with five channels that can work in single-ended and differential modes. Four channels have external inputs, and the last one is connected to the outputs of the built-in operational amplifiers, which we will talk about in the next tutorial. As for the conversion modes, they are tricky here, which made me spend several unpleasant hours trying to figure out what was wrong until I decided to read the datasheet very attentively, which eventually helped. So, a piece of advice for you - if you start with some new module even if you know it from other MCUs, don’t be lazy to read the documentation attentively as it may have some unexpected features.

So, about the tricks of the input channels. In the single-ended mode, the input voltage is applied to one of the MCU pins. But unlike normal ADCs, in this one, the second input is not the GND but the biased voltage source of 1V; this means that if you apply 0V to the SDADC input pin, the actual input voltage of it will be -1V (or +1V depending on the settings). Thus, the input voltage should also be biased, which sometimes seems inconvenient for me.

In the differential mode, there is also a limitation that the common mode voltage of the differential input should be greater than 0.2 V and smaller than Vref - 0.2 V. This also means the need for the bias of the input.

Here is the story of my troubles and struggles. You can skip it, as there is not much information about the SDADC module itself, but here are my mistakes, so this may prevent you from making them as well. Initially, I wanted to make a tutorial about measuring the current consumed by the MCU in different operation modes. In the EK-RA2A1 board, there is even a special jumper “CURRENT MEAS” located near the device USB port. I even cut the traces and soldered the shunt resistor there, but when I tried to measure the voltage drop on it (which, as you know, is proportional to the consumed current), the SDADC readings were not correct and always floated near the upper limit of the ADC values. This made me read the documentation attentively, from which I knew this limitation of the common mode voltage. It makes it impossible to measure self-consuming current both at high-end and low-end, which is sad. I tried to shift the common mode voltage with the built-in operational amplifiers, but the external circuit’s accuracy was not enough to provide the same differential voltage (which is just several mV) so I dropped this idea. Then I decided to make a power meter of some external load, providing the sine wave signal to it and measuring the current and the voltage to calculate the power, RMS values, power factor, and other parameters. But this idea also failed because I needed to provide more current from the DAC output. Even with the built-in operational amplifier working in the voltage follower mode, the max load current is just 100 uA (read the manuals attentively, remember?), which is very few. So I dropped this idea as well. Finally, with a hint from Josh, I got to the idea of measuring the thermocouple voltage. This idea also was manageable, as the voltage produced by the thermocouple is just several tens of mV, so the bias was still needed. But it could be applied relatively simply, but I will talk about it later. And now, let’s return to the SDADC module.

It can operate in two modes - normal (with an oversampling frequency of 1 MHz) and low-power (with an oversampling frequency of 125 kHz).

The SDADC module has a selectable reference voltage source of 0.8 V to 2.4 V with a step of 0.2 V. The greatest value of 2.4 V is achievable with the external Vref source, while others use the internal source.

There is a two-stage programmable gain amplifier (PGA) in the SDADC module. Each stage has the programmable gain of x1, x2, x4, or x8, but the total maximum gain can be only x32. Also, the second PGA has a programmable voltage offset of -164 mV to +164 mV (which doesn’t save you from having the input common-mode voltage bias). This PGA offset can be measured as a self-diagnosis. Also, the PGA is used in the disconnection detection circuit. In the single-ended mode, only the x1 gain is applicable.

Unlike ADC16, where each channel has its own conversion result register, SDADC has only one such register, and the number of channels from which this result was obtained can be read in another register (which is also weird for me). When the conversion is completed, the SDADC module generates the interrupt. The conversion can be started by the software or by the ELC module. SDADC can operate in continuous and single scan modes.

You can select the oversampling ratio for each channel independently from the following list: 64, 128, 256, 512, 1024, or 2048. For the single-ended mode, only the ratio of 256 is applicable. As I understood, the oversampling ratio is the number of readings of the Sigma-Delta modulator. Increasing it improves the measurement accuracy but reduces the conversion speed. With the oversampling frequency of 1 MHz and the oversampling ratio of 64, the max sampling rate is 1000 kHz / 64 = 15.625 kHz which is relatively small in comparison to the ADC16 module.

In the SDADC module, there is such a feature as AD conversion count. It means you can set the number of conversions of each channel before switching to the next one. This number is quite broad - 1 to 8032. This feature can be useful when using the DTC module to read the conversion result. For example, you can set five consequent readings in channel 1, then two in channel 2, and one in channel 3.

Also, SDADC has an averaging feature, which means you can average the consequent readings using the number of readings from this list: 8, 16, 32, or 64.

The SDADC module can perform the calibration (gain error and offset error). The calibration is mandatory when you select the differential mode.

These are the main features and options of the SDADC module (at least it will be enough for you to understand what’s going on in the program). For more details, please refer to Chapter 33 of the RA2A1 User’s Manual.

Introduction to the temperature sensor (TSN)

The on-chip temperature sensor is used to monitor the die temperature to prevent the chip from overheating. It is an analog sensor that produces the output voltage proportional to the temperature. The dependency between the temperature and the voltage is linear. The sensor’s output is connected to one of the inputs of the ADC16 multiplexer, and thus its voltage can be read by the ADC16 module.

The temperature can be calculated using the formula provided in Chapter 36 of the RA2A1 User’s Manual:

T = (Vs - V1) / Slope + T1.

Where:

T is the measured temperature (°C)
Vs is the voltage output by the temperature sensor when temperature is measured (V)
T1 is the temperature experimentally measured at one point (°C)
V1 is the voltage output by the temperature sensor when T1 is measured (V)
Slope = (V2 - V1) / (T2 - T1) is the temperature gradient of the temperature sensor (V / °C)
T2 is the temperature experimentally measured at a second point (°C)
V2 is the voltage output by the temperature sensor when T2 is measured (V)

So you see, to calculate the temperature, you need two additional reference points at which you know the temperature and can measure the sensor’s output voltage. If you need excellent accuracy, you can do this. Otherwise, you can use the default values. In Table 47.56 you can find that the Slope is -3.65 mV / °C.

Also, there are two registers in the RA2A1 MCU. TheTSCDRH and TSCDRL. Which consist of the calibrated value of the voltage provided by the sensor at a temperature of 150 °C. This value is individual for each part of the MCU and can be considered quite accurate.

Using these registers, you can calculate the temperature as follows:

First, we need to read the values of the TSCDRH and TSCDRL registers and join them into one 16-bit value CAL125:

CAL125 = (TSCDRH << 8) + TSCDRL

V125 is calculated from CAL125 as follows:

V125 = 3.3 x CAL125 / 32768 [V]

The value 3.3 is the AVCC0 voltage.

Using this value, the measured temperature can be calculated according to the following formula:

T = (Vs - V125) / Slope + 125 [°C].

In this formula, you shouldn't forget to divide the Slope value of -3.65 mV / °C by 1000 to meet the voltage unit compatibility.

That’s all about the internal temperature sensor and the new modules, so now we can move to the schematic diagram of the device.

Schematic Diagram of the Thermometer

Figure 2 - Schematic diagram of the device
Figure 2 - Schematic diagram of the device

The schematic diagram is still quite simple and similar to the one we tackled in tutorial 18. This time we’re not using the OLED display, so we’ve eliminated it. Even though SDADC doesn’t use external reference voltage in our project, we still need it, as we will use the internal temperature sensor connected to the ADC16 module. Here we will use the external source, which is applied to pins VREFH0 and VREFL0, so we need to connect them to AVCC0 and AVSS0 pins, respectively.

The thermocouple T1 is connected to pins P104 and P105, merged with the positive and negative inputs of channel 2 of the SDADC, respectively. You may ask why I used channel 2 and not channel 0 or 1. It’s because the pins are merged with these channels have other internal connections in the board, which can affect the measurement results. And P104 and P105 are not connected anywhere else.

As I said, for proper operation of the SDADC, we need to shift the input voltage level, so the input range lies in the boundaries from 0.2 V to Vref - 0.2 V. We make this shift artificially using the voltage divider R1-R2. As you see, the resistors have equal values, so they divide the input voltage in half. They are connected between pins SBIAS/AVREF and AVSS1. The first pin is the output of the internal reference voltage source of the SDADC module, and the second one is connected to the common ground. So the divider shifts the input voltage to the level of half of the reference voltage, which suits us at any value of the latter. You can see that the voltage from the divider goes to the negative input of SDADC along with the negative output of the thermocouple. This connection works as a series connection of two voltage sources - one is the voltage divider and another is the thermocouple. These two voltages are summed, which provides the required offset for SDADC. But this affects only the common mode voltage. The differential one (generated by the thermocouple) remains the same, and this is exactly what we need.

I never mentioned why we needed the internal temperature sensor and took it for granted. If you have read the theory of operation of the thermocouples, you should know that the voltage it generates depends on the temperatures of the hot and cold junctions. The hot junction measures the required temperature, while the cold junction usually has the environmental temperature. So we need to measure this environmental temperature to compensate for the cold junction. And we do this with the internal temperature sensor. This is not a very good idea in actual thermometers for several reasons. First, the cold junction can be located far from the MCU and have a different temperature. And second, due to the self-heating of the MCU, its die temperature can differ from the environmental one. So in real thermometers, it’s better to use the external sensor located right at the cold junction (you can use the DS18B20 for this), but even the internal sensor is enough for educational purposes.

Project Creation and Configuration

Now we have enough information to start with the project. So let’s open e2 studio and create a regular project based on C language using the “Bare Metal - Minimal” template.

This time let’s break the order and first switch to the “Clock” tab to change the PCLKD clock frequency from 48 MHz to 24 MHz (the same as we did in the previous tutorial) to avoid the problems with the ADC16 clocking (fig. 3).

Figure 3 - Changing the PCLKD frequency
Figure 3 - Changing the PCLKD frequency

And now, let’s switch to the “Pins” clock and configure the SDADC inputs. To do this, we need to scroll down the “Pin selection” list until we find the “Analog: SDADC” list. When we expand it, we will see only the position “SDADC0” which we need to select and then configure according to Figure 4.

Figure 4 - SDADC0 module pin configuration
Figure 4 - SDADC0 module pin configuration

As you see, we need to change the “Operation Mode” from “Disabled” to “Custom,” after which the changing of the ANDSxx pins will become available. As I mentioned, we need to enable the pins P104 and P105, which are merged with the SDADC channel 2 pins ANDSP2 and ANDSN2, respectively.

And that’s all about the pins configuration; everything else is connected inside the MCU. So we can switch to the “Stacks” tab and add the required stacks. Let’s consider which ones we will need this time. As I already mentioned, we will need the ADC16 and SDADC modules. Also, we will use some timer to trigger the conversion start of both ADCs. To enable the hardware triggering, we will need the ELC stack. And that’s kind of it! Let’s first start with the modules which we are already familiar with.

We have already used the ELC module several times, so you should already know how to add it. And if you don’t, please refer to tutorial 14. I will not add any pictures about ELC as it doesn’t have any configurable parameters, so please don’t miss adding it!

Next, let’s add the timer. For a change, we will use the “Timer, Low-power (r_agt)” instead of the GPT we got to use. There is no particular reason why we do this, so you can use the GPT if you want; your configuration will be slightly different from mine. After adding, we need to configure the AGT timer according to Figure 5.

Figure 5 - Configuration of the AGT stack
Figure 5 - Configuration of the AGT stack

The AGT timer was discussed in detail in Tutorial 15, so please refer to it for more information. Here we set the period of the timer as 1 second. Such a period can’t be reached using the PCLKB source, which is 24 MHz (see Figure 3) because the AGT is just 16 bit. But it can be easily reached with the LOCO source, which is just 32768 Hz, so we need to change the “Count Source” field from “PCLKB” to “LOCO.” Also, we need to enable the timer underflow interrupt by changing the “Underflow Interrupt Priority” from “Disabled” to “Priority 2” (or any other priority by your wish). This interrupt will be used only by the ELC module to start the ADC16 and SDADC conversions, so we don’t need to declare any callback function and leave the “Callback” field as “NULL.”

Now, let’s add the “ADC (r_adc)” stack, the same as we did in the previous tutorial. Even, its configuration is very similar. The only difference is that instead of Channel 0, we will enable the internal channel “Temperature Sensor” (Figures 6, 7).

Figure 6 - Configuration of the ADC stack (part 1)
Figure 6 - Configuration of the ADC stack (part 1)
Figure 7 - Configuration of the ADC stack (part 2)
Figure 7 - Configuration of the ADC stack (part 2)

So we need to change the “Mode” from “Single Scan” to “Group Scan” to be able to use the hardware trigger of the conversion starting from the AGT underflow signal. Then in the “Channel Scan Mask” list, we need to select only the “Temperature Sensor” channel and deselect the rest. We don’t need to make any changes in the lists that are not expanded in Figure 7. We will make a lot of changes in the “Interrupts” list. First, we need to change the “Normal/Group A Trigger” and “Group B Trigger” from “Disabled” to “AGT0 INT (AGT Interrupt)”. Even though we don’t use Group B, the ADC stack shows an error if we don’t assign its trigger, so in the “Group B Trigger,” you could select any available source.

This time we will not use the DTC module to copy the data from the AD conversion result register into the RAM as there is no rush; the sampling period is just 1 second, during which we can do a lot of actions and calculations (spoiler alert: and we will do them). So when the conversion is completed, we will generate an interrupt and read the conversion result afterward. To do this, we need to enable the “Scan End Interrupt Priority” and “Scan End Group B Interrupt Priority” (the latter is not needed in this project, but if we don’t enable it, the ADC stack shows the error). Also, we need to declare the callback function, so we change the “Callback” field from “NULL” to some name, for example, “adc_callback.” There are all changes in the ADC stack we need to do.

Now we will add and configure the new stack - SDADC. It can be found in the “Analog” list and is called “ADC (r_sdadc),” which is very similar to the previous one, so please don’t mix them up (Figure 8).

Figure 8 - Adding the SDADC stack
Figure 8 - Adding the SDADC stack

Don’t be afraid of the monstrous look of the appeared stack; it’s quite simple in fact. The big block at the top, called “g_adc1 ADC (r_sdadc),” consists of the common setting of the SDADC module. The five smaller blocks under the main block represent the input channels of SDADC. By default, no channels are selected, and the main block shows the error. As I mentioned before, we will use channel 2, so click on the block with the text “Add Configuration for Channel 2 [Optional, only if used]” and in the drop-down menu “New >” select the only available point “SDADC Channel Configuration (r_sdadc)” (Figure 9).

Figure 9 - Adding the new SDADC Channel Configuration
Figure 9 - Adding the new SDADC Channel Configuration

After that, the name of the block will change to “SDADC Channel2 Configuration (r_sdadc),” but the error will still remain. Don’t worry; we will get rid of it soon. First, select the big upper block “g_adc1 ADC (r_sdadc)” and configure it according to Figure 10.

Figure 10 - Configuration of the SDADC module
Figure 10 - Configuration of the SDADC module

As you can see, the number of settings is not that large. Let’s briefly consider them all.

  • “Name,” as usual, means the name of the current SDADC stack in the program. We can leave it as default.
  • “Mode” sets the scanning mode. In single scan mode, all channels are converted once per start trigger, and conversion stops after all enabled channels are scanned. In continuous scan mode, the conversion starts after a start trigger, then continues until stopped in the software. As we read the temperature once per second, we should change this value from “Continuous Scan” to “Single Scan”.
  • “Resolution” specifies that the resolution of the SDADC conversion result can be 16-bit or 24-bit. In our case, we need the high resolution as the thermocouple voltage is very low, so we leave this field as “24 Bit”.
  • “Alignment” defines the alignment of the result within the 32-bit value. If the “Right” is selected, then the result is aligned right, and the top 8 bits remain unused. If the “Left” is selected, then the result is aligned left, and the lower 8 bits remain unused. Here I selected the latter option, and I will explain why. The thing is that for the differential mode, the result represents the signed value in 2’s complement format. In this format, the MSB of the result represents the sign. If we use the alignment right, then the MSB of the result is the 23rd bit of the 32-bit number, which is not the MSB of it, so we need to take some actions to distinguish the sign bit. If the result is aligned left, then the MSB of the result and the MSB of the 32-bit number match and are the 31st-bit. In this case, to get the correct value, we just need to divide the number by 256, not caring about the sign.
  • “Trigger” selects conversion start trigger. It can be started by the software (default option) or by hardware events using the ELC module. In our case, we want the AGT0 timer to start the conversion, so we select the same trigger as for the ADC16 module - “AGT0 INT (AGT Interrupt)”.
  • “Vref Source” selects the source of the reference voltage of the SDADC module. We will use the internal source, so we leave this field unchanged.
  • “Vref Voltage” select Vref voltage. If Vref is input externally, the voltage on VREFI must match the voltage selected within 3%. In our case, we select the value “1.0 V”. We don’t need the higher Vref as the thermocouple max voltage is just about 50 mV. Actually, we even could use the value of “0.8 V” but 1.0 is better for the calculations.
  • “Callback” specifies the name of the SDADC callback function. We will use it in our program, so I set it as “sdadc_callback,” but you can select the name you like more.
  • “Conversion End Interrupt Priority” sets the priority of the conversion end interrupt. You may notice that this field’s value is already selected as “Priority 2,” as this interrupt is mandatory.
  • “Scan End Interrupt Priority” sets the priority of the scan end interrupt. The difference between this interrupt and the previous one is that this one is generated when all the selected channels have finished the conversion, and the “Conversion End” interrupt is generated after every conversion at whichever channel.
  • “Calibration End Interrupt Priority” sets the priority of the calibration end interrupt. As I mentioned when describing the SDADC module, if we use the input channel in the differential mode (which we do), the calibration is mandatory, and thus we also must enable this interrupt. Also, you can see that after doing this, the error of the SDADC block disappears.

This is all about the configuration of the SDADC module itself. Now, let’s select the “SDADC Channel2 Configuration (r_sdadc)” block and configure it according to Figure 11.

Figure 11 - Configuration of the SDADC channel
Figure 11 - Configuration of the SDADC channel

This block has even fewer options than the main one.

  • “Input” selects the type of the input - “Single-Ended” or “Differential.” In our case, it is differential, so we leave this field unchanged.
  • “Stage 1 Gain” and “Stage 2 Gain” set the gain for stages 1 and 2 of the PGA, respectively. Initially, I thought of increasing the gain and thus the accuracy of the measurement but then calculated that if the Vref is 1 V, then the LSB of the conversion result is 1 x 1000 000 uV / 224 = 0.06 uV; and for the K-type of the thermocouple the average slope is about 35uV / °C, so there is no need for the additional amplification.
  • “Oversampling Ratio” selects the oversampling ratio for the SDADC. It must be 256 for single-ended input. A higher value of the oversampling ratio (OSR) increases the signal-to-noise (SNR) and signal-to-noise and distortion ratio (SINAD) so, as we are not in a rush, let’s set the OSR as high as possible - 2048.
  • “Polarity (Valid for Single-Ended Input Only)” selects positive or negative polarity for single-ended input. VBIAS (1.0 V typical) is connected to the opposite input. As we’re using the differential mode, we can leave this field with no changes.
  • “Conversions to Average per Result” selects the number of conversions to the average for each result. The ADC_EVENT_CONVERSION_END event occurs after each average or after each individual conversion if averaging is disabled. Let’s also set the maximum available option “Average64” to reduce the noise.
  • “Invert (Valid for Negative Single-Ended Input Only)” selects whether to invert negative single-ended input. When the result is inverted, the lowest measurable voltage gives a result of 0, and the highest measurable voltage gives a result of 2 ^ resolution - 1. We shouldn’t care about this option for now.
  • “Number of Conversions Per Scan” sets the number of conversions on this channel before AUTOSCAN moves to the next channel. When all conversions of all channels are complete, the ADC_EVENT_SCAN_END event occurs. Here we don’t need to perform several conversions simultaneously, so let’s leave this field as “1”.

And that’s all about the SDADC stack configuration. For more information, please refer to the corresponding page of the FSP documentation. As you can see, it has much fewer options than ADC16. But before proceeding to the programming code, let’s check a few things first. The whole stack configuration should look like in Figure 12.

Figure 12 - Stacks configuration
Figure 12 - Stacks configuration

Also, let’s switch to the “Event Links” tab and ensure that the required links are established (Figure 13).

Figure 13 - Event links allocation
Figure 13 - Event links allocation

As you can see, both ADC16 and SDADC are linked to the AGT0 INT (AGT interrupt) event, so everything is configured correctly.

And now that’s it. So we can press the button “Generate Project Content” and proceed to the program code.

Program Code of the Project

Before we proceed with creating our own code, we need to add the files for supporting the RTT. I have explained in detail how to do this in Tutorial 14, so please refer to it. Also, you can just copy and paste the RTT-related files from the project of Tutorial 14 (if you have them) into the current project.

Now, let’s open the “hal_entry.cpp” file and write the following code in it.

#include "hal_data.h"

#include "SEGGER_RTT.h"

#include <math.h>

FSP_CPP_HEADER

void R_BSP_WarmStart(bsp_warm_start_event_t event);

FSP_CPP_FOOTER

float temperature; //Final calculated temperature

float cold_end_voltage; //Voltage generated by the thermocouple at the cold junction temperature

volatile uint8_t calibration_complete;//Flag indicating that the SDADC calibration is complete

volatile uint8_t sdadc_conversion_complete;//Flag indicating that the SDADC conversion is complete

volatile uint8_t adc_conversion_complete;//Flag indicating that the ADC16 conversion is complete

//Coefficients to calculate the temperature from the thermo-EMF

const float vr[] = {-6.404f, -3.554f, 4.096f, 16.397f, 33.275f, 69.553f};

const float t0[] = {-1.2147164E+02f, -8.7935962E+00f, 3.1018976E+02f, 6.0572562E+02f, 1.0184705E+03f};

const float v0[] = {-4.1790858E+00f, -3.4489914E-01f, 1.2631386E+01f, 2.5148718E+01f, 4.1993851E+01f};

const float p1[] = { 3.6069513E+01f, 2.5678719E+01f, 2.4061949E+01f, 2.3539401E+01f, 2.5783239E+01f};

const float p2[] = { 3.0722076E+01f, -4.9887904E-01f, 4.0158622E+00f, 4.6547228E-02f,-1.8363403E+00f};

const float p3[] = { 7.7913860E+00f, -4.4705222E-01f, 2.6853917E-01f, 1.3444400E-02f, 5.6176662E-02f};

const float p4[] = { 5.2593991E-01f, -4.4869203E-02f,-9.7188544E-03f, 5.9236853E-04f, 1.8532400E-04f};

const float q1[] = { 9.3939547E-01f, 2.3893439E-04f, 1.6995872E-01f, 8.3445513E-04f,-7.4803355E-02f};

const float q2[] = { 2.7791285E-01f, -2.0397750E-02f, 1.1413069E-02f, 4.6121445E-04f, 2.3841860E-03f};

const float q3[] = { 2.5163349E-02f, -1.8424107E-03f,-3.9275155E-04f, 2.5488122E-05f, 0.0f};

void sdadc_callback (adc_callback_args_t *p_args) //SDADC interrupt callback

{

if (p_args->event == ADC_EVENT_CALIBRATION_COMPLETE) //If interrupt was caused by the Calibration complete event

{

calibration_complete = 1; //Set the flag calibration_complete

}

else if (p_args->event == ADC_EVENT_CONVERSION_COMPLETE)//If interrupt was caused by the Conversion complete event

{

sdadc_conversion_complete = 1; //Set the flag sdadc_conversion_complete

}

}

void adc_callback (adc_callback_args_t *p_args) //ADC16 interrupt callback

{

if (p_args->event == ADC_EVENT_SCAN_COMPLETE)//If interrupt was caused by the Scan complete event

{

adc_conversion_complete = 1; //Set the flag adc_conversion_complete

}

}

/*******************************************************************************************************************//**

* main() is generated by the RA Configuration editor and is used to generate threads if an RTOS is used. This function

* is called by main() when no RTOS is used.

**********************************************************************************************************************/

void hal_entry(void)

{

int cal125 = (R_TSN->TSCDRH << 8) + R_TSN->TSCDRL; //Read the calibrated ADC value produced by the temperature sensor at 125 deg. C

R_ELC_Open(&g_elc_ctrl, &g_elc_cfg); //Open the ELC stack

R_ELC_Enable(&g_elc_ctrl); //Enable ELC

R_AGT_Open(&g_timer0_ctrl, &g_timer0_cfg); //Open the AGT stack

R_AGT_Start(&g_timer0_ctrl); //Start AGT counting

R_SDADC_Open(&g_adc1_ctrl, &g_adc1_cfg); //Open the SDADC stack

R_SDADC_ScanCfg (&g_adc1_ctrl, &g_adc1_channel_cfg); //Configure the input channels

sdadc_calibrate_args_t calibrate_args; //SDADC calibration parameters

calibrate_args.mode = SDADC_CALIBRATION_INTERNAL_GAIN_OFFSET; //Calibrate internal gain and offset error

calibrate_args.channel = ADC_CHANNEL_2; //Select channel 2 for calibration

calibration_complete = 0; //Reset the calibration_complete flag

R_SDADC_Calibrate(&g_adc1_ctrl, &calibrate_args);//Start calibration of Channel 2 of SDADC

while (!calibration_complete); //Wait while calibration is not completed

R_SDADC_ScanStart(&g_adc1_ctrl); //Start scanning waiting for the selected trigger event

R_ADC_Open(&g_adc0_ctrl, &g_adc0_cfg); //Open the ADC stack

R_ADC_ScanCfg(&g_adc0_ctrl, &g_adc0_channel_cfg);//Configure the ADC channels

R_ADC_ScanStart(&g_adc0_ctrl); //Start scanning waiting for the selected trigger event

while (1)

{

if (adc_conversion_complete) //If ADC16 conversion is completed

{

uint16_t vs; //Current voltage of the internal temperature sensor

float t_cj; //Temperature of the cold junction

R_ADC_Read(&g_adc0_ctrl, ADC_CHANNEL_TEMPERATURE, &vs); //Read the current ADC conversion complete result

t_cj = ((cal125 - vs) * 3.3f / 32768) / (3.65f / 1000) + 125; //Calculate the temperature of the cold junction

SEGGER_RTT_printf(0, "Cold end temperature is %d C\r\n", lroundf(t_cj)); //Send the temperature of the cold junction to the RTT Viewer

cold_end_voltage = 1.0003453E+00f + (t_cj - 25.0f) * (4.0514854E-02f + (t_cj - 25.0f) * (-3.8789638E-05f + (t_cj - 25.0f) * (-2.8608478E-06f - 9.5367041E-10f * (t_cj - 25.0f)))) / (1 + (t_cj - 25.0f) * (-1.3948675E-03f - 6.7976627E-05f * (t_cj - 25.0f))); //Calculate the thermo-EMF of the thermocouple at the cold junction temperature

adc_conversion_complete = 0; //Clear the adc_convestion_complete flag

}

if (sdadc_conversion_complete) //If SDADC conversion is completed

{

int32_t adc_reading; //Variable for reading the SDADC conversion result

R_SDADC_Read32(&g_adc1_ctrl, ADC_CHANNEL_2, &adc_reading);//Read the SDADC Channel 2 conversion result

float v = ((adc_reading / 256) * 1000.0f)/ 16777216 + cold_end_voltage; //Calculate the thermo-EMF generated by the thermocouple

for (uint8_t i = 0; i < 5; i ++) //Loop to find the proper coefficients to calculate the temperature

{

if ((v >= vr[i]) && (v < vr[i + 1])) //Search in which voltage range the current thermo-EMF lies

{

temperature = t0[i] + (((v - v0[i]) * (p1[i] + (v - v0[i]) * (p2[i] + (v - v0[i]) * (p3[i] + p4[i] * (v - v0[i]))))))/(1 + (v - v0[i])*(q1[i] + (v - v0[i]) * (q2[i] + q3[i] * (v - v0[i])))); //Calculate the temperature using the polynomial formula

int32_t temp = lroundf(temperature * 10); //Calculate the integer value of the temperature to send it to the RTT Viewer

SEGGER_RTT_printf(0, "Hot end temperature is %d.%u C\r\n\r\n", temp / 10, temp % 10); //Send the temperature value to the RTT Viewer

break;

}

}

sdadc_conversion_complete = 0; //Clear the sdadc_conversion_complete flag

}

}

#if BSP_TZ_SECURE_BUILD

/* Enter non-secure code */

R_BSP_NonSecureEnter();

#endif

}

This program is short, and not that complex despite the monstrous formulas in lines 83 and 95. But let’s take it in order.

In lines 1-3, we include the required header files. File “SEGGER_RTT.h” is needed to use Segger RTT interface. We have already used it several times in the previous tutorials. File “math.h” is necessary to use the floating number rounding functions.

In lines 9-24 we declare the global variables and constants:

  • temperature (line 9) is the final temperature value after all measurements and calculations;
  • cold_end_voltage (line 10) is the thermo-EMF generated by the thermocouple at the temperature of the cold junction.
  • calibration_complete (line 11) is the flag that indicates that the SDADC channel calibration is completed.
  • sdadc_conversion_complete (line 12) is the flag that indicates that the SDADC conversion is completed.
  • adc_conversion_complete (line 13) is the flag that indicates that the ADC16 conversion is completed.
  • vr, t0, v0, p1, p2, p3, p4, q1, q2, q3 (lines 15-24) are the coefficients that are used to convert the thermo-EMF generated by the thermocouple into the temperature using the piecewise polynomial approximation (just leave it for now, I will explain everything later).

In lines 26-36, there is an SDADC callback function. Its name must be the same as in the SDADC stack configuration (Figure 10). The SDADC interrupt can be caused by either the conversion complete event or the calibration complete event. So in line 28, we first check if the event that caused the interrupt was ADC_EVENT_CALIBRATION_COMPLETE. If so, we set the calibration_complete flag (line 30). Then we check if the event that caused the interrupt was ADC_EVENT_CONVERSION_COMPLETE (line 32), and in this case, we set the sdadc_conversion_complete flag. We will check the state of these flags further in the program and process them.

In lines 38-44, there is an ADC16 callback function whose name must correspond to the ADC16 stack configuration (Figure 7). The ADC16 interrupt can be caused by the Group A scan complete event or Group B scan complete event (we do not need the latter one). In line 40, we check if the event that caused the interrupt was ADC_EVENT_SCAN_COMPLETE. If so, we set the adc_conversion_complete flag (line 42), which also will be processed further in the program.

In lines 50-109, the program’s main function is hal_entry.

In line 52, we declare the variable cal125 and assign the expression (R_TSN->TSCDRH << 8) + R_TSN->TSCDRL to it. When describing the temperature sensor, I already told you that TSCDRH and TSCDRL registers contain the calibrated voltage generated by the sensor at the temperature of 125 °C. So we will use this variable in calculating the chip temperature.

In lines 54-55, we open and enable the ELC stack, respectively. In line 57, we open the AGT stack, and in line 58, we start the AGT counting. Underflow of the AGT counter will trigger the ADC16 and SDADC conversions, as you remember, I hope.

In lines 60-68, we initialize the SDADC stack. First, as usual, we need to open the stack using the R_SDADC_Open function (line 60), which has two parameters - the pointer to the SDADC instance g_adc1_ctrl and the pointer to the configuration structure g_adc1_cfg. Then, we call the function R_SDADC_ScanCfg (line 61), which configures and enables the channels of the SDADC. This function also has two parameters - the same pointer to the SDADC instance g_adc1_ctrl and the pointer to the channels configuration structure &g_adc1_channel_cfg.

In line 62, we declare the variable calibrate_args of the sdadc_calibrate_args_t type, which will be used for configuring the parameters of the SDADC channel offset and gain calibration. In line 63, we set the calibration mode as SDADC_CALIBRATION_INTERNAL_GAIN_OFFSET, which means that we are going to calibrate the internal gain and offset error. The other options are SDADC_CALIBRATION_EXTERNAL_OFFSET and SDADC_CALIBRATION_EXTERNAL_GAIN and are used if the external reference voltage source is used. In line 64, we set the calibration channel as ADC_CHANNEL_2, which apparently means that we’re going to calibrate Channel 2, which we use in our project.

In line 65, we clear the calibration_complete flag and run the calibration process invoking the function R_SDADC_Calibrate (line 66). It has two arguments - the pointer to the SDADC instance g_adc1_ctrl and the pointer to the calibration parameters structure &calibrate_args.

When the calibration is completed, the corresponding interrupt is triggered, which causes invoking of the sdadc_callback function with the event ADC_EVENT_CALIBRATION_COMPLETE, which in its turn causes the setting of the calibration_complete flag. We are waiting while this flag is set in line 67, after which we invoke the R_SDADC_ScanStart function (line 68), which either starts a software scan or enables the hardware trigger for a scan depending on how the triggers were configured in Visual FSP Configurator. If the unit was configured for ELC or external hardware triggering, this function allows the trigger signal to reach the SDADC unit. After that, the SDADC stack is totally configured and ready for work.

In lines 70-72, we initialize the ADC16 stack in the same way as we did in the previous tutorial - open the ADC16 stack (line 70), configure and enable the channels (line 71), and start the scanning waiting for the hardware trigger from the ELC module (line 72).

In lines 74-102, the main loop is located inside, which we’re waiting for while either the adc_conversion_complete (line 76) or sdadc_conversion_complete (line 86) flag is set.

Before proceeding with the code, I need to explain how the temperature measurement with the thermocouple is done. You have probably already read about this at the links I provided you at the beginning of this tutorial, but if not, I will briefly explain it here so you can understand the code.

The thermo-EMF generated by the thermocouple depends on two temperatures - the temperature of the cold junction t0 and the measured temperature t (where the hot junction is located). But it’s not convenient to have two unknown values in one equation, so it was agreed that the thermo-EMF of the thermocouple would be measured when the temperature of the cold junction is 0 °C. For such dependency, special reference tables were created for each thermocouple type. For example, you can find such for the K-type thermocouple here. This table shows the correspondence of the thermo-EMF and the temperature for each degree from -270 °C to 1373 °C. Sure, you can copy this table into the MCU memory and use it (and you can do so if you need speedy conversion), but it’s better to have the formulas of the type

t = f(E(t,0))

Where t is the measured temperature, f is some function, and E(t,0) is the thermo-EMF the thermocouple generates when the hot junction is at temperature t, and the cold junction is at 0 °C. I found such a formula here:

or

It looks a bit monstrous (hmm… it looks like I’m using this word too often in this tutorial), but let’s try to understand it. Here, T is the measured temperature, and v is the corresponding thermo-EMF E(t,0). Other parameters are just the coefficients. Also, the whole range is divided into five parts, each of which has different values of these coefficients. Such a huge polynomial is used to increase the accuracy of the conversion to match the reference table as close as possible. So now you can understand which coefficients I declared in lines 15-24.

Actually, vr is line 15 and is not a coefficient. This array consists of the ranges of the thermo-EMF within which the corresponding coefficients from the following arrays are applicable.

And now, only the last thing to consider is left. As I already said, the reference tables consist of the thermo-EMF, which corresponds to the cold junction temperature of 0 °C. But in fact, it has some different temperature t0 in most cases, so the EMF generated by the thermocouple in normal condition E(t, t0) will be less than E(t, 0) by the value E(t0, 0), or

E(t, t0) = E(t, 0) - E(t0, 0)

From which:

E(t, 0) = E(t, t0) + E(t0, 0)

From the program’s perspective, E(t, t0) is the voltage generated by the thermocouple and measured by the SDADC module; t0 is the temperature measured by the internal temperature sensor. Knowing it, we can calculate the E(t0, 0) using the same reference table or another equation which is provided here:

This equation is very similar to the one I provided before. Here vCJ is the E(t0, 0), TCJ is the t0, and other values are coefficients. Warning! These coefficients, even though they have the same name, are different from those in the previous equation.

Finally, the algorithm of the temperature measurement is the following.

  1. Measure the temperature of the cold junction t0
  2. Using the last formula, calculate the thermo-EMF at the cold junction temperature E(t0, 0).
  3. Measure the voltage generated by the thermocouple E(t, t0)
  4. Sum the values E(t, t0) and E(t0, 0) to get the value E(t, 0).
  5. Find in which range the E(t, 0) lies, and calculate the temperature t using the previous big formula with the corresponding coefficients.

Now it will be easy to understand the code. So, let’s return to it.

In line 78, we declare the variable vs., representing the voltage generated by the internal temperature sensor. In line 79, we declare the variable t_cj, which is the temperature of the cold junction, and in fact, the temperature of the internal sensor.

In line 80, we read the ADC16 conversion result of the internal temperature sensor channel using the function R_ADC_Read. This function has three arguments - the pointer to the ADC16 instance g_adc0_ctrl, the ADC channel, which in our case is called ADC_CHANNEL_TEMPERATURE, and the pointer to the variable into which the result will be copied, in our case, its vs.

In line 81, we calculate the temperature value using a formula similar to the one I already provided in the description of the temperature sensor. Compare this formula

T = (Vs - V125) / Slope + 125

with line 81

t_cj = ((cal125 - vs) * 3.3f / 32768) / (3.65f / 1000) + 125

As I mentioned, the Slope value is -3.65 mV / °C, or (-3.65 / 1000) V / °C, which you can notice in line 81. The minus sign of the Slope changes the order in the first brackets, so instead of (vs - cal125), we have (cal125 - vs). The factor 3.3 / 32768 converts the ADC reading from the code into Volts, where 3.3 V is the AVCC voltage. You may wonder what the letter “f” at the end of the number means. It implicitly casts the constant floating point number from the double type (which is the default) to the float type. Calculations with double precision take a lot of time and resources, and we don’t need such high accuracy; that’s why we use the float type everywhere.

In line 82, we send the t_cj value via the RTT interface using the SEGGER_RTT_printf function. Unfortunately, it can’t operate with the floating point numbers. That’s why we use the lroundf function to round the t_cj value to the nearest integer number. The lroundf function is declared inside the “math.h” header file; we have included it into the project.

In line 83, we calculate the voltage that generates the thermocouple at the cold junction temperature cold_end_voltage using this page’s last large formula and the coefficients for the type K thermocouple. The cold_end_voltage represents the E(t0,0) from the formula above.

In line 84, we clear the adc_conversion_complete flag and wait while it is set again in the adc_callback function.

In line 86, we check if the SDADC conversion is complete, if so; lines 88-101 are executed.

In line 88, we declare the variable adc_reading, which will be used to read the rare SDADC value. In line 89, we invoke the R_SDADC_Read32 function, which by its structure, is the same as the R_ADC_Read function described before. The difference is that R_SDADC_Read32 is applicable for SDADC, and the result conversion is 32-bit.

In line 89, we calculate the reference value of the thermo-EMF of the thermocouple:

v = ((adc_reading / 256) * 1000.0f) / 16777216 + cold_end_voltage

First, we need to divide the adc_reading by 256 as the conversion result is left aligned (see Figure 10). In this case, we can’t use just the shift by 8 bits to the right as this will lead to losing the sign, and it is important. Then we use the regular conversion from the ADC code to the voltage. The 1000.0 value is the reference voltage in mV (see Figure 10 again), and the 16777216 is just 224. This first term is the thermo-EMF of the thermocouple E(t, t0). When we add the cold_end_voltage to it (which, as you remember, represents the E(t0, 0)), we get the E(t, 0) thermo-EMF, which can be used to find the real temperature from the reference table or using the polynomial approximation formula.

In line 91, we start the loop to find in which range the current v value lies. In line 93, we check if the v is greater than the current vr value and is smaller than the next vr value. If so, we calculate the temperature value using the polynomial coefficients corresponding to the current range (line. 95).

In line 96, we declare the variable temp of type int_32 and assign the rounded value of the temperature multiplied by 10. It will be used only to send the temperature with the 0.1 °C resolution to the RTT interface (line 97). In line 98, we leave the loop using the break instruction, as checking the next ranges is impossible.

In line 101, we clear the sdadc_conversion_complete flag and wait while it is set again in the sdadc_callback function.

And that’s it! As you can see, if you know the theoretical part well, everything is simple here despite the use of these huge formulas. Now, let’s switch to practical work.

Testing of the Thermometer

Let’s assemble the circuit according to Figure 2, connect the EK-RA2A1 board to the PC, build the project, make sure that there are no errors, and start the debugging. Then run the J-Link RTT Viewer application and connect it to the board. If you need to learn how to do it, please refer to Tutorial 14, where I described it in detail.

If you did everything correctly, you will see something like this in the RTT Viewer (Figure 14).

Figure 14 - Result of the operation of the program
Figure 14 - Result of the operation of the program

As you can see, the hot end temperature is close to the cold end temperature in normal conditions. Let’s try to heat the hot end of the thermocouple using either a match or a lighter (take care with fire safety!!!).

Figure 15 - Result of the operation when the thermocouple is heated
Figure 15 - Result of the operation when the thermocouple is heated

As you can see, at some points, the hot end temperature even exceeds the maximum limit for this thermocouple, which has no consequences.

So, as you can see, everything works as desired. In this tutorial, we learned how to use the SDADC module to measure the thermocouple thermo-EMF and convert it to the temperature with the compensation of the cold junction temperature.

As homework, I suggest you add the external thermometer chip located right at the cold junction and see how this affects the measurement accuracy. For such a chip, you can use LM35, which has the analog input and often comes in the Arduino sets. Otherwise, you can use any available sensor, even the DS18B20, but you will need to write the code to operate it yourself.

P.S.

For some reason, when I used the FSP v.3.7, the SDADC results were randomly altered within about 5 degrees range. But when I switched to the FSP v.4.0, the problem was gone. I strongly recommend updating your FSP version to the latest available one, as the old errors and issues are being fixed there.

Make Bread with our CircuitBread Toaster!

Get the latest tools and tutorials, fresh from the toaster.

What are you looking for?