DDS Sinewave Generator using PWM as an 8-bit DAC, on a PIC18F26K22 device

Started by top204, Aug 17, 2022, 05:46 PM

Previous topic - Next topic

top204

On the forum recently, there has been discussions about using DDS (Direct Digital Synthesis) to create a sinewave signal from a PIC microcontroller. So I have created the program below in order to demonstrate how easy it is to create it when written with the Positron8 compiler.

The program uses the compiler's 24-bit, Long, type variables where it can, so that it makes the program both smaller and faster to operate. It also uses the CCP1 peripheral acting as an 8-bit DAC so the sine waveforms are quite good.

The demo is written for a PIC18F26K22 device, but it can easily be transferred to any 18F or enhanced 14-bit core device. And with a few changes, also on to a standard 14-bit core device:

'
'   /\\\\\\\\\
'  /\\\///////\\\
'  \/\\\     \/\\\                                                 /\\\          /\\\
'   \/\\\\\\\\\\\/        /\\\\\     /\\\\\\\\\\     /\\\\\\\\   /\\\\\\\\\\\  /\\\\\\\\\\\  /\\\\\\\\\
'    \/\\\//////\\\      /\\\///\\\  \/\\\//////    /\\\/////\\\ \////\\\////  \////\\\////  \////////\\\
'     \/\\\    \//\\\    /\\\  \//\\\ \/\\\\\\\\\\  /\\\\\\\\\\\     \/\\\         \/\\\        /\\\\\\\\\\
'      \/\\\     \//\\\  \//\\\  /\\\  \////////\\\ \//\\///////      \/\\\ /\\     \/\\\ /\\   /\\\/////\\\
'       \/\\\      \//\\\  \///\\\\\/    /\\\\\\\\\\  \//\\\\\\\\\\    \//\\\\\      \//\\\\\   \//\\\\\\\\/\\
'        \///        \///     \/////     \//////////    \//////////      \/////        \/////     \////////\//
'                                  Let's find out together what makes a PIC Tick!
'
' DDS sinewave generator with a PWM acting as an 8-bit DAC.
' This demo is created for standard 18F devices that contain a CCP peripheral.
'
' In tests, the program can output sine wave frequencies from a few 10s of Hz to over 32 KHz,
' but for higher frequencies, the low pass filter will need to be a lot better than the simple circuit shown below.
'
' Written for the Positron8 compiler by Les Johnson.
'
' A simple low pass filter circuit, as shown below, can be used to get a sine waveform out.
' The filter's components will need to be changed for different frequency sine waves.
'
'         |      1k
' PORTC.2 |----/\/\/\-----o------------------> Output
'         |               |
'         |             __|__
'         |             _____ 1nf to 100nF
'         |               |
'         |               |
'         |             -----
'                        ---  Gnd
'                         -
'
    Device = 18F26K22                                       ' Tell the compiler what device it will compile for
    Declare Xtal = 64                                       ' Tell the compiler what frequency the device is operating at (in MHz)

'-----------------------------------------------------------------------------------------
' A preprocessor meta-macro to write to the 8-bit PWM peripheral
' Input     : pDutyCycle holds the 8-bit duty cycle for the PWM (0 to 255)
' Output    : None
' Notes     : None
'
$define PWM1_Write(pDutyCycle)     '
    CCP1CON = CCP1CON & 0b11001111 '
    WREG = pDutyCycle << 4         '
    WREG = WREG & 0b00110000       '
    CCP1CON = CCP1CON | WREG       '
    CCPR1L = pDutyCycle >> 2

'-----------------------------------------------------------------------------------------
' The main program starts here
' Output a sine wave signal from the CCP1 pin (PORTC.2)
'
Main:
    Setup()                                                 ' Setup the program

    DDS_SineWave(5000, 1000)                                ' Output a 5 KHz sinewave for approx 1000 ms

'-----------------------------------------------------------------------------------------
' Generate a sinewave signal from the CCP1 peripheral pin acting as an 8-bit DAC
' Input     : pFreq holds the frequency to generate
'           : pDuration holds the approximate time (in ms) for the waveform to be outputted
' Output    : None
' Notes     : DDS values calculated by:
'               cDivisor = (Device MIPS * 1000000) / 128
'               DDS value = (Frequency(in Hz) * 128) * (65536 / cDivisor)
'
Proc DDS_SineWave(pFreq As Word, pDuration As Dword)
    Dim dAccum As Dword Access                              ' Accumulator for the DDS
    Dim dTemp  As Dword Access                              ' A temporary variable
    Dim bDuty  As Byte  Access                              ' Holds the 8-bit duty cycle value for the PWM DAC
    Symbol cDivisor = (((_xtal / 4) * 1000000) / 128)       ' Create a divisor based upon the device's operating frequency
    Symbol cMult = (65536.0 / cDivisor)                     ' Create a floating point multiplier constant
