Today we will see briefly the conditional jumps, or branches. The name branch comes from the fact the code may or may not jump to a certain location in memory, depending if a condition is true or not. Branching is the equivalent of the Basic IF GOTO construct.
The Status Register as I wrote yesterday, is the onboard cpu register which stores some flags. The flags are side-effects of instructions. Normally instructions will manipulate and store the data in the accumulator, but depending on the result the CPU will set (put to 1) or reset (put to 0) some bits in this register.
The most important bits of the Status Register are:
Zero Flag
Every instructions that results in a zero, including transfers to accumulator or the x and y registers, will set this flag to 1, while any other result will set it to 0.
Carry Flag
Indicates if a mathematical instruction result in a situation where you will have to carry its most significant byte to another byte. It’s not the same as the overflow flag.
Overflow Flag
Indicates when the result of the operation sets the 7th bit. It’s a more restricted case of the Carry Flag.
Negative Flag
Indicates if the result is to be considered “negative”.
There are three other flags that are not used with branching, but they are present in the Status Register.
Decimal Flag,
Iit’s used to set the computer mathematical operations in “decimal” mode. Normally the 6510 will operate on bits with normal binary maths. If the decimal flag is set then the CPU will instead use the encoding and rules of Binary Coded Decimal, which trades off some ram (every byte coded in BCD will store only one out of 100 values instead of one out of 255, so it’s less compact) for getting more precise calculations.
Break Flag,
To be used to test if the processor returned from a BRK (break) instruction. I don’t know much about this flag yet.
Interrupt flag,
This indicates if the CPU is currently executing an interrupt sequence. Interrupts are special ways a machine has to hijack control from a central program to handle important events like input or output operations (for example, when the user presses a button or when the TV finishes rendering a screen).
How and why to test for the flags
The Zero Flag
Testing for the zero flag is useful, for example, in situations where you have to test a FOR-style loop or when you do comparisons on bytes.
BEQ
with BEQ you test on the result of the previous operation being EQUAL to zero (Z = 1)
LDX #$20 LOOP
DEX
BEQ EXIT
JMP LOOP
EXIT
...
BNE
with BNE you test on the result of the previous operation being NOT EQUAL to zero (Z = 0)
LDX #$20 LOOP
DEX
BNE LOOP
...
Note that a comparison between two numbers can also set the zero flag if both numbers are equal.
For BEQ:
LDX #$20 LDA #$20 STX $03FF CMP $03FF BEQ OK
...
For BNE:
LDX #$20 LDA #$21 STX $03FF CMP $03FF BNE OK
...
The Negatifve Flag.
The Negative flag is useful, again, for countdowns, or is useful too in mathematical operations or other comparisons. You can test the Negative Flag with the following operations:
BMI
You use this to test if the result is negative (N = 1)
LDX #$20 LDA #$1F STX $03FF CMP $03FF BMI OK
...
BPL
You use this to test if the result is positive (N = 0)
LDX #$20 LDA #$21 STX $03FF CMP $03FF BPL OK
...
The Carry Flag.
The Carry flag is useful with mathematical calculations. With an 8 bit computer you are not limited to work on 8 bit values, but you can chain those values to represent an higher precision number spending a bit more memory. Note that the Carry flag is automatically used by several mathematical operations as well. You can also see a bit more info on the C64 Wiki.
BCS
By now you can imagine that Branch on Carry Set branches when the Carry flag is set (C = 1)
LDA #$FF ADC #$01 BCS OK
...
BCC
Branch on carry clear instead jumps only if the Carry flag is reset (C = 0)
LDA #$FE ADC #$01 BCC OK
...
The Overflow Flag
There is a more limited number of instructions that will set the overflow flag, actually there are only two mathematical instructions (ADC and SBC) that will set this (and the BIT comparison instruction, but apparently BIT is rarely used). The Overflow flag is set when a 7-bit operation makes the number overflow in the 8th bit. If you add 1 to 127, you will obtain 128, which is represented like this: 10000000. In that case the ADC will remind you that, if you are using 7-bit precision numbers, to check the overflow flag if you need to test for SIGNED numbers, but you can ignore this flag instead if you are using UNSIGNED numbers. As usual, the C64 wiki explains stuff better than me.
BVS
Branch on Overflow Set (V = 1)
LDA #$7F ADC #$01 BVS OK
...
BVC
Branch on Overflow Clear (or “reset”) (V = 0)
LDA #$7E ADC #$01 BVC OK
...
The following is the code I used to test the various snippets.
*=$0810 PREAMBLE CLD MAIN ; you can change the parts between MAIN and KO LDA #$7F ADC #$01 BVC OK KO ; if your calculation is wrong the program should print two black @'s LDA #$00 STA $0400 STA $0401 LDX #$00 STX $D800 STX $D801 RTS OK ; if your calculation is correct the code should print two white A's LDA #$01 STA $0400 STA $0401 LDX #$01 STX $D800 STX $D801 RTS ; The following is how the machine stores the SYS 2080 command on
Code rarely goes in a straightforward motion. Actually to do something useful we must teach the code how to jump to the correct parts, to stop when it needs to, to call subroutines... Before seeing Jumps we need to know about three processor registers:
The Program Counter (PC) is a 16-bit register containing the address of the instruction the processor will read next.
When the current instruction is read, the processor calculates (based on the current opcode and the operands it believes come next) the address of the following instruction. When the cpu finishes processing the current instruction then it will use the value in the program counter to continue the operation.
The Stack Pointer (SP) is an 8-bit register containing an index which points to the current top of a data structure called the stack.
The stack, on the 6510 cpu, is an area of memory reserved for keeping return addresses for subroutines, in case you need to make nested function calls. You can push data on the stack, and then get it back from it, or pop it, with a last-in-first-out precedence rule. The Stack Pointer only needs 8 bits, because it’s a fixed section of memory starting from $0100 and ending with $01FF.
The 6510 cpu reserves a very tiny space for its own stack because programming the 6510 you are expected to store the accumulator, the CPU status register and the CPU program counter. It’s a design choice that is sensible considering the era when it was created (its father, the 6502, was created in 1975) and the scarcity of RAM available for computer programmers. Modern CPUs allow programmers to keep complex data on the stack (function parameters, function results), and each process actually manages its own stack.
Note that the stack is built downwards, so the first value you will push (insert in it) will be in $01FF, the second $01FE.
The Status Register is an 8-bit register containing some flags. Several instructions will set or reset the single bits in the Status Register, and most jump instructions will test those bits to decide if to jump or not. We will see the Status Register more in detail with the next part, when we will talk about conditional jumps.
JMP - Unconditional Jump
JMP LABEL ; here we will use an assembler LABEL.
JMP $0820 ; here we will directly point to a certain address
; in memory.
The unconditional jump resembles the GOTO instruction in basic. You are telling the CPU to set the Program Counter to the operand, either the label or the memory address.
Note that the assembler software calculates the correct memory address for a LABEL automatically. It’s better to use (and abuse) LABELs because if you need to amend your code you don’t need to calculate the memory area, or worse, the number of bytes you need to pass to the operand.
Example:
LDA #$01
JMP PRINT
RETURN_LABEL
RTS
PRINT
STA $0400
STA $D800
JMP RETURN_LABEL
JSR - Jump to Subroutine
JSRLABEL
JSR$0820
Jump to Subroutine is an unconditional jump too, with a key difference from JMP: it’s used together with the RTS (return from subroutine) instruction.
JSR and RTS are conceptually equivalent to the basic GOSUB and RETURN statements.
Technically speaking, JSR will PUSH the current Program Counter on the stack, or better said record the two bytes of the PC on the stack and decrease the value in the SP register by two. RTS instead will POP the PC from the Stack, or better said, increase the value in the SP register by 2 read from the stack the value of the last program counter recorded on it.
Example:
LDA #$01
JSR PRINT
RTS ; this RTS halts execution of your program and
; eventually gives the control back to the C64
; operating system
PRINT
STA $0400
STA $D800
RTS ; this RTS “returns” from the PRINT subroutine
Next time we will see conditional jumps, or branches.
Yesterday I was going to find how to write this program:
GOSUB SET-UP-SID
# we set up the continue variables here
# we want to have them visible in the main loop
CO% = 1 # # we can also initialize further variables here [...] LOOP:
# this is how we can emulate a while-true cycle in basic
IF CO% = 0 GOTO END-LOOP
# now we will read the notes and upload them to the correct channel
[...]
# now we will wait 1/4 of a second [...] # and after it we jump back
GOTO LOOP
END-LOOP:
GOSUB RESET-SID
END
SET-UP-SID:
# rem here we will set up the channels with the correct instruments # We already did this
RETURN
RESET-SID:
# rem here we will clear the SID # We already did this
RETURN And now I am going to continue it.
Please note that this formalism has nothing to do with how someone else would approach compuer programming. In the good old Basic Days planning the software was done using Flow charts. Nowadays Flow charts aren't neither taught nor used, and someone expects you to just fill random methods provided by your framework or choice in random points of the application.
How do we wait for one second?
I'm choosing to tackle this problem next because I need to have a sort of slowdown in place before reading the notes and putting them into the SID registers. The Commodore 64 basic gives two commands to read the time: TIME and TIME$. Due to architectural limitations they will give the time from the last system boot (including when you soft-reset the computer with SYS 64738). In addition to it the TIME is stopped when the computer is serializing data. As a last limitation I can think is that TIME is given pace from the current video signal coming out from the Commodore (an NTSC commodore will have a different timing from a PAL commodore, but I have to check).
The difference between the two instructions is that TIME$ will print the time as seconds and it should be text while TIME will print the time as 60th of seconds and will be a number.
We can type this program in and see the two commands in action.
10 FOR I = 1 TO 24
20 PRINT TIME ; TIME$
30 REM WE WANT A SLOWDOWN BEFORE
31 REM PRINTING THE NEXT VALUE OF TIME
35 FOR J = 1 TO 100 : NEXT J
40 NEXT I
Armed with this insight we can speculate that we can store the current time in a variable, test if 60 jiffies have passed and print the new time.
10 PRINT "{CLR/HOME}" ; TIME$ , TIME
20 TT = TIME + 60
30 IF TT =< TIME GOTO 10
40 REM IF YOU PRESS ANY KEY THE PROGRAM WILL END
41 GET A$ : IF A$ <> "" THEN END
50 GOTO 30
This is not perfect but for now it will do.
You can see that we have to change the central loop
# since this is a value that does not change we calculate it # here, just once DT = INT(60 / 4)
LOOP:
TT = TIME + DT
# this is how we can emulate a while-true cycle in basic
IF CO% = 0 GOTO END-LOOP
# now we will read the notes and upload them to the correct # channel
[...]
# now we will wait 1/4 of a second # and after it we jump back CHECK-TIME: IF TT =< TIME GOTO LOOP
GOTO CHECK-TIME
This does not complete all the reasoning about time because now we will have to test if the time has come for reading our notes in the SID.
Reading and playing the note data.
The following part is tricky, because we still need to do the following things: finalize the data structure, understand about which channel we are reading the following note, and understand
Reading the documentation for the SID we see that for playing a single note we need to insert two values for each channel, one for the LOW frequency and the other for the HIGH frequency. The two frequencies will be combined into a single note.
We also decide that the delay, for now, will be "quarters of second", so when the program jumps to loop we will need to check if the delay has stopped. Remember also that we want to test if the channel is still active.
So we need to change this representation:
[note-1-pitch],[note-1-delay]
to this one:
[note-1-low],[note-1-high],[note-1-delay]
And when we read the data we will just do READ LO READ HI READ DL
But before reading it we have to decide if we need to read a note, and after reading that note we need to decide when we have to read it next.
Each channel will have a timer. Each will be initialized, before entering the loop, with the current time.
T1 = TIME
If T1 is lower or equals to the current TIME then we will make the program read its values
IF T1 > TIME SKIP READ LO # read the low frequency READ HI # read the high frequency READ DL # read the delay. POKE L1,LO # we put in the Sid 1 low register the value read by low POKE H1,HL # same but with the high value T1 = TT + DT * (DL - 1) # this is when we will read the next note. SKIP: # we skipped here and here we will test channel 2 [...]
What does it happen if we want to shut off the channel? Or if the channel was shut off before?
We use another variable: C1, that we will initialize to 1 at the beginning of the program. So our micro-block becomes:
IF C1 = 0 SKIP IF T1 > TIME SKIP READ LO # read the low frequency READ HI # read the high frequency READ DL # read the delay. IF DL = -1 THEN C1 = 0 IF DL = -1 THEN SKIP POKE L1,LO # we put in the Sid 1 low register the value read by low POKE H1,HL # same but with the high value T1 = TT + DT * (DL - 1) # this is when we will read the next note. SKIP: [...] #we skipped here and here we will test for C2 and C3
Now we could copy the block and do the same things again
IF C1 = 0 SKIP_1 IF T1 > TIME SKIP_1 READ LO READ HI READ DL IF DL = -1 THEN C1 = 0 IF DL = -1 THEN SKIP_1 POKE L1,LO POKE H1,HL T1 = TT + DT * (DL - 1) SKIP_1: IF C2 = 0 SKIP_2 IF T2 > TIME SKIP_2 READ LO READ HI READ DL IF DL = -1 THEN C2 = 0 IF DL = -1 THEN SKIP_2 POKE L2,LO POKE H2,HL T2 = TT + DT * (DL - 1) SKIP_2: IF C3 = 0 SKIP_3 IF T3 > TIME SKIP_3 READ LO READ HI READ DL IF DL = -1 THEN C3 = 0 IF DL = -1 THEN SKIP_3 POKE L3,LO POKE H3,HL T3 = TT + DT * (DL - 1) SKIP_3:
However as you see this wall of text is unreadable, and as such it's prone to inserting errors. It's better to have a subroutine to handle the common parts of this block of code.
# channel 1 CS = C1 # status of the channel TS = T1 # time of the channel LT = L1 # low frequency register of the channel HT = H1 # high frequency register of the channel GOSUB COMMON_LOADER C1 = CS T1 = TS # channel 2 CS = C2 TS = T2 LT = L2 HT = H1 GOSUB COMMON_LOADER C2 = CS T2 = TS # channel 3 CS = C3 TS = T3 LT = L3 HT = H3 GOSUB COMMON_LOADER C3 = CS T3 = TS [..]
COMMON_LOADER: IF CS = 0 SKIP IF TS > TIME SKIP READ LO READ HI READ DL #note that we set the "off" channel in CS IF DL = -1 THEN CS = 0 IF DL = -1 THEN SKIP # the values of LT and HT will be L1 and H1 # in the first channel, L2, H2 and L3, H3 # in the other two POKE LT,LO # we put in the Sid 1 low register the value read by low POKE HT,HL # same but with the high value TS = TT + DT * (DL - 1) # this is when we will read the next note. SKIP: RETURN