Jump to content
IGNORED

Assembly on the 99/4A


matthew180

Recommended Posts

Thanks, I got it running. Cool test. Any plans to do something more with this? It's good start

Yes! I've always loved the simple 1970s Odyssey2 style football games. The maIn problem with early sports titles was no AI, so no single player mode.

 

TI Football is a drag! It's like someone put a crappy graphical UI onto a boring turn based simulator. I remember buying it early on (1980-81) and wishing it was joystick playable, like Atari and especially Odyssey2.

 

I'd like to remedy that situation with a simple Football game for the 99/4A.

 

Working Title: Madden79

 

-James

Edited by Airshack
Link to comment
Share on other sites

Take a look at On Field Football for the commodore 64, it simple but pretty great. Your test reminded me of it instantly.

 

presentation is vertical not horizontal, but it 4 vs 4 player game.

 

http://www.lemon64.com/?mainurl=http%3A//www.lemon64.com/games/details.php%3FID%3D1834

Thanks! Awesome tip! I'm just beginning to explore the C64. Will check it out.

Link to comment
Share on other sites

When I first started messing around in Magellan I made this

 

VDHyQvy.png

Those players look great! Willing to upload the Magellan file?

 

It does seem better to start with a single-directional scrolling field, L/R or U/D. The up/down configuration you have looks great.

Edited by Airshack
Link to comment
Share on other sites

Those players look great! Willing to upload the Magellan file?

It does seem better to start with a single-directional scrolling field, L/R or U/D. The up/down configuration you have looks great.

 

Yes, I'll upload it later tonight, use it as you see fit, my programming skills are lacking to take it any further.

  • Like 1
Link to comment
Share on other sites

If I were writing a CRPG, for combat I think I would use a structure of data for the mobs (monsters) and players. It might look something like (using a C-ish pseudo code):

structure mob
begin
    hitpts   data    * total hit points
    damage   data    * damage taken, dead when equal to hitpts
    armor    data    * armor class, used in mob hit calc
    weapon   data    * damage done by current weapon
end
This is a very minimal set of data for an RPG combat system, but it is only to demonstrate the use of a structure. So you would have this "set" of data, i.e. 1 structure for each mob. You would also have a similar structure for the player(s), but those would probably contain a lot more "fields" (each piece of data in the structure).

 

In a higher level programming language, the language itself would give you a way to define variables of the structure itself, as well as access the fields. Something like this:

    skeleton mob    * Make a variable called "skeleton" of type "mob"
    level    data   * Current level
.
.
.
    level := 3

    * Initialize the skeleton based on the level or area
    skeleton.hitpts := level * 25
    skeleton.damage := level * 0
    skeleton.armor  := level * 5
    skeleton.weapon := level * 8
Again this is very simplistic, but is used to demonstrate creating and accessing a "mob" variable. Of course you won't always have just 1 mob. So you would make an array of mobs and cycle through each one during combat until they were all dead, or the player(s) are all dead.

    num_mobs  data      * number of mobs
    mobs      mob[10]   * fixed array of up to 10 mobs max
    i         data      * general loop variable
.
.
.
    num_mobs := rnd(1,10)       * pick a random number between 1 and 10

    for i := 0 to num_mobs - 1  * loop through the mobs and initialize them all
    begin
       mobs[i].hitpts := level * 25
       mobs[i].damage := level * 0
       mobs[i].armor  := level * 5
       mobs[i].weapon := level * 8
    next i
Note that our array is zero-offset, i.e. the first subscript to the first element is an index of zero, not one.

 

In assembly language, the language does not provide us with structures (although it could), so we have to maintain them manually. Also, because of the limited resources on older computers, it is a lot easier to use static sized arrays vs. trying to deal with managing dynamic memory use. In a modern system, you could ask the OS for a blob of memory after you choose your random number for the number of mobs. For us, the memory management would be overkill. You might, however, have a blob of RAM designated for multiple uses. So during combat it could contain mob structure data, and when in town it could contain store and merchant structures, etc.

