PDM library (Pulse Density Modulation)

Dependents:   mbed-shiny

Software PDM

Software PDM (Pulse Density Modulation) is a viable alternative to software PWM where mcu resources are limited because we only need one timer to control the output (whereas PWM requires two timers).

The code presented here is based on an article and source code released by Ken Wada from Aurium Technologies.
Additional info on Pulse Density Modulation is available here.

Keep in mind that the dynamic range is limited when we bit-bang PDM as following limitations apply:

  • A very low pulse width value will inevitably break (and even stop) other code. This value depends on the complexity of the additional user code. A decent start value for a single instance PDM is 100us or more. Multiple PDM instances will require a higher value.
  • Keep the maximum level (DMAX) within reasonable limits. To get a higher precision, we would be tempted to set this as high as possible. As a result, to realize the highest and lowest states (near zero, and near maximum), we need to wait at least for the DAC's maxlevel number of updates in order for those states to manifest properly into the output.

Both the pulse width and DMAX values determine the highest frequency we can reproduce :

                         1
max. frequency = -----------------
                 PulseWidth x DMAX

Example
-------
pulse width = 10us
DMAX        = 256 counts

                       1
max. frequency = ------------- = 390.625Hz
                 0.00001 x 256

Applications

  • Low frequency analog output for mcu's without DA converters.
  • LED brightness control.
  • ...

Examples

Basic use

// mbed-shiny
#include "mbed.h"
#include "SoftPdmOut.h"

SoftPdmOut pdm(LED1);

int main()
{
    float pdmSet = 0.0f;
    float pdmAdd = 0.01f;

    // Continuously cycle the output
    while(1)
    {
        pdm = pdmSet;
        wait_ms(10);
        if(pdmSet >= 1.0f)
            pdmAdd = -0.01f;
        if(pdmSet <= 0.0f)
            pdmAdd = 0.01f;
        pdmSet += pdmAdd;
    }
}



Demonstrate library functions

#include "mbed.h"
#include "SoftPdmOut.h"

Serial pc(USBTX, USBRX);

// optional parameters, when omitted, their default library value will be used.
#define PULSEWIDTH 100
#define DMAX       64
#define STARTLEVEL 0.5f

// PinName and optional pulse width, dmax, start level
// When  SoftPdmOut pdm(LED1);  is used, all optional parameters revert to their default values (defined in the library).
SoftPdmOut pdm(LED1, PULSEWIDTH, DMAX, STARTLEVEL);

int main()
{
    pc.baud (38400);
    
    float pdmVal;
    
    printf("PDM test\r\n");
    pdmVal = 0.1f;

    // Stop the PDM with pin idle value = 0
    printf("Stop the PDM with pin idle value = 0\r\n");
    pdm.stop(0);
    wait(1);
    pdm.start();

    // Stop the PDM with pin idle value = 1
    printf("Stop the PDM with pin idle value = 1\r\n");
    pdm.stop(1);
    wait(1);
    pdm.start();

    // Set PDM level, read it back and print it, can be done in two ways :
    // using pdm.write(nn) and pdm.read(nn)  OR  pdm = nn and <variable> = pdm.
    printf("Set the current PDM level to %f\r\n", pdmVal);
    // Write the current PDM level using pdm.write()
    pdm.write(pdmVal);
    // Write the current PDM level using the shorthand notation instead of pdm.write()
    pdm = pdmVal;
    printf("Get the current PDM level : %f\r\n", pdm.read());
    // Read the current PDM level using the shorthand notation instead of pdm.read()
    float dummy = pdm;
    printf("Get the current PDM level : %f\r\n", dummy);
    wait(1);
    // Change the pulse width and DMAX values.
    pdm.PulseWidth(100);
    pdm.Dmax(128);

    float pdmSet = 0.0f;
    float pdmAdd = 0.01f;

    // Cycle the output
    while(1)
    {
        pdm = pdmSet;
        wait_ms(10);
        if(pdmSet >= 1.0f)
            pdmAdd = -0.01f;
        if(pdmSet <= 0.0f)
            pdmAdd = 0.01f;
        pdmSet += pdmAdd;
    }
}

SoftPdmOut.cpp

Committer:
frankvnk
Date:
2014-12-28
Revision:
6:85236b955cf6
Parent:
5:408b68033540

File content as of revision 6:85236b955cf6:

/******************************************************************************************************/
/*  Pulse Density Modulation driver.                                                                  */
/*  FILE: PDM.cpp                                                                                     */
/*                                                                                                    */
/*  Code based on an article and source code released by Ken Wada from Aurium Technologies Inc.       */
/*  http://www.embedded.com/electronics-blogs/embedded-round-table/4414405/Pulse-density-modulation   */
/*                                                                                                    */
/******************************************************************************************************/

#include "SoftPdmOut.h"

SoftPdmOut::SoftPdmOut(PinName pdm, uint32_t PulseWidth, uint32_t Dmax, float StartLevel) : _pdm(pdm), _PulseWidth(PulseWidth), _Dmax(Dmax)
{
    _pdm = 0;
    _running = 0;
    if(StartLevel >= 0.0f && StartLevel <= 1.0f) _Level = (uint32_t)(StartLevel * (float)_Dmax);
    else _Level = 0;
    start();
}

void SoftPdmOut::start(void)
{
    if (!_running) {
        // enable ONLY if state has changed
        _accumulator  = 0;                                              // zero out the error accumulator
        _running      = 1;                                              // NOW ... enable the controller
        _pdmTicker.attach_us(this,&SoftPdmOut::pdmTick,_PulseWidth);    // Start the PDM.
        wait_ms(5);                                                     // Wait for output to settle
    }
}

void SoftPdmOut::stop(bool idleState)
{
    if (_running) {
        // disable ONLY if state has changed
        _running      = 0;          // disable the controller
        _pdmTicker.detach();        // Stop the PDM.
        _pdm = idleState;           // Set output (default = 0)
    }
}

void SoftPdmOut::PulseWidth(uint32_t level)
{
    if(!_running)                // When pdm is inactive, only change the pulse width and don't start the pdm
        _PulseWidth = level;
    else
    {
        stop();
        _PulseWidth = level;
        start();
    }
}

uint32_t SoftPdmOut::getPulseWidth(void)
{
    return _PulseWidth;
}

void SoftPdmOut::Dmax(uint32_t level)
{
    _Level = (uint32_t)((float)_Level * ((float)level / (float)_Dmax));  // Rescale level : level = level * (new level / current DMAX)
    _Dmax = level;
    _accumulator  = 0;                  // zero out the error accumulator
}

uint32_t SoftPdmOut::getDmax(void)
{
    return _Dmax;
}

void SoftPdmOut::write(float level)
{
    // Only change the level when the desired level is within range
    if(level <= 0.0f) _Level = 0;
    else if(level >= 1.0f) _Level = _Dmax;
    else if (level > 0.0f && level < 1.0f) _Level = (uint32_t)(level * (float)_Dmax);
}

float SoftPdmOut::read(void)
{
    return ((float)_Level / (float)_Dmax);
}

SoftPdmOut::operator float()
{
    return read();
}

SoftPdmOut & SoftPdmOut::operator= (float value)
{
    write(value);
    return(*this);
}

void SoftPdmOut::pdmTick(void)
{
    if(_Level == 0) {
        _pdm = 0;
        return;
    }
    if(_Level == _Dmax) {
        _pdm = 1;
        return;
    }
    _accumulator += _Level;         // accumulate error integral
    if (_accumulator > _Dmax) {
        _accumulator -= _Dmax;
        _pdm = 1;
    } else
        _pdm = 0;
}