PCW boot sequence

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:

  1. Z80 held in reset for approximately 400ms after power-on.
  2. Execute instructions to copy 256 bytes from boot ROM into RAM. The last such instruction is "end bootstrap mode".
  3. Execute boot ROM instructions from RAM.
  4. These load a 512-byte boot sector from drive A and execute it.

First instructions executed

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.

Boot ROM "proper"

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

Boot sector from floppy

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.)

Method

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.


CP/M page. 2007 September 16; mail.