FB pixel

Renesas RA - 18. Making a simple Digital Oscilloscope using 16 bit ADC, GPT, DTC and ELC

Published


Hi there! In this tutorial I will continue to acquaint you with the analog modules of the RA2A1 family. And this time, as follows from the title, we will study the 16 bit analog-to-digital converter (ADC16) module. I thought that it would be too boring to read the ADC values and send them to the Segger RTT terminal in this tutorial (you can do it by yourself though). So I recalled that we are already familiar with the 1602 OLED display (see Tutorial 8) and decided to make the simple oscilloscope which will read the ADC data (for sure, using the ELC and DTC modules, otherwise it would be not interesting) and send them to this OLED display.

But first, let’s, as usual, consider the new MCU module that will be used in the current project.

Introduction into 16 bit Analog to Digital Converter (ADC16)

Previous analog modules (comparators and DACs) were quite simple, so their description didn’t take a lot of time. Unlike them, ADC16 is quite complicated with a lot of modes and features. So I will briefly provide the required information which will be needed to understand what we do in this tutorial and why, and the rest you can read by yourself in the Chapter 32 of the RA2A1 User’s Manual.

The RA2A1 family has only one ADC16 module which has up to 17 single-ended inputs (AN000-AN008, AN016-AN023) or up to 4 differential inputs (AN000-AN008). These inputs also include the internal sources - temperature sensor, internal reference voltage, and reference voltage of the 24 bit sigma-delta ADC (which we will talk about next time). ADC16 module uses the successive approximation method of the measurement which provides a high speed conversion with decent accuracy (if you don’t know how it works, you can refer to this article). The ADC16 module of the RA2A1 family has a conversion time of just 0.82 us per channel, which means that you can reach a sample rate of 1 / 0.82 us = 1.22 MHz.

ADC16 module can operate in three modes:

  • Single scan mode. The conversion is performed only once on the arbitrarily selected channels (external or internal) in ascending order. The conversion can only start with the software trigger.
  • Continuous scan mode. The conversion is performed repeatedly on the arbitrarily selected channels (external or internal) in ascending order. The conversion can only start with the software trigger.
  • Group scan mode. Here the channels are arbitrarily divided into two groups - Group A and Group B. Each group can start the conversion independently from the software trigger, ELC event trigger or external trigger from the dedicated ADTRG0 pin. Also, in this mode you can prioritize Group A. In this case, if the start conversion of the Group A is triggered during the active conversion of the Group B, the latter will be suspended while all the channels of the Group A will finish the conversion, and then resume.

As you see, we can use the ELC to start the conversion only in the Group scan mode, so in our program we will use it even though it looks excessive for measuring just one input signal.

Additional features of the ADC16 module are:

  • Self-diagnostic of AD converter which is performed once at the beginning of each scan.
  • Selectable AD-converted average mode. In this mode the same channel is scanned 2, 4, 8, or 16 times and the mean value is stored in the data register. This can improve the accuracy of the conversion at the expense of speed.
  • Analog input disconnection detection.
  • Double-trigger mode, in which the analog input of an arbitrarily selected channel is converted in single scan mode or group scan mode (group A), and the data converted by the first and second A/D conversion start triggers are stored in different registers, providing duplexing of A/D-converted data.
  • Automatic clear function for A/D data registers.
  • Digital comparison of values in the comparison register and the data register, and between values in the data registers.
  • Analog characteristics can be corrected by calibration.

Actually, that’s all you need to know so far. We will talk about the ADC16 features in detail later, when we consider the ADC16 stack in the FSP visual configurator.

The other modules used in this device have been already considered in previous tutorials, so now we can move to the schematics diagram of the device.

Schematics Diagram of the Digital Oscilloscope

Figure 1 - Schematics diagram of the device
Figure 1 - Schematics diagram of the device

As you can see, the schematics diagram is quite simple. It only contains one external part - the OLED display X1 which is connected in the same way as in Tutorial 8 - to the SCL and SDA pins of the connector J1 of the EK-RA2A1 board. The analog signal with the waveform we want to display is applied to the pin AN000/P500 of the connector J3. As I mentioned before, ADC16 can use the external or internal reference voltage source. Internal sources available in the FSP visual configurator are 1.5V, 2V, or 2.5V, which is not that good, as we want to have the full range from 0V to the AVCC level. In this case we should use the external source which is applied to pins VREFH0 and VREFL0. By default these pins have no power, so you can connect them to any voltage source which doesn’t exceed AVCC, and it will be used as the reference. We will not use any external source, and just connect the VREFH0 pin to AVCC0, and VREFL0 pin to AVSS0. You can even use simple jumpers, as these pins are located next to each other. This action is quite simple but don’t forget about it! I spent about an hour trying to find out why my ADC values are random, and it turned out that I just didn’t connect the reference voltage.

That’s all about the schematics diagram. Let’s now briefly consider the block diagram of operation of the oscilloscope.

Algorithm of the Operation of the Oscilloscope

The block diagram of the oscilloscope is presented in Figure 2.

