mirror of
https://github.com/compute-blade-community/compute-blade-agent.git
synced 2026-04-16 07:25:41 +02:00
feat(hal): add BCM2712 (CM5/Pi 5) HAL support (#154)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Cedric Specht <cedric@specht-labs.de>
This commit is contained in:
@@ -54,7 +54,7 @@ type computeBladeAgent struct {
|
||||
|
||||
// NewComputeBladeAgent creates and initializes a new ComputeBladeAgent, including gRPC server setup and hardware interfaces.
|
||||
func NewComputeBladeAgent(ctx context.Context, config agent.ComputeBladeAgentConfig, agentInfo agent.ComputeBladeAgentInfo) (agent.ComputeBladeAgent, error) {
|
||||
blade, err := hal.NewCm4Hal(ctx, config.ComputeBladeHalOpts)
|
||||
blade, err := hal.NewHal(ctx, config.ComputeBladeHalOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ type bcm2711 struct {
|
||||
fanUnit FanUnit
|
||||
}
|
||||
|
||||
func NewCm4Hal(ctx context.Context, opts ComputeBladeHalOpts) (ComputeBladeHal, error) {
|
||||
func newBcm2711Hal(ctx context.Context, opts ComputeBladeHalOpts) (ComputeBladeHal, error) {
|
||||
// /dev/gpiomem doesn't allow complex operations for PWM fan control or WS281x
|
||||
devmem, err := os.OpenFile("/dev/mem", os.O_RDWR|os.O_SYNC, os.ModePerm)
|
||||
if err != nil {
|
||||
|
||||
@@ -20,7 +20,7 @@ type SimulatedHal struct {
|
||||
isStealthMode bool
|
||||
}
|
||||
|
||||
func NewCm4Hal(_ context.Context, _ ComputeBladeHalOpts) (ComputeBladeHal, error) {
|
||||
func NewHal(_ context.Context, _ ComputeBladeHalOpts) (ComputeBladeHal, error) {
|
||||
logger := otelzap.L().Named("hal").Named("simulated-cm4")
|
||||
logger.Warn("Using simulated hal")
|
||||
|
||||
|
||||
653
pkg/hal/hal_bcm2712.go
Normal file
653
pkg/hal/hal_bcm2712.go
Normal file
@@ -0,0 +1,653 @@
|
||||
//go:build linux && !tinygo
|
||||
|
||||
package hal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
|
||||
"github.com/warthog618/gpiod"
|
||||
"github.com/warthog618/gpiod/device/rpi"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const (
|
||||
// RP1 southbridge is connected via PCIe on BCM2712.
|
||||
// The BAR base address is fixed by firmware at 0x1f00000000.
|
||||
rp1BarBase int64 = 0x1f00000000
|
||||
rp1GpioBase int64 = rp1BarBase + 0xd0000
|
||||
rp1Pwm0Base int64 = rp1BarBase + 0x98000
|
||||
rp1PageSize = 4096
|
||||
|
||||
// RP1 PWM input clock is 50 MHz (from device tree assigned-clock-rates)
|
||||
rp1PwmClockHz = 50_000_000
|
||||
|
||||
// RP1 GPIO register layout: each GPIO has 8 bytes (STATUS + CTRL)
|
||||
rp1GpioCtrlOffset = 0x04 // CTRL register offset within each GPIO's 8-byte block
|
||||
rp1GpioRegSize = 0x08
|
||||
rp1GpioFuncselMask = 0x1f // CTRL bits [4:0]
|
||||
|
||||
// GPIO function select values (confirmed via `pinctrl set/get` on CM5)
|
||||
// GPIO 12: funcsel 0 (a0) = PWM0_CHAN0 (fan PWM)
|
||||
// GPIO 18: funcsel 3 (a3) = PWM0_CHAN2 (WS281x LEDs)
|
||||
rp1Gpio12FuncselPwm = 0
|
||||
rp1Gpio18FuncselPwm = 3
|
||||
rp1GpioFuncselSio = 5 // Software IO (standard GPIO)
|
||||
rp1GpioFuncselNull = 0x1f // Null function (disabled)
|
||||
|
||||
// RP1 PWM register offsets (byte offsets, divide by 4 for []uint32 index)
|
||||
rp1PwmGlobalCtrl = 0x00
|
||||
rp1PwmFifoCtrl = 0x04
|
||||
rp1PwmFifoPush = 0x08
|
||||
rp1PwmFifoLevel = 0x0c
|
||||
|
||||
// Per-channel registers: base = 0x14 + channel * 0x10
|
||||
// From kernel pwm-rp1.c: CTRL(x)=0x14+x*16, RANGE(x)=0x18+x*16, DUTY(x)=0x20+x*16
|
||||
rp1PwmChanCtrlOff = 0x00 // relative to channel base
|
||||
rp1PwmChanRangeOff = 0x04
|
||||
rp1PwmChanPhaseOff = 0x08 // undocumented in kernel driver, possibly COUNT
|
||||
rp1PwmChanDutyOff = 0x0c
|
||||
rp1PwmChanSize = 0x10
|
||||
rp1PwmChanBase = 0x14 // first channel base offset (NOT 0x10)
|
||||
|
||||
// PWM global ctrl bits (from kernel pwm-rp1.c)
|
||||
rp1PwmGlobalChanEnBit = 0 // bits [3:0] = per-channel enable, BIT(x)
|
||||
rp1PwmGlobalSetUpdate = 31 // BIT(31) = global set_update trigger
|
||||
|
||||
// PWM channel ctrl bits
|
||||
rp1PwmChanCtrlModeBit = 0 // bits [1:0]
|
||||
rp1PwmChanCtrlInvertBit = 2
|
||||
rp1PwmChanCtrlUseFifoBit = 4
|
||||
rp1PwmChanCtrlFifoPopMaskBit = 7 // bits [8:7]
|
||||
|
||||
// PWM modes
|
||||
rp1PwmModeTrailingEdge = 0
|
||||
rp1PwmModeMarkSpace = 1
|
||||
rp1PwmModeSerializer = 3
|
||||
|
||||
// FIFO ctrl bits
|
||||
rp1PwmFifoFlushBit = 5
|
||||
|
||||
// Fan PWM: 25 kHz for Noctua fans
|
||||
// RANGE = 50 MHz / 25 kHz = 2000
|
||||
rp1FanPwmRange = rp1PwmClockHz / 25000
|
||||
|
||||
bcm2712SmartFanUnitDev = "/dev/ttyAMA4" // UART4 on BCM2712 (same GPIO 12/13 pins as UART5 on BCM2711)
|
||||
bcm2712ThermalZonePath = "/sys/class/thermal/thermal_zone0/temp"
|
||||
bcm2712DebounceInterval = 100 * time.Millisecond
|
||||
|
||||
// WS281x LED encoding for RP1 at 50MHz with RANGE=32 serializer mode.
|
||||
// Each WS281x data bit is encoded as 2 × 32-bit FIFO words (1280ns per bit ≈ 781kHz).
|
||||
// Channel 2 on GPIO 18 is used independently from fan PWM on channel 0.
|
||||
rp1Ws281xChan = 2 // PWM0 channel 2 on GPIO 18
|
||||
rp1Ws281xRange = 32 // full 32-bit word in serializer mode
|
||||
|
||||
// "0" bit: T0H=400ns (20 high bits), T0L=880ns (44 low bits)
|
||||
rp1Ws281xBit0Word0 uint32 = 0xFFFFF000
|
||||
rp1Ws281xBit0Word1 uint32 = 0x00000000
|
||||
// "1" bit: T1H=800ns (40 high bits), T1L=480ns (24 low bits)
|
||||
rp1Ws281xBit1Word0 uint32 = 0xFFFFFFFF
|
||||
rp1Ws281xBit1Word1 uint32 = 0xFF000000
|
||||
|
||||
// Reset: >50μs of low. At 640ns/word, 80 words = 51.2μs.
|
||||
rp1Ws281xResetWords = 80
|
||||
// Conservative FIFO depth assumption (BCM2835 has 16, RP1 may differ)
|
||||
rp1Ws281xFifoMax uint32 = 8
|
||||
// Safety timeout for FIFO operations
|
||||
rp1Ws281xTimeout = 5 * time.Millisecond
|
||||
)
|
||||
|
||||
// pwmChanRegIdx returns the []uint32 index for a per-channel register.
|
||||
func pwmChanRegIdx(channel, regOffset int) int {
|
||||
return (rp1PwmChanBase + channel*rp1PwmChanSize + regOffset) / 4
|
||||
}
|
||||
|
||||
type bcm2712 struct {
|
||||
opts ComputeBladeHalOpts
|
||||
|
||||
wrMutex sync.Mutex
|
||||
|
||||
currFanSpeed uint8
|
||||
|
||||
devmem *os.File
|
||||
gpioMem8 []uint8
|
||||
gpioMem []uint32
|
||||
pwmMem8 []uint8
|
||||
pwmMem []uint32
|
||||
|
||||
gpioChip0 *gpiod.Chip
|
||||
|
||||
// WS281x LED colors (top + edge)
|
||||
leds [2]led.Color
|
||||
|
||||
// Stealth mode output
|
||||
stealthModeLine *gpiod.Line
|
||||
|
||||
// Edge button input
|
||||
edgeButtonLine *gpiod.Line
|
||||
edgeButtonDebounceChan chan struct{}
|
||||
edgeButtonWatchChan chan struct{}
|
||||
|
||||
// PoE detection input
|
||||
poeLine *gpiod.Line
|
||||
|
||||
// Fan unit
|
||||
fanUnit FanUnit
|
||||
}
|
||||
|
||||
func newBcm2712Hal(ctx context.Context, opts ComputeBladeHalOpts) (ComputeBladeHal, error) {
|
||||
devmem, err := os.OpenFile("/dev/mem", os.O_RDWR|os.O_SYNC, os.ModePerm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open /dev/mem: %w", err)
|
||||
}
|
||||
|
||||
gpioChip0, err := gpiod.NewChip("gpiochip0")
|
||||
if err != nil {
|
||||
_ = devmem.Close()
|
||||
return nil, fmt.Errorf("failed to open gpiochip0: %w", err)
|
||||
}
|
||||
|
||||
// Memory-map RP1 GPIO bank 0 (GPIOs 0-27)
|
||||
gpioMem, gpioMem8, err := mmap(devmem, rp1GpioBase, rp1PageSize)
|
||||
if err != nil {
|
||||
_ = gpioChip0.Close()
|
||||
_ = devmem.Close()
|
||||
return nil, fmt.Errorf("failed to mmap RP1 GPIO at 0x%x: %w", rp1GpioBase, err)
|
||||
}
|
||||
|
||||
// Memory-map RP1 PWM0
|
||||
pwmMem, pwmMem8, err := mmap(devmem, rp1Pwm0Base, rp1PageSize)
|
||||
if err != nil {
|
||||
_ = syscall.Munmap(gpioMem8)
|
||||
_ = gpioChip0.Close()
|
||||
_ = devmem.Close()
|
||||
return nil, fmt.Errorf("failed to mmap RP1 PWM0 at 0x%x: %w", rp1Pwm0Base, err)
|
||||
}
|
||||
|
||||
bcm := &bcm2712{
|
||||
devmem: devmem,
|
||||
gpioMem: gpioMem,
|
||||
gpioMem8: gpioMem8,
|
||||
pwmMem: pwmMem,
|
||||
pwmMem8: pwmMem8,
|
||||
gpioChip0: gpioChip0,
|
||||
opts: opts,
|
||||
edgeButtonDebounceChan: make(chan struct{}, 1),
|
||||
edgeButtonWatchChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
computeModule.WithLabelValues("cm5").Set(1)
|
||||
|
||||
log.FromContext(ctx).Info("starting hal setup", zap.String("hal", "bcm2712"))
|
||||
if err := bcm.setup(ctx); err != nil {
|
||||
_ = bcm.Close()
|
||||
return nil, err
|
||||
}
|
||||
return bcm, nil
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) Close() error {
|
||||
errs := errors.Join(
|
||||
bcm.closeFanUnit(),
|
||||
bcm.unmapMem(),
|
||||
bcm.closeDevmem(),
|
||||
bcm.closeGpio(),
|
||||
)
|
||||
return errs
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) closeFanUnit() error {
|
||||
if bcm.fanUnit != nil {
|
||||
return bcm.fanUnit.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) unmapMem() error {
|
||||
return errors.Join(
|
||||
munmapIfNonNil(bcm.gpioMem8),
|
||||
munmapIfNonNil(bcm.pwmMem8),
|
||||
)
|
||||
}
|
||||
|
||||
func munmapIfNonNil(mem []uint8) error {
|
||||
if mem != nil {
|
||||
return syscall.Munmap(mem)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) closeDevmem() error {
|
||||
if bcm.devmem != nil {
|
||||
return bcm.devmem.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) closeGpio() error {
|
||||
var errs []error
|
||||
if bcm.gpioChip0 != nil {
|
||||
errs = append(errs, bcm.gpioChip0.Close())
|
||||
}
|
||||
if bcm.poeLine != nil {
|
||||
errs = append(errs, bcm.poeLine.Close())
|
||||
}
|
||||
if bcm.stealthModeLine != nil {
|
||||
errs = append(errs, bcm.stealthModeLine.Close())
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// setGpioFuncsel sets the function select for a GPIO pin via direct register write.
|
||||
func (bcm *bcm2712) setGpioFuncsel(gpio int, funcsel uint32) {
|
||||
ctrlIdx := (gpio*rp1GpioRegSize + rp1GpioCtrlOffset) / 4
|
||||
ctrl := bcm.gpioMem[ctrlIdx]
|
||||
ctrl = (ctrl &^ uint32(rp1GpioFuncselMask)) | (funcsel & uint32(rp1GpioFuncselMask))
|
||||
bcm.gpioMem[ctrlIdx] = ctrl
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) setup(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
// Register edge event handler for edge button (GPIO 20)
|
||||
bcm.edgeButtonLine, err = bcm.gpioChip0.RequestLine(
|
||||
rpi.GPIO20, gpiod.WithEventHandler(bcm.handleEdgeButtonEdge),
|
||||
gpiod.WithFallingEdge, gpiod.WithPullUp, gpiod.WithDebounce(50*time.Millisecond))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to request GPIO20 (edge button): %w", err)
|
||||
}
|
||||
|
||||
// Register input for PoE detection (GPIO 23)
|
||||
bcm.poeLine, err = bcm.gpioChip0.RequestLine(rpi.GPIO23, gpiod.AsInput, gpiod.WithPullUp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to request GPIO23 (PoE detect): %w", err)
|
||||
}
|
||||
|
||||
// Register output for stealth mode (GPIO 21)
|
||||
bcm.stealthModeLine, err = bcm.gpioChip0.RequestLine(rpi.GPIO21, gpiod.AsOutput(1))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to request GPIO21 (stealth mode): %w", err)
|
||||
}
|
||||
|
||||
// Detect fan unit type
|
||||
log.FromContext(ctx).Info("detecting fan unit")
|
||||
detectCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if smartFanUnitPresent, err := SmartFanUnitPresent(detectCtx, bcm2712SmartFanUnitDev); err == nil && smartFanUnitPresent {
|
||||
log.FromContext(ctx).Info("detected smart fan unit")
|
||||
bcm.fanUnit, err = NewSmartFanUnit(bcm2712SmartFanUnitDev)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create smart fan unit: %w", err)
|
||||
}
|
||||
} else {
|
||||
log.FromContext(ctx).WithError(err).Info("no smart fan unit detected, assuming standard fan unit")
|
||||
|
||||
// Set GPIO 12 to PWM0_CHAN0 function
|
||||
bcm.setGpioFuncsel(12, rp1Gpio12FuncselPwm)
|
||||
|
||||
// Initialize PWM0 channel 0 for fan control (25 kHz mark-space)
|
||||
bcm.initFanPwm()
|
||||
|
||||
bcm.fanUnit = &standardFanUnitBcm2711{
|
||||
GpioChip0: bcm.gpioChip0,
|
||||
DisableRpmReporting: !bcm.opts.RpmReportingStandardFanUnit,
|
||||
SetFanSpeedPwmFunc: func(speed uint8) error {
|
||||
bcm.setFanSpeedPWM(speed)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// initFanPwm configures PWM0 channel 0 in mark-space mode at 25 kHz.
|
||||
func (bcm *bcm2712) initFanPwm() {
|
||||
ch := 0
|
||||
|
||||
// Disable channel 0
|
||||
globalCtrl := bcm.pwmMem[rp1PwmGlobalCtrl/4]
|
||||
globalCtrl &^= (1 << (rp1PwmGlobalChanEnBit + ch))
|
||||
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Configure channel 0: mark-space mode, no FIFO, no invert
|
||||
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanCtrlOff)] = uint32(rp1PwmModeMarkSpace) << rp1PwmChanCtrlModeBit
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Set range (period) for 25 kHz
|
||||
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanRangeOff)] = rp1FanPwmRange
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Set initial duty to 0 (fan off)
|
||||
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanDutyOff)] = 0
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Phase = 0
|
||||
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanPhaseOff)] = 0
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Trigger update for channel 0
|
||||
globalCtrl = bcm.pwmMem[rp1PwmGlobalCtrl/4]
|
||||
globalCtrl |= (1 << rp1PwmGlobalSetUpdate)
|
||||
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Enable channel 0
|
||||
globalCtrl = bcm.pwmMem[rp1PwmGlobalCtrl/4]
|
||||
globalCtrl |= (1 << (rp1PwmGlobalChanEnBit + ch))
|
||||
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) Run(parentCtx context.Context) error {
|
||||
ctx, cancel := context.WithCancel(parentCtx)
|
||||
defer cancel()
|
||||
|
||||
group := errgroup.Group{}
|
||||
|
||||
group.Go(func() error {
|
||||
defer cancel()
|
||||
return bcm.fanUnit.Run(ctx)
|
||||
})
|
||||
|
||||
return group.Wait()
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) handleEdgeButtonEdge(evt gpiod.LineEvent) {
|
||||
select {
|
||||
case bcm.edgeButtonDebounceChan <- struct{}{}:
|
||||
go func() {
|
||||
<-bcm.edgeButtonDebounceChan
|
||||
time.Sleep(bcm2712DebounceInterval)
|
||||
edgeButtonEventCount.Inc()
|
||||
close(bcm.edgeButtonWatchChan)
|
||||
bcm.edgeButtonWatchChan = make(chan struct{})
|
||||
}()
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) WaitForEdgeButtonPress(parentCtx context.Context) error {
|
||||
ctx, cancel := context.WithCancel(parentCtx)
|
||||
defer cancel()
|
||||
|
||||
fanUnitChan := make(chan struct{})
|
||||
go func() {
|
||||
err := bcm.fanUnit.WaitForButtonPress(ctx)
|
||||
if err != nil && err != context.Canceled {
|
||||
log.FromContext(ctx).WithError(err).Error("failed to wait for button press")
|
||||
} else {
|
||||
close(fanUnitChan)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-bcm.edgeButtonWatchChan:
|
||||
return nil
|
||||
case <-fanUnitChan:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) GetFanRPM() (float64, error) {
|
||||
rpm, err := bcm.fanUnit.FanSpeedRPM(context.TODO())
|
||||
return float64(rpm), err
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) GetPowerStatus() (PowerStatus, error) {
|
||||
val, err := bcm.poeLine.Value()
|
||||
if err != nil {
|
||||
return PowerPoeOrUsbC, err
|
||||
}
|
||||
|
||||
if val > 0 {
|
||||
powerStatus.WithLabelValues(fmt.Sprint(PowerPoe802at)).Set(1)
|
||||
powerStatus.WithLabelValues(fmt.Sprint(PowerPoeOrUsbC)).Set(0)
|
||||
return PowerPoe802at, nil
|
||||
}
|
||||
powerStatus.WithLabelValues(fmt.Sprint(PowerPoe802at)).Set(0)
|
||||
powerStatus.WithLabelValues(fmt.Sprint(PowerPoeOrUsbC)).Set(1)
|
||||
return PowerPoeOrUsbC, nil
|
||||
}
|
||||
|
||||
// SetFanSpeed sets the fan speed in percent.
|
||||
func (bcm *bcm2712) SetFanSpeed(speed uint8) error {
|
||||
fanTargetPercent.Set(float64(speed))
|
||||
return bcm.fanUnit.SetFanSpeedPercent(context.TODO(), speed)
|
||||
}
|
||||
|
||||
// setFanSpeedPWM sets the fan PWM duty cycle using RP1 PWM0 channel 0 in mark-space mode.
|
||||
func (bcm *bcm2712) setFanSpeedPWM(speed uint8) {
|
||||
ch := 0
|
||||
|
||||
var duty uint32
|
||||
if speed == 0 {
|
||||
duty = 0
|
||||
} else if speed <= 100 {
|
||||
duty = uint32(float64(rp1FanPwmRange) * float64(speed) / 100.0)
|
||||
} else {
|
||||
duty = rp1FanPwmRange
|
||||
}
|
||||
|
||||
// Update duty cycle
|
||||
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanDutyOff)] = duty
|
||||
|
||||
// Trigger update
|
||||
globalCtrl := bcm.pwmMem[rp1PwmGlobalCtrl/4]
|
||||
globalCtrl |= (1 << rp1PwmGlobalSetUpdate)
|
||||
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
|
||||
|
||||
bcm.currFanSpeed = speed
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) SetStealthMode(enable bool) error {
|
||||
if enable {
|
||||
stealthModeEnabled.Set(1)
|
||||
return bcm.stealthModeLine.SetValue(1)
|
||||
}
|
||||
stealthModeEnabled.Set(0)
|
||||
return bcm.stealthModeLine.SetValue(0)
|
||||
}
|
||||
|
||||
func (bcm *bcm2712) StealthModeActive() bool {
|
||||
val, err := bcm.stealthModeLine.Value()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return val > 0
|
||||
}
|
||||
|
||||
// SetLed sets the WS281x LED color via RP1 PWM0 channel 2 serializer mode.
|
||||
// Unlike BCM2711 which shares PWM0 between fan and LEDs, RP1 has independent channels:
|
||||
// channel 0 (GPIO 12) handles fan PWM, channel 2 (GPIO 18) handles WS281x LEDs.
|
||||
func (bcm *bcm2712) SetLed(idx LedIndex, color led.Color) error {
|
||||
if idx >= 2 {
|
||||
return fmt.Errorf("invalid led index %d, supported: [0, 1]", idx)
|
||||
}
|
||||
|
||||
// Update the fan unit LED if this is the edge LED
|
||||
if idx == LedEdge {
|
||||
if err := bcm.fanUnit.SetLed(context.TODO(), color); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
bcm.leds[idx] = color
|
||||
|
||||
return bcm.updateLEDs()
|
||||
}
|
||||
|
||||
// updateLEDs sends WS281x data via RP1 PWM0 channel 2 serializer mode.
|
||||
// Uses the shared FIFO (channel 0 fan PWM uses mark-space mode without FIFO, so no conflict).
|
||||
// The RP1 PWM clock is fixed at 50MHz, so we use 2 FIFO words per WS281x data bit
|
||||
// at RANGE=32 to achieve ~1280ns per bit (within WS281x tolerance).
|
||||
func (bcm *bcm2712) updateLEDs() error {
|
||||
bcm.wrMutex.Lock()
|
||||
defer bcm.wrMutex.Unlock()
|
||||
|
||||
ledColorChangeEventCount.Inc()
|
||||
|
||||
ch := rp1Ws281xChan
|
||||
|
||||
// Build complete FIFO data stream
|
||||
data := bcm.buildWs281xStream()
|
||||
|
||||
// Set GPIO 18 to PWM0_CH2 function
|
||||
bcm.setGpioFuncsel(18, rp1Gpio18FuncselPwm)
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Disable channel 2
|
||||
globalCtrl := bcm.pwmMem[rp1PwmGlobalCtrl/4]
|
||||
globalCtrl &^= (1 << (rp1PwmGlobalChanEnBit + ch))
|
||||
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Configure channel 2: serializer mode, use FIFO, SBIT=0 (low when idle)
|
||||
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanCtrlOff)] =
|
||||
(rp1PwmModeSerializer << rp1PwmChanCtrlModeBit) | (1 << rp1PwmChanCtrlUseFifoBit)
|
||||
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanRangeOff)] = rp1Ws281xRange
|
||||
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanPhaseOff)] = 0
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Flush FIFO
|
||||
bcm.pwmMem[rp1PwmFifoCtrl/4] = (1 << rp1PwmFifoFlushBit)
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Trigger update for channel 2
|
||||
globalCtrl = bcm.pwmMem[rp1PwmGlobalCtrl/4]
|
||||
globalCtrl |= (1 << rp1PwmGlobalSetUpdate)
|
||||
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
|
||||
time.Sleep(10 * time.Microsecond)
|
||||
|
||||
// Pre-fill FIFO before enabling channel
|
||||
idx := 0
|
||||
for idx < len(data) && uint32(idx) < rp1Ws281xFifoMax {
|
||||
bcm.pwmMem[rp1PwmFifoPush/4] = data[idx]
|
||||
idx++
|
||||
}
|
||||
|
||||
// Lock OS thread for tight FIFO feeding
|
||||
runtime.LockOSThread()
|
||||
|
||||
// Enable channel 2
|
||||
globalCtrl = bcm.pwmMem[rp1PwmGlobalCtrl/4]
|
||||
globalCtrl |= (1 << (rp1PwmGlobalChanEnBit + ch))
|
||||
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
|
||||
|
||||
// Push remaining data, polling FIFO level to avoid overflow
|
||||
fifoLevelReg := rp1PwmFifoLevel / 4
|
||||
fifoPushReg := rp1PwmFifoPush / 4
|
||||
deadline := time.Now().Add(rp1Ws281xTimeout)
|
||||
|
||||
for idx < len(data) {
|
||||
if time.Now().After(deadline) {
|
||||
break
|
||||
}
|
||||
if bcm.pwmMem[fifoLevelReg] < rp1Ws281xFifoMax {
|
||||
bcm.pwmMem[fifoPushReg] = data[idx]
|
||||
idx++
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for FIFO to drain
|
||||
for bcm.pwmMem[fifoLevelReg] > 0 {
|
||||
if time.Now().After(deadline) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
runtime.UnlockOSThread()
|
||||
|
||||
// Wait for last word to finish shifting out
|
||||
time.Sleep(200 * time.Microsecond)
|
||||
|
||||
// Disable channel 2
|
||||
globalCtrl = bcm.pwmMem[rp1PwmGlobalCtrl/4]
|
||||
globalCtrl &^= (1 << (rp1PwmGlobalChanEnBit + ch))
|
||||
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
|
||||
|
||||
// Disconnect GPIO 18 from PWM to prevent residual noise on the data line
|
||||
bcm.setGpioFuncsel(18, rp1GpioFuncselNull)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildWs281xStream constructs the complete FIFO word stream for both WS281x LEDs.
|
||||
// Format: [reset padding] [top LED RGB] [edge LED RGB] [trailing zeros]
|
||||
func (bcm *bcm2712) buildWs281xStream() []uint32 {
|
||||
// Pre-allocate: 80 reset + 96 data (2 LEDs × 3 bytes × 16 words) + 2 trailing
|
||||
words := make([]uint32, 0, rp1Ws281xResetWords+96+2)
|
||||
|
||||
// Reset padding (>50μs of low signal)
|
||||
for i := 0; i < rp1Ws281xResetWords; i++ {
|
||||
words = append(words, 0)
|
||||
}
|
||||
|
||||
// Top LED (index 0) - RGB order (matches BCM2711 upstream)
|
||||
words = appendByteWs281x(words, bcm.leds[0].Red)
|
||||
words = appendByteWs281x(words, bcm.leds[0].Green)
|
||||
words = appendByteWs281x(words, bcm.leds[0].Blue)
|
||||
|
||||
// Edge LED (index 1)
|
||||
words = appendByteWs281x(words, bcm.leds[1].Red)
|
||||
words = appendByteWs281x(words, bcm.leds[1].Green)
|
||||
words = appendByteWs281x(words, bcm.leds[1].Blue)
|
||||
|
||||
// Trailing zeros for clean end-of-frame
|
||||
words = append(words, 0, 0)
|
||||
|
||||
return words
|
||||
}
|
||||
|
||||
// appendByteWs281x encodes one byte as 16 FIFO words (2 per data bit, MSB first) for RP1 WS281x.
|
||||
func appendByteWs281x(words []uint32, b uint8) []uint32 {
|
||||
for i := 7; i >= 0; i-- {
|
||||
if (b>>uint(i))&1 == 0 {
|
||||
words = append(words, rp1Ws281xBit0Word0, rp1Ws281xBit0Word1)
|
||||
} else {
|
||||
words = append(words, rp1Ws281xBit1Word0, rp1Ws281xBit1Word1)
|
||||
}
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
// GetTemperature returns the SoC temperature in degrees Celsius.
|
||||
func (bcm *bcm2712) GetTemperature() (float64, error) {
|
||||
f, err := os.Open(bcm2712ThermalZonePath)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
raw, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
cpuTemp, err := strconv.Atoi(strings.TrimSpace(string(raw)))
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
temp := float64(cpuTemp) / 1000.0
|
||||
socTemperature.Set(temp)
|
||||
|
||||
return temp, nil
|
||||
}
|
||||
37
pkg/hal/platform_linux.go
Normal file
37
pkg/hal/platform_linux.go
Normal file
@@ -0,0 +1,37 @@
|
||||
//go:build linux && !tinygo
|
||||
|
||||
package hal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const deviceTreeCompatiblePath = "/sys/firmware/devicetree/base/compatible"
|
||||
|
||||
// NewHal creates the appropriate HAL implementation based on the detected platform.
|
||||
// It reads the device tree compatible string to determine whether the SoC is a
|
||||
// BCM2711 (CM4/Pi 4) or BCM2712 (CM5/Pi 5).
|
||||
func NewHal(ctx context.Context, opts ComputeBladeHalOpts) (ComputeBladeHal, error) {
|
||||
compatible, err := os.ReadFile(deviceTreeCompatiblePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read device tree compatible string: %w", err)
|
||||
}
|
||||
|
||||
compatStr := string(compatible)
|
||||
log.FromContext(ctx).Info("detected platform", zap.String("compatible", strings.ReplaceAll(compatStr, "\x00", ", ")))
|
||||
|
||||
switch {
|
||||
case strings.Contains(compatStr, "bcm2712"):
|
||||
return newBcm2712Hal(ctx, opts)
|
||||
case strings.Contains(compatStr, "bcm2711"):
|
||||
return newBcm2711Hal(ctx, opts)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported platform: %s", strings.ReplaceAll(compatStr, "\x00", ", "))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user