Bit-banging a digital-to-analog converter - Embedded.com

Bit-banging a digital-to-analog converter

Many times in designing an embedded system, you'll need a simple analog output from the microcontroller but won't have access to a hardware resource that can effect the digital-to-analog (D/A) conversion. Perhaps your system doesn't have a converter built in or it's too expensive to add one. In such a case, you can use a port pin and bit bang using pulse-width modulation (PWM) to accomplish the D/A output.

A simple timer resource produces an analog output from an embedded controller. I'll demonstrate some things that may be done using very little resources and the timer service as I described in a previous blog.

Bit-banging a D/A converter has distinct advantages and disadvantages:

Advantages:

  1. A bit banging PWM converter is very cheap. The only cost to implement this in hardware is a spare port pin and a simple single pole resistor–capacitor (RC) filter.
  2. The implementation of a simple PWM is relatively straightforward. The actual code to do a PWM bit-bang interface requires a timer service interrupt and very little other code and resources.
  3. Using object-oriented techniques; we can instance many bit-banging PWM instances. The limit is set by the number of available port pins on the microcontroller.

Disadvantages:

  1. The bit-banging PWM may not be accurate because the output voltage of the converter depends on the actual logic level of the port pin. Of course, we could use the port pin to modulate a field-effect transister (FET), which in turn is connected to a band-gap reference. However, this may defeat the “cheapness” of the bit-bang PWM.
  2. You'll either need to really amp up the sampling rate for the timer service, (several 100s of kilohertz), have a very large RC filter, or add more poles to the filter using op-amp circuitry. Again, this will also defeat the relative cheapness of the bit-bang structure.

An overview of pulse-width modulation
So let us start by describing the basics of the PWM, or pulse width modulation. This won't take long. I am doing this merely as a starting point so you and I have a common reference as I explain things.

Figure 1 shows the basics of a PWM and the idea of how to implement one. The basic idea is to start with the desired digital output, (in this case a digital sine wave value), and to convert it via a triangle wave, (in this case a timer-based counter). We use this to generate the desired PWM signal. We add a low-pass filter this signal to get the resultant analog output, (sine wave) that we desire.


Click on image to enlarge.
Figure 1: PWM signal generation.

Objects and interfaces
First describe all the things you want the PWM to do. You'll need to describe the various variables, interface, and other such things. The first step to the design concept and implementation should start out with the header file. Listing 1 shows the header file for our simple bit-banging PWM.

A few things of note in this listing: a forward reference to the main structure struct pwm_BBS is declared near the beginning. This is because the member function pointer may require the definition of this type just in case it's needed.

I created this member function pointer to serve a similar purpose as a virtual function in C++. This allows the user to define a place holder for the actual hardware port pin driver without knowing how it will eventually be implemented.

#ifndef __PWM_BB_H    #define __PWM_BB_H/****************************************************************************//*    FILE: pwm_BB.h                                                        *//*                                                                          *//*    These file(s) contain the methods and attributes required for         *//*    the management of a bit-bang PWM interface. It is expected that       *//*    the PWM_BB_update() subroutine is called in a TIMER ISR context.      *//*                                                                          *//*      BY:   Ken Wada                                                      *//*            Aurium Technologies Inc.                                      *//*            20-February-2013                                              *//*                                                                          *//****************************************************************************/    struct pwm_bbS;    typedef void (*VBIT_ENABLEFN)(struct pwm_bbS *, const int);    typedef struct  pwm_bb_dcbS    {      int enable;                             /* 1 == enable PWM            */      int idle_state;                         /* PIN state when disabled    */      unsigned int  duty;                     /* current duty cycle setting */      unsigned int  max_count;                /* # of cycles in PWM counter */      VBIT_ENABLEFN vbit_enableFn;            /* set or clear hardware bit  */    } pwm_bb_dcbS;    typedef struct  pwm_bbS    {      pwm_bb_dcbS dcb;                        /* PWM device control block   */      unsigned int  dcount;                   /* PWM tick down counter      */    } pwm_bbS;    #ifdef  __cplusplus      extern  "C" {    #endif      void  pwm_bbS_init          (pwm_bbS *_this, const pwm_bb_dcbS *dcb);      void  pwm_bbS_enable        (pwm_bbS *_this, const int enable);      void  pwm_bbS_dutySet       (pwm_bbS *_this, const unsigned int duty);      void  pwm_bbS_idleSet       (pwm_bbS *_this, const int idle_state);      void  pwm_bbS_dutyCycleSet  (pwm_bbS *_this, const float dutyCycle);      unsigned int pwm_bbS_dutyGet  (const pwm_bbS *_this);      int pwm_bbS_idleGet           (const pwm_bbS *_this);      float pwm_bbS_dutyCycleGet    (const pwm_bbS *_this);      void  pwm_bbS_bitEnable     (pwm_bbS *_this, const int enable);      /*****        The following is the main ISR-based update function. Call this        subroutine DIRECT from the TIMER ISR      ***/      void  pwm_bbS_updateISR (pwm_bbS *_this) __arm;    #ifdef  __cplusplus      }    #endif#endif