'
' 256 element, 8-bit sinewave data table
'
    Dim SineTable As Flash8 = {$80, $83, $86, $89, $8C, $8F, $92, $95,
                               $98, $9C, $9F, $A2, $A5, $A8, $AB, $AE,
                               $B0, $B3, $B6, $B9, $BC, $BF, $C1, $C4,
                               $C7, $C9, $CC, $CE, $D1, $D3, $D5, $D8,
                               $DA, $DC, $DE, $E0, $E2, $E4, $E6, $E8,
                               $EA, $EC, $ED, $EF, $F0, $F2, $F3, $F5,
                               $F6, $F7, $F8, $F9, $FA, $FB, $FC, $FC,
                               $FD, $FE, $FE, $FF, $FF, $FF, $FF, $FF,
                               $FF, $FF, $FF, $FF, $FF, $FF, $FE, $FE,
                               $FD, $FC, $FC, $FB, $FA, $F9, $F8, $F7,
                               $F6, $F5, $F3, $F2, $F0, $EF, $ED, $EC,
                               $EA, $E8, $E6, $E4, $E2, $E0, $DE, $DC,
                               $DA, $D8, $D5, $D3, $D1, $CE, $CC, $C9,
                               $C7, $C4, $C1, $BF, $BC, $B9, $B6, $B3,
                               $B0, $AE, $AB, $A8, $A5, $A2, $9F, $9C,
                               $98, $95, $92, $8F, $8C, $89, $86, $83,
                               $7F, $7C, $79, $76, $73, $70, $6D, $6A,
                               $67, $63, $60, $5D, $5A, $57, $54, $51,
                               $4F, $4C, $49, $46, $43, $40, $3E, $3B,
                               $38, $36, $33, $31, $2E, $2C, $2A, $27,
                               $25, $23, $21, $1F, $1D, $1B, $19, $17,
                               $15, $13, $12, $10, $0F, $0D, $0C, $0A,
                               $09, $08, $07, $06, $05, $04, $03, $03,
                               $02, $01, $01, $00, $00, $00, $00, $00,
                               $00, $00, $00, $00, $00, $00, $01, $01,
                               $02, $03, $03, $04, $05, $06, $07, $08,
                               $09, $0A, $0C, $0D, $0F, $10, $12, $13,
                               $15, $17, $19, $1B, $1D, $1F, $21, $23,
                               $25, $27, $2A, $2C, $2E, $31, $33, $36,
                               $38, $3B, $3E, $40, $43, $46, $49, $4C,
                               $4F, $51, $54, $57, $5A, $5D, $60, $63,
                               $67, $6A, $6D, $70, $73, $76, $79, $7C}

    dAccum = pFreq * 128)                                   ' \
    dTemp = dAccum * cMult                                  ' / Calculate the value for the DDS, based upon the frequency required

    pDuration = pDuration * 241                             ' Calculate the duration in approx milliseconds (for use with a device operating at 64MHz)
    PinOutput PORTC.2                                       ' Make the PWM peripheral's pin an output
    dAccum.Long = 0                                         ' Reset the accumulator
    Do                                                      ' Create a loop
        Repeat : Until PIR1bits_TMR2IF = 1                  ' \ Synchronise to the start of the PWM cycle
        PIR1bits_TMR2IF = 0                                 ' /
        dAccum.Long = dAccum.Long + dTemp.Long              ' Accumulation of the frequency
        bDuty = CRead8 SineTable[dAccum.Byte2]              ' Divide by 65536, and read the table's data element
        PWM1_Write(bDuty)                                   ' Write the duty cycle value to the PWM peripheral
        Dec pDuration                                       ' Decrement the duration counter
        If pDuration = 0 Then                               ' Has the duration reached 0?
            PinInput PORTC.2                                ' Yes. So make the PWM peripheral's pin an input
            ExitProc                                        ' Exit the procedure
        EndIf
    Loop
EndProc

