Joined: Thu May 28, 2009 9:46 pm Posts: 8392 Location: Midwestern USA
|
Johnny Starr wrote: I would like to understand more about ASM tables though. What do you mean by "ASM tables?" Is ASM an acronym for something? In computing, terminology is critical when trying to learn something new. You shouldn't use acronyms or abbreviations unless you are certain that both the reader and you know what they mean.
Quote: I sort of get where White Flame was going in his example, but I don't really understand how or where the "tables" are stored... Okay, Johnny. Here goes...long-winded post to follow.
At the risk of stating the obvious, data of any kind are stored in memory (RAM, but could be mass storage). A table is a specific type of data that has some kind of programmer-determined structure, which is no different than defining a table in C with the struct keyword. In the abstract, a table is the same whether written on paper or created in RAM: it's organized data with a meaning determined by whomever created it. Your table could be a list of names (e.g., your friends and their phone numbers), a list of numeric constants used in a mathematical function, a list of addresses pointing to objects (e.g., sprite definitions) located somewhere else in RAM, etc. Before you get involved with the programming mechanics of table creation and management, you have to define the size of the table and the nature of the data in it, after which you can concentrate on the required code. This is no different than accomplishing the same task in C: the concept is the same. Only the code syntax differs.
As for where tables are stored, they are stored at a location of your choosing, which you determine either by coding the location into your program, or by allocating memory on the fly at run-time. The latter method would be analogous to calling malloc() in a C program that runs on a system that supports such a function. If you know in advance exactly how much storage will be required for your table you can define the table's extent in code, as well as its starting location. It sounds from what you have already said about what you are doing that static storage will suffice, so it becomes a matter of determining where to put the table and how much room it needs.
To illustrate how to get from concept to code, let's go back to an example I presented a few posts ago: a SCSI device table. First some background information.
SCSI (Small Computer Standard Interface) is a method of attaching various kinds of peripherals to a computer. Depending on the system, either seven or 15 peripherals can be attached to the SCSI bus. My POC unit implements SCSI-2 with an eight bit bus, so up to seven peripherals (aka devices) can be attached. SCSI devices vary widely in characteristics and capabilities, and aren't limited to just mass storage (I have a document scanner in my office that is SCSI). Attached to POC's SCSI port is a 36GB hard drive, a DDS2 cartridge tape drive and a CD-ROM drive. Each device is assigned a unqiue device ID, the hard drive being $00, the CD-ROM being $05 and the tape drive being $06. POC itself is assigned $07, which is something that is set during POST—device numbers on the other hardware are set via jumpers,
At boot time, POC doesn't actually know if anything is connected to the SCSI port. So following POST the BIOS "enumerates" the bus by attempting to contact each of the SCSI IDs that would be valid on the bus, namely $00-$06 (POC doesn't try to talk to itself, unlike its designer). If a device responds to a given ID, it is commanded to identify itself by type, storage capacity (if capable of mass storage), how it handles data (in blocks or in streams), is it ready to accept and/or send data, etc. The information thus gathered is stored in a SCSI device table in RAM, eight bytes per device, and logically organized in a row/column format, with the SCSI ID acting as the row index. In POC, the table starts at $000110 and as there are eight possible devices (including POC), extends to $00014F, 64 bytes in total. The memory dump of the table would look like the following inside POC's machine language monitor:
Code: .m 110 15F 000110 C3 80 CC DC 45 04 00 02 00 00 00 00 00 00 00 00 000120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000130 00 00 00 00 00 00 00 00 C2 05 F2 09 00 00 00 08 000140 C2 01 00 00 00 00 00 00 FF FF FF FF FF FF FF FF The M/L monitor dumps 16 bytes per line, so it isn't immediately obvious from the above that what is being viewed is a data table. However, reformatting the display makes it a little clearer:
Code: 000110 C3 80 CC DC 45 04 00 02 000118 00 00 00 00 00 00 00 00 000120 00 00 00 00 00 00 00 00 000128 00 00 00 00 00 00 00 00 000130 00 00 00 00 00 00 00 00 000138 C2 05 F2 09 00 00 00 08 000140 C2 01 00 00 00 00 00 00 000148 FF FF FF FF FF FF FF FF Now, if I annotate the above it becomes even clearer:
Code: Mem Addr Assignment —————————————————————————————————————————————————————————————————————— 000110 C3 80 CC DC 45 04 00 02 <—— SCSI device $00 (hard drive) 000118 00 00 00 00 00 00 00 00 <—— SCSI device $01 (not present) 000120 00 00 00 00 00 00 00 00 <—— SCSI device $02 (not present) 000128 00 00 00 00 00 00 00 00 <—— SCSI device $03 (not present) 000130 00 00 00 00 00 00 00 00 <—— SCSI device $04 (not present) 000138 C2 05 F2 09 00 00 00 08 <—— SCSI device $05 (CD-ROM) 000140 C2 01 00 00 00 00 00 00 <—— SCSI device $06 (tape drive) 000148 FF FF FF FF FF FF FF FF <—— SCSI device $07 (POC's host adapter) ——————————————————————————————————————————————————————————————————————— | | | | | | | | | | | | | | +——+———> Block size (2 bytes) | | +——+——+——+—————————> Total blocks (4 bytes) | +—————————————————————> Device type bit field (1 byte) +————————————————————————> Device flag bit field (1 byte) It's clear (or should be from the above) that the SCSI device ID can be used as an index into the table, since the ID is zero-based. More on this below.
Now, your next question should be, "What determined the above layout?" The answer is in the assembly language source statements that define field sizes and the relationships of the fields to each other. Obviously, you have to conceptualize the table layout, most likely by doing some scribbling on paper to determine what you need. Some careful thinking can save you a lot of grief in getting it to work. The above annotated memory dump more-or-less is such scribbling. I knew what information I would need to store in the table and knew that I would be accessing the table by SCSI device ID, so it became a matter of mostly determining the order of the fields. As bit 7 in the device flag is used to indicate that a device was found at the corresponding SCSI ID, I place that field first in the row to make testing it convenient. It seemed logical to following the flag with the type, so I made that field the second in the row. The remaining fields, total blocks and block size, are unsigned integers and thus use multiple contiguous bytes, in little-endian order. The order of the fields themselves didn't matter, so I placed the total blocks field first.
The process of determining just how much data each field has to hold is simplified somewhat by the fact that the block size and total blocks fields have to match the size of the data returned from each device when interrogated. POC supports both SCSI-1 and SCSI-2 devices, the latter implementing both larger block size and total blocks fields. So I had to size the fields to accommodate the SCSI-2 standard, in which the block size in bytes is defined by a 16 bit unsigned integer and the total blocks field is defined by a 32 bit unsigned integer (SCSI-1 devices report total blocks in 21 bits). Hence two bytes have to be allocated to store the block size and four to store the total blocks field.
The device flag and type fields are one byte each, something that I defined, as by using each as a bit field, I could fit all desired information into two bytes. When the individual field sizes are added up, the result is eight bytes. Eight bytes times eight devices means the table size is 64 bytes.
Putting this all together resulted in the following assembly language statements:
Code: ;LOGGED SCSI DEVICES TABLE ; ; —————————————————————————————————————— ; row offset ——> $00 $01 $02 $03 $04 $05 $06 $07 ; —————————————————————————————————————— ; | | ^——————————————^ ^————^ ; | | capacity block ; | | size ; | +———> type ; +————————> flag ; ; ; field sizes (description order)... ; s_sdbsiz =s_word ;block size s_sdflag =s_byte ;flag s_sdcap =s_dword ;capacity in blocks s_sdtype =s_byte ;type ; ; ; row & table sizes... ; sdslotex =ex2_x3 ;row size computation factor... ; ; —————————————————————————————————————————————————————————————————————— ; SDSLOTEX is used to atomically define the size of the device table row ; so bit shifts can be used to compute a row address for a SCSI ID. The ; above definition should be changed with caution. See additional notes ; in the SSLOTADR function. ; —————————————————————————————————————————————————————————————————————— ; s_sdslot =1 << sdslotex ;row s_sd_tab =s_sdslot*n_scsiid ;table ; ; ; field offsets in row... ; o_sdflag =0 ;device flag... ; ; xx000xxx ; || ||| ; || +++———> ANSI version ; |+—————————> 1: supports sync transfer ; +——————————> 1: device enumerated ; o_sdtype =o_sdflag+s_sdflag ;device type... ; ; x00xxxxx ; | ||||| ; | +++++———> ANSI device type ; +——————————> 1: device recognizes 32 bit LBAs ; o_sdcap =o_sdtype+s_sdtype ;capacity in blocks o_sdbsiz =o_sdcap+s_sdcap ;block size "Row offset" is another way of saying "column."
In the above code, the s_byte, s_word and s_dword symbols are atomically defined to be 1, 2 and 4, respectively. The symbol n_scsiid is atomically defined as 8, as that is an immutable SCSI hardware characteristic. "Magic numbers" of this type should be defined in an INCLUDE file to maintain consistency throughout the program. It is bad programming practice to embed constants in program code, as tracking down and changing them, if necessary, becomes an onerous task. Also, it's easier for someone reading the source code to understand the use of constants when they are named. I'm sure you'd agree that LDA #n_scsiid means more to the reader than LDA #8 (from where did that 8 come???).
Note the order in which the definitions are made. First the field sizes are defined. For example, the s_sdcap symbol sets the size of the total blocks field. With all field sizes defined, the assembler can then calculate the row size (eight bytes) and the table size (64 bytes). The expression s_sdslot =1 << sdslotex means s_sdslot is equal to 1 left-shifted sdslotex bits, where sdslotex happens to be 8. Again, this sort of thing is defined in a separate INCLUDE file and never embedded into the code. This somewhat odd way of defining the row size is to assure that it is always a multiple of 2, should I see fit to change the row size. If the row size is a multiple of 2 then the row offset in the table can be computed with some left shifts, rather than by multiplying the row size by the SCSI ID.
The final section of the above code sets the relative offsets in each row to each field, that is, the "column" numbers. Note that the only field that has an actual column number set is the device flag bit field, which is the first field in a row and hence in the zeroth column. All the others are computed at assembly time from the previously defined field sizes. This use of "assembler automation" helps to keep errors from creeping into the code and sabotaging your work.
All of the above defines the table structure but doesn't actually reserve any storage for the table. You can do so by either setting an absolute address, which is, in a roundabout way, how the device table address is set in the POC's BIOS ROM source code, or by having the assembler do it for you. Let's assume the latter.
In MOS Technology convention, the statement *=*+N advances the program counter N bytes without generating any code. In other words, this syntax allocates uninitialized storage, what is often referred to as BSS ("block started by symbol"). In this usage, * represents the current value of the program counter, which is the assembler's notion of where it is in memory as assembly progresses. So, to allocate uninitialized storage for my SCSI device table I would code as follows (again, using MOS Technology convention):
Code: sd_tab *=*+s_sd_tab The above will assign the current value of the program counter to the symbol sd_tab and then advance the program counter by s_sd_tab, hence reserving 64 bytes for the table. Elsewhere in the program, I can refer to the device table with the sd_tab symbol, which will always point to the very first byte of the table. Note how the allocation of storage for the table built upon everything that went before it (field size definitions, etc.). The best part is if I restructure the table in some way the assembler will automatically fix up everything and keep it organized.
To access any given row in the table, knowing the SCSI device ID, the program would execute ID × s_sdslot. Recall that s_sdslot is defined so it is always a power of 2. Hence ID × s_sdslot may be computed by left-shifting instead of outright multiplication, leading to smaller and faster code. Remember the sdslotex symbol? It was used to define the row size as a power of 2, resulting in 8 bytes per row in this case. Here's how sdslotex would be used to compute the row address in the table. In the following code snippet, the SCSI ID is in the accumulator (.A):
Code: ldx #sdslotex ;row size factor (power of 2) ; .0000010 asl a ;SCSI ID × 2 dex bne .0000010 ;loop ; ldx #>sd_tab ;device table base MSB adc #<sd_tab ;device table base LSB bcc .0000020 ; inx ;account for carry ; .0000020 sta zpptr ;set up zero page pointer LSB stx zpptr+s_byte ;set up zero page pointer MSB ; ...program continues... Upon completion, the address of the device flag in the row corresponding to the SCSI ID will be in zpptr and zpptr+1 (s_byte is atomically defined to be 1). To access any given field in the row, you would use the field size and offset together to, say, copy the field to a work location in RAM. For example, to copy the block size field to somewhere in RAM, the following code would be executed:
Code: ldx #0 ;storage offset ldy #o_sdbsiz ;block size field offset in row ; .0000010 lda (zpptr),y ;get byte from field &... sta faca,x ;store in RAM inx ;storage_offset=storage_offset+1 iny ;column=column+1 cpx #s_sdbsiz ;get all bytes? bne .0000010 ;no, loop ; ...program continues... Note how the previously-defined field size and offset in row values are used.
Quote: Would I even need indirect addressing to accomplish this? Couldn't I just send the address of the sprites first location and index accordingly? See above examples. It would be kind of hard to avoid indirection in the case of the SCSI device table, at least if some generality is desired. That's not necessarily true in all cases. You'd have to evaluate your particular situation to determine if indirection is required.
If you use the 65C02 (recommended over the NMOS 6502) you have the availability of true indirection, e.g., LDA (<zp>) instead of only LDA (<zp>),Y. The 65C816 also offers stack indirect addressing:
Code: ; copying device table block size field via stack indirect addressing... ; SCSI ID is in 16 bit accumulator ; longa ;ensure 16 bit accumulator shortx ;8 bit index registers ; ; ————————————————————————————————————————————————————————————————————————— ; Above instructions are macros that issue the proper machine instructions, ; as well as tell the assembler how to assemble immediate mode values. ; ————————————————————————————————————————————————————————————————————————— ; ldx #sdslotex ;row size factor (power of 2) ; .0000010 asl a ;SCSI ID × 2 bcs error ;SCSI ID way out of range!!! ; dex bne .0000010 ;loop ; adc #sd_tab ;device table base address (16 bits) pha ;push row address to stack shorta ;8 bit accumulator (a macro) ldx #s_sdbsiz-1 ;storage offset ldy #o_sdbsiz+s_sdbsiz-1 ;column offset in row ; .0000020 lda (1,s),y ;get byte from field &... sta faca,x ;store in RAM workspace dey ;column=column-1 dex ;storage_offset=storage_offset-1 bpl .0000020 ;get some more ; pla ;clean up stack pla ; ...program continues... No zero page is used at all in the above code, which assumes the '816 is operating in native mode.
_________________ x86? We ain't got no x86. We don't NEED no stinking x86!
|
|