Listing 1: The Bit-Bang PWM attributes methods and interfaces. (To download all the code listings, click here.)

PWM device control block
Something I'm introducing here is the sub-object called the device control block (DCB). The DCB is the block that defines all of the control and interface definitions required for proper configuration and startup of the main firmware controller module. In this case, the DCB pwm_pp_dcBS serves as the configuration and initial control settings for the main PWM pwm_bbS object.

enable : The enable variable allows you to enable or disable the PWM controller. In this case you define an integer 1 to enable the controller and the integer zero (0) to disable the controller.

idle_state : The idle_state variable defines the logic level that you wish to hold the PWM output to when it's in the disabled state. There are times when this may be handy to define whether you want to turn the PWM wide open or fully turned off when the controller is disabled.

duty : The duty variable represents the duty cycle in the controller's native units. The native units for the duty may range from zero(0) to full scale, in this case the value of the max_count parameter. In some processor architecture, you don't have the luxury of converting engineering units, (usually floating-point percentage values), to the controller native units. Therefore, you'll need to provide interfaces for the native units for those architectures, too.

max_count : This is really the maximum bit resolution of our PWM-based D/A converter. I could have easily stated 8-bits, or 12-bits. As a design decision, I decided that to ensure maximum flexibility and reuse, to define this as some unsigned integer that can be initialized to any value. This is the maximum counter that will generate the triangle wave.

vbit_enableFn : This is a C function pointer. The idea here is to create a generic device independent PWM hardware-specific control. This allows us to define and implement the PWM without worrying about how to set or clear the hardware bit.

PWM object
The main PWM object consists of the PWM's DCB and the down counter, dcount . The down counter is used to count from the DCB.max_count down to zero. When the dcount reaches zero, it resets to the DCB.max_count value. This counter “digitally” forms the triangle wave that's used to compare and calculate the bit setting for the PWM signal.

Interfaces: The function interfaces define everything you want the PWM object to do and how you will monitor and set it up. Table 1 briefly gives an overview of all of the interfaces and a brief description of how they are used.

Interface

Description

pwm_bbS_init

Initialize an PWM controller object

pwm_bbS_enable

Enable or disable the PWM controller

pwm_bbS_dutySet

Setup the duty setting in native PWM units, (internal function)

pwm_bbs_dutyGet

Get the current duty setting in native PWM units

pwm_bbS_idleSet

Set the desired IDLE state when the PWM is disabled

pwm_bbS_idleGet

Get the current IDLE state setting

pwm_bbS_bitEnable

This function encapsulates the function pointer hardware interface

pwm_bbS_dutyCycleSet

Allows us to setup the duty cycle in engineering (0-100%) units

pwm_bbS_dutyCycleGet

Retrieves the duty cycle in engineering, (0-100%) units

pwm_bbS_updateISR

This is the main timer interrupt service routine for the PWM

Table 1: PWM interface descriptions.

Instead of presenting every listing within this article, I shall present the listing of the most important functions along with a brief description of the internal workings of the code.

Interrupt service routine
Listing 2 shows the main PWM engine. Here is where the “rubber meets the road.” This code is the primary service that does all the work for the PWM engine. As you can see, there is very little code here. The code is compact because most of the processing is done as pre-processing and post-processing. The actual code consists of the following parts:

  1. Test to see if the PWM controller is enabled
  2. Comparing the down counter with the duty setting
  3. Setting the hardware port pin to a one or zero on the comparison result.
  4. Updating the down counter

