FB pixel

Generating Waveforms with the DAC module | Embedded C Programming - Part 30


Hello again! This tutorial is the final one devoted to the analog modules of the PIC18F14K50 MCU. In it, we will consider the voltage reference modules. I have already mentioned them in the previous two tutorials, and now the time has come to familiarize you with them in more detail.

So PIC18F14K50 has two voltage references: a fixed voltage reference of 1.024 V and a programmable voltage reference whose voltage can be changed within 32 steps. So the latter is often called the 5-bit digital-to-analog converter (DAC).

In this tutorial, we will use them both to build the signal generator to produce sine, saw, or triangle waveforms with different frequencies. The type of the signal and the frequency will be displayed in the same 1602 LCD we used before. Changing the frequency and waveform will be implemented by means of two buttons.

Traditionally, let’s first get familiar with the new modules used in the current project. Their description won’t take long as they are quite simple.

Fixed Voltage Reference (FVR) Module

Actually, both voltage references are considered as two parts of the same module, but let’s split their description not to mix up what is what.

FVR (as follows from its name) is the stable, fixed, VDD-independent reference voltage source. Its nominal voltage is 1.024 V but can be multiplied by 2 or 4. In the last case, the VDD voltage should be higher than the desired reference voltage.

The FVR voltage can be used as the reference voltage for the analog comparator, ADC, or DAC modules.

After the FVR module is enabled, it takes some time before the output voltage is stabilized, so it would be good to provide some small delay waiting while it becomes stable.

A single register controls this module- REFCON0 (REFerence CONtrol), which has the following bits:

  • bit #7 - FVR1EN (Fixed Voltage Reference 1 ENable). Setting this bit to 1 enables the FVR module, and clearing it disables the module.
  • bit #6 - FVR1ST (Fixed Voltage Reference 1 STable). This bit indicates whether the FVR output voltage is already stable (FVR1ST = 1) or not yet (FVR1ST = 0). So after setting the bit FVR1EN to 1, it’s good to wait while the FVR1ST bit becomes 1 as well.
  • bits #5, #4 - FVR1S1 and FVR1S0 (Fixed Voltage Reference 1 voltage Selection) set the output voltage of the FVR module according to the following table



FVR output voltage



Reserved, do not use



1.024 V (default value)



2.048 V



4.096 V

  • bits #3-#0 are unused and read as 0

Programmable Voltage Reference, or Digital-to-Analog Converter (DAC) module

This module operates independently from other analog modules, meaning it can be turned on or off separately at any moment. As I mentioned before, it provides 32 voltage levels, which the following equation can calculate:

where Vsource+ is the positive reference voltage of the DAC module;

Vsource- is the negative reference voltage of the DAC module;

DAC1R is the value of the DAC1R register, which can have values from 0 to 31 and sets the output voltage of the DAC module.

The positive and negative reference voltages are selectable, like in the ADC module.

The output voltage of the DAC module can be driven to the positive input of the analog comparator if it is selected by setting the CxRSEL bit of the CM2CON1 register (see Tutorial 24 for more details). Also, the output voltage can be output to the pin CVREF, which is merged with the RC2. The DAC’s current drive ability is quite low, so you can’t connect it to the low-resistance load. To do this, you need to use some external buffer like the operational amplifier connected as the voltage follower (Figure 1, copied from Figure 21-2 of the PIC18F14K50 data sheet)

Figure 1 - DAC output buffer example
Figure 1 - DAC output buffer example

The capacitor at the output of the DAC module, along with the internal resistance R, works as a low-pass filter smoothing the steps of the DAC voltage levels. We proved its effectiveness when we created the sine wave generator based on the PIC10F200 MCU.

The DAC module is controlled by two registers: REFCON1 and REFCON2.