* Structure MOB
MOB_HP EQU  0      * 1 word, bytes 0,1
MOB_DM EQU  2      * 1 word, bytes 2,3
MOB_AR EQU  4      * 1 word, bytes 4,5
MOB_WP EQU  6      * 1 word, bytes 6,7
MOBSIZ EQU  8      * Size of the mob structure in bytes, word aligned

BLOB   EQU  >F000  * 1K blob of RAM from >F000 to >FFFF

MOBS   EQU  BLOB   * the mob array uses the RAM blob
TOWN   EQU  BLOB   * the town array uses the RAM blob

MOBNUM DATA 0
LEVEL  DATA 0
.
.
.

* Set up the mob array for combat
*
       BL   @RANDNO          * Get a random number of mobs from 1 to 10
       MOV  R2,@MOBNUM       * Store the number of mobs (assumes RANDNO returns a number 1 to 10 in R2)

       LI   R1,MOBS          * Set R1 to the start of the array, i.e. blob RAM
MOBINI
       LI   R3,25            * do mobs[i].hitpts := level * 25
       MPY  @LEVEL,R3        * R3,R4:= 25 * level
       MOV  R4,@MOB_HP(R1)   * Store in the current mob structure in the array

       CLR  @MOB_DM(R1)      * mobs[i].damage := level * 0

       LI   R3,5             * do mobs[i].armor  := level * 5
       MPY  @LEVEL,R3        * R3,R4:= 5 * level
       MOV  R4,@MOB_AR(R1)   * Store in the current mob structure in the array

       LI   R3,5             * do mobs[i].weapon := level * 8
       MPY  @LEVEL,R3        * R3,R4 := 8 * level
       MOV  R4,@MOB_WP(R1)   * Store in the current mob structure in the array

       AI   R1,MOBSIZ        * Adjust R1 to point to the next structure in the array
       DEC  R2               * Account for this mob
       JNE  MOBINI           * Not the last mob, initialize the next one

* Start combat
COMBAT
       LI   R1,MOBS          * Reset R1 to the start of the mobs array in blob RAM
       MOV  @MOBNUM,R2       * Reset the mob counter for combat
.
. Insert combat code here... <img src='http://www.atariage.com/forums/public/style_emoticons/<#EMO_DIR#>/icon_winking.gif' class='bbc_emoticon' alt=';-)' />
.
The first time you do combat the mob data could randomly be generated. If the players end up fleeing then the mob data could be stored to disk for that map / world location and then reloaded next time the players enter that area again. Or the mobs could be randomly generated every time in an endless stream of mobs in that area. It all depends on how you decide to deal with such things. So the initialization code should probably be a subroutine (and made to deal with all the mob types in the game), and you would either call the initialization subroutine or the "load mobs" subroutine depending on various factors.

 

Is this the kind of information you are were looking for?

 

Matthew

 

 

I have a few questions about Matthew's post from way back in 19 Aug 2010. I really appreciate how he used C-ish pseudo code to describe building a data structure in Assembly. This was easy to understand as I do have some distant memories of programming in C. Again, well done and exceptionally expressed!

 

Questions come from this line of code: BLOB EQU >F000 * 1K blob of RAM from >F000 to >FFFF

 

A. Why do you choose to store your data structures (variables) in the "Upper" 24K?

 

