Open bbbradsmith opened 5 years ago
An alternative idea I saw in the manual for ORCA/M. It has a USING directive where you put USING segment_name
within another SEGMENT, and it will assume that all references to that segment are bank-local (abs). Otherwise references are assumed far.
That might be a nice way to do it, which requires less assembly time information. All the assembler needs to know is "the programmer ensures us that this is in DB right now".
It does not take a similar approach for direct page, instead having something pretty close to the .setdb idea... I guess DP needs a more specific reference point, though.
However, this probably rubs against the one-pass concept. A USING equivalent might have to be label based, but that might be good enough? Sketch:
.segment "a" : far
.near b ; b will be considered abs for the scope of this segment (in this file) or until changed
.direct c ; c will be considered on the direct page
.far b ; b will now be considered far
.auto c ; c will revert c to its global address type
.segment "d" : far
; the .near / .dp / .far / .auto directives used in segment "a" do not apply here
segment "a" : far
; the previous directives for this segment apply again
.auto + ; return all labels to auto status
This would let you use the same labels as near and far at different points within the same file, and you wouldn't have to specify the bank to the assembler, so the linker could verify its bank later. Ideally you would set the actual values of DB and DP using a link time expression.
With the error checking I suggested in #879 you could safely continue to assume abs
for expressions not yet known, and throw a link time error for anything that was found to require far
later on, at which point it's easy to correct with a 'f:' operand prefix. (In general I'd expect abs/bank-local to be the common case, and far to be the rarity, so correcting like that does not sound too onerous to me.)
I must admit that I do not know much about the 65816.
If I understand you correctly, your proposal for the direct page is similar to what the ASSUME
statement in x86 Assembly is supposed to do:
MOV AX,DSEG
MOV DS,AX
ASSUME DS:DSEG
...
I do think there are strong parallels between x86 and 65816 memory segmentation. Yes I believe ASSUME is very analogous to the proposed .setdp. The 65816 has two separate segmentations going on though (DP and DB).
I think the one-pass constraint of ca65 makes it a little harder to solve the problem than in some other assemblers. Like ORCA/M's USING feels really nice, just to tie whole segments together with a single statement, but with ca65 it seems like the one-pass model should either require a lot of declarations (cumbersome), or addresses specified at assembly-time (redundant/rigid), to know what labels belong to its abs/dp set?
Just want to post some workaround suggestions, because I've found them practical enough that the lack of this feature isn't a very pressing issue to me.
Workaround 1 for variables used only on DP: reserve some space in RAM, and create a zeropage segment for the DP variables stored there. Edit: as suggested below, this can be done even better by using separate LOAD and RUN addresses for the DP segment, which will reserve in both places at once.
.segment "RAM"
dvseg: res 6
.segment "DV" : zeropage
dv1: .res 2
dv2: .res 2
dv3: .res 2
Workaround 2 for variables used in both places: create DP aliases.
.segment "RAM"
v1: .res 2
v2: .res 2
v3: .res 2
; direct page aliases
dvseg = v1
dv1 = <(v1 - dvseg)
dv2 = <(v2 - dvseg)
dv3 = <(v3 - dvseg)
Either way, using the variables on DP:
.segment "CODE"
lda #dvseg
tcd
lda dv1
Macros could make this a little "nicer" looking if you need that. For workaround 1 you might want to use some assert that assures the right amount of space is reserved?
When we build a program for a cartridge, we put the DATA:
segment into two memory areas (ROM and RAM). The same thing can be done with direct pages. They can be put into the program's space and into (fake) zero-pages.
Example additions to an ld65 configuration file:
diff --git 1/c64-asm.cfg 2/scpu64-asm.cfg
--- 1/c64-asm.cfg
+++ 2/scpu64-asm.cfg
@@ -8,6 +8,7 @@ MEMORY {
ZP: file = "", start = $0002, size = $00FE, define = yes;
LOADADDR: file = %O, start = %S - 2, size = $0002;
MAIN: file = %O, start = %S, size = $D000 - %S;
+ DP: file = "", start = $0000, size = $0100;
}
SEGMENTS {
ZEROPAGE: load = ZP, type = zp, optional = yes;
@@ -16,5 +17,7 @@ SEGMENTS {
CODE: load = MAIN, type = rw;
RODATA: load = MAIN, type = ro, optional = yes;
DATA: load = MAIN, type = rw, optional = yes;
+ DV: load = MAIN,
+ run = DP, type = rw, optional = yes, define = yes;
BSS: load = MAIN, type = bss, optional = yes, define = yes;
}
If you want the segments to be written into the program file, then use "type = rw"
. If you don't want them to occupy space, when they're at the end of the file, then use "type = zp"
. ld65 will make sure that you didn't try to put more than 256 bytes in each of your direct pages.
Example ca65 Assembly file:
.p816
.forceimport __EXEHDR__
.mac ABS label, seg
label := seg + .ident(.concat("d", .string(label)))
.endmac
.segment "DV": direct
.import __DV_LOAD__: abs
dvseg := __DV_LOAD__
; Direct-mode labels
dv1: .res 2
dv2: .word $ABCD
dv3: .res 2
; Absolute-mode labels
ABS v1, dvseg
ABS v2, dvseg
ABS v3, dvseg
.code
clc
xce ; 65816 mode
rep #%00100000
.a16
lda #dvseg
tad
lda dv2 ; direct (8-bit) addressing
sta dv3
lda v1 ; absolute (16-bit) addressing
sta v2
lda f:v3 ; far (24-bit) addressing
sta f:v1
lda #$0000
tad
sec
xce ; 65sc02 mode
rts
No further feedback -> closing question
.
This is a wishlist feature. It's been talked about a bit on the mailing lists in the past. It seems to require a lot of deep changes, so I'm not sure it will ever be viable, but I wanted to open it for discussion here. Even if this is never implemented, it might be worth discussing alternatives here.
.
On 65816 it would be useful to have directives to inform the assembler of the current direct page DP register and data bank DB register.
There are available ways to work around this, which I'll outline below, but this would add a lot of a facility for working with multiple banks.
Once set, the assembler could optimize accesses to labels on the current direct page, as is normally done with zero page. Similarly, setting the data bank would allow far labels to be optimized to absolute access.
Secondly the assembler would need new address modifiers to inform it about bank/page.
A sketch of the syntax, which would be available in 65816 mode only:
The
dp 0
modifier would be equivalent tozp
.The bank modifier makes accesses to its labels from other banks into
far
access, and optimizes accesses to its labels from the same bank intoabs
access. This would use the current assumed DB for data instructions, and it would use the current PC / run address bank (K) for JSR/JMP, and could also facilitate warnings/errors when jumping across banks. The existingabs
designation can be used for RAM/fixed segments that are shared across all banks.Finally the linker must check the segments which were used with
dp
orbank
modifiers to make sure they appear within these regions at link time. It seems less ideal that we couldn't just use the linker config to do this in one place, but can't see a way to make it work unless it's already defined at assemble time. (...and at least it's not as impossible to error-check as.org
.)As an additional pipe dream, the
.smart
mode might be able to deduce DP/DB from instructions that modify those registers. Not sure how practical that is. One person I talked to likes the REP/SEP detection of .smart but not the RTS to RTL promotion, so I think the more .smart does, the more it might help to be able to turn off specific components of it..
Workarounds
What seems to me to be the best practice for the current system:
Try to keep each translation unit in a single bank. Far addresses come from imports at the top of the file, and local labels are absolute. Exported procedures will use RTL, and local ones get RTS. These exported entry points will be responsible for setting and restoring DB. (Use .bank rather than hard coded addresses.)
This doesn't need to be rigid, but if normal practice encapsulates banks like this, it seems easier to me to manage the rarer cases that need to mix banks and be more careful about them.
Direct page is a little bit fussier. Either explicitly truncate every variable access with
<
or create a set of aliased labels for the ones you want to use via DP, also explicitly truncating (in a table rather than inline with the instructions). Keep the sections of your code that change DP very carefully marked, and switch it back to 0 when done. (Or back to $200 if we're talking about Hu/PCE/TG16?).
Those practices are working OK for me, but I have seen people who are accustomed to writing large assembly programs all in one file having a lot of problems doing so. The one-pass model is error prone to forgetting to declare :far variables ahead of use (issue #879 proposes a safety measure), but even if that had more error checking I think there's no convenient way to mix bank-local abs addressing with far addressing within the same translation unit.
Same deal with direct page, not that bad to use in a very localized way, but gets really messy when you need to use many different pages in one file.
Does anyone else have any notes about practices that work for them?
.
Other Notes
Issue #194 more or less re-raised the questions about direct page.
greg-king5 created a branch for direct page, though I don't think it's as extensive as what I'm proposing: https://github.com/greg-king5/cc65/tree/direct-page
There's a thread from a few years ago on the mailing list, though there were probably others: https://sourceforge.net/p/cc65/mailman/cc65-devel/thread/CAOHrKHZpoBuP4s0%2BoJE0N8CU0F5UOg0mfVm87dcVVw8ri%3DPRZw%40mail.gmail.com/