The REFCON1 register has the following bits:

  • bit #7 - D1EN (DAC1 ENable). When this bit is set to 1, the DAC module is enabled; otherwise, it’s disabled.
  • bit #6 - D1LPS (DAC1 Low-Power voltage State). This bit specifies the output of the DAC module in the low-power state, as in this state, DAC is disabled to reduce power consumption. If this bit is 0, the negative reference voltage is output, and if it is 1, the positive reference voltage is output.
  • bit #5 - DAC1OE (DAC1 Output Enable). If this bit is 1 then the DAC output is also connected to the RC2/CVREF pin. In this case, the digital buffer of this pin is disabled, and the reading of its input register will always be 0. If this bit is 0, the RC2/CVREF pin is disconnected from the DAC output and can be used as a regular GPIO pin.
  • bit #4 - is unused and read as 0.
  • bits #3-2 - D1PSS1 and D1PSS0 (DAC1 Positive Source Select) select the positive voltage reference source according to the following table:



Positive voltage reference source is supplied…



internally by VDD



externally through the VREF+ pin



internally through FVR




  • bit #1 - D1NSS (DAC1 Negative Source Select) select the negative voltage reference source. If this bit is 0, then the VSS is used, and if it is 1, then the VREF- pin is used.
  • bit #0 - is unused and read as 0.

The REFCON2 register has the following bits:

  • bits #7-#5 are unused and read as 0.
  • bits #3-2 - DAC1R4…DAC1R0. These bits specify the output voltage of the DAC module according to the equation (1).

And that’s enough information about the voltage reference modules to write the program that uses them. As usual, please refer to Chapter 21 of the PIC18F14K50 data sheet for more information.

Schematic Diagram

The schematic diagram is shown in Figure 2.

Figure 2 - Schematic diagram with the PIC18F14K50 with 1602 character LCD
Figure 2 - Schematic diagram with the PIC18F14K50 with 1602 character LCD

This schematic diagram is very similar to the one presented in tutorial 28, but it has some extra parts: it consists of the PIC18F14K50 MCU (DD1) and 1602 character LCD (X2), which are connected in the same way as there. Also, it has two push buttons - S1 for setting the waveform and S2 for setting the frequency. The DAC output is read by the oscilloscope from the pin RC2/CVREF. In this schematic, the filtering capacitor is absent to see the raw output waveform; also, the output buffer (like in Figure 1) is absent because the oscilloscope input has very high resistance. You should add these parts to an actual signal generator for proper operation.

That’s everything about the device schematic diagram so we can proceed to the next step.

Program Code Description

Let’s create a new project now. I’ve called it “Signal_generator” but you can give it any name you want. Then create the new “main.c” file in it, as we are used to doing.

The same as in tutorials 24 and 28, we will use the separate “config.h” file in which the configuration bits are defined. Also, we will need to copy and paste the “lcd_1602.h” and “lcd_1602.c”. One can take them from tutorial 24 or 28, as I showed you. After all, your project should look like in Figure 3.

Figure 3 - Project structure with all required files
Figure 3 - Project structure with all required files

If you compare the schematic diagrams (Figure 2) in the current tutorial and Tutorial 24 (or 28), you will notice that the connection of the display is equal, so if you took the LCD-related files from them, you don’t need to make any changes to them. And if you copied them from one of the previous tutorials, please remember to change the pin assignment; otherwise, your display won’t work properly.

Now let’s consider the “main.c” file and write the following code.

#include <xc.h>

#include "config.h"

#include "lcd_1602.h"

uint8_t waves_5b[3][32] = {{16, 19, 21, 24, 26, 28, 30, 31, 31, 31, 30, 28, 26, 24, 21, 19,

16, 12, 10, 7, 5, 3, 1, 0, 0, 0, 1, 3, 5, 7, 10, 12},//Sine

{ 0, 2, 4, 6, 8, 10, 12, 14, 16, 17, 19, 21, 23, 25, 27, 29,

31, 29, 27, 25, 23, 21, 19, 17, 16, 14, 12, 10, 8, 6, 4, 2}, //Saw

{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,

16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}};//Triangle

uint16_t frequencies[6] = {2000, 1000, 500, 200, 100, 50}; //Supported frequencies in Hz

uint8_t pr2[6] = {124, 249, 249, 249, 249, 249}; //Value of the Timer2 period register PR2

uint8_t postscaler[6] = {0, 0, 1, 4, 9, 4}; //Value of the Timer2 postscaler

uint8_t prescaler[6] = {0, 0, 0, 0, 0, 1}; //Value of the Timer2 prescaler

