Search Microcontrollers

Tuesday, February 18, 2014

Stellaris Launchpad - PWM

One thing being a bit confusing is that the Cortex M4F used in the Launchpad (LM4F120H5QR) DOES NOT have a PWM module.

[Note : the pwm module is present in the Tiva C version of the launchpad]

Does it mean you cannot generate HW controlled PWM signals?
Nope, you can and you can have quite some good control over them, still having the MCU regulating them in the background without the need of forcing GPIO pins up & down.

Some more advanced functionality is available only in those parts containing the dedicated PWM peripherals, but what really matters is that, when using the launchpad, we are actually using timers to generate PWM signals.

This is key, as an example when we set the function mode of the GPIO pin it is going to be

GPIOPinTypeTimer(GPIO_PORTF_BASE, GPIO_PIN_2) 

and not

GPIOPinTypePWM(GPIO_PORTF_BASE, GPIO_PIN_2)


The M4F has several 32 bit timers, that can be split into 2 x16 bit timers each.
According to the documentation PWM functionality should be available both in 32 bit and in 2x16 bit mode, however, I never managed to have it working in 32 bit mode.

Does it matter?
It actually does because the number of bits of the timer limits the number of cpu cycles that define the PWM period.
16 bit = 65.536 and if the cpu Frequency is set to 50MHz , the minimum frequency you could achieve with 16 bits is just  a bit less than 1KHz

P = 65536 * 1 / 50.000.000 = 0.00131072 s
F = 1 / 0.00131072 = 762.939453125 Hz

(Note : I verified these values with my oscilloscope by setting the period counter to 0xffff)

There is actually a solution since timers can be pre-scaled (a 16 bit timer can be pre-scaled with an 8 bit value -> the minimum frequency goes down to 763  / 256 = 2.98Hz)

However the precision with which you can set the duty cycle is still at 16 bit, but still it should be enough for most applications.

In general a 32 bit timer can be split into TIMER_A and TIMER_B each of them being 16 bits.
If you are using the full 32 bits for the same timer, then it will be only TIMER_A.

In my first experiment I used the blue component of the on-board LED to test the PWM signal, this is connected to PORT_F , PIN2.
If you check the M4F datasheet , in table 11-2 you can discover that PF2 (Port F , Pin 2) can be connected to T1CCP0 whcih actually means Timer 1 Capture Compare Pin 0

(@TI - Lmf4f120h5qr datasheet)

The previous table (11-1) shows that such pin is related to TIMER1 - TIMER_A

 
(@TI - Lmf4f120h5qr datasheet)

What all this means is that since we want to use GPIO_PF2, then we need to use TIMER1_TIMER_A

Now that we know all this, we can start to enable our GPIO port :
SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOF);  
// enables LED GPIO Port

Set all 3 color components as outputs (we need this to turn the led completely off before starting)
GPIOPinTypeGPIOOutput(GPIO_PORTF_BASE,
GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3); 
// sets r,g,b as output

and turn the LED off

GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3, 0); 
// turns off the led

Since we are dealing with a LED, I configured the Pad control to output 8mA (you can set it at 2,4 and 8mA).
I am not sure this is really needed since setting the pin as timer output (which we will do later) might do that for us automatically... still, it doesn't  hurt.

