Jump to content
IGNORED

Couple assembly questions


Opry99er

Recommended Posts

I know this is very elementary, but I have a couple simple questions for you guys.

 


LOOP BLWP @VSBW
    INC R0
    DEC R2
    JNE LOOP

 

 

How does JNE know to look at R2 here... is it the fact that R2 was the last register referred to in the source? For instance, if the code was written:

 

DEC R2

INC R0

JNE LOOP

 

Then would JNE look at R0?

 

I don't have any reference material on me right now, and my slow-ass tethered internet is not allowing me to DL anything to help me out. Sorry. =)

 

**I have a couple more questions as well, but I'm going to read through the assembly threads on here before I start asking questions which have already been answered. This forum is amazing for knowledge. =)

Edited by Opry99er
Link to comment
Share on other sites

Thanks. =) I'd love to look at the manual... but can't look at any source material right now, unfortunately. My internet is slower than 56k dialup and I don't have the majority of my books here... In KY in a box. Grrrr....

Edited by Opry99er
Link to comment
Share on other sites

So, I INCreased R0 and DECreased R2 in the example code there...

 

Are the status bits of a register "reset" after another register's status bits are set by another instruction?

 

By that I mean, if the code is linear, are the bits ONLY set for the most recently modified register?

Edited by Opry99er
Link to comment
Share on other sites

I don't believe they are really "reset", but some bits are "affected". The Status Register can therefore have "leftovers" from previously executed instructions. So you can actually make a conditional jump based on the result of an instruction executed "long before" (relatively speaking).

 

9900.2.gif

Edited by sometimes99er
Link to comment
Share on other sites

The 9900 has an internal status register that contains several "flags" (bits in the register) that are *possibly* affected by each instruction. The tables posted above summarize the flags and which instructions affect each of the flags.

 

The logical "jump" instructions look at specific flags to determine whether or not to take the branch. For example, the JEQ (jump if equal) and JNE (jump if not equal) instructions only look at the EQ (equal) flag to decide what to do. For many instructions the CPU will automatically compare the result of the instruction to zero and set the EQ flag accordingly. In your example, the DEC instruction is one such instruction that will have its result compared to zero and the EQ flag set.

 

The advantage is, if you count down in a loop, instead of up, then you can save yourself a compare instruction inside the loop. For example, this code will clear the screen, assuming the name table is at VRAM >0000 (typical):

 

LI R0,>0040

MOVB R0,@>8C02

SWPB R0

MOVB R0,@>8C02

 

LI R1,>2000

LI R2,768

LOOP MOVB R1,>8C00

DEC R2

JNE LOOP

 

VS this (just the loop part this time):

 

CLR R2

LOOP MOVB R1,>8C00

INC R2

CI R2,768

JNE LOOP

 

 

Also, you might see this in some code somewhere and wonder why:

 

MOV R0,R0

 

The MOV instruction will have the result of the move compared to zero, so this is a faster and more compact way to see if a register's value is zero. This would be more along the lines of what you might expect to see:

 

CI R0,0

 

I hope this helps somewhat.

 

Link to comment
Share on other sites

Actually yes... That is an excellent explanation! I have used JNE almost every time I sat down to refresh myself with assembly (usually writing a series of single bytes to VDP RAM) but never actually understood WHY it focused on my R2 and not on any other register.

 

In looking at some of my old source code, there are many things I did on "convention" but did not understand fully. My goal is to understand the "whys" and not just the "hows" on this pass.

 

I'm going to attempt to convert Riding For The Brand into assembly language sometime in the next couple months, and I'm trying to refresh my knowledge of the language... A language I never really "knew" in the first place. :) Reading back through some of the assembly threads on here is bringing my meager knowledge back up to speed...

 

It was quite a shocker when I tried to write a screen clear from scratch and didn't remember to setup my RED DEF tables... :)

 

Thanks for the info.

Link to comment
Share on other sites

So many of these instructions SEEM to be 16 bit "word" modifiers... However that is a bit deceiving as they operate only on the MSB of the value at the specific memory address indicated in the operand field...

 

