From ccba646e81267889b7a1bb95d347c9434b55171a Mon Sep 17 00:00:00 2001 From: R0ck Date: Sun, 25 Oct 2026 01:50:46 +0000 Subject: [PATCH 1/2] renode: draw the RAK4631's SSD1306 over I2C The TWIM gains a slave registry and EasyDMA transfers, with the shortcuts the driver uses; an unclaimed address still NACKs. MeshBenchOled models an SSD1306 on it - control bytes, the column and page window, display on and off, page-addressed data - and sends the controller's memory as a 1-bit frame on the existing panel channel. Closes #602. --- CHANGELOG.md | 6 + internal/firmware/emulated/renode.go | 11 +- .../firmware/emulated/renode/oled_test.go | 20 ++ internal/firmware/emulated/renode/script.go | 14 + internal/sim/engine/boardsettings_test.go | 12 + internal/sim/engine/nodeparts.go | 19 +- tools/renode/peripherals/MeshBenchOled.cs | 319 ++++++++++++++++++ tools/renode/peripherals/NRF52840_TWIM.cs | 178 +++++++++- 8 files changed, 558 insertions(+), 21 deletions(-) create mode 100644 internal/firmware/emulated/renode/oled_test.go create mode 100644 tools/renode/peripherals/MeshBenchOled.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index a38e02b4..7fa09e13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,12 @@ had changed in them - which is the gap this file exists to close. ### Added +- **The RAK4631's display is drawn under Renode.** Its SSD1306 is on I2C, + and the TWIM model answered every address with a NACK. The TWIM now carries + devices registered at an address and moves each transfer's bytes through + EasyDMA, and a new SSD1306 model sends the controller's memory to the board + view. An address nobody claims is still a NACK. The published v1.17.1 + repeater draws its own boot screen. - **Script input at an instant of simulated time.** `console.type`, `console.cli` and the `companion.*` verbs that write to a node take `at_ms`: the call is held and runs when the run reaches that instant, so a script diff --git a/internal/firmware/emulated/renode.go b/internal/firmware/emulated/renode.go index 87017859..796c18b9 100644 --- a/internal/firmware/emulated/renode.go +++ b/internal/firmware/emulated/renode.go @@ -52,8 +52,14 @@ lora: Radio.VirtualSX1262 @ radiospi %d -> lora@0 `, renode.EasyDMASPI(e.SPIBase), e.SPIBase, engineHost, enginePort, e.IrqPort, e.IrqPin, e.NssPort, e.NssPin) - wiring += renode.Panel(e.SPIBase, e.PanelPort, e.PanelWidth, e.PanelHgt, - e.PanelCS, e.PanelDC, e.NssPort) + // A colour panel has a select line of its own; an I2C display has an + // address instead. + if e.PanelCS != 0 { + wiring += renode.Panel(e.SPIBase, e.PanelPort, e.PanelWidth, e.PanelHgt, + e.PanelCS, e.PanelDC, e.NssPort) + } else { + wiring += renode.Oled(e.PanelPort, e.PanelAddr, e.PanelWidth, e.PanelHgt, e.PanelOffset) + } wiring += renode.Inputs(e.ButtonPort, e.HasMeter) if err := os.WriteFile(repl, []byte(wiring), 0o644); err != nil { return "", err @@ -80,6 +86,7 @@ i @%[1]s/peripherals/VirtualSX1262Engine.cs i @%[1]s/peripherals/VirtualSX1262.cs i @%[1]s/peripherals/MeshBenchInputs.cs i @%[1]s/peripherals/MeshBenchPanel.cs +i @%[1]s/peripherals/MeshBenchOled.cs i @%[1]s/peripherals/NRF52840_Temp.cs i @%[1]s/peripherals/NRF52840_Clock.cs i @%[1]s/peripherals/NRF52840_SAADC.cs diff --git a/internal/firmware/emulated/renode/oled_test.go b/internal/firmware/emulated/renode/oled_test.go new file mode 100644 index 00000000..4acaf4a3 --- /dev/null +++ b/internal/firmware/emulated/renode/oled_test.go @@ -0,0 +1,20 @@ +package renode + +import ( + "strings" + "testing" +) + +// An I2C display hangs off the first TWIM at its own address, told its glass +// and where its columns start (#602). +func TestAnI2CDisplayIsRegisteredAtItsAddress(t *testing.T) { + got := Oled(4242, 0x3C, 128, 64, 0) + for _, want := range []string{"Video.MeshBenchOled @ twi0 0x3C", "port: 4242", "width: 128", "height: 64", "columnOffset: 0"} { + if !strings.Contains(got, want) { + t.Errorf("missing %q in:\n%s", want, got) + } + } + if Oled(0, 0x3C, 128, 64, 0) != "" || Oled(4242, 0, 128, 64, 0) != "" { + t.Error("a display with no channel or no address was wired anyway") + } +} diff --git a/internal/firmware/emulated/renode/script.go b/internal/firmware/emulated/renode/script.go index 14f8b531..2c4a80b0 100644 --- a/internal/firmware/emulated/renode/script.go +++ b/internal/firmware/emulated/renode/script.go @@ -64,6 +64,20 @@ func Panel(radioBase uint32, port, width, height, cs, dc int, dcPort string) str `, stockSPIName, port, width, height, cs, dc, dcPort, cs, cs, dc, dc) } +// Oled is an I2C display on the first TWIM, at its address. offset is where +// the glass starts in the controller's columns: 2 for an SH1106. +func Oled(port int, addr uint8, width, height, offset int) string { + if port == 0 || addr == 0 || width <= 0 || height <= 0 { + return "" + } + return fmt.Sprintf(`oled: Video.MeshBenchOled @ twi0 0x%X + port: %d + width: %d + height: %d + columnOffset: %d +`, addr, port, width, height, offset) +} + func UnregisterStockSPI() string { return "sysbus Unregister sysbus." + stockSPIName + "\n" } diff --git a/internal/sim/engine/boardsettings_test.go b/internal/sim/engine/boardsettings_test.go index 5a292ef1..fb5e160a 100644 --- a/internal/sim/engine/boardsettings_test.go +++ b/internal/sim/engine/boardsettings_test.go @@ -160,3 +160,15 @@ func shortNodeFS(t *testing.T) string { t.Cleanup(func() { _ = os.RemoveAll(dir) }) return dir } + +// The RAK4631's SSD1306 is drawn under Renode now, so the window must say so +// rather than say the gap is ours (#602). +func TestTheRAK4631DisplayIsModelled(t *testing.T) { + b, err := hw.BoardByName("RAK_4631") + if err != nil { + t.Fatal(err) + } + if !ScreenModelled(b) { + t.Error("the RAK4631's I2C display reads as not modelled") + } +} diff --git a/internal/sim/engine/nodeparts.go b/internal/sim/engine/nodeparts.go index f15e15b2..ac3677c8 100644 --- a/internal/sim/engine/nodeparts.go +++ b/internal/sim/engine/nodeparts.go @@ -149,6 +149,11 @@ func withParts(node *emulated.EmulatedNode, board hw.Board, spec scenario.Node, node.PanelCS, node.PanelDC = p.Screen.CS, p.Screen.DC node.PanelWidth, node.PanelHgt = p.Screen.WidthPx, p.Screen.HeightPx } + // Renode's I2C display model is told its glass too; QEMU's takes it + // from the controller it emulates. + if p.Screen.Bus == hw.BusI2C && board.Renode != nil { + node.PanelWidth, node.PanelHgt = p.Screen.WidthPx, p.Screen.HeightPx + } } return node, nil } @@ -162,11 +167,10 @@ func withParts(node *emulated.EmulatedNode, board hw.Board, spec scenario.Node, // nothing draws must read as "no display modelled" and never as "silent", which // says the firmware chose not to draw. // -// QEMU has both buses. Renode has the SPI panels - an ST7789 and an ST7735, -// sharing the radio's controller and told apart by chip select, which is how -// they are wired in copper - and not the I2C one: its TWIM model answers an -// address with a NACK because until now no board under it had anything on that -// bus to answer. +// QEMU has both buses. So does Renode: the SPI panels - an ST7789 and an +// ST7735, sharing the radio's controller and told apart by chip select, which +// is how they are wired in copper - and an SSD1306 on I2C, registered on the +// TWIM at its address. func ScreenModelled(board hw.Board) bool { p := board.Hardware if p == nil || p.Screen == nil { @@ -178,6 +182,11 @@ func ScreenModelled(board hw.Board) bool { // other port would be wired wrong rather than unsupported. Both boards // that carry one have it on the radio's port; this is the check that // says so rather than a comment claiming it. + // An I2C display hangs off the first TWIM at its address, where + // MeshBenchOled answers for an SSD1306 or an SH1106 (#602). + if p.Screen.Bus == hw.BusI2C { + return p.Screen.Controller == "SSD1306" || p.Screen.Controller == "SH1106" + } return p.Screen.Bus == hw.BusSPI && p.Screen.CS != 0 && p.Screen.DC != 0 && onSamePort(p.Screen.CS, board.Renode.NssPort) && diff --git a/tools/renode/peripherals/MeshBenchOled.cs b/tools/renode/peripherals/MeshBenchOled.cs new file mode 100644 index 00000000..73748c0f --- /dev/null +++ b/tools/renode/peripherals/MeshBenchOled.cs @@ -0,0 +1,319 @@ +// +// A board's monochrome OLED, under Renode: an SSD1306 on I2C. +// +// The RAK4631's display, and the last board panel that was not drawn (#602). +// It registers on the TWIM at its address, so the controller answers the +// address rather than NACKing it, and every transfer's bytes arrive here. +// +// The part's protocol on I2C: each transfer starts with a control byte. 0x00 +// says commands follow, 0x40 that display data does, and a set bit 7 (0x80, +// 0xC0) that only one byte follows before another control byte. The driver +// (Adafruit_SSD1306) sets a column and page window and then streams the +// buffer in, so that is what is modelled: the addressing commands, display on +// and off, and the data. Contrast, charge pump, multiplex and the rest change +// how the glass looks, not what is on it, and are consumed with their +// arguments and otherwise ignored. +// +// Like MeshBenchPanel, nothing here improves on the input. The frame is the +// controller's own memory, page-addressed as the part lays it out - a byte per +// column per page of eight rows, bit 0 at the top - and it is not sent until +// the firmware has written something: nothing drawn is not black. +// +using System; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Threading; +using Antmicro.Renode.Logging; +using Antmicro.Renode.Peripherals.I2C; + +namespace Antmicro.Renode.Peripherals.Video +{ + public class MeshBenchOled : II2CPeripheral, IDisposable + { + public MeshBenchOled(int port, int width = 128, int height = 64, int columnOffset = 0) + { + this.port = port; + this.width = width; + this.height = height; + this.columnOffset = columnOffset; + pages = height / 8; + ram = new byte[width * pages]; + Reset(); + Start(); + } + + public void Reset() + { + Array.Clear(ram, 0, ram.Length); + colStart = 0; + colEnd = width - 1; + pageStart = 0; + pageEnd = pages - 1; + col = 0; + page = 0; + horizontal = false; + on = false; + drawn = false; + pending.Clear(); + need = 0; + } + + // One I2C write: a control byte, then commands or data. + public void Write(byte[] data) + { + var i = 0; + while(i < data.Length) + { + var control = data[i++]; + var single = (control & 0x80) != 0; + var isData = (control & 0x40) != 0; + if(single) + { + if(i < data.Length) + { + Byte(data[i++], isData); + } + continue; + } + while(i < data.Length) + { + Byte(data[i++], isData); + } + } + } + + // The SSD1306 can be read only for its status byte, which says the + // display is on in bit 6 inverted. Nothing MeshCore drives reads it. + public byte[] Read(int count = 1) + { + var status = (byte)(on ? 0x00 : 0x40); + var answer = new byte[count]; + for(var k = 0; k < count; k++) + { + answer[k] = status; + } + return answer; + } + + public void FinishTransmission() { } + + public void Dispose() + { + running = false; + var c = client; + client = null; + if(c != null) + { + try { c.Close(); } catch(Exception) { } + } + } + + private void Byte(byte b, bool isData) + { + if(isData) + { + Data(b); + return; + } + if(need > 0) + { + pending.Add(b); + need--; + if(need == 0) + { + Apply(); + } + return; + } + Command(b); + } + + private void Command(byte b) + { + pending.Clear(); + pending.Add(b); + need = ArgsOf(b); + if(need == 0) + { + Apply(); + } + } + + private static int ArgsOf(byte b) + { + switch(b) + { + case 0x20: case 0x81: case 0x8D: case 0xA8: case 0xD3: + case 0xD5: case 0xD9: case 0xDA: case 0xDB: + return 1; + case 0x21: case 0x22: case 0xA3: + return 2; + case 0x29: case 0x2A: + return 5; + case 0x26: case 0x27: + return 6; + default: + return 0; + } + } + + private void Apply() + { + var b = pending[0]; + switch(b) + { + case 0x20: + // Memory addressing mode: 0 horizontal, 1 vertical, 2 page. The + // driver uses horizontal; page mode is what the part resets to. + horizontal = (pending[1] & 0x03) == 0; + break; + case 0x21: + colStart = Clamp(pending[1] - columnOffset, width - 1); + colEnd = Clamp(pending[2] - columnOffset, width - 1); + col = colStart; + break; + case 0x22: + pageStart = Clamp(pending[1] & 0x07, pages - 1); + pageEnd = Clamp(pending[2] & 0x07, pages - 1); + page = pageStart; + break; + case 0xAE: + on = false; + Send(); + break; + case 0xAF: + on = true; + Send(); + break; + default: + if(b >= 0xB0 && b <= 0xB7) + { + page = Clamp(b & 0x07, pages - 1); // page mode: page start + } + else if(b <= 0x0F) + { + col = Clamp((col & 0xF0) | b, width - 1); + } + else if(b >= 0x10 && b <= 0x1F) + { + col = Clamp(((b & 0x0F) << 4) | (col & 0x0F), width - 1); + } + break; + } + pending.Clear(); + } + + private static int Clamp(int v, int max) + { + return v < 0 ? 0 : (v > max ? max : v); + } + + private void Data(byte b) + { + ram[page * width + col] = b; + drawn = true; + if(!horizontal) + { + // Page mode: the column advances and wraps within the page. + col = col >= width - 1 ? 0 : col + 1; + return; + } + col++; + if(col > colEnd) + { + col = colStart; + page++; + if(page > pageEnd) + { + page = pageStart; + // The window is full: the firmware has finished drawing. + Send(); + } + } + } + + private void Start() + { + running = true; + thread = new Thread(Serve) { IsBackground = true, Name = "meshbench-oled" }; + thread.Start(); + } + + private void Serve() + { + while(running) + { + try + { + client = new TcpClient("127.0.0.1", port) { NoDelay = true }; + } + catch(Exception e) + { + if(!running) + { + return; + } + this.Log(LogLevel.Debug, "oled: not connected yet: {0}", e.Message); + Thread.Sleep(200); + continue; + } + this.Log(LogLevel.Info, "oled: {0}x{1} connected on port {2}", width, height, port); + return; + } + } + + // One frame in the shape the workbench reads: MBF2, the size, one bit + // a pixel, whether the panel is on, then the controller's memory. + private void Send() + { + if(!drawn) + { + return; + } + var c = client; + if(c == null) + { + return; + } + var header = new byte[] + { + (byte)'M', (byte)'B', (byte)'F', (byte)'2', + (byte)(width & 0xFF), (byte)((width >> 8) & 0xFF), + (byte)(height & 0xFF), (byte)((height >> 8) & 0xFF), + 1, (byte)(on ? 1 : 0), + }; + lock(sendLock) + { + try + { + var wire = c.GetStream(); + wire.Write(header, 0, header.Length); + wire.Write(ram, 0, ram.Length); + } + catch(Exception) + { + client = null; + } + } + } + + private readonly int port; + private readonly int width; + private readonly int height; + private readonly int pages; + private readonly int columnOffset; + private readonly byte[] ram; + private readonly List pending = new List(); + private readonly object sendLock = new object(); + + private int need; + private int colStart, colEnd, pageStart, pageEnd, col, page; + private bool horizontal; + private bool on; + private bool drawn; + + private volatile bool running; + private TcpClient client; + private Thread thread; + } +} diff --git a/tools/renode/peripherals/NRF52840_TWIM.cs b/tools/renode/peripherals/NRF52840_TWIM.cs index fbaeb30c..e78c9d7d 100644 --- a/tools/renode/peripherals/NRF52840_TWIM.cs +++ b/tools/renode/peripherals/NRF52840_TWIM.cs @@ -12,19 +12,27 @@ // 3c1da: ldr.w r2, [r3, #0x150] ; EVENTS_TXSTARTED // 3c1e0: beq.n 0x3c1d4 // -// What this models is a bus with nothing on it, which is the truth: MeshBench -// simulates radio, not sensors. A transfer starts and then the address goes -// unanswered, so ERROR is raised with the address-NACK bit. Firmware probing -// for a sensor gets the answer real hardware would give on a board without one -// - not there - and carries on, rather than waiting for a reply that cannot come. +// What this models is a bus with only what the board declares on it. An +// address nobody claims goes unanswered, so ERROR is raised with the +// address-NACK bit: firmware probing for a sensor gets the answer real hardware +// gives on a board without one - not there - and carries on, rather than +// waiting for a reply that cannot come. That stays the default. // +// A device registered at an address (a display, `oled: ... @ twi0 0x3C`) is +// answered instead: the transfer's bytes move through EasyDMA between memory +// and the device, the AMOUNT registers say how many, and the shortcuts the +// driver set decide what follows. That is the slave registry #602 asked for. +// +using System.Collections.Generic; using Antmicro.Renode.Core; +using Antmicro.Renode.Core.Structure; using Antmicro.Renode.Core.Structure.Registers; using Antmicro.Renode.Peripherals.Bus; namespace Antmicro.Renode.Peripherals.I2C { - public class NRF52840_TWIM : BasicDoubleWordPeripheral, IKnownSize + public class NRF52840_TWIM : BasicDoubleWordPeripheral, IKnownSize, + IPeripheralContainer> { public NRF52840_TWIM(IMachine machine) : base(machine) { @@ -32,6 +40,50 @@ public NRF52840_TWIM(IMachine machine) : base(machine) Reset(); } + public void Register(II2CPeripheral peripheral, NumberRegistrationPoint registrationPoint) + { + slaves[registrationPoint.Address] = peripheral; + machine.RegisterAsAChildOf(this, peripheral, registrationPoint); + } + + public void Unregister(II2CPeripheral peripheral) + { + foreach(var at in new List(slaves.Keys)) + { + if(slaves[at] == peripheral) + { + slaves.Remove(at); + } + } + machine.UnregisterAsAChildOf(this, peripheral); + } + + public IEnumerable> GetRegistrationPoints(II2CPeripheral peripheral) + { + var points = new List>(); + foreach(var kv in slaves) + { + if(kv.Value == peripheral) + { + points.Add(new NumberRegistrationPoint(kv.Key)); + } + } + return points; + } + + public IEnumerable>> Children + { + get + { + var children = new List>>(); + foreach(var kv in slaves) + { + children.Add(Registered.Create(kv.Value, new NumberRegistrationPoint(kv.Key))); + } + return children; + } + } + public long Size => 0x1000; public GPIO IRQ { get; } = new GPIO(); @@ -48,6 +100,11 @@ private void DefineRegisters() Registers.Stop.Define(this) .WithValueField(0, 32, FieldMode.Write, writeCallback: (_, __) => { + II2CPeripheral slave; + if(slaves.TryGetValue((int)address.Value, out slave)) + { + slave.FinishTransmission(); + } stopped.Value = true; UpdateInterrupt(); }, name: "TASKS_STOP"); @@ -75,10 +132,27 @@ private void DefineRegisters() Registers.LastTx.Define(this).WithFlag(0, out lastTx, name: "EVENTS_LASTTX") .WithWriteCallback((_, __) => UpdateInterrupt()); - // ERRORSRC: bit 1 is address NACK, which is what an empty bus gives. + // ERRORSRC: bit 1 is address NACK, which is what an empty address gives. Registers.ErrorSource.Define(this) .WithValueField(0, 32, FieldMode.Read | FieldMode.WriteOneToClear, - valueProviderCallback: _ => AddressNack, name: "ERRORSRC"); + valueProviderCallback: _ => errorSource, + writeCallback: (_, v) => errorSource &= ~(uint)v, name: "ERRORSRC"); + + Registers.Shorts.Define(this) + .WithValueField(0, 32, out shorts, name: "SHORTS"); + Registers.Address.Define(this) + .WithValueField(0, 7, out address, name: "ADDRESS") + .WithReservedBits(7, 25); + Registers.RxdPtr.Define(this).WithValueField(0, 32, out rxdPtr, name: "RXD.PTR"); + Registers.RxdMaxCnt.Define(this).WithValueField(0, 16, out rxdMaxCnt, name: "RXD.MAXCNT") + .WithReservedBits(16, 16); + Registers.RxdAmount.Define(this).WithValueField(0, 16, FieldMode.Read, + valueProviderCallback: _ => rxdAmount, name: "RXD.AMOUNT").WithReservedBits(16, 16); + Registers.TxdPtr.Define(this).WithValueField(0, 32, out txdPtr, name: "TXD.PTR"); + Registers.TxdMaxCnt.Define(this).WithValueField(0, 16, out txdMaxCnt, name: "TXD.MAXCNT") + .WithReservedBits(16, 16); + Registers.TxdAmount.Define(this).WithValueField(0, 16, FieldMode.Read, + valueProviderCallback: _ => txdAmount, name: "TXD.AMOUNT").WithReservedBits(16, 16); Registers.InterruptEnableSet.Define(this) .WithValueField(0, 32, FieldMode.Read | FieldMode.Set, @@ -96,26 +170,85 @@ private void DefineRegisters() valueProviderCallback: _ => enabled ? 6u : 0u, name: "ENABLE"); } - // Start, then fail to find anybody. Both events, in that order, because - // firmware polls TXSTARTED to know the controller took the job and ERROR - // to know how it ended. + // Start, then either find nobody - the NACK an empty address gives - + // or move the bytes to or from the device that is there. Both events + // in that order, because firmware polls STARTED to know the controller + // took the job and ERROR or STOPPED to know how it ended. private void Transfer(bool receiving) { if(receiving) { rxStarted.Value = true; - lastRx.Value = true; } else { txStarted.Value = true; + } + II2CPeripheral slave; + if(!slaves.TryGetValue((int)address.Value, out slave)) + { + if(receiving) + { + lastRx.Value = true; + } + else + { + lastTx.Value = true; + } + errorSource = AddressNack; + error.Value = true; + stopped.Value = true; + UpdateInterrupt(); + return; + } + if(receiving) + { + var want = (int)rxdMaxCnt.Value; + var got = slave.Read(want) ?? new byte[0]; + var n = System.Math.Min(want, got.Length); + sysbus.WriteBytes(got, rxdPtr.Value, n); + rxdAmount = (uint)n; + lastRx.Value = true; + if((shorts.Value & ShortLastRxStop) != 0) + { + Stop(slave); + } + else if((shorts.Value & ShortLastRxStartTx) != 0) + { + Transfer(receiving: false); + return; + } + } + else + { + var n = (int)txdMaxCnt.Value; + var bytes = sysbus.ReadBytes(txdPtr.Value, n); + slave.Write(bytes); + txdAmount = (uint)n; lastTx.Value = true; + if((shorts.Value & ShortLastTxStop) != 0) + { + Stop(slave); + } + else if((shorts.Value & ShortLastTxStartRx) != 0) + { + Transfer(receiving: true); + return; + } + else if((shorts.Value & ShortLastTxSuspend) != 0) + { + suspended.Value = true; + } } - error.Value = true; - stopped.Value = true; UpdateInterrupt(); } + private void Stop(II2CPeripheral slave) + { + slave.FinishTransmission(); + stopped.Value = true; + } + private void UpdateInterrupt() { var pending = @@ -130,6 +263,15 @@ private void UpdateInterrupt() } private const uint AddressNack = 1u << 1; + private const ulong ShortLastTxStartRx = 1u << 7; + private const ulong ShortLastTxSuspend = 1u << 8; + private const ulong ShortLastTxStop = 1u << 9; + private const ulong ShortLastRxStartTx = 1u << 10; + private const ulong ShortLastRxStop = 1u << 12; + + private readonly Dictionary slaves = new Dictionary(); + private IValueRegisterField shorts, address, rxdPtr, rxdMaxCnt, txdPtr, txdMaxCnt; + private uint rxdAmount, txdAmount, errorSource; private IFlagRegisterField stopped, error, suspended, rxStarted, txStarted, lastRx, lastTx; private uint interruptEnabled; @@ -149,10 +291,18 @@ private enum Registers : long TxStarted = 0x150, LastRx = 0x15C, LastTx = 0x160, + Shorts = 0x200, InterruptEnableSet = 0x304, InterruptEnableClear = 0x308, ErrorSource = 0x4C4, Enable = 0x500, + RxdPtr = 0x534, + RxdMaxCnt = 0x538, + RxdAmount = 0x53C, + TxdPtr = 0x544, + TxdMaxCnt = 0x548, + TxdAmount = 0x54C, + Address = 0x588, } } } From bc53354b5322516e8e388a8c52a415d2bdefcf68 Mon Sep 17 00:00:00 2001 From: R0ck Date: Sun, 25 Oct 2026 01:56:03 +0000 Subject: [PATCH 2/2] boardview: the undrawable-panel test makes its own board --- internal/ui/workbench/boardview/rows_test.go | 23 ++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/internal/ui/workbench/boardview/rows_test.go b/internal/ui/workbench/boardview/rows_test.go index 415ba56c..abe37401 100644 --- a/internal/ui/workbench/boardview/rows_test.go +++ b/internal/ui/workbench/boardview/rows_test.go @@ -232,9 +232,9 @@ func TestABoardWithNoRecordedPanelStillHasARadio(t *testing.T) { // reporting that as silence sends somebody looking for a fault in a firmware // that is behaving perfectly. // -// The RAK4631 is the one left. Its display is on I2C, and Renode's TWIM model -// answers an address with a NACK, so there is nowhere to put an SSD1306 yet - -// where the two SPI panels beside it are drawn. +// Every Renode board in the catalogue has its panel drawn now (the RAK4631's +// SSD1306 was the last, #602), so the case is made by hand: the same board +// with a controller nothing models, an e-paper one. func TestAnUndrawablePanelBlamesUsRatherThanTheFirmware(t *testing.T) { b, err := hw.BoardByName("RAK_4631") if err != nil { @@ -243,24 +243,29 @@ func TestAnUndrawablePanelBlamesUsRatherThanTheFirmware(t *testing.T) { if b.Renode == nil || b.Hardware == nil || b.Hardware.Screen == nil { t.Fatal("this test needs a Renode board that declares a screen") } + panel := *b.Hardware + screen := *panel.Screen + screen.Controller = "UC8151" + panel.Screen = &screen + b.Hardware = &panel running := &state.NodeStat{Name: "n", Board: b.Name, Running: true} - var screen *Row + var row *Row rows := wiringRows(b, running) for i := range rows { if rows[i].Group == "Display" { - screen = &rows[i] + row = &rows[i] break } } - if screen == nil { + if row == nil { t.Fatal("a board that declares a screen produced no display row") } - if screen.Verdict == Silent { + if row.Verdict == Silent { t.Errorf("a panel with no model behind it read as %v, which says the "+ "firmware chose not to draw", Silent) } - if screen.Verdict != NotModelled { - t.Errorf("read %v (%q), want %v", screen.Verdict, screen.Observed, NotModelled) + if row.Verdict != NotModelled { + t.Errorf("read %v (%q), want %v", row.Verdict, row.Observed, NotModelled) } }