8048 Introduction

        .cr     8021        To load the 8021 cross overlay
        .cr     8022        To load the 8022 cross overlay
        .cr     8041        To load the 8041 cross overlay
        .cr     8041a       To load the 8041A cross overlay
        .cr     8048        To load the 8048 cross overlay
        .cr     80c48       To load the 80C48 cross overlay
        .cr     mab8400     To load the MAB8400 cross overlay

Talking about vintage, the 8048 family can be considered the dinosaurs under the micro controllers. They are not extinct yet, some devices are still running on them, but for how long from now? In fact at the time I wrote this text for the SB-Assembler version 2 the brains of my living room TV set was still a 8048 based micro controller!

The 8048 was the predecessor of the still very popular 8051 family of micro controllers. Many of the 8051's features are inherited from the 8048 family, like the programming model and the timer functions.
Compared to present standards the performance of the 8048 family is very modest. It can address up to 4k of program ROM and has a maximum of 128 bytes of RAM, although some models were equipped with an incredible 256 bytes of RAM.
Programming a 8048 micro controller can be compared to a survival camping trip. You'll soon run into the limitations that are unknown to modern micro controllers. One of the main limitations is the memory page boundary. A memory page is a piece of memory of 256 bytes, where the high order address bits (bits 8 and up) remain the same. Branch instructions can't reach beyond those boundaries and lookup tables can't be read from other memory pages than the current one. This means that you will have to move subroutines up and about every time your code changes in length to avoid page boundary crossings.
Luckily the SB-Assembler warns us when a page boundary problem occurs, so we can fix these problems when they arise.

Many different derivatives exist, marketed by different manufacturers. The individual derivatives mostly differ in I/O capabilities. Due to the architecture these I/O features all require new instructions or different operands. Therefore every derivative needs its own cross assembler.
Fortunately all derivatives have the same programming model and many instructions in common, that's why I describe all versions in this single page. Refer to the opcode test files to see what instructions are unique to the different cross overlays. And of course you'll still need the original document of the selected micro controller.

Programming Model

The programming model in the picture below shows the most important registers of the 8048 processor. I only include a little summary about the features of the 8048's programming model here. It is not my intention to make the original documentation obsolete, so please refer to the original documentation for further details.

8048 programming model

The Accumulator

The Accumulator is the most important register for 8 bit arithmetic operations. Its standard name is A, which is a reserved word.

The Program Status Word

The PSW contains 8 system flags:

Bit 7CYCarry flag
Bit 6ACYAuxiliary Carry
Bit 5F0General purpose Flag
Bit 4RBRegister bank select Flag
Bit 31Always 1
Bit 2SP2Stack pointer
Bit 1SP1Stack pointer
Bit 0SP0Stack pointer

Bits 2..0 are the stack pointer of the 8048, limiting the stack to a maximum depth of 8 levels. This means that up to 8 subroutine calls (or 7 and one interrupt) can be nested before the stack overflows. No particular action is taken when the stack does overflow, you simply loose your oldest return address, which will very likely crash your program. The stack is part of internal RAM and occupies addresses $08 to $17, which is in between the two register banks mapping. A call instruction (or an interrupt) will cause the low byte of the program counter to be stored first in the lowest address, followed by a concatenation of the 4 upper bits of the PSW and bits b11..b8 of the program counter which is saved at the next address. After the stack pointer is incremented by one. Although the stack pointer is incremented by 1 the actual increment is 2 because every call involves the pushing of 2 bytes on the stack.
The stack grows up in memory as more subroutines are nested. A stack pointer with a value of 0 points to RAM memory $08 and $09. A value of 7 points to RAM memory $16 and $17.
The upper 4 bits of the PSW are always saved on the stack, whether it is caused by a CALL instruction or by an interrupt. It depends on the return instruction that's used at the end of the routine whether the PSW is restored or not (RETR or RET).

The Register R0 ... R7

8 registers assist the 8048 as intermediate registers during calculations. The registers are part of internal RAM memory.
Registers can be addressed with only 3 bits, instead of the 8 bits needed to address one of the internal RAM addresses. RAM addresses can't be addressed directly by the 8048, so if you want to use other memory locations than the registers you'll have to use @R0 or @R1 to point indirectly to the desired RAM locations. There are 2 banks of 8 registers to accomplish fast context switching during interrupts.
Register bank 0 is mapped in RAM addresses $00 to $07, while register bank 1 is mapped to addresses $18 to $1F.

Registers R0 and R1 can also be used as pointers during indirect addressing.

The Program Counter

The program counter PC is only 11 bits long, enabling it to address 2k of memory. A 12th bit is added to the program counter and effectively is a bank switch bit. If the 11 bit program counter overflows it will wrap around and doesn't increment b11 automatically. The only way to change b11 is by executing the SEL MB0 or SEL MB1 instruction, followed by a JMP or CALL instruction. The actual switching to the new bank is delayed until the program actually JMPs or CALLs to the new location. Interrupts can only use bank 0 of the program memory.