>2000 would not be "32" in decimal assuming we were taking an entire word value, correct? That would be >20. >2000 Would be 8192 in decimal...

 

So can I assume that the opcodes which only act on the MSB are actually BYTE modifiers?

 

***Again, forgive me for my lack of understanding... I'm just tired of not being in the ranks of "assembly programmer". =)

Link to comment
Share on other sites

Learning assembly language is really an exercise in understanding how a CPU and computer represent and handle data, and then learning how to describe your problem within those constraints. I think this is where people have the hardest time when they come *down* to assembly. The high-level abstractions are gone and you no longer have "strings" or "number with decimal points", or "arrays", or "loops", etc. All you get are the raw data types of the CPU, which are bytes and 16-bit words in this case. Everything else you have to build up yourself.

 

Not knowing how a high-level language implements things like strings and arrays can make using those "ideas of data representation" in assembly somewhat daunting for the beginner. However, once you learn what is going on behind the scenes in your favorite high-level language, you will start to have a different perspective and understanding of that language, and you will become a better programmer in that language as well.

 

Data is data. You have bytes to work with. How you treat those bytes is up to you. For example, what is the difference between these numbers being held in a byte of memory:

 

-1

255

>FF

 

It is a trick, they are the same. Look at them in binary:

 

-1 = 1111 1111

255 = 1111 1111

>FF = 1111 1111

 

It is simply *how* you decide the treat the data, and how you decide to write your data in the assembler. A number can be negative or positive simply depending on how you work with the data. A solid understanding of how a computer represents negative numbers is very important. Also, learning the relationship between binary and hex, as well as getting comfortable with both, is equally important.

 

You also don't need the "REF" stuff. Those cause the assembler to give you "references" to routines built-in to either the console or the cartridge (like the E/A). While using those routines can be handy for sure, depending on your needs they can be a source of frustration, as well as continuing to keep you from completely understanding what you are doing. I assume you are using the Lottrup book? That book was great for me when I was learning and it really helped me start to produce working code. However, it taught me a lot of bad habits too (like INC/C in loops instead of DEC/JNE), and working with the VDP was always a mystery to me. I never knew how BLWP @VSBW would get data on the screen (or even what the heck BLWP and LWPI were/did.) I didn't realize it was an abstraction, and a very *slow* one at that.

 

The built in routines were designed to be compact, not fast. For games, you really want to avoid them. At the very least learn how to use the VDP (and other hardware) directly, and then you can make an informed decision as to when you might "roll your own code" vs. use a REFerenced ROM routine.

 

In looking at some of my old source code, there are many things I did on "convention" but did not understand fully. My goal is to understand the "whys" and not just the "hows" on this pass.

 

Goes hand-in-hand with my opinions above. :-) Other than this forum with all the great people here, the 9900 "datasheet" will be your second best friend, followed by the E/A manual. The E/A is good for 99/4A specific stuff, and some verbose explanations of the 9900 instruction set, but it can also be very confusing sometimes until you understand more of what is going on.

 

Porting your game to assembly is a good goal! You have working code and you know what it is supposed to do. It will also be very rewarding.

 

So many of these instructions SEEM to be 16 bit "word" modifiers... However that is a bit deceiving as they operate only on the MSB of the value at the specific memory address indicated in the operand field...

 

