The difficulties in supporting “write-only memory” in assembly

When I last wrote about this [1], I had one outstanding problem with static analysis of read-only/write-only memory, and that was with hardware that could be input or output only. It was only after I wrote that that I realized the solution—it's the same as a hardware register having different semantics on read vs. write—just define two labels with the semantics I want. So for the MC6821 [2], I could have:

		org	$FF00
PIA0.A		rmb/r	1	; read only
		org	$FF00
PIA0.Adir	rmb/w	1	; write only, to set the direction of each IO pin
PIA0.Acontrol	rmb	1	; control for port A

So that was a non-issue. It was then I started looking over some existing code I had to see how it might look. I didn't want to just jump into an implementation without some forethought, and I quickly found some issues with the idea by looking at my maze generation program [3]. The code in question initializes the required video mode (in this case 64×64 with four colors). Step one involves writing a particular value to the MC6821:

		lda	#G1C.PIA ; 64x64x4
		sta	PIA1.B

So far so good. I can mark PIA1.B as write-only (technically, it also has some input pins so I really can't, but in theory I could).

Now, the next bit requires some explaining. There's another 3-bit value that needs to be configured on the MC6883 [4], but it's not as simple as writing the 3-bit value to a hardware register—each bit requires writing to a different address, and worse—it's a different address if the bit is 0 or 1. So that's six different addresses required. It's not horrible though—the addresses are sequential:

Table: 6883 VDG (Video Display Generator) Addressing Mode
bit	0/1	address
------------------------------
V0	0	$FFC0
V0	1	$FFC1
V1	0	$FFC2
V1	1	$FFC3
V2	0	$FFC4
V2	1	$FFC5

Yeah, to a software programmer, hardware can be weird. To set bit 0 to 0, you do a write (and it does not matter what the value is) to address $FFC0. If bit 0 is 1, then it's a write to $FFC1. So with that in mind, I have:

		sta	SAM.V0 + (G1C.V & 1<<0 <> 0)
		sta	SAM.V1 + (G1C.V & 1<<1 <> 0)
		sta	SAM.V2 + (G1C.V & 1<<2 <> 0)

OOh. Yeah.

I wrote it this way so I wouldn't have to look up the appropriate value and write the more opaque (to me):

		sta	$FFC1
		sta	SFFC2
		sta	$FFC4

The expression (G1C.V & 1<<n <> 0) checks bit n to see if it's set or not, and returns 0 (for not set) or 1 (for set). This is then added to the base address for bit n, and it all works out fine. I can change the code for, say, the 128×192 four color mode by using a different constant:

		lda	#G6C.PIA
		sta	PIA1.B
		sta	SAM.V0 + (G6C.V & 1<<0 <> 0)
		sta	SAM.V1 + (G6C.V & 1<<1 <> 0)
		sta	SAM.V2 + (G6C.V & 1<<2 <> 0)

But I digress.

This is a bit harder to support. The address being written is part of an expression, and only the label (defining the address) would have the read/write attribute associated with it. At least, that was my intent. I suppose I could track the read/write attribute by address, which would solve this particular segment of code.

And the final bit of code to set the address of the video screen (or frame buffer):

		ldx	#SAM.F6		; point to frame buffer address bits
		lda	ECB.grpram	; get MSB of frame buffer
mapframebuf	clrb
		lsla
		rolb
		sta	b,x		; next bit of address
		leax	-2,x
		cmpx	#SAM.F0
		bhs	mapframebuf

Like the VDG Address Mode bits, the bits for the VDG Address Offset have unique addresses, and because the VDG Address Offset has seven bits, the address is aligned to a 512 byte boundary. Here, the code loads the X register with the address of the upper end of the VDG Address Offset, and the seven top most bits of the video address is sent, one at a time, to the B register, which is used as an offset to the X register to set the appropriate address for the appropriate bit. So now I would have to track the read/write attributes via the index registers as well.

That is not so easy.

I mean, here, it could work, as the code is all in one place, but what if instead it was:

		ldx	#SAM.F6
		lda	ECB.grpram
		jsr	mapframebuf

Or an even worse example:

costmessage	fcc/r	"A constant message" ; read only text
buffer		rmb	18

		ldx	#constmessage
		ldy	#buffer
		lda	#18
		jsr	memcpy

The subroutine memcpy might not even be in the same source unit, so how would the read/write attribute even be checked? This is for static analysis, not runtime.

I have one variation on the maze generation program that generates multiple mazes at the same time, on the same screen (it's fun to watch) and as such, I have the data required for each “maze generator” stored in a structure:

explorec	equ	0	; read-only
backtrackc	equ	1	; read-only
xmin		equ	2	; read-only
ymin		equ	3	; read-only
xstart		equ	4	; read-only
ystart		equ	5	; read-only
xmax		equ	6	; read-only
ymax		equ	7	; read-only
xpos		equ	8	; read-write
ypos		equ	9	; read-write
color		equ	10	; read-write
func		equ	11	; read-write

This is from the source code, but I've commented each “field” as being “read-only” or “read-write.” That's another aspect of this that I didn't consider:

		lda	explorec,x	; this is okay
		sta	explorec,x	; this is NOT okay

Not only would I have to track read/write attributes for addresses, but for field accesses to a structure as well. I'm not saying this is impossible, it's just going to take way more thought than I thought. I don't think I'll have this feature done any time soon …

[1] /boston/2024/01/31.3

[2] https://en.wikipedia.org/wiki/Peripheral_Interface_Adapter

[3] /boston/2023/11/27.1

[4] https://archive.org/details/Motorola_MC6883_Synchronous_Address_Multiplexer_Advance_Sheet_19xx_Motorola

Gemini Mention this post

Contact the author