'-----------------------------------------------------------------------------------------
' Configure CCP1 as PWM with a resolution of 8-bits (0 - 255)
' Input     : None
' Output    : None
' Notes     : Keeps the CCP1 pin as an input, so no signal is available yet
'           : With a 64MHz oscillator, the PWM operates at 250 KHz
'
Proc PWM1_Init()
    T2CON = 0b00000100                                      ' \
    PR2 = 63                                                ' / Setup Timer2 to give a 250 KHz frequency for the 8-bit PWM
    CCPR1L = 0                                              ' Clear the PWM duty SFR
    CCP1CON = 0b00001100                                    ' Configure CCP1 as PWM
    PinInput PORTC.2                                        ' Make the PWM peripheral's pin an input
EndProc

'-----------------------------------------------------------------------------------------
' Setup the program
' Input     : None
' Output    : None
' Notes     : None
'
Proc Setup()
    PWM1_Init()                                             ' Setup CCP1 as an 8-bit PWM
EndProc

'-----------------------------------------------------------------------------------------
' Configure the fuses to operate at 64MHz using an external 16MHz crystal, on a PIC18F26K22 device
'
Config_Start
    FOSC = HSHP           ' HS oscillator (high power > 16 MHz)
    PLLCFG = On           ' Oscillator multiplied by 4
    PRICLKEN = On         ' Primary clock enabled
    FCMEN = Off           ' Fail-Safe Clock Monitor disabled
    IESO = Off            ' Internal/External Oscillator Switchover mode disabled
    PWRTEN = On           ' Power up timer enabled
    BOREN = SBORDIS       ' Brown-out Reset enabled in hardware only (SBOREN is disabled)
    BORV = 190            ' Brown Out Reset Voltage set to 1.90 V nominal
    WDTEN = Off           ' Watch dog timer is always disabled. SWDTEN has no effect.
    WDTPS = 128           ' Watchdog Timer Postscale 1:128
    CCP2MX = PORTC1       ' CCP2 input/output is multiplexed with RC1
    PBADEN = Off          ' PORTB<5:0> pins are configured as digital I/O on Reset
    CCP3MX = PORTB5       ' P3A/CCP3 input/output is multiplexed with RB5
    HFOFST = On           ' HFINTOSC output and ready status are not delayed by the oscillator stable status
    T3CMX = PORTC0        ' Timer3 Clock Input (T3CKI) is on RC0
    P2BMX = PORTB5        ' ECCP2 B (P2B) is on RB5
    MCLRE = EXTMCLR       ' MCLR pin enabled, RE3 input pin disabled
    STVREN = Off          ' Stack full/underflow will not cause Reset
    LVP = Off             ' Single-Supply ICSP disabled
    XINST = Off           ' Instruction set extension and Indexed Addressing mode disabled (Legacy mode)
    Debug = Off           ' Disabled
    Cp0 = Off             ' Block 0 (000800-001FFF) not code-protected
    CP1 = Off             ' Block 1 (002000-003FFF) not code-protected
    CP2 = Off             ' Block 2 (004000-005FFF) not code-protected
    CP3 = Off             ' Block 3 (006000-007FFF) not code-protected
    CPB = Off             ' Boot block (000000-0007FF) not code-protected
    CPD = Off             ' Data EEPROM not code-protected
    WRT0 = Off            ' Block 0 (000800-001FFF) not write-protected
    WRT1 = Off            ' Block 1 (002000-003FFF) not write-protected
    WRT2 = Off            ' Block 2 (004000-005FFF) not write-protected
    WRT3 = Off            ' Block 3 (006000-007FFF) not write-protected
    WRTC = Off            ' Configuration registers (300000-3000FF) not write-protected
    WRTB = Off            ' Boot Block (000000-0007FF) not write-protected
    WRTD = Off            ' Data EEPROM not write-protected
    EBTR0 = Off           ' Block 0 (000800-001FFF) not protected from table reads executed in other blocks
    EBTR1 = Off           ' Block 1 (002000-003FFF) not protected from table reads executed in other blocks
    EBTR2 = Off           ' Block 2 (004000-005FFF) not protected from table reads executed in other blocks
    EBTR3 = Off           ' Block 3 (006000-007FFF) not protected from table reads executed in other blocks
    EBTRB = Off           ' Boot Block (000000-0007FF) not protected from table reads executed in other blocks
Config_End

The source code and Proteus simulation project are attached below: "DDS_SineWave - 8-bit PWM", and here is a screenshot of the program listing above, working within the Proteus simulator. It is generating a 5KHz sine wave:

DDS - Sinewave Generator.jpg