The Pico In MicroPython: PWM
Written by Harry Fairhead & Mike James   
Monday, 26 July 2021
Article Index
The Pico In MicroPython: PWM
Changing The Duty Cycle
Duty Cycle Resolution

Although you don’t need to know anything about the PWM hardware, there is one limitation you need to be aware of. Each PWM generator supports two outputs which work at the same frequency and optionally different duty cycles. For example, if you look back to the table of PWM pin assignments you can see that PWM 0 has outputs on GP16 and GP17. This means that these two pins can only work at the same frequency. For example:

from machine import Pin, PWM
pwm16 = PWM(Pin(16))
pwm17 = PWM(Pin(17))
pwm16.freq(250)
pwm16.duty_u16(65535//2)
pwm17.duty_u16(65535//4)

produces a PWM signal on GP16 and GP17 with the same frequency of 250Hz and duty cycles of 50% and 25% respectively. If you change the frequency on either pin, both change frequency.

You can see the result in this logic analyzer display:

pwm2

You can see from the logic analyzer trace that the pulses on each line start their duty cycle at exactly the same time. The PWM hardware can create phase-correct pulses where the pulses are aligned about their center point, but MicroPython doesn’t support this mode. However, it is fairly easy to write a function that sets or unsets phase-correct mode:

def pwm_set_phase(sliceNo,phase):
  Addr = 0x40050000 +0x14*sliceNo
  if phase:
     machine.mem32[Addr]=machine.mem32[Addr] | 0x2
  else:
     machine.mem32[Addr]=machine.mem32[Addr] & 0xFFFFFFFD 

Using this we can generate phase-correct pulses:

pwm16 = PWM(Pin(16))
pwm17 = PWM(Pin(17))
pwm16.freq(250)
pwm_set_phase(0,True)
pwm16.duty_u16(65535//2)
pwm17.duty_u16(65535//4)

Now you can see that the pulses don’t start at the same time, but they are centered around the same time:

pwm3

As well as being able to set the level for each channel, you can also set the polarity. However, it is easy to write a function that will invert any channel:

def pwm_set_polarity(sliceNo,channel,invert):
    Addr = 0x40050000 +0x14*sliceNo
    if invert:
       machine.mem32[Addr]=machine.mem32[Addr] | 
0x1 << (2+channel) else: machine.mem32[Addr]=machine.mem32[Addr] &
~(0x1<<(2+channel))

For example:

pwm16 = PWM(Pin(16))
pwm17 = PWM(Pin(17))
pwm16.freq(250)
pwm_set_polarity(0,1,True)
pwm16.duty_u16(65535//4)
pwm17.duty_u16(65535//4)
  
pwm4

and you can see that the output of channel B is inverted.

Changing The Duty Cycle

For reasons that will be discussed later, in most cases the whole point is to vary the duty cycle or the period of the pulse train. This means that the next question is, how fast can you change the characteristics of a PWM line? In other words, how fast can you change the duty cycle? There is no easy way to give an exact answer and, in most applications, an exact answer isn't of much value. The reason is that for a PWM signal to convey information it generally has to deliver a number of complete cycles with a given duty cycle. This is because of the way pulses are often averaged in applications.

We also have another problem – synchronization. This is more subtle than it first seems. The hardware won't change the duty cycle until the current pulse is complete. You might think that the following program works to switch between two duty cycles on a per pulse basis:

pwm16 = PWM(Pin(16))
pwm16.freq(50)
pwm16.duty_u16(65535//2)
while True:
    pwm16.duty_u16(65535//2)
    pwm16.duty_u16(65535//4)

but if you try this out the result isn’t what you might expect on a first analysis:

pwm5

You don’t get one 25% followed by one 50% pulse, but a varying number of each in turn. The reason is, of course, that the duty cycle is being set asynchronously.

picoPython360

 



Last Updated ( Monday, 26 July 2021 )