The 9900 has about eight (going by memory here, I didn't look them up) byte/word instructions. Everything else is *word only*. The byte-oriented instructions only operate on the MSB of register source / destination operands because the 9900 is "big endian", i.e. a 16-bit value will be stored with the MSB at an even address, and the LSB at an odd address. You don't have to guess what instructions work with bytes, since they all have "B" in them, i.e. MOVB, CB, AB, etc. If it does not have the "B" then it is a word instruction. If it has the "B" then is only affects the MSB of the register.

 

For example:

 

LI R0,>ABCD * put the 16-bit value >ABCD into R1

MOV R0,@>A000 * store the value in R1 to memory at address >A000

CLR R1

CLR R2

MOVB @>A000,R1 * Move the *byte* at memory address >A000 to the MSB of R1

MOVB @>A001,R2 * Move the *byte* at memory address >A001 to the MSB or R2

MOVB @>A000,@>A003 * Move the byte of memory at address >A000 to the byte at address >A003

 

Memory starting at address >A000 would look like this:

 

>A000 >AB

>A001 >CD

>A002 ??

>A003 >AB

 

R1 would contain >AB00 and R2 would contain >CD00. Pay attention to that! Byte operations on register operands *always* operate on the MSB. However, for memory operands, the byte instructions can operate on any address. Also pay attention to when indirect addressing, i.e. *R1, *R2+, etc. is used. Indirect addressing takes the 16-bit value of the operand and uses that as an address of the source or destination, not the register itself. For example:

 

LI R0,>A001

MOVB *R0,R1 * The MSB of R1 now contains >CD, the LSB of R1 is unaffected.

 

That takes the *byte* at address >A001 (the *value* of the R0 register is used as the memory address to get the data from) and stores it in the MSB of R1. R0 does not change. The auto-increment does affect the value or R0 though:

 

LI R0,>A000

MOVB *R0+,R1

 

That would put >AB in the MSB of R1, and increment the full 16-bit value of R0 by 1. So R0 would not contain >A001. This is commonly used to help work with blocks of byte data.

 

If you used the MOV instruction, then you would copy a 16-bit value:

 

LI R0,>A000

MOV *R0,R1

 

R1 would contain >ABCD. If you use the auto-increment with the word instructions, the 9900 will increment the source by 2!

 

LI R0,>A000

MOV *R0+,R1

 

R1 still contains >ABCD like before, but R0 now contains >A002. This is commonly used to copy blocks of 16-bit words, vs. bytes.

 

Be *VERY* careful that you understand that the word operations *always* operate on 16-bit values at *EVEN* addresses. This is also a subtle nuance that can bite you. For example:

 

LI R0,>A001

MOV *R0,R1

 

What is R1? It will be >ABCD since MOV is a word instruction and will only operate on even addresses. So, the >A001 value of R0 is cut down to >A000 (the least significant *bit* is ignored) before being used as the address of where to get the source data. Sometimes you might want this, but usually not. Again, it depends on the circumstances and what you are doing. I remember a time when that came in handy (I can't remember the situation), and knowing that bit of information about the CPU made the code easier and more efficient. Of course I commented what was happening though!

 

>2000 would not be "32" in decimal assuming we were taking an entire word value, correct? That would be >20. >2000 Would be 8192 in decimal...

 

Correct. >2000 is either a 16-bit word with the value 8192 decimal, or two bytes with the values 32 and 0, depending on how you are working with the data. I used >2000 because in the example only one byte is being sent to the VDP, and that is the MSB in R0. Also, writing >2000 with a comment like "send the space character (32, >20) to the VDP" is easier to see in the code than using the number 8192:

 

LI R0,8192

 

vs.

 

LI R0,>2000

 

When you see the following: MOVB R0,>@8C00 then as an assembly programmer you would realize only >20 is being used, and this is more efficient than:

 

LI R0,32

SWPB R0

MOVB R0,@>8C00

 

Note that the address >8C00 is the "write port" of the VDP. How would you know that? By either looking at the 99/4A schematics or reading some technical documentation on the 99/4A, like the E/A manual. In other computers, like the ColecoVision for example, the VDP is at some other memory-mapped address (or I/O port), so for any particular computer it will be different depending on how it was designed. In assembly you will typically use an EQUate (which is just a search-and-replace at compile time) to help you, as a human, not to have to remember the memory addresses. You will always see something like this in code I write:

 

* VDP Memory Map

*

VDPRD EQU >8800 * VDP read data

VDPSTA EQU >8802 * VDP status

VDPWD EQU >8C00 * VDP write data

VDPWA EQU >8C02 * VDP set read/write address

 

Then I can use those words instead of the numbers, and let the assembler replace the words with the numbers during assembly:

 

MOVB R0,@VDPWD

 

after the assembler replaces the equates, becomes

 

MOVB 0,@>8C00

 

Note that the registers R0,R1,R2, etc. are just EQUates that are set by the E/A assembler -R option (or a radio-button option in the asm994a assembler). If you don't specify that option (or radio button), you would have to use "0" instead of "R0", etc. The assembler can tell the difference between, for example register 1 and the number 1, based on its placement in the assembly instruction. Now, how freaking confusing is that!?!? I have seen assembly examples that use the register numbers directly, and it was very confusing:

 

MOV 0,1

MOVB *0,*2+

MOVB @0[1+2],3 * "0" is a memory address, "1" is a register, "2" is a decimal value, "3" is a register.

 

Ugh. Without the -R option (or radio button in asm994a), you could do this yourself in your code, like this:

 

R0 EQU 0

R1 EQU 1

R2 EQU 2

.

.

.

R15 EQU 15

 

But, just let the assembler do it and always use -R or the radio button option.

 

Also, comments are critical in assembly. Even more so than in other languages. People tend to comment almost every line in assembly, which can be useful, but just as equally important is having blocks of comments that explains what the following code does in general as a whole.

 

***Again, forgive me for my lack of understanding... I'm just tired of not being in the ranks of "assembly programmer". =)

 

No apologies necessary.

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

VDPWD  EQU  >8C00                        * VDP RAM WRITE DATA

 

This is part of the redefinition of the conventions I have used a few times. I wonder where I got these routines?? =)

 

I DO understand this part of the puzzle, surprisingly enough, and in reading through many of the assembly comments on threads in this forum, I am coming to a better understanding of what is static and what is, as you put it, abstraction...

 

 

Data is data. You have bytes to work with. How you treat those bytes is up to you.

 

Yes... And aside from the advancements I made in the "Baby Steps" thread (c. 2010!!!) I have a limited understanding of what the hell to do with all these bytes! =) Placing values in memory locations and accessing them via built-in routines OR home-rolled routines makes a good bit of sense to me... The map scroller I did (which quickly was replaced by a better one ;)) was a true "AHA!!!" moment. =) I think the most difficult aspects of the language come in knowing specifically WHERE to write data to and HOW to use it once it's there. The concept is clear and I BELIEVE I have a good understanding of the methods... It's the direct application for specific tasks which seem to get me from time to time.

 

 

