EmbeddedRelated.com
Forums

EEPROM Variable Storage On MC9S12C128

Started by jmey...@emittechnologies.com December 17, 2008
Hello,

I need to store four strings into non-volatile storage. One string will be 15 chars in length and the other three will be 10 chars in length. The strings may be written less that 10 times in the device's lifetime, but will be read possibly thousands of times. It is very important that the strings remain in memory if the power is lost! Is EEPROM the right place for this?

I am working with the MC9S12C128. The memory map given by the development board user guide is as follows:

0x0000-0x03FF Registers
0x0400-0x2FFF EEPROM
0x3000-0x3FFF RAM
0x4000-0x7FFF Fixed FLASH
0x8000-0xBFFF Paged FLASH
0xC000-0xFF00 Fixed FLASH
0xFF00-0xFFFF Vectors

I am programming in C in Freescale Codewarrior. How do I store these strings into EEPROM?

Also, when I look at the Memory through the "True-Time Simulator & Real-Time Debugger," it shows memory ranging from 000000-FFFFFF. This is a 16-bit system. Where are the extra 8 bits coming from???

Yes, EEPROM should be used but this may be a two part problem:

A: how to physically write into the EEPROM memory

B: how to do this in a way that does not corrupt the EEPROM data if the
power is lost in the middle of writing.

It is not hard to find out to do A, either look into the manual or google
for "hc12 eeprom write", the first hit was a complete example API in c
http://dragonsgate.net/pipermail/icc-mot/2003-November/000718.html

B can be very inmportant in critical systems like automation/medical/space
etc, maybe not so important in consumer products like cell phones where the
user enters data manually and can correct if necessary.

One way to to B is like follows:

1. In the EEPROM, set up two identical data areas with your strings (say
area Main and area Temp) and also a "magic" string that will be used for
checking

2. Create two subroutines:

Write()
{
write new data into area Temp
set the magic string to some value e.g. "NewDataEntered"
}

Update()
{
if (magic string = "NewDataEntered")
{
copy data from Temp to Main
clear the magic string to e.g all zeroes
}
}

Then when data should be entered your program should do Write() and then
Update().

Also, during startup after power, always call Update().

This way data will always be guaranteed to be correct.

A variation that consumes less EEPROM cells is to write only one string at
the time and set magic to e.g. "NewDataEnteredForString1" etc etc.

Btw, if anyone has some better or simpler method to accomplish this it would
be very interesting to hear about it.

Regards, Anders

> -----Original Message-----
> From: 6... [mailto:6...]
> On Behalf Of j...@emittechnologies.com
> Sent: Wednesday, December 17, 2008 11:10 PM
> To: 6...
> Subject: [68HC12] EEPROM Variable Storage On MC9S12C128
>
> Hello,
>
> I need to store four strings into non-volatile storage. One
> string will be 15 chars in length and the other three will be
> 10 chars in length. The strings may be written less that 10
> times in the device's lifetime, but will be read possibly
> thousands of times. It is very important that the strings
> remain in memory if the power is lost! Is EEPROM the right
> place for this?
>
> I am working with the MC9S12C128. The memory map given by the
> development board user guide is as follows:
>
> 0x0000-0x03FF Registers
> 0x0400-0x2FFF EEPROM
> 0x3000-0x3FFF RAM
> 0x4000-0x7FFF Fixed FLASH
> 0x8000-0xBFFF Paged FLASH
> 0xC000-0xFF00 Fixed FLASH
> 0xFF00-0xFFFF Vectors
>
> I am programming in C in Freescale Codewarrior. How do I
> store these strings into EEPROM?
>
> Also, when I look at the Memory through the "True-Time
> Simulator & Real-Time Debugger," it shows memory ranging from
> 000000-FFFFFF. This is a 16-bit system. Where are the extra 8
> bits coming from???
>
>
> Btw, if anyone has some better or simpler method to accomplish this it would be
> very interesting to hear about it.
>
> Regards, Anders
>

Don't know if this is simpler or better but here is how I have done it:

Define a structure with your data to store in EEPROM. The first member in this
structure is a counter and the last is a CRC checksum of the contents of the
structure. The counter is used to identify which structure in EEPROM that was
written last and the CRC is used to validate the integrity of the non volatile
variables in the EEPROM structures.

Declare two of these structures in EEPROM memory, one in RAM and one in code
with preloaded default values. The structure in RAM will be your work structure
that the rest of the program uses to access the non volatile variables.

In RAM you also need one variable for the current count and one pointer to the
current EEPROM structure (I use a const array with pointers to the EEPROM
structures and a one bit index).

At startup you need to find out if one or both of the EEPROM structures data is
OK with the CRC. If both are OK, use the one with the highest count (taking
care of wraping). If one is OK, use that and if none is OK use the default in
code. Copy the one that is OK to the work structure in RAM (from EEPROM or
code). Also update the variables for the current counter and the pointer to the
next EEPROM location to write (or index in the array).

When you need to save the none volatile variables to EEPROM first update the
structure in RAM with the current counter+1 and make a new CRC. Then write it
to the next EEPROM structure in turn for writing and finally update the counter
and pointer to the next EEPROM location to write.

And also you need to tell the programmer that EEPROM should not be erased
whenever you program the code into the chip. Otherwise you will always start a
debug session with the default data (since there is no valid data in the
EEPROM).

And here is the code:

/*
** ###################################################################
**
** Filename : Config.c
**
** Project : LRPSMB01
**
** Processor : MC9S12A128BCFU
**
** Date : 2003-05-30
**
** Abstract :
**
** Routines to handle configurations
** saved in non volatile memory (EEPROM).
**
** There are two Config strucutures in EEPROM. Changes are
** written to these in a circular manner. The Config structure
** has a member for write count and a CRC16. At startup
** the one with the highest write count and valid CRC is copied
** to Ram and used.
**
** ###################################################################
*/

#include "config.h"
#include "pe_types.h"
#include "IEE1.h"
#include
#include

extern void Tasks(void);

Config RamConf; // This is the working configuration structure in RAM. This is
// the source or destination when moving data between the EEPROM
// and RAM. The application read and writes configuration data to
// this structure and then updates it to EEPROM.

const Config DefConf={ // Configuration structure that is used if no valid one
is found
// in the EEPROM at startup.
0, // CRC (calculated later)
0, // write counter
0, // uSwState0
0, // uSwState1
0, // uSwState2
0 // uSwState3
};

#pragma DATA_SEG EEPROM
Config EEConf1;
Config EEConf2;
#pragma DATA_SEG DEFAULT

const Config *EECfgArray[]={&EEConf1,&EEConf2};
byte CfgUse=0;

///// CRC16 functions

const byte CRC16TableHigh[]={
0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70,
0x81, 0x91, 0xA1, 0xB1, 0xC1, 0xD1, 0xE1, 0xF1
};

const byte CRC16TableLow[]={
0x00, 0x21, 0x42, 0x63, 0x84, 0xA5, 0xC6, 0xE7,
0x08, 0x29, 0x4A, 0x6B, 0x8C, 0xAD, 0xCE, 0xEF
};

TWREG CRC16Work;

void CRC16_Update4bits(byte val )
{
byte t;

// Step one, extract the Most significant 4 bits of the CRC register
t = CRC16Work.b.high >> 4;

// XOR in the Message Data into the extracted bits
t = t ^ val;

// Shift the CRC Register left 4 bits
CRC16Work.w<<=4;

// Do the table lookups and XOR the result into the CRC Tables
CRC16Work.b.high=CRC16Work.b.high ^ CRC16TableHigh[t & 0x0f];
CRC16Work.b.low=CRC16Work.b.low ^ CRC16TableLow[t & 0x0f];
}

word CRC16(void *adr,byte count)
{
CRC16Work.w=0xffff; // Initialize running CRC.
for(;count>0;count--){
CRC16_Update4bits(*((byte*)adr)>>4);
CRC16_Update4bits(*((byte*)adr));
((byte*)adr)++;
}
return CRC16Work.w;
}

///// Config functions

word Config_CRC(Config *conf)
// Get CRC16 for the Config struct (excluding the CRC member).
{
return(CRC16((byte*)(conf)+2,sizeof(Config)-2));
}

bool Config_CheckCrc(Config *conf)
// Check if the CRC16 of a Config struct is OK.
{
return(conf->ConfCrc==Config_CRC(conf));
}

void Config_DefaultToRam(void)
// Set up the Config struct in Ram with default values.
{
memcpy(&RamConf,&DefConf,sizeof(Config));
RamConf.ConfCrc=Config_CRC(&RamConf);
}

void Config_Init(void)
// Get the active Config from EEPROM to Ram (or the default if no
// one has been written to EEPROM yet.
{
bool bCrcEECfg1,bCrcEECfg2;

bCrcEECfg1=Config_CheckCrc(&EEConf1);
bCrcEECfg2=Config_CheckCrc(&EEConf2);

if (bCrcEECfg1 && bCrcEECfg2){ // Both are valid. Use the one with highest
writecount.
if (EEConf1.uWriteCnt>EEConf2.uWriteCnt)
CfgUse=0;
else
CfgUse=1;
if (abs((EEConf1.uWriteCnt>>8)-(EEConf2.uWriteCnt>>8))>0x80) // Check for
wrap.
CfgUse=(CfgUse+1)&1;
} else if (bCrcEECfg1){
CfgUse=0;
} else if (bCrcEECfg2){
CfgUse=1;
} else
CfgUse=2; // No config stored in EEPROM.

if (CfgUse<2){ // Get EEConfig to RamConfig
memcpy(&RamConf,EECfgArray[CfgUse],sizeof(Config));
CfgUse=(CfgUse+1)&1; // Update to other EE on next write.
} else {
Config_DefaultToRam();
CfgUse=0; // Write to EE 0 on next write.
// Config_UpdateConfigToEEPROM(); // Write the default to EEPROM
}
}

bool Config_UpdateConfigToEEPROM(void)
{
byte i; // dword counter

dword *pSrc=(ulong*)&RamConf; // Pointer to Config in Ram (src)
dword *pDest=(ulong*)EECfgArray[CfgUse]; // Pointer to config in EEPROM (dest)
RamConf.uWriteCnt++; // Update write counter
RamConf.ConfCrc=Config_CRC(&RamConf); // Update CRC

for(i=0;i dwords in
// the Config structure (allways an even number of
// dwords).
if (IEE1_SetLong((word)pDest,*pSrc)!=ERR_OK){ // Write config.
return FALSE;
}
do{
Tasks(); // Do backgroundtasks while waiting for write to
// be finished.
}while(ESTAT_CCIF==0);
pSrc++; // Next dword
pDest++;
}
CfgUse=(CfgUse+1)&1; // Update pointer (index) to other Config struct
// in EEPROM which is written to next time.
return TRUE;
}
/Ruben

Thank you both for your replies. I've been doing a lot of reading and researching this morning. I finally just broke down and studied the mC9S12C reference manual for writing to FLASH. Here is what I have so far:

#include /* derivative information */

#define Access_Error -1
#define Protection_Error -2

// setup Flash control unit
void Flash_Init(void)
{
// oscillator clock = 4MHz
// bus clock = 2MHz
// Tbus = 0.5us
// FDIV[5:0] = 22 = 0x10110
// PRDIV = 0
// FCLK = 4000000/(1+22) = 173.913MHz
FCLKDIV = 22;
// Clear Errors In Normal Mode
// Protection Violation Clear
// Access Error Clear
FSTAT = FSTAT | (FSTAT_PVIOL_MASK|FSTAT_ACCERR_MASK);
}

// writes a single 16-bit word to Flash
signed char Flash_Write_Word(unsigned char ppage,
unsigned int address,
unsigned int data)
{
PPAGE = ppage;
// Waiting To Be Ready
while(!FSTAT_CBEIF);
// New Command Sequence Can Be Started
// Clearing Errors In Normal Mode
FSTAT = FSTAT | (FSTAT_PVIOL_MASK|FSTAT_ACCERR_MASK);
// Checking For Word Alignment
if(address & 0x0001) {return Access_Error}
// Writing Data To Given Address
(*(unsigned int *)address) = data;
// Storing Program Command In FCMD
FCMD = FCMD_CMDB5_MASK;
// Clearing CBEIF flag to launch command
FSTAT_CBEIF = 1;
if(FSTAT_ACCERR) { return Access_Error }
if(FSTAT_PVIOL) { return Protection_Error }
return 1;
}
// Erase Flash 1k Sector
signed char Flash_Erase_Sector(void)
{
// Waiting To Be Ready
while(!FSTAT_CBEIF);
// Clearing Errors
FSTAT = FSTAT | (FSTAT_PVIOL_MASK|FSTAT_ACCERR_MASK);
// Writing to Flash Array Address To Start Command
(*(unsigned int *)0x8000) = 0xFFFF;
// Writing Sector Erage Command
FCMD = 40;
// Clearing CBEIF flag to launch command
FSTAT_CBEIF = 1;
if(FSTAT_ACCERR) { return Access_Error }
if(FSTAT_PVIOL) { return Protection_Error }
return 1;
}

// Needs Finished
// Reads A Single Word From Flash
unsigned int Flash_Read_Word(unsigned char ppage,
unsigned int address)
{
PPAGE = ppage;
while(!FSTAT_CCIF);
return (unsigned int)*(unsigned int *)address;
}

Previous code didn't compile. Just in case someone is interested, here is the confirmed working code for the C128:

#include // derivative information
#include "flash.h" // prototypes
// setup Flash control unit
void Flash_Init()
{
// oscillator clock = 4MHz
// bus clock = 2MHz
// Tbus = 0.5us
// FDIV[5:0] = 22 = 0b10110 =0x16
// PRDIV = 0
// FCLK = 4000000/(1+22) = 173.913MHz
FCLKDIV = 22;
// Clear Errors In Normal Mode
// Protection Violation Clear
// Access Error Clear
FSTAT = FSTAT | (FSTAT_PVIOL_MASK|FSTAT_ACCERR_MASK);
}

// writes a single 16-bit word to Flash
signed char Flash_Write_Word(unsigned char ppage,
unsigned int address,
unsigned int data)
{
PPAGE = ppage;
// Waiting To Be Ready
while(!FSTAT_CBEIF);
// New Command Sequence Can Be Started
// Clearing Errors In Normal Mode
FSTAT = FSTAT | (FSTAT_PVIOL_MASK|FSTAT_ACCERR_MASK);
// Checking For Word Alignment
if(address & 0x0001) { return Odd_Access;}
// Writing Data To Given Address
(*(unsigned int *)address) = data;
// Storing Program Command In FCMD
FCMD = FCMD_CMDB5_MASK;
// Clearing CBEIF flag to launch command
FSTAT_CBEIF = 1;
if(FSTAT_ACCERR) { return Access_Error; }
if(FSTAT_PVIOL) { return Protection_Error; }
return 1;
}
// Erase Flash 1k Sector
signed char Flash_Erase_Sector()
{
// Waiting To Be Ready
while(!FSTAT_CBEIF);
// Clearing Errors
FSTAT = FSTAT | (FSTAT_PVIOL_MASK|FSTAT_ACCERR_MASK);
// Writing to Flash Array Address To Start Command
(*(unsigned int *)0x8000) = 0xFFFF;
// Writing Sector Erage Command
FCMD = 40;
// Clearing CBEIF flag to launch command
FSTAT_CBEIF = 1;
if(FSTAT_ACCERR) { return Access_Error; }
if(FSTAT_PVIOL) { return Protection_Error; }
return 1;
}

// Reads A Single Word From Flash
unsigned int Flash_Read_Word(unsigned char ppage,unsigned int address)
{
PPAGE = ppage;
while(!FSTAT_CCIF);
return (unsigned int)*(unsigned int *)address;
}

Nice to see someone making progress. Just two notes about flash and flags
clearable writing ones to them.

1)
> FSTAT = FSTAT | (FSTAT_PVIOL_MASK|FSTAT_ACCERR_MASK);

You think you are clearing just PVIOL and ACCERR here? Nope! You are
clearing all FSTAT flags that are set. One of flags you are clearing is
CBEIF. Clearing CBEIF means start executing flash command... Probably not
what you want. Correct code for clear PVIOL and ACCER is this

FSTAT = FSTAT & (FSTAT_PVIOL_MASK|FSTAT_ACCERR_MASK);

or just

FSTAT = (FSTAT_PVIOL_MASK|FSTAT_ACCERR_MASK);
2) > FSTAT_CBEIF = 1;

Wrong again! Again you are clearing all FSTAT bits. If you want to clear
only CBEIF and keep ACCERR and PVIOL as they are, then please don't do
FSTAT_CBEIF = 1; , wchich is the same like FSTAT |= FSTAT_CBEIF_MASK; . If
you want to clear just CBEIF, then you should do

FSTAT &= FSTAT_CBEIF_MASK;

or

FSTAT = FSTAT_CBEIF_MASK;
3) I don't remember how many flash arrays does C128 have. But flash array is
not readable while being programmed. If your code worked, then probably you
were programming one flash array, while executing code from another flash
array.

Regards
Edward

----- Original Message -----
From:
To: <6...>
Sent: Friday, December 19, 2008 6:44 PM
Subject: [68HC12] Re: EEPROM Variable Storage On MC9S12C128
> Previous code didn't compile. Just in case someone is interested, here is
> the confirmed working code for the C128:
>
> #include // derivative information
> #include "flash.h" // prototypes
> // setup Flash control unit
> void Flash_Init()
> {
> // oscillator clock = 4MHz
> // bus clock = 2MHz
> // Tbus = 0.5us
> // FDIV[5:0] = 22 = 0b10110 =0x16
> // PRDIV = 0
> // FCLK = 4000000/(1+22) = 173.913MHz
> FCLKDIV = 22;
> // Clear Errors In Normal Mode
> // Protection Violation Clear
> // Access Error Clear
> FSTAT = FSTAT | (FSTAT_PVIOL_MASK|FSTAT_ACCERR_MASK);
> }
>
> // writes a single 16-bit word to Flash
> signed char Flash_Write_Word(unsigned char ppage,
> unsigned int address,
> unsigned int data)
> {
> PPAGE = ppage;
> // Waiting To Be Ready
> while(!FSTAT_CBEIF);
> // New Command Sequence Can Be Started
> // Clearing Errors In Normal Mode
> FSTAT = FSTAT | (FSTAT_PVIOL_MASK|FSTAT_ACCERR_MASK);
> // Checking For Word Alignment
> if(address & 0x0001) { return Odd_Access;}
> // Writing Data To Given Address
> (*(unsigned int *)address) = data;
> // Storing Program Command In FCMD
> FCMD = FCMD_CMDB5_MASK;
> // Clearing CBEIF flag to launch command
> FSTAT_CBEIF = 1;
> if(FSTAT_ACCERR) { return Access_Error; }
> if(FSTAT_PVIOL) { return Protection_Error; }
> return 1;
> }
> // Erase Flash 1k Sector
> signed char Flash_Erase_Sector()
> {
> // Waiting To Be Ready
> while(!FSTAT_CBEIF);
> // Clearing Errors
> FSTAT = FSTAT | (FSTAT_PVIOL_MASK|FSTAT_ACCERR_MASK);
> // Writing to Flash Array Address To Start Command
> (*(unsigned int *)0x8000) = 0xFFFF;
> // Writing Sector Erage Command
> FCMD = 40;
> // Clearing CBEIF flag to launch command
> FSTAT_CBEIF = 1;
> if(FSTAT_ACCERR) { return Access_Error; }
> if(FSTAT_PVIOL) { return Protection_Error; }
> return 1;
> }
> // Reads A Single Word From Flash
> unsigned int Flash_Read_Word(unsigned char ppage,unsigned int address)
> {
> PPAGE = ppage;
> while(!FSTAT_CCIF);
> return (unsigned int)*(unsigned int *)address;
> }
>
>
You are correct. I was a little pre-mature on my "working" call. Well, it worked when I was stepping through on the debugger, but nothing else. Even after doing the fixes you mentioned, it was still not working properly.

While doing some research, I found this gem:

http://www.embeddedlearningcenter.com/scripts/tol.exe?CONFIG,pc-freescale.txt%26TEMPLATE,pc_main.ops%26AREA,1

I made a few modifications for the C128, and got the darn thing working. For real this time!! Here is my code:

/*******************************************************************
*
* DESCRIPTION: S12 single array Flash routines
* SOURCE: flash.c
*
*******************************************************************/
#include "flash.h"
#include /* common defines and macros */
#include /* derivative information */
#pragma LINK_INFO DERIVATIVE "MC9S12C128"

#define Flash_Sector_Size 0x200 /* must be modified for particular device */
extern DoOnStack(unsigned int* address);

#pragma MESSAGE DISABLE C1860 //Pointer conversion: Possible loss of data

//*****************************************************************************
//* Function Name : Flash_Init
//* Description : Initialize Flash NVM for HC9S12 by programming
//* FCLKDIV based on passed oscillator frequency, then
//* uprotect the array, and finally ensure PVIOL and
//* ACCERR are cleared by writing to them.
//*
//*****************************************************************************
void Flash_Init()
{
FCLKDIV = 22;
FPROT = 0x18; /* Disable protection for 0x4000 - 0x4FFF (only in special modes)*/
FSTAT |= (FSTAT_PVIOL|FSTAT_ACCERR);/* Clear any errors */
}
//*****************************************************************************
//* Function Name : Flash_Write_Word
//* Description : Program a given Flash location using address and data
//* passed from calling function.
//*
//*****************************************************************************
signed char Flash_Write_Word(unsigned int *far_address,unsigned int data)
{
unsigned int *address;
address = (unsigned int *)far_address; // strip page off
FSTAT = FSTAT_ACCERR | FSTAT_PVIOL;
if((unsigned int)address & 0x0001) {return Flash_Odd_Access;} // Aligned word?
if(*far_address != 0xFFFF) {return Flash_Not_Erased;} // Is it erased?
// FCNFG = 0x00;
(*far_address) = data; // Store desired data to address being programmed

FCMD = 0x20; // Store programming command in FCMD
(void)DoOnStack(far_address);
if (FSTAT_ACCERR) {return Access_Error;}
if (FSTAT_PVIOL) {return Protection_Error;}
return 1;
}
//*****************************************************************************
//* Function Name : Flash_Erase_Sector
//* Description : Erases a given Flash sector using address
//* passed from calling function.
//*
//*****************************************************************************
signed char Flash_Erase_Sector(unsigned int *far_address)
{
unsigned int *address;
address = (unsigned int *)far_address; // strip page off
if((unsigned int)address & 0x0001) {return Flash_Odd_Access;} // Aligned word?
if((unsigned int)address % Flash_Sector_Size !=0) {return Not_StartofSector_Error;}
FCNFG = 0x00;
FSTAT = (FSTAT_ACCERR | FSTAT_PVIOL); // clear errors
(*far_address) = 0xFFFF; /* Dummy store to page to be erased */

FCMD=0x40;
(void)DoOnStack(far_address);
if (FSTAT_ACCERR) {return Access_Error;}
if (FSTAT_PVIOL) {return Protection_Error;}
return 1;
}

Do_On_Stack.asm

;*******************************************************************
;*
;* DESCRIPTION: S12 Flash Asm Routines
;* SOURCE: Do_On_Stack.asm
;*
;*******************************************************************/
;*****************************************************************************
; Local defines
;*****************************************************************************
CBEIF EQU $80
FSTAT EQU $105
FCMD EQU $106
CCIF EQU $40
PAGE_ADDR EQU $30

xdef DoOnStack
;*********************************************************************
;* DoOnStack - copy SpSub onto stack and call it (see also SpSub)
;* De-allocates the stack space used by SpSub after returning from it.
;* Allows final steps in a flash prog/erase command to execute out
;* of RAM (on stack) while flash is out of the memory map
;* This routine can be used for flash word-program or erase commands
;*
;* Calling Convention:
;* jsr DoOnStack
;*
;********************************************************************
DoOnStack:
pshx ;save IX
ldx #SpSubEnd-2 ;point at last word to move to stack
SpmoveLoop: ldd 2,x- ;read from flash
pshd ;move onto stack
cpx #SpSub-2 ;past end?
bne SpmoveLoop ;loop till whole sub on stack
tfr sp,x ;point to sub on stack
ldaa #CBEIF ;preload mask to register command
;call 0,x,00 ;execute the sub on the stack
jsr ,x ;execute the sub on the stack
leas SpSubEnd-SpSub,sp ;de-allocate space used by sub
ldaa FSTAT ;get result of operation
anda #$30 ;and mask all but PVIOL or ACCERR
pulx ;restore IX
rts ;to flash where DoOnStack was called

;*********************************************************************
;* SpSub - register flash command and wait for Flash CCIF
;* this subroutine is copied onto the stack before executing
;* because you can't execute out of flash while a flash command is
;* in progress (see DoOnStack to see how this is used)
;*
;* Note: must be even # of bytes!
;*
;*********************************************************************
EVEN ;Make code start word aliened
SpSub:
tfr ccr,b ;get copy of ccr
orcc #$10 ;disable interrupts
staa FSTAT ;[PwO] register command
nop ;[O] wait min 4~ from w cycle to r
nop ;[O]
nop ;[O]
brclr FSTAT,CCIF,* ;[rfPPP] wait for queued commands to finish
tfr b,ccr ;restore ccr and int condition
rts ;back into DoOnStack in flash
SpSubEnd: