This thread has been locked.

If you have a related question, please click the "Ask a related question" button in the top right corner. The newly created question will be automatically linked to this question.

PWM duty cycle controlled by ADC value

I'm looking for help regarding the use of an ADC value to control a PWM duty cycle. I require a 1kHz frequency PWM signal with two outputs and one of the two inverted. The duty cycle is varied using a potentiometer attached to an ADC channel. I have the code working to generate the signals as well as vary the duty cycle with the potentiometer. The only problem is that the duty cycle stops quite a bit short of 100%. It does however go down close to 0%. Am I doing something wrong? Here is all my code:

/*The circuit
 * One LED on PB6 (this is the inverted signal)
 * One LED on PB7
 * Potentiometer output connected to PB5
*/

#include "driverlib/pin_map.h"
#include <stdint.h>
#include <stdbool.h>
#include "inc/hw_gpio.h"
#include "inc/hw_types.h"
#include "inc/hw_memmap.h"
#include "driverlib/sysctl.h"
#include "driverlib/pin_map.h"
#include "driverlib/gpio.h"
#include "driverlib/pwm.h"
#include "driverlib/adc.h"
#include "driverlib/fpu.h"

#define FREQUENCY 1000 //1kHz

void delayMS(int);
void init_FPU(void);
void init_PWM(void);
void init_sysCtl(void);
void init_Pins(void);
void init_ADC(void);
void set_Duty(uint32_t);

uint32_t timer_count =0;

int main(void)
{
   uint32_t analogData =0;
   
   //initialise peripherals
   init_sysCtl();
   init_Pins();
   init_ADC();
   init_PWM();
   init_FPU();

   delayMS(1000);
   
    while(1)
    {
      	ADCProcessorTrigger(ADC0_BASE,0);
    	while(!ADCIntStatus(ADC0_BASE, 0, false)){} //wait for adc to finish conversion
    	ADCSequenceDataGet(ADC0_BASE, 0, &analogData); //store the ADC data 
    	set_Duty(analogData); //change the duty cycle
    }

}

void init_FPU(void){
	FPULazyStackingEnable();
	FPUEnable();
}

void init_sysCtl(void){
	 //Set the clock
	   SysCtlClockSet(SYSCTL_SYSDIV_1 | SYSCTL_USE_OSC |   SYSCTL_OSC_MAIN | SYSCTL_XTAL_16MHZ);

	   //Configure PWM Clock divide system clock by 1 (Higher resolution)
	   SysCtlPWMClockSet(SYSCTL_PWMDIV_1);

	   // Enable the peripherals used by this program.
	   SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOB);	//Enable PortB
	   SysCtlPeripheralEnable(SYSCTL_PERIPH_PWM0);  //Enable PWM module 0
	   SysCtlPeripheralEnable(SYSCTL_PERIPH_ADC0); //Enable ADC0
}

void init_Pins(void)
{
	//Configure PB6,PB7 Pins as PWM
	GPIOPinConfigure(GPIO_PB6_M0PWM0);
    GPIOPinConfigure(GPIO_PB7_M0PWM1);
    GPIOPinTypePWM(GPIO_PORTB_BASE, GPIO_PIN_6 | GPIO_PIN_7);
	    
    //Configure PB5 as ADC_Ch11
    GPIOPinTypeADC(GPIO_PORTB_BASE, GPIO_PIN_5);

}

void init_PWM(void)
{
	uint32_t PWM_clock;
	PWM_clock = SysCtlClockGet();
	timer_count = ((PWM_clock)/(FREQUENCY))-1; //set the count value

	PWMGenConfigure(PWM0_BASE, PWM_GEN_0, PWM_GEN_MODE_DOWN | PWM_GEN_MODE_NO_SYNC);
	PWMOutputInvert(PWM0_BASE, PWM_OUT_0_BIT,true);

    //Set the Period (expressed in clock ticks)
	PWMGenPeriodSet(PWM0_BASE, PWM_GEN_0, timer_count);

    //Set PWM duty cycle so that both outputs are close to 0 at startup
	PWMPulseWidthSet(PWM0_BASE, PWM_OUT_0,timer_count-1); //inverse PWM, therefore need close to 100% for 0 output
    PWMPulseWidthSet(PWM0_BASE, PWM_OUT_1,1);

	// Enable the PWM generator
	PWMGenEnable(PWM0_BASE, PWM_GEN_0);

	// Turn on the Outputs
	PWMOutputState(PWM0_BASE, PWM_OUT_0_BIT | PWM_OUT_1_BIT, true);
}

void set_Duty(uint32_t data)
{
	uint32_t dutyCycle = 0;
	double scaleFactor = ((timer_count)/4096); // (period/max_ADC_value)
	if (data <= 5)// to ensure duty cycle is never exactly 0%
	{
		data=5;
	}
	dutyCycle = (int)data*scaleFactor; 

	//set the duty cycles to be the same. PWM0 is automatically the inverse of PWM1
	PWMPulseWidthSet(PWM0_BASE, PWM_OUT_0, dutyCycle);
	PWMPulseWidthSet(PWM0_BASE, PWM_OUT_1, dutyCycle);
}

void init_ADC(void){
	ADCSequenceConfigure(ADC0_BASE, 0, ADC_TRIGGER_PROCESSOR, 0);
	ADCSequenceStepConfigure(ADC0_BASE, 0, 0,ADC_CTL_IE | ADC_CTL_END | ADC_CTL_CH11); //Enable ADC on PB5 (CH11)
	ADCSequenceEnable(ADC0_BASE, 0);
}