I'm a very visual-oriented person... I think in terms of shapes and dimensions... When I think about the TI-99 memory, I don't have a clear VISUAL of what it looks like... =) Is it a series of tens of thousands of bytes in one long line? Is it a 2 dimensional "square"? What does it "look" like. (This may not make any sense to anyone, but it's how I think). I "get" screen displays... 768 bytes which are displayed in 24 rows of 32 columns... Writing to the screen is "natural" to me (I guess) because I can "visualize" it as I'm writing the source code. When it comes to storing data in other places in memory, I can't "visualize" the data structures, so it's not as natural.

Link to comment
Share on other sites

Having a mental visualization of your data is very important. How does memory look you ask? Well, it depends. :-) It can be 2D if you want, for example the way you think of the screen. I hate to burst your bubble though, but the screen is just linear memory. All memory is linear addressed bytes. Learning to think about your data and how you have it arranged in memory is the key to being a good assembly programmer.

 

Take the screen example. Memory (VRAM) 0 to 31 (offset from the name-table base address) is used by the VDP to draw the top row of tiles. But where is the second row? The VDP does not have a memory address [1,1] or [10,12] or anything like that. Storing 2D data is analogous to arrays, and you have to decide if you are going to use row-major or column-major organization (Google that if you want to know). *Most* high-level languages, as well as the VDP hardware in this case, use row-major organization. So in memory, the 2D screen would be (assuming we start at VRAM address >0000):

 

Coordinates are x,y with x being horizontal.

 

>0000 [0,0] first tile, row 0

>0001 [1,0] second tile, row 0

>0002 [2,0] third tile, row 0

. . .

>001F [31,0] end of the first row (0)

>0020 [0,1] start of the second row (1)

>0021 [1,1]

>0022 [2,1]

. . .

 

So, every 32-bytes linear bytes of memory makes up one "row" on the screen, with the x coordinate representing a specific byte in that row. To get to a particular memory address of a row, you have to multiply the y coordinate by the number of bytes in a row. So row y=1 would be 1*32 or 32, which we can see above is the start of row 1. You also have to learn that things start with 0 in assembly. :-) To get the memory address give x,y values, you would use:

 

