diff --git a/src/examples/pwm/pico.go b/src/examples/pwm/pico.go new file mode 100644 index 00000000..0c0c0a83 --- /dev/null +++ b/src/examples/pwm/pico.go @@ -0,0 +1,11 @@ +// +build pico + +package main + +import "machine" + +var ( + pwm = machine.PWM4 // Pin 25 (LED on pico) corresponds to PWM4. + pinA = machine.LED + pinB = machine.GPIO24 +) diff --git a/src/machine/machine_rp2040_gpio.go b/src/machine/machine_rp2040_gpio.go index 5e3d06cc..3aea11e8 100644 --- a/src/machine/machine_rp2040_gpio.go +++ b/src/machine/machine_rp2040_gpio.go @@ -81,6 +81,7 @@ const ( PinInputPullup PinAnalog PinUART + PinPWM PinI2C PinSPI ) @@ -181,6 +182,8 @@ func (p Pin) Configure(config PinConfig) { p.pulloff() case PinUART: p.setFunc(fnUART) + case PinPWM: + p.setFunc(fnPWM) case PinI2C: // IO config according to 4.3.1.3 of rp2040 datasheet. p.setFunc(fnI2C) diff --git a/src/machine/machine_rp2040_pwm.go b/src/machine/machine_rp2040_pwm.go new file mode 100644 index 00000000..e0ecc987 --- /dev/null +++ b/src/machine/machine_rp2040_pwm.go @@ -0,0 +1,384 @@ +// +build rp2040 + +package machine + +import ( + "device/rp" + "errors" + "runtime/volatile" + "unsafe" +) + +var ( + ErrPeriodTooBig = errors.New("period outside valid range 1..4e9ns") +) + +const ( + maxPWMPins = 29 +) + +// pwmGroup is one PWM peripheral, which consists of a counter and two output +// channels. You can set the frequency using SetPeriod, +// but only for all the channels in this PWM peripheral at once. +// +// div: integer value to reduce counting rate by. Must be greater than or equal to 1. +// +// cc: counter compare level. Contains 2 channel levels. The 16 LSBs are Channel A's level (Duty Cycle) +// and the 16 MSBs are Channel B's level. +// +// top: Wrap. Highest number counter will reach before wrapping over. usually 0xffff. +// +// csr: Clock mode. PWM_CH0_CSR_DIVMODE_xxx registers have 4 possible modes, of which Free-running is used. +// csr contains output polarity bit at PWM_CH0_CSR_x_INV where x is the channel. +// csr contains phase correction bit at PWM_CH0_CSR_PH_CORRECT_Msk. +// csr contains PWM enable bit at PWM_CH0_CSR_EN. If not enabled PWM will not be active. +// +// ctr: PWM counter value. +type pwmGroup struct { + CSR volatile.Register32 + DIV volatile.Register32 + CTR volatile.Register32 + CC volatile.Register32 + TOP volatile.Register32 +} + +// Equivalent of +// var pwmSlice []pwmGroup = (*[8]pwmGroup)(unsafe.Pointer(rp.PWM))[:] +// return &pwmSlice[index] +// 0x14 is the size of a pwmGroup. +func getPWMGroup(index uintptr) *pwmGroup { + return (*pwmGroup)(unsafe.Pointer(uintptr(unsafe.Pointer(rp.PWM)) + 0x14*index)) +} + +// PWM peripherals available on RP2040. Each peripheral has 2 pins available for +// a total of 16 available PWM outputs. Some pins may not be available on some boards. +var ( + PWM0 = getPWMGroup(0) + PWM1 = getPWMGroup(1) + PWM2 = getPWMGroup(2) + PWM3 = getPWMGroup(3) + PWM4 = getPWMGroup(4) + PWM5 = getPWMGroup(5) + PWM6 = getPWMGroup(6) + PWM7 = getPWMGroup(7) +) + +// Configure enables and configures this PWM. +func (pwm *pwmGroup) Configure(config PWMConfig) error { + return pwm.init(config, true) +} + +// Channel returns a PWM channel for the given pin. If pin does +// not belong to PWM peripheral ErrInvalidOutputPin error is returned. +// It also configures pin as PWM output. +func (pwm *pwmGroup) Channel(pin Pin) (channel uint8, err error) { + if pin > maxPWMPins || pwmGPIOToSlice(pin) != pwm.peripheral() { + return 3, ErrInvalidOutputPin + } + pin.Configure(PinConfig{PinPWM}) + return pwmGPIOToChannel(pin), nil +} + +// Peripheral returns the RP2040 PWM peripheral which ranges from 0 to 7. Each +// PWM peripheral has 2 channels, A and B which correspond to 0 and 1 in the program. +// This number corresponds to the package's PWM0 throughout PWM7 handles +func PWMPeripheral(pin Pin) (sliceNum uint8, err error) { + if pin > maxPWMPins { + return 0, ErrInvalidOutputPin + } + return pwmGPIOToSlice(pin), nil +} + +// returns the number of the pwm peripheral (0-7) +func (pwm *pwmGroup) peripheral() uint8 { + return uint8((uintptr(unsafe.Pointer(pwm)) - uintptr(unsafe.Pointer(rp.PWM))) / 0x14) +} + +// 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 (p *pwmGroup) SetPeriod(period uint64) error { + if period > 0xffff_ffff { + return ErrPeriodTooBig + } + if period == 0 { + period = 1e5 + } + p.setPeriod(period) + return nil +} + +// Top returns the current counter top, for use in duty cycle calculation. +// +// 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 (p *pwmGroup) Top() uint32 { + return p.getWrap() +} + +// Counter returns the current counter value of the timer in this PWM +// peripheral. It may be useful for debugging. +func (p *pwmGroup) Counter() uint32 { + return (p.CTR.Get() & rp.PWM_CH0_CTR_CH0_CTR_Msk) >> rp.PWM_CH0_CTR_CH0_CTR_Pos +} + +// Period returns the used PWM period in nanoseconds. It might deviate slightly +// from the configured period due to rounding. +func (p *pwmGroup) Period() uint64 { + periodPerCycle := getPeriod() + top := p.getWrap() + phc := p.getPhaseCorrect() + Int, frac := p.getClockDiv() + return uint64((Int + frac/16) * (top + 1) * (phc + 1) * periodPerCycle) // cycles = (TOP+1) * (CSRPHCorrect + 1) * (DIV_INT + DIV_FRAC/16) +} + +// 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 (p *pwmGroup) SetInverting(channel uint8, inverting bool) { + channel &= 1 + p.setInverting(channel, inverting) +} + +// 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 (p *pwmGroup) Set(channel uint8, value uint32) { + val := uint16(value) + channel &= 1 + p.setChanLevel(channel, val) +} + +// Get current level (last set by Set). Default value on initialization is 0. +func (p *pwmGroup) Get(channel uint8) (value uint32) { + channel &= 1 + return uint32(p.getChanLevel(channel)) +} + +// SetTop sets TOP control register. Max value is 16bit (0xffff). +func (p *pwmGroup) SetTop(top uint32) { + p.setWrap(uint16(top)) +} + +// Enable enables or disables PWM peripheral channels. +func (p *pwmGroup) Enable(enable bool) { + p.enable(enable) +} + +// IsEnabled returns true if peripheral is enabled. +func (p *pwmGroup) IsEnabled() (enabled bool) { + return (p.CSR.Get()&rp.PWM_CH0_CSR_EN_Msk)>>rp.PWM_CH0_CSR_EN_Pos != 0 +} + +// Hardware Pulse Width Modulation (PWM) API +// +// The RP2040 PWM block has 8 identical slices. Each slice can drive two PWM output signals, or +// measure the frequency or duty cycle of an input signal. This gives a total of up to 16 controllable +// PWM outputs. All 30 GPIOs can be driven by the PWM block +// +// The PWM hardware functions by continuously comparing the input value to a free-running counter. This produces a +// toggling output where the amount of time spent at the high output level is proportional to the input value. The fraction of +// time spent at the high signal level is known as the duty cycle of the signal. +// +// The default behaviour of a PWM slice is to count upward until the wrap value (\ref pwm_config_set_wrap) is reached, and then +// immediately wrap to 0. PWM slices also offer a phase-correct mode, where the counter starts to count downward after +// reaching TOP, until it reaches 0 again. +type pwms struct { + slice pwmGroup + hw *rp.PWM_Type +} + +// Handle to all pwm peripheral registers. +var _PWM = pwms{ + hw: rp.PWM, +} + +// Initialise a PWM with settings from a configuration object. +// If start is true then PWM starts on initialization. +func (pwm *pwmGroup) init(config PWMConfig, start bool) error { + // Not enable Phase correction + pwm.setPhaseCorrect(false) + + // Clock mode set by default to Free running + pwm.setDivMode(rp.PWM_CH0_CSR_DIVMODE_DIV) + + // Set Output polarity (false/false) + pwm.setInverting(0, false) + pwm.setInverting(1, false) + + // Set wrap. The highest value the counter will reach before returning to zero, also known as TOP. + pwm.setWrap(0xffff) + // period is set after TOP (Wrap). + err := pwm.SetPeriod(config.Period) + if err != nil { + return err + } + // period already set beforea + // Reset counter and compare (pwm level set to zero) + pwm.CTR.ReplaceBits(0, rp.PWM_CH0_CTR_CH0_CTR_Msk, 0) // PWM_CH0_CTR_RESET + pwm.CC.Set(0) // PWM_CH0_CC_RESET + + pwm.enable(start) + return nil +} + +func (pwm *pwmGroup) setPhaseCorrect(correct bool) { + pwm.CSR.ReplaceBits(boolToBit(correct)< cycles = target_period/period_per_cycle + Int := targetPeriod/((1+phc)*periodPerCycle*(1+top)) - frac/16 + if Int > 0xff { + Int = 0xff + } + pwm.setClockDiv(uint8(Int), 0) +} + +// 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). +// frac's (DIV_FRAC) default value on reset is 0. Max value for frac is 15 (4 bits). This is known as a fixed-point +// fractional number. +// +// cycles = (TOP+1) * (CSRPHCorrect + 1) * (DIV_INT + DIV_FRAC/16) +func (pwm *pwmGroup) setClockDiv(Int, frac uint8) { + pwm.DIV.ReplaceBits((uint32(frac)<> pos) + return level +} + +func (pwm *pwmGroup) getWrap() (top uint32) { + return (pwm.TOP.Get() & rp.PWM_CH0_TOP_CH0_TOP_Msk) >> rp.PWM_CH0_TOP_CH0_TOP_Pos +} + +func (pwm *pwmGroup) getPhaseCorrect() (phCorrect uint32) { + return (pwm.CSR.Get() & rp.PWM_CH0_CSR_PH_CORRECT_Msk) >> rp.PWM_CH0_CSR_PH_CORRECT_Pos +} + +func (pwm *pwmGroup) getClockDiv() (Int, frac uint32) { + div := pwm.DIV.Get() + 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 +} + +// pwmGPIOToSlice Determine the PWM channel that is attached to the specified GPIO. +// gpio must be less than 30. Returns the PWM slice number that controls the specified GPIO. +func pwmGPIOToSlice(gpio Pin) (slicenum uint8) { + return (uint8(gpio) >> 1) & 7 +} + +// Determine the PWM channel that is attached to the specified GPIO. +// Each slice 0 to 7 has two channels, A and B. +func pwmGPIOToChannel(gpio Pin) (channel uint8) { + return uint8(gpio) & 1 +} + +// Returns the period of a clock cycle for the raspberry pi pico in nanoseconds. +func getPeriod() uint32 { + const periodIn uint32 = 1e9 / (125 * MHz) + return periodIn +}