This page describes the boot sequence of an Amstrad PCW, including the contents of its minimal "boot ROM". This information was derived from oscilloscope traces of the data and address lines at power-on.
In summary, the observed boot sequence is:
Execution begins at address 0000h.
The PCW comes up in a special "bootstrap mode". This mode of operation appears to be such that:
The "special" fetches are stretched using the /WAIT signal, and are very slow, a typical M1 cycle taking about 18.3us. The whole sequence takes about 15ms to execute.
Note that different data values are fetched from the same memory address at different times. Also, the location from which the boot ROM is copied depends on the startup value of the HL register, which "Undocumented Z80" says is indeterminate (but tends towards FFFFh, which is what was always observed).
This suggests that whatever is providing the data (gate array and/or printer controller) is ignoring the address lines for memory fetches, and returning a pre-ordered sequence of data for both program and data fetches.
The first instructions executed are additional to the 256 bytes of boot ROM which are usually quoted, and are mostly repetitive (particularly the LDIs). It may be that they are not in ROM per se, but are invented by logic in the gate array and/or printer controller on the fly.
Unlike the boot ROM "proper", there's no reason these first instructions would need to differ between different PCW models and revisions, although there's also no reason they couldn't differ anyway.
These unusual execution properties are presumably the reason for the ROM contents being copied to RAM for execution; it's not possible to execute a real program from the ROM, and in any case, it would probably be too slow to service the FDC.
Disassembly of these first instructions:
0000 AF XOR A ; A = 0 0001 D3 OUT (0F0h),A ; Select standard block 0 0002 F0 ; for both reads and writes ; 0000h..3FFFh 0003 11 LD DE,0002h 0004 02 0005 00 0006 ED LDI 0007 A0 0008 ED LDI 0009 A0 ; ... ; Snip boring repetitive bit. ; There are 256 LDI instructions in total. ; These copy 256 bytes from an indeterminate ; address range (observed as FFFFh..00FEh with ; wrapping) to 0002h..0101h. ; Note that the observed source address range overlaps ; with these instructions, but different data is ; fetched. ; ... 0204 ED LDI 0205 A0 0206 C3 JP 0000h 0207 00 0208 00 ; Note, different contents again for address 0. 0000 D3 OUT (0F8h),A ; A is still 0. 0001 F8 ; This is the "end bootstrap mode" sequence. ; Execution continues in the copied boot ROM, see ; below.
These instructions execute from RAM at what is plausibly full speed.
There must clearly be different variants of this code in different PCW models; according to John Elliott, the checksum calculation for the boot sector will be different, and different FDC parameters will presumably be required.
The disassembly is possibly ropey - it was done by hand, and I'm not that familiar with the 765 FDC, so the labels and comments are particularly suspect - but the object code should be accurate. Corrections are welcome.
0002 01 LD BC,83F3h 0003 F3 0004 83 ; Set up memory map. memmaplp: 0005 ED OUT (C),B 0006 41 0007 0D DEC C 0008 78 LD A,B 0009 05 DEC B 000A 87 ADD A,A 000B 20 JR NZ,memmaplp 000C F8 ; At this point, we have: ; F3 = 83 standard block 3 0xC000..0xFFFF ; F2 = 82 standard block 2 0x8000..0xBFFF ; F1 = 81 standard block 1 0x4000..0x7FFF ; F0 = 80 standard block 0 0x0000..0x3FFF 000D 31 LD SP,FFF0h ; stack below memory-mapped keyboard 000E F0 000F FF 0010 3E LD A,9 0011 09 0012 D3 OUT (0F8h),A ; start drive motor(s) 0013 F8 0014 11 LD DE,0732h ; E = wait_for_disc loop variable ; D affects no. beeps on error 0015 32 0016 07 ; Sit around for about 100 seconds, prodding the ; FDC every so often to see if there's a disc. wait_for_disc: 0017 06 LD B,0C8h 0018 C8 0019 DC CALL C,delay 001A B1 001B 00 001C CD CALL try_read 001D 84 001E 00 001F 1D DEC E 0020 F2 JP P,wait_for_disc 0021 17 0022 00 ; Timeout or error -- the bleeps of DOOM error: 0023 3E LD A,80h 0024 80 0025 D3 OUT (0F7h),A ; bright video 0026 F7 motor_off: 0027 0E LD C,9 ; inc to func 10 => motors off 0028 09 ; This loop will cycle through ; { 10=motors off, 11=beeper on, 12=beeper off } ; with equal delay between each. ; Motors-off does nothing after the first time; ; the effect is to give a 1/3 beeper duty cycle. ; If D=7 on entry we get three beeps. alarums: 0029 0C INC C 002A 79 LD A,C 002B D3 OUT (0F8h),A 002C F8 002D 06 LD B,21h 002E 21 002F CD CALL delay 0030 B1 0031 00 0032 CB BIT 2,C ; has C reached func 12 (beeper off)? 0033 51 0034 20 JR NZ,motor_off ; yes, reset C 0035 F1 0036 15 DEC D 0037 20 JR NZ,alarums 0038 F0 ; Wait for user to press space bar. 0039 21 LD HL,0FFF5h ; memory-mapped keyboard 003A F5 003B FF 003C 77 LD (HL),A ; A=10; ? keyblp: 003D CB BIT 7,(HL) ; space bar 003E 7E 003F 28 JR Z,keyblp 0040 FC 0041 3C INC A ; A=11; bleeper? 0042 D3 OUT (0F8h),A 0043 F8 ; fall through: ; Do something with interrupts, ; then read a sector from the disc to 0F000h. ; If successful, does not return but executes ; boot sector from disc. ; Clobbers: A ; Reduces E by 2 (we're less patient with unsuccessful ; reads than total absence of disc). fdc_read: 0044 1D DEC E 0045 1D DEC E 0046 3E LD A,6 ; clear FDC terminal count 0047 06 0048 D3 OUT (0F8h),A 0049 F8 004A CD CALL wait_clear 004B E4 004C 00 004D 09 DB 9 ; length of following: 004E 66 DB 66h ; READ DATA, ; MT=0 (multi-track=0), ; MF=1 (MFM), ; SK=1 (skip del. addr mark) 004F 00 DB 0 ; Head 0, unit 0 0050 00 DB 0 ; Cylinder 0051 00 DB 0 ; Head 0052 01 DB 1 ; Record 0053 02 DB 2 ; No. bytes in sector = 512 ; (+MT,MF = 15 sectors) 0054 01 DB 1 ; EOT (last sector in track) 0055 2A DB 2Ah ; GPL = gap length 0056 FF DB 0FFh ; DTL = data len (unused) ; control returns here: 0057 21 LD HL,0F000h ; stuff from FDC goes here 0058 00 0059 F0 fdcrdy3: 005A DB IN A,(0) ; FDC status register 005B 00 005C 87 ADD A,A 005D 30 JR NC,fdcrdy3 ; wait for data register ready (b7) 005E FB 005F 87 ADD A,A 0060 F2 JP P,fdcdone ; b5: 1=busy, 0=done (in which case jump) 0061 6B 0062 00 0063 ED INI ; C => (HL)++, B-- 0064 A2 0065 20 JR NZ,fdcrdy3 ; while any bit in status[6:0] 0066 F3 0067 7C LD A,H 0068 1F RRA ; b0 of H set? (HL=F1xx, F3xx, ...) 0069 38 JR C,fdcrdy3 ; yes, loop back 006A EF fdcdone: 006B 3E LD A,5 ; set FDC terminal count 006C 05 006D D3 OUT (0F8h),A ; terminate FDC operation? 006E F8 006F CD CALL fdcin ; get status of read 0070 C7 0071 00 0072 E6 AND 0CBh ; IC|NR|US = bad news 0073 CB 0074 C0 RET NZ ; return if bad news 0075 47 LD B,A ; B=0 0076 21 LD HL,0F010h ; start of boot sector code 0077 10 0078 F0 ; Boot sector checksum ; Sum 512 bytes F000..F1FF, starting and ending ; at F010. cklp: ; boot sector checksum 0079 24 INC H 007A 86 ADD A,(HL) 007B 25 DEC H 007C 86 ADD A,(HL) 007D 2C INC L 007E 10 DJNZ cklp ; 256 times 007F F9 0080 3C INC A ; sum should be 0FFh 0081 20 JR NZ,error ; oh dear. Complain 0082 A0 ; Transfer control to boot sector! 0083 E9 JP (HL) ; Try to read and execute bootsector. ; If successful, does not return. ; Clobbers: A, BC, HL ; May decrement E try_read: 0084 0E LD C,80h ; bright screen 0085 80 0086 CD CALL vid_fdc 0087 DB 0088 00 0089 05 DB 5 ; length of: 008A 03 DB 3 ; SPECIFY 008B 0F DB 0Fh ; SRT (stepping rate time) = 1ms, ; HUT (head unload time) = 240ms 008C FF DB 0FFh ; HLT (head load time) = 254ms, ; ND (non-DMA) = yes 008D 07 DB 7 ; RECALIBRATE (=> track 0) 008E 00 DB 0 ; unit 0 ; control returns here 008F CD CALL wait_int 0090 A5 0091 00 0092 06 LD B,0C8h 0093 C8 0094 38 JR C,delay ; delay and return to caller ; if the results were bad 0095 1B 0096 CD CALL fdc_read ; Try to read and execute bootsector 0097 44 0098 00 0099 CD CALL fdc_read ; Once more for luck 009A 44 009B 00 ; If we ended up here, we weren't successful ; reading the boot sector. 009C 0E LD C,0 ; dark screen 009D 00 009E CD CALL vid_fdc 009F DB 00A0 00 00A1 03 DB 3 ; length for: 00A2 0F DB 0Fh ; SEEK 00A3 00 DB 0 ; head 0, unit 0 00A4 14 DB 20 ; NCN (cyl = track) ; control returns here ; Wait for interrupt, check return status. ; Carry set = bad, clear = ok. ; Clobbers: A, BC, HL wait_int: ; wait for interrupt? 00A5 CD CALL fdc_int 00A6 BD 00A7 00 00A8 30 JR NC,wait_int ; NC => no interrupt ; A has ST0 from FDC response 00A9 FB 00AA 17 RLA ; bit 7 of ST0 00AB 38 JR C,wait_int ; IC=1x: invalid command or ; ready changed state during 00AC F8 00AD 17 RLA ; bit 6 of ST0 00AE D8 RET C ; IC=01: abnormal termination ; else IC=00, normal termination 00AF 06 LD B,14h ; delay then return to caller 00B0 14 ; fall through to: ; Delay loop. B is length of delay. ; Clobbers: A. (Preserves carry.) delay: 00B1 3E LD A,0B3h 00B2 B3 delaylp: ; inner loop controlled by A 00B3 E3 EX (SP),HL ; beefy NOPs? 00B4 E3 EX (SP),HL 00B5 E3 EX (SP),HL 00B6 E3 EX (SP),HL 00B7 3D DEC A 00B8 20 JR NZ,delaylp 00B9 F9 00BA 10 DJNZ delay 00BB F5 00BC C9 RET ; Sense/handle FDC interrupt ; None: flags = NC, Z (clobbers: A) ; Int: carry is set (clobbers: A, BC, HL) fdc_int: 00BD DB IN A,(0F8h) 00BE F8 00BF E6 AND 20h ; Disc controller interrupt status 00C0 20 00C1 C8 RET Z ; No interrupt, presumably ; Work out what the interrupt was. 00C2 CD CALL fdcout 00C3 E9 00C4 00 00C5 01 DB 1, 8 ; SENSE INTERRUPT STATUS 00C6 08 ; fall through (with C=1) to: ; Pull response from FDC ; First byte returned in A ; C must be 1 (fdcout leaves it that way) ; Clobbers: A, B, HL fdcin: 00C7 21 LD HL,fdc_response ; where FDC response data goes 00C8 02 00C9 01 fdcrdy2: 00CA DB IN A,(0) ; FDC status register 00CB 00 00CC 87 ADD A,A fdcinlp: 00CD 30 JR NC,fdcrdy2 ; wait for data register ready 00CE FB 00CF 3A LD A,(fdc_response) ; first byte of FDC response 00D0 02 00D1 01 00D2 F0 RET P ; status:d6=0 => FDC wants data (ie done) ; else d6=1, incoming data from FDC 00D3 ED INI ; from C => (HL)++, B-- 00D4 A2 00D5 E3 EX (SP),HL ; wait for at least 12us 00D6 E3 EX (SP),HL 00D7 E3 EX (SP),HL 00D8 E3 EX (SP),HL 00D9 18 JR fdcinlp ; more response data 00DA EF ; Change screen colour to C, wait for interrupt clear, ; then send FDC command. ; Clobbers: A, BC, HL? vid_fdc: 00DB DB IN A,(0F8h) 00DC F8 00DD E6 AND 40h ; Frame Flyback Time 00DE 40 00DF 28 JR Z,vid_fdc ; wait for frame flyback 00E0 FA 00E1 79 LD A,C 00E2 D3 OUT (0F7h),A ; video control 00E3 F7 ; fall through: ; Wait for an interrupt status to clear, ; then send FDC command. ; Clobbers: A, BC, HL? wait_clear: 00E4 CD CALL fdc_int 00E5 BD 00E6 00 00E7 38 JR C,wait_clear ; NC => no interrupt 00E8 FB ; fall through: ; Output a variable number of bytes to the FDC. ; Bytes are stored after CALL -- this function will ; modify return address. ; Side effect: sets C register to 1 (used elsewhere) ; Clobbers: A, BC fdcout: 00E9 E3 EX (SP),HL ; return address in HL 00EA 46 LD B,(HL) ; count of output data 00EB 23 INC HL ; move ret addr over count 00EC E3 EX (SP),HL 00ED 0E LD C,1 ; FDC data register port 00EE 01 fdclp: 00EF E3 EX (SP),HL ; return address in HL fdcrdy: 00F0 DB IN A,(0) ; FDC status register 00F1 00 00F2 87 ADD A,A 00F3 30 JR NC,fdcrdy ; wait for data register ready (b7) 00F4 FB 00F5 FA JP M,fdcrd ; b6 set => incoming data 00F6 FB ; else OK to write data out: 00F7 00 00F8 7E LD A,(HL) ; outgoing command byte 00F9 ED OUT (C),A ; to data register 00FA 79 fdcrd: 00FB 23 INC HL ; next command byte 00FC E3 EX (SP),HL ; wait for at least 12us 00FD E3 EX (SP),HL 00FE E3 EX (SP),HL ; return address now back on stack 00FF 10 DJNZ fdclp 0100 EE 0101 C9 RET ; having skipped bytes fdc_response: 0102 ; response bytes go here
Salient points about the environment the boot sector is executed in, derived from the above disassembly:
John Elliott has some more information about the format of PCW boot sectors.
(I have a half-finished disassembly of an 8000-series boot sector from years ago lying around somewhere.)
This scope has 16 data probes and 4 analogue probes. In order to verify the entire 16-bit address bus, 8-bit data bus, and various control lines, data from several runs was combined, with care taken to ensure that this gave a consistent view of events.
The PCW's edge connector is awkward to probe. However, an expansion pack (I used a CPS8256) with the case removed provides a convenient place to mount digital probes (see photo).
An example scope trace from this setup, taken at the point where we start running from RAM.
The fact that the ROM contents were copied out sequentially made this much easier than I thought it was going to be; I thought I'd have to find inventive ways of exercising every code path in the boot ROM in order to see it all, or end up with holes in my disassembly.