diff options
author | Ayke van Laethem <[email protected]> | 2020-05-19 22:30:46 +0200 |
---|---|---|
committer | Ron Evans <[email protected]> | 2021-04-06 20:36:10 +0200 |
commit | 72acda22b0a8d137405e41e9ed54cbfbcce7b26f (patch) | |
tree | baed2cc5aa5bd3f4582a6ef4aa439928acdf38f9 /src/machine/machine_nrf528xx.go | |
parent | f880950c3efbd005d2e171a5fd1c879a3221367c (diff) | |
download | tinygo-72acda22b0a8d137405e41e9ed54cbfbcce7b26f.tar.gz tinygo-72acda22b0a8d137405e41e9ed54cbfbcce7b26f.zip |
machine: refactor PWM support
This commit refactors PWM support in the machine package to be more
flexible. The new API can be used to produce tones at a specific
frequency and control servos in a portable way, by abstracting over
counter widths and prescalers.
Diffstat (limited to 'src/machine/machine_nrf528xx.go')
-rw-r--r-- | src/machine/machine_nrf528xx.go | 227 |
1 files changed, 196 insertions, 31 deletions
diff --git a/src/machine/machine_nrf528xx.go b/src/machine/machine_nrf528xx.go index 72f248c19..6c22463e2 100644 --- a/src/machine/machine_nrf528xx.go +++ b/src/machine/machine_nrf528xx.go @@ -4,6 +4,7 @@ package machine import ( "device/nrf" + "runtime/volatile" "unsafe" ) @@ -256,41 +257,205 @@ func (spi SPI) Tx(w, r []byte) error { return nil } -// InitPWM initializes the registers needed for PWM. -func InitPWM() { - return +// PWM is one PWM peripheral, which consists of a counter and multiple output +// channels (that can be connected to actual pins). You can set the frequency +// using SetPeriod, but only for all the channels in this PWM peripheral at +// once. +type PWM struct { + PWM *nrf.PWM_Type + + channelValues [4]volatile.Register16 +} + +// Configure enables and configures this PWM. +// On the nRF52 series, the maximum period is around 0.26s. +func (pwm *PWM) Configure(config PWMConfig) error { + // Enable the peripheral. + pwm.PWM.ENABLE.Set(nrf.PWM_ENABLE_ENABLE_Enabled << nrf.PWM_ENABLE_ENABLE_Pos) + + // Use up counting only. TODO: allow configuring as up-and-down. + pwm.PWM.MODE.Set(nrf.PWM_MODE_UPDOWN_Up << nrf.PWM_MODE_UPDOWN_Pos) + + // Indicate there are four channels that each have a different value. + pwm.PWM.DECODER.Set(nrf.PWM_DECODER_LOAD_Individual<<nrf.PWM_DECODER_LOAD_Pos | nrf.PWM_DECODER_MODE_RefreshCount<<nrf.PWM_DECODER_MODE_Pos) + + err := pwm.setPeriod(config.Period, true) + if err != nil { + return err + } + + // Set the EasyDMA buffer, which has 4 values (one for each channel). + pwm.PWM.SEQ[0].PTR.Set(uint32(uintptr(unsafe.Pointer(&pwm.channelValues[0])))) + pwm.PWM.SEQ[0].CNT.Set(4) + + // SEQ[0] is not yet started, it will be started on the first + // PWMChannel.Set() call. + + return nil +} + +// SetPeriod updates the period of this PWM peripheral. +// To set a particular frequency, use the following formula: +// +// period = 1e9 / frequency +// +// If you use a period of 0, a period that works well for LEDs will be picked. +// +// SetPeriod will not change the prescaler, but also won't change the current +// value in any of the channels. This means that you may need to update the +// value for the particular channel. +// +// Note that you cannot pick any arbitrary period after the PWM peripheral has +// been configured. If you want to switch between frequencies, pick the lowest +// frequency (longest period) once when calling Configure and adjust the +// frequency here as needed. +func (pwm *PWM) SetPeriod(period uint64) error { + return pwm.setPeriod(period, false) +} + +func (pwm *PWM) setPeriod(period uint64, updatePrescaler bool) error { + const maxTop = 0x7fff // 15 bits counter + + // The top value is the number of PWM ticks a PWM period takes. It is + // initially picked assuming an unlimited COUNTERTOP and no PWM prescaler. + var top uint64 + if period == 0 { + // The period is 0, which means "pick something reasonable for LEDs". + top = maxTop + } else { + // The formula below calculates the following formula, optimized: + // period * (16e6 / 1e9) + // The max frequency (16e6 or 16MHz) is set by the hardware. + top = period * 2 / 125 + } + + // The ideal PWM period may be larger than would fit in the PWM counter, + // which is only 15 bits (see maxTop). Therefore, try to make the PWM clock + // speed lower with a prescaler to make the top value fit the COUNTERTOP. + if updatePrescaler { + // This function was called during Configure(). + switch { + case top <= maxTop: + pwm.PWM.PRESCALER.Set(nrf.PWM_PRESCALER_PRESCALER_DIV_1) + case top/2 <= maxTop: + pwm.PWM.PRESCALER.Set(nrf.PWM_PRESCALER_PRESCALER_DIV_2) + top /= 2 + case top/4 <= maxTop: + pwm.PWM.PRESCALER.Set(nrf.PWM_PRESCALER_PRESCALER_DIV_4) + top /= 4 + case top/8 <= maxTop: + pwm.PWM.PRESCALER.Set(nrf.PWM_PRESCALER_PRESCALER_DIV_8) + top /= 8 + case top/16 <= maxTop: + pwm.PWM.PRESCALER.Set(nrf.PWM_PRESCALER_PRESCALER_DIV_16) + top /= 16 + case top/32 <= maxTop: + pwm.PWM.PRESCALER.Set(nrf.PWM_PRESCALER_PRESCALER_DIV_32) + top /= 32 + case top/64 <= maxTop: + pwm.PWM.PRESCALER.Set(nrf.PWM_PRESCALER_PRESCALER_DIV_64) + top /= 64 + case top/128 <= maxTop: + pwm.PWM.PRESCALER.Set(nrf.PWM_PRESCALER_PRESCALER_DIV_128) + top /= 128 + default: + return ErrPWMPeriodTooLong + } + } else { + // Do not update the prescaler, but use the already-configured + // prescaler. This is the normal SetPeriod case, where the prescaler + // must not be changed. + prescaler := pwm.PWM.PRESCALER.Get() + switch prescaler { + case nrf.PWM_PRESCALER_PRESCALER_DIV_1: + top /= 1 + case nrf.PWM_PRESCALER_PRESCALER_DIV_2: + top /= 2 + case nrf.PWM_PRESCALER_PRESCALER_DIV_4: + top /= 4 + case nrf.PWM_PRESCALER_PRESCALER_DIV_8: + top /= 8 + case nrf.PWM_PRESCALER_PRESCALER_DIV_16: + top /= 16 + case nrf.PWM_PRESCALER_PRESCALER_DIV_32: + top /= 32 + case nrf.PWM_PRESCALER_PRESCALER_DIV_64: + top /= 64 + case nrf.PWM_PRESCALER_PRESCALER_DIV_128: + top /= 128 + } + if top > maxTop { + return ErrPWMPeriodTooLong + } + } + pwm.PWM.COUNTERTOP.Set(uint32(top)) + + // Apparently this is needed to apply the new COUNTERTOP. + pwm.PWM.TASKS_SEQSTART[0].Set(1) + + return nil } -// Configure configures a PWM pin for output. -func (pwm PWM) Configure() { +// Top returns the current counter top, for use in duty cycle calculation. It +// will only change with a call to Configure or SetPeriod, otherwise it is +// constant. +// +// The value returned here is hardware dependent. In general, it's best to treat +// it as an opaque value that can be divided by some number and passed to +// pwm.Set (see pwm.Set for more information). +func (pwm *PWM) Top() uint32 { + return pwm.PWM.COUNTERTOP.Get() } -// Set turns on the duty cycle for a PWM pin using the provided value. -func (pwm PWM) Set(value uint16) { - for i := 0; i < len(pwmChannelPins); i++ { - if pwmChannelPins[i] == 0xFFFFFFFF || pwmChannelPins[i] == uint32(pwm.Pin) { - pwmChannelPins[i] = uint32(pwm.Pin) - pwmChannelSequence[i] = (value >> 2) | 0x8000 // set bit 15 to invert polarity - - p := pwms[i] - - p.PSEL.OUT[0].Set(uint32(pwm.Pin)) - p.PSEL.OUT[1].Set(uint32(pwm.Pin)) - p.PSEL.OUT[2].Set(uint32(pwm.Pin)) - p.PSEL.OUT[3].Set(uint32(pwm.Pin)) - p.ENABLE.Set(nrf.PWM_ENABLE_ENABLE_Enabled << nrf.PWM_ENABLE_ENABLE_Pos) - p.PRESCALER.Set(nrf.PWM_PRESCALER_PRESCALER_DIV_2) - p.MODE.Set(nrf.PWM_MODE_UPDOWN_Up) - p.COUNTERTOP.Set(16384) // frequency - p.LOOP.Set(0) - p.DECODER.Set((nrf.PWM_DECODER_LOAD_Common << nrf.PWM_DECODER_LOAD_Pos) | (nrf.PWM_DECODER_MODE_RefreshCount << nrf.PWM_DECODER_MODE_Pos)) - p.SEQ[0].PTR.Set(uint32(uintptr(unsafe.Pointer(&pwmChannelSequence[i])))) - p.SEQ[0].CNT.Set(1) - p.SEQ[0].REFRESH.Set(1) - p.SEQ[0].ENDDELAY.Set(0) - p.TASKS_SEQSTART[0].Set(1) - - break +// Channel returns a PWM channel for the given pin. +func (pwm *PWM) Channel(pin Pin) (uint8, error) { + config := uint32(pin) + for ch := uint8(0); ch < 4; ch++ { + channelConfig := pwm.PWM.PSEL.OUT[ch].Get() + if channelConfig == 0xffffffff { + // Unused channel. Configure it. + pwm.PWM.PSEL.OUT[ch].Set(config) + // Configure the pin (required by the reference manual). + pin.Configure(PinConfig{Mode: PinOutput}) + // Set channel to zero and non-inverting. + pwm.channelValues[ch].Set(0x8000) + return ch, nil + } else if channelConfig == config { + // This channel is already configured for this pin. + return ch, nil } } + + // All four pins are already in use with other pins. + return 0, ErrInvalidOutputPin +} + +// SetInverting sets whether to invert the output of this channel. +// Without inverting, a 25% duty cycle would mean the output is high for 25% of +// the time and low for the rest. Inverting flips the output as if a NOT gate +// was placed at the output, meaning that the output would be 25% low and 75% +// high with a duty cycle of 25%. +func (pwm *PWM) SetInverting(channel uint8, inverting bool) { + ptr := &pwm.channelValues[channel] + if inverting { + ptr.Set(ptr.Get() &^ 0x8000) + } else { + ptr.Set(ptr.Get() | 0x8000) + } +} + +// Set updates the channel value. This is used to control the channel duty +// cycle. For example, to set it to a 25% duty cycle, use: +// +// ch.Set(ch.Top() / 4) +// +// ch.Set(0) will set the output to low and ch.Set(ch.Top()) will set the output +// to high, assuming the output isn't inverted. +func (pwm *PWM) Set(channel uint8, value uint32) { + // Update the channel value while retaining the polarity bit. + ptr := &pwm.channelValues[channel] + ptr.Set(ptr.Get()&0x8000 | uint16(value)&0x7fff) + + // Start the PWM, if it isn't already running. + pwm.PWM.TASKS_SEQSTART[0].Set(1) } |