#define __PWM_BBS_UPDATEISR_C/****************************************************************************//*    FILE: pwm_bbS_updateISR.c                                             *//*                                                                          *//*    These file(s) contain the methods and attributes required for         *//*    the management of a bit-bang PWM interface. It is expected that       *//*    the pwm_bbS_updateISR () subroutine is called in a TIMER ISR context. *//*                                                                          *//*      BY:   Ken Wada                                                      *//*            Aurium Technologies Inc.                                      *//*            20-February-2013                                              *//*                                                                          *//****************************************************************************/#include  "pwm_BB.h"/****************************************************************************//*                            CODE STARTS HERE                              *//****************************************************************************/void  pwm_bbS_updateISR (pwm_bbS *_this){    if (_this->dcb.enable)    {      pwm_bbS_bitEnable (_this, (_this->dcount <= _this->dcb.duty));      _this->dcount = (0==_this->dcount) ?_this->dcb.max_count : _this->dcount - 1;    }}

Listing 2: The PWM update interrupt service routine. (To download all the code listings, click here.)

Hardware encapsulation
Listing 3 shows how the hardware is effectively encapsulated in the main PWM object. This technique enables you the maximum flexibility with microcontroller architecture and hardware variation. In effect, doing this type of encapsulation will enable us to reuse this PWM module in many different processor architectures and with many different hardware implementations.

#define   __PWM_BBS_BITENABLE_C/****************************************************************************//*    FILE: pwm_bbs_bitEnable.c                                             *//*                                                                          *//*    These file(s) contain the methods and attributes required for         *//*    the management of a bit-bang PWM interface.                           *//*                                                                          *//*      BY:   Ken Wada                                                      *//*            Aurium Technologies Inc.                                      *//*            20-February-2013                                              *//*                                                                          *//****************************************************************************/#include  "pwm_BB.h"/****************************************************************************//*                            CODE STARTS HERE                              *//****************************************************************************/void pwm_bbS_bitEnable(pwm_bbS *_this, const int enable){    if (_this->dcb.vbit_enableFn)    {      (* _this->dcb.vbit_enableFn)(_this, enable);    }}

Listing 3: using a function pointer member variable to encapsulate the hardware details (To download all the code listings, click here.)

Example implementation
Listing 4 shows how to implement the PWM controller object in a sample application. In this listing, the details of the USER application, initializations and service vectors are left out. These interfaces are defined as external interfaces to some other C code. Did you really want me to do all the work for you? In this example listing, I do show a few things about the PWM object usage. The following is a list of some of the salient details.

  • PWM object instantiation; in this case it is a global instance
  • How the timer interrupt service calls the PWM driver
  • How to properly initialize the DCB and set the PWM up
  • How to properly call the PWM object initialization, (before the interrupt is turned on!)

#define __MAIN_C/****************************************************************************//*    This is a code snippet of how to initialize and use the bit-bang PWM  *//*                                                                          *//*    Of course, this code will not compile because it is just an example   *//*    which is used to demonstrate how to initialize a single bit bang      *//*    object.                                                               *//*                                                                          *//*      BY:   Ken Wada                                                      *//*            Aurium Technologies Inc.                                      *//*            20o-February-2013                                             *//*                                                                          *//****************************************************************************/#include  "pwm_BB.h"                  /* bit-bang PWM interface             *//****************************************************************************//*    We define some initialization, startup and USER services here         *//****************************************************************************/extern  void system_setup (void);     /* some system setup                  */extern  void timer_isr_setup (void);  /* setup the interrupt service        */extern  void timer_setup (void);      /* setup and start the timer          */extern  void main_user_app (void);    /* some main user application         *//****************************************************************************//*    The following is some hardware specific interface for setting and     *//*    clearing some port or I/O pin.                                        *//****************************************************************************/extern  void hardware_bitEnable (const int enable);/****************************************************************************//*    We instance a single PWM object here. Of course, we can instance      *//*    many different objects such as an array of bit-bang PWM objects.      *//****************************************************************************/pwm_bbS my_pwm;                   /* instance one object                    *//****************************************************************************//*                          CODE STARTS HERE                                *//****************************************************************************/static void my_pwmBB_bitEnableFn (struct pwm_bbS *_this, const int enable){    hardware_bitEnable (enable);}/****************************************************************************//*    This is an old-school interrupt service routine. The vector is, of    *//*    course, dispatched by the main system vector interrupt table.          *//****************************************************************************/void interrupt my_timer_service (void){    pwm_bbS_updateISR (&my_pwm);}void init_pwm (void){    pwm_bb_dcbS my_dcb  =         /* initialize a config DCB on the stack   */    {      1,                          /* default to enable                      */      0,                          /* IDLE pin state == 0                    */      0,                          /* start at 0% duty cycle                 */      256,                        /* 8-bit PWM                              */      my_pwmBB_bitEnableFn        /* hardware bit enable / disable fn       */    };    pwm_bbS_init(&my_pwm, &my_dcb);   /* do the PWM initialization          */}/****************************************************************************//*    The main code                                                         *//****************************************************************************/void main (void){    system_setup ();              /* setup the device and hardware          */    init_pwm ();                  /* init the PWM object before the service */    timer_isr_setup ();           /* setup and configure the timer ISR      */    timer_setup ();               /* setup and start the system timer       */    while (1)    {      main_user_app ();    }}

