feat: make ws281x work next to PWM based fan speed control

Signed-off-by: Matthias Riegler <me@xvzf.tech>
This commit is contained in:
Matthias Riegler
2023-07-07 19:28:07 +02:00
parent 8d5eb4349b
commit 906f56fe24
2 changed files with 184 additions and 65 deletions

View File

@@ -28,11 +28,34 @@ const (
GPFSEL2 = 0x02
PWM_CTL = 0x00
PWM_STA = 0x01
PWM_DMAC = 0x02
PWM_RNG1 = 0x04
PWM_DAT1 = 0x05
PWM_FIF1 = 0x06
PWMCLK_CNTL = 40
PWMCLK_DIV = 41
PWM_CTL_PWEN2 = 8 // Enable (pwm2)
PWM_CTL_CLRF1 = 6 // Clear FIFO
PWM_CTL_MSEN1 = 7 // Use M/S algorithm
PWM_CTL_USEF1 = 5 // Use FIFO
PWM_CTL_POLA1 = 4 // Invert polarity
PWM_CTL_SBIT1 = 3 // Line level when not transmitting
PWM_CTL_RPTL1 = 2 // Repeat last data when FIFO is empty
PWM_CTL_MODE1 = 1 // Mode; 0: PWM, 1: Serializer
PWM_CTL_PWEN1 = 0 // Enable (pwm1)
PWM_STA_STA1 = 9 // Status
PWM_STA_BERR = 8 // Bus Error
PWM_STA_GAPO1 = 4 // Gap detected
PWM_STA_RERR1 = 3 // FIFO Read Error
PWM_STA_WERR1 = 2 // FIFO Write Error
PWM_STA_EMPT1 = 1 // FIFO Empty
PWM_STA_FULL1 = 0 // FIFO Full
PWMCLK_CNTL = 0x28
PWMCLK_CNTL_SRC_OSC = 0
PWMCLK_CNTL_ENABLE = 4
PWMCLK_DIV = 0x29
)
type bcm2711bcm struct {
@@ -108,7 +131,7 @@ func (bcm *bcm2711bcm) Close() error {
// Init initialises GPIOs and sets sane defaults
func (bcm *bcm2711bcm) Init() {
bcm.InitGPIO()
bcm.SetFanSpeed(bcm.opts.DefaultFanSpeed)
// bcm.SetFanSpeed(bcm.opts.DefaultFanSpeed)
bcm.SetStealthMode(bcm.opts.DefaultStealthModeEnabled)
}
@@ -126,16 +149,11 @@ func (bcm *bcm2711bcm) InitGPIO() {
// -> GPFSEL2 5:3, output
bcm.gpioMem[GPFSEL2] = (bcm.gpioMem[GPFSEL2] &^ (0b111 << 3)) | (0b001 << 3)
// WS281x Output (GPIO 18)
// -> GPFSEL1 24:26, regular output; it's configured as alt5 whenever pixel data is sent.
// This is not performant but required as the pwm0 peripheral is shared between fan and data line for the LEDs.
bcm.gpioMem[GPFSEL1] = (bcm.gpioMem[GPFSEL1] &^ (0b111 << 24)) | (0b000 << 24)
// FAN PWM output for standard fan unit (GPIO 12)
if bcm.opts.FanUnit == hal.FAN_UNIT_STANDARD {
// -> GPFSEL1 8:6, alt0
bcm.gpioMem[GPFSEL1] = (bcm.gpioMem[GPFSEL1] &^ (0b111 << 6)) | (0b100 << 6)
bcm.setupFanPwm0()
bcm.setFanSpeedPWM(bcm.opts.DefaultFanSpeed)
}
// FAN TACH input for standard fan unit (GPIO 13)
@@ -144,43 +162,50 @@ func (bcm *bcm2711bcm) InitGPIO() {
bcm.gpioMem[GPFSEL1] = (bcm.gpioMem[GPFSEL1] &^ (0b111 << 9)) | (0b000 << 9)
}
// FIXME add pullup
// Set WS2812 output (GPIO 18)
// -> GPFSEL1 24:26, set as regular output by default. On-demand, it's mapped to pwm0
bcm.gpioMem[GPFSEL1] = (bcm.gpioMem[GPFSEL1] &^ (0b111 << 24)) | (0b000 << 24)
// FIXME add WS2812 GPIO 18
// FIXME add edge button
}
func (bcm *bcm2711bcm) setupFanPwm0() {
func (bcm *bcm2711bcm) setPwm0Freq(targetFrequency uint64) error {
// Calculate PWM divisor based on target frequency
divisor := 54000000 / targetFrequency
realDivisor := divisor & 0xfff // 12 bits
if divisor != realDivisor {
return errors.New("invalid frequency, max divisor is 4095, calculated divisor is " + string(divisor))
}
// Stop pwm for both channels; this is required to set the new configuration
bcm.pwmMem[PWM_CTL] &^= 1<<8 | 1
bcm.pwmMem[PWM_CTL] &^= (1 << PWM_CTL_PWEN1) | (1 << PWM_CTL_PWEN2)
time.Sleep(time.Microsecond * 10)
// Stop clock w/o any changes, they cannot be made in the same step
bcm.clkMem[PWMCLK_CNTL] = bcm2711ClkManagerPwd | (bcm.clkMem[PWMCLK_CNTL] &^ (1 << 4))
time.Sleep(time.Microsecond * 10)
// Wait for the clock to not be busy so we can perform the changes
for bcm.clkMem[PWMCLK_CNTL]&(1<<7) != 0 {
time.Sleep(time.Microsecond * 20)
time.Sleep(time.Microsecond * 10)
}
// passwd, mash, disabled, source (oscillator)
bcm.clkMem[PWMCLK_CNTL] = bcm2711ClkManagerPwd | (0 << 9) | (0 << 4) | (1 << 0)
// passwd, disabled, source (oscillator)
bcm.clkMem[PWMCLK_CNTL] = bcm2711ClkManagerPwd | (0 << PWMCLK_CNTL_ENABLE) | (1 << PWMCLK_CNTL_SRC_OSC)
time.Sleep(time.Microsecond * 10)
// set PWM freq; the BCM2711 has an oscillator freq of 52 Mhz
// Noctua fans are expecting a 25khz signal, where duty cycle controls fan on/speed/off
// -> we'll need to get ~2.5Mhz of signal resultion in order to incorporate a 0-100 range
// The following settings setup ~2.571Mhz resultion, resulting in a ~25,71khz signal
// lying within the specifications of Noctua fans.
bcm.clkMem[PWMCLK_DIV] = bcm2711ClkManagerPwd | (20 << 12) | (3276 << 0)
bcm.clkMem[PWMCLK_DIV] = bcm2711ClkManagerPwd | (uint32(divisor) << 12)
time.Sleep(time.Microsecond * 10)
// wait for changes to take effect before enabling it.
// Note: 10us seems sufficient on idle systems, but doesn't always work when
time.Sleep(time.Microsecond * 50)
// Start clock (passwd, mash, enable, source)
bcm.clkMem[PWMCLK_CNTL] = bcm2711ClkManagerPwd | (0 << 9) | (1 << 4) | (1 << 0)
// Start clock (passwd, enable, source)
bcm.clkMem[PWMCLK_CNTL] = bcm2711ClkManagerPwd | (1 << PWMCLK_CNTL_ENABLE) | (1 << PWMCLK_CNTL_SRC_OSC)
time.Sleep(time.Microsecond * 10)
// Start pwm for both channels again
bcm.pwmMem[PWM_CTL] &= 1<<8 | 1
bcm.pwmMem[PWM_CTL] &= (1 << PWM_CTL_PWEN1)
time.Sleep(time.Microsecond * 10)
return nil
}
// SetFanSpeed sets the fanspeed of a blade in percent (standard fan unit)
@@ -189,15 +214,34 @@ func (bcm *bcm2711bcm) SetFanSpeed(speed uint8) {
}
func (bcm *bcm2711bcm) setFanSpeedPWM(speed uint8) {
bcm.wrMutex.Lock()
defer bcm.wrMutex.Unlock()
// Noctua fans are expecting a 25khz signal, where duty cycle controls fan on/speed/off
// With the usage of the FIFO, we can alter the duty cycle by the number of bits set in the FIFO, maximum of 32.
// We therefore need a frequency of 32*25khz = 800khz, which is a divisor of 67.5 (thus we'll use 68).
// This results in an actual period frequency of 24.8khz, which is within the specifications of Noctua fans.
err := bcm.setPwm0Freq(800000)
if err != nil {
// we know it produces a valid divisor, so this should never happen
panic(err)
}
// set MSEN=0
bcm.pwmMem[PWM_CTL] = bcm.pwmMem[PWM_CTL]&^(0xff) | (0 << 7) | (1 << 0)
// Using hardware ticks would offer a better resultion, but this works for now.
var targetvalue uint32 = 0
if speed == 0 {
targetvalue = 0
} else if speed <= 100 {
for i := 0; i <= int((float64(speed)/100.0)*32.0); i++ {
targetvalue |= (1 << i)
}
} else {
targetvalue = ^(uint32(0))
}
bcm.pwmMem[PWM_DAT1] = uint32(speed)
bcm.pwmMem[PWM_RNG1] = 100
time.Sleep(3 * time.Microsecond)
// Use fifo, repeat, ...
bcm.pwmMem[PWM_CTL] = (1 << PWM_CTL_PWEN1) | (1 << PWM_CTL_MODE1) | (1 << PWM_CTL_RPTL1) | (1 << PWM_CTL_USEF1)
time.Sleep(10 * time.Microsecond)
bcm.pwmMem[PWM_RNG1] = 32
time.Sleep(10 * time.Microsecond)
bcm.pwmMem[PWM_FIF1] = targetvalue
// Store fan speed for later use
bcm.currFanSpeed = speed
@@ -209,34 +253,6 @@ type LedColor struct {
Blue uint8
}
// SetLEDs sets the color of the WS281x LEDs
func (bcm *bcm2711bcm) SetLEDs(top LedColor, edge LedColor) {
bcm.wrMutex.Lock()
defer bcm.wrMutex.Unlock()
// Restore fan PWM after setting the LEDs & set the GPIO18 as a regular output
defer func() {
bcm.setupFanPwm0()
bcm.setFanSpeedPWM(bcm.currFanSpeed)
}()
// Datarate for WS281x LEDs is 800kHz
// Every bit transmitted takes 3 bits on of buffer (-../--.) thus we need (3*3*8) = 72 bits per LED and therefore 144 bits in total
// ws2812 reset expects 55us of low signal, which is 132bits in the buffer (44 logocal bits) -> will take 55us to transmit -> reset signal
// -> we need 144 + 132 = 276 bits in total, which when rounded up to the next multiple of 8 is 280 bits or 35 bytes
const bufferSize = 35
// Get DMA buffer
// Stop pwm for both channels; this is required to set the new configuration
bcm.pwmMem[PWM_CTL] &^= 1<<8 | 1
// Set GPIO18 to alt5 (PWM0_0)
bcm.gpioMem[GPFSEL1] = (bcm.gpioMem[GPFSEL1] &^ (0b111 << 24)) | (0b010 << 24)
// Start pwm for both channels again
bcm.pwmMem[PWM_CTL] &= 1<<8 | 1
}
func (bcm *bcm2711bcm) SetStealthMode(enable bool) {
bcm.wrMutex.Lock()
defer bcm.wrMutex.Unlock()
@@ -249,3 +265,68 @@ func (bcm *bcm2711bcm) SetStealthMode(enable bool) {
bcm.gpioMem[10] = 1 << (bcm2711StealthPin)
}
}
// serializePwmDataFrame converts a byte to a 24 bit PWM data frame for WS281x LEDs
func serializePwmDataFrame(data uint8) uint32 {
var result uint32 = 0
for i := 7; i >= 0; i-- {
if i != 7 {
result <<= 3
}
if (uint32(data)&(1<<i))>>i == 0 {
result |= 0b100 // -__
} else {
result |= 0b110 // --_
}
}
return result
}
// SetLEDs sets the color of the WS281x LEDs
func (bcm *bcm2711bcm) SetLEDs(top LedColor, edge LedColor) {
bcm.wrMutex.Lock()
defer bcm.wrMutex.Unlock()
// Set frequency to 3*800khz.
// we'll bit-bang the data, so we'll need to send 3 bits per bit of data.
bcm.setPwm0Freq(3 * 800000)
time.Sleep(10 * time.Microsecond)
// WS281x Output (GPIO 18)
// -> GPFSEL1 24:26, regular output; it's configured as alt5 whenever pixel data is sent.
// This is not optimal but required as the pwm0 peripheral is shared between fan and data line for the LEDs.
time.Sleep(10 * time.Microsecond)
bcm.gpioMem[GPFSEL1] = (bcm.gpioMem[GPFSEL1] &^ (0b111 << 24)) | (0b010 << 24)
time.Sleep(10 * time.Microsecond)
defer func() {
// Set to regular output again so the PWM signal doesn't confuse the WS2812
bcm.gpioMem[GPFSEL1] = (bcm.gpioMem[GPFSEL1] &^ (0b111 << 24)) | (0b000 << 24)
bcm.setFanSpeedPWM(bcm.currFanSpeed)
}()
bcm.pwmMem[PWM_CTL] = (1 << PWM_CTL_MODE1) | (1 << PWM_CTL_RPTL1) | (0 << PWM_CTL_SBIT1) | (1 << PWM_CTL_USEF1) | (1 << PWM_CTL_CLRF1)
time.Sleep(10 * time.Microsecond)
bcm.pwmMem[PWM_RNG1] = 32
time.Sleep(10 * time.Microsecond)
// Add sufficient padding to clear
bcm.pwmMem[PWM_FIF1] = 0
bcm.pwmMem[PWM_FIF1] = 0
bcm.pwmMem[PWM_FIF1] = 0
// Write top LED data
bcm.pwmMem[PWM_FIF1] = serializePwmDataFrame(top.Red)
bcm.pwmMem[PWM_FIF1] = serializePwmDataFrame(top.Green)
bcm.pwmMem[PWM_FIF1] = serializePwmDataFrame(top.Blue)
// Write edge LED data
bcm.pwmMem[PWM_FIF1] = serializePwmDataFrame(edge.Red)
bcm.pwmMem[PWM_FIF1] = serializePwmDataFrame(edge.Green)
bcm.pwmMem[PWM_FIF1] = serializePwmDataFrame(edge.Blue)
// make sure there's >50us of silence
bcm.pwmMem[PWM_FIF1] = 0
bcm.pwmMem[PWM_FIF1] = 0
bcm.pwmMem[PWM_FIF1] = 0
bcm.pwmMem[PWM_CTL] = (1 << PWM_CTL_PWEN1) | (1 << PWM_CTL_MODE1) | (1 << PWM_CTL_RPTL1) | (0 << PWM_CTL_SBIT1) | (1 << PWM_CTL_USEF1)
// sleep for 4*50us to ensure the data is sent.
time.Sleep(200 * time.Microsecond)
}