-
Notifications
You must be signed in to change notification settings - Fork 4
PWM
One of the most useful pieces of hardware in the Raspberry Pi is the Pulse With Modulator (PWM). With a Clock as an input, and a GPIO as an output, the PWM generates repeating square waves of defined durations and pulse widths:
These have a wide variety of uses. One of the most fundamental is dimming LEDs and controlling other voltage-sensitive components, such as varying the speed of a motor. By using a PWM, and varying the duty cycle, we can vary the brightness of LEDs linearly. Since at a 50% duty cycle, the LED is only receiving half of the energy, it will only be half as bright; or the motor half the speed.
Another is providing basic analog signals, for example servo motors use the duty cycle of a digital signal to determine the position for the arm to be in.
Finally the PWM can be used to convey arbitrary digital logic signals consisting of periods of +3.3V and periods of 0V.
The documentation for the PWM can be found in p138–147 of the datasheet, except for the critically important register offset which we have to obtain from the errata.
let peripheralBlockSize = 0x1000
let pwmRegistersOffset = 0x20c000
guard let pwmRegisters = mmap(nil, peripheralBlockSize, PROT_READ | PROT_WRITE, MAP_SHARED, memFd, off_t(peripheralAddressBase + pwmRegistersOffset)),
pwmRegisters != MAP_FAILED else { fatalError("Couldn't mmap PWM registers") }
let pwmControlOffset = 0x00
let pwmRange1Offset = 0x10
let pwmData1Offset = 0x14
let pwmControl = pwmRegisters.advanced(by: pwmControlOffset).assumingMemoryBound(to: Int.self)
let pwmRange1 = pwmRegisters.advanced(by: pwmRange1Offset).assumingMemoryBound(to: Int.self)
let pwmData1 = pwmRegisters.advanced(by: pwmData1Offset).assumingMemoryBound(to: Int.self)
The Raspberry Pi provides two PWM channels, however they don't act entirely independently. For most of this document we'll use PWM Channel 1, but will briefly explore using Channel 2 as well later.
The PWM hardware doesn't function on its own, it requires a clock as an input to act as a timing source. A dedicated PWM clock exists and sets the frequency of both PWM channels.
Configuring the output frequency of a clock is a topic in its own right and as such given its own page, which includes the addresses of the registers for the PWM's clock source. For the PWM, what matters is how that translates to the pulse width and duration of the output.
The PWM is fundamentally a digital piece of hardware, so operates on a sequence of bits. A bit of one means that the output will be a +3.3V high, while a bit of zero means that the output will be a 0V low. The above output is thus the following repeated bit pattern:
1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 |
The frequency of the input clock defines the bit rate, the length in time of one bit, whether it be a zero or a one. The bit pattern defines the resulting pulse-width, duration, and thus duty cycle.
In the example above, given an input clock frequency of 1 Mhz, the pulse-width would be 4µs and the duration 8µs, with a 50% duty cycle.
In addition to a clock acting as a timing source input, the PWM needs a GPIO to act as an output. This requires configuring the GPIO to an alternate function corresponding to the desired PWM channel:
GPIO | PWM Channel 1 | PWM Channel 2 |
---|---|---|
Alt 0 | GPIO12 | GPIO13 |
Alt 5 | GPIO18 | GPIO19 |
Note that the documentation is rather inconsistent as to whether it refers to the first PWM channel as PWM Channel 1 or PWM0, and more confusingly, the second as PWM Channel 2 or PWM1. For clarity I'm only using the channel numbers here.
With the input clock frequency configured, and the output GPIO set to the correct alternate function, we can configure the PWM itself.
The "default" mode of the PWM is the one that we get by enabling the PWM Channel with all of the other option bits set to zero. This mode is the most suitable for dimming LEDs, or adjusting motor speeds, because it outputs pulse-widths of as little as a single bit in length at a high frequency.
As with all of the modes, the output signal is controlled by the PWM Channel 1 Range and PWM Channel 1 Data registers. The two configure the duty cycle desired, ie. the ratio of time that the output is high vs. low.
We place the duration into the Range register, and the pulse-width into the Data register, and then we enable the PWM by setting the Channel 1 Enable bit and leaving all others as zero:
pwmRange1.pointee = 100
pwmData1.pointee = 25
pwmControl.pointee = (1 << 0)
In this mode, the units of value for the Range and Data registers are largely irrelavant; they serve simply to set the duty cycle—in this case, 25%. The actual pulse-width and duration times will be as short as possible.
An alternative mode for the PWM is the mark-space (M/S) mode. In this mode, the Range and Data registers still configure the duty cycle of the output, but now they directly configure the duration and pulse-width respectively, in units of bits.
Now the output is high for as many bits as specified in the Data register, and then low for as many bits remaining for the Range register.
When we configure this, in addition to enabling the channel, we also set the Channel 1 Mark-space Mode bit.
pwmRange1.pointee = 16
pwmData1.pointee = 4
pwmControl.pointee = (1 << 0) | (1 << 7)
Since this mode gives us direct control over the length in time of the pulse-width and duration, it's very suitable for controlling devices such as servo motors which require specific timings.
The final alternative mode for the PWM is the serializer mode, and in this mode, the Range and Data registers are interpreted quite differently.
The Range register still, as with the Mark-space mode, specifies the duration of the output signal in bits, however now the Data register directly specifies the bit pattern which the PWM will output.
The bits of the Data register are interpreted from most-significant to least-significant, when a bit is one, the output will be a +3.3V high; and conversely when a bit is zero, the output will be a 0V low. As before, the length in time of a bit is driven by the input clock.
Since the Data register is 32-bits in size, the values of the Range register are important. When the Range register is exactly 32, all 32-bits of the Data register will be used. But when the Range register is less than 32, the duration is reduced as you would expect, and the Data register will be truncated with only the most-significant bits used.
If the Range register is greater than 32, the full Data register will be used, and since the duration is extended as you would expect, the remaining time is filled with the output as a 0V low (unless the Silence Bit bit is set, in which case a +3.3 high instead).
pwmRange1.pointee = 16
pwmData1.pointee = 0b1101111000111000 << 16
pwmControl.pointee = (1 << 0) | (1 << 1)
Note that we have to shift the 16-bits of data in our example to the left so that it occupies the 16 most-significant bits of the Data register.
This mode gives us the most flexibility over the output signal, and is ideal for custom logic signals; though obviously constrained to those of 32-bits or less in length.