volatile uint8_t wave_index; //Index of the waveform in the waves_5b array

uint8_t index; //Index of the element within one waveform

uint16_t frequency_index; //Index of the element of the frequencies array

void __interrupt() INTERRUPT_InterruptManager (void)//Interrupt subroutine


if(INTCONbits.PEIE == 1) //If peripheral interrupts are enabled


if((PIE1bits.TMR2IE == 1) && (PIR1bits.TMR2IF == 1))//If Timer2 interrupt is enabled and Timer2 interrupt flag is set


PIR1bits.TMR2IF = 0; //Clear the Timer2 interrupt flag

VREFCON2 = waves_5b[wave_index][index]; //Copy the current element of the waves_5b array to the VREFCON2 register

index ++; /Increment the index to set the next element of the waves_5b array the next time

if (index == 32) //If index is 32 (the limit of the array)

index = 0; //Then set index as 0




void refresh_lcd (void) //Refresh the LCD information


lcd_clear(); //Clear the LCD

lcd_write_string("Waveform:"); //Write the "Waveform" text

switch (wave_index) //If wave_index is...


case 0: lcd_write_string("SINE"); break; //0 then write "SINE"

case 1: lcd_write_string("TRIANGLE"); break; //1 then write "TRIANGLE"

case 2: lcd_write_string("SAW"); break; //2 then write "SAW"


lcd_set_cursor(1, 2); //Set cursor at the beginning of of the next line

lcd_write_string("Frequency:"); //Write the "Frequency" text

lcd_write_number(frequencies[frequency_index], 0); //Write the value of the frequency from the array

lcd_write_string("Hz"); //Write the "Hz" text


void main(void)


//GPIO configure

TRISCbits.RC2 = 0; //Configure RC2 (DAC output) as output

TRISAbits.RA4 = 1; //Configure RA4 (Frequency button) as input

TRISAbits.RA5 = 1; //Configure RA5 (Waveform button) as input

WPUAbits.WPUA4 = 1; //Enable WPU on RA4 pin

WPUAbits.WPUA5 = 1; //Enable WPU on RA5 pin

ANSELbits.ANS3 = 0; //Disable analog buffer at RA4 (AN3) pin

INTCON2bits.nRABPU = 0; //Enable pull-up resistors at ports A and B

//Oscillator module configuration

OSCCONbits.IRCF = 6; //Set CPU frequency as 8 MHz

OSCTUNEbits.SPLLEN = 1; //Enable PLL

//Timer2 configuration

T2CONbits.T2OUTPS = postscaler[0];//Postscaler 1:1

T2CONbits.T2CKPS = prescaler[0];//Prescaler 1:1

PR2 = pr2[0]; //Period 124 ticks

T2CONbits.TMR2ON = 1; //Timer2 is enabled

PIE1bits.TMR2IE = 1; //Enable Timer2 underflow interrupt

//FVR module configuration

VREFCON0bits.FVR1S = 2; //FVR voltage is 2.048 V

VREFCON0bits.FVR1EN = 1;//Enable FVR

while (!VREFCON0bits.FVR1ST);//Wait until FVR voltage becomes stable

//DAC module configuration

VREFCON1bits.DAC1OE = 1;//Enable DAC output on RC2 pin

VREFCON1bits.D1PSS = 2; //Select the FVR output as the positive reference voltage

VREFCON1bits.D1NSS = 0; //Select the ground as the negative reference voltage

VREFCON1bits.D1EN = 1; //Enable DAC

//Interrupts configuration

INTCONbits.GIE = 1; //Enable global interrupts

INTCONbits.PEIE = 1; //Enable peripheral interrupts

lcd_init(0, 0); //Initialize the LCD without cursor and blinking

frequency_index = 0; //Set the frequency_index as 0

wave_index = 0; //Set the wave_index as 0

refresh_lcd(); //Write the initial text at the LCD

while (1)


if (PORTAbits.RA5 == 0) //If waveform button is pressed


__delay_ms(20); //Then perform the debounce delay

if (PORTAbits.RA5 == 0) //If after the delay button is still pressed


while (PORTAbits.RA5 == 0); //Then wait while button is pressed