Figure 2 - Block diagram of the software flow
Figure 2 - Block diagram of the software flow

This block diagram is very similar to the one from the DAC tutorial. The “instigator” of everything is still the GPT. On overflow, the timer generates the interrupt which is used as an activation source for the DTC module, and simultaneously as the source event for the ELC module.

When the timer overflows, the ELC module automatically initiates the new AD conversion. Simultaneously with this, the DTC module gets the previously converted value from the ADC16 module and saves it into the array of 256 16-bit words. So if in the DAC tutorial the DTC module sent the data from the array into the DAC module, this time it works in the opposite direction - from ADC module to the array. Also this time the DTC works in the Normal mode instead of the Repeat mode. This is done because we don’t need to continuously update the array. We need to read 256 values, process and send them to the OLED display, and only then initialize the new cycle. This cycle will be started by another timer one time per second, so here we unfortunately can’t totally eliminate the CPU usage, but we reduced it significantly.

Somebody can ask, why do we need to use a 256-word array if the OLED has only 128 dots length. Well, the only reason for this is the voltage level synchronization. I believe you saw in the normal oscilloscopes that the periodic signal looks stable on the display. But the reading of the signal can start at any moment of the period. That’s why we read more values than needed, and then search throughout the array where the voltage reaches the given threshold. When it is found, we start sending the data to the display from this position. And if it is not found, we just send the last 128 values.

To be able to change the time resolution of the oscilloscope sampling, we will also change the period of the GPT0 from 100 us to 1 ms. Actually this range could be widened, but this is just a demonstration project, so I limited it with these boundaries.

Project Creation and Configuration

I think 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 (be attentive here - we need to use C++ because the OLED library is written in it) using the “Bare Metal - Minimal” template.

First, we will switch to the “Pins” tab and configure the required pins. First, we need to expand the “Connectivity:IIC” list and select the “IIC0” line to enable the I2C pins used for communication with the OLED display. Actually, I have already described in detail how to configure this module in Tutorial 8, so here I will just briefly provide screenshots of the configuration (Figure 3).

Figure 3 - IIC0 module pin configuration
Figure 3 - IIC0 module pin configuration

Next, we need to configure the AN000/P500 pin as the ADC input. So, find and expand the “Analog:ADC” list and select the “ADC0” line. By default the channel AN06 is enabled (Figure 4).

Figure 4 - Initial ADC0 module pin configuration
Figure 4 - Initial ADC0 module pin configuration

So what we need to do is disable channel AN006 and then enable channel AN00. For the AN06 channel click on the “P003” text in the “Value” column and in the drop-down list select “None”. For the AN00 channel click on the “None” text in the “Value” column and select “P500”. After that your configuration should look like in Figure 5.

Figure 5 - ADC0 module final pin configuration
Figure 5 - ADC0 module final pin configuration

Leave all other ADC channels and auxiliary pins unchanged. And actually these are all the required changes in the “Pins” tab so we can switch to the “Stacks” tab. There, let's first add the stacks we are already familiar with. I will not stop on them in detail, just provide you the configuration screenshots and short comments if needed.

The first thing to add is the “I2C Master (r_iic_master)” stack to communicate with the OLED display, which is configured the same as in Tutorial 8 (Figure 6).

Figure 6 - Configuration of the I2C Master stack
Figure 6 - Configuration of the I2C Master stack

Next, let’s add two instances of the “Timer, General PWM (r_gpt)” stacks. The first one: GPT0 - will set the sampling rate of the ADC readings, and the second one: GPT1 - will set the display update rate which we will fix at 1 second. The configurations of GPT0 and GPT1 are provided in Figure 7 and Figure 8, respectively.

Figure 7 - Configuration of the GPT0 stack
Figure 7 - Configuration of the GPT0 stack
Figure 8 - Configuration of the GPT1 stack
Figure 8 - Configuration of the GPT1 stack

Timer GPT0, as follows from the block diagram (Figure 2) will be used to trigger the ELC and the DTC modules, so we need to enable its overflow interrupt, which also automatically enables the “GPT0 COUNTER OVERFLOW” events generated by this timer. As we won’t need to process the timer’s interrupts, we leave the “Callback” field as “NULL”. Also, we change the timer’s period to 120 raw counts - I will explain later why this exact value.

As for the GPT1 timer, we change its channel to 1 and its period to 1 second, as I mentioned before. Also, we will need its interrupt to trigger the measurement and data processing and displaying process. So we enable the Overflow interrupt, and set the Callback name as “g_timer1_callback”.

Now, as we are going to use the ELC module, let’s just add the ELC stack to the project. It doesn’t have any options to change, so we will leave it as is. The next thing to add is the DTC stack. As I said in the previous chapter, it will be used to send the data from the ADC to the 16-bit array located in RAM. Its configuration is shown in Figure 9.

Figure 9 - Configuration of the DTC stack
Figure 9 - Configuration of the DTC stack