addr = y*32+x

 

Of course 32 is a known value because we know the screen is 32 tiles wide. Other data 2D data structures would vary. Also note that this is for "fixed size" data structures. When you start to get in to structures that change size at run-time, then you get in to an entirely different problem. Try to use fixed-size data models for now!

 

For other data structures you will have different organization depending on what you are doing, what data you have, and how you need to manipulate that data. I suppose it can be overwhelming, but I never had that impression.

 

You need to know how much memory you have, and know what you need to do, and let that be your guide. Your code itself will take up some memory, your variables, data, etc. It all goes into memory and memory is just big list of addresses (think of houses with addresses on their mailboxes if that analogy helps. Mailboxes have addresses and they store a value in the box). How you store your data and access it in memory is up to you.

 

This is where thinking like a computer comes in to play. You can think abstract, but at some point you have to take the attributes and decide how to store them. For example, if you are making a shooting game, your main "player" might have these attributes:

 

x,y location (2 bytes)

shield strength (1 byte)

damage multiplier (1 byte)

lives (1 byte)

bullets in motion (1 byte)

current animation frame (1 byte)

max animation frame (1 byte)

 

Based on this you need 8 bytes of memory to keep tabs on what is going on with the player. In a higher level language you might use a structure to bundle this all up instead of using discrete variables. Unfortunately the current 99/4A assemblers don't support using structures for variable naming, so we have to do it ourselves:

 

P1X BYTE 0

P1Y BYTE 0

P1SHLD BYTE 0

P1DMG BYTE 0

P1LIVE BYTE 0

P1BLTS BYTE 0

P1FRAM BYTE 0

P1FMAX BYTE 0

 

Since these are all bytes and some values will be at odd memory addresses, when working with them you have to keep that in mind. For bullets you might want to have more than 1 in motion at a time, so an "array" of bullets might be something you use:

 

BLTS DATA 0

DATA 0

DATA 0

DATA 0

 

That reserves 8 bytes of data that could store the x,y location (which takes two bytes) for up to four bullets. "DATA" reserves words, "BYTE" reserves bytes, but again, *how* you work with those values is up to you. In this case, using indexed memory access would be one way to manage the bullets:

 

CLR R1

LI R2,4

 

LOOP

MOVB @BLTS[R1],R3 * Moves the x location to the MSB of R3

MOVB @BLTS+1[R1],R4 * Moves the y location to the MSB of R4

.

. Update the bullet and store back to array

.

MOVB R3,@BLTS[R1]

MOVB R4,@BLTS+1[R1]

 

INC R1 * Next bullet x,y pair

DEC R2

JNE LOOP

 

It goes on and on, and just depends on what you are doing. It takes time, and the more you work with it the better you become. Seeing how other people solve the same problem is probably the best way to learn, IMO.

 

Link to comment
Share on other sites

I hate to burst your bubble though, but the screen is just linear memory

 

Oh, yes I understand that--- it's the manipulating of that memory that I "get"... If I'm manipulating the "screen," I have some set parameters to work with. I know that to draw a line of three vertical "*" characters in column 0, I'll write a byte to 0, then add 32 for the next byte write. **Even if the "structure" is implied or symbolic, it makes more sense to me... It's not a HCHAR or even a DISPLAY AT... but having that set of parameters makes the math easy and fun. =)

 

 

It takes time, and the more you work with it the better you become.

 

 

As with most things in life. =) Right now I'm just refreshing myself for the impending project and trying to understand a little better how to "think 9900".

Edited by Opry99er
Link to comment
Share on other sites

Sorry for breaking into this thread, but I have a question about bit shifting.

 

When you want to shift a fixed number of positions you can do it like this:

 

   SRA R1,3

 

When you want to shift a variable number of positions you load the number into R0 like this:

 

   MOV @MYVAR,R0
   SRA R1,0

 

But when R0 is zero the shift is not zero positions but 16, according to the E/A manual. This means you have to add a check:

 

   MOV @MYVAR,R0
   JEQ SKIP
   SRA R1,0