__delay_ms(20); //After button has been released, perform another delay

if (PORTAbits.RA5 == 1) //If the button is released after the delay


wave_index ++; //Increment the wave_index

if (wave_index == 3) //If wave_index is 3 (the last element in the waves_5b array)

wave_index = 0; //then set the wave_index as 0

refresh_lcd(); //Update the LCD information




if (PORTAbits.RA4 == 0) //If the frequency button is pressed


__delay_ms(20); //Then perform the debounce delay

if (PORTAbits.RA4 == 0) //If after the delay button is still pressed


while (PORTAbits.RA4 == 0); //Then wait while button is pressed

__delay_ms(20); //After button has been released, perform another delay

if (PORTAbits.RA4 == 1) //If the button is released after the delay


frequency_index ++; //Increment the frequency_index

if (frequency_index == 6) //If frequency_index is 6 (the last element in the frequencies array)

frequency_index = 0; //then set the frequency_index as 0

PR2 = pr2[frequency_index]; //Copy the corresponding element of the pr2 array into the PR2 register

T2CONbits.T2OUTPS = postscaler[frequency_index]; //Copy the corresponding element of the postscaler array into the T2OUTPS bits of the T2CON register

T2CONbits.T2CKPS = prescaler[frequency_index]; //Copy the corresponding element of the prescaler array into the T2CKPS bits of the T2CON register

refresh_lcd(); //Update the LCD information






This program is quite large but much simpler in comparison to the one from tutorial 28.

In line 1, we, as usual, include the “xc.h” file to use the PIC MCU-related variables, functions, and macros. In line 2, we include the “config.h” file, in which the configuration bits are defined. And finally, in line 3, we include the “lcd1602.h” file, which consists of the 1602 LCD-related functions.

In line 5, we declare the waves_5b array of the uint8_t type. This array is two-dimensional. It consists of 3 one-dimensional arrays, each of which consists of 32 values. Elements of the waves_5b array represent the values of the DAC1R register, which allow to generate the desired waveform.

Let’s briefly consider how it works. Generation of any waveform represents the continuous calculation of the signal amplitude with the fixed time step and converting this amplitude into the DAC voltage using the equation (1). For sure, we can calculate the amplitudes on the fly, but this can significantly reduce the maximum frequency because the mathematical calculations take a lot of time (especially for a sine wave). So in practice, the period of the function is divided into some parts. The more parts provided, the better the waveform quality, but this also decreases the maximum frequency. In our case, I divided the whole period into 32 parts and calculated the DAC1R values for each of the three supported waveforms.

Let me show an example of the generation of the sine waveform. As you know, the sin(x) function has the ranges [-1; 1], so -1 of the sin(x) should correspond to 0 of DAC1R, and 1 of sin(x) should correspond to 31 of DAC1R. Here we can use the formula of linear interpolation:

from where

As we know, the sinusoidal period is and if we calculate the DAC1R values for x = 0,

The same with other waveforms. I won’t reproduce all the calculations here. You can treat it as an exercise in mathematical calculations by yourself if you want. As it is, you can see all the data points within the program code itself: the sine wave values are located in waves_5b[0] (lines 5-6), the saw wave values are located in waves_5b[1] (lines 7-8), and the triangle wave values are located in waves_5b[2] (lines 9-10).

In lines 12-19, the global variables are located:

  • The frequencies array (line 12) consists of six elements which represent the frequencies that the device can generate. This array is needed only to display the current frequency in the LCD.
  • The pr2 (line 13), postscaler (line 14), and prescaler (line 15) arrays also consist of six elements and are used to configure Timer2, which is used to provide stable time steps to generate the waveforms. Each set of elements sets the frequency corresponding to the element in the frequencies array. For example, to generate the frequency of 2000 Hz, you need to set the PR2 register value as 124, the postscaler value as 0, and the prescaler value as 0. We will talk about these values in more detail later.
  • The wave_index variable (line 17) is used as the number of the part of the waves_5b array from which the data is currently taken and, thus, corresponds to the type of waveform. You can see that this variable has the volatile modifier, which is needed because it will be used both in the main program and the interrupt subroutine, and without this modifier, the compiler would consider it as two different variables.
  • The index variable (line 18) is the number of the element of the subarray of the waves_5b array, which is currently copied into the DAC1R register.
  • The frequency_inxed (line 19) is the number of the element in the arrays frequencies, pr2, postscaler, and prescaler. This variable is used to set and display the proper frequency.

