Search Microcontrollers

Sunday, March 2, 2014

Stellaris Launchpad - PWM Library

I played a bit with PWM, which I actually need to drive a laser diode (2.4W 808nm) at the moment (will dig into this probably more in future posts).
Once you get the basics PWM is not difficult with the LM4F, however I found way easier for my application to create a minimalist library to deal with PWM signals, each time I look at it I keep repeating to myself that it could be better... but the way it is now already fits my needs (and maybe yours, but feel free to improve it and let me know :) ).

One thing I wanted was to have a simple interface that would allow me to play with multiple PWM generators at the same time, 8 seemed a fair amount, so I pre-allocated an array of 8, but it is fairly easy to expand the array.
Meh, some range checking would help maybe, but the Stellaris does not run on MS Windows, so it does pretty much what you tell it to do, just be careful not to add more than 8 pwm or at least remember to expand the array and you should be fine.

That said, each pwm generator is actually coming from a timer, we can use 16 bit or 32 bit timers and I figured that a 16 bit timer would do (did not try yet, but I think I found how to use it in 32 bit mode).
Still I did not implement pre-scaling which could be beneficial if you have to run with very low frequencies (<2Hz).

Here's  the library (don' t expect anything fancy, it's  just easy to use and actually works)


#include <inc/hw_memmap.h>
#include <inc/hw_types.h>
#include <driverlib/gpio.h>
#include <driverlib/sysctl.h>
#include <driverlib/uart.h>
#include <inc/hw_timer.h>
#include <driverlib/timer.h>
#include <driverlib/pin_map.h>

int pwm_count = 0;
int pwmtimers[8][4];

/**
 * each timer can send the signal out to 2 different gpios
 * 0 means to set it to the lowest of the two 1 to the highest
 *
 * TIMER    PIN(gpio=0) PIN(1)
 * T0A         PB6 PF0
 * T0B PB7 PF1
 * T1A PB4     PF2
 * T1B          PB5     PF3
 * T2A          PB0     PF4
 * T2B          PB1     PB1
 * T3A          PB2     PB2
 * T3B          PB3     PB3
 *
 *
 *
 */
int addPWM(int pwm_timer,int subtimer, int gpio) {
pwmtimers[pwm_count][0] = pwm_timer;
pwmtimers[pwm_count][1] = subtimer;
if (gpio == 0) {
if ((int)subtimer == (int)TIMER_A) {
switch (pwm_timer) {
case TIMER0_BASE:
pwmtimers[pwm_count][2] = GPIO_PIN_6;
pwmtimers[pwm_count][3] = GPIO_PB6_T0CCP0;
break;
case TIMER1_BASE:
pwmtimers[pwm_count][2] = GPIO_PIN_4;
pwmtimers[pwm_count][3] = GPIO_PB4_T1CCP0;
break;
case TIMER2_BASE:
pwmtimers[pwm_count][2] = GPIO_PIN_0;
pwmtimers[pwm_count][3] = GPIO_PB0_T2CCP0;
break;
case TIMER3_BASE:
pwmtimers[pwm_count][2] = GPIO_PIN_2;
pwmtimers[pwm_count][3] = GPIO_PB2_T3CCP0;
break;
}
} else {
switch (pwm_timer) {
case TIMER0_BASE:
pwmtimers[pwm_count][2] = GPIO_PIN_7;
pwmtimers[pwm_count][3] = GPIO_PB7_T0CCP1;
break;
case TIMER1_BASE:
pwmtimers[pwm_count][2] = GPIO_PIN_5;
pwmtimers[pwm_count][3] = GPIO_PB5_T1CCP1;
break;
case TIMER2_BASE:
pwmtimers[pwm_count][2] = GPIO_PIN_1;
pwmtimers[pwm_count][3] = GPIO_PB1_T2CCP1;
break;
case TIMER3_BASE:
pwmtimers[pwm_count][2] = GPIO_PIN_3;
pwmtimers[pwm_count][3] = GPIO_PB3_T3CCP1;
break;
}
}
} else if (subtimer == TIMER_A) {
switch (pwm_timer) {
case TIMER0_BASE:
pwmtimers[pwm_count][2] = GPIO_PIN_0 | 0x100; // port F
pwmtimers[pwm_count][3] = GPIO_PF0_T0CCP0;
break;
case TIMER1_BASE:
pwmtimers[pwm_count][2] = GPIO_PIN_2 | 0x100;
pwmtimers[pwm_count][3] = GPIO_PF2_T1CCP0;
break;
case TIMER2_BASE:
pwmtimers[pwm_count][2] = GPIO_PIN_4 | 0x100;
pwmtimers[pwm_count][3] = GPIO_PF4_T2CCP0;
break;
case TIMER3_BASE:
pwmtimers[pwm_count][2] = GPIO_PIN_2;
pwmtimers[pwm_count][3] = GPIO_PB2_T3CCP0;
break;
}
} else { // timer B
switch (pwm_timer) {
case TIMER0_BASE:
pwmtimers[pwm_count][2] = GPIO_PIN_1 | 0x100;
pwmtimers[pwm_count][3] = GPIO_PF1_T0CCP1;
break;
case TIMER1_BASE:
pwmtimers[pwm_count][2] = GPIO_PIN_3 | 0x100;
pwmtimers[pwm_count][3] = GPIO_PF3_T1CCP1;
break;
case TIMER2_BASE:
pwmtimers[pwm_count][2] = GPIO_PIN_1;
pwmtimers[pwm_count][3] = GPIO_PB1_T2CCP1;
break;
case TIMER3_BASE:
pwmtimers[pwm_count][2] = GPIO_PIN_3;
pwmtimers[pwm_count][3] = GPIO_PB3_T3CCP1;
break;
}
}
return pwm_count++;
}

