diff --git a/rp2-pio/examples/hub40screen/hub40screen.go b/rp2-pio/examples/hub40screen/hub40screen.go new file mode 100644 index 0000000..6dc0332 --- /dev/null +++ b/rp2-pio/examples/hub40screen/hub40screen.go @@ -0,0 +1,205 @@ +package main + +import ( + "machine" + "runtime" + "time" + "unsafe" + + pio "github.com/tinygo-org/pio/rp2-pio" + "github.com/tinygo-org/pio/rp2-pio/piolib" +) + +func main() { + time.Sleep(time.Second) + const usePIO = true + var p6 Parallel6Bus + if usePIO { + sm, _ := pio.PIO0.ClaimStateMachine() + p6concrete, err := piolib.NewParallel6(sm, 12_000, 0, 6) + if err != nil { + panic(err) + } + err = p6concrete.EnableDMA(true) + if err != nil { + panic(err) + } + p6 = p6concrete + } else { + p6 = &bitbangParallel6{ + rd0: makepinout(0), + gd0: makepinout(1), + bd0: makepinout(2), + rd1: makepinout(3), + gd1: makepinout(4), + bd1: makepinout(5), + clk: makepinout(6), + } + } + var emptyScreen [4][4]uint32 + s := NewHUB40Screen(p6, makepinout(7), makepinout(8)) + s.Draw(&emptyScreen) + for { + // testScreen(s) + testScreenBus(s) + } +} + +// SetScreen sets a 3-bit RGB value at (ix, iy). +// ix: 0..15, iy: 0..7 +func SetScreen(screen *[4][4]uint32, ix, iy int, rgb3bits uint8) { + if ix < 0 || ix >= 16 || iy < 0 || iy >= 8 { + return // or panic + } + colBlock := ix / 4 // 0..3 + rowIdx := iy % 4 // 0..3 within the half + up := 1 - (iy / 4) // 0 = lower half, 1 = upper half + + groupBase := (ix % 4) * 6 // 6 bits per group + bitoff := groupBase + up*3 + + mask := uint32(0b111) << bitoff + v := &screen[colBlock][rowIdx] + *v &^= mask + *v |= uint32(rgb3bits&0x7) << bitoff +} + +type Parallel6Bus interface { + Tx24(buf []uint32) error +} + +func NewHUB40Screen(bus Parallel6Bus, latch, outputEnable pinout) *HUB40Screen { + return &HUB40Screen{ + lat: latch, + oe: outputEnable, + p6: bus, + } +} + +func testScreen(s *HUB40Screen) { + var emptyscreen [4][4]uint32 + var screen [4][4]uint32 + s.Draw(&emptyscreen) + for iy := 0; iy < 8; iy++ { + for ix := 0; ix < 16; ix++ { + SetScreen(&screen, ix, iy, 0b101) + s.Draw(&screen) + time.Sleep(30 * time.Millisecond) + } + } + for range 2 { + s.Draw(&screen) + time.Sleep(time.Second / 3) + s.Draw(&emptyscreen) + time.Sleep(time.Second / 3) + } + for i := range screen { + for j := range screen[i] { + screen[i][j] = 0xffff_ffff + } + } + s.Draw(&screen) + time.Sleep(2 * time.Second) +} + +func testScreenBus(s *HUB40Screen) { + var screen [4][4]uint32 + buf := unsafe.Slice(&screen[0][0], 4*4) + for j := range buf { + for i := 0; i < 24; i++ { + mask := uint32(1) << i + buf[j] = mask + if i >= 24 { + println("off screen") + } + buf[0] |= 1 + buf[len(buf)-1] |= 1 << 23 + s.Draw(&screen) + time.Sleep(50 * time.Millisecond) + } + } +} + +// HUB40Screen is a RGB single-bit-per-pixel screen that works on the basis of +// several shift registers arranges in two rows. Each clock represents 6 bits +// over the parallel-6 bus, so two LEDs per clock. The device I am using has +// no markings other than "HUB40" and "OF-20R3A2-0816-V21JC" which is likely not a real model. +// It seems to be a one-off design for a media company. Data lines are: +// - RD0..BD1: Six Red/Green/Blue data lines for row 0 and row 1. +// - OE: Output enable. Turns entire display on or off. Is negated. +// - LAT: Latch. Latches shift registers values. Basically switches display buffer to the one written most recently. +type HUB40Screen struct { + // data pins. + lat pinout + oe pinout + p6 Parallel6Bus +} + +func (s *HUB40Screen) Latch() { + s.lat.High() + runtime.Gosched() + s.lat.Low() +} + +func (s *HUB40Screen) Enable(b bool) { + println("enable", b) + s.oe(!b) +} + +// Write writes the screen data without latching. +func (s *HUB40Screen) Write(screen *[4][4]uint32) (err error) { + buf := unsafe.Slice(&screen[0][0], 4*4) + return s.p6.Tx24(buf) +} + +// Draw writes and then latches screen data. +func (s *HUB40Screen) Draw(screen *[4][4]uint32) (err error) { + err = s.Write(screen) + if err == nil { + s.Latch() + } + return err +} + +type bitbangParallel6 struct { + rd0, gd0, bd0, rd1, gd1, bd1 pinout + clk pinout +} + +func (s *bitbangParallel6) Tx24(buf []uint32) error { + for i := range buf { + s.clkout24(buf[i]) + } + return nil +} + +func (s *bitbangParallel6) clkout24(v uint32) { + const bits6 = 0b11_1111 + s.rawclkout6(uint8(v & bits6)) + s.rawclkout6(uint8((v >> 6) & bits6)) + s.rawclkout6(uint8((v >> 12) & bits6)) + s.rawclkout6(uint8((v >> 18) & bits6)) +} + +func (s *bitbangParallel6) rawclkout6(rgbBits uint8) { + s.clk.Low() + s.rd0(rgbBits&(1<<0) != 0) + s.gd0(rgbBits&(1<<1) != 0) + s.bd0(rgbBits&(1<<2) != 0) + + s.rd1(rgbBits&(1<<3) != 0) + s.gd1(rgbBits&(1<<4) != 0) + s.bd1(rgbBits&(1<<5) != 0) + s.clk.High() + s.clk.Low() +} + +type pinout func(level bool) + +func (p pinout) High() { p(true) } +func (p pinout) Low() { p(false) } + +func makepinout(pin machine.Pin) pinout { + pin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + return pin.Set +} diff --git a/rp2-pio/piolib/all_generate.go b/rp2-pio/piolib/all_generate.go index 0bcbcaa..dcc1b7f 100644 --- a/rp2-pio/piolib/all_generate.go +++ b/rp2-pio/piolib/all_generate.go @@ -17,6 +17,7 @@ var ( errDMAUnavail = errors.New("piolib:DMA channel unavailable") ) +//go:generate pioasm -o go parallel6.pio parallel6_pio.go //go:generate pioasm -o go parallel8.pio parallel8_pio.go //go:generate pioasm -o go pulsar.pio pulsar_pio.go //go:generate pioasm -o go spi.pio spi_pio.go diff --git a/rp2-pio/piolib/parallel6.go b/rp2-pio/piolib/parallel6.go new file mode 100644 index 0000000..c9dd5c6 --- /dev/null +++ b/rp2-pio/piolib/parallel6.go @@ -0,0 +1,115 @@ +package piolib + +import ( + "machine" + + pio "github.com/tinygo-org/pio/rp2-pio" +) + +// Parallel6 implements a 6-parallel bus. +type Parallel6 struct { + sm pio.StateMachine + dma dmaChannel + rgboffset uint8 +} + +// NewParallel6 instantiates a 6-parallel bus with pins dataBase..dataBase+5 and clock pin. +func NewParallel6(sm pio.StateMachine, baud uint32, dataBase machine.Pin, clock machine.Pin) (*Parallel6, error) { + sm.TryClaim() + Pio := sm.PIO() + whole, frac, err := pio.ClkDivFromFrequency(baud, machine.CPUFrequency()) + if err != nil { + return nil, err + } + rgbOffset, err := Pio.AddProgram(parallel6Instructions, parallel6Origin) + if err != nil { + return nil, err + } + pinCfg := machine.PinConfig{Mode: Pio.PinMode()} + clock.Configure(pinCfg) + clkMask := uint32(1) << clock + var pinMask uint32 = clkMask + for i := 0; i < 6; i++ { + pin := dataBase + machine.Pin(i) + pinMask |= 1 << pin + pin.Configure(pinCfg) + } + + // Configuration. + cfg := parallel6ProgramDefaultConfig(rgbOffset) + // Statemachine Pin configuration. + cfg.SetOutPins(dataBase, 6) + cfg.SetOutShift(true, true, 24) + cfg.SetSidesetPins(clock) + + cfg.SetClkDivIntFrac(whole, frac) + cfg.SetFIFOJoin(pio.FifoJoinTx) + + sm.SetPinsMasked(0, pinMask) + sm.SetPindirsMasked(pinMask, pinMask) + sm.Init(rgbOffset, cfg) + sm.SetEnabled(true) + p6 := Parallel6{ + sm: sm, + rgboffset: rgbOffset, + } + return &p6, nil +} + +// IsEnabled returns true if the state machine on the Parallel6 is enabled and ready to transmit. +func (p6 *Parallel6) IsEnabled() bool { + return p6.sm.IsEnabled() +} + +// SetEnabled enables or disables the state machine. +func (p6 *Parallel6) SetEnabled(b bool) { + p6.sm.SetEnabled(b) +} + +// Tx24 transmits 6-parallel data over pins. Each 32 bit value contains 24 effective bits +// making a total of 4 clocks. +func (p6 *Parallel6) Tx24(data []uint32) (err error) { + p6.sm.ClearTxStalled() + if p6.IsDMAEnabled() { + err = p6.tx24DMA(data) + } else { + err = p6.tx24(data) + } + if err != nil { + return err + } + for !p6.sm.HasTxStalled() { + gosched() // Block until empty. + } + return nil +} + +func (p6 *Parallel6) tx24(data []uint32) error { + i := 0 + for i < len(data) { + if p6.sm.IsTxFIFOFull() { + gosched() + continue + } + p6.sm.TxPut(data[i]) + i++ + } + return nil +} + +func (p6 *Parallel6) IsDMAEnabled() bool { + return p6.dma.helperIsEnabled() +} + +func (p6 *Parallel6) EnableDMA(enabled bool) error { + return p6.dma.helperEnableDMA(enabled) +} + +func (p6 *Parallel6) tx24DMA(data []uint32) error { + dreq := dmaPIO_TxDREQ(p6.sm) + err := p6.dma.Push32(&p6.sm.TxReg().Reg, data, dreq) + if err != nil { + return err + } + return nil +} diff --git a/rp2-pio/piolib/parallel6.pio b/rp2-pio/piolib/parallel6.pio new file mode 100644 index 0000000..c401556 --- /dev/null +++ b/rp2-pio/piolib/parallel6.pio @@ -0,0 +1,18 @@ +.program parallel6 +.side_set 1 + +.wrap_target + out pins, 6 side 0 ; write OSR databits into pin + nop side 1 + nop side 0 +.wrap + +% go { +//go:build rp2040 || rp2350 + +package piolib + +import ( + pio "github.com/tinygo-org/pio/rp2-pio" +) +%} \ No newline at end of file diff --git a/rp2-pio/piolib/parallel6_pio.go b/rp2-pio/piolib/parallel6_pio.go new file mode 100644 index 0000000..df3b531 --- /dev/null +++ b/rp2-pio/piolib/parallel6_pio.go @@ -0,0 +1,27 @@ +// This file is autogenerated by pioasm version 2.2.0; do not edit! + +//go:build rp2040 || rp2350 +package piolib +import ( + pio "github.com/tinygo-org/pio/rp2-pio" +) +// parallel6 + +const parallel6WrapTarget = 0 +const parallel6Wrap = 2 + +var parallel6Instructions = []uint16{ + // .wrap_target + 0x6006, // 0: out pins, 6 side 0 + 0xb042, // 1: nop side 1 + 0xa042, // 2: nop side 0 + // .wrap +} +const parallel6Origin = -1 +func parallel6ProgramDefaultConfig(offset uint8) pio.StateMachineConfig { + cfg := pio.DefaultStateMachineConfig() + cfg.SetWrap(offset+parallel6WrapTarget, offset+parallel6Wrap) + cfg.SetSidesetParams(1, false, false) + return cfg; +} + diff --git a/rp2-pio/statemachine.go b/rp2-pio/statemachine.go index 565cb0b..2381ffa 100644 --- a/rp2-pio/statemachine.go +++ b/rp2-pio/statemachine.go @@ -167,6 +167,17 @@ func (sm StateMachine) TxFIFOLevel() uint32 { return (sm.pio.hw.FLEVEL.Get() >> uint32(bitoffs)) & mask } +// IsTxStalled returns true if state machine has stalled. +// This value is sticky so it must be cleared with [StateMachine.ClearTxStalled] after reading true to be reset. +func (sm StateMachine) HasTxStalled() bool { + return sm.pio.hw.FDEBUG.HasBits(1 << (rp.PIO0_FDEBUG_TXSTALL_Pos + sm.index)) +} + +// ClearTxStalled clears the value of tx stall. See [StateMachine.HasTxStalled]. +func (sm StateMachine) ClearTxStalled() { + sm.pio.hw.FDEBUG.Set(1 << (rp.PIO0_FDEBUG_TXSTALL_Pos + sm.index)) +} + // IsTxFIFOEmpty returns true if state machine's TX FIFO is empty. func (sm StateMachine) IsTxFIFOEmpty() bool { return (sm.pio.hw.FSTAT.Get() & (1 << (rp.PIO0_FSTAT_TXEMPTY_Pos + sm.index))) != 0