In lines 21-34, there is the interrupt subroutine INTERRUPT_InterruptManager which we will consider later. In lines 36-50, there is the function refresh_lcd which we will also consider later.

And now, let’s switch to the main function of the program (lines 52-135), and particularly to its initialization part (lines 54-94).

In lines 55-61, we configure the IO pins: configure the RC2/CVREF pin as an output (line 55), and pins RA4, RA5 to which the buttons are connected, as inputs (lines 56, 57). Then we enable the pull-up resistors at RA4 and RA5 pins (lines 58, 59), disable the analog buffer at the RA4 pin (line 60), and enable the pull-up resistors at ports A and B (line 61).

In lines 64-65, we set the CPU frequency as 32 MHz.

In lines 68-72, we configure Timer2. We considered this timer in detail in the tutorial 20.

There was actually a weird thing with the timers. I intended to use any but Timer2 initially because all of them except Timer2 can work in 16-bit mode, so we can achieve the required time step without changing the timer’s prescaler. But for some reason, when I used these timers, I couldn't achieve the desired frequency, as it was always smaller. And this problem persisted with Timer0, Timer1, and Timer3. I was ready to believe that this was the issue of the internal oscillator accuracy, but then, out of desperation, I decided to try Timer2, and suddenly, it turned out that with it, the frequency fit ideally, which surprised me a lot. The configuration of this timer is more complex than others because it has a resolution of just 8 bits, so we need to change the prescaler and postscaler for every frequency. But as it was the only working solution, I had to deal with it. Well, that was a lyrical digression, and now let’s return to the program.

In lines 68-70, we copy the first elements of the arrays pr2, prescaler, and postscaler into the period register PR2, bits T2OUTPS and T2CKPS of the register T2CON, respectively. Then we enable Timer2 (line 71) and enable Timer2 underflow interrupt (line 72).

Now, let’s consider how the values of the arrays pr2, prescaler, and postscaler were calculated on the example of their first elements, which are 124, 0, and 0, respectively. These values correspond to the signal frequency of 2000 Hz (the first element of the array frequencies). Each signal period is divided into 32 parts (because we use 32 values in the array waves_5b to generate the waveforms). So, to generate the signal with the frequency of 2000 Hz, we need to load the new values into the DAC1R register with the frequency of 2000 x 32 = 64000 Hz. This frequency corresponds to 1 / 64000 Hz = 15.625 us. The clocking frequency of Timer2 is Fosc / 4 = 32 MHz / 4 = 8 MHz, so each timer tick takes 1 / 8 = 0.125 us. And thus, to provide the period of 15.625 us, we need 15.625 / 0.125 = 125 ticks. As the timer reloads when it counts down to 0, we need to start counting from 125 - 1 = 124, which we load into the PR2 register.

Let’s also see how the lowest frequency of 50 Hz can be achieved. Now the desired timer period is 1 / (50 x 32) = 625 us, and the number of ticks of the timer should be 625 / 0.125 = 5000, which definitely can’t be achieved by an 8-bit timer. Both prescaler and postscaler can divide the frequency maximum by 16. To maintain the accuracy, we should use the minimum required dividers. If we use the postscaler of 1:16, then the timer’s period will be 16 times longer, and thus, the number of required timer ticks will be 5000 / 16 = 312.5. First, this value is not an integer, which doesn’t suit us; second, it still doesn’t fit into the 8-bit value. Let’s then use the first prescaler value of 1:4. In this case, the input timer frequency will be 8 / 4 = 2 MHz, and the timer tick will take 1 / 2 = 0.5 us. In this case, we will need 625 / 0.5 = 1250 ticks. If we divide this number by 256, we can get the minimum required postscaler value: 1250 / 256 = 4.89, which we round to the nearest greater integer value, which is 5. Thus, the number of timer ticks, in this case, will be 1250 / 5 = 250, which now perfectly fits into 8-bit. And now, we need to convert these numbers into the register values. The PR2 value should be 250 - 1 = 254. The T2CKPS value corresponding to the 1:4 divider is 1, and the T2OUTPS value that corresponds to the 1:5 divider is 4. You can see these values as the last elements of the corresponding arrays. The other values are calculated similarly.