/**
 *
 */
void pwmStop(int pidx) {
TimerDisable(pwmtimers[pidx][0], pwmtimers[pidx][1]);       
 //enables timerx_y
}

/**
 *
 */
void pwmStart(int pidx) {
TimerEnable(pwmtimers[pidx][0], pwmtimers[pidx][1]);        
//enables timerx_y
}

/**
 * Frequency in HZ and Duty cycle in %
 * example : pwm_setFreqDC(0,25000,0.3)  sets 25KHz with 30% duty cycle
 */
void pwmSetFreqDC(int idx, int freq,double dutyCycle) {
int period = (SysCtlClockGet() / freq);
int pidx = idx;
TimerLoadSet(pwmtimers[pidx][0], 
                     pwmtimers[pidx][1], period - 1); // sets pwm period
TimerMatchSet(pwmtimers[pidx][0], 
                     pwmtimers[pidx][1], period *dutyCycle); // sets duty cycle
}



/**
 *
 */
void pwmConfigure(int idx) {
int timerp = 0;
// ENABLE gpio port
int gpioPort;
volatile int pidx = idx;

if ((pwmtimers[pidx][2] & 0x100) > 0) { // pidx changes value here ????

SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOF);
gpioPort = GPIO_PORTF_BASE;
} else {

SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOB);
gpioPort = GPIO_PORTB_BASE;
}

// configure gpio pin
GPIOPinTypeGPIOOutput(gpioPort, pwmtimers[pidx][2] & 0xff);
GPIOPadConfigSet(gpioPort, // identifies the GPIO PORT
pwmtimers[pidx][2] & 0xff,  // identifies the pin within the GPIO Port
GPIO_STRENGTH_8MA_SC, // sets the strength to 8mA with Slew Rate control
GPIO_PIN_TYPE_STD);   // sets the pad control type to push-pull

// clear output pin
GPIOPinWrite(gpioPort, pwmtimers[pidx][2] & 0xff, 0);

// setup timer
switch (pwmtimers[pidx][0]) {
case TIMER0_BASE:
timerp = SYSCTL_PERIPH_TIMER0;
break;
case TIMER1_BASE:
timerp = SYSCTL_PERIPH_TIMER1;
break;
case TIMER2_BASE:
timerp = SYSCTL_PERIPH_TIMER2;
break;
case TIMER3_BASE:
timerp = SYSCTL_PERIPH_TIMER3;
break;
}
SysCtlPeripheralEnable(timerp);  // enables timer x

GPIOPinConfigure(pwmtimers[pidx][3]);  // port x pin y set to TIMERz_k output
    GPIOPinTypeTimer(gpioPort, pwmtimers[pidx][2] & 0xff); // port x pin y set to timer type output, does not bind

// configure Timer
if (pwmtimers[pidx][1] == TIMER_A)
TimerConfigure(pwmtimers[pidx][0],
(TIMER_CFG_SPLIT_PAIR | TIMER_CFG_A_PWM));
else
TimerConfigure(pwmtimers[pidx][0],
(TIMER_CFG_SPLIT_PAIR | TIMER_CFG_B_PWM));
// 16 bit timer TIMERx_y set to PWM mode
TimerControlLevel(pwmtimers[pidx][0], pwmtimers[pidx][1], 1); 
        // invert the signal for TIMERx_y
}

---------------------

