From 906f56fe24c62c3fa0467efd9eeef74f735f2918 Mon Sep 17 00:00:00 2001 From: Matthias Riegler Date: Fri, 7 Jul 2023 19:28:07 +0200 Subject: [PATCH] feat: make ws281x work next to PWM based fan speed control Signed-off-by: Matthias Riegler --- main.go | 40 ++++++- pkg/hal/bcm2711/hal_bcm2711.go | 209 +++++++++++++++++++++++---------- 2 files changed, 184 insertions(+), 65 deletions(-) diff --git a/main.go b/main.go index e20a2e5..ec3cc95 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,8 @@ package main import ( + "time" + "github.com/xvzf/computeblade-agent/pkg/hal" "github.com/xvzf/computeblade-agent/pkg/hal/bcm2711" ) @@ -8,7 +10,7 @@ import ( func main() { blade, err := bcm2711.New(hal.ComputeBladeHalOpts{ FanUnit: hal.FAN_UNIT_STANDARD, - DefaultFanSpeed: 40, + DefaultFanSpeed: 10, DefaultStealthModeEnabled: false, }) if err != nil { @@ -16,4 +18,40 @@ func main() { } defer blade.Close() blade.Init() + + ledColorToggle := []bcm2711.LedColor{ + { + Green: 50, + Blue: 0, + Red: 0, + }, + { + Green: 0, + Blue: 50, + Red: 0, + }, + { + Green: 0, + Blue: 0, + Red: 50, + }, + } + + // just randomly go through colors! + idxTop := 1 + idxEdge := 0 + for { + <-time.After(time.Second * 1) + idxTop++ + if idxTop > 2 { + idxTop = 0 + } + idxEdge++ + if idxEdge > 2 { + idxEdge = 0 + } + blade.SetLEDs(ledColorToggle[idxTop], ledColorToggle[idxEdge]) + blade.SetFanSpeed(30 * uint8(idxTop)) + } + } diff --git a/pkg/hal/bcm2711/hal_bcm2711.go b/pkg/hal/bcm2711/hal_bcm2711.go index 9017374..28d0203 100644 --- a/pkg/hal/bcm2711/hal_bcm2711.go +++ b/pkg/hal/bcm2711/hal_bcm2711.go @@ -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 == 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) +}