Generating Arbitrary Waveforms with PIC Microcontrollers

PIC has been manufacturing microcontrollers with built-in DACs for quite some time now, and I thought that an article on generating arbitrary waveforms using nothing more than an MCU, PIC18F45k50 in this particular case, would be useful to the newbies.

SP-4 Preview

Image 1 - 8-bit Triangle Wave

Basic, and keep in mind unoptimized, library would consist of only a few simple functions. Function "vtob" would be used for converting voltages from the human-readable form ex. 2.5 to its "binary" representation that could be pushed to a device's register - VREFCON2 register for PIC18F45k50.

Function "sin_map" would generate an array containing values needed for producing a continuous sine wave, and the "print_map" function would print the values of an already generated map for use-cases requiring lower RAM footprint ie. storing the map as a constant in the program memory.

// Function Definitions

int dac_vtob(float);
void dac_sin_map(int*, int, float, float);
void dac_print_map(int*, int);

The library should be easily customizable to accommodate a variety of different devices. The following block contains definitions for reference voltage, DAC resolution, and size of the signal map buffer.

// Library Configuration

#define DAC_PI_N     -3.14
#define DAC_PI_P      3.14
#define DAC_REF       5
#define DAC_RES       5
#define DAC_BUFFER    32

Basic conversion function as described in one of the previous paragraphs.

/**
  * @param v
  *     Human-readable foltage ex. 2.5
  * @return int
  *     Binary-representation ex. 16 for a 5-bit DAC using a 5V reference
  */
int dac_vtob(float v) {
    return (int)((v / DAC_REF) * pow(2, DAC_RES));
}

When dealing with low-resolution DACs, you can get away with using a small buffer ex. 16-byte buffer when dealing with a 5-bit DAC.

/**
  * @param *buff
  *     Pointer to a buffer in which the map will be stored - memory must be pre-allocated
  * @param len
  *     Size of the buffer ex. 128
  * @param dc_off
  *     DC offset of the signal ex. 2.5
  * @param ac_vpp
  *     Peak-to-peak voltage of the signal ex. 1.0
  * @return void
  */
void dac_sin_map(int *buff, int len, float dc_off, float ac_vpp) {

    int   i = 0;
    float j = DAC_PI_N;

    float incr = DAC_PI_P / (len / 2);

    dc_off = dac_vtob(dc_off);
    ac_vpp = dac_vtob(ac_vpp / 2);

    do {

        buff[i] = (int)(ac_vpp * sin(j) + dc_off);

        if (buff[i] < 0) {
            buff[i] = 0;
        }
    }
    while (i++ < len && (j += incr) < DAC_PI_P);
}

If RAM is at a premium, you can compile the library with GCC and pre-generate signal maps to store them as constants in the program memory of your device.

/**
  * @param *buff
  *     Pointer to a buffer in which the map is stored
  * @param len
  *     Size of the buffer ex. 128
  * @return void
  */
void dac_print_map(int *buff, int len) {

    for (int i = 0; i < DAC_BUFFER; i++) {

        if (i == 0) {
            printf("x = [ %d", buff[i]);
        }
        else {
            printf(", %d", buff[i]);
        }
    }

    printf(" ];");
}

The frequency of the generated signal is ~600Hz in the snippet below, and can be easily adjusted by tweaking the value of the XC's "__delay" function, or by altering the buffer size.

Output Frequency
// Basic Implementation

int buffer[DAC_BUFFER];

dac_sin_map(&buffer[0], DAC_BUFFER, 2.5, 4);

while (true) {
    
    VREFCON2 = buffer[i++];
    
    if (i == DAC_BUFFER) {
        i = 0;
    }
    
    __delay_us(100);
}

Here are some previews of the signals that could be generated using a 5-bit DACs on 8-bit microcontrollers.

Contact

If you don't want to contact me via office@djordjejocic.com, please use the form bellow. All fields are required.