In lines 75-77, we configure the FVR module. First, we set the output voltage as 2.048 V by setting the bits FVR1Sx of the VREFCON0 register as 2 (FVR1S1 = 1, FVR1S0 = 0) (line 75) so that the maximum output of the DAC will be 2.048 x 31 / 32 = 1.984 V. Then we enable the FVR module by setting the FVR1EN bit of the same register as 1 (line 76). And finally, we wait while the output voltage of the FVR module stabilizes by waiting until bit FVR1ST becomes 1 (line 77).

In lines 80-83, we configure the DAC module as follows. First, we configure the RC2 pin as the DAC output by setting the bit DAC1OE of the register VREFCON1 (line 80). Then we select the FVR as the positive reference voltage by setting the D1PSSx bits as 2 (line 81), and the GND as the negative reference voltage by clearing the bit D1NSS (line 82). And finally, we enable the DAC module by setting the bit D1EN (line 83).

In line 86, we enable all unmasked interrupts, and in line 87, we enable the peripheral interrupts.

In line 89, we initialize the 1602 LCD without the cursor and blinking.

In lines 91 and 92, we assign 0 to the variables frequency_index and wave_index, respectively, to start with the sine wave with the 2000 Hz frequency.

In line 94, we call the function refresh_lcd to write the information about the waveform and the frequency at the LCD.

That’s all about the initialization. The main loop of the program is located in lines 96-134.

Everything we do inside the main loop is checking if any button is pressed and processing it. The S1 button (Waveform) is processed in lines 98-113. Here is the standard blocking algorithm, which we considered a lot of times, so I will only stop the action performed when it’s pressed (lines 107-110).

In line 107, we increment the wave_index variable to switch to the next waves_5b subarray and thus to generate the next waveform. If the value of the wave_index becomes 3 (line 108), which means that we run out of the array boundaries, then we set it as 0 (line 109) to return to the first subarray. Then, after all the manipulations, we update the LCD information (line 110).

The payload of the S2 button (Frequency) processing (lines 115-133) is located in lines 124-130 and is similar to the previous one. We first increment the frequency_index (line 124), then we check if it becomes 6 (line 125), and if yes, we set it as 0 (line 126). Now, we need to update the Timer2 registers PR2 (line 127) and T2CON (lines 128-129) to change its period to generate the waveform with the new frequency. Finally, we update the LCD information by calling the refresh_lcd function (line 130).

That’s everything about the main function of the program. Let’s now consider the interrupt subroutine INTERRUPT_InterruptManager (lines 21-34).

We don’t need to use the priorities here because we have only one active interrupt source - Timer2 underflow. So, the processing of the interrupt is standard. We first check if the peripheral interrupts are enabled (line 23). Then we check if the Timer2 underflow interrupt is enabled and the corresponding interrupt flag is set (line 25). If all these conditions are met, we process the interrupt (lines 27-31). First, we clear the interrupt flag (line 27). Then, we copy the current element of the waves_5b array into the DAC1R bits of the VREFCON2 register (line 28) by which the DAC updates its output voltage. Then we increment the index variable (line 29), check if it becomes 32 (line 30), and if yes, then we assign 0 to it (line 31).

Finally, let’s consider the refresh_lcd function (lines 36-50). In the previous tutorial, the eponymous function was very complex and tangled. Here, it’s extremely straightforward. I don’t know if it requires any clarification, so let’s do it briefly. First, we clear the LCD and set the cursor at the first position of the first line (line 38). Then we write the static text “Waveform:” (line 39) and add the waveform type according to the wave_index variable value (lines 40-45). Then we move the cursor to the first position of the second line (line 46), write the static text “Frequency:” (line 47), and add the frequency value from the frequencies array, which corresponds to the frequency_index value (line 48). Finally, we add the static text “Hz” (line 49).

