Hello!
I would like to suggest a little program illustrating what can be done with a timer configured as
PWM output. Note that I don't have an AT89C51 environment, so I'm not sure that the port can be
as easy as this one. It's made with MSP430 Launchpad (that you can buy for 4.30 USD and use
with a free version of Code Composer Studio compiler).
1. Using a a timer.
A timer is basically a counter with comparison functionality. That's the only thing it can do but it
does it very well. It has usually myriads of configurations, counting up, counting down, counting
up to a certain value and then counting down to another value, etc. This time, I will use a count up
method, and I will use 2 values. One sets the timer upper limit, and the other one sets up the
comparison. And I set the comparison result to output.
Since the purpose here is to set a duty ratio, I will set up the upper limit at 100, and the comparison
limit at 25, which will correspond to the duty ratio in %. The source code looks like this:
(I just modified TI's source code, nothing fancy here)
Code C - [expand] |
1
2
3
4
5
6
7
8
9
10
11
12
| #include "MSP430G2452.h"
void main(void) {
WDTCTL = WDTPW + WDTHOLD; // Stop WDT
P1DIR |= 0x04; // P1.2 output
P1SEL |= 0x04; // PWM output selected
CCR0 = 100; // PWM Period/2
CCR1 = 25; // CCR1 PWM duty cycle
CCTL1 = OUTMOD_6; // CCR1 toggle/set
TACTL = TASSEL_2 + MC_1; // SMCLK, up mode
_BIS_SR(LPM0_bits); // Enter LPM0
} |
What you can notice is that there is no while loop. This is because the timer can run by itself,
and once configured it does not need the CPU anymore. Put the CPU to sleep and that's it.
2. Some cosmetics
Now it works, but if you read this code after 6 months, you may not understand anymore what
it does. Let's do some cosmetics for a more readable version:
Code C - [expand] |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| #include "MSP430G2452.h"
#define PWM_PERIOD 100
#define DEFAULT_RATIO 25
typedef unsigned char uint8;
// Function prototypes
void setup_pwm(void);
void set_duty(uint8 duty);
// Global variables
uint8 duty;
//------------------------------------------------------------------------------
// Main program. Does basically nothing except setting the start condition
void main(void) {
WDTCTL = WDTPW + WDTHOLD; // Stop WDT
duty = DEFAULT_RATIO;
setup_pwm();
set_duty(DEFAULT_RATIO);
_BIS_SR(LPM0_bits + GIE); // Enter low power mode
}
//------------------------------------------------------------------------------
// Functions implementation
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// Configures timer A for direct pwm output to port 1.2. The reference frequency
// is SMCLK (about 1 MHz);
void setup_pwm(void) {
P1DIR |= 0x04; // P1.2 output
P1SEL |= 0x04; // PWM output selected
CCTL1 = OUTMOD_6; // CCR1 toggle/set
TACTL = TASSEL_2 + MC_1; // SMCLK, up mode
CCR0 = 100; // PWM Period
}
//------------------------------------------------------------------------------
// As the total period has already been set in timer configuration, this
// function only sets the "up" part of the signal.
void set_duty(uint8 duty) {
CCR1 = duty; // CCR1 PWM duty cycle
} |
This time, there are a few more definitions and if you read this program later or give it to
someone, the main() is short and therefore easy to understand with self-explanatory function
names.
3. Using a button
Next step. We need a button input. The launchpad board has only 1 usable button (the other
is reset) on P1.3. We will therefore add one function to setup the button interrupts:
Code C - [expand] |
1
2
3
4
5
6
7
8
| #define BUTTON 0x08
void setup_button(void) {
// P1DIR &= ~BUTTON; // Make sure that port 1 is in input
P1IE |= BUTTON; // Port 1 interrupt enable at bit 3
P1IES |= BUTTON; // Trigger on falling edge
P1IFG &= ~BUTTON; // Reset port vector in case it was high.
} |
Port1 bit 2 is used for PWM output, bit 3 will be used for the button input (no choice here).
In the above code, P1IE (IE for interrupt enable) is set to the button value. P1IES sets
the edge. As pressing the button makes the level go to 0V, we have to setup the interrupt
edge to 1. And the last instruction is in the case that the interrupt flag is already high.
When detecting an interrupt, the following function is called:
Code C - [expand] |
1
2
3
4
5
6
7
8
9
10
| #pragma vector=PORT1_VECTOR
__interrupt void Port1ISR(void) {
_DINT();
duty += 25;
if(duty > 100) duty = 0;
set_duty(duty);
P1IFG &= ~BUTTON; // Reset interrupt flag
_EINT();
} |
In the "specs" of this program, the original poster wanted to have 4 setup levels,
and that's why I add 25 to the duty value. The values sequence will therefore be:
25 -> 50 -> 75 -> 100 -> 0 -> 25, etc...
Note that the adjustment can be arbitrarily small without code modification.
And if you need even finer steps, you can set PWM_PERIOD to 1000 and add
1 to duty everytime... But careful: you will have to press 1000 times to come
back to 0. And on top of that, if the timer total count is high, then the PWM
period will be long, and the motor might become "jumpy".
Note that I wrote _DINT() (disable interrupts) and _EINT(). This is not really
required here, but it's a good programming practice in order to avoid nested
interrupts.
Important: P1IFG (the interrupt flag) has to be reset after each interrupt. Button
interrupts are not automatically reset.
4. Program summary
Here is the full code:
Code C - [expand] |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
| #include "MSP430G2452.h"
#define PWM_PERIOD 100
#define DEFAULT_RATIO 25
#define BUTTON 0x08 // For P1.3
typedef unsigned char uint8;
// Function prototypes
void setup_pwm(void);
void setup_button(void);
void set_duty(uint8 duty);
// Global variables
uint8 duty;
//------------------------------------------------------------------------------
// Main program. Does basically nothing except setting the start condition
void main(void) {
WDTCTL = WDTPW + WDTHOLD; // Stop WDT
duty = DEFAULT_RATIO;
setup_pwm();
set_duty(DEFAULT_RATIO);
setup_button();
_BIS_SR(LPM0_bits + GIE); // Enter low power mode
}
//------------------------------------------------------------------------------
// Functions implementation
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// Configures timer A for direct pwm output to port 1.2. The reference frequency
// is SMCLK (about 1 MHz);
void setup_pwm(void) {
P1DIR |= 0x04; // P1.2 output
P1SEL |= 0x04; // PWM output selected
CCTL1 = OUTMOD_6; // CCR1 toggle/set
TACTL = TASSEL_2 + MC_1; // SMCLK, up mode
CCR0 = 100; // PWM Period
}
//------------------------------------------------------------------------------
// As the total period has already been set in timer configuration, this
// function only sets the "up" part of the signal.
void set_duty(uint8 duty) {
CCR1 = duty; // CCR1 PWM duty cycle
}
//------------------------------------------------------------------------------
// Setup launchpad's button (which is on port 1.3)
void setup_button(void) {
// P1DIR &= ~BUTTON; // Make sure that port 1 is in input
P1IE |= BUTTON; // Port 1 interrupt enable at bit 3
P1IES |= BUTTON; // Trigger on falling edge
P1IFG &= ~BUTTON; // Reset port vector in case it was high.
}
//------------------------------------------------------------------------------
// Port 1 interrupt service routine
#pragma vector=PORT1_VECTOR
__interrupt void Port1ISR(void) {
_DINT();
duty += 25;
if(duty > 100) duty = 0;
set_duty(duty);
P1IFG &= ~BUTTON; // Reset interrupt flag
_EINT();
} |
What can be noticed (same as previously) is that there is no while loop.
- When there is no interrupt, the timer runs by itself in PWM mode. The CPU sleeps.
- When there is an interrupt, the CPU wakes up, performs the interrupt tasks
as defined in the interrupt service routine and goes back to sleep.
Last step, here are a few scope captures of the PWM output.
5. Comments, conclusion
There is apparently a small problem in PWM100: when reaching the value 100,
the output spends one clock low. That's because I forgot to set the maximal count
at 100 -1. If I correct it to 99 and set the duty at 100, the output would be always
low. Anyway, it works and this bug is a really minor issue.
You can notice from the scope screen copies that the PWM frequency is around 10 kHz
(not a surprise, the default chip frequency is 1 MHz and I used a period of 100 clocks).
10 kHz should be more than enough to drive a motor accurately. For even more
accuracy, the chip frequency could be set at to 16 MHz, which would yield a PWM frequency
of 160 kHz. Or 1 frequency of 16 kHz with a PWM resolution of 1000 instead of 100.
So there are many ways of tuning this program for your needs.
A simple PWM program has been built, that uses minimal CPU resources.
Flash footprint:
The needed flash size is 480 bytes, but could certainly become smaller with some optimization.
Responsivity:
It's quite responsive. By toggling a port in a while loop, the PWM frequency would be at least
10 times lower. In the present case, it's not an issue, but responsivity could become an issue
in a servo loop.
That's it. Let's hope somebody can use that.
Dora.