The configuration of the DTC stack is somehow similar to the previous tutorial but has certain differences. The main difference is that this time the “Source address” is “Fixed” because we always take the data from the same ADC register, and the “Destination address” is “Incremented” because the array index increases after every transfer. The “Transfer Size” remains “2 Bytes” as the ADC output is 16 bits wide. We set the “Number of Transfers” as 256 but actually we could skip this as we will reconfigure the transfer with the R_DTC_Reset function. The “Activation Source” is “GPT0 COUNTER OVERFLOW”, as I mentioned before.

And now the familiar stacks are finished. Now we need to click on the “New Stack >” once again, expand the “Analog” list, and select the “ADC (r_adc)” (Figure 10).

Figure 10 - Adding the ADC stack
Figure 10 - Adding the ADC stack

Before we proceed to configuration of the ADC stack, let’s see the whole structure of the added stacks (Figure 11) and make sure we didn’t forget to add anything.

Figure 11 - Stacks configuration
Figure 11 - Stacks configuration

As you can see, the “g_adc0 ADC (r_adc)” block is red now, which means there are some errors. If we hold the cursor over this block, we can see the following hint (Figure 12).

Figure 12 - ADC stack error explanation
Figure 12 - ADC stack error explanation

If you are not lazy, you can open the RA2A1 User’s Manual and find Table 47.40 in it, where it’s said that the max ADCLK frequency is 32 MHz. And if we switch to the “Clocks” tab of the FSP Configurator, you can see that the PCLKD which is the clocking source of the ADC16 module is 48 MHz. To reduce it, we can change the divider value from “PCLKD Div / 1” to “PCLKD Div / 2”, after which the PCLKD frequency will become 24 MHz (Figure 13) which perfectly fits into the given range.

Figure 13 - Changing the PCLKD frequency
Figure 13 - Changing the PCLKD frequency

Now, we can switch back to the “Stacks” tab and see that the error has gone. Now let’s configure the ADC stack according to Figure 14-15.

Figure 14 - Configuration of the ADC stack (part 1)
Figure 14 - Configuration of the ADC stack (part 1)
Figure 15 - Configuration of the ADC stack (part 2)
Figure 15 - Configuration of the ADC stack (part 2)

As you can see, the ADC stack has a lot of parameters but we need to change only a few of them. Let’s consider all its parameters in detail.

  • “Name” as usual means the name of the current ADC stack in the program. We can leave it as default.
  • “Unit” is the number of the hardware ADC Unit of the MCU. The RA2A1 family has only one unit - 0, so we leave this field unchanged.
  • “Resolution” is the resolution of the ADC16 module. This field is unchangeable and always is “16-Bit’.
  • “Alignment” is the conversion result alignment. But as in the current case the result spends all 16 bits, this field makes no sense and is marked as “Unsupported”.
  • “Clear after read” specifies if the result register will be automatically cleared after the conversion result is read. In our case the value of this field doesn’t matter, so we can leave the default option “On”.
  • “Mode” selects the mode in which the ADC module will work - “Single Scan”, “Continuous Scan” or “Group Scan”. As I said, we need to select the group scan in case of using the ELC events to trigger the conversion start, so we select this option.
  • “Double-trigger”. When enabled, the scan-end interrupt for Group A is only thrown on every second scan. Extended double-trigger mode (single-scan only) triggers on both ELC events, allowing (for example) a scan on two different timer compare match values. In group mode Group B is unaffected. We don’t need this option, so we leave it as “Disabled”.
  • “Channel Scan Mask (channel availability varies by MCU)”. In Normal mode of operation, this bitmask field specifies the channels that are enabled in that ADC unit. In group mode, this field specifies which channels belong to group A. In our case, we need to select only “Channel 0”, as it is the only one to be scanned in our program.
  • “Group B Scan Mask (channel availability varies by MCU)”. In group mode, this field specifies which channels belong to group B. Here we leave all the channels unselected.
  • “Addition/Averaging Mask (channel availability varies by MCU)” selects channels to include in the Addition/Averaging Mask. As we’re not going to use this functionality, we leave all the channels here unselected, too.
  • “Sample and Hold” list isn’t clear for me, as I didn’t find any information about this functionality in the RA2A1 User’s Manual. Probably, it’s applicable to other families. We will leave it unchanged.
  • “Windows compare” list contains options that configure windows comparator. You can set the lower and upper limits, and select the ADC channels used in this comparator. When the input value of the selected channels run out of the specified limits, the ADC module generates the interrupt. We don’t need this functionality, so I’ll skip the detailed explanation of it. For more information please refer to the FSP documentation.
  • “Add/Average Count” selects the number of samples for averaging the results (can be 2, 4, 8, or 16). As we don’t use this function, we leave it as “Disabled”.
  • “Reference Voltage control” selects the reference voltage source. The available options are: external source VREFH0, or one of three internal sources VREFADC (1.5V, 2V, or 2.5V). We are going to use the VREFH0 option to provide the rail-to-rail input voltage range. For this, we have already connected the VREFH0 pin to the AVCC0 pin (see Figure 1).
  • “Normal/Group A Trigger” sets the trigger type for the selected ADC channels in Normal mode or for Group A in Group Mode. As I already mentioned (see Figure 2), we need to use the “GPT0 COUNTER OVERFLOW” event as the trigger for the Group A to which the Channel 0 now belongs.
  • “Group B Trigger” sets the trigger type for the selected ADC of Group B in Group Mode. Not applicable in other modes. Actually I wanted to leave this field as”Disabled” but the FSP configurator showed the ADC stack error saying that the Group B trigger should also be initialized. So I used the same “GPT0 COUNTER OVERFLOW” event. Actually it doesn’t matter as there are no active channels in Group B.
  • “Group Priority (Valid only in Group Scan Mode)” determines whether an ongoing group B scan can be interrupted by a group A trigger, whether it should abort on a group A trigger, or if it should pause to allow group A scan and restart immediately after group A scan is complete. In our case we can leave this option by default.
  • “Callback” is the name of the ADC callback. We don’t need to use it, so we leave it as “NULL”.
  • “Scan End Interrupt Priority” enables the scan end interrupt in Normal mode or scan end Group A interrupt in the Group mode.
  • “Scan End Group B Interrupt Priority” enables the scan end Group B interrupt in the Group mode.
  • “Window Compare A Interrupt Priority” enables the window compare interrupt for Group A.
  • “Window Compare B Interrupt Priority” enables the window compare interrupt for Group B.
  • “Extra” list contains just one feature - “ADC Ring Buffer” which anyway is not supported by the RA2A1 family.
  • “Pins” list shows the pins that are configured to be used with the ADC module. In our case only the pair “AN00” - “P500” should be active.