GPIOPadConfigSet(GPIO_PORTF_BASE , // identifies the GPIO PORT
 GPIO_PIN_2, // 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

Time to enable our timer (note : enabling it does not mean we are also starting it).

SysCtlPeripheralEnable(SYSCTL_PERIPH_TIMER1);// enables timer 1 

Now we need to configure PF2 in its mux mode 7 which represents TIMER1_A output

GPIOPinConfigure(GPIO_PF2_T1CCP0);  
// port F pin 2 set to TIMER1_A output
Remember T1CCP0 we found before in the table?

Then we configure the output mode of the pin to be adequate for timer output (this is the part I am guessing overrides the Pad control configuration we did before)

GPIOPinTypeTimer(GPIO_PORTF_BASE, GPIO_PIN_2); 
// port F pin 2 set to timer type output, does not bind  it to any timer, the driverlib guide states :

This function cannot be used to turn any pin into a timer pin; it only configures a timer pin for proper operation. Devices with flexible pin muxing also require a GPIOPinConfigure() function call.

As stated before, I did not manage to have it working at 32 bits, the only configuration I was able to succesfully implement was with SPLIT_PAIR

TimerConfigure(TIMER1_BASE,        
   (TIMER_CFG_SPLIT_PAIR|TIMER_CFG_A_PWM)); 
// 16 bit timer TIMER1_A set to PWM mode

Ok, time to set the PWM parameters : Frequency (Period) and dutyCycle.

ulPeriod = SysCtlClockGet() / 10000; // 10KHz
dutyCycle = (unsigned long) (ulPeriod - 1) * 0.3 // 30% DC

Here, the interesting thing to notice is that the dutyCycle variable is in fact a value that the timer will compare with its counter, when it reaches it, it turns ON the signal and when it reaches the end of the period it resets the signal to zero.
What this means is that if you calculated your duty cycle variable as a percentage, say 30% of the total period, the signal will be 0 (OFF) from 0 to that value and 1 from that value up to the end of the period.
In fact setting the variable to 30% means you will have a 70% duty cycle, setting it at 80% means you will have a 20% DC.
You can work out the math easily to calculate the complementary value or you could simply invert the signal generated by the timer with this function call :

TimerControlLevel(TIMER1_BASE, TIMER_A, 1);  
// inverts the signal for TIMER1_A

And finally, we set period, duty cycle and fire up the timer

TimerLoadSet(TIMER1_BASE, TIMER_A, ulPeriod - 1); 
// sets pwm period
TimerMatchSet(TIMER1_BASE, TIMER_A, dutyCycle); 
// sets duty cycle
TimerEnable(TIMER1_BASE, TIMER_A);  
//starts timer1_a
while (1)
{
 // nothing to do here, pwm is active
}


You can see the full code here :
--------------------------------------------------------------/*
 * Stellaris LAUNCHPAD LM4F120H5QR
 * --->  PART_LM4F120H5QR  <---
 */
#define 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>

int main(void) {
unsigned long ulPeriod, dutyCycle;

SysCtlClockSet(
SYSCTL_SYSDIV_4 | SYSCTL_USE_PLL | SYSCTL_XTAL_16MHZ
| SYSCTL_OSC_MAIN);
SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOF);  // enables LED GPIO Port
GPIOPinTypeGPIOOutput(GPIO_PORTF_BASE,
GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3); // sets r,g,b as output

GPIOPadConfigSet(GPIO_PORTF_BASE , // identifies the GPIO PORT
GPIO_PIN_2,           // 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

GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3, 0); // turns off the led

SysCtlPeripheralEnable(SYSCTL_PERIPH_TIMER1);  // enables timer 1 which can be connected to Port F / Pin 2
GPIOPinConfigure(GPIO_PF2_T1CCP0);             // port F pin 2 set to TIMER1_A output
GPIOPinTypeTimer(GPIO_PORTF_BASE, GPIO_PIN_2); // port F pin 2 set to timer type output, does not bind
/* it to any timer, the driverlib guide states :
This function cannot be used to turn any pin into a timer pin; it only configures a timer pin for
proper operation. Devices with flexible pin muxing also require a GPIOPinConfigure() function
call.*/

ulPeriod = (SysCtlClockGet() / 25000); // set to 25KHz
dutyCycle = (unsigned long) (ulPeriod - 1) * 0.5;
TimerControlLevel(TIMER1_BASE, TIMER_A, 1);  // invert the signal for TIMER1_A
//
TimerConfigure(TIMER1_BASE, (TIMER_CFG_SPLIT_PAIR|TIMER_CFG_A_PWM)); // 16 bit timer TIMER1_A set to PWM mode
TimerLoadSet(TIMER1_BASE, TIMER_A, ulPeriod - 1); // sets pwm period
TimerMatchSet(TIMER1_BASE, TIMER_A, dutyCycle); // sets duty cycle
TimerEnable(TIMER1_BASE, TIMER_A);              //enables timer1_a
while (1) {

}
}

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

 PF2 is also exported to the J4 connector of the Launchpad, so we can probe the signal with an oscilloscope.


This is how the signal looks like on my DSO when setting  F = 25KHz and DutyCycle = 50%


I did not cover the pre-scaling part, maybe I will add that in the future.

No comments: