Skip to content

Commit bf0b05e

Browse files
soypatdeadprogram
authored andcommitted
machine/rp2040: refactor PWM code. fix Period calculation
1 parent 348a02d commit bf0b05e

File tree

2 files changed

+83
-63
lines changed

2 files changed

+83
-63
lines changed

src/machine/machine_rp2040_clocks.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ func CPUFrequency() uint32 {
1818
return 125 * MHz
1919
}
2020

21+
// Returns the period of a clock cycle for the raspberry pi pico in nanoseconds.
22+
// Used in PWM API.
23+
func cpuPeriod() uint32 {
24+
return 1e9 / CPUFrequency()
25+
}
26+
2127
// clockIndex identifies a hardware clock
2228
type clockIndex uint8
2329

src/machine/machine_rp2040_pwm.go

Lines changed: 77 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1+
//go:build rp2040
12
// +build rp2040
23

34
package machine
45

56
import (
67
"device/rp"
78
"errors"
9+
"math"
810
"runtime/volatile"
911
"unsafe"
1012
)
1113

1214
var (
13-
ErrPeriodTooBig = errors.New("period outside valid range 1..4e9ns")
15+
ErrBadPeriod = errors.New("period outside valid range 8ns..268ms")
1416
)
1517

1618
const (
@@ -50,8 +52,21 @@ func getPWMGroup(index uintptr) *pwmGroup {
5052
return (*pwmGroup)(unsafe.Pointer(uintptr(unsafe.Pointer(rp.PWM)) + 0x14*index))
5153
}
5254

55+
// Hardware Pulse Width Modulation (PWM) API
5356
// PWM peripherals available on RP2040. Each peripheral has 2 pins available for
5457
// a total of 16 available PWM outputs. Some pins may not be available on some boards.
58+
//
59+
// The RP2040 PWM block has 8 identical slices. Each slice can drive two PWM output signals, or
60+
// measure the frequency or duty cycle of an input signal. This gives a total of up to 16 controllable
61+
// PWM outputs. All 30 GPIOs can be driven by the PWM block
62+
//
63+
// The PWM hardware functions by continuously comparing the input value to a free-running counter. This produces a
64+
// toggling output where the amount of time spent at the high output level is proportional to the input value. The fraction of
65+
// time spent at the high signal level is known as the duty cycle of the signal.
66+
//
67+
// The default behaviour of a PWM slice is to count upward until the wrap value (\ref pwm_config_set_wrap) is reached, and then
68+
// immediately wrap to 0. PWM slices also offer a phase-correct mode, where the counter starts to count downward after
69+
// reaching TOP, until it reaches 0 again.
5570
var (
5671
PWM0 = getPWMGroup(0)
5772
PWM1 = getPWMGroup(1)
@@ -94,30 +109,23 @@ func (pwm *pwmGroup) peripheral() uint8 {
94109
return uint8((uintptr(unsafe.Pointer(pwm)) - uintptr(unsafe.Pointer(rp.PWM))) / 0x14)
95110
}
96111

97-
// SetPeriod updates the period of this PWM peripheral.
112+
// SetPeriod updates the period of this PWM peripheral in nanoseconds.
98113
// To set a particular frequency, use the following formula:
99114
//
100115
// period = 1e9 / frequency
101116
//
102-
// If you use a period of 0, a period that works well for LEDs will be picked.
103-
//
104-
// SetPeriod will not change the prescaler, but also won't change the current
105-
// value in any of the channels. This means that you may need to update the
106-
// value for the particular channel.
117+
// Where frequency is in hertz. If you use a period of 0, a period
118+
// that works well for LEDs will be picked.
107119
//
108-
// Note that you cannot pick any arbitrary period after the PWM peripheral has
109-
// been configured. If you want to switch between frequencies, pick the lowest
110-
// frequency (longest period) once when calling Configure and adjust the
111-
// frequency here as needed.
120+
// SetPeriod will try not to modify TOP if possible to reach the target period.
121+
// If the period is unattainable with current TOP SetPeriod will modify TOP
122+
// by the bare minimum to reach the target period. It will also enable phase
123+
// correct to reach periods above 130ms.
112124
func (p *pwmGroup) SetPeriod(period uint64) error {
113-
if period > 0xffff_ffff {
114-
return ErrPeriodTooBig
115-
}
116125
if period == 0 {
117126
period = 1e5
118127
}
119-
p.setPeriod(period)
120-
return nil
128+
return p.setPeriod(period)
121129
}
122130

123131
// Top returns the current counter top, for use in duty cycle calculation.
@@ -135,14 +143,14 @@ func (p *pwmGroup) Counter() uint32 {
135143
return (p.CTR.Get() & rp.PWM_CH0_CTR_CH0_CTR_Msk) >> rp.PWM_CH0_CTR_CH0_CTR_Pos
136144
}
137145

138-
// Period returns the used PWM period in nanoseconds. It might deviate slightly
139-
// from the configured period due to rounding.
146+
// Period returns the used PWM period in nanoseconds.
140147
func (p *pwmGroup) Period() uint64 {
141-
periodPerCycle := getPeriod()
148+
periodPerCycle := cpuPeriod()
142149
top := p.getWrap()
143150
phc := p.getPhaseCorrect()
144151
Int, frac := p.getClockDiv()
145-
return uint64((Int + frac/16) * (top + 1) * (phc + 1) * periodPerCycle) // cycles = (TOP+1) * (CSRPHCorrect + 1) * (DIV_INT + DIV_FRAC/16)
152+
// Line below can overflow if operations done without care.
153+
return (16*uint64(Int) + uint64(frac)) * uint64((top+1)*(phc+1)*periodPerCycle) / 16 // cycles = (TOP+1) * (CSRPHCorrect + 1) * (DIV_INT + DIV_FRAC/16)
146154
}
147155

148156
// SetInverting sets whether to invert the output of this channel.
@@ -180,6 +188,12 @@ func (p *pwmGroup) SetTop(top uint32) {
180188
p.setWrap(uint16(top))
181189
}
182190

191+
// SetCounter sets counter control register. Max value is 16bit (0xffff).
192+
// Useful for synchronising two different PWM peripherals.
193+
func (p *pwmGroup) SetCounter(ctr uint32) {
194+
p.CTR.Set(ctr)
195+
}
196+
183197
// Enable enables or disables PWM peripheral channels.
184198
func (p *pwmGroup) Enable(enable bool) {
185199
p.enable(enable)
@@ -190,29 +204,6 @@ func (p *pwmGroup) IsEnabled() (enabled bool) {
190204
return (p.CSR.Get()&rp.PWM_CH0_CSR_EN_Msk)>>rp.PWM_CH0_CSR_EN_Pos != 0
191205
}
192206

193-
// Hardware Pulse Width Modulation (PWM) API
194-
//
195-
// The RP2040 PWM block has 8 identical slices. Each slice can drive two PWM output signals, or
196-
// measure the frequency or duty cycle of an input signal. This gives a total of up to 16 controllable
197-
// PWM outputs. All 30 GPIOs can be driven by the PWM block
198-
//
199-
// The PWM hardware functions by continuously comparing the input value to a free-running counter. This produces a
200-
// toggling output where the amount of time spent at the high output level is proportional to the input value. The fraction of
201-
// time spent at the high signal level is known as the duty cycle of the signal.
202-
//
203-
// The default behaviour of a PWM slice is to count upward until the wrap value (\ref pwm_config_set_wrap) is reached, and then
204-
// immediately wrap to 0. PWM slices also offer a phase-correct mode, where the counter starts to count downward after
205-
// reaching TOP, until it reaches 0 again.
206-
type pwms struct {
207-
slice pwmGroup
208-
hw *rp.PWM_Type
209-
}
210-
211-
// Handle to all pwm peripheral registers.
212-
var _PWM = pwms{
213-
hw: rp.PWM,
214-
}
215-
216207
// Initialise a PWM with settings from a configuration object.
217208
// If start is true then PWM starts on initialization.
218209
func (pwm *pwmGroup) init(config PWMConfig, start bool) error {
@@ -253,24 +244,53 @@ func (pwm *pwmGroup) setDivMode(mode uint32) {
253244
pwm.CSR.ReplaceBits(mode<<rp.PWM_CH0_CSR_DIVMODE_Pos, rp.PWM_CH0_CSR_DIVMODE_Msk, 0)
254245
}
255246

256-
// setPeriod sets the pwm peripheral period (frequency). Calculates DIV_INT and sets it from following equation:
247+
// setPeriod sets the pwm peripheral period (frequency). Calculates DIV_INT,DIV_FRAC and sets it from following equation:
257248
// cycles = (TOP+1) * (CSRPHCorrect + 1) * (DIV_INT + DIV_FRAC/16)
258249
// where cycles is amount of clock cycles per PWM period.
259-
func (pwm *pwmGroup) setPeriod(period uint64) {
260-
targetPeriod := uint32(period)
261-
periodPerCycle := getPeriod()
262-
top := pwm.getWrap()
263-
phc := pwm.getPhaseCorrect()
264-
_, frac := pwm.getClockDiv()
250+
func (pwm *pwmGroup) setPeriod(period uint64) error {
251+
// This period calculation algorithm consists of
252+
// 1. Calculating best-fit prescale at a slightly lower-than-max TOP value
253+
// 2. Calculate TOP value to reach target period given the calculated prescale
254+
// 3. Apply calculated Prescale from step 1 and calculated Top from step 2
255+
const (
256+
maxTop = math.MaxUint16
257+
// start algorithm at 95% Top. This allows us to undershoot period with prescale.
258+
topStart = 95 * maxTop / 100
259+
milliseconds = 1_000_000_000
260+
// Maximum Period is 268369920ns on rp2040, given by (16*255+15)*8*(1+0xffff)*(1+1)/16
261+
// With no phase shift max period is half of this value.
262+
maxPeriod = 268 * milliseconds
263+
)
264+
265+
if period > maxPeriod || period < 8 {
266+
return ErrBadPeriod
267+
}
268+
if period > maxPeriod/2 {
269+
pwm.setPhaseCorrect(true) // Must enable Phase correct to reach large periods.
270+
}
271+
265272
// clearing above expression:
266-
// DIV_INT = cycles / ( (TOP+1) * (CSRPHCorrect+1) ) - DIV_FRAC/16
273+
// DIV_INT + DIV_FRAC/16 = cycles / ( (TOP+1) * (CSRPHCorrect+1) ) // DIV_FRAC/16 is always 0 in this equation
267274
// where cycles must be converted to time:
268275
// target_period = cycles * period_per_cycle ==> cycles = target_period/period_per_cycle
269-
Int := targetPeriod/((1+phc)*periodPerCycle*(1+top)) - frac/16
270-
if Int > 0xff {
271-
Int = 0xff
276+
periodPerCycle := uint64(cpuPeriod())
277+
phc := uint64(pwm.getPhaseCorrect())
278+
rhs := 16 * period / ((1 + phc) * periodPerCycle * (1 + topStart)) // right-hand-side of equation, scaled so frac is not divided
279+
whole := rhs / 16
280+
frac := rhs % 16
281+
if whole > 0xff {
282+
whole = 0xff
272283
}
273-
pwm.setClockDiv(uint8(Int), 0)
284+
285+
// Step 2 is acquiring a better top value. Clearing the equation:
286+
// TOP = cycles / ( (DIVINT+DIVFRAC/16) * (CSRPHCorrect+1) ) - 1
287+
top := 16*period/((16*whole+frac)*periodPerCycle*(1+phc)) - 1
288+
if top > maxTop {
289+
top = maxTop
290+
}
291+
pwm.SetTop(uint32(top))
292+
pwm.setClockDiv(uint8(whole), uint8(frac))
293+
return nil
274294
}
275295

276296
// Int is integer value to reduce counting rate by. Must be greater than or equal to 1. DIV_INT is bits 4:11 (8 bits).
@@ -360,9 +380,9 @@ func (pwm *pwmGroup) getPhaseCorrect() (phCorrect uint32) {
360380
return (pwm.CSR.Get() & rp.PWM_CH0_CSR_PH_CORRECT_Msk) >> rp.PWM_CH0_CSR_PH_CORRECT_Pos
361381
}
362382

363-
func (pwm *pwmGroup) getClockDiv() (Int, frac uint32) {
383+
func (pwm *pwmGroup) getClockDiv() (Int, frac uint8) {
364384
div := pwm.DIV.Get()
365-
return (div & rp.PWM_CH0_DIV_INT_Msk) >> rp.PWM_CH0_DIV_INT_Pos, (div & rp.PWM_CH0_DIV_FRAC_Msk) >> rp.PWM_CH0_DIV_FRAC_Pos
385+
return uint8((div & rp.PWM_CH0_DIV_INT_Msk) >> rp.PWM_CH0_DIV_INT_Pos), uint8((div & rp.PWM_CH0_DIV_FRAC_Msk) >> rp.PWM_CH0_DIV_FRAC_Pos)
366386
}
367387

368388
// pwmGPIOToSlice Determine the PWM channel that is attached to the specified GPIO.
@@ -376,9 +396,3 @@ func pwmGPIOToSlice(gpio Pin) (slicenum uint8) {
376396
func pwmGPIOToChannel(gpio Pin) (channel uint8) {
377397
return uint8(gpio) & 1
378398
}
379-
380-
// Returns the period of a clock cycle for the raspberry pi pico in nanoseconds.
381-
func getPeriod() uint32 {
382-
const periodIn uint32 = 1e9 / (125 * MHz)
383-
return periodIn
384-
}

0 commit comments

Comments
 (0)