The SB-Assembler won't allow you to use memory locations above 2k. If you want to use memory in bank 1 you'll have to restart your program counter at $000 again. The easiest way to do that is to use the directives .NO $800 and .PH $000 together at the end of bank 0. That way the object file will continue to grow with unique addresses, while the assembler uses the right addressing range for the 8048.
In version 3 there is an other way to solve the bank switching challange. You can simply restart the program counter with an .OR $000 or .NO $000 followed by a .TA $800. This will reset the program counter to 0 again, while the target address is positioned beyond bank 0.


SB-Assembler Version 3 can show you the cycle times of each instruction when the TON list flag is switched on. The numbers presented are the number of cycles each instruction takes.

Reserved Words

The SB-Assembler 8048 cross overlay has a few reserved words. Reserved words are all fixed register names. You better avoid these reserved words when you assign your own labels. E.g. don't name your labels R0, or A.
If you do use the reserved words as label names you may expect unpredictable behaviour of the assembler sooner or later. Please note that the assembler will not warn you if you try to assign a label with a reserved name!
Reserved names can not be used in expressions like label names can. An Undefined label error will be reported if you do try to use a reserved word in an expression because it is treated as a normal label in this case.

Here's the list of all reserved words:

A, R0, R1, R2, R3, R4, R5, R6, R7

If you take a look at the 8048 instruction set you may expect more reserved words to exist, like MB0 or TCNTI. But because the instructions that use these operands never expect expressions as their operands no unpredictable behaviour of the assembler is to be expected if you do name your labels equally.

Special Features

Memory Page boundary detection

The program memory space of the 8048 is (invisibly) divided in pages of 256 bytes each. Branch instructions cannot cross these boundaries and destinations are restricted to the same page as the branch instruction itself.
Naturally the SB-Assembler will warn you when a branch instruction tries to jump to a location that's out of reach.
But the behaviour of the 8048 page boundary limitations may seem a bit odd to programmers who are familiar with other processors. A program that assembled correctly last time could produce assembly errors in many locations after adding or removing some instructions somewhere in the program. The reason for this is that all instructions are shifted in memory when you add instructions somewhere in between. This shifting may result in a branch instruction crossing a page boundary, resulting in Out of range error that were not there the last time.
You better get used to this because programming the 8048 involves moving your subroutines about constantly to avoid crossing page boundaries by branch instructions. Small programs can be written with an occasional .OR instruction to force your program to continue at the next new page to avoid all this. But this won't be possible anymore when the program memory is almost full.

There is also another problem caused by this page boundary system. ROM lookup tables are also restricted to pages. The instructions reading the table must reside in the same page as the table and the table itself may not cross a page boundary. The only exception to this rule is when you place your table on memory page 3, because a special instruction (MOVP3 A,@A) exists which can read a table from page 3 from anywhere in program memory.
Because the assembler has no way of knowing where your table starts or ends I've added two new directives to the 8048 crosses, .OT and .CT. These directives signal the beginning and the end of a table. At the end of a table a test is made to see if a page boundary has crossed or not. A Table crossed page boundary error will be reported when the table did cross a page boundary.

Bank switching

Standard features of the SB-Assembler aid in the concept of bank switching in 8048 programming. Remember that the 8048 can only address 2k of ROM memory. It has a bank switch bit (b11 of PC) to expand its ROM capacity to 4k.
The SB-Assembler does not allow your 8048 program to grow beyond 2k, or you risk getting an Illegal address error. This doesn't mean that you can't use bank 1 of the 8048 though!

The trick is to start page 1 with address $000 again, but that could cause some unexpected problems. Suppose you want the whole object code written to the same formatted target file like e.g. a file with the Intel hex format. Now your programming device probably gets quite confused when it finds 2 memory blocks both starting from address $000 and both having different data.
That's why we have to use a different approach, which is standard available in the SB-Assembler. You need to carry out the following 2 steps at the end of bank 0:

  • In between the code for bank 0 and bank 1 insert a line with the directive .OR $800, or .NO $800. As long as you are using a formatted target file you're free to choose either one of them. But if you send your object code to a plain BIN or HEX file you'll have to use the .NO directive.
  • The next line should hold the .PH $000 directive.

Let me explain what happens. The .OR or the .NO directive will set the program counter to $800, which is actually one more than the 8048 can handle. But the SB-Assembler will not protest as long as you don't write anything to that address. Now the .PH directive lets the SB-Assembler think it is assembling a program from address $000. In reality the generated object code is written to your target file from address $800 and up, while labels, jumps and calls are properly calculated having addresses in the range from $000 to $7FF.
Please take a look at the description of the .PH directive to find a detailed example program.