And that’s actually all about the ADC module configuration. But before we end the FSP configuration we need to increase the heap size like we did in Tutorial 8 because otherwise the OLED display will not work properly (Figure 16).

Figure 16 - Configuring the heap size
Figure 16 - Configuring the heap size

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

Program Code of the Project

Before we start with creating our own code, we need to add the files for supporting the OLED display. Actually we can add the whole library like we did in Tutorial 8 but then we will need to disable the unused files again. Let’s now select only the files we really need and in the future projects add just them. So from the folder “Adafruit-GFX-Library-master” we need the following files:

  • “Adafruit_GFX.cpp”;
  • “Adafruit_GFX.h”;
  • “gfxfont.h”;
  • “glcdfont.h”.

And from the folder “Adafruit-SSD1306-master” we need to add these files:

  • “Adafruit_SSD1306.cpp”;
  • “Adafruit_SSD1306.h”;
  • “splash.h”.

When you add these files to the “src” folder of your project (either using the copy-paste or drag-and-drop method) your project should look like in Figure 17.

Figure 17 - Project content after adding the OLED display library files
Figure 17 - Project content after adding the OLED display library files

In Figure 16 in the “configuration.xml” file, e2 studio still shows some error which didn’t disappear from here even after I fixed the clocking issue. So don’t worry, there are no errors, it’s just an e2 studio glitch. We don’t need to change anything in these files, and if you click the “Build” button, everything should finish without any errors but with a lot of warnings, mainly about the integer conversion but we can ignore them as we already know that this library works fine.

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

#include "hal_data.h"

#include "Adafruit_SSD1306.h"

#include <stdio.h>

FSP_CPP_HEADER

void R_BSP_WarmStart(bsp_warm_start_event_t event);

FSP_CPP_FOOTER

#define DEBOUNCE_DELAY 20 //Debounce delay of 20 ms for the button

#define THRESHOLD 8192 //Threshold to synchronize the graph output

Adafruit_SSD1306 display(128, 64); //OLED display object creation

uint16_t adc_data[256]; //Array to receive the data from ADC

uint8_t resolution_index; //Index of the element of the resolution array

const uint16_t resolution[4] = {100, 200, 500, 1000};//Resolution of the graph by time axis

volatile uint8_t update_start; //Flag to start the display update

void update_display (void)//Function to update the display content every second

{

char str[20]; //Array to form the string to display

uint8_t start_index; //Index of the element in the array from which to start the graph

display.clearDisplay(); //Clear the OLED display

display.setTextColor(WHITE); //Set the dots color as white

//Draw horizontal grid lines

for (uint8_t i = 0; i < 3; i ++)//Draw three dashed lines

for (uint8_t j = 0; j < 128; j += 5) //Lines are formed with dots with the interval 5 dots

display.drawPixel(j, i * 20 + 23, WHITE); //Draw the pixels

//Draw vertical grid lines

for (uint8_t i = 0; i < 7; i ++)//Draw seven dashed lines

for (uint8_t j = 63; j > 16; j -= 5)//Lines are formed with dots with the interval 5 dots

display.drawPixel(i * 20, j, WHITE);//Draw the pixels

//Write legend

display.setCursor(0, 0); //Set the cursor at the position [0,0]

sprintf(str,"X resolution: %i us", resolution[resolution_index]);//Form the string about the X resolution

display.print(str); //Display the formed string

display.setCursor(0, 8); //Set the cursor at the position [0,8]

display.print("Y resolution: 1.5 V");//Write the information about Y resolution

//Find the start index in the array to synchronize the output

start_index = 128; //Initially we set the start_index as 128

for (uint8_t i = 0; i < 128; i ++)//Search in the first part of the array

{

if ((adc_data[i] < THRESHOLD) && (adc_data[i + 1] >= THRESHOLD)) //If the values cross the threshold

{

start_index = i; //Save the index of this value

break; //And leave the loop

}

}

//Display the graph

for (uint8_t i = 0; i < 127; i ++)

{

display.drawLine(i, 63 - (47 * adc_data[i + start_index]) / 32767, i + 1, 63 - (47 * adc_data[i + start_index + 1]) / 32767, WHITE);

}

display.display(); //Send the display buffer to the display

}