SKIP

 

This makes the code longer and more difficult to understand. Is there a trick to avoid this? I don't understand why the instruction was designed like this. When do you need to shift 16 positions? Why not just shift zero positions?

Link to comment
Share on other sites

But when R0 is zero the shift is not zero positions but 16, according to the E/A manual. This means you have to add a check:

 

MOV @MYVAR,R0
JEQ SKIP
SRA R1,0
SKIP

 

This makes the code longer and more difficult to understand. Is there a trick to avoid this? I don't understand why the instruction was designed like this. When do you need to shift 16 positions? Why not just shift zero positions?

 

Use SRC (Shift Right Circular) instead to avoid the check. This does have the effect of rotating the bits on the right over to the left, though. If that's not desirable you'll have to use the JEQ to bypass. (At least it's only two bytes to do it...)

 

Adamantyr

Link to comment
Share on other sites

...

But when R0 is zero the shift is not zero positions but 16, according to the E/A manual. This means you have to add a check:

...

 

Actually, it's not when R0 is zero but when the least significant nybble (4 bits) is zero, so your test isn't safe.

 

...lee

Link to comment
Share on other sites

Actually, it's not when R0 is zero but when the least significant nybble (4 bits) is zero, so your test isn't safe.

 

That can be fixed with the following, assuming you don't have rigid control of the input value:

 

  MOV @MYVAR,R0
  ANDI R0,>000F
  JEQ SKIP
  SRA R1,0
SKIP

 

Adamantyr

Edited by adamantyr
Link to comment
Share on other sites

Sorry for breaking into this thread, but I have a question about bit shifting.

 

When you want to shift a fixed number of positions you can do it like this:

 

SRA R1,3

 

When you want to shift a variable number of positions you load the number into R0 like this:

 

MOV @MYVAR,R0
SRA R1,0

 

But when R0 is zero the shift is not zero positions but 16, according to the E/A manual. This means you have to add a check:

 

MOV @MYVAR,R0
JEQ SKIP
SRA R1,0
SKIP

 

This makes the code longer and more difficult to understand. Is there a trick to avoid this? I don't understand why the instruction was designed like this. When do you need to shift 16 positions? Why not just shift zero positions?

 

The shift is not circular nor logical. SRA fills the vacated bits with the 'sign' bit (IIRC) thus the use of "0" to represent shifting all 16 bits.

 

Shifting 0 bits would not provide an equivalent result.

Link to comment
Share on other sites

The shift is not circular nor logical. SRA fills the vacated bits with the 'sign' bit (IIRC) thus the use of "0" to represent shifting all 16 bits.

 

Shifting 0 bits would not provide an equivalent result.

 

True—except that all four of the shift operations look to the least significant nybble of R0 when the shift count is 0. All four affect the L (logical greater than), A (arithmetic greater than), EQ (equal) and C (carry) status bits, with SLA the only one that affects the OV (overflow) status bit.

 

...lee

Link to comment
Share on other sites

True—except that all four of the shift operations look to the least significant nybble of R0 when the shift count is 0. All four affect the L (logical greater than), A (arithmetic greater than), EQ (equal) and C (carry) status bits, with SLA the only one that affects the OV (overflow) status bit.

 

...lee

Agreed, I intended to point out the instruction's design and the reason it shifts 16 bits when the shift count is 0. In order to shift and fill 16 vacated bits, the instruction requires a value to represent 16 bits. The status bits can then be used to pick apart the value being shifted. There is no value in shifting zero bits - a NOP would suffice. ;)

Link to comment
Share on other sites

I don't understand why the instruction was designed like this. When do you need to shift 16 positions? Why not just shift zero positions?

 

In addition to what InsaneMultitasker said about a shift of 0-bits being of no value, I would speculate that the hardware implementation is done with a 4-bit counter and decremented until the value is zero. The way a FSM typically works in hardware would mean that the decrement would happen at least once, and an initial count of '0000' would roll to '1111', and continue until it was '0000' again which would terminate the shift operation. Thus 0 becomes 16 shifts. It was probably easier to do in hardware, and thus became a "feature".

 

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...