/* hp3000_mem.c: HP 3000 main memory simulator | |
Copyright (c) 2016, 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 | |
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 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 (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 = *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, access [classification].debug_flag, | |
BOV_FORMAT " %s%s\n", bank, offset, *value, | |
access [classification].name, | |
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 (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 = *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, access [classification].debug_flag, | |
BOV_FORMAT " %s write\n", bank, offset, value, | |
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 (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 = *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 */ | |
} |