void g_timer1_callback (timer_callback_args_t *p_args) //GPT1 Callback function

{

if (p_args->event == TIMER_EVENT_CYCLE_END) //If the event that caused the interrupt is timer overflow

update_start = 1; //Then set the flag update_start

}



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

* 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)

{

R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_SECONDS);// Perform 1 second delay to let the OLED power stabilize

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

R_ELC_Enable(&g_elc_ctrl); //Enable ELC

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

R_DTC_Open(&g_transfer2_ctrl, &g_transfer2_cfg);//Open the DTC stack

R_DTC_Reset(&g_transfer2_ctrl, (void*)&R_ADC0->ADDR[0], adc_data, 256); //Set the source and destination addresses

R_DTC_Enable(&g_transfer2_ctrl); //Enable DTC

R_GPT_Open(&g_timer0_ctrl, &g_timer0_cfg); //Open the GPT stack

R_GPT_Start(&g_timer0_ctrl); //Start GPT counting

R_GPT_Open(&g_timer1_ctrl, &g_timer1_cfg); //Open the GPT stack

R_GPT_Start(&g_timer1_ctrl); //Start GPT counting

resolution_index = 0; //Set the initial resolution as 100 us

update_start = 0; //Reset the update_start flag


display.begin(SSD1306_SWITCHCAPVCC, 0x3C, false, true); //Initialize OLED display with the I2C address 0x3C (for the 128x64)

/* Main loop */

while (true)

{

if (update_start) //If update_start flag is set

{

update_display(); //Refresh the display

update_start = 0; //Reset the update_start flag

R_DTC_Reset(&g_transfer2_ctrl, (void*)&R_ADC0->ADDR[0], adc_data, 256);//Reset the DTC transfer configuration

R_DTC_Enable(&g_transfer2_ctrl); //And enable the new transfer cycle

}

if (R_BSP_PinRead(BSP_IO_PORT_02_PIN_06) == BSP_IO_LEVEL_LOW) //If button is pressed

{

R_BSP_SoftwareDelay(DEBOUNCE_DELAY, BSP_DELAY_UNITS_MILLISECONDS); //Debounce delay

if (R_BSP_PinRead(BSP_IO_PORT_02_PIN_06) == BSP_IO_LEVEL_LOW) //If button is still pressed

{

while (R_BSP_PinRead(BSP_IO_PORT_02_PIN_06) == BSP_IO_LEVEL_LOW);//Wait while button is pressed

R_BSP_SoftwareDelay(DEBOUNCE_DELAY, BSP_DELAY_UNITS_MILLISECONDS);//Debounce delay

if (R_BSP_PinRead(BSP_IO_PORT_02_PIN_06) == BSP_IO_LEVEL_HIGH) //If button is released

{

resolution_index ++; //Select the next X resolution index

if (resolution_index > 3) //If the index is beyond the array boundaries

resolution_index = 0; //Set the index as 0

switch (resolution_index) //Set the GPT1 timer period according to the selected index

{

case 0: R_GPT_PeriodSet(&g_timer0_ctrl, 120); break;//5 us

case 1: R_GPT_PeriodSet(&g_timer0_ctrl, 240); break;//10 us

case 2: R_GPT_PeriodSet(&g_timer0_ctrl, 600); break;//25 us

case 3: R_GPT_PeriodSet(&g_timer0_ctrl, 1200); break;//50 us

}

}

}

}

}

#if BSP_TZ_SECURE_BUILD

/* Enter non-secure code */

R_BSP_NonSecureEnter();

#endif

}

This program is longer than the previous analog modules related ones but the majority of the functions have already been discussed earlier so it should be easy to understand this program.

In lines 1-3, we include the required header files. File “stdio.h” is needed to use the sprintf function in line 37, but we will talk about it later.

In lines 9-10, we define the macros. DEBOUNCE_DELAY (line 9) specifies the delay when the user button S1 is being pressed-unpressed (I didn’t show the S1 button in the schematics diagram explicitly but it is located in the left bottom corner of the EK-RA2L1 board and is called “USER BTN” there). THRESHOLD (line 10) is the value of the ADC register by which the displaying of the graph will be synchronized. I will explain this point later when I show you how the synchronization works.

