~shiny/gbcap

ref: 1da1303482353adde97934cacee4f402f4e1c118 gbcap/gbcap.py -rw-r--r-- 10.0 KiB
1da13034Thomas Spurden Add uart test helper 5 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
from migen import *
from migen.build.generic_platform import *
from migen.build.platforms import papilio_pro
from migen.genlib.fifo import SyncFIFO, SyncFIFOBuffered
from migen.genlib.cdc import MultiReg
from migen.genlib.io import DDRInput

import uart

# Number of lines to buffer. Since the UART can't output the frame data as fast
# as the Gameboy is generating it make this as big as will fit
BUFFER_LINES = 7 * 144

LINE_PX = 160
LINE_BITS = LINE_PX * 2
LINE_BYTES = LINE_BITS // 8

class Input(Module):
    def __init__(self, inp, out, ddr):
        if ddr:
            # DDR input, ignore pulses of <= 0.5 clock cycles to filter noise
            def one_bit(inp_bit, out_bit):
                ddr_inp = [Signal(), Signal()]
                self.specials += DDRInput(inp_bit, *ddr_inp)
                self.sync += If(ddr_inp[0] == ddr_inp[1], out_bit.eq(ddr_inp[0]))
        else:
            # Resync input through a MultiReg
            # No implementation of DDRInput in sim so use this
            def one_bit(inp_bit, out_bit):
                self.specials += MultiReg(inp_bit, out_bit)

        all_bits = []
        for bidx in range(inp.nbits):
            all_bits.append(Signal())
            one_bit(inp[bidx], all_bits[-1])
        self.comb += out.eq(Cat(*all_bits))

class PixelStream(Module):
    def __init__(self, lcd, ddr_inp):
        # data, eol gated by valid
        # eol signals last pixel in a line
        self.data = Signal(2, name='pixel_data')
        self.eol = Signal()
        self.valid = Signal(name='pixel_valid')
        self.frame_start = Signal()

        ###

        lcd_clk = Signal()
        lcd_cpl = Signal()
        lcd_hsync = Signal()
        lcd_vsync = Signal()
        lcd_pixel = Signal(2)

        self.submodules += [
            Input(lcd.clk, lcd_clk, ddr_inp),
            Input(lcd.cpl, lcd_cpl, ddr_inp),
            Input(lcd.hsync, lcd_hsync, ddr_inp),
            Input(lcd.vsync, lcd_vsync, ddr_inp),
            Input(lcd.pixel, lcd_pixel, ddr_inp),
        ]

        old_clk = Signal()
        old_cpl = Signal()
        old_vsync = Signal()
        old_pixel = Signal(2)

        self.sync += [
            # Keep old values to detect edges
            old_clk.eq(lcd_clk),
            old_cpl.eq(lcd_cpl),
            old_vsync.eq(lcd_vsync),
            # Actually sample the pixel data from the previous cycle
            old_pixel.eq(lcd_pixel),
        ]

        # Rising edge of vsync is frame start
        self.sync += [
            self.frame_start.eq(lcd_vsync & ~old_vsync)
        ]

        any_pixels = Signal()

        self.sync += [
            self.valid.eq(0),
            self.eol.eq(0),

            # read pixel on rising edge of clk or cpl
            # final pixel in each line is the one signalled by cpl
            If((lcd_clk & ~old_clk) | (lcd_cpl & ~old_cpl),
                self.data.eq(old_pixel),
                self.eol.eq(lcd_cpl),
                # Always generate a pixel on lcd_clk, but only for cpl if it was
                # preceeded by pixels generated from lcd_clk
                # cpl continues to pulse in the vblank but no pixels are
                # generated (lcd_clk=0)
                self.valid.eq(any_pixels | lcd_clk),
                any_pixels.eq(lcd_clk),
            ),
        ]

class LCDLineInput(Module):
    def __init__(self, lcd, capture, ddr_inp):
        self.valid = Signal(name='line_valid')
        # rightmost pixel in MSB, leftmost pixel in LSB
        self.line = Signal(LINE_BITS, name='line_shiftreg')
        self.frame_done = Signal()

        self.capturing = Signal(reset=0)

        self.submodules.pixels = PixelStream(lcd, ddr_inp)

        ###

        self.sync += [
            # Only start/stop capturing at the start of a frame
            If(self.pixels.frame_start, self.capturing.eq(capture)),
            self.frame_done.eq(self.pixels.frame_start & self.capturing)
        ]

        # The logic here isn't *exactly* what the Gameboy hardware implements -
        # eol doesn't actually shift the shift register, but this is simple and
        # works as the PixelStream supresses eols from empty (vsync) lines.
        self.sync += [
            self.valid.eq(0),
            If(self.capturing & self.pixels.valid,
                self.line.eq(Cat(self.line[2:], self.pixels.data)),
                self.valid.eq(self.pixels.eol)
            )
        ]