void delayMS(int ms) {
    SysCtlDelay( (SysCtlClockGet()/(3*1000))*ms ) ; //rudimentary delay
}

  • Hello Jarryd,

    I think the Duty Cycle of 100% and 0% are not possible with PWM due to the nature of the comparison.

    Regards
    Amit
  • I understand that I won't achieve the absolute limits but my maximum duty cycle was only about 85% which is well short of 100%.
  • Have you:

    a) confirmed that "near" 100% PWM duty cycle occurs when you "manually" enter the proper (full) value into, "PWMPulseWidthSet()?"    We don't want the PWM "held hostage" by other of your program scalings/manipulations!

    b) might dead-band be "on" (by default) and set so wide that it limits the width of your PWM pulse?

    c) you don't describe "where" you're measuring the "limited duty" PWM pulse. Might a gate-driver or other circuitry be limiting?

    d) your 1KHz PWM frequency is rather low.   (imho)    As the ARM PWM Generator employs a 16 bit counter - might you be "bumping" against that 65K count limit - thus abbreviating your PWM pulse width?

    We've used ARM MCUs for past 10 years - always able to obtain stable PWM in the 95-98% (max duty) range...

  • Hello Jarryd,

    Check the PWM Comparator registers as they get set in promotion to the ADC value being read.

    Regards
    Amit
  • Hi Amit,

    Seems so much quicker/more efficient to "bypass" any/all complications added by the ADC - and simply test/verify the PWM Generator's ability to deliver a "near 100%" duty cycle via direct, numeric entry into, "PWMPulseWidthSet()!"

    Bypassing KISS - by "expanding the field" - runs counter to the medical practice of "draping" - in which (only) the area of interest is exposed/probed...
  • Hello cb1,

    Indeed. Or could it be a reference v/s actual voltage source being different prohibiting a reach of full range.

    Regards
    Amit
  • Amit Ashara said:
    could it be a reference v/s actual voltage source being different prohibiting a reach of full range

    Surely Amit - yet as you (so) often advise, "First things first!"    (and that is SO KISS!)

    Establish that the PWM Generator can really "approach" 100% duty!    Only when that's been test/verified - move to the other (lesser) functions - which may be limiting PWM Duty.

    "Wheel spin" - much like smoke/bright lights - denotes activity (misguided) but too often "diverts" from the real issue...

  • Hello cb1,

    I did with a walk through of the PWM and it goes all the way above 90%...

    Regards
    Amit
  • Yet - your "walk through" may not have fully duplicated poster's.

    Until his PWM Generator is proven to yield 95%+ PWM duty - all other activity (remains) premature.

    First things first (for you/me/poster) continues as "sage" advice.
  • Hello cb1,
    Agreed...
    Regards
    Amit
  • Hi guys,

    Thanks for your suggestions.
    I can confirm that the PWM generator is capable of reaching +95% duty cycle, just not using the potentiometer values.
    I was able to solve the problem by changing the scaling factor but I have no understanding what the problem is. I originally scaled the ADC value by multiplying it by (timer_count)/(max_ADC_value) and after finding no other solutions I decided to change the scaling factor. I tested out changing the scaling factor by dividing the timer_count value by less than the max ADC to force higher dutyCycle values and thus reach the max duty cycle at a lower potentiometer position. There seems to be strange behaviour with the max_ADC_value. At a value of 4000 the behaviour is very close to that of the actual max_ADC_value of 4096, but changing it just one value down to 3999 makes the duty cycle exceed the timer_count value and then some type of overflow occurs. So in the mean time I've had to add soft limits to trim the dutyCycle value to 99% of the timer_count value if it exceeds it.

    Any idea what is happening? Or if my thinking is wrong?
  • Your chosen PWM frequency will determine the maximum value which may be (properly/successfully) loaded into the function, "PWMPulseWidthSet()."    I was before - and remain now - suspicious that you are placing an illegal value into the final parameter - that function.

    And - as stated (harped upon) your "blend" of multiple functions prevented both Amit & myself from quicker - less effort - analysis.   Combining functions - as so many do here - and then encountering trouble - is a sure violation of KISS!    And causes pain/delay/heartache - and for what?

    I'm near certain that you are improperly loading the last parameter (as stated).    Your focus now must be upon revisiting your math - and adjusting such that illegal parameters are prevented!

    Give KISS a chance - it will (quickly) become your "best friend."

    [edit] it is possible that you are "bumping against" known ARM PWM limitations when "getting too close" to PWM duty limits.   We find near 97% duty to be safe - for "most" ARM MCUs.   Above that duty may place you into, "No man's land" and the difference between 97% and 100% is unlikely to (ever) be noted!

  • Hello Jarryd,

    Shouldn't the equation

    double scaleFactor = ((timer_count)/4096); // (period/max_ADC_value)
    if (data <= 5)// to ensure duty cycle is never exactly 0%
    {
    data=5;
    }
    dutyCycle = (int)data*scaleFactor;

    be

    scaleFactor = (data*100)/4096; // for converting the ADC to scale factor
    dutyCycle = (scaleFactor*timer_count)/100;

    This will help avoid issues with round off

    Regards
    Amit