In lines 12-16 we declare the global variables and constants:

  • display (line 12) is the object of the Adafruit_SSD1306 class which is used to handle the OLED display;
  • adc_data (line 13) is the array of 256 16-bit values into which the results of the AD conversion will be copied using the DTC module (see Figure 2).
  • resolution_index (line 14) is the current index of the element in the resolution array defined in the next line.
  • resolution (line 15) is the constant array of 4 elements, each of which define the resolution of the graph along the X axis in microseconds. In real oscilloscopes you can also see this but with more available options.
  • update_start (line 16) is the flag that indicates that the screen view requires to be refreshed.

In lines 18-55 there is a function update_display which performs the display refresh, but we will consider it in detail later.

In lines 57-61 there is a GPT1 callback function. Its name must be the same as in the GPT1 stack configuration (Figure 8). In this callback function we first check if the event that caused the interrupt was TIMER_EVENT_CYCLE_END (line 59) which corresponds to the Timer Overflow event. If this was really the overflow event, we set the update_start flag (line 60) to indicate that the time has come to refresh the display.

In lines 68-131 there is the main function of the program hal_entry. Its initialization part occupies lines from 70 to 92.

First, we implement the 1 second delay to let the OLED display voltage become stable (line 70). Then we open (line 72) and enable (line 73) the ELC stack.

In lines 75-77 we initialize the ADC16 stack. Even though the functions here are new to us, it’s still possible to understand what they mean. So in line 75 we call the R_ADC_Open function which, like other R_xxx_Open functions, opens the corresponding stack, and has two parameters - the pointer to the ADC instance g_adc0_ctrl and the pointer to the configuration structure g_adc0_cfg. Then we invoke the R_ADC_ScanCfg function (line 76) which configures the ADC scan parameters. It also has two parameters - the same pointer to the ADC instance g_adc0_ctrl, and the pointer to the scan configuration structure g_adc0_channel_cfg. Finally, in line 77, we invoke the function R_ADC_ScanStart which has only one parameter - still the same g_adc0_ctrl. This function 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, then this function allows the trigger signal to get to the ADC unit. As we have configured the Group A trigger as the TMR0_COUNTER_OVERFLOW event, after calling this function, the ADC module will be waiting for this event to start the new conversion.

In lines 79-81 we configure the DTC module. Actually its configuration is very similar to the one in Tutorial 17, and all used functions are already familiar to us: In line 79, we open the DTC stack. Please be attentive - this time the name of the DTC instance is g_transfer2_ctrl, as the g_transfer0_ctrl and g_transfer1_ctrl are used to send and receive I2C data, respectively (see Figure 11). In line 80, we invoke the function R_DTC_Reset which reconfigures the current transfer parameters. As you can see, the source address is R_ADC0->ADDR[0] this time. Here, R_ADC0 is the structure that controls the ADC16 module. ADDR[0] is the conversion result of the Channel 0 of the ADC. If we want to use any channel, we can simply change the index of the ADDR register here. The destination address of the transfer is adc_data, which is the array that we declared in line 13. And the number of transfers is 256 for the reasons I have already explained before.

In line 81, we enable the DTC transfers but as they are triggered by the GPT0_COUNTER_OVERFLOW event, nothing will start until the GPT0 timer is configured, which we do in lines 83-84. After implementation of these lines, the ADC will start the conversion and the DTC module will start sending the conversion results into the adc_data array. After 256 samples, the DTC will stop and the adc_data array will be waiting for further processing.

In lines 86-87, we configure and start the GPT1 timer in the same way as the GPT0 timer. So in a second the GPT1 timer will overflow and the data in the adc_array will be finally processed.

In lines 89-90, we initialize the global variables. First, we set the resolution_index to 0 to indicate that the default resolution is 100 us/div (if it’s not clear why, I will explain everything soon). And second, we reset the update_start flag to indicate that no display refresh is pending for now.

In line 92, we initialize the OLED display the same as in Tutorial 8. And this is all about the initialization part. Now let’s see what happens in the main loop of the program (lines 95-126).

In line 97, we check if the update_start flag is set, which means that the GPT1 timer has overflowed, and we need to refresh the display, which we do by invoking the update_display function (line 99). Then we clear the update_start flag (line 100). Finally, as we have already processed the old adc_data, we can reconfigure (line 101) and reenable (line 102) the DTC transfer #2. As DTC works in Normal mode, we need to restart it manually. Also, we need to reconfigure it every time using the R_DTC_Reset function. If we don’t do this, the old final destination address will remain unchanged in the DTC module, and the new transfer will send the data to the addresses beyond the adc_data array boundaries which can (and actually eventually will) lead to the program malfunctioning.

In line 104-125 we process button S1 using the regular blocking algorithm which I have already described several times, for example, in Tutorial 5. So I will only consider the payload of this part, namely, what usefulness is done when button S1 is pressed. This part is located in lines 113-123. First, we increment the resolution_index variable (line 113). If it exceeds the size of the resolution array (line 114), we assign 0 to it (line 115). In this way, we use all the elements of this array in a loop.

