Wednesday, February 21, 2018

Memory Directives

My original thoughts about assembler directives would be that they would either start a line or be right after a label. After thinking about how I want to use directives in my assembler, I realize that they could also be used for variables and special label handling, so what I am doing is having a process directives function that will be called just after the parser determines if there is a label on the line. This takes the tokens and has the ability to alter the list of tokens if necessary to implement features like variables. For example:

LDA #.HIGH label

Would remove the declaration and label tokens replacing them with a number token. If the label is an address the number would be 0 but the directive processor would add the appropriate link to the label list, while if label is a variable would just have the high order byte of the value of the variable. This will be discussed in more detail in a future article.

The code for processing directives simply loops through the token list looking for directive and label tokens. Label tokens will be processed by the variable system which will be covered next week. Directives are ran through a when block (the Kotlin equivalent of a switch statement) which then takes each directive and processes the command. There are a number of directives that we will be supporting, with my current list being .BANK, .ORG, .EQU, .HIGH, .LOW, .BYTE, .WORD, .JUMPTABLE, .STARTMACRO, .ENDMACRO, .MACRO with additional directives added as necessary. 

The .BANK directive will have the format .BANK number [origin [size]] with bank numbering starting at 0 and going as high as the cartridges memory manager will allow. The following test code shows how the bank directive would be used. Notice that bank 1 and bank 2 overlap the same memory region.

.BANK 0 $1000 1024
     JSR bank1
; would have bank switching code here
     JSR bank2
.bank 1 $2000 1024
bank1:     LDA #1
.BANK 2 $2000 1024
     LDA #2

The bank directive is simple to implement, though a bit longer than one would expect. To begin, the number of the bank is required so if the next token isn’t a number then there is an assembly problem.

"BANK" -> {
     if ((indx >= tokens.size) or (tokens[indx].type != AssemblerTokenTypes.NUMBER)) {
           throw AssemblyException("Invalid bank specified $assemblyLine")

Getting the parameters for the bank turned out to be trickier than it should have with the version of the Kotlin compiler that I was using as it does not seem to shortcut ANDconditionals when the first condition fails so will go on to attempt an invalid array index which was checked for in the first part of the if statement. This is why my code is nested so deep within the parameter checking routines. The basic idea here is that we don’t want to alter the size and origin unless those parameters are provided so we flag those changes as false. We then see if the next parameters are numbers and if so assign them to the appropriate variable. This gives us the number of the bank, the origin of the bank, and the size of the bank. Default values are provided for cases where we are accessing a bank that has not been created yet.

     val bankID = tokens[indx].num
     var bankOrg = 0
     var orginParamSet = false
     var bankSize = 4096
     var sizeParamSet = false
     if (indx < tokens.size)
           if (tokens[indx].type == AssemblerTokenTypes.NUMBER) {
                bankOrg = tokens[indx].num
                orginParamSet = true
                if (indx < tokens.size)
                     if (tokens[indx].type == AssemblerTokenTypes.NUMBER) {
                           bankSize = tokens[indx].num
                           sizeParamSet = true

Once we have the desired parameters for the bank, we need to process this by adjusting the existing bank if it exists or by creating a new bank at the appropriate slot in the array if it doesn’t exist. While Kotlin has an ensureCapacity function, which would be ideal for adjusting the size of an array list, it doesn’t do anything so manually creating banks was done instead.

     // apply bank directive
     if (banks.size > bankID) {
           currentBank = banks[bankID]
           if ((orginParamSet) and (currentBank.bankOrigin != bankOrg)) {
                currentBank.bankOrigin = bankOrg
                currentBank.curAddress = bankOrg
           if ((sizeParamSet) and (currentBank.size != bankSize))
     } else {
           while(banks.size < bankID) {
                val skippedBank = banks.size
           banks.add(AssemblyBank(bankID, bankSize, bankOrg))
           currentBank = banks[bankID]

It is possible to switch back an forth between banks with this technique, but good assembly language code shouldn’t do that. Ideally the source code should have the banks and their instructions laid out consecutively in the source file. Having the flexibility to switch between banks may be useful in some situations, but is not likely something I will ever take advantage of.

Having code appear at specific locations within a bank is also possible, and for some bank switching schemes may be required. The more common reason for needing to put the generated machine language in specific locations is for data and the vector table. Still, a .ORG directive is necessary and easy to test.

.BANK 0 $1000
                JMP ten
.ORG 4106
ten: LDA #10

The implementation of .ORG simply gets the next parameter and makes sure that the address specified is within the range for the current bank. If it is, that is set as the banks current address. I don’t think there is anything special about this so am not going to show the code, but if you want to look at the code feel free to grab it from the GitHub repository . Next week we will look at variable directives.

No comments:

Post a Comment