That’s everything about the program code. As you can see, it’s quite simple but requires some preparations, like calculating the DAC values for different waveforms and the timer parameters for different frequencies. Let’s now proceed to the testing of our device.

Testing of the Signal Generator

Let’s now assemble the device according to Figure 2, compile and build the project and flash it into the PIC18F14K50 MCU. If everything is assembled and programmed correctly, you should see the following text on your LCD (Figure 4).

Figure 4 - Initial text on the LCD
Figure 4 - Initial text on the LCD

When you press the buttons, the text should be changed: the waveform should change in the following loop: SINE-TRIANGLE-SAW, and the frequency in the following one: 2000Hz-1000Hz-500Hz-200Hz-100Hz-50Hz. For example, this is how the screen looks after several presses (Figure 5).

Figure 5 - LCD text after several button presses
Figure 5 - LCD text after several button presses

But for proper device testing, you need to connect the oscilloscope to the RC2/CVREF pin of the MCU to see the real waveforms generated by it. If you don’t have one, you can use the primitive oscilloscope that we have assembled in the previous tutorial, but you will not see the details of the signal in it.

I’ve owned the DS203 oscilloscope for many years. Now, it has become obsolete, and I’m pretty sure you can’t buy it, but even something primitive from AliExpress or Amazon (for example, DSO138 (Figure 6), which costs less than $20) will work out for this case.

Figure 6 - DSO138 oscilloscope
Figure 6 - DSO138 oscilloscope

So, I wanted to complain about my DS203. For some reason, it doesn’t make screenshots anymore. Well, it makes them, but they can’t be read from the internal memory, which is unfortunate, so the next images are just photos of the oscilloscope screen, not the real screenshots, which I liked more. But we have what we have.

So the following are several photos of the oscilloscope screen for different waveforms and frequencies (Figure 7-11).

Figure 7 - Sine wave with the frequency of 2000 Hz
Figure 7 - Sine wave with the frequency of 2000 Hz
Figure 8 - Sine wave signal with the frequency of 1000 Hz
Figure 8 - Sine wave signal with the frequency of 1000 Hz
Figure 9 - Sine wave signal with the frequency of 500 Hz
Figure 9 - Sine wave signal with the frequency of 500 Hz
Figure 10 - Triangle wave signal with the frequency of 2000 Hz
Figure 10 - Triangle wave signal with the frequency of 2000 Hz
Figure 11 - Saw wave signal with the frequency of 2000 Hz
Figure 11 - Saw wave signal with the frequency of 2000 Hz

What can be concluded from these images?

  1. The signal forms are close to the desired but are still stepped, which is especially noticeable in Figure 9. Actually, the other signals are the same stepped, but it’s not that noticeable with this horizontal resolution. As I said, to reduce these steps, we need to connect the filtering capacitor between the output of the DAC and the ground. The value of this capacitor is discovered empirically and should be a manageable size - several tens or hundreds of pF, most likely.
  2. The frequency of the signals is very close to the desired one. You can see it in the right part of the screen as the parameter “FRQ”. For a 2 kHz signal, the measured frequency varies from 2.004 to 2.017 kHz, which gives the max relative error of 0.017 / 2 x 100% = 0.85%. With the other timers, this error reached several percent.
  3. The signal’s amplitude is also close to the calculated one, which is 1.984 V but is always smaller. You can see its value in the right part of the screen as the parameter “Vpp”. In Figure 7-11, it varies from 1.82 to 1.86 V, which gives the maximum relative error of (1.984 - 1.82) / 1.984 x 100% = 8.3 %, which is quite high. I don’t know what causes such a deviation - the FVR error measurement and calculation error, or even both.

And that’s finally it. In this tutorial, we have familiarized ourselves with the voltage reference modules of the PIC18F14K50 MCU. The programmable voltage reference can be used as a low-resolution 5-bit DAC, which still can produce quite decent waveforms, especially along with the external capacitive filter and the output buffer.

As homework, I suggest you add more waveforms, like a trapezoid or backward saw or whatever you want, and also make more signal frequencies.

Make Bread with our CircuitBread Toaster!

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

What are you looking for?