In lines 116-122 we sort out all possible values of the resolution_index using the switch operator. If the resolution_index is 0, this corresponds to the first element of the resolution array which is 100 us, and we set the GPT0 timer period as 120 raw counts (line 118). The same we do with the other values of the resolution_index (lines 119-121). I owe you the explanation of this value of 120 raw counts from the time when I put it in the GPT0 configuration (Figure 7). The timer is clocked with the PCLKD source, as you can see in Figure 7. The frequency of PCLKD is 24 MHz now (see Figure 13), so each timer’s tick is 1/24µs, and 120 ticks will spend 120/24 = 5µs. But why do we set it to 5µs if we claim that the resolution is 100 µs/div? The thing is that the resolution is calculated not between the neighboring dots on the graph but between the neighboring grid lines. The distance between these lines is 20 dots (as I will show you when describing the update_display function), and 5 x 20 = 100 us, which is exactly what we need. The same with the other values which you can calculate by yourself if you don’t trust me enough.

And that’s all about the hal_entry function. As you can see, it’s still quite simple. Let’s now consider the last and the most interesting function here, namely, update_display (lines 18-55).

In line 20, we declare the local array str of the char type which will be used to form the string to be sent to the OLED display. In line 21, we declare the variable start_index which is the index of the element in the adc_data array from which sending the data to display starts.

In line 22, we clear the display, and in line 23 we set the text (and actually all lines and dots) color as WHITE which means that the active dots are lit, and the background is black.

In lines 25-27 we draw the horizontal grid lines. Well, they are not actual lines, they are the dots straightened in one line with the distance between them of 4 pixels. If we made the grid lines solid they could merge with the input signal because the OLED display is monochrome. That’s why we use such a solution. In line 25 we start the for loop with the i variable that changes from 0 to 3. This will give us three vertical lines. As I mentioned before, the distance between the grid lines is 20 pixels, so three lines will occupy… 40 pixels (and what did you think? 60? Nope, just 40!). The rest space will be occupied by the graph legend.

In line 26 we start the other loop with the j variable which changes from 0 to 128 with the step of 5. This loop provides drawing the dashed line with one lit pixel over 5 positions along the whole display whose horizontal resolution is exactly 128 pixels.

In line 27 we invoke the drawPixel method which draws the pixel at the given coordinates with the given color. The first parameter is the X coordinate, and we use the j variable in its role. The second parameter is the Y coordinate, and we use the expression i * 20 + 23 for it. Let’s consider what it means. The multiplier 20 sets the distance between the lines, which is exactly 20 pixels. And the added value 23 sets the offset from the upper line. I wanted the last grid line to be the bottom line of the display, which has the coordinate 63. The second line will have the coordinate 43, and the first line - 23, voila! The last parameter of the drawPixel method is the color of the dot - to show the dot we write WHITE, to erase the dot we write BLACK there.

In lines 29-31 we draw seven vertical grid lines which spend… right! - 120 pixels. Actually their drawing doesn’t differ much from the horizontal lines. In line 29, we start the same loop as in line 25 but here the i variable changes from 0 to 7. In line 30 we start the loop with the j variable which counts backward from 63 to 16 with the step of 5. The backward count is needed to match the intersection points of the vertical and horizontal grids (I will show you the result in the next chapter). In line 31, we invoke the drawPixel method again but this time i * 20 is used as the X coordinate, and j is used as the Y coordinate.

After the grid is done, we need to write the legend. So we set the cursor at the coordinate [0, 0] (line 33) then form the string to write using the sprintf function (line 34), and finally send the formed string to the LCD (line 35). I will not describe here the operation of the sprintf function, as it is a standard C function. If you are not familiar with it, please refer to the official documentation.

Then we set the cursor at the next line at the coordinate [0, 8] (line 36) and write the second line of the legend which is fixed, so we send it just as a string constant (line 37).

Now, we need to synchronize the output of the graph to the THRESHOLD value. I think I should explain this point in more detail. In real oscilloscopes you might see the option called “Trigger” or “Synchronization”. It is used to make the image on the oscilloscope screen steady (I’m talking about the periodic input signals). The oscilloscope can start the measurement cycle at any moment of the input signal’s period. So if you display it then “as is”, it will “jump” after each refresh of the screen. To avoid this, the oscilloscope reads more samples than can fit into the display and then finds the position in the saved array where the input graph crosses the threshold value in the selected direction (either up or down, this option also is available in real oscilloscopes). Then it outputs the graph from this position.

In real oscilloscopes the threshold value can be changed, also the trigger options are selectable, but here we for the simplicity will have the constant threshold level of ¼ of the max input value, and the constant synchronization option - “up”, so the graph will be synchronized by its rising edge crossing the threshold.