class PixelsToUART(Module):
    def __init__(self, lcd, serial, clk_period_ns, baud_rate, ddr_inp):
        self.capture = Signal(reset=0)

        self.submodules.lcd = LCDLineInput(lcd, self.capture, ddr_inp)
        self.submodules.fifo = SyncFIFOBuffered(LINE_BITS, BUFFER_LINES)

        self.submodules.uart = uart.RS232PHY(serial, clk_period_ns, baud_rate)
        self.frame_done = self.lcd.frame_done

        self.error = Signal()

        ###

        count = Signal(max=LINE_BYTES, reset=0)
        line = Signal(LINE_BITS - 8)

        self.sync += [
            # Start capture on frame start after UART rx
            If(self.uart.rx.valid, self.capture.eq(1)),
            # Stop on error (fifo overflow), and clear the error so it is
            # possible to start caturing again
            If(self.error,
                self.capture.eq(0),
                self.error.eq(0)),
        ]

        # Lines write straight into FIFO
        self.comb += [
            self.fifo.din.eq(self.lcd.line),
            self.fifo.we.eq(self.lcd.valid)
        ]

        # Set error flag when FIFO overflows
        self.sync += [
            If(self.lcd.valid & ~self.fifo.writable, self.error.eq(1))
        ]

        self.sync += [
            self.fifo.re.eq(0),
            self.uart.tx.valid.eq(0),
            # Not sending anything or final byte finished sending
            If((count == 0) | ((count == 1) & self.uart.tx.ready),
                # More to send, get right on it
                If(self.fifo.readable,
                    # Save all except first byte to line
                    self.fifo.re.eq(1),
                    line.eq(self.fifo.dout[8:]),
                    count.eq(LINE_BYTES),
                    # First byte of line straight to UART
                    self.uart.tx.valid.eq(1),
                    self.uart.tx.data.eq(self.fifo.dout[:8])
                ).Else(
                    # Nothing to send, reset into idle state
                    count.eq(0)
                )
            ).Elif(self.uart.tx.ready,
                # Next byte into UART
                self.uart.tx.valid.eq(1),
                self.uart.tx.data.eq(line[:8]),
                count.eq(count - 1),
                # Shift it off
                line.eq(Cat(line[8:], 0))
            )
        ]

if __name__ == '__main__':
    import sys
    if sys.argv[1] == 'build':
        plat = papilio_pro.Platform()

        plat.add_extension([
            ('gb_lcd', 0,
                Subsignal('vsync', Pins('B:5')),
                Subsignal('hsync', Pins('B:4')),
                Subsignal('cpl', Pins('B:6')),
                Subsignal('clk', Pins('B:2')),
                Subsignal('pixel', Pins('B:0', 'B:1')))])

        led = plat.request('user_led')
        lcd = plat.request('gb_lcd')
        m = Module()
        m.submodules.cap = PixelsToUART(lcd, plat.request('serial'), plat.default_clk_period, baud_rate=1000000, ddr_inp=True)
        m.comb += led.eq(m.cap.lcd.capturing)

        plat.build(m)

    elif sys.argv[1] == 'test':
        import vcd_parse
        import vcd2pixels

        def gen(dut):
            with open('6coins-title.vcd') as f:
                for t, signals in vcd_parse.parse(f):
                    yield dut.pads.vsync.eq(signals['vsync'])
                    yield dut.pads.hsync.eq(signals['hsync'])
                    yield dut.pads.clk.eq(signals['clk'])
                    yield dut.pads.cpl.eq(signals['cpl'])
                    yield dut.pads.pixel.eq(signals['data'])
                    yield

        def tx(dut):
            # Send a byte
            yield
            yield dut.loopback.tx.data.eq(88)
            yield dut.loopback.tx.valid.eq(1)
            yield
            yield dut.loopback.tx.valid.eq(0)

            # Wait for capture to start (byte received)
            while (yield dut.pixel_uart.capture) == 0:
                yield

            yield from gen(dut)

        def rx(dut):
            pixel_bytes = []
            with open('6coins-title.vcd') as f:
                packed = 0
                pack_count = 0
                for pixel in vcd2pixels.pixels(f):
                    # first (left-most) pixel in LSBs
                    packed |= pixel << (2 * pack_count)
                    pack_count += 1
                    if pack_count == 4:
                        pixel_bytes.append(packed)
                        pack_count = 0
                        packed = 0

            x = 0
            y = 0
            for b in pixel_bytes:
                while (yield dut.loopback.rx.valid) == 0:
                    yield

                v = (yield dut.loopback.rx.data)
                print('({}, {}) {:02x}'.format(x, y, v))
                assert v == b, 'rx: {:02x}, expected: {:02x}'.format(v, b)

                x += 1
                if x == LINE_BYTES:
                    x = 0
                    y += 1

                while (yield dut.loopback.rx.valid) == 1:
                    yield


        class DUT(Module):
            def __init__(self):
                self.pads = Record([('vsync', 1), ('hsync', 1), ('cpl', 1), ('clk', 1), ('pixel', 2)])
                self.serial_pads = Record([('tx', 1), ('rx', 1)])
                self.loop_serial_pads = Record([('tx', 1), ('rx', 1)])
                self.submodules.pixel_uart = PixelsToUART(self.pads, self.serial_pads, 31.25, baud_rate=1000000, ddr_inp=False)
                self.submodules.loopback = uart.RS232PHY(self.loop_serial_pads, 31.25, baud_rate=1000000)

                self.comb += [
                    self.serial_pads.rx.eq(self.loop_serial_pads.tx),
                    self.loop_serial_pads.rx.eq(self.serial_pads.tx)
                ]

        dut = DUT()
        run_simulation(dut, [tx(dut), rx(dut)], vcd_name='out.vcd')