It is totaly possible to implement as many code memory you want in the 8051, using some available port pins, or implementing a 74HCT573 8 bits latch (for a max of 16MB) or a 74HCT75 4 bits latch (for a max of 1MB eprom).
Several devices in the market use that technique, including the famous VGA or SVGA pc card. It has only 64k or 96k of window memory into the PC addressing, but you can have as many memory you want, 1M, 2M even 4MB, since it is pageable into that 64 or 96k "window" from B800h to AFFFh.
It works like a book. You do not see all pages at the same time, but you can read all the book, page by page, and understand it all.
As a doctor in a hospital, with many rooms and patients, he can visit one by one, and take care of everyone without any problem, since he knows where he is. At any time the doctor can be called by an emergency, leaving that patient for a later visit, he just need to keep track of what and where he's doing.
The 8051 family can access only 64k of code memory and 64k or ram memory, since it uses only 16 addressing bits. But it is actually accessing 128k, 64k for code, and 64k for data. This is done by using different lines (PSEN or /RD or /WR) that mean to which chip the 16 bit address is for.
A bigger chip, as 8088 or 80486 can access more memory because it has the 17th, 18th and more addressing bits, so it can select other chips or other memory banks inside the same chip.
What we will do in here is just create those 17th, 18th and more addressing bits. This is quite easy, just connect some other port pins to those addressing bits of your bigger eprom and that's it. Whenever that particular port pin goes high or low, it will telling the eprom which internal bank is to be used, that's easy.
The problem is how to control it in an easy fashion.
Some solutions in the market, including a Philips forum file, refers to split the 64k in two 32k pages, so the lower 32k still to be always the same, the first 32k of the 512k eprom, as example. It means that whenever you access code memory from 00000h to 07FFFh, it will access the first 32k of that eprom. The other 15 pages of 32k, from eprom address 08001h to 7FFFFh, will be "windowed" in the top 32k, possible to be addressed by the 8051, from 08001h to 0FFFFh.
Suppose you use 3 port pins P1.0, P1.1 and P1.2 to generate the extra 17th, 18th and 19th addressing lines for the 512k eprom. Whenever you need to access the lower 32k block, those 3 lines need to be down, at low level. Whenever you need to access some other block, those three lines need to be logically set.
There is a problem, when you reset the 8051, all port pins go to high level, so it will select the higher 32k eprom block, from 78000h to 7FFFFh. It means that your bytes at 78000h will really "boot" your machine, not the code at 00000h. You can bypass that situation and install inverters like 74HCT04 between those port pins and the eprom addressing lines, so at the power on it will address the real first low 32k block.
This solution implies that the lower 32k will holds all the important tasks as interrupts and other main functions, including the page control to access the other pages and things.
The solution I am suggesting here uses a different approach. It uses pages of full 64k. It is easier to control, easier to keep track and easier to implement.
The same problem resides for the power on, when the port pins goes high after the reset. The solution for that is just use a simple instruction as MOV P1,#00h as the first bytes of every 64k page, so whenever the machine is initialized, independently of what is in those three addressing lines P1.0, P1.1 and P1.2, the first instruction just drop the selection for the first 64k page, the correct boot page.
The idea of use a full 64k page allows you to develop a lot of code in the first page, all main routines and interrupts, including lots of tables and stuff.
Now, as you repeated in every page the instruction MOV P1,#00h, you need also to repeat in every page a "traffic table". It will take control of where your software is and where to go.
Differently from what you can be thinking this is quite simple.
Each page will be created by your editor assembler as a independent code program. Some of those editor assembler doesn't allow more than 64k of code at all.
Suppose you have 16
long math routines at the second 64k page. The first starts at that
page address 0500h, the second at 0700h... in real at the eprom it would
be 10500h, and 10700h, now, you just create a jump table starting at 100h
of that page. At the 100h: JMP 0500h, and at 0103h: JMP
0700h, so, whenever you need to execute the routine at 500h, just jump
to 0100h and it goes there.
There is a problem with this technique, it nees to be at that page, selected by P1.0, P1.1, P1.2 port bits.
To make sure that it would be at that page whenever you jump to 100h, you need to implement a little bit that traffic table, and include the selection of those memory banks by those port pins.
Now, suppose P1.0 is connected to eprom address A16, P1.1 to A17 and P1.2 to A18.
Then, if the routine
pointer by 100h is in the second 64k bank, it means that P1.0 must be at
level "1" while P1.1 and P1.2 at level "0", so it is just a matter to do
it BEFORE the JMP 0500h instruction.
; General Return to Caller
GENRET: Mov A,P1 ; 2 bytes
Anl A,#0F8h ; 2 bytes - Selects the basic lower 64k page, bits P1.0,1,2 = "0"
Mov P1,A ; 2 bytes - Here selecting the lower 64k page
Ret ; 1 byte - here already running in that first 64k page.
; 2 bytes
Anl A,#0F8h ; 2 bytes
Orl A,#1 ; 2 bytes
Mov P1,A ; 2 bytes
Call 0500h ; 3 bytes
Jmp 0100h ; 2 bytes
; 2 bytes - Get actual P1 value
Anl A,#0F8h ; 2 bytes - Keep the other P1 bits intact
Orl A,#1 ; 2 bytes - Here selects the 64k page, 0 = 0, 1=1, 2=2, etc.
Mov P1,A ; 2 bytes - Here physically that page is selected
Call 0700h ; 3 bytes - Here is running in that page, not any more in first 64k
Jmp 0100h ; 2 bytes - here will reselect first 64k page and return to caller.
; 2 bytes
Anl A,#0F8h ; 2 bytes
Orl A,#1 ; 2 bytes
Mov P1,A ; 2 bytes
Call 0900h ; 3 bytes
Jmp 0100h ; 2 bytes
So, you will need to create this traffic table for all routines you need to call from the basic page to any other page. Calls, Returns, Jumps in the same page doesn't need to go via this traffic table, since it is already running in that page.
Then suppose your machine
is running at the lower 64k page and you need to call the routine ROUT2,
so the CALL ROUT2 will save PC in the stack and jump to ROUT2 that will
be located at
address 0114h, so exactly after the instruction MOV P1,A, the lines at P1.0,1 and 2 will be changing and selecting the second 64k page (since P1.0 now is "1"), and then the next instruction will be the CALL 0700h in the second page already. When that routine finishes, it will returns to the instruction right after the CALL that is the JMP 0100h, in the second page yet. At the 0100h, the GENRET routine just drop all those selection bits, so right after the instruction MOV P1,A, it will be running at the lower page (page zero), and the following RET instruction will return to the ROUT2 routine caller.
It is that easy.
You can implement this in several other ways, but the more safe way is just do duplicate the traffic table in every 64k page, exactly the same, starting at the address 0100h.
Remember that when you are running your editor assembler, it can not deal with two different routines at the same address, it means, you can not make it understand that you have some routine starting at address 0500h in page 1 and another routine starting at 04F0h in the page zero, both crossing each other. You need to create it as different programs, and then create the same traffic table by hand in each one.
The GENRET routine,
existent in all pages, will reselect to page zero and return to caller.
The interrupts follows the same fashion. Whenever an interrupts happens, its routine will jump to one special routine at the traffic table.
It means that if your
machine is running the ROUT2 at 0700h at page 1 and an interrupts happens,
you have two choices, duplicate its routine in every page, so you dont
need to worry about, or then jump to a special routine in the traffic table,
that first save the contents of P1, so it can return to page 1 after the
interrupt routine ends, and then jump to a regular traffic routine.
The only difference is that when returning from the interrupt routine,
it needs to first restore the old P1 saved at the interrupt routine first
bytes, different from GENRET routine that always returns to caller in page
This is duplicated
at every page:
; SERIAL PORT INTERRUPT
Mov A,P1 ;
PUSH ACC ;
Call ROUTS ; ROUTS is the traffic routine to jump to page zero serial routine.
Pop ACC ; Get back P1 old information
Mov P1,A ; Select old page when interrupts happened
RETI ; Return from interrupt to the old routine interrupted.
Personally I preffer to chose the option to have the complete interrupt routines duplicated in every page, for several reasons:
1) An interrupt routine needs to be small, so it will not create any space problem to duplicate it in every page.
2) It needs to be speedy, so extra code to control page selection will be an issue
3) It will be simple to understand.
I already implemented a voice module that used 1MB eproms and used this technique, it is obvious that you can not run a routine in page zero to read data from another page. The type of instruction MOVC A,@A+DPTR runs easy in the same page, so in my project, I needed to implement the audio routine (Read and Move to DAC) in every page, but it was less than 50 bytes...
Remember that when switching pages, all you have inside the 8051 still the same, so, you can use the internal ram memory as argument storage between pages to transport values, flags, and so on.
Remember that instead to use only DPTR as a 16 bit pointer, you can also use P2 and R0 to address external memory, example;
Mov DPTR, #0519h
you can use
R0 will select address lower bits to 19h while P2 will be set to select higher address to 05h, so they really suck data from eprom address 0519h into A register.
Now, remember that you have R1 that can do the same (Movc A,@R1) and also remember that you have 4 sets of R0 and R1, so you can have in real 9 x 16 bits pointers instead of blame the 8051 to have only 1 dptr.
You can switch between banks with just a single instruction to select register bank0, bank1, bank2 or bank3, according to what you setb or clr bits RS1 and RS0.