Listing 4. (To download all the code listings, click here.)
Hardware consideration
You thought that was the end of the blog no? Well, no PWM is complete without discussing how to implement the low-pass filter. Since we are embedded systems engineers, we need to delve into the realm of hardware from time to time.

The way to select the proper resistor and capacitor for the low-pass filter can be done one of two ways. The first (the one I see most often), is to eyeball it, make an educated guess, and adjust accordingly using a scope until you get the desired ripple on the PWM signal.

The second–the way I think gets a result faster plus it makes you look really clever–is to use those techniques you learned in university to select the RC time constant. I'll go over a brief overview of how to make the RC selection here.

Single-pole low-pass filter: The simple RC filter is a single-pole low-pass filter. The Bode plot of the filter, along with the critical frequencies is shown in Figure 2 .


Click on image to enlarge.
Figure 2: Bode plot of the PWM low-pass filter.

The critical parameters to note are the two frequencies FPWM and FFILT . These are the PWM repetition frequency and the low-pass filter cut-off frequency respectively.

FPWM : The PWM repetition frequency is directly related to the TIMER interrupt interval. We can call this interrupt interval TS , for T-sampling. The TS parameter is the number of times per second that the timer service is called. The sampling time is also the reciprocal of the sampling frequency FS . The relationship between the sampling frequency, FS to the PWM repetition frequency FPWM is shown in Equation 1 .

Equation 1: Relationship between the sampling frequency and the PWM repetition frequency

Since, in our case, 'max_count' is 256, (8-bit converter), the PWM repetition frequency is 256 times slower than the sampling frequency!

The cut-off frequency, FFILT is defined as a set of two equations. The first equation, Equation 2 relates the cut-off frequency inequality to the RC filter time constant:

Equation 2: Inequality relation between cut-off frequency and the filter time constant.

The single-pole frequency transfer characteristic is 20dB per decade. We can use this information and the desired ripple in order to determine the relationship between the filter cut-off frequency, FFILT and the PWM repetition, FPWM frequency. For a 1% ripple and a single-pole filter we get the relationship as defined in Equation 3 .

Equation 3: Relationship between the cut-off frequency and the PWM repetition frequency.

Combining Equation 2 and Equation 3 will yield the inequality which relates the RC filter time constant to the PWM repetition frequency. This is shown as Equation 4 .

Equation 4: Relationship between the RC filter time constant to the PWM repetition frequency.

So, let us plug in some numbers here. If we use a sampling frequency of 10,240 Hertz, and plug this into Equation 1 , we get Equation 5 for FPWM :

Equation 5: Calculation of the PWM repetition frequency

A 100 µSEC interrupt which yields a 40-Hz PWM repetition frequency is a lot of work to yield such a low result!

Substituting the result from Equation 5 into Equation 4 will yield the minimum RC time constant for our simple single-pole low pass filter. This is shown in Equation 6 :

Equation 6: PWM low-pass filter time constant .

A better solution
0.40 seconds is pretty darned slow. All of this effort running at a 100 µ-second timer service clip to get such a slow result. The PWM controller is OK for slow processes. However, it does require a fair amount of processor time resources even to regulate a value to near DC.

Another method is superior to the software PWM. This method is the inverse of the popular sigma-delta modulation scheme. Basically, this is PDM, or pulse density modulation. I'll do a how-to blog on how to implement the PDM scheme in next time.

Ken Wada is president and owner of Aurium Technologies, an independent product design and consulting firm in California's Silicon Valley. Ken has over 25 years of experience architecting and designing high-tech products and systems, including the FASTRAK vehicle-sensing system for toll roads and bridges. His expertise includes industrial automation, biotechnology, and high-speed optical networks. Ken holds four patents. You may reach him at .

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.