forked from derekfountain/zx-spectrum-pico-interface-one
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathnotebook.txt
448 lines (289 loc) · 13.5 KB
/
notebook.txt
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
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
ZX Spectrum Pico Interface One
==============================
Implement an Interface One using a Pico. Currently envisaged as a software only
microdrive emulator, providing a high volume storage facility the traditional
way. I wasn't planning doing an actual Microdrive emulation, or the network.
Pico-1 Emulates the IF1 ROM
===========================
Error Handling
--------------
The Service Manual says the Spectrum transfers to address 0x08 when it hits an
error. Putting a breakpoint on that address in Fuse shows it gets hit a various
points. Enter garbage and it gets hit when you'd expect to see the ? symbol come
up. It gets hit at the "D Break - CONT repeats" message. It seems to be the
standard error handler point.
0x08 is called via a RST 8 instruction. That's got CALL semantics, it PUSHes the
PC ready for a RET.
There's a disassembly of the IF1 ROM here: https://www.tablix.org/~avian/spectrum/rom/if1_2.htm
The IF1 hardware traps the address and Z80 lines when it goes to read an instruction from
address 0x08. Thus, when the ROM (or user program) calls that address the IF1 ROM is
already in place.
How does the ROM get paged?
---------------------------
Hardware looks for 0x0008.
AAAA AAAA AAAA AAAA
1111 1100 0000 0000
5432 1098 7654 3210
0000 0000 0000 1000
* * *** *** Marked address lines are fed into IC3
That's bit A3 being one, all others being zero.
Also, hardware looks for 0x1708.
AAAA AAAA AAAA AAAA
1111 1100 0000 0000
5432 1098 7654 3210
0001 0111 0000 1000
* * *** *** Marked address lines are fed into IC3
That's bit A3, A8, A9, A10 and A12 being one, all others being zero.
How does the ROM get unpaged?
-----------------------------
UNPAGE is at 0x0700 in the IF1 disassembly.
AAAA AAAA AAAA AAAA
1111 1100 0000 0000
5432 1098 7654 3210
0000 0111 0000 0000
* * *** *** Marked address lines are fed into IC3
That's bits A8, A9 and A10 being one, all others being zero.
It contains a single RET instruction so the stack needs to have the required return
address ready. Assumption is that the IF1 ROM is unpaged when address 0x0700 has been
read. Not sure how.
From Kiwi at Spectrum Computing:
There are three addresses where the paging mechanism kicks in:
0x0008 - error RST - page in
0x1708 - channel handler - page in
0x0700 - page out
The LS260 NOR gates, diode AND gate and ULA decode these addresses
along with M1 and MREQ to determine when a paging event occurs. If you
decode all the other address bits then A3 indicates if the shadow ROM
should be paged in or out.
In hardware terms, the paging mechanism can be thought of as a two
stage process. The first stage flags if a paging event has occurred -
it decodes the address bits, MREQ and M1 and uses the signal as a
clock input to a LS74 flip flop, with A3 being the data input. The
second stage delays the actual paging of the ROM to the next
instruction. It does this with a second flip flop where the clock
input is the falling edge of M1 and data is the stage 1 page flag. The
combined mechanism recognises the page address and delays the ROM page
until the next M1 cycle (when it pages immediately).
This means that the CPU picks up the return instruction in the IF1 ROM
at 0x0700, executes the instruction (changing the PC), and the on CPU
setting up the address bus to get the instruction at the new PC
location, i.e. M1 active, the IF1 ROM pages out immediately meaning
the CPU picks up the value from the 48K ROM/RAM and normal execution
continues.
Pcio-1 Conclusion
-----------------
This is implemented in my ROM emulator. It requires the entire Pico,
all 26 GPIOs. The magic address ROM paging is done in software.
Proven working, final version needs the /M1 signal which I've got in
place on the schematic.
Pico-2 Emulates the IF1 Hardware
================================
Data Bus (8 GPIOs required)
---------------------------
8 lines required, all need level shifting and direction swapping. Input from ZX is needed
for the IF1 to receive the data to be written to the microdrive. Output to the ZX is
needed for the ROM emulation and to pass read data out to the ZX.
The ROM emulation only needs output data lines. The ROM emulator project ties the
direction of the level shifter to Pico->ZX. That needs changing because Pico-2 needs
to handle INs (Pico->ZX) and OUTs (ZX->Pico).
Z80 Control Bus (3 GPIOs required)
----------------------------------
The ROM part uses /MREQ; Pico-2 needs /IORQ.
/RD and /WR are required so Pico-2 can tell when it needs to field an IN or an OUT.
Pico-1 only needs Pico->ZX, which is B->A, which is low on the pin. Pico-2 needs
both directions. But I can't have Pico-1 driving the data bus when Pico-2 wants
it, so I need to update the ROM emulation software to keep the data bus GPIOs as
inputs until they need to output their value.
The data bus level shifter should be permanently enabled. I'll tie /RD to the
shifter's direction pin then a read from ROM or a read of the IO space will set the
shifter to Pico->ZX, switching straight back when the read finishes. The ROM code
(Pico-1) needs to keep the data bus GPIOs as inputs while it's not supplying a data
byte. A write-to-ROM won't change the level shifter, so that scenario is harmless.
ROM_ACCESS_INV now appears redundant.
One issue: one of the Picos will have it's databus GPIOs set as outputs. When the
Z80's read finishes the /RD will go high and the level shifter will drive its
3V3 side as outputs. I don't want both Pico and level shifter driving each other,
but there's no easy alternative. So I'll put some resistors inline to prevent
too much current flowing from one device to the other. It'll only be brief, I
think it'll be OK.
Pico-3 Handles the user interface
=================================
Currently assuming an SD card reader and an LCD screen. Some buttons or a rotary?
Not sure. This comes later.
I think this is the one AM uses:
https://github.com/carlk3/no-OS-FatFS-SD-SPI-RPi-Pico/tree/sdio
How does Fuse code work?
========================
The Z80 opcode handler for IN uses a function called readport(). That's in periph.c, it loops
over a list of 'port' structures calling a function called read_peripheral(). That looks at
the list entry and the port being read by the Z80 and calls the handler function for either
a port read or a port write if the IN instruction the Z80 is running matches what's been
registered. The IF1's port input and output functions are if1_port_in() and if1_port_out():
static const periph_port_t if1_ports[] = {
{ 0x0018, 0x0010, if1_port_in, if1_port_out },
{ 0x0018, 0x0008, if1_port_in, if1_port_out },
{ 0x0018, 0x0000, if1_port_in, if1_port_out },
{ 0, 0, NULL, NULL }
};
Putting a breakpoint on one and doing a CAT 1 command gives:
(gdb) bt
#0 if1_port_in (port=24559, attached=0x7fffffffdc46 "") at peripherals/if1.c:787
#1 0x0000555555588aad in read_peripheral (data=<optimised out>, user_data=0x7fffffffdc44) at periph.c:304
#2 0x00007ffff7154b20 in g_slist_foreach () at /lib/x86_64-linux-gnu/libglib-2.0.so.0
#3 0x0000555555589057 in readport_internal (port=24559) at periph.c:344
#4 readport_internal (port=<optimised out>) at periph.c:312
#5 0x00005555555891a4 in readport (port=port@entry=24559) at periph.c:277
#6 0x00005555555ed965 in z80_do_opcodes () at ./z80/opcodes_base.c:944
#7 0x000055555558001d in main (argc=<optimised out>, argv=<optimised out>) at fuse.c:203
Decoding the port gives 1 of 3 responses:
static enum if1_port
decode_port( libspectrum_word port )
{
switch( port & 0x0018 ) {
case 0x0000: return PORT_MDR;
case 0x0008: return PORT_CTR;
case 0x0010: return PORT_NET;
default: return PORT_UNKNOWN;
}
}
0xF7 & 0x18 gives 0x10, so port 0xF7 is PORT_NET (network)
0xEF & 0x18 gives 0x08, so port 0xEF is PORT_CTR (control)
0xE7 & 0x18 gives 0x00, so port 0xE7 is PORT_MDR (microdrive)
The one I want is the MDR one, so if1_port_in() for a read calls port_mdr_in(). Which
is this:
static libspectrum_byte
port_mdr_in( void )
{
libspectrum_byte ret = 0xff;
int m;
for( m = 0; m < 8; m++ ) {
microdrive_t *mdr = µdrive[ m ];
if( mdr->motor_on && mdr->inserted ) {
if( mdr->transfered < mdr->max_bytes ) {
mdr->last = libspectrum_microdrive_data( mdr->cartridge,
mdr->head_pos );
increment_head( m );
}
mdr->transfered++;
ret &= mdr->last; /* I assume negative logic, but how know? */
}
}
return ret;
}
So for each microdrive, if it's on and inserted, read some data, increment the head
position. This seems to work with the idea that more than one microdrive can be
running, which is not possible in hardware as far as I know. Nevertheless, it merges
the bytes read from each device and that's what it returns. It also returns the next
byte in the data sequence (i.e on the tape) for each IN instruction.
I think what's happening is that each IN instruction is assumed to be reading the
next byte from the IF1 ULA. The IF1 does all the timing stuff, then says right,
/now/ is the time to read the next data byte from the stream. And that's what it
gets. In practise it doesn't matter what timing it puts in place, it'll get the next
byte in the stream regardless.
Logic to operate the level shifter
==================================
To switch the level shifter on the data bus to output mode (Pico->ZX) the ROM
read negative logic is
!(MREQ OR A14 OR A15 OR RD)
So if A14/5, or MREQ, or RD are high, keep it at input mode (ZX->Pico).
Otherwise pull it low (Pico->ZX).
So that's
(MREQ AND A14 AND A15 AND RD)
The IO Pico also needs to be able to switch that level shifter to output
mode (Pico->ZX)
(IORQ AND RD)
So if it's an IO, and it's a read, pull the level shifter direction low
(Pico->ZX).
So that's
!(IORQ OR RD)
/MREQ A14 A15 DIR /IORQ /RD DIR
0 0 0 0 0 0 0 IO read (IN instruction)
0 0 1 1 0 1 1 IO write (OUT instruction)
0 1 0 1 1 0 1 Mem read
0 1 1 1 1 1 1 Mem write
1 0 0 1
1 0 1 1
1 1 0 1
1 1 1 1
DIR = ( (MREQ OR A14 OR A15) AND (IORQ OR RD) )
gpios_state when broken on OUT 223,0 0x1C507A00
0b11100010100000111101000000000
0 0000 1111 0000 0111 1111 0000 0000 = 0x00F07F00 is the mask
0b1 1100 1101 0000 0111 1010 0000 0000
AAAA xxxx xAAA AWRI xxxx xxxx
7654 321 0RDO
R
One clock cycle on 3.5MHz Z80 is 285ns. IORQ is down for 2.5 clocks, which is
about 715ns. That lines up perfectly with the scope trace.
Cartridge Loading
=================
Pico2
-----
typedef struct _md_flash_slot
{
uint8_t cartridge_present;
libspectrum_microdrive cartridge;
}
MD_FLASH_SLOT;
On start up force empty slots. A new installation will set the 8 flash
slots with cartridge_present=0 and zeroes in the cartridge structure.
Pico3 can hold persistent MDR data and re-insert the last cartridges
used if necessary.
Like Fuse, start with everything empty, the UI Pico will need to load
MDR images, either through the GUI or via an auto-restore thing.
For test purposes, keep an MDR image in Pico2 flash. Load that into
slot 1 on startup. Need to check the "MD not present" thing works if
this is skipped, so might need a dummy image for loading into RAM.
On motor_on, load from slot 1 into RAM. For test, only use the first
slot (MD 1).
New cartridge takes a slot, 1 to 8, do whatever Fuse "Insert new" does.
I could save a new (unformatted) image in Fuse for this. Probably easier
to build what it wants in memory.
On motor_on, memcpy() from flash slot into RAM slot.
On motor_off, memcpy() from RAM back to flash slot.
Fuse.
I don't think I can do automatic Pico2 flash back to Pico3 SD card.
It'd need doing on every sector write. So that leaves the Fuse model:
the user will have to choose Save for each cartridge image. Maybe
add a single button to save all cartridges back to SD.
* Insert new
* Insert
* Eject
* Save to SD card
* Save As to SD card
* Write protect enable/disable
The FORMAT Issue
================
Disassembly:
https://www.tablix.org/~avian/spectrum/rom/if1_2.htm#L1B5D
Start the motor, check for write protect.
https://www.tablix.org/~avian/spectrum/rom/if1_2.htm#L1B75
Enable writing - don't know what this does. Apparently nothing.
Creates an unuable record: header, record, EOF, checksum
It then works down from sector number 255 to 0
Writes out a header
Small gap
Writes out record https://www.tablix.org/~avian/spectrum/rom/if1_2.htm#L16AF writes the 0xFC
Small gap
Loop back to do all 255
All sectors from 255 down to 1 have been written
THIS APPEARS TO WORK
https://www.tablix.org/~avian/spectrum/rom/if1_2.htm#L1BDF
Turn on appropriate drive motor
Counter to 50
Read next header from heads
Check whether sector read is free in the map
If sector is free, or bad read, or bad checksum
Move to next sector
Else
Mark sector as usable in sector map
https://www.tablix.org/~avian/spectrum/rom/if1_2.htm#L1C35
; Now prepare to overwrite the unusable sectors (which are mapped as usable)
; with record descriptors which are usable.
Updates the in-memory sector image with a useable one
https://www.tablix.org/~avian/spectrum/rom/if1_2.htm#L1C40
https://www.tablix.org/~avian/spectrum/rom/if1_2.htm#L15B3
Write usable block to next free sector
Mark map showing sector is now used
OTIR is here:
https://www.tablix.org/~avian/spectrum/rom/if1_2.htm#L15D0