Create An Arbitrary Waveform Generator

Posted on - Last Modified on

I had a project which included controling a solenoid pushing a bulb to simulate a human heartbeat. The waveform needed to be a sine wave to simulate a Beats Per Minute (BPM). The output had these constraints: the signal out needed to be centered about 2.5 volts; the signal amplitude needed to be settable from 1% (2.5 volts) to 100% (0 to 5 volts); and the frequency for one complete cycle was settable between 0.5 Hz (30 BPM) and 3 Hz (180 BPM). (In reality, anything under 10% is basically a flatline because the solenoid could not respond fast enough.

Basic Concepts

Digital arbitrary waveform generators use a table for each type of waveform (sawtooth, square, etc). The generator typically copies the table to RAM, creating a “working” table with the desired amplitude. The system then walks the RAM table at the selected frequency, and outputs the current table entry to a DAC.

This particular project used an Arduino UNO, which uses an Atmel ATMega328 [328]. The Arduino had several advantages: the client (a Mechanical Engineer) could get the IDE for free; the hardware was cheap and easily available, I really like Atmel MPUs; and this was a one-off prototype.

I used a control loop to also control two motors and a CLI. The best way to create a reliable waveform timing output was a timer ISR. I changed the frequency of the timer to change the frequency of the output waveform. This decision had three implications: the ISR used a RAM table; the ISR would only change the timer frequency at the end of walking the table; and the timer frequency is the output frequency divided by the length of the table.

The waveform table is stored in a “canonical” form in FLASH data space (100% amplitude), then copied to a RAM buffer and modified as needed with the desired amplitude. The total waveform has NUM_WAVE_STEPS = 72 long. The 100% amplitude plot is:

 

 

Figure 1: Waveform Plot at 100% Amplitude

 

 

Example Tables

 

The FLASH version is a byte array. The waveform is 72 steps, 100% amplitude values cycle 127->0->128->255->(value before 127).

byte waveform_base_table[ NUM_WAVE_STEPS ] =

{

    127, 116, 105,  95,  84,  74,  64,  54,  46,    //   0 deg

     37,  30,  23,  17,  12,   8,   4,   2,   0,

      0,   0,   2,   4,   8,  12,  17,  23,  30,    // -90 deg

     37,  46,  54,  64,  74,  84,  95, 105, 116,

    128, 139, 150, 160, 171, 181, 191, 201, 209,    //   0 deg

    218, 225, 232, 238, 243, 247, 251, 253, 255,

    255, 255, 253, 251, 247, 243, 238, 232, 225,    // +90 deg

    218, 209, 201, 191, 181, 171, 160, 150, 139,

 

};

Table 1: Waveform values at 100% Amplitude

I used the [328] Timer1 because the Arduino software does not use it, and Timer1 is a 16-bit timer. The critical thing is the fastest output (3 Hz) needed enough time between the 72 ISR calls for the table walk. If the ISR call frequency is too fast, the timer can starve the rest of the code in the control loop. Also, anytime you use an ISR, you need to spend the minimum amount of time in the ISR.

This is the table needed to set the [328] Timer1 OCR1A register. These values will be divided by NUM_WAVE_STEPS before setting the register. The table uses an increment of 5 BPM, thus NUM_BPM = 31.

unsigned int timer1_bpm_table[ NUM_BPM ] =

{

// BPM

//     30     35     40     45     50     55

    31350, 26871, 23513, 20900, 18810, 17100,

//     60     65     70     75     80     85

    15675, 14469, 13436, 12540, 11756, 11065,

//     90     95    100    105    110    115

    10450,  9900,  9405,  8957,  8550,  8178,

//    120    125    130    135    140    145

     7838,  7524,  7235,  6967,  6718,  6486,

//    150    155    160    165    170    175    180

     6270,  6068,  5878,  5700,  5532,  5374,  5225

};

Table 2: Timer1 Frequency Table per BPM

 

Timer ISR Frequency Considerations

The Timer1 Prescaler = clock/1024. Notice the largest value (31350) for 30 BPM. I had three choices: make the table as above; make the table an unsigned long (decreasing the prescaler and increasing the values), or pre-dividing the table values by NUM_WAVE_STEPS.

The most critical aspect is making sure the fastest ISR rate was slow enough to not starve the MPU:

5225 / 72 = 72.57

72.57 * 1024 = 74311 clock cycles

Thus, the fastest the ISR is called is 74311 clock cycles, which gives the rest of the colde plenty of time to execute.

The Timer1 OCR1A register is set via (index is the BPM, offset to the start of the table)

OCR1A = timer1_bpm_table[ index ] / NUM_WAVE_STEPS;

 

Output Options

The [328] does not have a DAC. The normal method is to use a timer in Pulse Width Modulation (PWM) mode, and convert the PWM to an analog value with an RC network.

The critical issues are providing enough PWM pulses per ISR call, and the RC network time constant. The client was providing the RC network, so I had to assure there were enough PWM pulses per ISR call to provide a smooth analog output.

The Arduino software has an analogWrite() function that uses timer 2, channel B to generate the PWM. The default speed of a nominal 488 Hz is not fast enough:

488 Hz / 72 = 6.7 PWM (periods /step) best case

1 / 488Hz = 2.04 mSec per PWM

333 mSec / 72 steps = 4.625 mSec / step

Timer 2 needs to be faster by changing the prescaler divider to clk/1, making the Solenoid Analog value PWM period is 32.00 uSec:

 

 

Next Article

5 Questions Freelancers Should Ask Themselves