B. While messing around with mini-memory for a few days, I learned to use AORG, to set the Location Counter. This gave my object code an absolute address. Some examples (Assembly code I've examined) do not utilize the AORG Assembler Directive? Why? Seems this will put your data structures in jeopardy!

 

C. Why wouldn't the programmer always want the object code to have an absolute address? I'm thinking to de-conflict code lines with stored data.

 

D. If I store all my variables up in >F000->FFFF, how do I know something won't mess the numbers up later on at runtime? Much like the scratchpad gets messed with in some cases... say the way BLWP can trample on a portion of the scratchpad RAM?

 

E. Are there any accepted best-practices for where variables are stored beyond the speedier scratchpad RAM?

 

F. Is there a good rule of thumb regarding how to design the location of your data structures beyond the scratchpad? Seems to me I'd probably put everything there until I ran out, and then re-examine my code and later move less utilized data to the Upper/Lower RAM?

 

I suppose my questions all concern memory management.

 

-James

Link to comment
Share on other sites

 

I have a few questions about Matthew's post from way back in 19 Aug 2010. I really appreciate how he used C-ish pseudo code to describe building a data structure in Assembly. This was easy to understand as I do have some distant memories of programming in C. Again, well done and exceptionally expressed!

 

Questions come from this line of code: BLOB EQU >F000 * 1K blob of RAM from >F000 to >FFFF

 

A. Why do you choose to store your data structures (variables) in the "Upper" 24K?

 

B. While messing around with mini-memory for a few days, I learned to use AORG, to set the Location Counter. This gave my object code an absolute address. Some examples (Assembly code I've examined) do not utilize the AORG Assembler Directive? Why? Seems this will put your data structures in jeopardy!

 

C. Why wouldn't the programmer always want the object code to have an absolute address? I'm thinking to de-conflict code lines with stored data.

 

D. If I store all my variables up in >F000->FFFF, how do I know something won't mess the numbers up later on at runtime? Much like the scratchpad gets messed with in some cases... say the way BLWP can trample on a portion of the scratchpad RAM?

 

E. Are there any accepted best-practices for where variables are stored beyond the speedier scratchpad RAM?

 

F. Is there a good rule of thumb regarding how to design the location of your data structures beyond the scratchpad? Seems to me I'd probably put everything there until I ran out, and then re-examine my code and later move less utilized data to the Upper/Lower RAM?

 

I suppose my questions all concern memory management.

 

-James

 

A. The space >F000 - >FFFF is actually 4 Kbytes. Since a mob is eight bytes, it's 512 mobs.

Where to put them doesn't really matter. You should store them somewhere.

 

B. Relocatable code is more flexible. Instead of you keeping track of where everything goes in memory, you let the loader do that instead. The assembler will generate relocatable addresses to both code and data from your symbols, and the loader will convert them to absolute when the code is loaded. That you used absolute code with Mini Memory is because you had to force the code to go into that module, not wherever the loader preferred.

 

C. There's no conflict between code and data for relocatable code. It's rather the opposite. The assembler/loader will keep track, but if you generate absolute code, it's up to you entirely.

 

D. If you store something at >F000, then that's per definition an absolute location. It's up to you to make sure you don't mess that area up. A combination of absolute and relocatable code is the most difficult to keep track of, except for when storing data in reserved areas, like the scratch pad RAM. Such a place is pretty easy to keep track of.

 

E. The scratch pad RAM is too small to store large data structures in. Workspaces, frequently accessed data and sometimes small chunks of time critical code is best placed there. For that, the AORG directive is good, provided your loader can handle it properly.

 

F. I would analyze which data to put in fast memory from the beginning. Start messing with that later is hardly the most efficient way. You should have at least a reasonably good idea of what will be most frquently used already when you start.

  • Like 1
Link to comment
Share on other sites

 

A. The space >F000 - >FFFF is actually 4 Kbytes. Since a mob is eight bytes, it's 512 mobs.

Where to put them doesn't really matter. You should store them...

This question needs rephrasing. I know things need to be stored and 2^12 = 4K. This 4K resides in the top of the upper 24K. My question here is basically why did Matthew decide to define this range specifically?

Link to comment
Share on other sites

 

 

B. Relocatable code is more flexible. Instead of you keeping track of where everything goes in memory, you let the loader do that instead. The assembler will generate relocatable addresses to both code and data from your symbols, and the loader will convert them to absolute when the code is loaded. That you used absolute code with Mini Memory is because you had to force the code to go into that module, not wherever the loader preferred.

 

 

So with a full-up expanded system I don't need to concern myself with absolute code?

 

In BASIC you can always check MEMory size as your code grows. It's probably something obvious I'm missing but how is this done in Assembly?

 

-j

Link to comment
Share on other sites

I totally liked Matthews tutorial on his way to learn and speed up assembly. So I created a .pdf of all the post to make for easier reading. I did some very,very minor cleaning up and added some of the side information into the document.

thanks for the great tutorial Matthew.

here's the .pdf if anyone else would like to use it.

http://atariage.com/forums/topic/162941-assembly-on-the-994a/?p=2414702

 

This PDF is an exceptional summary for anyone following this thread. Many thanks to hloberg! Reposted in case anyone missed it.

Edited by Airshack
Link to comment
Share on other sites

This question needs rephrasing. I know things need to be stored and 2^12 = 4K. This 4K resides in the top of the upper 24K. My question here is basically why did Matthew decide to define this range specifically?

 

Instead of defining a fixed location you could also end the program with

 

MOBS BSS 4096

 

Then the mobs would always follow right after the program, no matter how long it is.

  • Like 2
Link to comment
Share on other sites

 

Instead of defining a fixed location you could also end the program with

 

MOBS BSS 4096

 

Then the mobs would always follow right after the program, no matter how long it is.

It appears the example code's fixed location actually complicated matters without providing any real gain.

 

So it's common to use Block Starting with Symbol at the end of code to reserve memory space for variables? Trying to shift from the BASIC paradigm here.

 

The Tombstone City code was provided with the E/A Manual I purchased. Apparently, TI felt the code was worthy of examination by those beginning with Assembly.

 

I'm wondering if this is an exercise I should consider at this point?

 

I've been told some of the older code from programming books of the day (80s) contain bad coding practices. Obviously some improvements in technique have materialized over the years.

 

Any thoughts on the Tombstone City code?

Link to comment
Share on other sites

If you want to reserve a memory area, you can place BSS or BES anywhere in your program. The computer doesn't care if your data is in the middle of the code or at any end. As long as you don't start executing the data as code, it doesn't matter. But that's something you have to avoid wherever you have your data.

BSS (Block starting with symbol) implies that it defines a symbol at the first address of the block. You then increase the address to go further into the data. BES (Block ending with symbol) is the same, but the symbol is defined at the end of the data. If you have a stack, where you want it to grow towards lower addresses, then you want the address to initially load to the stack pointer to be at the end (high address) of the data.

 

A push is then done with

DECT SP

MOV @DATA,*SP

 

and a pop with

MOV *SP+,@DATA

 

As there's no pre-autodecrement, only post-autoincrement, this works better than having the stack grow in the other direction, as the stack pointer here always point at the current top entry on the stack.

You don't need BES to define such a stack, since you can do

 

DUMMY BSS stackspace

STACKBOTTOM EQU $

 

but it's convenient.

 

Anyway, placing player data at >F000 is completely arbitrary. You can have it wherever you like, as long as you know where you have it.

And as long as your program (game) fits in the memory as a single item, i.e. isn't bigger than 24 Kbytes, there's no need to worry about absolute locations. Just let the program and data be relocateable, and let the loader/linker stuff it where it wants to. The only thing you should AORG in such a case is variables and stuff you want to optimize access to, so that you want to direct them to fast memory. I never care with my TI, but I have fast memory everywhere.

  • Like 1
Link to comment
Share on other sites

Depending on the assembler/linker, BSS, DATA, BYTe, and other directives reserve the space and can increase your final program size. For example, if I write a simple program such as :

 

DEF HELLO

DATA1 BSS >2000

DATA2 BSS >2000

HELLO BLWP @0

 

The program image size would usually show as >4004 bytes (64+ sectors) not counting the program header.

 

If we use EQUate to point to our data:

 

DEF HELLO

HELLO BLWP @0

DATA1 EQU >C000

DATA2 EQU >E000

 

The program image here would compile to 4 bytes. Two 8K data segments would be hard-coded to start at >c000 and >e000, but since we are only pointing to the location, no space is reserved for the program image (unless you explicitly save the memory contents). Thus knowing your data expectations is important for program size.

 

A Dummy ORiGin can simplify allocation by allowing you to use the BSS, BYTE, and other directives to set the address without actually allocating the space in the program image. The following program should still only compile to 4 bytes.

 

DEF HELLO

DORG >C000

DATA1 BSS >2000

DATA2 BSS >2000

RORG

HELLO BLWP @0

 

DORG helps avoid the dreaded EQUate chains that pop up from time to time:

 

DATA1 EQU >C000

DATA2 EQU DATA1+>2000 (Infers the length of DATA1 is >2000 bytes)

DATA3 EQU DATA2+>400 (infers the length of DATA2 is >400 bytes)

... and so on.

 

Ultimately it is up to the programmer to define how the program and data structures reside in memory. Overlapping / multi-use buffers, initial data values, and program size are important deciding factors.

Edited by InsaneMultitasker
  • Like 1
Link to comment
Share on other sites

DORG can be convenient. You can map a memory area without actually populating it with data. Like if you want to use the fast RAM in the console, you can have a DORG >8300 directive, below which you lay out data and perhaps code that you plan to have in that memory area. When your program starts, you'll have to move the proper data into the space, but you already then have the symbols correct.

  • Like 1
Link to comment
Share on other sites

D. If I store all my variables up in >F000->FFFF, how do I know something won't mess the numbers up later on at runtime? Much like the scratchpad gets messed with in some cases... say the way BLWP can trample on a portion of the scratchpad RAM?

 

Because there is nothing else except your code running. Unless you are calling system routines, but even then those functions have documented what memory areas they access. You have to keep in mind that *your code* has complete control over the whole system, and there is never any code running at the same time. That's what makes assembly language on retro-hardware so cool IMO.

 

BLWP is not really trampling on anything. You know when you make the call what is happening, it is not an unknown, and what is does is very well defined. You have a choice to use it or not.

 

This question needs rephrasing. I know things need to be stored and 2^12 = 4K. This 4K resides in the top of the upper 24K. My question here is basically why did Matthew decide to define this range specifically?

 

That was a long time ago to remember. I suspect I chose that address for no other reason than to simply to satisfy the example. And I even messed up the size calculation. Memory management is always going to be up to you as the programmer, and letting the compiler deal with code and data locations as much as possible will always make your life easier.

 

It appears the example code's fixed location actually complicated matters without providing any real gain.

Mostly true, but it always depends on the situation. For example, for code that will run out of a cartridge you have to at least use fixed origins for the code in the cartridge. And you typically don't "load" your code from a cartridge into RAM, so if your code uses expansion RAM then you will have to manage the use of the 32K as fixed locations.

 

So it's common to use Block Starting with Symbol at the end of code to reserve memory space for variables? Trying to shift from the BASIC paradigm here.

Some people put it at the front, some the back, others use it where ever. It depends on your own style and how you like to organize your code and data. I like variables defined first so I know what they are before I see them used, then code, then block data storage at the bottom where it has as much room as possible.

 

The Tombstone City code was provided with the E/A Manual I purchased. Apparently, TI felt the code was worthy of examination by those beginning with Assembly.

Yeah, that was pretty awesome of TI to do that! It took me a while to understand what those disks actually were BITD, and when I made the realization I was very giddy about it.

 

I'm wondering if this is an exercise I should consider at this point?

That is up to you. There are some interesting techniques in there, and I probably picked up a few ideas here and there. I always found the inline parameter passing interesting, and I have used that from time to time. TSC was also where I got an assembly language RNG, since BITD I had no idea how to generate random number in assembly. I did not understand how it worked, but I lifted and used the function successfully.

 

I've been told some of the older code from programming books of the day (80s) contain bad coding practices. Obviously some improvements in technique have materialized over the years.

That sounds like comments I have made. ;-) Keep in mind that everyone has their own way of doing things, and there is rarely an all-out "wrong" way to code something. Also, sometimes bad practices make for easier learning, which can sometimes out-weigh the bad practice. For example, it was the Lottrup book that opened assembly language to me (prior to that book all I had was the E/A manual). For me the assembly language equivalent to a "hello world" program was "clearing the screen", which happens to be one of the first programs in the Lottrup book.

 

The idea of writing a "space character" to every location on the screen was very understandable to me, and a cool revelation (I had no idea how clearing the screen worked before that). So seeing a simple loop calling VSBW for every screen location made sense and was so fast compared to BASIC that it just did not matter. The example was very short and to the point (as was most of the book), which was probably what made it so successful, and exactly what I failed to do when I started this thread. If the book had tried to explain the VDP auto-increment register, and that VSBW was inefficient because it unnecessarily sets the VDP address register every time it is called, or what BLWP was really doing, or what VSBW really was, or how the VDP registers were memory-mapped, I probably would have gotten confused, or I didn't care at the time, or the book would have been the size of the E/A manual, etc.

 

The point is, when learning you typically just want to get on with making things happen and need to push the "it just works like that" button from time to time. Once you get a better understanding of what is going on, then come back around to learn the low-level details and all the "whys and hows" of everything. This is also a point I think most assembly programming books make universally across the board. Probably every assembly language book I have (for any CPU) always start out trying to teach and explain numbering systems, binary, hex, etc. using exponential notation no-less, then follow that with boring-ass examples of converting from one base to another. UGH! Give me a break! No wonder people get turned off! In this regard I think TI BASIC was absolutely brilliant for using hex to define characters. Immediately you have a visual and meaningful application of binary *and* hex, all in one place, and you are creating something at the same time! The results were immediate and people like graphics and sound.

 

Any thoughts on the Tombstone City code?

 

I used TSC code a lot BITD when I was learning and did not know how to do certain things. It was an awesome resource for a 13-year-old in isolation (I had no idea BBSes even existed let alone a modem to use them, or technical magazines, or shareware, etc.) There is still something to learn, but the tools, information, and community we have these days is far greater than a single body of work.

 

The random number generator is a bit flawed. In particular, the values always alternate even and odd.

 

This is easily fixable with some bit rotation; I've used the VDP timer value as the number to rotate to keep it very random.

 

There is a whole thread (too lazy to find it) here on random number generation in assembly. I started it with a naive (and false) statement, but by the end there is good example code to follow, plus a nice understanding of pseudo RNGs for retro computers.

  • Like 1
Link to comment
Share on other sites

A push is then done with

DECT SP

MOV @DATA,*SP

 

and a pop with

MOV *SP+,@DATA

 

As there's no pre-autodecrement, only post-autoincrement, this works better than having the stack grow in the other direction, as the stack pointer here always point at the current top entry on the stack.

You don't need BES to define such a stack, since you can do

 

Well, on the TI-99/4A, I've always used the stack growing in the other direction. To follow your example ...

 

A push is then done with

MOV @DATA,,*SP+

 

and a pop with

DECT SP

MOV *SP,@DATA

;)

 

  • Like 1
Link to comment
Share on other sites

 

Because there is nothing else except your code running. Unless you are calling system routines, but even then those functions have documented what memory areas they access. You have to keep in mind that *your code* has complete control over the whole system, and there is never any code running at the same time. That's what makes assembly language on retro-hardware so cool IMO.

 

 

The only thing that does run at the same time is the interrupt routine (ISR), unless you turn it off with LIMI 0.

  • Like 1
Link to comment
Share on other sites

True, but technically the ISR does not run at the same time, it is just a subroutine that is executed in response to a hardware event. The 9900 is a single-core CPU and can only run a single instruction at a time. If the ISR is running, your code is not, and vice versa.

 

I was mostly making the point to help break the association with modern multi-core CPUs that can actually be executing multiple instructions from different programs at the same time; and which typically run in an environment controlled by a host operating system.

 

Most retro computers of the 99/4A era do not have such "problems" like a pesky OS or other programs to share the CPU with, and your code can be in complete control over the entire system all the time.

 

Even the 99/4A ISR is limited and has known functionality, and won't go tromping through memory.

  • Like 1
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...