Let’s now see how it’s done in our program. The logic is the following - we are searching for the synchronization condition in the first 128 elements of the array. If it’s found, we send the graph from this fount position. If not, we just send the last 128 elements without any synchronization. So, we first set the start_index value as 128 in case nothing is found (line 40). Then we start the loop to search within the first 128 elements of the array (line 41). In this loop we check if the current adc_data element is lower than the THRESHOLD, and the next element is greater or equal than the THRESHOLD (line 43). This condition corresponds to crossing the threshold level by the graph from below, and if it meets the condition, we save the current i value as start_index (line 45) and then leave the loop (line 46).

Now, the only thing left is to send the graph to the display which we do in lines 50-53. In line 50 we start the loop from 0 to 127 to send 128 values to the display because the display’s horizontal resolution is 128 pixels. The attentive reader can ask: why then to 127 but not to 128? Well, that’s a reasonable question. The thing is that if we draw the graph using the drawPixel method it will not look solid but will be dotted, and moreover, with a variable distance between the dots which doesn’t look good. Thus, we will draw the graph as the set of consequent line segments between the neighbor measurements. The first line is between the measurements #0 and #1, the second - between #1 and #2, and so on and so far. And the last line is between the measurements #127 and #128 which happens on the 127’th iteration of the loop. That’s why there are just 127 loop iterations.

Inside the loop we call the single function - method drawLine (line 52) which has five parameters - coordinates [X1, Y1], [X2, Y2] of the dots between which to draw the line, and the color of the line. Let’s consider the arguments of this method in more detail.

display.drawLine(i, 63 - (47 * adc_data[i + start_index]) / 32767, i + 1, 63 - (47 * adc_data[i + start_index + 1]) / 32767, WHITE)

With X coordinates everything’s clear - they are i for X1 and i + 1 for X2 which correspond to the neighbor X positions in the display. With the color everything is also clear - it’s WHITE. But with the Y coordinates the expressions need more explanation.

The thing is that even though the ADC16 module claims to have 16 bit resolution, in fact for single ended inputs the available resolution is 15 bits, and the range of changing of the result if AD conversion is 0…32767. For the differential inputs the MSB of the result represents the sign, so the range becomes -32768…32767 (but that’s not our case).

So the ADC value of 0 should be shown in the bottom line of the OLED display which has the Y coordinate 63, and the ADC value of 32767 should be shown in the top line minus 16 lines for the legend, which has the Y coordinate 16. Let’s now find out the formula how to convert the ADC value into the display Y coordinate using the linear interpolation method. It’s initial formula is

In our case x is the ADC value, and y is the Y coordinate. The corresponding points were just mentioned: x1 = 0, x2 = 32767, y1 = 63, y2 = 16. With these value we have:

From which we have:

Now, if you look again at the line 52 of the code, you will see that the values 63 - (47 * adc_data[i + start_index]) / 32767 and 63 - (47 * adc_data[i + start_index + 1]) / 32767 exactly correspond to the last formula. The adc_data array indexes are shifted by the start_index value to send the synchronized chunk of the whole array.

Finally, we should not forget to invoke the method display (line 54) to flush the display buffer to the actual display.

And that’s actually all about the program code. Now we can build the application, connect the board to the PC and run the debug.

Testing of the Oscilloscope

For testing the oscilloscope you now need some signal generator. Fortunately, I have one which is quite simple but works well. And if you don’t have any, you can try to merge both the oscilloscope and the signal generator in one device joining the current program with the one from Tutorial 16 (I will leave you this task as homework). And now I will show you some photos of the result of the operation of our oscilloscope.

Figure 18 - Sine wave signal with the 5 kHz frequency, X resolution is 100 us/div
Figure 18 - Sine wave signal with the 5 kHz frequency, X resolution is 100 us/div
Figure 19 - Sine wave signal with the 2 kHz frequency, X resolution is 100 us/div
Figure 19 - Sine wave signal with the 2 kHz frequency, X resolution is 100 us/div
Figure 20 - Sine wave signal with the 2 kHz frequency, X resolution is 200 us/div
Figure 20 - Sine wave signal with the 2 kHz frequency, X resolution is 200 us/div
Figure 21 - Saw signal with the 2 kHz frequency, X resolution is 100 us/div
Figure 21 - Saw signal with the 2 kHz frequency, X resolution is 100 us/div

Sorry for the quality of the photos, the camera on my phone is not very good. But anyway you can see that the oscilloscope works as desired. For example, in Figure 18 you can see the sine wave with the frequency of 5 kHz. The X resolution is 100 us/div. The 5 kHz wave has the period 1 000 000 / 5 000 = 200 us. And we can see in Figure 18 that the whole period fits exactly in two squares. You can make the same calculations for other signal frequencies and X resolutions and make sure that everything works fine.

Well, I think that’s all about this project. In this tutorial we have learned how to use the ADC16 module of the RA2A1 family and how to use the DTC and ELC modules to copy the AD conversion result from the ADC module into the array in the RAM without the CPU intrusion.

I already formulated the homework task for you but will copy it here as well to let it be in the usual place at the end of the tutorial: merge both the oscilloscope and the signal generator in one device joining the current program with the one from Tutorial 16.

Make Bread with our CircuitBread Toaster!

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

What are you looking for?