From 72acda22b0a8d137405e41e9ed54cbfbcce7b26f Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Tue, 19 May 2020 22:30:46 +0200 Subject: [PATCH] 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. --- Makefile | 2 - src/examples/pwm/arduino.go | 12 + src/examples/pwm/feather-m4.go | 11 + src/examples/pwm/itsybitsy-m0.go | 11 + src/examples/pwm/itsybitsy-m4.go | 11 + src/examples/pwm/pwm.go | 104 +++-- src/machine/board_circuitplay_express.go | 4 +- src/machine/machine.go | 4 - src/machine/machine_atmega328p.go | 480 +++++++++++++++++--- src/machine/machine_atsamd21.go | 508 ++++++++++++++------- src/machine/machine_atsamd51.go | 544 +++++++++++++---------- src/machine/machine_atsamd51g19.go | 68 ++- src/machine/machine_atsamd51j19.go | 97 ++-- src/machine/machine_atsamd51j20.go | 97 ++-- src/machine/machine_atsamd51p19.go | 97 ++-- src/machine/machine_generic.go | 18 - src/machine/machine_nrf52.go | 6 +- src/machine/machine_nrf52840.go | 7 +- src/machine/machine_nrf528xx.go | 221 +++++++-- src/machine/pwm.go | 21 + 20 files changed, 1565 insertions(+), 758 deletions(-) create mode 100644 src/examples/pwm/arduino.go create mode 100644 src/examples/pwm/feather-m4.go create mode 100644 src/examples/pwm/itsybitsy-m0.go create mode 100644 src/examples/pwm/itsybitsy-m4.go create mode 100644 src/machine/pwm.go diff --git a/Makefile b/Makefile index d3b36f1a..6c7f142e 100644 --- a/Makefile +++ b/Makefile @@ -341,8 +341,6 @@ smoketest: @$(MD5SUM) test.hex $(TINYGO) build -size short -o test.hex -target=feather-m4 examples/pwm @$(MD5SUM) test.hex - $(TINYGO) build -size short -o test.hex -target=pyportal examples/pwm - @$(MD5SUM) test.hex ifneq ($(STM32), 0) $(TINYGO) build -size short -o test.hex -target=bluepill examples/blinky1 @$(MD5SUM) test.hex diff --git a/src/examples/pwm/arduino.go b/src/examples/pwm/arduino.go new file mode 100644 index 00000000..f6739b5f --- /dev/null +++ b/src/examples/pwm/arduino.go @@ -0,0 +1,12 @@ +// +build arduino + +package main + +import "machine" + +var ( + // Configuration on an Arduino Uno. + pwm = machine.Timer2 + pinA = machine.PB3 // pin 11 on the Uno + pinB = machine.PD3 // pin 3 on the Uno +) diff --git a/src/examples/pwm/feather-m4.go b/src/examples/pwm/feather-m4.go new file mode 100644 index 00000000..1c77be8b --- /dev/null +++ b/src/examples/pwm/feather-m4.go @@ -0,0 +1,11 @@ +// +build feather_m4 + +package main + +import "machine" + +var ( + pwm = machine.TCC0 + pinA = machine.D12 + pinB = machine.D13 +) diff --git a/src/examples/pwm/itsybitsy-m0.go b/src/examples/pwm/itsybitsy-m0.go new file mode 100644 index 00000000..c0b2f7d7 --- /dev/null +++ b/src/examples/pwm/itsybitsy-m0.go @@ -0,0 +1,11 @@ +// +build itsybitsy_m0 + +package main + +import "machine" + +var ( + pwm = machine.TCC0 + pinA = machine.D3 + pinB = machine.D4 +) diff --git a/src/examples/pwm/itsybitsy-m4.go b/src/examples/pwm/itsybitsy-m4.go new file mode 100644 index 00000000..1e13f9ef --- /dev/null +++ b/src/examples/pwm/itsybitsy-m4.go @@ -0,0 +1,11 @@ +// +build itsybitsy_m4 + +package main + +import "machine" + +var ( + pwm = machine.TCC0 + pinA = machine.D12 + pinB = machine.D13 +) diff --git a/src/examples/pwm/pwm.go b/src/examples/pwm/pwm.go index 6bcc46fc..10c102e3 100644 --- a/src/examples/pwm/pwm.go +++ b/src/examples/pwm/pwm.go @@ -1,64 +1,74 @@ package main +// This example demonstrates some features of the PWM support. + import ( "machine" "time" ) -// This example assumes that an RGB LED is connected to pins 3, 5 and 6 on an Arduino. -// Change the values below to use different pins. -const ( - redPin = machine.D4 - greenPin = machine.D5 - bluePin = machine.D6 -) - -// cycleColor is just a placeholder until math/rand or some equivalent is working. -func cycleColor(color uint8) uint8 { - if color < 10 { - return color + 1 - } else if color < 200 { - return color + 10 - } else { - return 0 - } -} +const delayBetweenPeriods = time.Second * 5 func main() { - machine.InitPWM() + // Delay a bit on startup to easily catch the first messages. + time.Sleep(time.Second * 2) - red := machine.PWM{redPin} - err := red.Configure() - checkError(err, "failed to configure red pin") + // Configure the PWM with the given period. + err := pwm.Configure(machine.PWMConfig{ + Period: 16384e3, // 16.384ms + }) + if err != nil { + println("failed to configure PWM") + return + } - green := machine.PWM{greenPin} - err = green.Configure() - checkError(err, "failed to configure green pin") + // The top value is the highest value that can be passed to PWMChannel.Set. + // It is usually an even number. + println("top:", pwm.Top()) - blue := machine.PWM{bluePin} - err = blue.Configure() - checkError(err, "failed to configure blue pin") + // Configure the two channels we'll use as outputs. + channelA, err := pwm.Channel(pinA) + if err != nil { + println("failed to configure channel A") + return + } + channelB, err := pwm.Channel(pinB) + if err != nil { + println("failed to configure channel B") + return + } - var rc uint8 - var gc uint8 = 20 - var bc uint8 = 30 + // Invert one of the channels to demonstrate output polarity. + pwm.SetInverting(channelB, true) + + // Test out various frequencies below, including some edge cases. + + println("running at 0% duty cycle") + pwm.Set(channelA, 0) + pwm.Set(channelB, 0) + time.Sleep(delayBetweenPeriods) + + println("running at 1") + pwm.Set(channelA, 1) + pwm.Set(channelB, 1) + time.Sleep(delayBetweenPeriods) + + println("running at 25% duty cycle") + pwm.Set(channelA, pwm.Top()/4) + pwm.Set(channelB, pwm.Top()/4) + time.Sleep(delayBetweenPeriods) + + println("running at top-1") + pwm.Set(channelA, pwm.Top()-1) + pwm.Set(channelB, pwm.Top()-1) + time.Sleep(delayBetweenPeriods) + + println("running at 100% duty cycle") + pwm.Set(channelA, pwm.Top()) + pwm.Set(channelB, pwm.Top()) + time.Sleep(delayBetweenPeriods) for { - rc = cycleColor(rc) - gc = cycleColor(gc) - bc = cycleColor(bc) - - red.Set(uint16(rc) << 8) - green.Set(uint16(gc) << 8) - blue.Set(uint16(bc) << 8) - - time.Sleep(time.Millisecond * 500) - } -} - -func checkError(err error, msg string) { - if err != nil { - print(msg, ": ", err.Error()) - println() + time.Sleep(time.Second) } } diff --git a/src/machine/board_circuitplay_express.go b/src/machine/board_circuitplay_express.go index e1fd7be2..be990471 100644 --- a/src/machine/board_circuitplay_express.go +++ b/src/machine/board_circuitplay_express.go @@ -25,8 +25,8 @@ const ( // Analog Pins const ( - A0 = PA02 // PWM available, also ADC/AIN[0] - A1 = PA05 // ADC/AIN[5] + A0 = PA02 // ADC/AIN[0] + A1 = PA05 // PWM available, also ADC/AIN[5] A2 = PA06 // PWM available, also ADC/AIN[6] A3 = PA07 // PWM available, also ADC/AIN[7] A4 = PB03 // PORTB diff --git a/src/machine/machine.go b/src/machine/machine.go index ce519f64..d40c39f0 100644 --- a/src/machine/machine.go +++ b/src/machine/machine.go @@ -37,10 +37,6 @@ func (p Pin) Low() { p.Set(false) } -type PWM struct { - Pin Pin -} - type ADC struct { Pin Pin } diff --git a/src/machine/machine_atmega328p.go b/src/machine/machine_atmega328p.go index 7eefc914..fe5ace95 100644 --- a/src/machine/machine_atmega328p.go +++ b/src/machine/machine_atmega328p.go @@ -4,6 +4,7 @@ package machine import ( "device/avr" + "runtime/interrupt" "runtime/volatile" ) @@ -21,71 +22,432 @@ func (p Pin) getPortMask() (*volatile.Register8, uint8) { } } -// InitPWM initializes the registers needed for PWM. -func InitPWM() { - // use waveform generation - avr.TCCR0A.SetBits(avr.TCCR0A_WGM00) - - // set timer 0 prescale factor to 64 - avr.TCCR0B.SetBits(avr.TCCR0B_CS01 | avr.TCCR0B_CS00) - - // set timer 1 prescale factor to 64 - avr.TCCR1B.SetBits(avr.TCCR1B_CS11) - - // put timer 1 in 8-bit phase correct pwm mode - avr.TCCR1A.SetBits(avr.TCCR1A_WGM10) - - // set timer 2 prescale factor to 64 - avr.TCCR2B.SetBits(avr.TCCR2B_CS22) - - // configure timer 2 for phase correct pwm (8-bit) - avr.TCCR2A.SetBits(avr.TCCR2A_WGM20) +// PWM is one PWM peripheral, which consists of a counter and two output +// channels (that can be connected to two fixed pins). You can set the frequency +// using SetPeriod, but only for all the channels in this PWM peripheral at +// once. +type PWM struct { + num uint8 } -// Configure configures a PWM pin for output. -func (pwm PWM) Configure() error { - switch pwm.Pin / 8 { - case 0: // port B - avr.DDRB.SetBits(1 << uint8(pwm.Pin)) - case 2: // port D - avr.DDRD.SetBits(1 << uint8(pwm.Pin-16)) +var ( + Timer0 = PWM{0} // 8 bit timer for PD5 and PD6 + Timer1 = PWM{1} // 16 bit timer for PB1 and PB2 + Timer2 = PWM{2} // 8 bit timer for PB3 and PD3 +) + +// Configure enables and configures this PWM. +// +// For the two 8 bit timers, there is only a limited number of periods +// available, namely the CPU frequency divided by 256 and again divided by 1, 8, +// 64, 256, or 1024. For a MCU running at 16MHz, this would be a period of 16µs, +// 128µs, 1024µs, 4096µs, or 16384µs. +func (pwm PWM) Configure(config PWMConfig) error { + switch pwm.num { + case 0, 2: // 8-bit timers (Timer/counter 0 and Timer/counter 2) + // Calculate the timer prescaler. + // While we could configure a flexible top, that would sacrifice one of + // the PWM output compare registers and thus a PWM channel. I've chosen + // to instead limit this timer to a fixed number of frequencies. + var prescaler uint8 + switch config.Period { + case 0, (uint64(1e9) * 256 * 1) / uint64(CPUFrequency()): + prescaler = 1 + case (uint64(1e9) * 256 * 8) / uint64(CPUFrequency()): + prescaler = 2 + case (uint64(1e9) * 256 * 64) / uint64(CPUFrequency()): + prescaler = 3 + case (uint64(1e9) * 256 * 256) / uint64(CPUFrequency()): + prescaler = 4 + case (uint64(1e9) * 256 * 1024) / uint64(CPUFrequency()): + prescaler = 5 + default: + return ErrPWMPeriodTooLong + } + + if pwm.num == 0 { + avr.TCCR0B.Set(prescaler) + // Set the PWM mode to fast PWM (mode = 3). + avr.TCCR0A.Set(avr.TCCR0A_WGM00 | avr.TCCR0A_WGM01) + } else { + avr.TCCR2B.Set(prescaler) + // Set the PWM mode to fast PWM (mode = 3). + avr.TCCR2A.Set(avr.TCCR2A_WGM20 | avr.TCCR2A_WGM21) + } + case 1: // Timer/counter 1 + // The top value is the number of PWM ticks a PWM period takes. It is + // initially picked assuming an unlimited counter top and no PWM + // prescaler. + var top uint64 + if config.Period == 0 { + // Use a top appropriate for LEDs. Picking a relatively low period + // here (0xff) for consistency with the other timers. + top = 0xff + } else { + // The formula below calculates the following formula, optimized: + // top = period * (CPUFrequency() / 1e9) + // By dividing the CPU frequency first (an operation that is easily + // optimized away) the period has less chance of overflowing. + top = config.Period * (uint64(CPUFrequency()) / 1000000) / 1000 + } + + avr.TCCR1A.Set(avr.TCCR1A_WGM11) + + // The ideal PWM period may be larger than would fit in the PWM counter, + // which is 16 bits (see maxTop). Therefore, try to make the PWM clock + // speed lower with a prescaler to make the top value fit the maximum + // top value. + const maxTop = 0x10000 + switch { + case top <= maxTop: + avr.TCCR1B.Set(3<<3 | 1) // no prescaling + case top/8 <= maxTop: + avr.TCCR1B.Set(3<<3 | 2) // divide by 8 + top /= 8 + case top/64 <= maxTop: + avr.TCCR1B.Set(3<<3 | 3) // divide by 64 + top /= 64 + case top/256 <= maxTop: + avr.TCCR1B.Set(3<<3 | 4) // divide by 256 + top /= 256 + case top/1024 <= maxTop: + avr.TCCR1B.Set(3<<3 | 5) // divide by 1024 + top /= 1024 + default: + return ErrPWMPeriodTooLong + } + + // A top of 0x10000 is at 100% duty cycle. Subtract one because the + // counter counts from 0, not 1 (avoiding an off-by-one). + top -= 1 + + avr.ICR1H.Set(uint8(top >> 8)) + avr.ICR1L.Set(uint8(top)) } return nil } -// Set turns on the duty cycle for a PWM pin using the provided value. On the AVR this is normally a -// 8-bit value ranging from 0 to 255. -func (pwm PWM) Set(value uint16) { - value8 := uint8(value >> 8) - switch pwm.Pin { - case PD3: - // connect pwm to pin on timer 2, channel B - avr.TCCR2A.SetBits(avr.TCCR2A_COM2B1) - avr.OCR2B.Set(value8) // set pwm duty - case PD5: - // connect pwm to pin on timer 0, channel B - avr.TCCR0A.SetBits(avr.TCCR0A_COM0B1) - avr.OCR0B.Set(value8) // set pwm duty - case PD6: - // connect pwm to pin on timer 0, channel A - avr.TCCR0A.SetBits(avr.TCCR0A_COM0A1) - avr.OCR0A.Set(value8) // set pwm duty - case PB1: - // connect pwm to pin on timer 1, channel A - avr.TCCR1A.SetBits(avr.TCCR1A_COM1A1) - // this is a 16-bit value, but we only currently allow the low order bits to be set - avr.OCR1AL.Set(value8) // set pwm duty - case PB2: - // connect pwm to pin on timer 1, channel B - avr.TCCR1A.SetBits(avr.TCCR1A_COM1B1) - // this is a 16-bit value, but we only currently allow the low order bits to be set - avr.OCR1BL.Set(value8) // set pwm duty - case PB3: - // connect pwm to pin on timer 2, channel A - avr.TCCR2A.SetBits(avr.TCCR2A_COM2A1) - avr.OCR2A.Set(value8) // set pwm duty - default: - panic("Invalid PWM pin") +// 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 { + if pwm.num != 1 { + return ErrPWMPeriodTooLong // TODO better error message + } + + // The top value is the number of PWM ticks a PWM period takes. It is + // initially picked assuming an unlimited counter top and no PWM + // prescaler. + var top uint64 + if period == 0 { + // Use a top appropriate for LEDs. Picking a relatively low period + // here (0xff) for consistency with the other timers. + top = 0xff + } else { + // The formula below calculates the following formula, optimized: + // top = period * (CPUFrequency() / 1e9) + // By dividing the CPU frequency first (an operation that is easily + // optimized away) the period has less chance of overflowing. + top = period * (uint64(CPUFrequency()) / 1000000) / 1000 + } + + prescaler := avr.TCCR1B.Get() & 0x7 + switch prescaler { + case 1: + top /= 1 + case 2: + top /= 8 + case 3: + top /= 64 + case 4: + top /= 256 + case 5: + top /= 1024 + } + + // A top of 0x10000 is at 100% duty cycle. Subtract one because the counter + // counts from 0, not 1 (avoiding an off-by-one). + top -= 1 + + if top > 0xffff { + return ErrPWMPeriodTooLong + } + + // Warning: this change is not atomic! + avr.ICR1H.Set(uint8(top >> 8)) + avr.ICR1L.Set(uint8(top)) + + // ... and because of that, set the counter back to zero to avoid most of + // the effects of this non-atomicity. + avr.TCNT1H.Set(0) + avr.TCNT1L.Set(0) + + 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 Set +// (see Set documentation for more information). +func (pwm PWM) Top() uint32 { + if pwm.num == 1 { + // Timer 1 has a configurable top value. + low := avr.ICR1L.Get() + high := avr.ICR1H.Get() + return uint32(high)<<8 | uint32(low) + 1 + } + // Other timers go from 0 to 0xff (0x100 or 256 in total). + return 256 +} + +// Counter returns the current counter value of the timer in this PWM +// peripheral. It may be useful for debugging. +func (pwm PWM) Counter() uint32 { + switch pwm.num { + case 0: + return uint32(avr.TCNT0.Get()) + case 1: + mask := interrupt.Disable() + low := avr.TCNT1L.Get() + high := avr.TCNT1H.Get() + interrupt.Restore(mask) + return uint32(high)<<8 | uint32(low) + case 2: + return uint32(avr.TCNT2.Get()) + } + // Unknown PWM. + return 0 +} + +// Period returns the used PWM period in nanoseconds. It might deviate slightly +// from the configured period due to rounding. +func (pwm PWM) Period() uint64 { + var prescaler uint8 + switch pwm.num { + case 0: + prescaler = avr.TCCR0B.Get() & 0x7 + case 1: + prescaler = avr.TCCR1B.Get() & 0x7 + case 2: + prescaler = avr.TCCR2B.Get() & 0x7 + } + top := uint64(pwm.Top()) + switch prescaler { + case 1: // prescaler 1 + return 1 * top * 1000 / uint64(CPUFrequency()/1e6) + case 2: // prescaler 8 + return 8 * top * 1000 / uint64(CPUFrequency()/1e6) + case 3: // prescaler 64 + return 64 * top * 1000 / uint64(CPUFrequency()/1e6) + case 4: // prescaler 256 + return 256 * top * 1000 / uint64(CPUFrequency()/1e6) + case 5: // prescaler 1024 + return 1024 * top * 1000 / uint64(CPUFrequency()/1e6) + default: // unknown clock source + return 0 + } +} + +// Channel returns a PWM channel for the given pin. +func (pwm PWM) Channel(pin Pin) (uint8, error) { + pin.Configure(PinConfig{Mode: PinOutput}) + pin.Low() + switch pwm.num { + case 0: + switch pin { + case PD6: // channel A + avr.TCCR0A.SetBits(avr.TCCR0A_COM0A1) + return 0, nil + case PD5: // channel B + avr.TCCR0A.SetBits(avr.TCCR0A_COM0B1) + return 1, nil + } + case 1: + switch pin { + case PB1: // channel A + avr.TCCR1A.SetBits(avr.TCCR1A_COM1A1) + return 0, nil + case PB2: // channel B + avr.TCCR1A.SetBits(avr.TCCR1A_COM1B1) + return 1, nil + } + case 2: + switch pin { + case PB3: // channel A + avr.TCCR2A.SetBits(avr.TCCR2A_COM2A1) + return 0, nil + case PD3: // channel B + avr.TCCR2A.SetBits(avr.TCCR2A_COM2B1) + return 1, nil + } + } + 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%. +// +// Note: the invert state may not be applied on the AVR until the next call to +// ch.Set(). +func (pwm PWM) SetInverting(channel uint8, inverting bool) { + switch pwm.num { + case 0: + switch channel { + case 0: // channel A + if inverting { + avr.PORTB.SetBits(1 << 6) // PB6 high + avr.TCCR0A.SetBits(avr.TCCR0A_COM0A0) + } else { + avr.PORTB.ClearBits(1 << 6) // PB6 low + avr.TCCR0A.ClearBits(avr.TCCR0A_COM0A0) + } + case 1: // channel B + if inverting { + avr.PORTB.SetBits(1 << 5) // PB5 high + avr.TCCR0A.SetBits(avr.TCCR0A_COM0B0) + } else { + avr.PORTB.ClearBits(1 << 5) // PB5 low + avr.TCCR0A.ClearBits(avr.TCCR0A_COM0B0) + } + } + case 1: + // Note: the COM1A0/COM1B0 bit is not set with the configuration below. + // It will be set the following call to Set(), however. + switch channel { + case 0: // channel A, PB1 + if inverting { + avr.PORTB.SetBits(1 << 1) // PB1 high + } else { + avr.PORTB.ClearBits(1 << 1) // PB1 low + } + case 1: // channel B, PB2 + if inverting { + avr.PORTB.SetBits(1 << 2) // PB2 high + } else { + avr.PORTB.ClearBits(1 << 2) // PB2 low + } + } + case 2: + switch channel { + case 0: // channel A + if inverting { + avr.PORTB.SetBits(1 << 3) // PB3 high + avr.TCCR2A.SetBits(avr.TCCR2A_COM2A0) + } else { + avr.PORTB.ClearBits(1 << 3) // PB3 low + avr.TCCR2A.ClearBits(avr.TCCR2A_COM2A0) + } + case 1: // channel B + if inverting { + avr.PORTD.SetBits(1 << 3) // PD3 high + avr.TCCR2A.SetBits(avr.TCCR2A_COM2B0) + } else { + avr.PORTD.ClearBits(1 << 3) // PD3 low + avr.TCCR2A.ClearBits(avr.TCCR2A_COM2B0) + } + } + } +} + +// 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: +// +// pwm.Set(channel, pwm.Top() / 4) +// +// pwm.Set(channel, 0) will set the output to low and pwm.Set(channel, +// pwm.Top()) will set the output to high, assuming the output isn't inverted. +func (pwm PWM) Set(channel uint8, value uint32) { + switch pwm.num { + case 0: + value := uint16(value) + switch channel { + case 0: // channel A + if value == 0 { + avr.TCCR0A.ClearBits(avr.TCCR0A_COM0A1) + } else { + avr.OCR0A.Set(uint8(value - 1)) + avr.TCCR0A.SetBits(avr.TCCR0A_COM0A1) + } + case 1: // channel B + if value == 0 { + avr.TCCR0A.ClearBits(avr.TCCR0A_COM0B1) + } else { + avr.OCR0B.Set(uint8(value) - 1) + avr.TCCR0A.SetBits(avr.TCCR0A_COM0B1) + } + } + case 1: + mask := interrupt.Disable() + switch channel { + case 0: // channel A, PB1 + if value == 0 { + avr.TCCR1A.ClearBits(avr.TCCR1A_COM1A1 | avr.TCCR1A_COM1A0) + } else { + value := uint16(value) - 1 // yes, this is safe (it relies on underflow) + avr.OCR1AH.Set(uint8(value >> 8)) + avr.OCR1AL.Set(uint8(value)) + if avr.PORTB.HasBits(1 << 1) { // is PB1 high? + // Yes, set the inverting bit. + avr.TCCR1A.SetBits(avr.TCCR1A_COM1A1 | avr.TCCR1A_COM1A0) + } else { + // No, output is non-inverting. + avr.TCCR1A.SetBits(avr.TCCR1A_COM1A1) + } + } + case 1: // channel B, PB2 + if value == 0 { + avr.TCCR1A.ClearBits(avr.TCCR1A_COM1B1 | avr.TCCR1A_COM1B0) + } else { + value := uint16(value) - 1 // yes, this is safe (it relies on underflow) + avr.OCR1BH.Set(uint8(value >> 8)) + avr.OCR1BL.Set(uint8(value)) + if avr.PORTB.HasBits(1 << 2) { // is PB2 high? + // Yes, set the inverting bit. + avr.TCCR1A.SetBits(avr.TCCR1A_COM1B1 | avr.TCCR1A_COM1B0) + } else { + // No, output is non-inverting. + avr.TCCR1A.SetBits(avr.TCCR1A_COM1B1) + } + } + } + interrupt.Restore(mask) + case 2: + value := uint16(value) + switch channel { + case 0: // channel A + if value == 0 { + avr.TCCR2A.ClearBits(avr.TCCR2A_COM2A1) + } else { + avr.OCR2A.Set(uint8(value - 1)) + avr.TCCR2A.SetBits(avr.TCCR2A_COM2A1) + } + case 1: // channel B + if value == 0 { + avr.TCCR2A.ClearBits(avr.TCCR2A_COM2B1) + } else { + avr.OCR2B.Set(uint8(value - 1)) + avr.TCCR2A.SetBits(avr.TCCR2A_COM2B1) + } + } } } diff --git a/src/machine/machine_atsamd21.go b/src/machine/machine_atsamd21.go index c01b69b9..8915211c 100644 --- a/src/machine/machine_atsamd21.go +++ b/src/machine/machine_atsamd21.go @@ -31,8 +31,8 @@ const ( PinInput PinMode = 9 PinInputPullup PinMode = 10 PinOutput PinMode = 11 - PinPWM PinMode = PinTimer - PinPWMAlt PinMode = PinTimerAlt + PinTCC PinMode = PinTimer + PinTCCAlt PinMode = PinTimerAlt PinInputPulldown PinMode = 12 ) @@ -1421,201 +1421,373 @@ func (spi SPI) txrx24mhz(tx, rx []byte) { rx[len(rx)-1] = byte(spi.Bus.DATA.Get()) } -// PWM -const period = 0xFFFF +// TCC is one timer/counter 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 TCC +// peripheral at once. +type TCC sam.TCC_Type -// InitPWM initializes the PWM interface. -func InitPWM() { - // turn on timer clocks used for PWM - sam.PM.APBCMASK.SetBits(sam.PM_APBCMASK_TCC0_ | sam.PM_APBCMASK_TCC1_ | sam.PM_APBCMASK_TCC2_) +// The SAM D21 has three TCC peripherals, which have PWM as one feature. +var ( + TCC0 = (*TCC)(sam.TCC0) + TCC1 = (*TCC)(sam.TCC1) + TCC2 = (*TCC)(sam.TCC2) +) - // Use GCLK0 for TCC0/TCC1 - sam.GCLK.CLKCTRL.Set((sam.GCLK_CLKCTRL_ID_TCC0_TCC1 << sam.GCLK_CLKCTRL_ID_Pos) | - (sam.GCLK_CLKCTRL_GEN_GCLK0 << sam.GCLK_CLKCTRL_GEN_Pos) | - sam.GCLK_CLKCTRL_CLKEN) - for sam.GCLK.STATUS.HasBits(sam.GCLK_STATUS_SYNCBUSY) { - } - - // Use GCLK0 for TCC2/TC3 - sam.GCLK.CLKCTRL.Set((sam.GCLK_CLKCTRL_ID_TCC2_TC3 << sam.GCLK_CLKCTRL_ID_Pos) | - (sam.GCLK_CLKCTRL_GEN_GCLK0 << sam.GCLK_CLKCTRL_GEN_Pos) | - sam.GCLK_CLKCTRL_CLKEN) - for sam.GCLK.STATUS.HasBits(sam.GCLK_STATUS_SYNCBUSY) { - } +//go:inline +func (tcc *TCC) timer() *sam.TCC_Type { + return (*sam.TCC_Type)(tcc) } -// Configure configures a PWM pin for output. -func (pwm PWM) Configure() error { - // figure out which TCCX timer for this pin - timer := pwm.getTimer() - if timer == nil { - return ErrInvalidOutputPin +// Configure enables and configures this TCC. +func (tcc *TCC) Configure(config PWMConfig) error { + // Enable the clock source for this timer. + switch tcc.timer() { + case sam.TCC0: + sam.PM.APBCMASK.SetBits(sam.PM_APBCMASK_TCC0_) + // Use GCLK0 for TCC0/TCC1 + sam.GCLK.CLKCTRL.Set((sam.GCLK_CLKCTRL_ID_TCC0_TCC1 << sam.GCLK_CLKCTRL_ID_Pos) | + (sam.GCLK_CLKCTRL_GEN_GCLK0 << sam.GCLK_CLKCTRL_GEN_Pos) | + sam.GCLK_CLKCTRL_CLKEN) + for sam.GCLK.STATUS.HasBits(sam.GCLK_STATUS_SYNCBUSY) { + } + case sam.TCC1: + sam.PM.APBCMASK.SetBits(sam.PM_APBCMASK_TCC1_) + // Use GCLK0 for TCC0/TCC1 + sam.GCLK.CLKCTRL.Set((sam.GCLK_CLKCTRL_ID_TCC0_TCC1 << sam.GCLK_CLKCTRL_ID_Pos) | + (sam.GCLK_CLKCTRL_GEN_GCLK0 << sam.GCLK_CLKCTRL_GEN_Pos) | + sam.GCLK_CLKCTRL_CLKEN) + for sam.GCLK.STATUS.HasBits(sam.GCLK_STATUS_SYNCBUSY) { + } + case sam.TCC2: + sam.PM.APBCMASK.SetBits(sam.PM_APBCMASK_TCC2_) + // Use GCLK0 for TCC2/TC3 + sam.GCLK.CLKCTRL.Set((sam.GCLK_CLKCTRL_ID_TCC2_TC3 << sam.GCLK_CLKCTRL_ID_Pos) | + (sam.GCLK_CLKCTRL_GEN_GCLK0 << sam.GCLK_CLKCTRL_GEN_Pos) | + sam.GCLK_CLKCTRL_CLKEN) + for sam.GCLK.STATUS.HasBits(sam.GCLK_STATUS_SYNCBUSY) { + } } - // disable timer - timer.CTRLA.ClearBits(sam.TCC_CTRLA_ENABLE) - // Wait for synchronization - for timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_ENABLE) { - } + // 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) // 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) { + tcc.timer().WAVE.Set(sam.TCC_WAVE_WAVEGEN_NPWM) + + // Wait for synchronization of all changed 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) { + // Set the period and prescaler. + err := tcc.setPeriod(config.Period, true) + + // Enable the timer. + tcc.timer().CTRLA.SetBits(sam.TCC_CTRLA_ENABLE) + + // Wait for synchronization of all changed registers. + for tcc.timer().SYNCBUSY.Get() != 0 { } - // Set pin as output - sam.PORT.DIRSET0.Set(1 << uint8(pwm.Pin)) - // Set pin to low - sam.PORT.OUTCLR0.Set(1 << uint8(pwm.Pin)) + // Return any error that might have occured in the tcc.setPeriod call. + return err +} - // Enable the port multiplexer for pin - pwm.setPinCfg(sam.PORT_PINCFG0_PMUXEN) - - // Connect TCCX timer to pin. - // we normally use the F channel aka ALT - pwmConfig := PinPWMAlt - - // in the case of PA6 or PA7 we have to use E channel - if pwm.Pin == 6 || pwm.Pin == 7 { - pwmConfig = PinPWM +// 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 { + err := tcc.setPeriod(period, false) + if err == nil { + if tcc.Counter() >= tcc.Top() { + // When setting the timer to a shorter period, there is a chance + // that it passes the counter value and thus goes all the way to MAX + // before wrapping back to zero. + // To avoid this, reset the counter back to 0. + tcc.timer().COUNT.Set(0) + } } + return err +} - if pwm.Pin&1 > 0 { - // odd pin, so save the even pins - val := pwm.getPMux() & sam.PORT_PMUX0_PMUXE_Msk - pwm.setPMux(val | uint8(pwmConfig<> 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 period (the counter top). + tcc.timer().PER.Set(uint32(top) - 1) + + // Wait for synchronization of CTRLA.PRESCALER and PER registers. + for tcc.timer().SYNCBUSY.Get() != 0 { } return nil } -// 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 +// 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 Set +// (see Set documentation for more information). +func (tcc *TCC) Top() uint32 { + return tcc.timer().PER.Get() + 1 +} + +// 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() +} + +// Some constans to make pinTimerMapping below easier to read. +const ( + pinTCC0 = 1 + pinTCC1 = 2 + pinTCC2 = 3 + pinTimerCh0 = 0 << 3 + pinTimerCh2 = 1 << 3 + pinTCC0Ch0 = pinTCC0 | pinTimerCh0 + pinTCC0Ch2 = pinTCC0 | pinTimerCh2 + pinTCC1Ch0 = pinTCC1 | pinTimerCh0 + pinTCC1Ch2 = pinTCC1 | pinTimerCh2 + pinTCC2Ch0 = pinTCC2 | pinTimerCh0 +) + +// Mapping from pin number to TCC peripheral and channel using a special +// encoding. Note that only TCC0-TCC2 are included, not TC3 and up. +// Every byte is split in two nibbles where the low nibble describes PinTCC and +// the high nibble describes PinTCCAlt. Within a nibble, there is one bit that +// indicates Ch0/Ch1 or Ch2/Ch3, and three other bits that contain the TCC +// peripheral number plus one (to distinguish between TCC0Ch0 and 0). +// +// The encoding can be so compact because all pins are configured in pairs, so +// if you know PA00 you can infer the configuration of PA01. And only channel 0 +// or 2 need to be included (taking up just one bit), because channel 0 and 2 +// are only ever used on odd pins and channel 1 and 3 on even pins, again using +// the pin pair pattern to reduce the amount of information needed to be stored. +// +// Datasheet: https://cdn.sparkfun.com/datasheets/Dev/Arduino/Boards/Atmel-42181-SAM-D21_Datasheet.pdf +var pinTimerMapping = [...]uint8{ + // page 21 + PA00 / 2: pinTCC2Ch0 | 0, + PA04 / 2: pinTCC0Ch0 | 0, + PA06 / 2: pinTCC1Ch0 | 0, + PA08 / 2: pinTCC0Ch0 | pinTCC1Ch2<<4, + PA10 / 2: pinTCC1Ch0 | pinTCC0Ch2<<4, + // page 22 + PB10 / 2: 0 | pinTCC0Ch0<<4, + PB12 / 2: 0 | pinTCC0Ch2<<4, + PA12 / 2: pinTCC2Ch0 | pinTCC0Ch2<<4, + PA14 / 2: 0 | pinTCC0Ch0<<4, + PA16 / 2: pinTCC2Ch0 | pinTCC0Ch2<<4, + PA18 / 2: 0 | pinTCC0Ch2<<4, + PB16 / 2: 0 | pinTCC0Ch0<<4, + PA20 / 2: 0 | pinTCC0Ch2<<4, + PA22 / 2: 0 | pinTCC0Ch0<<4, + PA24 / 2: 0 | pinTCC1Ch2<<4, + // page 23 + PA30 / 2: 0 | pinTCC1Ch0<<4, + PB30 / 2: pinTCC0Ch0 | pinTCC1Ch2<<4, +} + +// findPinPadMapping returns the pin mode (PinTCC or PinTCCAlt) 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) { + mapping := pinTimerMapping[pin/2] + // evenChannel below indicates the channel 0 or 2, for the even part of the + // pin pair. The next pin will also have the next channel (1 or 3). + if mapping&0x07 == timer+1 { + // PWM output is on peripheral function E. + evenChannel := ((mapping >> 3) & 1) * 2 + return PinTCC, evenChannel + uint8(pin&1) + } + if (mapping&0x70)>>4 == timer+1 { + // PWM output is on peripheral function F. + evenChannel := ((mapping >> 7) & 1) * 2 + return PinTCCAlt, evenChannel + uint8(pin&1) + } + 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 peripheral or consider using a +// different pin. +func (tcc *TCC) Channel(pin Pin) (uint8, error) { + var pinMode PinMode + var channel uint8 + switch tcc.timer() { + case sam.TCC0: + pinMode, channel = findPinTimerMapping(0, pin) + case sam.TCC1: + pinMode, channel = findPinTimerMapping(1, pin) + case sam.TCC2: + pinMode, channel = findPinTimerMapping(2, pin) } - // disable output - timer.CTRLA.ClearBits(sam.TCC_CTRLA_ENABLE) - - // Wait for synchronization - for timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_ENABLE) { + if pinMode == 0 { + // No pin could be found. + return 0, ErrInvalidOutputPin } + // Enable the port multiplexer for pin + pin.setPinCfg(sam.PORT_PINCFG0_PMUXEN) + + if pin&1 > 0 { + // odd pin, so save the even pins + val := pin.getPMux() & sam.PORT_PMUX0_PMUXE_Msk + pin.setPMux(val | uint8(pinMode< 0 { - // odd pin, so save the even pins - val := pwm.getPMux() & sam.PORT_GROUP_PMUX_PMUXE_Msk - pwm.setPMux(val | uint8(pwmConfig<CTRLA.reg = TCC_CTRLA_PRESCALER_DIV256 | TCC_CTRLA_PRESCSYNC_GCLK; - timer.CTRLA.SetBits(sam.TCC_CTRLA_PRESCALER_DIV256 | sam.TCC_CTRLA_PRESCSYNC_GCLK) + // 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) // 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) { + tcc.timer().WAVE.Set(sam.TCC_WAVE_WAVEGEN_NPWM) + + // 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) { + // Set the period and prescaler. + err := tcc.setPeriod(config.Period, true) + + // Enable the timer. + tcc.timer().CTRLA.SetBits(sam.TCC_CTRLA_ENABLE) + + // Wait for synchronization of all changed registers. + for tcc.timer().SYNCBUSY.Get() != 0 { } - // Set the initial value - // TCCx->CC[tcChannel].reg = (uint32_t) value; - pwm.setChannel(timer, 0) + // Return any error that might have occured in the tcc.setPeriod call. + return err +} - for timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_CC0) || - timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_CC1) { +// 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 } - // 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) { + 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 } - // enable timer - timer.CTRLA.SetBits(sam.TCC_CTRLA_ENABLE) - // Wait for synchronization - for timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_ENABLE) { + 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 period (the counter top). + tcc.timer().PER.Set(uint32(top) - 1) + + // Wait for synchronization of CTRLA.PRESCALER and PER registers. + for tcc.timer().SYNCBUSY.Get() != 0 { } return nil } -// 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 +// 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 +} + +// 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() +} + +// 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}, +} + +// 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) { - } - for timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_CC0) || - timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_CC1) { + 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 } - // TCCx->CCBUF[tcChannel].reg = (uint32_t) value; - pwm.setChannelBuffer(timer, uint32(value)) - - 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->CTRLBCLR.bit.LUPD = 1; - timer.CTRLBCLR.SetBits(sam.TCC_CTRLBCLR_LUPD) - for timer.SYNCBUSY.HasBits(sam.TCC_SYNCBUSY_CTRLB) { + // 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) + + if pinMode == 0 { + // No pin could be found. + return 0, ErrInvalidOutputPin + } + + // 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 + } + + // Enable the port multiplexer for pin + pin.setPinCfg(sam.PORT_GROUP_PINCFG_PMUXEN) + + // 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<> 2) | 0x8000 // set bit 15 to invert polarity +// 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) +} - p := pwms[i] +func (pwm *PWM) setPeriod(period uint64, updatePrescaler bool) error { + const maxTop = 0x7fff // 15 bits counter - 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) + // 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 + } - break + // 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 +} + +// 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() +} + +// 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) } diff --git a/src/machine/pwm.go b/src/machine/pwm.go new file mode 100644 index 00000000..06d61b9a --- /dev/null +++ b/src/machine/pwm.go @@ -0,0 +1,21 @@ +package machine + +import "errors" + +var ( + ErrPWMPeriodTooLong = errors.New("pwm: period too long") +) + +// PWMConfig allows setting some configuration while configuring a PWM +// peripheral. A zero PWMConfig is ready to use for simple applications such as +// dimming LEDs. +type PWMConfig struct { + // PWM period in nanosecond. Leaving this zero will pick a reasonable period + // value for use with LEDs. + // If you want to configure a frequency instead of a period, you can use the + // following formula to calculate a period from a frequency: + // + // period = 1e9 / frequency + // + Period uint64 +}