As of version 3 of the SB-Assembler there's another way of handling the bank switching. This way is a bit more straight forward, and therefore possibly easier to comprehend.

  • At the boundary between bank 0 and bank 1 add the directive .OR $000 or .NO $800.
  • Follow that directive with a .TA $800 directive.

Please note that this approach is just the opposite of the previous one. Instead of setting the new program counter to $800, it is now set to $000. The correct target address in the target file is then enforced by the .TA directive.
But keep in mind that either method will have the same result, despite the different approaches.

Error Messages

One new error message is added to the SB-Assembler while the 8048 cross overlay is loaded:

Table crossed page boundary error

This error is generated by the .CT directive and sometimes by the .OT.
The error is only generated if the .CT directive is on a different memory page than the previous .OT directive. If the error occurs it will be reported during pass 2 of the assembly process only. This is to simplify the search for a suitable new place for your table. You would have no way of telling at what address the page was crossed if the error was reported during pass 1 instead.

Overlay Initialization

Three things are set while initializing the 8048 related overlays every time one is loaded by the .CR directive.

  • The memory location holding the page address is cleared to indicate no .OT directive is active at the time.
  • Little endian mode is selected for the .DA and .DL directives. I had to pick one mode at random because the 8048 has no 16 bit operands of its own. There is no particular reason why I chose the little endian mode, most likely because all other Intel processors used it too..
  • The maximum program counter value is set to $07FF (2k).

.CT    Close Table




This directive signals the end of your table in memory. It will present a Table crossed page boundary error if this directive is located on a different page than was stored by the previous .OT directive.


Tables are not allowed to cross page boundaries. That's why the .OT and .CT directives are used to signal the beginning and the end of a table, verifying that a page crossing hasn't occurred in between.

The .OT directive should be placed at the beginning of the table. More precisely just before the instruction that reads the table, because that instruction should be on the same page too. The only exception to this rule would be the MOVP3 A,@A instruction that may be anywhere in the same memory bank as the table which should be stored in page 3.

The .OT directive will memorize the memory page of the beginning of the table. At the end of the table the closing .CT directive should still be in the same memory page. There's only one exception to this rule and that is when .CT is at the first location of the next page, because then the table ended just in time on the previous page.
No error message is reported when both pages are equal. But a Table crossed page boundary error is reported when they do differ during pass 2 of the assembly process. The error message is only reported during pass 2 to enable you to find out at what location the table crossed the page boundary in order to fix the problem by either moving the table or moving some code in front of the table.


The example below shows a typical lookup table routine. It converts a decimal digit to a seven segment pattern. I think it clearly demonstrates the use of the .OT and .CT directives.

DEC2SEGM    MOV    A,R0            Get decimal value
            ADD    A,#SEGMENTS     Add offset to table to it
            .OT                    Mark the beginning
            MOVP   A,@A            Get 7 segment pattern
SEGMENTS    .DA    #%0011.1111     0
            .DA    #%0000.0110     1
            .DA    #%0101.1011     2
            .DA    #%0100.1111     3
            .DA    #%0110.0110     4
            .DA    #%0110.1101     5
            .DA    #%0111.1101     6
            .DA    #%0000.0111     7
            .DA    #%0111.1111     8
            .DA    #%0110.1111     9
            .CT                    End of table, checking page

Please note that the .OT directive is placed before the MOVP instruction because that instruction must be on the same page as the rest of the table.

The .CT directive is ignored if no matching .OT directive was used.

.OT    Open Table




This directive signals the beginning of your table in memory.


.OT saves the current memory page address internally. Later on it should match the memory page address of the next .CT directive.

The .OT directive will automatically call the .CT function if you previously started a table without closing it properly with a .CT directive. After closing the previous table this way the new table will start at the current location as if you had inserted a .CT directive yourself. This way you can concatenate several tables after each other.
Remember though that it becomes more difficult to find a piece of memory large enough to hold all your concatenated tables if you rely on this automatic closing of previously opened tables.

Please note that the .OT directive would never generate the Table crossed page boundary error" itself if it wasn't for the automatic calling of the .CT routine when a previous table wasn't closed.

Differences Between Other Assemblers

There are no specific differences between the SB-Assembler and other assemblers for the 8048 processor family other than the obvious differences which all SB-Assembler crosses have in common. These differences require you to adapt existing source files before they can be assembled by the SB-Assembler. This is not too difficult though and is the (small) price you have to pay for having a very universal cross assembler.

  • Other assemblers probably don't support the .OT and .CT directives.
  • The obvious differences in notation of directives and radixes common to all SB-Assembler crosses.
  • Don't forget that the SB-Assembler does not allow spaces in or between operands. Only Version 3 will allow one space after each comma separating operands in the operand field.

Warning: In Version 2 of the SB-Assembler there is a small bug in the 8041A Cross Overlay. The opcodes of the instructions EN FLAG and EN DMA are mixed up. Because this has just very little impact and because Version 2 is discontinued I chose not to fix this little bug.
The bug is corrected in Version 3.