Yes, no sugarcoating! In this third part we will be talking about pointers! Hello?….. Hello?
Usually, when people start to learn a language like C, the first topic that sends chills down your spine, but in a very bad way is “pointers”. However, rest assured, the concept is very simple as long as you have a basic understanding of how the computer memory works.
- Part 1: https://vitno.org/going-into-action-with-atari-xl-xe-part-1-hello-world/
- Part 2: https://vitno.org/going-into-action-with-atari-xl-xe-part-2-graphics/
Down the Memory Lane
Eight-bit computers are very easy to understand (if you ignore some specifics). For example, a 64K system has the memory mapped in 65536 bytes. Think as each byte to be a slot that can hold a number from 0 to 255 (for 8 bits machines).
Some of these slots are free spaces where your program can be placed, data stored, etc. Some of it is mapped to the video and yet some are mapped to the operating system where some addresses have specific meaning/functionality. The illustration below shows, for example, the memory map of an Atari 400/800:
Let’s see some examples. For now, boot in BASIC on your Atari or emulator for that.
One example of memory addresses (slot) that are connected (or mapped) to other chips functions is the address 710 ($02C6 hexadecimal) . This address lets you write a value to it, but when you do so, you will trigger a specific route in the hardware that will tell the computer to turn the screen purple.
The address 710 (as many others) are actually physically connected to other chips in the computer board. That chip will keep looking at that address over and over and when a new value is added there, the chip will blindly follow the order. An interesting analogy is a Cook locked in the kitchen with a small window from where the orders come from. When the waiter puts an order on that window, the Cook will take action based on the value that is in there.
Other addresses are plain free RAM slots meant to be used by the programmer to either store the program code, or some data. For example, if you look at the address 20480 ($5000 Hex) in the map above you can see this is free RAM space. The small program below reads what is in the address, then store the number 78 in it, then reads it again:
10 PRINT PEEK(20480) 20 POKE 20480,78 30 PRINT PEEK(20480)
Running the code will output 0 and 78. Line 10 reads that address for the first time and it will have zero in it. Line 20 stores the number 78 there and line 30 reads the same address, now showing what you had stored there.
This type of address acts like a dumb shelf storage. Just a place for you to put something so you won’t forget. Next time you need that information, you know where to get it from!
How Action! Variables work?
I’ve talked briefly about variables at the end of the Part 2 but now we need to dig a bit deeper on that concept.
When we declare a variable in Action!, let’s say, a BYTE, we tell the compiler to reserve a slot in memory to save a number that can be from 0 to 255 (representing 8 bits). It is the compiler job to find an empty slot (address) in memory and allocate that for the variable. It is easy to see this using the Action! integrated environment. Fire it up and type the following program:
PROC MAIN() BYTE X X=65 PRINTBE(X) RETURN
Then, press Shift+Ctrl M to enter Action! monitor. From there, simply type “C” to compile then “R” to execute the program. If everything is correct, you will see the number “65” being display on the screen. That is not what you are after, though.
Still on the monitor screen type, at the top, “? X” which tells the monitor to tell you the content of the variable X that you declared in your program. (If you haven’t compiled the program before giving this command, you will get an Error 8: Variable Not Declared message). This is the expected output:
After the program is compiled, Action! decided that the variable X will be stored in the address 26990 (You might see a different number) displayed in decimal (1) and in hexadecimal (2). The (3) displays the ATASCII representation of the value we stored and (4) the value itself in decimal. Let’s ignore the other two values for now.
If you went back and reviewed the part 2, you remember that Action! also supports another two data types (I will copy them here so you don’t have to click away again):
- INT: 16-bits signed, can hold numbers from -32768 to +32767
- CARD: 16-bits unsigned, can hold numbers from 0 to 65535
If each memory slot can hold only a 8-bt number (0-255), how Action! can store a CARD or an INT value of, let’s say, 28320.
Since both data-types are 16-bit you might have already realized that this is 8-bits times 2! :) That said, we can infer that the bigger number will be stored in two contiguous memory slots. But how to split it? It is easier to realize how to do that if you take the number’s hexadecimal representation: $6EA0. One slot gets $6E and the other gets $A0, since both are 8-bits long!
Let’s repeat the previous exercise but using a CARD instead:
PROC MAIN() CARD X X= 28320 PRINTCE(X) RETURN
Back to the monitor, compile the program and type again “? X” and you will see:
At a glance, this might be a bit confusing. The good part is that clearly, you can see the references to $6EA0/28320 on the right side of the equal symbol! So these two values indicate the 16-bit value starting on this memory address, which is the value we indeed stored in the variable X (Emphasis on the underlined starting!). That means that when you type “? X” you are asking for the byte that stores the first part of the number.
Type now “? X+1” and you will see:
The memory slot 26994 contains the second part of the number. However, if your decimal to hexadecimal mental converter is sharp you will notice that decimal 160 equates to $A0 ( 10(A) * 16 + 0 = 160), and 110 to $6E ( 6 * 16 + 14(E) = 110), which means the number parts are inverted in memory. This is normal and expected since this is the way 6502-based machines work (LSB, MSB).
Note: The second line on the monitor output above shows another 16-bit value ($4C6E/19566), but since this is based on the current address and the next, it has no meaning here, since we don’t know or care what is in the address 26995.
Since we are messing with the monitor, you can also dump the memory starting at the address where the variable X is stored typing ‘* X’. This will display all contiguous addresses without stopping. You can pause the list pressing CTRL+1 or stop it pressing SPACE.
I particularly don’t like the two columns that show the 16-bit values of the specific address and the following one, since it is not always that the value is relevant but since it is there, better to learn to accept it.
When I started writing the article, I didn’t think it would take this long to get to the point, but I hope the previous explanations are worth the reading. It will help for what is coming…
Other than the three basic data types (BYTE, INT, and CARD) Action! offers an extended data type called POINTER. Generally speaking, a variable of the type POINTER doesn’t store a simple 16-bit number, it stores a 16-bit memory address. Let’s think on another analogy.
The sign on the left holds a simple CARD number. When your Action! program reads that variable, it reads whatever is written there, in our example, the 765.
The sign next to it holds a POINTER value. In essence, the value is still a 16-bit number (Ignore the Main St part for sake of my argument ;) ), but when it is accessed by your Action! program, it will read the value of the address the POINTER is pointing to. In our example, also the 765 that is in the house at 123 Main St!
Hopefully, the analogy is good to make understand the mechanism, but I know it fails to show what is the advantage of using pointers. Reading a single value like that seems a waste of time and resources since I could simply read a CARD. However, the access to pointers is crazy fast and it makes a lot of sense when you are accessing many addresses in a row since you can easily increment the pointer value to read the next address.
As example, let’s see this example in Action!
PROC MAIN() CARD I CARD POINTER P16BIT ; SET WHAT ADDRESS THE POINTER ; POINTS TO P16BIT=28672 ; $7000 P16BIT^=62436 ; $F3E4 PRINT("VALUE AT ") PRINTC(P16BIT) PRINT(" IS: ") PRINTCE(P16BIT^) RETURN
After you compile and run the program you should see:
Line 3 declares a POINTER variable (16-bit long) named P16BIT of the type CARD. Like I said before, POINTER will always use 2 bytes to store the value. The CARD keyword indicates that the memory address of which P16BIT points to will store a 16-bit value as well (e.g. the player score).
Line 7, the program tells the P16BIT variable that it will point to the address 28672 (Hex $7000) (This address 28672 is an arbitrary pick from the free RAM).
Now the fun starts. Line 9 we tell the program to store the arbitrary value (e.g. our player score) 62436 ($F3E4) at the address indicated (pointed) by the variable P16BIT, which is in this case the address 28672. The big difference from line 7 is the presence of the symbol ^ after the variable name, indicating this is a pointer operation!
The next lines are responsible to print the output shown above. When you print simply P16BIT, you print the destination address or the address the variable is pointing to. When you print P16BIT^, you are printing the value of the address the variable P16BIT is pointing at.
A good way to see that is really what happened, you can use again the monitor debug capabilities. At the monitor command prompt, type * $7000 (get ready to press SPACE after a few lines are displayed!). Look what you should see the following output (I added the emulator monitor for sake of clarity):
As you can see, the address 28672 ($7000) holds the less significative byte (LSB) $E4 and the 28673 ($7001) holds the MSB $F3 (62436 = $F3E4). The Action! monitor output shows the first value of 28672 complete with 16 bits and in the correct order, but internally it is represented like the emulator debug window shows on the left. This can be proved if you look at the Action! monitor output for the address 28673 which shows only the $F3. Please, if this is not very clear, use the comments below to ask!
Another similar example where instead of setting one memory address, we set 5 consecutive ones:
PROC MAIN() CARD I CARD POINTER P16BIT ; SET WHAT ADDRESS THE POINTER ; POINTS TO P16BIT=28672 ; $7000 FOR I=1 TO 5 DO P16BIT^=62436 ; $F3E4 P16BIT==+2 OD PRINT("DONE") RETURN
This example only difference from the previous is that we are setting the address pointed by P16BIT five times in a row, thanks to the FOR loop. In the first iteration of the loop, when we add 2 to the P16BIT variable, it starts to point to the next 16-bit address, which in this case is 28674. Every loop, it points to the next until it does that five times or 10 bytes.
After running it and using the same debug techniques we used earlier, this is the content of the memory you should see:
As you can see, the memory addresses from 28672 to 28680 will be filled with the value 62436. Remember that for the Action! monitor to make sense you should only pay attention to the odd lines (highlighted). The even lines are displaying a byte from the previous address and a byte from the next!
If instead of a CARD, you declare the pointer as a BYTE, the behaviour will be different just by the fact you are reading and storing 8-bit values. The next example is a slightly modified version of the previous, using BYTE instead of CARD and instead of writing a constant number, we are storing 242, 243, 244, 245, 246 just to make it easier to see the difference.
PROC MAIN() CARD I BYTE POINTER P8BIT ; SET WHAT ADDRESS THE POINTER ; POINTS TO P8BIT=28672 ; $7000 FOR I=1 TO 5 DO P8BIT^=241+I ;$F2,$F3,ETC P8BIT==+1 OD RETURN
After running this program and typing * $7000 (don’t forget the $, I did that many times and looked at the wrong address!), you should see:
This time around, you see the five bytes with new values, incremented by one when we set it using the command at line 11. And line 12, the pointer is incremented by one byte only!
To see pointers in action (pun intended) lets now execute this program below, which will fill the screen with a graphic pattern. The program uses two pointers, a CARD DLADDR which points to the 16-bit DLI address (to find where the screen memory starts on graphic mode 7) and a BYTE SCR which points to the actual memory address content and is used to store a byte there.
PROC MAIN() CARD I BYTE POINTER SCR ; BEGINNING OF SCREEN POINTER CARD POINTER DLADDR ;DISPLAY LIST ADDR GRAPHICS(7+16) DLADDR=PEEKC(560)+4 SCR=DLADDR^ ; PLOT AN ARBITRARY PATTERN ON SCREEN FOR I=1 TO 3840 DO SCR^=135 SCR==+1 OD FOR I=1 TO 33000 DO OD RETURN
After run the program, you will see the following output:
The program loops 3840 times and quickly fills up the whole screen! The power of the pointer!
I hope this sheds some light to the pointer concept. Was it helpful? I invite you to comment, make suggestions, correct me and asking questions in the comments below.