The addPWM(timer, subtimer, gpio)  function allows you to select which timer you want to use.
To see which timers are available, you should use the lm4f datahseet or the summary table I placed at the beginning of the code, it should be quite easy to read.
For each timer/subtimer there are up to two output options in regard to the gpio pin to be used.
I sorted them alphabetically, so if for a timerxy you have options being PB2 and PF4 then PB2 would be 0 and PF4 1 in the gpio parameter of the function.
If the timer has one single configurable output, then both 0 and 1 would produce the same result.

This function just fills in the pwmtimers array, does not do anything with the hardware.
Say you call addPWM 3 times, the first one would be index 0 in the array, the second one index 1 and so on.
There is no functionality to remove an element from that array (did not see the point).

Once all the basic parameters are set (by addPWM) it is time to configure the needed hardware.
Relevant timer peripheral must be enabled, associated gpio port should be enabled too, the selected pin should be configured as output, set in timer mode and eventually pad-configured with high current push-pull with slew rate control.
The timer should be configured in split mode / pwm, also the pwm signal should be inverted (see previous article for a detailed explanation of these steps).
It is not difficult, but honestly i' d rather not code this process each time I need to use a pwm, so the pwmConfigure(idx) function takes care of ALL of it by fetching the needed values from the pwmtimers array.
Not really elegant maybe, but quite effective. 

The idea here is that once you specified what a pwm signal should be, the next operation should just refer to the array index, like configure idx 0, set frequency and duty cycle for idx 0 , start idx 0 etc.

So, next in line comes the pwmSetFreqDC( idx,  freq, dutyCycle) which allows us to set the frequency (you need to pass the number of hertz, like 25000  if you want 25KHz, no range checking is done, it' s  up to you to pass a possible value) and the duty cycle as a fraction (i.e. 0.3 if you want 30%).

The you can start your signal with pwmStart(idx)

Let's see an example that drives the R,G and B components of the onboard LED.

---------------------

 /*
 * Stellaris LAUNCHPAD LM4F120H5QR
 * --->  PART_LM4F120H5QR  <---
 */

#include <inc/hw_memmap.h>
#include <inc/hw_types.h>
#include <driverlib/gpio.h>
#include <driverlib/sysctl.h>
#include <driverlib/uart.h>
#include <inc/hw_timer.h>
#include <driverlib/timer.h>
#include <driverlib/pin_map.h>
#include <utils/uartstdio.c>


void setClock()
{
 SysCtlClockSet(  SYSCTL_SYSDIV_4 | SYSCTL_USE_PLL 
             | SYSCTL_XTAL_16MHZ  | SYSCTL_OSC_MAIN);
}


int main(void) {
 setClock();

 // Add PWM timers to the library array

 addPWM(TIMER0_BASE,TIMER_B,1); //0
 addPWM(TIMER1_BASE,TIMER_A,1); //1
 addPWM(TIMER1_BASE,TIMER_B,1); //2

 // Configure Them
 pwmConfigure(0);
 pwmConfigure(1);
 pwmConfigure(2);

 // Set Frequency and duty cycle
 pwmSetFreqDC(0,25000,0.9);
 pwmSetFreqDC(1,25000,0.7);
 pwmSetFreqDC(2,25000,0.3);

 // Start the signal
 pwmStart(0);
 pwmStart(1);
 pwmStart(2);

 while (1) {
    // do nothing, forever... looks like my dream life :)
  }
}

--------------

When I was debugging it, I really stumbled against a stupid detail which I am going to tell you now, so you may avoid to think you are stupid and you should not deal with these things (because I did, at some point).

As you can see I am using all 3 components of the led, to debug my library I tried to use one , then two.
I tried to configure but not start them... but each time all 3 components were active.
I thought that the timers were starting as soon as they were configured, possibly with the frequency and DC I used before.
Why, if I did not start the green signal, the green component is there?
Turns out the answer is way easier than you' d think.
No, there is no green pwm signal, but the green light is on and the reason (my DSO confirmed it, I would have discovered this before if I was not too lazy to pull it out and probe the pins) is that the output is a steady HIGH.
Why?
I configured the pwm signals to be inverted, so when the timers are off, you should expect a steady high output, if this bothers you, then change it to not inverted in the TimerControlLevel call ( set to 0 the last parameter) and calculate the complementary value for the dutycycle in  pwmSetFreqDC simply using (1-dutyCycle).
I tried and it worked (my DSO confirmed it, I love that instrument!).

I hope this simple library can be useful for you, if you are using  another model of the lm4f, you might need to add some code to the addPWM function in order to consider the timers available in your device, the rest should probably be ok.

If you have improvements, I'd be glad to see them.
My next step will be to add UART or I2C control to drive the signals, again nothing fancy, but this is what I need :)

Happy coding!

No comments: