The new BS2SX provides 16K bytes of EEPROM organized in eight blocks, each block consisting of 2048 bytes.
On boot, either by resetting or applying power, the BS2SX zeros all of the 63 bytes of scratchpad memory and begins execution at block 0.
Program execution may be redirected to any of the other blocks using the RUN command; e.g,
RUN 1Note that program execution begins at the beginning of the program in block 1. One may return to block 0 by executing;
RUN 0It is critical to note that on returning to block 0, program execution does not pick up at the statement after the RUN 1 command. Rather, RUN 0 causes program execution to begin at the beginning of the program in block 0.
That is, the RUN command is not a subroutine call. One might think of it as a crude GOTO where the only permissable labels are the begining of each of the eight 2048 byte blocks.
One might be quick to criticize Parallax in that this is not a Stamp with 16K of EEPROM in the same sense as the BS2 has 2K of EEPROM and ask why not. Why can't one treat the BS2SX as a BS2 with 16K.
Recognize that when you prepare a program, the PC generates tokens which are saved in the Stamp's EEPROM. Each token is fetched from EEPROM and interpretted by the processor as to exactly what to do. Thus, a GOTO has a particular code along with an 11 bit argument as to where go. This limits the range to locations 0 - $7FF.
Much the same is true of RAM locations. Note that the BS2 has 32 bytes of RAM of which six are reserved for input, output and direction. In this case, the interpretter is using 5 bits in their tokens to identify the specific RAM location. With the BS2SX, they have added 64 new RAM locations but they are unable to directly address them as the token field is limited to five bits.
Thus, Parallax is retrofitting an older design as best they can without starting all over. They have preserved their interpretter and yet expanded the capabilities by providing new commands. Thus the RUN command is used to accommodate the increased EEPROM and the GET and PUT commands to handle the increased RAM.
Note that all 26 bytes of RAM and the 63 bytes of scratchpad RAM are global. That is, changes made in one program block will appear in the other blocks as well. Thus, it is important to declare the variables the same in each of the program blocks. That is, if variable A occupies byte B0 in one program and another block uses the variable, it is important that this variable also be in the same location.
None of the RAM locations are changed when the program is redirected from one block to another. Thus, the states of the DIRS and OUTS words remain the same. That is, the I/O is not affected.
The stack is destroyed when transferring from one block to another. The ramifications of this are discussed below.
One might use this added EEPROM capability in a number of ways; distinctly different programs, chaining, interleaving and for logging data and for storing text.
One might redirect to any of a number specific programs on boot. One application which comes to mind is a data logger where the hardware is the same, but there are clearly distinct tasks. Thus, on boot, the BS2SX might read three inputs and redirect to a distinct block, for example, a block which communicates with the user via a serial link to "arm" the logger. That is, set up the sampling time and other parameters. Other blocks might be devoted to calibration, actual logging and uploading the data to a PC.
This is illustrated in Program LOG1.
' LOG1_0.BSX ' ' Reads switches on P1 and P0 and redirects to the appropriate block. ' ' Peter H. Anderson, Nov, '98 BRANCH (INS.NIB0 & $03), [SETUP, CAL, LOG, DUMP] CAL: RUN 1 ' calibration routine is in block 1 LOG: RUN 2 ' logging routine in block 2 DUMP: RUN 3 ' dump to PC routine in block 3 SETUP: ' the code to communicate with the PC and setup the logger appears ' here.
The idea here is that the program starts at the beginning of block 0 and when you run out of program memory, transfer to block 1, etc. However, it is important to note that subroutines in one block are not accessible when the Stamp is in another block.
Assume you have a clearly defined task which uses a good deal of programming memory. This task might be placed in another block and free up memory for your main program in block 0. This might be performing 256 A/D conversions and then performing an RMS calculation.
In the following example, I use this technique to pulse dial a telephone number.
Note that a RAM location is used to indicate just how the program on page 0 was entered. On boot, all RAM memory is set to zero. This is convenient in ascertaining that the program was entered from boot.
Thus the BRANCH command is used to direct the program to either STARTUP or to _1 based on the value of RET_PLACE.
In STARTUP, RET_PLACE is set to a one. A lookup table is used to pass each digit to TEMP. Program flow is then passed to block 1.
In program 1, an LED (or dial pulse relay is pulsed TEMP times. Control is then passed back to block 0.
Note that now, RET_PLACE is 1 and the program is directed to label _1 which picks up at the statement where the program was directed to block 1. Thus, one might think of this technique as being somewhat like a subroutine.
' Program DIAL1_0.BSX (Program 0) ' ' Dials tel number 1-800-555-1212 ' ' Illustrates a parent spawning a child process ' ' Peter H. Anderson, Nov, '98 RET_PLACE VAR BYTE INDEX VAR BYTE TEMP VAR BYTE N VAR BYTE DIRS = $0001 ' P0 is an output BRANCH RET_PLACE, [STARTUP, _1] STARTUP: RET_PLACE=1 ' new place to retun to FOR INDEX=0 TO 15 LOOKUP INDEX, [1,8,0,0,5,5,5,1,2,1,2, $F], TEMP DEBUG ? TEMP IF TEMP = $F THEN DONE ' done dialing RUN 1 ' dial the digit _1: NEXT DONE GOTO DONE ''''''''''''''''''''''' ' Program DIAL1_1.BSX (Program 1) RET_PLACE VAR BYTE INDEX VAR BYTE TEMP VAR BYTE N VAR BYTE IF TEMP <> 0 THEN SKIP_1 TEMP = TEMP + 10 ' zero is really ten pulses SKIP_1: FOR N = 1 TO TEMP ' dial the digit OUT0=1 PAUSE 63 OUT0=0 PAUSE 37 NEXT PAUSE 500 ' interdigit delay RUN 0 ' back to block 0Assume Program 0 had been written as follows.
' Program DIAL2_0.BSX (Program 0). ' This program will not work. INDEX VAR BYTE TEMP VAR BYTE N VAR BYTE DIRS = $0001 ' P0 is an output BRANCH RET_PLACE, [STARTUP, _1] STARTUP: GOSUB DIAL DEBUG "Done" ' the program will never get here DONE GOTO DONE DIAL: RET_PLACE=1 FOR INDEX=0 TO 15 LOOKUP INDEX, [1,8,0,0,5,5,5,1,2,1,2, $F], TEMP IF TEMP = $F THEN DIAL_DONE ' done dialing RUN 1 _1: NEXT DIAL_DONE: RETURNThis will not work. Note that at STARTUP, subroutine DIAL is called and thus the next address is placed on the stack. However, the RUN command destroys the stack. Thus, when the program hits the RETURN in the DIAL routine, there is no valid return address on the stack.
The point is that this ability to transfer control to another block to perform a task and then retun to the calling program is powerful. However, recognize its limitation. The RUN command should only be executed from the main program, not from a subroutine. Of course, the program in block 1 can call subroutines, at least to the limitation of a nesting level of four. Thus, this technique might be reserved for "calling" major tasks from the parent block (block 0) using the RUN command and then use subroutines in the child block to implement this major task.
In the DIAL program, a parent process spawned a child process and the parent communicated with the parent. There was no communication back to the parent. In the following, multiple tasks and child to parent communication are discussed.
The following program illustrates how two different child processes may be implemented with communication from the parent to the child and from the child to the parent.
The program writes the numbers 0 - 24 to consecutive locations in EEPROM in block 0. It then writes 255, 254, 253, etc to the first 25 locations in EEPROM in block 1. To verify this, the first 25 EEPROM locations in block 0 and then block 1 are dumped using the DEBUG command.
In this case, there are two tasks; write to EEPROM and read from EEPROM, in block 1. Thus, I used a location in scratchpad to identify the task for the child process. In this case, the EEPROM address and the data were passed using the conventional RAM. In writing to EEPROM, the address and data are passed to the child process. In reading from EEPROM, the parent passes the address and the child returns the data which was read.
As in the previous example, on boot all RAM is set to 0. Thus, on entering the program from boot, RET_PLACE is zero and the program is directed to STARTUP.
In writing to EEPROM in block 1, the TASK is set to 0. This is used by Program 1 to direct the program to the WRITE_EE code. The RET_PLACE is set to 1 such that on the return from block 1, the program picks up at label _1.
In reading from EEPROM in block 1, the TASK is set to 1 indicating to Program 1 that the task is one of reading from EEPROM. The RET_PLACE is set to 2 such that on return from block 1, the program is directed to label _2.
' EE1_0.BSX (Program 0). ' ' Writes data to block 0 and to block 1 and then reads and displays. ' Illustrates communication from a parent to a child and child to parent. ' ' Peter H. Anderson, Nov, '98 RET_PLACE VAR BYTE TASK VAR BYTE X VAR BYTE N VAR WORD P VAR WORD BRANCH RET_PLACE, [START, _1, _2] START: DEBUG "START", CR P = $000 ' write 0, 1, 2 to data locations beginning at $000 FOR N=0 TO 24 X = N WRITE P, X P=P+1 NEXT ' write 255, 254, 253 to locations starting at $800 TASK=0 ' Program EEPROM RET_PLACE = 1 P=$000 FOR N = 0 TO 24 X = 255 - N RUN 1 _1: P = P+1 NEXT P=$000 ' read data from block 0 FOR N=0 TO 24 READ P, X DEBUG HEX P, " ", HEX X, CR P=P+1 NEXT P=$000 ' read data from block 1 RET_PLACE=2 TASK=1 FOR N=0 TO 24 RUN 1 _2: DEBUG HEX P+$800, " ", HEX X, CR P=P+1 NEXT DONE: GOTO DONE ''''''''''''''''''''''' ' EE1_1.BSX (Program 1) RET_PLACE VAR BYTE TASK VAR BYTE X VAR BYTE N VAR WORD P VAR WORD BRANCH TASK, [WRITE_EE, READ_EE] WRITE_EE: WRITE P, X RUN 0 READ_EE: READ P, X RUN 0The idea in presenting the above routine was to illustrate how several tasks may be identified and executed in block 1, 2, etc and how data may be passed from the parent to the child and from the child back to the parent.
However, there is a great deal more power here in using the BS2X to log data.
Recall that the tokens associated with a Stamp program begin at $7FF. This leaves EEPROM addresses 0 though to the end of the program available for data logging. For example, in the above, Program 1 uses fewer than 32 bytes and occupies locations $7E0 - $7FF in block 1. This leaves 2016 locations ($000 - $7DF in block 1) free for logging data.
If block 0 is reserved for the main program and blocks 1 through 7 are all used to save data, the BS2SX provides the capability to save over 14,000 bytes. Although this could be done with two Microchip 24LC64 EEPROMs or similar, the fact is that the extra $10 for the BS2SX may well be worth the convenience.
This concept is presented in Program LOG1.
' LOG1_0.BSX (Program 0). ' ' Writes data to 4096 EEPROM locations. Reads and displays using DEBUG ' command. ' ' Peter H. Anderson, Nov, '98 RET_PLACE VAR BYTE TASK VAR BYTE P VAR WORD BLOCK VAR BYTE ADR VAR WORD X VAR BYTE N VAR WORD BRANCH RET_PLACE, [STARTUP, _1, _2] STARTUP: TASK=0 ' Program EEPROM RET_PLACE=1 P=$0000 FOR N = 0 TO 4095 X = N//127 ' this might be the result of an a/d con BLOCK = P/(2016) + 1 ADR = P//(2016) RUN BLOCK _1: P = P+1 DEBUG ? P NEXT DUMP: TASK=1 ' Read EEPROM RET_PLACE=2 P=$0000 FOR N=0 TO 4095 BLOCK = P/(2016) + 1 ADR = P//(2016) RUN BLOCK _2: DEBUG HEX4 P, " ", HEX2 X, CR P=P+1 NEXT DONE: GOTO DONE '''''''''''''''''''''''''''''' 'LOG1_1.BSX (Program 1) ' ' This is also Programs 2, 3, 4, 5, 6 and 7 RET_PLACE VAR BYTE TASK VAR BYTE P VAR WORD BLOCK VAR BYTE ADR VAR WORD X VAR BYTE N VAR WORD BRANCH TASK, [WRITE_EE, READ_EE] ' figure out the task WRITE_EE: WRITE ADR, X RUN 0 READ_EE: READ ADR, X RUN 0In this program, variable P is the data sample number and may range between 0 and 14111. (7 blocks * (2048-32 bytes per block)). The specific block is calculated as (P/2016) + 1 and the absolute address in that block is P//2016.
X is the data to be written. I simply generated data using X = N//127. However, note that X could be the result of an A/D conversion.
Data is written to 4096 bytes using TASK identifier 0 to communicate to the child process that data X is to be written to location ADR and the RET_PLACE is set to 1. The child process is spawned using the RUN command. It is useful to note that the arguement for the RUN command may be a variable, in this case;
RUN BLOCKData is then read from the 4096 memory locations using TASK identifier 1 to indicate to the child that the data is to be read from location ADR into X. The data is then displayed using the DEBUG command.
Note that the programs for blocks 1 through 7 are all the same. They simply provide the code to determine whether the task is writing or reading and the implementations of these tasks.
In many designs there is a need to communicate with the user and text can quickly use up the 2K memory associated with the BS2. The added EEPROM associated with the BS2SX permits you to output some 14,000 bytes of different strings.
This concept is illustrated in Program STR1.
'STR1_0.BSX (Program 0). ' 'Illustrates how to output strings using blocks 1, 2, ... ' 'Peter H. Anderson, Nov, '98 RET_PLACE VAR BYTE STR_NUM VAR BYTE BLOCK VAR BYTE P VAR WORD X VAR BYTE N VAR WORD DIRS = $0001 ' P0 an output DEBUG DEC RET_PLACE, CR PAUSE 2000 BRANCH RET_PLACE, [STARTUP, _1, _2] STARTUP: RET_PLACE=1 BLOCK=1 STR_NUM=1 RUN BLOCK ' display string 1 in block 1 _1: FOR N = 0 to 9 ' flash an LED OUT1=1 PAUSE 200 OUT0=0 PAUSE 200 NEXT RET_PLACE=2 BLOCK=2 STR_NUM=0 RUN BLOCK ' display string 0 in block 2 _2: DONE: GOTO DONE '''''''''''''''''''''''''' 'STR1_1.BSX (Program 1) STR1_0 DATA "Hello World from Block 1", $0D, $0A, $00 STR1_1 DATA "abcdefghijklmnop from Block 1", $0D, $0A, $00 STR1_2 DATA "Parallax BS2SX from Block 1", $0D, $0A, $00 STRINGS DATA WORD STR1_0, WORD STR1_1, WORD STR1_2 RET_PLACE VAR BYTE STR_NUM VAR BYTE BLOCK VAR BYTE P VAR WORD X VAR BYTE N VAR WORD STR_OUT: ' map STR_NUM into a pointer READ STRINGS + (2 * STR_NUM), P.BYTE0 READ STRINGS + (2 * STR_NUM) + 1, P.BYTE1 ' DEBUG HEX P, " ", HEX STR_NUM, CR STR_OUT_1: READ P, X IF X=0 THEN STR_OUT_DONE DEBUG X P=P+1 GOTO STR_OUT_1 ' keeping going until null termination STR_OUT_DONE: RUN 0 '''''''''''''''''''''''''''''''' 'STR1_2.BSX (Program 2) STR2_0 DATA "Hello World from Block 2", $0D, $0A, $00 STR2_1 DATA "abcdefghijklmnop from Block 2", $0D, $0A, $00 STR2_2 DATA "Parallax BS2SX from Block 2", $0D, $0A, $00 STRINGS DATA WORD STR2_0, WORD STR2_1, WORD STR2_2 RET_PLACE VAR BYTE STR_NUM VAR BYTE BLOCK VAR BYTE P VAR WORD X VAR BYTE N VAR WORD STR_OUT: ' map STR_NUM into a pointer READ STRINGS + (2 * STR_NUM), P.BYTE0 READ STRINGS + (2 * STR_NUM) + 1, P.BYTE1 STR_OUT_1: READ P, X IF X=0 THEN STR_OUT_DONE DEBUG X P=P+1 GOTO STR_OUT_1 STR_OUT_DONE: RUN 0In this program I provided the capability to select any of six strings. Three are in block 1 and the other three in block 2. Clearly, this concept could be extended to more fully fill each block and to also use blocks 1 through 7.
In the main program, the block and the string number in that block are identified and the parent spawns the child which outputs the selected string using the DEBUG command.
I output string 1 in block 1, flashed an LED a number of times and then output string 0 in block 2. My point is that outputting a string from the main program is reduced to code of the following form.
RET_PLACE=1 BLOCK=1 STR_NUM=1 RUN BLOCK ' display a string _1:Each of the child processes are the same, except for the actual strings. I opted to give the strings unique names from one block to another, but this was only for clarity.
Note that the strings are implemented using the DATA directive followed by the string, followed by a zero to indicate the end of the string. Each string is given a unique identifier; e.g., STR1_0 and a table of the addresses of each of these strings is implemented;
STRINGS DATA WORD STR1_0, WORD STR1_1, WORD STR1_2The string number is mapped into the first address of the specified string;
READ STRINGS + (2 * STR_NUM), P.BYTE0 'low byte of address READ STRINGS + (2 * STR_NUM) + 1, P.BYTE1 'high byteEach character is then fetched and output using the DEBUG command until the null character is fetched at which time the child returns control to the parent.