Compare commits

...

9 Commits

Author SHA1 Message Date
David Anderson 2156d824c5 hardware/ulx3s: integrate new debugger, bump VRAM back to 128Kb 2024-09-19 20:55:27 -07:00
David Anderson 0d4855fc21 debugger: implement burst reading 2024-09-19 20:50:42 -07:00
David Anderson 789d8d002b debugger/Debugger: return correct magic from ping commands 2024-09-19 20:39:58 -07:00
David Anderson ee1aecf0b9 debugger/Debugger: adjust urgency annotations, use full VRAM in tests 2024-09-19 17:36:10 -07:00
David Anderson 1acec6d835 vram/VRAMCore: fix timing bug with slow readers
The internal EBR array must stall new operations if there's a pending
read result that hasn't been retired yet. All that clever stuff with
non-blocking DelayLines and all that? Yeah, spoiler alert, there's a
reason guarded FIFOs are the preferred API for this stuff. Play unsafe
games, win unsafe prizes.
2024-09-19 14:37:05 -07:00
David Anderson af91f1bda8 debugger/Debugger: add an actual debug module
Introduces a new serial wire format, and a ReadRange command to
dump larger chunks of memory at once.
2024-09-18 21:23:22 -07:00
David Anderson 770f515a72 delete log.txt, mistakenly committed 2024-09-16 19:24:07 -07:00
David Anderson 700a301468 debugger: more UI tweaks and playing with layout 2024-09-16 19:23:37 -07:00
David Anderson 8c729698a8 debugger: implement scrolling in the hex view 2024-09-16 16:10:24 -07:00
11 changed files with 684 additions and 189 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
.envrc .envrc
out/ out/
log.txt

180
debugger/Debugger.bsv Normal file
View File

@ -0,0 +1,180 @@
package Debugger;
import GetPut::*;
import ClientServer::*;
import FIFOF::*;
import SpecialFIFOs::*;
import VRAM::*;
export ReadRange(..);
export DebugRequest(..);
export DebugResponse(..);
export Debugger(..), mkDebugger;
// ReadRange is a request to read a range of bytes.
typedef struct {
VRAMAddr start;
UInt#(8) count;
} ReadRange deriving (Eq, FShow);
// DebugRequest is a request sent to the debugger.
typedef union tagged {
// Ping is a probe to check the connection to the debugger. The
// debugger responds with 0xEA (Ω in code page 437) to identify
// itself as a GARY debugger. The eschatological symbol for the end
// of everything seems appropriate for the level of desperation
// where you reach for the debug interface.
//
// Did you know Ω featured on the mission patch of STS-135, the
// final flight of the US Space Shuttle program?
//
// "Having fired the imagination of a generation,
// a ship like no other, its place in history secured,
// the space shuttle pulls into port for the last time.
// Its voyage, at an end."
void Ping;
// MemByteOp requests a read or write of a single byte of
// memory. The associated VRAMRequest gets passed unmodified to the
// memory port. For reads, produces a single VRAMResponse with the
// data byte. For writes, no response.
VRAMRequest MemByteOp;
// ReadRange requests a read of a range of memory. It's equivalent
// to N read MemByteOps, but the memory operations are pipelined,
// enabling 3x faster readout.
ReadRange ReadRange;
} DebugRequest deriving (Eq, FShow);
// DebugResponse is the debugger's response to a command. Depending on
// the command, one DebugRequest may result in 0, 1 or many
// DebugResponses. Requests are fully processed and responded to
// before following requests are processed.
typedef VRAMResponse DebugResponse;
// DebugCmd is the value of the Cmd byte in raw encoded RawDebugReq.
typedef enum {
Ping = 'h7F, // To force the enum to be Bit#(7)
ReadByte = 1,
WriteByte = 2,
ReadRange = 3
} DebugCmd deriving (Bits, Eq, FShow);
// RawDebugReq is the bit encoding of a DebugRequest. It is 32 bits
// for all commands, in a format that's reasonably easy to assemble
// from the client side.
typedef struct {
DebugCmd cmd;
VRAMAddr addr;
VRAMData data;
} RawDebugReq deriving (Bits, Eq, FShow);
instance Bits#(DebugRequest, 32);
function Bit#(32) pack(DebugRequest r);
case (r) matches
tagged Ping:
return pack(RawDebugReq{
cmd: Ping,
addr: 0,
data: 0
});
tagged MemByteOp .req:
if (req.data matches tagged Valid .data)
return pack(RawDebugReq{
cmd: WriteByte,
addr: req.addr,
data: data
});
else
return pack(RawDebugReq{
cmd: ReadByte,
addr: req.addr,
data: 0
});
tagged ReadRange .req:
return pack(RawDebugReq{
cmd: ReadRange,
addr: req.start,
data: unpack(pack(req.count))
});
endcase
endfunction
function DebugRequest unpack(Bit#(32) b);
RawDebugReq raw = unpack(b);
case (raw.cmd)
Ping: return tagged Ping;
ReadByte: return tagged MemByteOp VRAMRequest{addr: raw.addr, data: tagged Invalid};
WriteByte: return tagged MemByteOp VRAMRequest{addr: raw.addr, data: tagged Valid raw.data};
ReadRange: return tagged ReadRange ReadRange{start: raw.addr, count: unpack(pack(raw.data))};
endcase
endfunction
endinstance
// A Debugger provides debug access to GARY.
interface Debugger;
interface Server#(DebugRequest, DebugResponse) server;
interface Client#(VRAMRequest, VRAMResponse) vram;
endinterface
// mkDebugger constructs a GARY debugger.
(* descending_urgency="mem_response,issue_range,start_req" *)
module mkDebugger(Debugger);
FIFOF#(DebugRequest) req <- mkPipelineFIFOF();
FIFOF#(DebugResponse) resp <- mkBypassFIFOF();
FIFOF#(VRAMRequest) mem_req <- mkBypassFIFOF();
FIFOF#(VRAMResponse) mem_resp <- mkPipelineFIFOF();
Reg#(VRAMAddr) next_read_addr <- mkReg(0);
Reg#(UInt#(8)) read_issue_cnt <- mkReg(0);
Reg#(UInt#(8)) read_resp_cnt <- mkReg(0);
function Bool busy();
return read_resp_cnt != 0;
endfunction
rule start_req (req.notEmpty && !busy());
case (req.first) matches
tagged Ping: begin
req.deq();
resp.enq(DebugResponse{data: 'hEA});
end
tagged MemByteOp .op: begin
mem_req.enq(op);
req.deq();
if (op.data matches tagged Invalid)
read_resp_cnt <= 1;
end
tagged ReadRange .range: begin
mem_req.enq(VRAMRequest{
addr: range.start,
data: tagged Invalid
});
req.deq();
next_read_addr <= range.start+1;
read_issue_cnt <= range.count-1;
read_resp_cnt <= range.count;
end
endcase
endrule
rule issue_range (read_issue_cnt > 0);
mem_req.enq(VRAMRequest{
addr: next_read_addr,
data: tagged Invalid
});
next_read_addr <= next_read_addr+1;
read_issue_cnt <= read_issue_cnt-1;
endrule
rule mem_response (mem_resp.notEmpty);
resp.enq(mem_resp.first);
mem_resp.deq();
read_resp_cnt <= read_resp_cnt-1;
endrule
interface server = toGPServer(req, resp);
interface vram = toGPClient(mem_req, mem_resp);
endmodule
endpackage

132
debugger/Debugger_Test.bsv Normal file
View File

@ -0,0 +1,132 @@
package Debugger_Test;
import GetPut::*;
import ClientServer::*;
import Connectable::*;
import StmtFSM::*;
import Assert::*;
import VRAM::*;
import Debugger::*;
import Testing::*;
module mkTB();
let testflags <- mkTestFlags();
let cycles <- mkCycleCounter();
let vram <- mkVRAM(4);
let dut <- mkDebugger();
mkConnection(dut.vram, vram.debugger);
function Action put(DebugRequest req);
return action
if (testflags.verbose)
$display("%0d (%0d): Debugger.put( ", cycles.all, cycles, fshow(req), ")");
dut.server.request.put(req);
endaction;
endfunction
function ActionValue#(DebugResponse) get();
return actionvalue
let ret <- dut.server.response.get();
if (testflags.verbose)
$display("%0d (%0d): Debugger.get() = ", cycles.all, cycles, fshow(ret));
return ret;
endactionvalue;
endfunction
runTest(200,
mkTest("Debugger", seq
action
put(tagged Ping);
cycles.reset();
endaction
action
let pong <- get();
dynamicAssert(pong == VRAMResponse{data: 'hEA}, "wrong pong response");
dynamicAssert(cycles == 1, "wrong debugger delay");
endaction
put(tagged MemByteOp VRAMRequest { addr: 0, data: tagged Valid 0 });
put(tagged MemByteOp VRAMRequest { addr: 1, data: tagged Valid 42 });
put(tagged MemByteOp VRAMRequest { addr: 2, data: tagged Valid 66 });
put(tagged MemByteOp VRAMRequest { addr: 3, data: tagged Valid 91 });
put(tagged MemByteOp VRAMRequest { addr: 4, data: tagged Valid 154 });
put(tagged MemByteOp VRAMRequest { addr: 5, data: tagged Valid 201 });
put(tagged MemByteOp VRAMRequest { addr: 6, data: tagged Valid 243 });
action
put(tagged MemByteOp VRAMRequest { addr: 1, data: tagged Invalid });
cycles.reset();
endaction
action
let read <- get();
dynamicAssert(read == VRAMResponse{data: 42}, "wrong read response");
dynamicAssert(cycles == 3, "wrong debugger delay");
endaction
action
put(tagged MemByteOp VRAMRequest { addr: 2, data: tagged Invalid });
cycles.reset();
endaction
action
let read <- get();
dynamicAssert(read == VRAMResponse{data: 66}, "wrong read response");
dynamicAssert(cycles == 3, "wrong debugger delay");
endaction
action
put(tagged ReadRange ReadRange { start: 0, count: 7 });
cycles.reset();
endaction
put(tagged MemByteOp VRAMRequest { addr: 1, data: tagged Invalid });
action
let read <- get();
dynamicAssert(read == VRAMResponse{data: 0}, "wrong read response");
dynamicAssert(cycles == 3, "wrong debugger delay");
endaction
action
let read <- get();
dynamicAssert(read == VRAMResponse{data: 42}, "wrong read response");
dynamicAssert(cycles == 4, "wrong debugger delay");
endaction
action
let read <- get();
dynamicAssert(read == VRAMResponse{data: 66}, "wrong read response");
dynamicAssert(cycles == 5, "wrong debugger delay");
endaction
action
let read <- get();
dynamicAssert(read == VRAMResponse{data: 91}, "wrong read response");
dynamicAssert(cycles == 6, "wrong debugger delay");
endaction
action
let read <- get();
dynamicAssert(read == VRAMResponse{data: 154}, "wrong read response");
dynamicAssert(cycles == 7, "wrong debugger delay");
endaction
action
let read <- get();
dynamicAssert(read == VRAMResponse{data: 201}, "wrong read response");
dynamicAssert(cycles == 8, "wrong debugger delay");
endaction
action
let read <- get();
dynamicAssert(read == VRAMResponse{data: 243}, "wrong read response");
dynamicAssert(cycles == 9, "wrong debugger delay");
endaction
// Result of the final byte read
action
let read <- get();
dynamicAssert(read == VRAMResponse{data: 42}, "wrong read response");
dynamicAssert(cycles == 12, "wrong debugger delay");
endaction
endseq));
endmodule
endpackage

View File

@ -56,6 +56,17 @@ func (m *HexView) moveSelection(delta int) bool {
newAddr := m.selectedAddr + delta newAddr := m.selectedAddr + delta
if newAddr >= 0 && newAddr < m.mem.Len() { if newAddr >= 0 && newAddr < m.mem.Len() {
m.selectedAddr = newAddr m.selectedAddr = newAddr
start, end := m.VisibleBytes()
newFirstLine := newAddr / 16
if newAddr < start {
newFirstLine = newFirstLine - (m.height * 4 / 5)
newFirstLine = max(newFirstLine, 0)
m.firstAddr = newFirstLine * 16
} else if newAddr >= end {
newFirstLine = newFirstLine - (m.height * 1 / 5)
newFirstLine = min(newFirstLine, (m.mem.Len()/16)-m.height)
m.firstAddr = newFirstLine * 16
}
return true return true
} }
return false return false
@ -165,7 +176,7 @@ func (m HexView) updateViewMode(msg tea.Msg) (HexView, tea.Cmd) {
return m, nil return m, nil
} }
func (m HexView) View(height int) string { func (m HexView) View() string {
startAddr, endAddr := m.VisibleBytes() startAddr, endAddr := m.VisibleBytes()
bytes := m.mem.Slice(startAddr, endAddr) bytes := m.mem.Slice(startAddr, endAddr)

View File

@ -14,7 +14,7 @@ func main() {
} }
defer dbg.Close() defer dbg.Close()
mem := memory.New(dbg, 768) mem := memory.New(dbg, 4*1024)
if err := UI(mem); err != nil { if err := UI(mem); err != nil {
log.Fatal(err) log.Fatal(err)

View File

@ -1,15 +1,23 @@
package serial package serial
import ( import (
"encoding/binary"
"errors" "errors"
"fmt"
"io"
"sync" "sync"
"time" "time"
"go.bug.st/serial" "go.bug.st/serial"
) )
const portDev = "/dev/ttyUSB0" const (
const maxAddr = 1 << 17 portDev = "/dev/ttyUSB0"
portSpeed = 115_200
maxAddr = 1 << 17
pingResponse = 0xEA
readTimeout = 2 * time.Second
)
type Serial struct { type Serial struct {
portMu sync.Mutex portMu sync.Mutex
@ -25,6 +33,21 @@ func Open() (*Serial, error) {
return nil, err return nil, err
} }
packet := encode(cmdPing, 0, 0)
if _, err := port.Write(packet[:]); err != nil {
port.Close()
return nil, err
}
port.SetReadTimeout(readTimeout)
if _, err := io.ReadFull(port, packet[:1]); err != nil {
port.Close()
return nil, err
}
if packet[0] != pingResponse {
port.Close()
return nil, fmt.Errorf("wrong ping response: got %x, want %x", packet[0], pingResponse)
}
return &Serial{port: port}, nil return &Serial{port: port}, nil
} }
@ -34,42 +57,71 @@ func (d *Serial) Close() error {
return d.port.Close() return d.port.Close()
} }
func encode(addr int, write bool, data byte) [4]byte { const (
writeVal := uint8(0) cmdReadByte = 1
if write { cmdWriteByte = 2
writeVal = 1 cmdReadRange = 3
} cmdPing = 0x7F
)
func encode(cmd int, addr int, data byte) [4]byte {
cmd = cmd & (1<<7 - 1)
addr = addr & (1<<17 - 1)
raw := (uint32(cmd) << 25) + (uint32(addr) << 8) + uint32(data)
var ret [4]byte var ret [4]byte
ret[3] = data binary.BigEndian.PutUint32(ret[:], raw)
ret[2] = byte(addr<<1) | writeVal
ret[1] = byte(addr >> 7)
ret[0] = byte(addr >> 15)
return ret return ret
} }
func (d *Serial) ReadAt(bs []byte, off int64) (int, error) { // readSlow reads memory one byte at a time, and is 10x slower than
// burst reading. The code is preserved here just because it's a
// simpler implementation in the hardware, and is thus sometimes
// useful for debugging.
func (d *Serial) readSlow(bs []byte, off int64) (int, error) {
d.portMu.Lock() d.portMu.Lock()
defer d.portMu.Unlock() defer d.portMu.Unlock()
if off+int64(len(bs)) >= maxAddr { if off+int64(len(bs)) >= maxAddr {
return 0, errors.New("OOB read") return 0, errors.New("OOB read")
} }
for i := range bs { for i := range bs {
packet := encode(int(off)+i, false, 0) packet := encode(cmdReadByte, int(off)+i, 0)
if _, err := d.port.Write(packet[:]); err != nil { if _, err := d.port.Write(packet[:]); err != nil {
return i, err return i, err
} }
d.port.SetReadTimeout(2 * time.Second) d.port.SetReadTimeout(readTimeout)
n, err := d.port.Read(packet[:1]) n, err := io.ReadFull(d.port, bs[i:i+1])
if err != nil { if err != nil {
return i, err return i + n, err
} else if n == 0 {
return i, errors.New("short read")
} }
bs[i] = packet[0]
} }
return len(bs), nil return len(bs), nil
} }
func (d *Serial) readBurst(bs []byte, off int64) (int, error) {
d.portMu.Lock()
defer d.portMu.Unlock()
if off+int64(len(bs)) >= maxAddr {
return 0, errors.New("OOB read")
}
for i := 0; i < len(bs); i += 255 {
burstLen := min(len(bs)-i, 255)
packet := encode(cmdReadRange, int(off)+i, byte(burstLen))
if _, err := d.port.Write(packet[:]); err != nil {
return i, err
}
d.port.SetReadTimeout(readTimeout)
n, err := io.ReadFull(d.port, bs[i:i+burstLen])
if err != nil {
return i + n, err
}
}
return len(bs), nil
}
func (d *Serial) ReadAt(bs []byte, off int64) (int, error) {
return d.readBurst(bs, off)
}
func (d *Serial) WriteAt(bs []byte, off int64) (int, error) { func (d *Serial) WriteAt(bs []byte, off int64) (int, error) {
d.portMu.Lock() d.portMu.Lock()
defer d.portMu.Unlock() defer d.portMu.Unlock()
@ -77,7 +129,7 @@ func (d *Serial) WriteAt(bs []byte, off int64) (int, error) {
return 0, errors.New("OOB write") return 0, errors.New("OOB write")
} }
for i, v := range bs { for i, v := range bs {
packet := encode(int(off)+i, true, v) packet := encode(cmdWriteByte, int(off)+i, v)
if _, err := d.port.Write(packet[:]); err != nil { if _, err := d.port.Write(packet[:]); err != nil {
return i, err return i, err
} }

View File

@ -2,7 +2,6 @@ package main
import ( import (
"fmt" "fmt"
"log"
"git.sentinel65x.com/dave/gary/debugger/memory" "git.sentinel65x.com/dave/gary/debugger/memory"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
@ -14,7 +13,7 @@ func UI(mem *memory.Memory) error {
width: 0, width: 0,
height: 0, height: 0,
mem: mem, mem: mem,
bottomMsg: "Loading...", statusMsg: "Loading...",
} }
initial.hex = NewHexView(mem, initial.commitWrites) initial.hex = NewHexView(mem, initial.commitWrites)
initial.hex.AddrStyle = text initial.hex.AddrStyle = text
@ -31,11 +30,19 @@ func UI(mem *memory.Memory) error {
var ( var (
amber = lip.Color("#ffb00") amber = lip.Color("#ffb00")
slate = lip.Color("235") slate = lip.Color("235")
text = lip.NewStyle().Foreground(amber).Background(slate) text = lip.NewStyle().
faintText = text.Faint(true) Foreground(amber).
box = text.Border(lip.NormalBorder(), true, true, true, true). Background(slate).
BorderForeground(amber). BorderForeground(amber).
BorderBackground(slate) BorderBackground(slate).
Border(lip.NormalBorder(), false, false, false, false)
faintText = text.Faint(true)
box = lip.NewStyle().
Border(lip.NormalBorder()).
BorderForeground(amber).
BorderBackground(slate).
Padding(1, 2)
) )
type msgMemoryChanged struct { type msgMemoryChanged struct {
@ -56,27 +63,34 @@ type debugger struct {
mem *memory.Memory mem *memory.Memory
hex HexView hex HexView
lastErr error lastErr error
bottomMsg string statusMsg string
} }
func (m debugger) Init() tea.Cmd { func (m debugger) Init() tea.Cmd {
return tea.Sequence( loads := m.readFullMemoryCmds()
m.readFullMemory(), ret := make([]tea.Cmd, 0, (len(loads)*2)+1)
staticMsg(msgUpdateStatus{"Initial load complete."}), for i, c := range loads {
) ret = append(ret, staticMsg(msgUpdateStatus{fmt.Sprintf("Loading block %d/%d", i+1, len(loads))}), c)
}
ret = append(ret, staticMsg(msgUpdateStatus{"Load complete."}))
return tea.Sequence(ret...)
} }
func (m debugger) readFullMemory() tea.Cmd { func (m debugger) readFullMemory() tea.Cmd {
return tea.Sequence(m.readFullMemoryCmds()...)
}
func (m debugger) readFullMemoryCmds() []tea.Cmd {
var ret []tea.Cmd var ret []tea.Cmd
for i := 0; i < m.mem.Len(); i += 128 { for i := 0; i < m.mem.Len(); i += 128 {
ret = append(ret, m.readMemory(i, 128)) ret = append(ret, m.readMemory(i, 128))
} }
return tea.Sequence(ret...) return ret
} }
func (m debugger) readMemory(start, count int) tea.Cmd { func (m debugger) readMemory(start, count int) tea.Cmd {
return func() tea.Msg { return func() tea.Msg {
if err := m.mem.Load(start, count); err != nil { if err := m.mem.Refresh(start, count); err != nil {
return msgErr{fmt.Errorf("loading region 0x%x+%x: %w", start, count, err)} return msgErr{fmt.Errorf("loading region 0x%x+%x: %w", start, count, err)}
} }
return msgMemoryChanged{start, count} return msgMemoryChanged{start, count}
@ -103,13 +117,12 @@ func (m debugger) commitWrites() tea.Cmd {
} }
func (m debugger) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m debugger) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
log.Printf("msv: %#v", msg)
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
m.width = msg.Width m.width = msg.Width
m.height = msg.Height m.height = msg.Height
var cmd tea.Cmd var cmd tea.Cmd
m.hex, cmd = m.hex.Update(HexViewSetHeight{m.height - 6}) // TODO: make less shit m.hex, cmd = m.hex.Update(HexViewSetHeight{m.height - box.GetVerticalFrameSize() - 1})
return m, cmd return m, cmd
case tea.KeyMsg: case tea.KeyMsg:
switch msg.String() { switch msg.String() {
@ -118,13 +131,14 @@ func (m debugger) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "r": case "r":
start, end := m.hex.VisibleBytes() start, end := m.hex.VisibleBytes()
return m, m.readMemory(start, end-start) return m, m.readMemory(start, end-start)
case "R":
return m, m.readFullMemory()
} }
case msgErr: case msgErr:
log.Print(msg.err)
m.lastErr = msg.err m.lastErr = msg.err
return m, nil return m, nil
case msgUpdateStatus: case msgUpdateStatus:
m.bottomMsg = msg.status m.statusMsg = msg.status
} }
var cmd tea.Cmd var cmd tea.Cmd
m.hex, cmd = m.hex.Update(msg) m.hex, cmd = m.hex.Update(msg)
@ -158,20 +172,23 @@ func (m debugger) View() string {
return lip.Place(m.width, m.height, lip.Center, lip.Center, "Please embiggen your terminal") return lip.Place(m.width, m.height, lip.Center, lip.Center, "Please embiggen your terminal")
} }
statusBar := text.Width(m.width).Height(1).Align(lip.Center) hex := box.Render(m.hex.View())
topSegment := statusBar.Width(m.width / 3).Reverse(true)
hexBox := lip.NewStyle().Border(lip.NormalBorder()).BorderForeground(amber).BorderBackground(slate).Padding(1, 2) barText := text.Reverse(true) //.Height(1)
hexHeight := m.height - 2 - hexBox.GetVerticalFrameSize()
hex := hexBox.Render(m.hex.View(hexHeight))
topLeft := text.Reverse(true).Bold(true).Render("Addr: ") + text.Reverse(true).Render(fmt.Sprintf("0x%-5x", m.hex.SelectedAddr())) topLeft := barText.Bold(true).PaddingLeft(1).Render("Addr: ")
topLeft = topSegment.Padding(0, 1).Align(lip.Left).Render(topLeft) topLeft += barText.Render(fmt.Sprintf("0x%x (%d)", m.hex.SelectedAddr(), m.hex.SelectedAddr()))
topMid := topSegment.Bold(true).Render("GARY Debugger") topRight := barText.PaddingRight(1).Render(m.statusMsg)
topRight := topSegment.Render("") title := barText.Padding(0, 1).Bold(true).Render("GARY Debugger")
topStatus := lip.JoinHorizontal(lip.Top, topLeft, topMid, topRight)
bottomStatus := statusBar.Render(m.bottomMsg)
top := lip.JoinVertical(lip.Center, topStatus, hex, bottomStatus) sideWidth := max(lip.Width(topLeft), lip.Width(topRight))
return top titleWidth := m.width - 2*sideWidth
topLeft = barText.Width(sideWidth).Align(lip.Left).Render(topLeft)
topRight = barText.Width(sideWidth).Align(lip.Right).Render(topRight)
title = barText.Width(titleWidth).Align(lip.Center).Render(title)
status := lip.JoinHorizontal(lip.Top, topLeft, title, topRight)
all := lip.JoinVertical(lip.Center, status, hex)
return all
} }

View File

@ -8,18 +8,22 @@ import Blinky::*;
import PackUnpack::*; import PackUnpack::*;
import UART::*; import UART::*;
import VRAM::*; import VRAM::*;
import Debugger::*;
module mkUARTDebugger(Integer clock_frequency, Integer uart_bitrate, VRAMServer mem, UART_PHY ifc); module mkUARTDebugger(Integer clock_frequency, Integer uart_bitrate, VRAMServer mem, UART_PHY ifc);
UART uart <- mkUART(clock_frequency, uart_bitrate); UART uart <- mkUART(clock_frequency, uart_bitrate);
disableFlowControl(uart); // Can't do hardware flow control on ULX3S disableFlowControl(uart); // Can't do hardware flow control on ULX3S
let uart_client = toGPClient(uart.receive, uart.send);
Server#(Bit#(8), VRAMRequest) decode <- mkUnpacker(); Server#(Bit#(8), DebugRequest) decode <- mkUnpacker();
Server#(VRAMResponse, Bit#(8)) encode <- mkPacker(); Server#(DebugResponse, Bit#(8)) encode <- mkPacker();
let bytes_server = toGPServer(decode.request, encode.response);
let debug_client = toGPClient(decode.response, encode.request);
mkConnection(uart_client, bytes_server);
mkConnection(uart.receive, decode.request); let debug <- mkDebugger();
mkConnection(decode.response, mem.request); mkConnection(debug_client, debug.server);
mkConnection(mem.response, encode.request); mkConnection(debug.vram, mem);
mkConnection(encode.response, uart.send);
return uart.phy; return uart.phy;
endmodule endmodule
@ -38,7 +42,7 @@ endinterface
module mkTop(Top); module mkTop(Top);
//////////// ////////////
// Memory // Memory
VRAM mem <- mkVRAM(4); VRAM mem <- mkVRAM(128);
//////////// ////////////
// Debugging // Debugging

11
log.txt
View File

@ -1,11 +0,0 @@
2024/09/16 15:45:59 msv: tea.sequenceMsg{(tea.Cmd)(0x50b580), (tea.Cmd)(0x50b380)}
2024/09/16 15:45:59 msv: tea.sequenceMsg{(tea.Cmd)(0x50b5c0), (tea.Cmd)(0x50b5c0), (tea.Cmd)(0x50b5c0), (tea.Cmd)(0x50b5c0), (tea.Cmd)(0x50b5c0), (tea.Cmd)(0x50b5c0)}
2024/09/16 15:45:59 msv: tea.WindowSizeMsg{Width:141, Height:39}
2024/09/16 15:45:59 msv: main.msgUpdateStatus{status:"Initial load complete."}
2024/09/16 15:46:00 msv: main.msgMemoryChanged{start:0, count:128}
2024/09/16 15:46:00 msv: main.msgMemoryChanged{start:128, count:128}
2024/09/16 15:46:00 msv: main.msgMemoryChanged{start:256, count:128}
2024/09/16 15:46:00 msv: main.msgMemoryChanged{start:384, count:128}
2024/09/16 15:46:00 msv: main.msgMemoryChanged{start:512, count:128}
2024/09/16 15:46:00 msv: main.msgMemoryChanged{start:640, count:128}
2024/09/16 15:46:01 msv: tea.KeyMsg{Type:-1, Runes:[]int32{113}, Alt:false, Paste:false}

View File

@ -4,6 +4,8 @@ import GetPut::*;
import ClientServer::*; import ClientServer::*;
import BRAMCore::*; import BRAMCore::*;
import Real::*; import Real::*;
import FIFOF::*;
import SpecialFIFOs::*;
import DelayLine::*; import DelayLine::*;
import ECP5_RAM::*; import ECP5_RAM::*;
@ -21,9 +23,15 @@ typedef UInt#(17) VRAMAddr;
typedef UInt#(2) ArrayAddr; typedef UInt#(2) ArrayAddr;
typedef UInt#(3) ChipAddr; typedef UInt#(3) ChipAddr;
typedef UInt#(12) ByteAddr; typedef UInt#(12) ByteAddr;
typedef struct {
ChipAddr chip;
ByteAddr addr;
} EBRAddr deriving (Bits, Eq, FShow);
// ByteRAM is two EBRs glued together to make a whole-byte memory. typedef struct {
typedef EBR#(ByteAddr, VRAMData, ByteAddr, VRAMData) ByteRAM; EBRAddr addr;
Maybe#(data) data;
} VRAMInternalRequest#(type data) deriving (Bits, Eq, FShow);
typedef struct { typedef struct {
VRAMAddr addr; VRAMAddr addr;
@ -34,6 +42,11 @@ typedef struct {
VRAMData data; VRAMData data;
} VRAMResponse deriving (Bits, Eq, FShow); } VRAMResponse deriving (Bits, Eq, FShow);
interface VRAMCoreInternal#(type data);
interface Server#(VRAMInternalRequest#(data), data) portA;
interface Server#(VRAMInternalRequest#(data), data) portB;
endinterface
module mkNibbleRAM_ECP5(ChipAddr chip_addr, EBR#(ByteAddr, Bit#(4), ByteAddr, Bit#(4)) ifc); module mkNibbleRAM_ECP5(ChipAddr chip_addr, EBR#(ByteAddr, Bit#(4), ByteAddr, Bit#(4)) ifc);
EBRPortConfig cfg = defaultValue; EBRPortConfig cfg = defaultValue;
cfg.chip_select_addr = chip_addr; cfg.chip_select_addr = chip_addr;
@ -61,96 +74,159 @@ module mkNibbleRAM_Sim(ChipAddr chip_addr, EBR#(ByteAddr, Bit#(4), ByteAddr, Bit
endinterface endinterface
endmodule endmodule
module mkNibbleRAM(ChipAddr chip_addr, EBR#(ByteAddr, Bit#(4), ByteAddr, Bit#(4)) ifc); module mkNibbleRAM(ChipAddr chip_addr, VRAMCoreInternal#(Bit#(4)) ifc);
let _ret; let _ret;
if (genC()) if (genC())
_ret <- mkNibbleRAM_Sim(chip_addr); _ret <- mkNibbleRAM_Sim(chip_addr);
else else
_ret <- mkNibbleRAM_ECP5(chip_addr); _ret <- mkNibbleRAM_ECP5(chip_addr);
return _ret;
interface Server portA;
interface Put request;
method Action put(req);
_ret.portA.put(req.addr.chip, isValid(req.data), req.addr.addr, fromMaybe(0, req.data));
endmethod
endinterface
interface Get response;
method ActionValue#(Bit#(4)) get();
return _ret.portA.read();
endmethod
endinterface
endinterface
interface Server portB;
interface Put request;
method Action put(req);
_ret.portB.put(req.addr.chip, isValid(req.data), req.addr.addr, fromMaybe(0, req.data));
endmethod
endinterface
interface Get response;
method ActionValue#(Bit#(4)) get();
return _ret.portB.read();
endmethod
endinterface
endinterface
endmodule endmodule
// mkByteRAM glues two ECP5 EBRs together to make a 4096x8b memory // mkByteRAM glues two ECP5 EBRs together to make a 4096x8b memory
// block. Like the underlying ECP5 EBRs, callers must bring their own // block. Like the underlying ECP5 EBRs, callers must bring their own
// flow control to read out responses one cycle after putting a read // flow control to read out responses one cycle after putting a read
// request. // request.
module mkByteRAM(ChipAddr chip_addr, ByteRAM ifc); module mkByteRAM(ChipAddr chip_addr, VRAMCoreInternal#(VRAMData) ifc);
EBR#(ByteAddr, Bit#(4), ByteAddr, Bit#(4)) upper <- mkNibbleRAM(chip_addr); VRAMCoreInternal#(Bit#(4)) upper <- mkNibbleRAM(chip_addr);
EBR#(ByteAddr, Bit#(4), ByteAddr, Bit#(4)) lower <- mkNibbleRAM(chip_addr); VRAMCoreInternal#(Bit#(4)) lower <- mkNibbleRAM(chip_addr);
interface EBRPort portA; interface Server portA;
method Action put(ChipAddr chip_select, Bool write, ByteAddr addr, VRAMData data_in); interface Put request;
upper.portA.put(chip_select, write, addr, truncate(data_in>>4)); method Action put(req);
lower.portA.put(chip_select, write, addr, truncate(data_in)); Maybe#(Bit#(4)) ud = tagged Invalid;
endmethod Maybe#(Bit#(4)) ld = tagged Invalid;
if (req.data matches tagged Valid .data) begin
method VRAMData read(); ud = tagged Valid data[7:4];
return (extend(upper.portA.read())<<4) | (extend(lower.portA.read())); ld = tagged Valid data[3:0];
end
upper.portA.request.put(VRAMInternalRequest{
addr: req.addr,
data: ud
});
lower.portA.request.put(VRAMInternalRequest{
addr: req.addr,
data: ld
});
endmethod endmethod
endinterface endinterface
interface Get response;
interface EBRPort portB; method ActionValue#(VRAMData) get();
method Action put(ChipAddr chip_select, Bool write, ByteAddr addr, VRAMData data_in); let u <- upper.portA.response.get();
upper.portB.put(chip_select, write, addr, truncate(data_in>>4)); let l <- lower.portA.response.get();
lower.portB.put(chip_select, write, addr, truncate(data_in)); return {u, l};
endmethod endmethod
endinterface
endinterface
method VRAMData read(); interface Server portB;
return (extend(upper.portB.read())<<4) | (extend(lower.portB.read())); interface Put request;
method Action put(req);
Maybe#(Bit#(4)) ud = tagged Invalid;
Maybe#(Bit#(4)) ld = tagged Invalid;
if (req.data matches tagged Valid .data) begin
ud = tagged Valid data[7:4];
ld = tagged Valid data[3:0];
end
upper.portB.request.put(VRAMInternalRequest{
addr: req.addr,
data: ud
});
lower.portB.request.put(VRAMInternalRequest{
addr: req.addr,
data: ld
});
endmethod endmethod
endinterface endinterface
interface Get response;
method ActionValue#(VRAMData) get();
let u <- upper.portB.response.get();
let l <- lower.portB.response.get();
return {u, l};
endmethod
endinterface
endinterface
endmodule : mkByteRAM endmodule : mkByteRAM
// mkByteRAMArray arrays up to 8 mkByteRAMs together, using the // mkByteRAMArray arrays up to 8 mkByteRAMs together, using the
// hardwired chip select lines to route inputs appropriately and a mux // hardwired chip select lines to route inputs appropriately and a mux
// tree to collect outputs. With num_chips=8, the resulting ByteRAM is // tree to collect outputs. With num_chips=8, the resulting ByteRAM is
// 32768x8b. // 32768x8b.
module mkByteRAMArray(Integer num_chips, ByteRAM ifc); //
// The returned ByteRAM _does_ provide flow control: both read() and
// put() are guarded. If reads are consumed as soon as they're
// available, the RAM can process a put() every cycle.
module mkByteRAMArray(Integer num_chips, VRAMCoreInternal#(VRAMData) ifc);
if (num_chips > 8) if (num_chips > 8)
error("mkByteRAMArray can only array 8 raw ByteRAMs"); error("mkByteRAMArray can only array 8 raw ByteRAMs");
ByteRAM blocks[num_chips]; VRAMCoreInternal#(VRAMData) blocks[num_chips];
for (Integer i=0; i<num_chips; i=i+1) for (Integer i=0; i<num_chips; i=i+1)
blocks[i] <- mkByteRAM(fromInteger(i)); blocks[i] <- mkByteRAM(fromInteger(i));
DelayLine#(ChipAddr) read_chip_A <- mkDelayLine(1); FIFOF#(ChipAddr) read_chip_A <- mkPipelineFIFOF();
DelayLine#(ChipAddr) read_chip_B <- mkDelayLine(1); FIFOF#(ChipAddr) read_chip_B <- mkPipelineFIFOF();
interface EBRPort portA; interface Server portA;
method Action put(ChipAddr chip_select, Bool write, ByteAddr addr, VRAMData data_in); interface Put request;
method Action put(req) if (read_chip_A.notFull);
for (Integer i=0; i<num_chips; i=i+1) for (Integer i=0; i<num_chips; i=i+1)
blocks[i].portA.put(chip_select, write, addr, data_in); blocks[i].portA.request.put(req);
if (!write) if (!isValid(req.data))
read_chip_A <= chip_select; read_chip_A.enq(req.addr.chip);
endmethod
method VRAMData read();
if (read_chip_A.ready)
if (read_chip_A <= fromInteger(num_chips-1))
return blocks[read_chip_A].portA.read();
else
return 0;
else
return 0;
endmethod endmethod
endinterface endinterface
interface Get response;
method ActionValue#(VRAMData) get();
read_chip_A.deq();
let res <- blocks[read_chip_A.first].portA.response.get();
return res;
endmethod
endinterface
endinterface
interface EBRPort portB; interface Server portB;
method Action put(ChipAddr chip_select, Bool write, ByteAddr addr, VRAMData data_in); interface Put request;
method Action put(req) if (read_chip_B.notFull);
for (Integer i=0; i<num_chips; i=i+1) for (Integer i=0; i<num_chips; i=i+1)
blocks[i].portB.put(chip_select, write, addr, data_in); blocks[i].portB.request.put(req);
if (!write) if (!isValid(req.data))
read_chip_B <= chip_select; read_chip_B.enq(req.addr.chip);
endmethod endmethod
method VRAMData read(); endinterface
if (read_chip_B.ready) interface Get response;
if (read_chip_B <= fromInteger(num_chips-1)) method ActionValue#(VRAMData) get();
return blocks[read_chip_B].portB.read(); read_chip_B.deq();
else let res <- blocks[read_chip_B.first].portB.response.get();
return 0; return res;
else
return 0;
endmethod endmethod
endinterface endinterface
endinterface
endmodule endmodule
interface VRAMCore; interface VRAMCore;
@ -180,49 +256,57 @@ module mkVRAMCore(Integer num_kilobytes, VRAMCore ifc);
let num_byterams = num_bytes/4096; let num_byterams = num_bytes/4096;
let num_arrays = ceil(fromInteger(num_byterams) / 8); let num_arrays = ceil(fromInteger(num_byterams) / 8);
function Tuple3#(ArrayAddr, ChipAddr, ByteAddr) split_addr(VRAMAddr a); function Tuple2#(ArrayAddr, EBRAddr) split_addr(VRAMAddr a);
return unpack(pack(a)); return unpack(pack(a));
endfunction endfunction
ByteRAM arrays[num_arrays]; VRAMCoreInternal#(VRAMData) arrays[num_arrays];
for (Integer i=0; i<num_arrays; i=i+1) begin for (Integer i=0; i<num_arrays; i=i+1) begin
let array_size = min(num_byterams - (i*8), 8); let array_size = min(num_byterams - (i*8), 8);
arrays[i] <- mkByteRAMArray(array_size); arrays[i] <- mkByteRAMArray(array_size);
end end
Reg#(Maybe#(ArrayAddr)) inflight_A[2] <- mkCReg(2, tagged Invalid); FIFOF#(ArrayAddr) array_addr_A <- mkPipelineFIFOF();
Reg#(Maybe#(ArrayAddr)) inflight_B[2] <- mkCReg(2, tagged Invalid); FIFOF#(ArrayAddr) array_addr_B <- mkPipelineFIFOF();
interface Server portA; interface Server portA;
interface Put request; interface Put request;
method Action put(VRAMRequest req) if (inflight_A[1] matches tagged Invalid); method Action put(req);
match {.array, .chip, .byteaddr} = split_addr(req.addr); match {.array, .addr} = split_addr(req.addr);
arrays[array].portA.put(chip, isValid(req.data), byteaddr, fromMaybe(0, req.data)); arrays[array].portA.request.put(VRAMInternalRequest{
addr: addr,
data: req.data
});
if (!isValid(req.data)) if (!isValid(req.data))
inflight_A[1] <= tagged Valid array; array_addr_A.enq(array);
endmethod endmethod
endinterface endinterface
interface Get response; interface Get response;
method ActionValue#(VRAMResponse) get() if (inflight_A[0] matches tagged Valid .array); method ActionValue#(VRAMResponse) get();
inflight_A[0] <= tagged Invalid; array_addr_A.deq();
return VRAMResponse{data: arrays[array].portA.read()}; let ret <- arrays[array_addr_A.first].portA.response.get();
return VRAMResponse{data: ret};
endmethod endmethod
endinterface endinterface
endinterface endinterface
interface Server portB; interface Server portB;
interface Put request; interface Put request;
method Action put(VRAMRequest req) if (inflight_B[1] matches tagged Invalid); method Action put(req);
match {.array, .chip, .byteaddr} = split_addr(req.addr); match {.array, .addr} = split_addr(req.addr);
arrays[array].portB.put(0, isValid(req.data), byteaddr, fromMaybe(0, req.data)); arrays[array].portB.request.put(VRAMInternalRequest{
addr: addr,
data: req.data
});
if (!isValid(req.data)) if (!isValid(req.data))
inflight_B[1] <= tagged Valid array; array_addr_B.enq(array);
endmethod endmethod
endinterface endinterface
interface Get response; interface Get response;
method ActionValue#(VRAMResponse) get() if (inflight_B[0] matches tagged Valid .array); method ActionValue#(VRAMResponse) get();
inflight_B[0] <= tagged Invalid; array_addr_B.deq();
return VRAMResponse{data: arrays[array].portB.read()}; let ret <- arrays[array_addr_B.first].portB.response.get();
return VRAMResponse{data: ret};
endmethod endmethod
endinterface endinterface
endinterface endinterface

View File

@ -21,40 +21,46 @@ function ActionValue#(Bool) verbose();
endactionvalue); endactionvalue);
endfunction endfunction
typedef (function ActionValue#(Bit#(8)) next()) ValFn; module mkConstantValue(Integer cnst, Get#(Bit#(8)) ifc);
method ActionValue#(Bit#(8)) get();
function ValFn constant_value(Integer cnst);
function ActionValue#(Bit#(8)) next();
return (actionvalue
return fromInteger(cnst); return fromInteger(cnst);
endactionvalue); endmethod
endfunction endmodule
return next; module mkIncrementingValue(Get#(Bit#(8)));
endfunction
module mkIncrementingValue(ValFn);
Reg#(Bit#(8)) val <- mkReg(0); Reg#(Bit#(8)) val <- mkReg(0);
function ActionValue#(Bit#(8)) next(); method ActionValue#(Bit#(8)) get();
return (actionvalue // Cycle through 101 values. 101 is prime, so the pattern it
// Cycle through 101 values. 101 is prime, so the // generates doesn't align to a power of two and should detect
// pattern it generates doesn't align to a power of // any memory mapping errors.
// two and should detect any memory mapping errors.
if (val == 100) if (val == 100)
val <= 0; val <= 0;
else else
val <= val+1; val <= val+1;
// Add another number to get all nonzero values, to
// detect writes that don't stick.
return 23+val;
endactionvalue);
endfunction
return next; // Add another number to get all nonzero values, to detect
// writes that don't stick.
return 23+val;
endmethod
endmodule endmodule
module mkWriter(Server#(VRAMRequest, VRAMResponse) dut, ValFn next_value, Machine ifc); module mkSlowReader(Get#(Bit#(8)) inner, Get#(Bit#(8)) ifc);
Reg#(Bool) delay <- mkReg(True);
(* no_implicit_conditions,fire_when_enabled *)
rule clear_delay (delay);
delay <= False;
endrule
method ActionValue#(Bit#(8)) get() if (!delay);
delay <= True;
let ret <- inner.get();
return ret;
endmethod
endmodule
module mkWriter(Server#(VRAMRequest, VRAMResponse) dut, Get#(Bit#(8)) next_value, Machine ifc);
let flags <- mkTestFlags(); let flags <- mkTestFlags();
let cycles <- mkCycleCounter(); let cycles <- mkCycleCounter();
let write_cycle_time <- mkCycleCounter(); let write_cycle_time <- mkCycleCounter();
@ -67,7 +73,7 @@ module mkWriter(Server#(VRAMRequest, VRAMResponse) dut, ValFn next_value, Machin
dynamicAssert(write_cycle_time == 1, "write didn't happen every cycle"); dynamicAssert(write_cycle_time == 1, "write didn't happen every cycle");
write_cycle_time.reset(); write_cycle_time.reset();
let data <- next_value(); let data <- next_value.get();
let req = VRAMRequest{ let req = VRAMRequest{
addr: idx, addr: idx,
data: tagged Valid data data: tagged Valid data
@ -96,7 +102,7 @@ module mkWriter(Server#(VRAMRequest, VRAMResponse) dut, ValFn next_value, Machin
endmethod endmethod
endmodule endmodule
module mkReader(Server#(VRAMRequest, VRAMResponse) dut, ValFn next_value, Machine ifc); module mkReader(Server#(VRAMRequest, VRAMResponse) dut, Get#(Bit#(8)) next_value, Machine ifc);
let flags <- mkTestFlags(); let flags <- mkTestFlags();
let cycles <- mkCycleCounter(); let cycles <- mkCycleCounter();
@ -126,11 +132,11 @@ module mkReader(Server#(VRAMRequest, VRAMResponse) dut, ValFn next_value, Machin
rule verify_read (verify_remaining > 0); rule verify_read (verify_remaining > 0);
let got <- dut.response.get(); let got <- dut.response.get();
let want <- next_value(); let want <- next_value.get();
dynamicAssert(got.data == want, "wrong value seen during read");
if (flags.verbose) if (flags.verbose)
$display("%0d: verify_read(%0d) = %0d, want %0d", cycles.all, verify_idx, got, want); $display("%0d: verify_read(%0d) = %0d, want %0d", cycles.all, verify_idx, got, want);
dynamicAssert(got.data == want, "wrong value seen during read");
if (verify_remaining == 1) if (verify_remaining == 1)
$display("Verified %0d reads in %0d cycles", total, cycles); $display("Verified %0d reads in %0d cycles", total, cycles);
@ -187,17 +193,36 @@ module mkTwoPortTest(VRAMCore dut, Stmt ret);
endseq); endseq);
endmodule endmodule
module mkSlowConsumerTest(VRAMCore dut, Stmt ret);
let winc <- mkIncrementingValue();
let writer <- mkWriter(dut.portA, winc);
let rinc <- mkIncrementingValue();
let rinc_slow <- mkSlowReader(rinc);
let reader <- mkReader(dut.portA, rinc_slow);
return (seq
writer.start(3000, 6000);
await(writer.done);
reader.start(3000, 6000);
await(reader.done);
endseq);
endmodule
(* descending_urgency="simple.reader.issue_read,two_port.writer.write" *) (* descending_urgency="simple.reader.issue_read,two_port.writer.write" *)
module mkTB(Empty); module mkTB(Empty);
let dut <- mkVRAMCore(112); let dut <- mkVRAMCore(112);
let simple <- mkSimpleTest(dut); let simple <- mkSimpleTest(dut);
let two_port <- mkTwoPortTest(dut); let two_port <- mkTwoPortTest(dut);
let slow_reader <- mkSlowConsumerTest(dut);
runTest(100000, runTest(100000,
mkTest("VRAMCore", seq mkTest("VRAMCore", seq
mkTest("VRAMCore/simple", simple); //mkTest("VRAMCore/simple", simple);
mkTest("VRAMCore/two_port", two_port); //mkTest("VRAMCore/two_port", two_port);
mkTest("VRAMCore/slow_reader", slow_reader);
endseq)); endseq));
endmodule endmodule