aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/machine/machine_nrf528xx.go
diff options
context:
space:
mode:
authorAyke van Laethem <[email protected]>2020-05-19 22:30:46 +0200
committerRon Evans <[email protected]>2021-04-06 20:36:10 +0200
commit72acda22b0a8d137405e41e9ed54cbfbcce7b26f (patch)
treebaed2cc5aa5bd3f4582a6ef4aa439928acdf38f9 /src/machine/machine_nrf528xx.go
parentf880950c3efbd005d2e171a5fd1c879a3221367c (diff)
downloadtinygo-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.go227
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)
}