| /* hp3000_mem.c: HP 3000 main memory simulator | |
| Copyright (c) 2016-2018, J. David Bryan | |
| Permission is hereby granted, free of charge, to any person obtaining a copy | |
| of this software and associated documentation files (the "Software"), to deal | |
| in the Software without restriction, including without limitation the rights | |
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| copies of the Software, and to permit persons to whom the Software is | |
| furnished to do so, subject to the following conditions: | |
| The above copyright notice and this permission notice shall be included in | |
| all copies or substantial portions of the Software. | |
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN | |
| ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | |
| WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
| Except as contained in this notice, the name of the author shall not be used | |
| in advertising or otherwise to promote the sale, use or other dealings in | |
| this Software without prior written authorization from the author. | |
| MEM HP 3000 Series III Main Memory | |
| 21-May-18 JDB Changed "access" to "mem_access" to avoid clashing | |
| 10-Oct-16 JDB Created | |
| References: | |
| - HP 3000 Series II/III System Reference Manual | |
| (30000-90020, July 1978) | |
| - HP 3000 Series III Engineering Diagrams Set | |
| (30000-90141, April 1980) | |
| The HP 3000 Memory Subsystem is an integral part of the 3000 computer. | |
| Replacing the core memory used in the earlier 3000 CX machines, the Series II | |
| introduced an all-semiconductor memory using 4K NMOS RAMs that provided error | |
| detection and correction. Single-bit errors are corrected automatically, and | |
| double-bit errors are detected. All errors are logged in hardware, and the | |
| logs are downloaded periodically by MPE to allow preventative maintenance and | |
| replacement of failing parts. | |
| The Series II supports a main memory size of 64K to 256K words in 32K | |
| increments. It uses four types of memory PCAs: | |
| - 30007-60002 MCL (Memory Control and Logging, up to 128K words) | |
| - 30008-60002 SMA (Semiconductor Memory Array, 32K words, 17 bits) | |
| - 30009-60001 FCA (Fault Correction Array, up to 128K words, 4 bits) | |
| - 30009-60002 FLI (Fault Logging Interface, up to 256K words) | |
| A 64K system uses one of each PCA. A 256K system uses 2 MCLs, 8 SMAs, 2 FCAs | |
| and 1 FLI. Five check bits (one on the SMA, four on the FCA) are used. | |
| The Series III supports a main memory size of 128K to 1024K words in 128K | |
| increments using 16K RAMs. It uses three types of memory PCAs: | |
| - 30007-60005 MCL (Memory Control and Logging, up to 512K words) | |
| - 30008-60003 SMA (Semiconductor Memory Array, 128K words, 22 bits) | |
| - 30009-60002 FLI (Fault Logging Interface, up to 1024K words) | |
| A 128K system uses one of each PCA. A 1024K system uses 2 MCLs, 8 SMAs, and | |
| 1 FLI. Six check bits (all on the SMA) are used. The standalone FLI PCA may | |
| be replaced with a 30135-60063 System Clock/Fault Logging Interface that | |
| combines both devices on a single PCA. | |
| Main memory consists of from one to eight 128K word memory arrays. Memory is | |
| divided into two 512K modules, each with its own Module Control Unit and | |
| Memory Control and Logging PCA. The two modules respond to module numbers 0 | |
| and 1 or 2 and 3. | |
| Error correction is implemented by storing five (Series II) or six (Series | |
| III) check bits with the sixteen data bits. The Series III check bits | |
| reflect the parity of sets of eight data bits, as follows: | |
| 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 C0 C1 C2 C3 C4 C5 Parity | |
| -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ------ | |
| X X X X X X X X X Even | |
| X X X X X X X X X Odd | |
| X X X X X X X X X Even | |
| X X X X X X X X X Odd | |
| X X X X X X X X X Even | |
| X X X X X X X X X Even | |
| -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- | |
| 07 13 23 03 15 25 11 21 16 06 32 22 34 14 24 30 00 20 10 04 02 01 Syndrome | |
| The check bits are generated by setting C0-C5 to zero. When read, the parity | |
| computations (syndrome) will result in all zeros if the data and check bits | |
| are correct and non-zero values if one or more bits are in error. If a | |
| single bit (either data or check) is in error, the syndrome itself will have | |
| odd parity and will indicate the bit in error as indicated above. If the | |
| syndrome is non-zero and has even parity, i.e., does not contain either one | |
| or three 1-bits, then a double-bit error has occurred, and the syndrome value | |
| is not significant. | |
| The MCL will correct single-bit data errors (single-bit check errors need not | |
| be corrected). Double-bit errors will result in data parity interrupts. | |
| Each MCL contains one 1024 x 1 static RAM ELA (Error Logging Array). The | |
| array stores a 1 in an address corresponding to the 4K or 16K RAM chip | |
| containing the bit in error. The address is 10 bits wide, consisting of a | |
| 5-bit chip-row address (2-bit SMA PCA address and 3-bit row address) and a | |
| 5-bit bit-in-error code (the lower five bits of the 6-bit ECC syndrome). The | |
| bit-in-error code is decoded as: | |
| Code Bit Code Bit Code Bit Code Bit | |
| ---- --- ---- --- ---- --- ---- --- | |
| 00 C0 10 C2 20 C1 30 D15 | |
| 01 C5 11 D6 21 D7 31 -- | |
| 02 C4 12 ** 22 D11 32 D10 | |
| 03 D3 13 D1 23 D2 33 -- | |
| 04 C3 14 D13 24 D14 34 D12 | |
| 05 * 15 D4 25 D5 35 -- | |
| 06 D9 16 D8 26 -- 36 -- | |
| 07 D0 17 -- 27 -- 37 -- | |
| * Forced double-error write | |
| ** Missing SMA | |
| If a parity error occurs on the data sent from the MCU to the SMA for a | |
| write, the MCL asserts a data parity error (CPX1.6) and forces a double-bit | |
| error into the check bits by complementing the C3 and C5 bits. This ensures | |
| that a read of the location will always cause a data parity error interrupt. | |
| If an addressed SMA is not present, the all-zeros data and check bits result | |
| in a syndrome of 12, due to the odd parity of the C2 and C4 calculations. | |
| Main memory is simulated by allocating an array of MEMORY_WORDs large enough | |
| to accommodate the largest system configuration (1024 KW). Array access is | |
| then restricted to the configured size; accesses beyond the end of configured | |
| memory result in an Illegal Address interrupt. | |
| All accesses to main memory are through exported functions. Examine and | |
| deposit routines provide for SCP interfacing, and general read and write | |
| routines are used by the other HP 3000 simulator modules. Each general | |
| access carries an access classification that determines how memory will be | |
| addressed. Program, data, and stack accesses use their respective memory | |
| bank registers to form the indices into the simulated memory array. DMA | |
| accesses on behalf of the multiplexer and selector channels use the memory | |
| banks supplied by the channel programs. Absolute accesses imply bank number | |
| zero. | |
| Several auxiliary functions provide memory initialization, filling, and | |
| checking that a specified range of memory has not been used. A full set of | |
| byte-access routines is provided to emulate byte addressing on the | |
| word-addressable HP 3000. | |
| The memory simulator provides the capability to trace memory reads and | |
| writes, as well as byte and BCD operands that are stored in memory. Three | |
| general memory debug flags are defined and can be used by the other simulator | |
| modules to trace memory reads and writes, instruction fetches, and operand | |
| accesses. | |
| Implementation notes: | |
| 1. Error detection and correction is not currently simulated. | |
| */ | |
| #include "hp3000_defs.h" | |
| #include "hp3000_cpu.h" | |
| #include "hp3000_mem.h" | |
| /* Memory access classification table */ | |
| typedef struct { | |
| HP_WORD *bank_ptr; /* a pointer to the bank register */ | |
| uint32 debug_flag; /* the debug flag for tracing */ | |
| const char *name; /* the classification name */ | |
| } ACCESS_PROPERTIES; | |
| static const ACCESS_PROPERTIES mem_access [] = { /* indexed by ACCESS_CLASS */ | |
| /* bank_ptr debug_flag name */ | |
| /* -------- ----------- ------------------- */ | |
| { NULL, DEB_MDATA, "absolute" }, /* absolute */ | |
| { NULL, DEB_MDATA, "absolute" }, /* absolute_mapped */ | |
| { & PBANK, DEB_MFETCH, "instruction fetch" }, /* fetch */ | |
| { & PBANK, DEB_MFETCH, "instruction fetch" }, /* fetch_checked */ | |
| { & PBANK, DEB_MDATA, "program" }, /* program */ | |
| { & PBANK, DEB_MDATA, "program" }, /* program_checked */ | |
| { & DBANK, DEB_MDATA, "data" }, /* data */ | |
| { & DBANK, DEB_MDATA, "data" }, /* data_checked */ | |
| { & DBANK, DEB_MDATA, "data" }, /* data_mapped */ | |
| { & DBANK, DEB_MDATA, "data" }, /* data_mapped_checked */ | |
| { & SBANK, DEB_MDATA, "stack" }, /* stack */ | |
| { & SBANK, DEB_MDATA, "stack" }, /* stack_checked */ | |
| { NULL, DEB_MDATA, "dma" } /* dma */ | |
| }; | |
| /* Memory local data structures */ | |
| /* Main memory */ | |
| static MEMORY_WORD *M = NULL; /* the pointer to the main memory allocation */ | |
| /* Memory global SCP helpers */ | |
| /* Examine a memory location. | |
| This routine is called by the SCP to examine memory. The routine retrieves | |
| the memory location indicated by "address" as modified by any "switches" that | |
| were specified on the command line and returns the value in the first element | |
| of "eval_array". | |
| On entry, if "switches" includes SIM_SW_STOP, then "address" is an offset | |
| from PBANK; otherwise, it is an absolute address. If the supplied address is | |
| beyond the current memory limit, "non-existent memory" status is returned. | |
| Otherwise, the value is obtained from memory and returned in "eval_array." | |
| */ | |
| t_stat mem_examine (t_value *eval_array, t_addr address, UNIT *uptr, int32 switches) | |
| { | |
| if (switches & SIM_SW_STOP) /* if entry is for a simulator stop */ | |
| address = TO_PA (PBANK, address); /* then form a PBANK-based physical address */ | |
| if (address >= MEMSIZE) /* if the address is beyond memory limits */ | |
| return SCPE_NXM; /* then return non-existent memory status */ | |
| else if (eval_array == NULL) /* if the value pointer was not supplied */ | |
| return SCPE_IERR; /* then return internal error status */ | |
| else { /* otherwise */ | |
| *eval_array = (t_value) M [address]; /* store the return value */ | |
| return SCPE_OK; /* and return success */ | |
| } | |
| } | |
| /* Deposit to a memory location. | |
| This routine is called by the SCP to deposit to memory. The routine stores | |
| the supplied "value" into memory at the "address" location. If the supplied | |
| address is beyond the current memory limit, "non-existent memory" status is | |
| returned. | |
| The presence of any "switches" supplied on the command line does not affect | |
| the operation of the routine. | |
| */ | |
| t_stat mem_deposit (t_value value, t_addr address, UNIT *uptr, int32 switches) | |
| { | |
| if (address >= MEMSIZE) /* if the address is beyond memory limits */ | |
| return SCPE_NXM; /* then return non-existent memory status */ | |
| else { /* otherwise */ | |
| M [address] = value & DV_MASK; /* store the supplied value into memory */ | |
| return SCPE_OK; /* and return success */ | |
| } | |
| } | |
| /* Memory global routines */ | |
| /* Initialize main memory. | |
| The array of MEMORY_WORDs that represent the main memory of the HP 3000 | |
| system is allocated and initialized to zero if the global pointer M has not | |
| been set. The number of words to be allocated is supplied. The routine | |
| returns TRUE if the allocation was successful or memory had already been | |
| allocated earlier, or FALSE if the allocation failed. | |
| */ | |
| t_bool mem_initialize (uint32 memory_size) | |
| { | |
| if (M == NULL) /* if memory has not been allocated */ | |
| M = (MEMORY_WORD *) calloc (memory_size, /* then allocate the maximum amount of memory needed */ | |
| sizeof (MEMORY_WORD)); | |
| return (M != NULL); | |
| } | |
| /* Check for non-zero value in a memory address range. | |
| A range of memory locations is checked for the presence of a non-zero value. | |
| The starting address of the range is supplied, and the check continues | |
| through the end of defined memory. The routine returns TRUE if the memory | |
| range was empty (i.e., contained only zero values) and FALSE otherwise. | |
| */ | |
| t_bool mem_is_empty (uint32 starting_address) | |
| { | |
| uint32 address; | |
| for (address = starting_address; address < MEMSIZE; address++) /* loop through the specified address range */ | |
| if (M [address] != NOP) /* if this location is non-zero */ | |
| return FALSE; /* then indicate that memory is not empty */ | |
| return TRUE; /* return TRUE if all locations contain zero values */ | |
| } | |
| /* Fill a range of memory with a value. | |
| Main memory locations from a supplied starting address through the end of | |
| defined memory are filled with the specified value. This routine is | |
| typically used by the cold-load routine to fill memory with HALT 10 | |
| instructions. | |
| */ | |
| void mem_fill (uint32 starting_address, HP_WORD fill_value) | |
| { | |
| uint32 address; | |
| for (address = starting_address; address < MEMSIZE; address++) /* loop through the specified address range */ | |
| M [address] = (MEMORY_WORD) fill_value; /* filling locations with the supplied value */ | |
| return; | |
| } | |
| /* Read a word from memory. | |
| Read and return a word from memory at the indicated offset and implied bank. | |
| If the access succeeds, the routine returns TRUE. If the accessed word is | |
| outside of physical memory, the Illegal Address interrupt flag is set for | |
| CPU accesses, the value is set to 0, and the routine returns FALSE. If | |
| access checking is requested, and the check fails, a Bounds Violation trap is | |
| taken. | |
| On entry, "dptr" points to the DEVICE structure of the device requesting | |
| access, "classification" is the type of access requested, "offset" is a | |
| logical offset into the memory bank implied by the access classification, | |
| except for absolute and DMA accesses, for which "offset" is a physical | |
| address, and "value" points to the variable to receive the memory content. | |
| Memory accesses other than DMA accesses may be checked or unchecked. Checked | |
| program, data, and stack accesses must specify locations within the | |
| corresponding segments (PB <= ea <= PL for program, or DL <= ea <= S for data | |
| or stack) unless the CPU in is privileged mode, and those that reference the | |
| TOS locations return values from the TOS registers instead of memory. | |
| Checked absolute accesses return TOS location values if referenced but | |
| otherwise access memory directly with no additional restrictions. | |
| For data and stack accesses, there are three cases, depending on the | |
| effective address: | |
| - EA >= DL and EA <= SM : read from memory | |
| - EA > SM and EA <= SM + SR : read from a TOS register if bank = stack bank | |
| - EA < DL or EA > SM + SR : trap if not privileged, else read from memory | |
| Implementation notes: | |
| 1. The physical address is formed by merging the bank and offset without | |
| masking either value to their respective register sizes. Masking is not | |
| necessary, as it was done when the bank registers were loaded, and it is | |
| faster to avoid it. Primarily, though, it is not done so that an invalid | |
| bank register value (e.g., loaded from a corrupted stack) will generate | |
| an illegal address interrupt and so will pinpoint the problem for | |
| debugging. | |
| 2. In hardware, bounds checking is performed explicitly by microcode. In | |
| simulation, bounds checking is performed explicitly by employing the | |
| "_checked" versions of the desired access classifications. | |
| */ | |
| t_bool mem_read (DEVICE *dptr, ACCESS_CLASS classification, uint32 offset, HP_WORD *value) | |
| { | |
| uint32 bank, address; | |
| if (mem_access [classification].bank_ptr == NULL) { /* if this is an absolute or DMA access */ | |
| address = offset; /* then the "offset" is already a physical address */ | |
| bank = TO_BANK (offset); /* separate the bank and offset */ | |
| offset = TO_OFFSET (offset); /* in case tracing is active */ | |
| } | |
| else { /* otherwise the bank register is implied */ | |
| bank = *mem_access [classification].bank_ptr; /* by the access classification */ | |
| address = bank << LA_WIDTH | offset; /* form the physical address with the supplied offset */ | |
| } | |
| if (address >= MEMSIZE) { /* if this access is beyond the memory size */ | |
| if (dptr == &cpu_dev) /* then if an interrupt is requested */ | |
| CPX1 |= cpx1_ILLADDR; /* then set the Illegal Address interrupt */ | |
| *value = 0; /* return a zero value */ | |
| return FALSE; /* and indicate failure to the caller */ | |
| } | |
| else { /* otherwise the access is within the memory range */ | |
| switch (classification) { /* so dispatch on the access classification */ | |
| case dma: | |
| case absolute: | |
| case fetch: | |
| case program: | |
| case data: | |
| *value = (HP_WORD) M [address]; /* unchecked access values come from memory */ | |
| break; | |
| case absolute_mapped: | |
| case data_mapped: | |
| case stack: | |
| if (offset > SM && offset <= SM + SR && bank == SBANK) /* if the offset is within the TOS */ | |
| *value = TR [SM + SR - offset]; /* then the value comes from a TOS register */ | |
| else /* otherwise */ | |
| *value = (HP_WORD) M [address]; /* the value comes from memory */ | |
| break; | |
| case fetch_checked: | |
| if (PB <= offset && offset <= PL) /* if the offset is within the program segment bounds */ | |
| *value = (HP_WORD) M [address]; /* then the value comes from memory */ | |
| else /* otherwise */ | |
| MICRO_ABORT (trap_Bounds_Violation); /* trap for a bounds violation */ | |
| break; | |
| case program_checked: | |
| if (PB <= offset && offset <= PL || PRIV) /* if the offset is within bounds or is privileged */ | |
| *value = (HP_WORD) M [address]; /* then the value comes from memory */ | |
| else /* otherwise */ | |
| MICRO_ABORT (trap_Bounds_Violation); /* trap for a bounds violation */ | |
| break; | |
| case data_checked: | |
| if (DL <= offset && offset <= SM + SR || PRIV) /* if the offset is within bounds or is privileged */ | |
| *value = (HP_WORD) M [address]; /* then the value comes from memory */ | |
| else /* otherwise */ | |
| MICRO_ABORT (trap_Bounds_Violation); /* trap for a bounds violation */ | |
| break; | |
| case data_mapped_checked: | |
| case stack_checked: | |
| if (offset > SM && offset <= SM + SR && bank == SBANK) /* if the offset is within the TOS */ | |
| *value = TR [SM + SR - offset]; /* then the value comes from a TOS register */ | |
| else if (DL <= offset && offset <= SM + SR || PRIV) /* if the offset is within bounds or is privileged */ | |
| *value = (HP_WORD) M [address]; /* then the value comes from memory */ | |
| else /* otherwise */ | |
| MICRO_ABORT (trap_Bounds_Violation); /* trap for a bounds violation */ | |
| break; | |
| } /* all cases are handled */ | |
| dpprintf (dptr, mem_access [classification].debug_flag, | |
| BOV_FORMAT " %s%s\n", bank, offset, *value, | |
| mem_access [classification].name, | |
| mem_access [classification].debug_flag == DEB_MDATA ? " read" : ""); | |
| return TRUE; /* indicate success with the returned value stored */ | |
| } | |
| } | |
| /* Write a word to memory. | |
| Write a word to memory at the indicated offset and implied bank. If the | |
| write succeeds, the routine returns TRUE. If the accessed location is outside | |
| of physical memory, the Illegal Address interrupt flag is set for CPU | |
| accesses, the write is ignored, and the routine returns FALSE. If access | |
| checking is requested, and the check fails, a Bounds Violation trap is taken. | |
| For data and stack accesses, there are three cases, depending on the | |
| effective address: | |
| - EA >= DL and EA <= SM + SR : write to memory | |
| - EA > SM and EA <= SM + SR : write to a TOS register if bank = stack bank | |
| - EA < DL or EA > SM + SR : trap if not privileged, else write to memory | |
| Note that cases 1 and 2 together imply that a write to a TOS register also | |
| writes through to the underlying memory. | |
| Implementation notes: | |
| 1. The physical address is formed by merging the bank and offset without | |
| masking either value to their respective register sizes. Masking is not | |
| necessary, as it was done when the bank registers were loaded, and it is | |
| faster to avoid it. Primarily, though, it is not done so that an invalid | |
| bank register value (e.g., loaded from a corrupted stack) will generate | |
| an illegal address interrupt and so will pinpoint the problem for | |
| debugging. | |
| 2. In hardware, bounds checking is performed explicitly by microcode. In | |
| simulation, bounds checking is performed explicitly by employing the | |
| "_checked" versions of the desired access classifications. | |
| 3. The Series II microcode shows that only the STOR and STD instructions | |
| write through to memory when the effective address is in a TOS register. | |
| However, in simulation, all (checked) stack and data writes will write | |
| through. | |
| */ | |
| t_bool mem_write (DEVICE *dptr, ACCESS_CLASS classification, uint32 offset, HP_WORD value) | |
| { | |
| uint32 bank, address; | |
| if (mem_access [classification].bank_ptr == NULL) { /* if this is an absolute or DMA access */ | |
| address = offset; /* then "offset" is already a physical address */ | |
| bank = TO_BANK (offset); /* separate the bank and offset */ | |
| offset = TO_OFFSET (offset); /* in case tracing is active */ | |
| } | |
| else { /* otherwise the bank register is implied */ | |
| bank = *mem_access [classification].bank_ptr; /* by the access classification */ | |
| address = bank << LA_WIDTH | offset; /* form the physical address with the supplied offset */ | |
| } | |
| if (address >= MEMSIZE) { /* if this access is beyond the memory size */ | |
| if (dptr == &cpu_dev) /* then if an interrupt is requested */ | |
| CPX1 |= cpx1_ILLADDR; /* then set the Illegal Address interrupt */ | |
| return FALSE; /* indicate failure to the caller */ | |
| } | |
| else { /* otherwise the access is within the memory range */ | |
| switch (classification) { /* so dispatch on the access classification */ | |
| case dma: | |
| case absolute: | |
| case data: | |
| M [address] = (MEMORY_WORD) value; /* write the value to memory */ | |
| break; | |
| case absolute_mapped: | |
| case data_mapped: | |
| case stack: | |
| if (offset > SM && offset <= SM + SR && bank == SBANK) /* if the offset is within the TOS */ | |
| TR [SM + SR - offset] = value; /* then write the value to a TOS register */ | |
| else /* otherwise */ | |
| M [address] = (MEMORY_WORD) value; /* write the value to memory */ | |
| break; | |
| case data_mapped_checked: | |
| case stack_checked: | |
| if (offset > SM && offset <= SM + SR && bank == SBANK) /* if the offset is within the TOS */ | |
| TR [SM + SR - offset] = value; /* then write the value to a TOS register */ | |
| /* fall into checked cases */ | |
| case data_checked: | |
| if (DL <= offset && offset <= SM + SR || PRIV) /* if the offset is within bounds or is privileged */ | |
| M [address] = (MEMORY_WORD) value; /* then write the value to memory */ | |
| else /* otherwise */ | |
| MICRO_ABORT (trap_Bounds_Violation); /* trap for a bounds violation */ | |
| break; | |
| case fetch: | |
| case fetch_checked: | |
| case program: | |
| case program_checked: /* these classes cannot be used for writing */ | |
| CPX1 |= cpx1_ADDRPAR; /* so set an Address Parity Error interrupt */ | |
| return FALSE; /* and indicate failure to the caller */ | |
| } /* all cases are handled */ | |
| dpprintf (dptr, mem_access [classification].debug_flag, | |
| BOV_FORMAT " %s write\n", bank, offset, value, | |
| mem_access [classification].name); | |
| return TRUE; /* indicate success with the value written */ | |
| } | |
| } | |
| /* Initialize a byte accessor. | |
| The supplied byte accessor structure is initialized for the starting relative | |
| byte offset pointer and type of access indicated. If the supplied block | |
| length is non-zero and checked accesses are requested, then the starting and | |
| ending word addresses are bounds-checked, and a Bounds Violation will occur | |
| if the address range exceeds that permitted by the access. If the block | |
| length is zero and checked accesses are requested, then only the starting | |
| address is checked, and it is the caller's responsibility to check additional | |
| accesses as they occur. | |
| The byte access routines assume that if the initial range or starting address | |
| is checked, succeeding accesses need not be checked, and vice versa. The | |
| implication is that if the access class passed to this routine is checked, | |
| the routine might abort with a Bounds Violation, but succeeding read or write | |
| accesses will not, and if the class is unchecked, this routine will not abort | |
| but a succeeding access might. | |
| On return, the byte accessor is ready for use with the other byte access | |
| routines. | |
| Implementation notes: | |
| 1. Calling mem_set_byte with the initial_byte_address field set to zero | |
| indicates an initialization call that should use the count field as the | |
| block length. Zero is not a valid value for initial_byte_address, as | |
| memory location 0 is reserved for the code segment table pointer. | |
| */ | |
| void mem_init_byte (BYTE_ACCESS *bap, ACCESS_CLASS class, HP_WORD *byte_offset, uint32 block_length) | |
| { | |
| bap->class = INVERT_CHECK (class); /* invert the access check for succeeding calls */ | |
| bap->write_needed = FALSE; /* and clear the word buffer occupation flag */ | |
| bap->byte_offset = byte_offset; /* save the pointer to the relative byte offset variable */ | |
| bap->first_byte_offset = *byte_offset; /* and initialize the lowest byte offset */ | |
| bap->length = block_length; /* set the maximum extent length to the block length */ | |
| bap->count = block_length; /* and pass the initial block length */ | |
| bap->initial_byte_address = 0; /* in an initialization call */ | |
| mem_set_byte (bap); /* set up the access from the initial byte offset */ | |
| bap->first_byte_address = bap->initial_byte_address; /* save the lowest byte address */ | |
| bap->count = 0; /* and clear the byte access count */ | |
| return; | |
| } | |
| /* Set a byte accessor. | |
| The supplied byte accessor is set to access the updated address specified by | |
| the byte offset variable. If the variable is altered directly, this routine | |
| must be called before calling any of the other byte access routines. It is | |
| also called to update the first byte offset and length in preparation for | |
| formatting an operand for tracing. | |
| On return, the byte accessor is ready for use with the other byte access | |
| routines. | |
| Implementation notes: | |
| 1. Entry with the initial_byte_address field set to zero indicates an | |
| initialization call; the count field will contain the block length. | |
| Entry with initial_byte_address non-zero indicates that the count field | |
| contains the number of bytes read or written since initialization. | |
| 2. The operand extents are updated only if an access was made with the | |
| current accessor. This avoids extending the bounds if the accessor was | |
| set but never used to read or write a byte. | |
| 3. The class field contains the access class used when reading or writing | |
| bytes. The initial access check uses the opposite sense. | |
| */ | |
| void mem_set_byte (BYTE_ACCESS *bap) | |
| { | |
| uint32 bank; | |
| mem_update_byte (bap); /* flush the last byte if written */ | |
| if (bap->count > 0 && bap->initial_byte_address > 0) { /* if bytes have been accessed */ | |
| if (bap->initial_byte_address < bap->first_byte_address) { /* then if the current address is lower */ | |
| bap->length = bap->length + bap->first_byte_address /* then extend the length */ | |
| - bap->initial_byte_address; /* by the additional amount */ | |
| bap->first_byte_address = bap->initial_byte_address; /* reset the lowest address seen */ | |
| bap->first_byte_offset = bap->initial_byte_offset; /* and the lowest offset seen */ | |
| } | |
| else /* otherwise the current address is higher */ | |
| bap->count = bap->count + bap->initial_byte_address /* (or unchanged) so extend the count */ | |
| - bap->first_byte_address; /* by the additional amount if any */ | |
| if (bap->length < bap->count) /* if the maximum length is less than the current count */ | |
| bap->length = bap->count; /* then reset the maximum to the current extent */ | |
| bap->count = 0; /* clear the access count */ | |
| } | |
| bap->initial_byte_offset = *bap->byte_offset; /* set the new starting relative byte offset */ | |
| bap->word_address = cpu_byte_ea (INVERT_CHECK (bap->class), /* convert the new byte offset to a word address */ | |
| *bap->byte_offset, /* and check the bounds if originally requested */ | |
| bap->count); | |
| if (mem_access [bap->class].bank_ptr == NULL) /* if this is an absolute or DMA access */ | |
| bank = 0; /* then the byte offset is already a physical address */ | |
| else /* otherwise */ | |
| bank = *mem_access [bap->class].bank_ptr; /* the bank register is implied by the classification */ | |
| bap->initial_byte_address = TO_PA (bank, bap->word_address) * 2 /* save the physical starting byte address */ | |
| + (bap->initial_byte_offset & 1); | |
| if ((bap->initial_byte_offset & 1) == 0) /* if the starting byte offset is even */ | |
| bap->word_address = bap->word_address - 1 & LA_MASK; /* then bias the address for the first read */ | |
| return; | |
| } | |
| /* Look up a byte in a table. | |
| The byte located in the table designated by the byte accessor pointer "bap" | |
| at the entry designated by the "index" parameter is returned. The table is | |
| byte-addressable and assumed to be long enough to contain the indexed | |
| entry. | |
| Implementation notes: | |
| 1. Successive lookups using the same index incur only one memory read | |
| penalty. | |
| */ | |
| uint8 mem_lookup_byte (BYTE_ACCESS *bap, uint8 index) | |
| { | |
| uint32 byte_offset, word_address; | |
| byte_offset = *bap->byte_offset + (HP_WORD) index /* get the offset to the indexed location */ | |
| & LA_MASK; | |
| word_address = cpu_byte_ea (bap->class, byte_offset, 0); /* convert to a word address and check the bounds */ | |
| if (word_address != bap->word_address) { /* if the address is not the same as the prior access */ | |
| bap->word_address = word_address; /* then set the new address */ | |
| cpu_read_memory (bap->class, word_address, /* and read the memory word */ | |
| &bap->data_word); /* containing the target byte */ | |
| } | |
| if (byte_offset & 1) /* if the byte offset is odd */ | |
| return LOWER_BYTE (bap->data_word); /* then return the lower byte */ | |
| else /* otherwise */ | |
| return UPPER_BYTE (bap->data_word); /* return the upper byte */ | |
| } | |
| /* Read the next byte. | |
| The next byte indicated by the supplied byte accessor is returned. | |
| If a new memory word must be read, and a previous byte write has not written | |
| the buffered word into memory, it is posted. Then the next word is read from | |
| memory, and the indicated byte is returned. | |
| Implementation notes: | |
| 1. The data_word field is not read until the first access is made. This | |
| ensures that a Bounds Violation does not occur on an unchecked | |
| initialization call but instead occurs when the byte is actually | |
| accessed. | |
| */ | |
| uint8 mem_read_byte (BYTE_ACCESS *bap) | |
| { | |
| uint8 byte; | |
| if (*bap->byte_offset & 1) { /* if the byte offset is odd */ | |
| if (bap->count == 0) /* then if this is the first access */ | |
| cpu_read_memory (bap->class, bap->word_address, /* then read the data word */ | |
| &bap->data_word); /* containing the target byte */ | |
| byte = LOWER_BYTE (bap->data_word); /* get the lower byte */ | |
| } | |
| else { /* otherwise */ | |
| if (bap->write_needed) { /* if the buffer is occupied */ | |
| bap->write_needed = FALSE; /* then mark it written */ | |
| cpu_write_memory (bap->class, bap->word_address, /* and write the word back */ | |
| bap->data_word); | |
| } | |
| bap->word_address = bap->word_address + 1 & LA_MASK; /* update the word address */ | |
| cpu_read_memory (bap->class, bap->word_address, /* read the data word */ | |
| &bap->data_word); /* containing the target byte */ | |
| byte = UPPER_BYTE (bap->data_word); /* and get the upper byte */ | |
| } | |
| *bap->byte_offset = *bap->byte_offset + 1 & LA_MASK; /* update the byte offset */ | |
| bap->count = bap->count + 1; /* and the access count */ | |
| return byte; | |
| } | |
| /* Write the next byte. | |
| The next byte indicated by the supplied byte accessor is written. If the | |
| lower byte is accessed, the containing word is written to memory, and the | |
| buffer word is marked vacant. Otherwise, the upper byte is placed in the | |
| buffer word, and the flag is set to indicate that the word will need to be | |
| written to memory. | |
| Implementation notes: | |
| 1. The data_word field is not read until the first access is made. This | |
| ensures that a Bounds Violation does not occur on an unchecked | |
| initialization call but instead occurs when the byte is actually | |
| accessed. | |
| */ | |
| void mem_write_byte (BYTE_ACCESS *bap, uint8 byte) | |
| { | |
| if (*bap->byte_offset & 1) { /* if the byte offset is odd */ | |
| if (bap->count == 0) /* then if this is the first access */ | |
| cpu_read_memory (bap->class, bap->word_address, /* then read the data word */ | |
| &bap->data_word); /* containing the target byte */ | |
| bap->data_word = REPLACE_LOWER (bap->data_word, byte); /* replace the lower byte */ | |
| cpu_write_memory (bap->class, bap->word_address, /* and write the word to memory */ | |
| bap->data_word); | |
| bap->write_needed = FALSE; /* clear the occupancy flag */ | |
| } | |
| else { /* otherwise the offset is even */ | |
| bap->word_address = bap->word_address + 1 & LA_MASK; /* so update the word address */ | |
| bap->data_word = REPLACE_UPPER (bap->data_word, byte); /* replace the upper byte */ | |
| bap->write_needed = TRUE; /* and set the occupancy flag */ | |
| } | |
| *bap->byte_offset = *bap->byte_offset + 1 & LA_MASK; /* update the byte offset */ | |
| bap->count = bap->count + 1; /* and the access count */ | |
| return; | |
| } | |
| /* Modify the last byte accessed. | |
| The last byte read or written as indicated by the supplied byte accessor is | |
| modified in-place with the new value supplied. The current byte offset will | |
| be odd if the last byte accessed was the upper (even) byte, or it will be | |
| even if the last byte accessed was the lower (odd) byte. The current byte | |
| offset is not changed by this routine. | |
| */ | |
| void mem_modify_byte (BYTE_ACCESS *bap, uint8 byte) | |
| { | |
| if (*bap->byte_offset & 1) { /* if the last byte offset was even */ | |
| bap->data_word = REPLACE_UPPER (bap->data_word, byte); /* then replace the upper byte */ | |
| bap->write_needed = TRUE; /* and set the occupancy flag */ | |
| } | |
| else { /* otherwise the last offset was odd */ | |
| bap->data_word = REPLACE_LOWER (bap->data_word, byte); /* so replace the lower byte */ | |
| cpu_write_memory (bap->class, bap->word_address, /* write the word back */ | |
| bap->data_word); | |
| bap->write_needed = FALSE; /* clear the occupancy flag */ | |
| } | |
| return; | |
| } | |
| /* Post the current buffer word. | |
| The buffer word held by the supplied byte accessor is written to memory if | |
| the occupancy flag is set. Otherwise, no action is taken. | |
| This routine must be called to terminate any sequence of byte operations | |
| that involves calls to mem_read_byte and mem_modify_byte. It ensures that | |
| the final byte written is flushed to memory. | |
| Implementation notes: | |
| 1. Because a preceding mem_read_byte call has been made, the data_word field | |
| already contains the byte that was NOT modified, so a read-modify-write | |
| access is not needed. | |
| */ | |
| void mem_post_byte (BYTE_ACCESS *bap) | |
| { | |
| if (bap->write_needed) { /* if the buffer needs to be written */ | |
| bap->write_needed = FALSE; /* then clear the occupancy flag */ | |
| cpu_write_memory (bap->class, bap->word_address, /* and write the word to memory */ | |
| bap->data_word); | |
| } | |
| return; | |
| } | |
| /* Rewrite the current buffer word. | |
| The upper byte of the buffer word held by the supplied byte accessor replaces | |
| the upper byte of the current memory word without disturbing the lower byte, | |
| and the word is rewritten to memory if the occupancy flag is set. Otherwise, | |
| no action is taken. | |
| This routine should be called to terminate any sequence of byte operations | |
| that involves calls to mem_write_byte. It ensures that the final byte | |
| written is flushed to memory. The read-modify-write sequence ensures that | |
| the existing lower byte in the memory word is retained. | |
| */ | |
| void mem_update_byte (BYTE_ACCESS *bap) | |
| { | |
| HP_WORD target_word; | |
| if (bap->write_needed) { /* if the buffer needs to be written */ | |
| bap->write_needed = FALSE; /* then clear the occupancy flag */ | |
| cpu_read_memory (bap->class, bap->word_address, &target_word); /* read the data word */ | |
| bap->data_word = REPLACE_LOWER (bap->data_word, target_word); /* and replace the lower byte */ | |
| cpu_write_memory (bap->class, bap->word_address, bap->data_word); /* and write the word back */ | |
| } | |
| return; | |
| } | |
| /* Format a byte operand. | |
| The byte string starting at the absolute byte address given by the | |
| "byte_address" parameter and of "byte_count" bytes in length is copied into | |
| a local character buffer and terminated by a NUL character. A pointer to the | |
| buffer is returned. | |
| No translation of non-printable characters is performed, so if the caller | |
| interprets the returned formatted operand as a character string, an embedded | |
| NUL will truncate the string. | |
| Implementation notes: | |
| 1. This routine accesses the memory array directly to avoid tracing the | |
| memory reads if debug tracing is enabled. | |
| 2. The byte count is assumed to be 256 or less for convenience. | |
| */ | |
| char *fmt_byte_operand (uint32 byte_address, uint32 byte_count) | |
| { | |
| static char buffer [257]; | |
| char *cptr; | |
| uint32 address; | |
| if (byte_count > 256) /* truncate the formatted operand */ | |
| byte_count = 256; /* if it's too long */ | |
| address = byte_address / 2; /* convert to an absolute word address */ | |
| cptr = buffer; /* point at the start of the buffer */ | |
| while (byte_count-- > 0) /* while there are bytes to transfer */ | |
| if (byte_address++ & 1) /* if the byte address is odd */ | |
| *cptr++ = LOWER_BYTE (M [address++]); /* then copy the lower byte and bump the word address */ | |
| else if (address < MEMSIZE) /* otherwise if the word address is valid */ | |
| *cptr++ = UPPER_BYTE (M [address]); /* then copy the upper byte */ | |
| else /* otherwise the address is beyond the end of memory */ | |
| break; /* so terminate the operand at this point */ | |
| *cptr = '\0'; /* add a trailing NUL */ | |
| return buffer; /* return a pointer to the formatted operand */ | |
| } | |
| /* Format a translated byte operand. | |
| The byte string starting at the absolute byte address given by the | |
| "byte_address" parameter and of "byte_count" bytes in length is formatted | |
| into a NUL-terminated character string and then translated using the lookup | |
| table given by the "table_address" parameter. A pointer to the string is | |
| returned. | |
| Implementation notes: | |
| 1. This routine accesses the memory array directly to avoid tracing the | |
| memory reads if debug tracing is enabled. | |
| 2. The routine will not return a string longer than 256 characters. | |
| */ | |
| char *fmt_translated_byte_operand (uint32 byte_address, uint32 byte_count, uint32 table_address) | |
| { | |
| char *bptr, *cptr; | |
| uint32 index; | |
| bptr = fmt_byte_operand (byte_address, byte_count); /* format the byte string */ | |
| cptr = bptr; /* point at the start of the buffer */ | |
| while (byte_count-- > 0) { /* while there are bytes to translate */ | |
| index = table_address + *cptr; /* index into the translation table */ | |
| if (index & 1) /* if the translated byte address is odd */ | |
| *cptr++ = LOWER_BYTE (M [index / 2]); /* then copy the lower byte from the table */ | |
| else /* otherwise */ | |
| *cptr++ = UPPER_BYTE (M [index / 2]); /* copy the upper byte from the table */ | |
| } | |
| return bptr; /* return a pointer to the translated operand */ | |
| } | |
| /* Format a BCD operand. | |
| The BCD numeric string starting at the absolute byte address given by the | |
| "byte_address" parameter and of "digit_count" BCD digits in length is | |
| formatted into a NUL-terminated character string and then reformatted into a | |
| local buffer as a hexadecimal character string. A pointer to the buffer is | |
| returned. | |
| The digit count does not include the numeric sign, located in the four bits | |
| following the last digit. If the digit count is even, the left-half of the | |
| first byte is unused, as BCD strings always end in the right-half of the last | |
| byte. | |
| Implementation notes: | |
| 1. The digit count is assumed to be 32 or less, as HP 3000 BCD ("packed | |
| decimal") numbers may not contain more than 28 digits. | |
| */ | |
| char *fmt_bcd_operand (uint32 byte_address, uint32 digit_count) | |
| { | |
| static char hex [] = "0123456789ABCDEF"; | |
| static char buffer [33]; | |
| uint32 byte_count; | |
| char *bptr, *cptr; | |
| if (digit_count > 32) /* if the operand is too long */ | |
| return "(invalid)"; /* then return an error indication */ | |
| byte_count = digit_count / 2 + 1; /* convert from a digit to a byte count */ | |
| bptr = fmt_byte_operand (byte_address, byte_count); /* and format the byte string */ | |
| cptr = buffer; /* point at the start of the buffer */ | |
| if ((digit_count & 1) == 0) { /* if the digit count is even */ | |
| *cptr++ = hex [LOWER_HALF (*bptr++)]; /* then the BCD string starts with */ | |
| byte_count--; /* the lower half of the first byte */ | |
| } | |
| while (byte_count-- > 0) { /* while there are digits to format */ | |
| *cptr++ = hex [UPPER_HALF (*bptr)]; /* format and copy the digit in the upper half */ | |
| *cptr++ = hex [LOWER_HALF (*bptr++)]; /* followed by the digit in the lower half */ | |
| } | |
| *cptr = '\0'; /* add a trailing NUL */ | |
| return buffer; /* return a pointer to the formatted operand */ | |
| } |