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.

Stellaris Launchpad - Interrupts 1

The Cortex M4F has a powerful interrupt handling mechanism.
Since these kind of devices are normally used in applications where they control events happening around them, they need to have a flexible and efficient interrupt handling mechanism.

If you ever coded ISR (Interrupt Service Routines) for pretty much any platform, you probably know that is generally a bad idea to have two interrupts serviced at the same time.
When an interrupt is being serviced, a second interrupt is generally queued and waits for the first ISR to be completed.
While in many applications this could be ok -i.e. you click with your mouse and press a key on your keyboard almost simultaneously, but it is usually ok for the keyboard event to wait until the mouse event is handled or vice-versa-
When you are dealing with external sensors that might require a very quick response, this behavior may not be desirable or even acceptable.
What would be cool is to be able to set priorities to those interrupts and if the keyboard is more important of the mouse then we expect the mouse ISR routine to be interrupted by the keyboard one and resume right after the latter is completed.

The cortex M4F allows just that using a Nested Vector Interrupt Controller (NVIC) that handles via hardware all the needed complexity (Pushing a popping the machine states as needed and optimizing such processes for Interrupt concurrency).

These concepts are well explained in the great video by TI


To be honest I am not adding much with this post as everything is well explained in the video itself, however, for my (and eventually yours) future reference, I find it easier to write a summary of the concepts.

Now that you know how cool the interrupt handling is in the M4F, let's see how to use it in a basic setup.

By default all interrupt vector are redirected to a default vector function called IntDefaultHandler.
If you enable a timer interrupt, a uart interrupt etc, the interrupt vector function found in the MCU vector table (by default it starts at addr 0x0, leave it there unless you have a good reason not too) is called.

The Stellaris has 65 interrupts, each one has a service function address stored in the int vector table.
This means that if you want to use your interrupt service routine, you need to store its address in the before mentioned table.

Configuring your startup_ccs.c file

To achieve this you need to properly alter the file startup_ccs.c (you have a copy of it in each CCS project) file in two points :

1) Search for this data structure :

 #pragma DATA_SECTION(g_pfnVectors, ".intvecs")
void (* const g_pfnVectors[])(void) =

You will find an array of function pointers, on the right side comments are added to indicate which ISR each value represents.
Carefully browse those comments and locate the element related to the interrupt you need to handle.
Most likely the associated value will be IntDefaultHandler, if it is not either you already mdified the file or you are trying to manage something you probably should not (a reserved or "core" interrupt).
Replace IntDefaultHandler with the name of your ISR function (i.e. MyCoolISRFunction ) .


2) Now you updated the int vector table  to consider your ISR, however in the startup_ccs.c file such function is normally not defined (you defined it in your main.c or whatever other source file you are using).
To fix that, scroll on top of startup_ccs.c and locate a section that contains external declarations, it should be around line 20 or so.
There add your ISR as

extern void MyCoolISRFunction;    

where MyCoolISRFunction is the same name you used to define the ISR in main.c and you copied into the int vector table structure.

Steps needed to service a peripheral using an ISR 

1) Enable your peripheral XXXX with

 SysCtlPeripheralEnable(XXXX);

2) configure your peripheral as needed by setting gpio pind directions, timer, uart, spi... parameters

3) Enable the interrupt associated to your peripheral
IntEnable(XXXX);

4) Enable your peripheral to generate interrupts
i.e. TimerIntEnable(TIMER0_BASE,TIMER_TIMA_TIMEOUT);

5) Generally enable interrupts on the M4F
IntMasterEnable();

6) Fire up your peripheral if needed
i.e. TimerEnable(TIMER0_BASE,TIMER_A);

7) define your ISR function

void MyCoolISRFunction(void)
{
 // clear the peripheral interrupt i.e. :
 TimerIntClear(TIMER0_BASE,TIMER_TIMA_TIMEOUT);
/*
 do whatever you need to do
*/
}

Note that all steps from 1 to 7 happened in your main.c (or other source module you may have added) file.

8) Finally you need to update your startup_ccs.c file as explained in the "configure your startup_ccs.c file" section