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_atsamd51.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_atsamd51.go')
-rw-r--r-- | src/machine/machine_atsamd51.go | 528 |
1 files changed, 311 insertions, 217 deletions
diff --git a/src/machine/machine_atsamd51.go b/src/machine/machine_atsamd51.go index 971658745..240b41272 100644 --- a/src/machine/machine_atsamd51.go +++ b/src/machine/machine_atsamd51.go @@ -40,9 +40,9 @@ const ( PinInput PinMode = 15 PinInputPullup PinMode = 16 PinOutput PinMode = 17 - PinPWME PinMode = PinTimer - PinPWMF PinMode = PinTimerAlt - PinPWMG PinMode = PinTCCPDEC + PinTCCE PinMode = PinTimer + PinTCCF PinMode = PinTimerAlt + PinTCCG PinMode = PinTCCPDEC PinInputPulldown PinMode = 18 ) @@ -1570,256 +1570,350 @@ const ( QSPI_DATA3 = PA11 ) -// PWM -const period = 0xFFFF +// TCC is one timer 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 timer peripheral at +// once. +type TCC sam.TCC_Type -// Configure configures a PWM pin for output. -func (pwm PWM) Configure() error { - // Set pin as output - sam.PORT.GROUP[0].DIRSET.Set(1 << uint8(pwm.Pin)) - // Set pin to low - sam.PORT.GROUP[0].OUTCLR.Set(1 << uint8(pwm.Pin)) +//go:inline +func (tcc *TCC) timer() *sam.TCC_Type { + return (*sam.TCC_Type)(tcc) +} - // Enable the port multiplexer for pin - pwm.setPinCfg(sam.PORT_GROUP_PINCFG_PMUXEN) +// Configure enables and configures this TCC. +func (tcc *TCC) Configure(config PWMConfig) error { + // Enable the TCC clock to be able to use the TCC. + tcc.configureClock() - // Connect timer/mux to pin. - pwmConfig := pwm.getMux() + // Disable timer (if it was enabled). This is necessary because + // tcc.setPeriod may want to change the prescaler bits in CTRLA, which is + // only allowed when the TCC is disabled. + tcc.timer().CTRLA.ClearBits(sam.TCC_CTRLA_ENABLE) - if pwm.Pin&1 > 0 { - // odd pin, so save the even pins - val := pwm.getPMux() & sam.PORT_GROUP_PMUX_PMUXE_Msk - pwm.setPMux(val | uint8(pwmConfig<<sam.PORT_GROUP_PMUX_PMUXO_Pos)) - } else { - // even pin, so save the odd pins - val := pwm.getPMux() & sam.PORT_GROUP_PMUX_PMUXO_Msk - pwm.setPMux(val | uint8(pwmConfig<<sam.PORT_GROUP_PMUX_PMUXE_Pos)) - } + // Use "Normal PWM" (single-slope PWM) + tcc.timer().WAVE.Set(sam.TCC_WAVE_WAVEGEN_NPWM) - // figure out which TCCX timer for this pin - timer := pwm.getTimer() - if timer == nil { - return ErrInvalidOutputPin + // Wait for synchronization of all changed registers. + for tcc.timer().SYNCBUSY.Get() != 0 { } - // disable timer - timer.CTRLA.ClearBits(sam.TCC_CTRLA_ENABLE) - // Wait for synchronization - for timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_ENABLE) { - } + // Set the period and prescaler. + err := tcc.setPeriod(config.Period, true) - // Set prescaler to 1/256 - // TCCx->CTRLA.reg = TCC_CTRLA_PRESCALER_DIV256 | TCC_CTRLA_PRESCSYNC_GCLK; - timer.CTRLA.SetBits(sam.TCC_CTRLA_PRESCALER_DIV256 | sam.TCC_CTRLA_PRESCSYNC_GCLK) + // Enable the timer. + tcc.timer().CTRLA.SetBits(sam.TCC_CTRLA_ENABLE) - // Use "Normal PWM" (single-slope PWM) - timer.WAVE.SetBits(sam.TCC_WAVE_WAVEGEN_NPWM) - // Wait for synchronization - for timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_WAVE) { + // Wait for synchronization of all changed registers. + for tcc.timer().SYNCBUSY.Get() != 0 { } - // while (TCCx->SYNCBUSY.bit.CC0 || TCCx->SYNCBUSY.bit.CC1); - for timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_CC0) || - timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_CC1) { + // Return any error that might have occured in the tcc.setPeriod call. + return err +} + +// SetPeriod updates the period of this TCC 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 TCC 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 (tcc *TCC) SetPeriod(period uint64) error { + return tcc.setPeriod(period, false) +} + +// setPeriod sets the period of this TCC, possibly updating the prescaler as +// well. The prescaler can only modified when the TCC is disabled, that is, in +// the Configure function. +func (tcc *TCC) setPeriod(period uint64, updatePrescaler bool) error { + var top uint64 + if period == 0 { + // Make sure the TOP value is at 0xffff (enough for a 16-bit timer). + top = 0xffff + } else { + // The formula below calculates the following formula, optimized: + // period * (120e6 / 1e9) + // This assumes that the chip is running from generic clock generator 0 + // at 120MHz. + top = period * 3 / 25 + } + + maxTop := uint64(0xffff) + if tcc.timer() == sam.TCC0 || tcc.timer() == sam.TCC1 { + // Only TCC0 and TCC1 are 24-bit timers, the rest are 16-bit. + maxTop = 0xffffff + } + + if updatePrescaler { + // This function was called during Configure(), with the timer disabled. + // Note that updating the prescaler can only happen while the peripheral + // is disabled. + var prescaler uint32 + switch { + case top <= maxTop: + prescaler = sam.TCC_CTRLA_PRESCALER_DIV1 + case top/2 <= maxTop: + prescaler = sam.TCC_CTRLA_PRESCALER_DIV2 + top = top / 2 + case top/4 <= maxTop: + prescaler = sam.TCC_CTRLA_PRESCALER_DIV4 + top = top / 4 + case top/8 <= maxTop: + prescaler = sam.TCC_CTRLA_PRESCALER_DIV8 + top = top / 8 + case top/16 <= maxTop: + prescaler = sam.TCC_CTRLA_PRESCALER_DIV16 + top = top / 16 + case top/64 <= maxTop: + prescaler = sam.TCC_CTRLA_PRESCALER_DIV64 + top = top / 64 + case top/256 <= maxTop: + prescaler = sam.TCC_CTRLA_PRESCALER_DIV256 + top = top / 256 + case top/1024 <= maxTop: + prescaler = sam.TCC_CTRLA_PRESCALER_DIV1024 + top = top / 1024 + default: + return ErrPWMPeriodTooLong + } + tcc.timer().CTRLA.Set((tcc.timer().CTRLA.Get() &^ sam.TCC_CTRLA_PRESCALER_Msk) | (prescaler << sam.TCC_CTRLA_PRESCALER_Pos)) + } 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 := (tcc.timer().CTRLA.Get() & sam.TCC_CTRLA_PRESCALER_Msk) >> sam.TCC_CTRLA_PRESCALER_Pos + switch prescaler { + case sam.TCC_CTRLA_PRESCALER_DIV1: + top /= 1 // no-op + case sam.TCC_CTRLA_PRESCALER_DIV2: + top /= 2 + case sam.TCC_CTRLA_PRESCALER_DIV4: + top /= 4 + case sam.TCC_CTRLA_PRESCALER_DIV8: + top /= 8 + case sam.TCC_CTRLA_PRESCALER_DIV16: + top /= 16 + case sam.TCC_CTRLA_PRESCALER_DIV64: + top /= 64 + case sam.TCC_CTRLA_PRESCALER_DIV256: + top /= 256 + case sam.TCC_CTRLA_PRESCALER_DIV1024: + top /= 1024 + default: + // unreachable + } + if top > maxTop { + return ErrPWMPeriodTooLong + } } - // Set the initial value - // TCCx->CC[tcChannel].reg = (uint32_t) value; - pwm.setChannel(timer, 0) + // Set the period (the counter top). + tcc.timer().PER.Set(uint32(top) - 1) - for timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_CC0) || - timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_CC1) { + // Wait for synchronization of CTRLA.PRESCALER and PER registers. + for tcc.timer().SYNCBUSY.Get() != 0 { } - // Set the period (the number to count to (TOP) before resetting timer) - //TCC0->PER.reg = period; - timer.PER.Set(period) - // Wait for synchronization - for timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_PER) { - } + return nil +} + +// 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 +// tcc.Set (see tcc.Set for more information). +func (tcc *TCC) Top() uint32 { + return tcc.timer().PER.Get() + 1 +} - // enable timer - timer.CTRLA.SetBits(sam.TCC_CTRLA_ENABLE) - // Wait for synchronization - for timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_ENABLE) { +// Counter returns the current counter value of the timer in this TCC +// peripheral. It may be useful for debugging. +func (tcc *TCC) Counter() uint32 { + tcc.timer().CTRLBSET.Set(sam.TCC_CTRLBSET_CMD_READSYNC << sam.TCC_CTRLBSET_CMD_Pos) + for tcc.timer().SYNCBUSY.Get() != 0 { } + return tcc.timer().COUNT.Get() +} - return nil +// Constants that encode a TCC number and WO number together in a single byte. +const ( + pinTCC0 = 1 << 4 // keep the value 0 usable as "no value" + pinTCC1 = 2 << 4 + pinTCC2 = 3 << 4 + pinTCC3 = 4 << 4 + pinTCC4 = 5 << 4 + pinTCC0_0 = pinTCC0 | 0 + pinTCC0_1 = pinTCC0 | 1 + pinTCC0_2 = pinTCC0 | 2 + pinTCC0_3 = pinTCC0 | 3 + pinTCC0_4 = pinTCC0 | 4 + pinTCC0_5 = pinTCC0 | 5 + pinTCC0_6 = pinTCC0 | 6 + pinTCC1_0 = pinTCC1 | 0 + pinTCC1_2 = pinTCC1 | 2 + pinTCC1_4 = pinTCC1 | 4 + pinTCC1_6 = pinTCC1 | 6 + pinTCC2_0 = pinTCC2 | 0 + pinTCC2_2 = pinTCC2 | 2 + pinTCC3_0 = pinTCC3 | 0 + pinTCC4_0 = pinTCC4 | 0 +) + +// This is a copy of columns F and G (the TCC columns) of table 6-1 in the +// datasheet: +// http://ww1.microchip.com/downloads/en/DeviceDoc/60001507E.pdf +// For example, "TCC0/WO[2]" is converted to pinTCC0_2. +// Only the even pin numbers are stored here. The odd pin numbers are left out, +// because their PWM output can be determined from the even number: just add one +// to the wave output (WO) number. +var pinTimerMapping = [...]struct{ F, G uint8 }{ + // page 33 + PC04 / 2: {pinTCC0_0, 0}, + PA08 / 2: {pinTCC0_0, pinTCC1_4}, + PA10 / 2: {pinTCC0_2, pinTCC1_6}, + PB10 / 2: {pinTCC0_4, pinTCC1_0}, + PB12 / 2: {pinTCC3_0, pinTCC0_0}, + PB14 / 2: {pinTCC4_0, pinTCC0_2}, + PD08 / 2: {pinTCC0_1, 0}, + PD10 / 2: {pinTCC0_3, 0}, + PD12 / 2: {pinTCC0_5, 0}, + PC10 / 2: {pinTCC0_0, pinTCC1_4}, + // page 34 + PC12 / 2: {pinTCC0_2, pinTCC1_6}, + PC14 / 2: {pinTCC0_4, pinTCC1_0}, + PA12 / 2: {pinTCC0_6, pinTCC1_2}, + PA14 / 2: {pinTCC2_0, pinTCC1_2}, + PA16 / 2: {pinTCC1_0, pinTCC0_4}, + PA18 / 2: {pinTCC1_2, pinTCC0_6}, + PC16 / 2: {pinTCC0_0, 0}, + PC18 / 2: {pinTCC0_2, 0}, + PC20 / 2: {pinTCC0_4, 0}, + PC22 / 2: {pinTCC0_6, 0}, + PD20 / 2: {pinTCC1_0, 0}, + PB16 / 2: {pinTCC3_0, pinTCC0_4}, + PB18 / 2: {pinTCC1_0, 0}, + // page 35 + PB20 / 2: {pinTCC1_2, 0}, + PA20 / 2: {pinTCC1_4, pinTCC0_0}, + PA22 / 2: {pinTCC1_6, pinTCC0_2}, + PA24 / 2: {pinTCC2_2, 0}, + PB26 / 2: {pinTCC1_2, 0}, + PB28 / 2: {pinTCC1_4, 0}, + PA30 / 2: {pinTCC2_0, 0}, + // page 36 + PB30 / 2: {pinTCC4_0, pinTCC0_6}, + PB02 / 2: {pinTCC2_2, 0}, } -// Set turns on the duty cycle for a PWM pin using the provided value. -func (pwm PWM) Set(value uint16) { - // figure out which TCCX timer for this pin - timer := pwm.getTimer() - if timer == nil { - // The Configure call above cannot have succeeded, so simply ignore this - // error. - return +// findPinPadMapping returns the pin mode (PinTCCF or PinTCCG) and the channel +// number for a given timer and pin. A zero PinMode is returned if no mapping +// could be found. +func findPinTimerMapping(timer uint8, pin Pin) (PinMode, uint8) { + if int(pin/2) >= len(pinTimerMapping) { + return 0, 0 // invalid pin number } - // Wait for synchronization - for timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_CTRLB) { + mapping := pinTimerMapping[pin/2] + + // Check for column F in the datasheet. + if mapping.F>>4-1 == timer { + return PinTCCF, mapping.F&0x0f + uint8(pin)&1 } - for timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_CC0) || - timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_CC1) { + + // Check for column G in the datasheet. + if mapping.G>>4-1 == timer { + return PinTCCG, mapping.G&0x0f + uint8(pin)&1 } - // TCCx->CCBUF[tcChannel].reg = (uint32_t) value; - pwm.setChannelBuffer(timer, uint32(value)) + // Nothing found. + return 0, 0 +} + +// Channel returns a PWM channel for the given pin. Note that one channel may be +// shared between multiple pins, and so will have the same duty cycle. If this +// is not desirable, look for a different TCC or consider using a different pin. +func (tcc *TCC) Channel(pin Pin) (uint8, error) { + pinMode, woOutput := findPinTimerMapping(tcc.timerNum(), pin) - for timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_CC0) || - timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_CC1) { + if pinMode == 0 { + // No pin could be found. + return 0, ErrInvalidOutputPin } - // TCCx->CTRLBCLR.bit.LUPD = 1; - timer.CTRLBCLR.SetBits(sam.TCC_CTRLBCLR_LUPD) - for timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_CTRLB) { + // Convert from waveform output to channel, assuming WEXCTRL.OTMX equals 0. + // See table 49-4 "Output Matrix Channel Pin Routing Configuration" on page + // 1829 of the datasheet. + // The number of channels varies by TCC instance, hence the need to switch + // over them. For TCC2-4 the number of channels is equal to the number of + // waveform outputs, so the WO number maps directly to the channel number. + // For TCC0 and TCC1 this is not the case so they will need some special + // handling. + channel := woOutput + switch tcc.timer() { + case sam.TCC0: + channel = woOutput % 6 + case sam.TCC1: + channel = woOutput % 4 } -} -// getPMux returns the value for the correct PMUX register for this pin. -func (pwm PWM) getPMux() uint8 { - return pwm.Pin.getPMux() -} + // Enable the port multiplexer for pin + pin.setPinCfg(sam.PORT_GROUP_PINCFG_PMUXEN) -// setPMux sets the value for the correct PMUX register for this pin. -func (pwm PWM) setPMux(val uint8) { - pwm.Pin.setPMux(val) + // Connect timer/mux to pin. + if pin&1 > 0 { + // odd pin, so save the even pins + val := pin.getPMux() & sam.PORT_GROUP_PMUX_PMUXE_Msk + pin.setPMux(val | uint8(pinMode<<sam.PORT_GROUP_PMUX_PMUXO_Pos)) + } else { + // even pin, so save the odd pins + val := pin.getPMux() & sam.PORT_GROUP_PMUX_PMUXO_Msk + pin.setPMux(val | uint8(pinMode<<sam.PORT_GROUP_PMUX_PMUXE_Pos)) + } + + return channel, nil } -// getPinCfg returns the value for the correct PINCFG register for this pin. -func (pwm PWM) getPinCfg() uint8 { - return pwm.Pin.getPinCfg() +// 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 (tcc *TCC) SetInverting(channel uint8, inverting bool) { + if inverting { + tcc.timer().WAVE.SetBits(1 << (sam.TCC_WAVE_POL0_Pos + channel)) + } else { + tcc.timer().WAVE.ClearBits(1 << (sam.TCC_WAVE_POL0_Pos + channel)) + } + + // Wait for synchronization of the WAVE register. + for tcc.timer().SYNCBUSY.Get() != 0 { + } } -// setPinCfg sets the value for the correct PINCFG register for this pin. -func (pwm PWM) setPinCfg(val uint8) { - pwm.Pin.setPinCfg(val) -} - -// setChannel sets the value for the correct channel for PWM on this pin. -func (pwm PWM) setChannel(timer *sam.TCC_Type, val uint32) { - switch pwm.Pin { - case PA14: - timer.CC[0].Set(val) - case PA15: - timer.CC[1].Set(val) - case PA16: - timer.CC[0].Set(val) - case PA17: - timer.CC[1].Set(val) - case PA18: - timer.CC[2].Set(val) - case PA19: - timer.CC[3].Set(val) - case PA20: - timer.CC[0].Set(val) - case PA21: - timer.CC[1].Set(val) - case PA22: - timer.CC[2].Set(val) - case PA23: - timer.CC[3].Set(val) - case PB12: - timer.CC[0].Set(val) - case PB13: - timer.CC[1].Set(val) - case PB14: - timer.CC[0].Set(val) - case PB15: - timer.CC[1].Set(val) - case PB16: - timer.CC[4].Set(val) - case PB17: - timer.CC[5].Set(val) - case PB31: - timer.CC[1].Set(val) - default: - return // not supported on this pin - } -} - -// setChannelBuffer sets the value for the correct channel buffer for PWM on this pin -func (pwm PWM) setChannelBuffer(timer *sam.TCC_Type, val uint32) { - switch pwm.Pin { - case PA14: - timer.CCBUF[0].Set(val) - case PA15: - timer.CCBUF[1].Set(val) - case PA16: - timer.CCBUF[0].Set(val) - case PA17: - timer.CCBUF[1].Set(val) - case PA18: - timer.CCBUF[2].Set(val) - case PA19: - timer.CCBUF[3].Set(val) - case PA20: - timer.CCBUF[0].Set(val) - case PA21: - timer.CCBUF[1].Set(val) - case PA22: - timer.CCBUF[2].Set(val) - case PA23: - timer.CCBUF[3].Set(val) - case PB12: - timer.CCBUF[0].Set(val) - case PB13: - timer.CCBUF[1].Set(val) - case PB14: - timer.CCBUF[0].Set(val) - case PB15: - timer.CCBUF[1].Set(val) - case PB16: - timer.CCBUF[4].Set(val) - case PB17: - timer.CCBUF[5].Set(val) - case PB31: - timer.CCBUF[1].Set(val) - default: - return // not supported on this pin - } -} - -// getMux returns the pin mode mux to be used for PWM on this pin. -func (pwm PWM) getMux() PinMode { - switch pwm.Pin { - case PA14: - return PinPWMF - case PA15: - return PinPWMF - case PA16: - return PinPWMF - case PA17: - return PinPWMF - case PA18: - return PinPWMF - case PA19: - return PinPWMF - case PA20: - return PinPWMG - case PA21: - return PinPWMG - case PA22: - return PinPWMG - case PA23: - return PinPWMG - case PB12: - return PinPWMF - case PB13: - return PinPWMF - case PB14: - return PinPWMF - case PB15: - return PinPWMF - case PB16: - return PinPWMG - case PB17: - return PinPWMG - case PB31: - return PinPWMF - default: - return 0 // not supported on this pin +// Set updates the channel value. This is used to control the channel duty +// cycle, in other words the fraction of time the channel output is high (or low +// when inverted). For example, to set it to a 25% duty cycle, use: +// +// tcc.Set(channel, tcc.Top() / 4) +// +// tcc.Set(channel, 0) will set the output to low and tcc.Set(channel, +// tcc.Top()) will set the output to high, assuming the output isn't inverted. +func (tcc *TCC) Set(channel uint8, value uint32) { + // Update CCBUF, which provides double buffering. The update is applied on + // the next cycle. + tcc.timer().CCBUF[channel].Set(value) + for tcc.timer().SYNCBUSY.Get() != 0 { } } |