luminous Home   Feature   Space   Science   Atheism   Paranonsense   Technology   Offsite   About   Search
Home : Technology : MMC to Serial Adapter Firmware

Download all of the Eagle schematics, AVR Studio project files, and documentation as a zip file (172KB).


MMC to Serial Adapter Firmware


Main Code

The main code is housed in file MMCSERIAL.ASM, and take care of initial setup, interface with the card, and parsing of the command sequences.

;-----------------------------------------------------------------
;
; Name:         MMCSerial.asm
; Title:        Adapter to communicate via RS232 with Multimedia Card.
; Version:      1.0
; Last updated: 2001.08.26
; Target:       AT90S2313
;
;-----------------------------------------------------------------
;
; DESCRIPTION
;
; Application to interface a Multimedia Card with RS232.
;
; The card is operated in SPI mode, with the signals being attached
; to pins on Port B as follows:
;
; Pin B0 is used as the chip select for the card.
; Pin B1 is the data output to the card (=MOSI)
; Pin B2 is the data input from the card (=MISO)
; Pin B3 is the clock to the card (=SCK)
;
; Three status LEDs are wired to pins PB4, PB5, PB6.
; 
; Communication with the host is via RS232 at 115,200 baud (8N1).
; The RTS and CTS lines are attached to pins D2 and D3 respectively,
; and the transmitter enable pin to pin D4.
;
; Clock speed is 3.6864 MHz, as required to get an accurate baud
; rate of 115200 baud.
;
;-----------------------------------------------------------------

Include the standard definitions for the AT90S2313, as supplied with AVR Studio.

.nolist
.include "2313def.inc"
.list

The registers are assigned particular roles. rRETURN is a general purpose return value from a subroutine call. rLENGTHn are storage for a 32 bit value, usually used a length as a number of bytes, although once as an address. rSCRATCHn and rISCRATCHn are just general scratch registers, for use in the main loop and the interrupt routines respectively. I have saved the bother of pushing the status register during an interrupt routine by dedicating a register to that task, rISREGSAVE. rTEMPn and rITEMPn are high registers which are used for temporary values, differing from the scratch registers in that they can operate with immediate values. rPARAMn are registers used mainly for passing parameters to subroutines, and rLOOPI and rLOOPJ are two loop counters. The X, Y and Z registers retain their usual designations.

;-----------------------------------------------------------------
;
; General purpose registers

.def    rRETURN         =r0     ; return value from functions

.def    rZERO           =r1

.def    rLENGTH0        =r2     ; 32 bit counter
.def    rLENGTH1        =r3
.def    rLENGTH2        =r4
.def    rLENGTH3        =r5

.def    rSCRATCH        =r7
.def    rSCRATCH2       =r8
.def    rSCRATCH3       =r9
.def    rSCRATCH4       =r10

.def    rISCRATCH       =r11    ; for use in interrupt routines
.def    rISCRATCH2      =r12    ; ditto
.def    rISCRATCH3      =r13    ; ditto
.def    rISCRATCH4      =r14    ; ditto

.def    rISREGSAVE      =r15    ; sreg saved here in interrupts

.def    rTEMP           =r16
.def    rTEMP2          =r17

.def    rITEMP          =r18    ; for use in interrupt routines
.def    rITEMP2         =r19    ; ditto

.def    rPARAM1         =r20
.def    rPARAM2         =r21

.def    rLOOPI          =r22    ; loop counters
.def    rLOOPJ          =r23

;-----------------------------------------------------------------
;
; Equates

;-----------------------------------------------------------------
;
; IO Port Bits
;
; Port B

These are the bit designations for port B, which is connected to the signals from the card and the status LEDs. CS is the card select line, MOSI stands for Master Out Slave In, which is an output from the microcontroller and is connected to the appropriate input on the card. MISO is the opposite, with data flowing into the microcontroller on this line. SCK is the serial clock to the card. Each clock transfers one bit from the microcontroller to the card along the MOSI line, and one bit from the card to the microcontroller on the MISO line. The three LEDs (LEDAUX was present only during debugging) are on when a logic '0' is written to them. These bit numbers are used in turn to define mask values which have a '1' bit in the appropriate position. All of these are outputs with the exception of MISO, which is an input.

.equ    bCS             =0
.equ    bMOSI           =1
.equ    bMISO           =2
.equ    bSCK            =3
.equ    bLEDWRITE       =4              ; Writing/erasing
.equ    bLEDREAD        =5              ; Reading
.equ    bLEDACTIVE      =6              ; Active and awake
.equ    bLEDAUX         =7              ; Aux (debug)

.equ    mCS             =(1<<bCS)
.equ    mMOSI           =(1<<bMOSI)
.equ    mMISO           =(1<<bMISO)
.equ    mSCK            =(1<<bSCK)
.equ    mLEDWRITE       =(1<<bLEDWRITE)
.equ    mLEDREAD        =(1<<bLEDREAD)
.equ    mLEDACTIVE      =(1<<bLEDACTIVE)
.equ    mLEDAUX         =(1<<bLEDAUX)

;
; Port D
;

Again, these are bit designations, this time for port D. RTS and CTS are connected to the serial buffer chip, and SERIALENABLE can turn the serial transmitters off, for use during sleep mode to save power.

.equ    bRTS            =2
.equ    bCTS            =3
.equ    bSERIALENABLE   =4
.equ    mRTS            =(1<<bRTS)
.equ    mCTS            =(1<<bCTS)
.equ    mSERIALENABLE   =(1<<bSERIALENABLE)

;
; Common response values
;

These are symbolic constants for the common return values, as outlined in the protocol specification.

.equ    Response_OK     =0x10
.equ    Response_Fail   =0x11
.equ    Response_Unk    =0x12
.equ    Response_Awake  =0x13
.equ    Response_Wait   =0x14
.equ    Response_Data   =0x15

;-----------------------------------------------------------------
;
; Timings

This is the clock frequency. It is an odd value, but this guarantees an accurate UART baud rate of 115200 baud.

.equ    CLOCK           =3686400        ;3.6864 MHz

;-----------------------------------------------------------------
;
; Macros

The SCKDELAY macro was originally designed to allow fine control of the timings of the clock pulses which are sent to the card to regulate data transfer. It turns out that the card can read and write data much faster than the microcontroller, so SCKDELAY resolves down to nothing, usually. The debugging version is very slow - slow enough to let a human read the stream of bits going to and from the card.

; This is a slow version of SCKDELAY for debugging purposes
; Each clock cycle lasts around 130ms
;
;.macro SCKDELAY
;       in      rITEMP2,PORTB
;       andi    rITEMP2,0xF0
;
;       clr     XL
;       clr     XH
;
;       in      rITEMP,PINB
;       com     rITEMP
;       andi    rITEMP,0xF0
;       swap    rITEMP
;       or      rITEMP,rITEMP2
;
;       out     PORTB,rITEMP
;
;       rcall   WaitMicro
;.endmacro

.macro  SCKDELAY
        ; No delay required - the card can run faster than us!
.endmacro

;-----------------------------------------------------------------
;
; Variables

.dseg

The Debug variable is used to store if we are in debug mode or not. In debug mode, communication with the card is done in ASCII hexadecimal rather than with raw bytes. This makes it possible to communicate with the card via a standard ASCII terminal program, for debugging purposes (of course).

Debug:  .byte   1               ; Bit 0:0 = raw, 1 = human-readable debug mode (hex)

;-----------------------------------------------------------------
;
; Interrupt service vectors

Vector table has entries for reset, UART receive, and both external interrupts. The external interrupt routines just return immediately, since their sole purpose is to break the microcontroller out of its power-down sleep mode.
.cseg
.org 0
        rjmp    Reset           ; Reset vector
        
.org INT0addr
        reti                    ; Used to wake up the MCU

.org INT1addr
        reti                    ; Not used, but better to be safe than sorry!

.org URXCaddr
        rjmp    uart_receive
        

;-----------------------------------------------------------------
;
; Start of actual code.
;

.org STDORG

;-----------------------------------------------------------------
;
; Include modules

.include "uart.asm"

;-----------------------------------------------------------------
;
; Reset vector - generic system.

Common reset tasks. Set up the stack, the MCU control register, and set debug mode off.

Reset:
        ldi     rTEMP,RAMEND-1          ; Stack setup
        out     SPL,rTEMP

        ldi     rTEMP,(1<<SE)           ; Sleep enable, idle mode on sleep, low levels to wake
        out     MCUCR,rTEMP

        clr     rZERO                   ; A handy 0
        sts     Debug,rZERO             ; Set debug mode to 0

We continue by setting up the initial state of the ports.

        ;
        ; Set up data directions etc. on ports. Unused pins are
        ; set to input with pullup resistors enabled to prevent
        ; floating.
        ;
        
        ldi     rTEMP,0xFF
        out     PORTD,rTEMP

        ldi     rTEMP,0xFF-(mSCK)       ; SCK low to start
        out     PORTB,rTEMP
        
        ldi     rTEMP,0xFF-(mMISO)      ; MISO is an input
        out     DDRB,rTEMP
        
        ldi     rTEMP,(mCTS|mSERIALENABLE)      ; Serial enabled to start with
        out     DDRD,rTEMP
        
        ;
        ; Reset uart
        ;
        
        rcall   uart_reset

        ;
        ; Enable interrupts and off we go...
        ;
        
        sei
                
        ;
        ; Initialize the card
        ;
        
        rcall   SPIInitialize

        tst     rRETURN
        breq    Main_Success    ; OK!

If the initialization failed, we should get here, which just flashes the Activity LED on and off forever.

Main_FatalError:
        cbi     PORTB,bLEDACTIVE
        rcall   WaitMicro
        sbi     PORTB,bLEDACTIVE
        rcall   WaitMicro
        rjmp    Main_FatalError
        
The adapter will start up in power-off mode, unless the RTS pin is already asserted (logic 0), in which case we go straight in, lighting the Activity LED on the way, and asserting CTS to allow the host to send us data.

        ;
        ; OK - Jump into main loop via power off mode, unless
        ; RTS is asserted already in which case, off we go.
        ;

Main_Success:
        sbic    PIND,bRTS               ; RTS asserted?
        rjmp    Main_PowerOff           ; No... go straight to power off
        
        cbi     PORTD,bCTS              ; OK to send commands
        cbi     PORTB,bLEDACTIVE        ; Active LED on

;-----------------------------------------------------------------
;
; Main loop.

At this point, we are expecting a command byte. If we are in debug mode, a cr/lf pair is sent, to advance the terminal by one line, and a '-' character to act as a command prompt. If debug mode is off, we just start listening.

Main_Loop:
        lds     rRETURN,Debug
        sbrs    rRETURN,0
        rjmp    Main_WaitCommand

        ldi     rPARAM1,0x0D            ; In debug mode, output a command prompt
        rcall   uart_writechar
        ldi     rPARAM1,0x0A
        rcall   uart_writechar
        ldi     rPARAM1,'-'
        rcall   uart_writechar

The sleep command here is in a mode where the UART is still active and will terminate the sleep if a character is received. It helps keep the power consumption fairly low even though we are still awake.

Main_WaitCommand:
        sleep                           ; wait for interrupt (from UART?)
        
        rcall   uart_charready          ; a character?
        breq    Main_WaitCommand        ; no... go back to sleep
        
        rcall   ReceiveData             ; read it and identify command
        
        mov     rPARAM1,rRETURN

Here we have to find out which command byte we received. This could have been done with a jump table or some other clever means, but for the limited number of possibilities, a simple compare-branch-compare-branch structure suffices.

        ;
        ; Switch on different valid values of command byte
        ;
        
        cpi     rPARAM1,'!'
        brne    PC+2
        rjmp    CommandDebug
        
        cpi     rPARAM1,'?'
        brne    PC+2
        rjmp    CommandStatus
        
        cpi     rPARAM1,0
        brne    PC+2
        rjmp    CommandNop
        
        cpi     rPARAM1,'S'
        brne    PC+2
        rjmp    CommandSleep

        cpi     rPARAM1,'I'
        brne    PC+2
        rjmp    CommandIdentify
        
        cpi     rPARAM1,'C'
        brne    PC+2
        rjmp    CommandIdentifyCard

        cpi     rPARAM1,'R'
        brne    PC+2
        rjmp    CommandRead
        
        cpi     rPARAM1,'W'
        brne    PC+2
        rjmp    CommandWrite
        
        cpi     rPARAM1,'E'
        brne    PC+2
        rjmp    CommandErase
        
If we get here, we haven't matched a command byte so send an "Unknown command" response back to the host. This is followed by some other common responses used at the end of commands.

        ;
        ; Drop through to...
        ;

Main_ErrorUnk:
        ldi     rPARAM1,Response_Unk
        rjmp    Main_Response

Main_ErrorFail:
        ldi     rPARAM1,Response_Fail
        rjmp    Main_Response

Main_ResponseOK:
        ldi     rPARAM1,Response_OK

Main_Response:
        rcall   SendData
        
        ; Turn off any transientLEDs
        
        sbi     PORTB,bLEDREAD
        sbi     PORTB,bLEDWRITE
        
        rjmp    Main_Loop

These are the routines for the actual commands. Most of them are fairly straightforward so I'll let the comments in the code speak for themselves.

;-----------------------------------------------------------------
;
; CommandDebug
; 21 mm 20
; Sets debug mode mm. Debug mode 01 = hex, debug mode 00 = raw.

CommandDebug:
        rcall   ReceiveData
        rcall   ReceiveSpace
        brne    Main_ErrorUnk

        sts     Debug,rRETURN
        
        rjmp    Main_ResponseOK

;-----------------------------------------------------------------
;
; CommandNop
; 00
; Does nothing, returns OK

CommandNop:
        rjmp    Main_ResponseOK
        
Here, we want to put everything into a minimum power mode, whilst retaining the ability to wake everything up again by asserting the RTS line. The microcontroller's sleep mode is switched so that the sleep command powers virtually everything down - clock, uart, etc. The only thing that will wake it is an internal timeout (also disabled) or a low level on bit 2 of Port B. Surprise, surprise, this is what RTS is connected to. The RS232 chip is also sent into power down mode, and the chip select on the card id turned off to put that to sleep. All the LEDs are turned off too, of course. Total power consumption should be of the order of 50uA for the card, 1uA for the UART receivers, and 1uA for the microcontroller.

;-----------------------------------------------------------------
;
; CommandSleep
; 53 20
; Sends card to sleep, awaiting a new command.
; Returns [Wait], waits until RTS is de-asserted, sends [OK], waits
; a further 65ms and then powers down. 
; If RTS is not deasserted within 65ms or so, powerdown is cancelled
; and it sends [Fail].

CommandSleep:
        rcall   ReceiveSpace
        brne    Main_ErrorUnk

        ldi     rPARAM1,Response_Wait
        rcall   SendData

        clr     XL
        clr     XH
        
CommandSleep_Wait:
        sbic    PIND,bRTS
        rjmp    CommandSleep_PowerOff

        sbiw    XL,1
        brne    CommandSleep_Wait
        
        rjmp    Main_ErrorFail  

CommandSleep_PowerOff:
        ldi     rPARAM1,Response_OK
        rcall   SendData                ; Send OK when RTS deasserted
        
        clr     XL
        clr     XH
        rcall   WaitMicro               ; Wait for 65ms

Main_PowerOff:
        sbi     PORTD,bCTS              ; Deassert CTS

        sbi     PORTB,bLEDACTIVE        ; Light off...
        cbi     PORTD,bSERIALENABLE     ; Disable serial transmission
        sbi     PORTB,bCS               ; Deassert CS

        ldi     rTEMP,(1<<SE)|(1<<SM)   ; Sleep enable, power down mode on sleep, low levels to wake
        out     MCUCR,rTEMP

        ldi     rTEMP,(1<<INT0)         ; Enable int0
        out     GIMSK,rTEMP

        sleep
        
        out     GIMSK,rZERO             ; Disable it again

        ldi     rTEMP,(1<<SE)           ; Sleep enable, idle mode on sleep, low levels to wake
        out     MCUCR,rTEMP
        
        cbi     PORTB,bCS               ; Assert CS
        sbi     PORTD,bSERIALENABLE     ; Re-enable serial transmission
        cbi     PORTB,bLEDACTIVE        ; Light on again

        cbi     PORTD,bCTS              ; Assert CTS

        rjmp    Main_Loop
        
;-----------------------------------------------------------------
;
; CommandIdentify
; 49 20
; Identifies hardware and software versions
; Returns [Data] 'P' 'F' HH SS [OK]
; HH is the hardware version, SS is the software version.

CommandIdentify:
        rcall   ReceiveSpace
        brne    Main_ErrorUnk

        ldi     rPARAM1,Response_Data
        rcall   SendData
        
        ldi     rPARAM1,'P'
        rcall   SendData

        ldi     rPARAM1,'F'
        rcall   SendData

        ldi     rPARAM1,'0'
        rcall   SendData

        ldi     rPARAM1,'0'
        rcall   SendData
        
        rjmp    Main_ResponseOK
        
This is the first command which actually talks to the card to do its job, so let's take a closer look. A command is issued, and a response is returned from the card, which indicates that the command was received successfully. Then, a data token (0xFE) and the data bytes come back from the card also. These data bytes are sent immediately out to the host, eliminating the need for a buffer on the microcontroller. In this case, it would be possible to buffer the 16 bytes and do some translation on them, but for more general data read operations, we can be dealing with up to 512 bytes, which is four times the amount of RAM available in total.

;-----------------------------------------------------------------
;
; CommandIdentifyCard
; 43 cc 20
; Reads identification information for card. If cc bit 0=0, returns
; CSD, else CID.
; Returns [Wait][Data] ...16 bytes... [OK]
; or      [Wait][Fail]

CommandIdentifyCard:
        rcall   ReceiveData
        rcall   ReceiveSpace
        breq    PC+2
        rjmp    Main_ErrorUnk
        
        push    rRETURN

        ldi     rPARAM1,Response_Wait
        rcall   SendData

        pop     rRETURN
        
        ldi     rPARAM1,0x0A            ; CMD10 = Send CID (Card ID)
        sbrs    rRETURN,0
        dec     rPARAM1                 ; CMD09 = Send CSD (Card Hardware Description)
        
        rcall   SPISendCommand  
        
        rcall   SPIReadResponse
        tst     rRETURN
        
        breq    PC+2
        rjmp    Main_ErrorFail
        
        rcall   SPIReadResponse
        mov     rTEMP,rRETURN
        cpi     rTEMP,0xFE      ; Data token?
        breq    PC+2
        rjmp    Main_ErrorFail

        ldi     rPARAM1,Response_Data
        rcall   SendData

        ldi     rLOOPI,16

CommandIdentifyCard_Loop:
        rcall   SPIReadByte

        mov     rPARAM1,rRETURN
        rcall   SendData

        dec     rLOOPI
        brne    CommandIdentifyCard_Loop

        rjmp    Main_ResponseOK

The read command actually translates into two commands to the card - set the number of bytes to read, and then read them. Again, we are sending the bytes back to the host as soon as we can.

;-----------------------------------------------------------------
;
; CommandRead
; 52 n3 n2 n1 n0 a3 a2 a1 a0 20
; Reads nn bytes from address aa.
; Returns [Wait][Data]...nn bytes of data.... crcH crcL [OK]
; or      [Wait][Fail]
        
CommandRead:
        cbi     PORTB,bLEDREAD          ; Light "read" LED

        rcall   ReceiveData32           ; Read length (32 bits)
        
        mov     rLENGTH0,XL             ; and save
        mov     rLENGTH1,XH
        mov     rLENGTH2,YL
        mov     rLENGTH3,YH
        
        ldi     rPARAM1,0x10            ; CMD16 = Set block length
        rcall   SPISendCommand  
        
        rcall   SPIReadResponse

        push    rRETURN

        rcall   ReceiveData32           ; Read address
        
        rcall   ReceiveSpace
        pop     rTEMP
        breq    PC+2
        rjmp    Main_ErrorUnk           ; no space - unknown command
        
        cpi     rTEMP,0                 ; OK?
        breq    PC+2
        rjmp    Main_ErrorFail          ; block length failed - error
        
        ldi     rPARAM1,Response_Wait
        rcall   SendData

        ldi     rPARAM1,0x11            ; CMD17 = read block
        rcall   SPISendCommand  

        rcall   SPIReadResponse
        tst     rRETURN
        breq    PC+2
        rjmp    Main_ErrorFail
        
        rcall   SPIReadResponse
        mov     rTEMP,rRETURN
        cpi     rTEMP,0xFE              ; Data token?
        breq    PC+2
        rjmp    Main_ErrorFail

        ldi     rPARAM1,Response_Data
        rcall   SendData

        rjmp    CommandRead_LoopEnd

CommandRead_Loop:
        rcall   SPIReadByte

        mov     rPARAM1,rRETURN
        rcall   SendData

        ldi     rTEMP,1
        sub     rLENGTH0,rTEMP
        sbc     rLENGTH1,rZERO
        sbc     rLENGTH2,rZERO
        sbc     rLENGTH3,rZERO
                
CommandRead_LoopEnd:

        mov     rSCRATCH,rLENGTH0
        or      rSCRATCH,rLENGTH1
        or      rSCRATCH,rLENGTH2
        or      rSCRATCH,rLENGTH3
        brne    CommandRead_Loop

        ;
        ; Now send CRC and OK.
        ;

        rcall   SPIReadByte
        mov     rPARAM1,rRETURN
        rcall   SendData

        rcall   SPIReadByte
        mov     rPARAM1,rRETURN
        rcall   SendData
        
        rjmp    Main_ResponseOK
                
;-----------------------------------------------------------------
;
; CommandWrite
; 57 n3 n2 n1 n0 a3 a2 a1 a0 d0 d1 d2 ... dn c1 c2 20
; Write data to card
; Writes nn bytes to address aa. Valid values of nn are determined
; by the card capabilities, as are valid values of aa.
;
; Returns [Fail] After receipt of nn and aa if invalid.
; or      [Wait] [OK]
; or      [Wait] [Fail]

CommandWrite:
        cbi     PORTB,bLEDWRITE         ; Light "write" LED

        rcall   ReceiveData32           ; Read length (32 bits)
        
        mov     rLENGTH0,XL             ; and save
        mov     rLENGTH1,XH
        mov     rLENGTH2,YL
        mov     rLENGTH3,YH
        
        ldi     rPARAM1,0x10            ; CMD16 = Set block length
        rcall   SPISendCommand  
        
        rcall   SPIReadResponse

        push    rRETURN

        rcall   ReceiveData32           ; Read address
        
        pop     rTEMP
        
        cpi     rTEMP,0                 ; OK?
        breq    PC+2
        rjmp    Main_ErrorFail          ; block length failed - error
        
        ldi     rPARAM1,0x18            ; CMD24 = write block
        rcall   SPISendCommand  

        rcall   SPIReadResponse
        tst     rRETURN
        breq    PC+2
        rjmp    Main_ErrorFail          ; Failed if invalid
        
        ldi     rPARAM1,0xFE            ; Data token first
        rcall   SPISendByte

        rjmp    CommandWrite_LoopEnd

CommandWrite_Loop:
        rcall   ReceiveData
        mov     rPARAM1,rRETURN
        rcall   SPISendByte

        ldi     rTEMP,1
        sub     rLENGTH0,rTEMP
        sbc     rLENGTH1,rZERO
        sbc     rLENGTH2,rZERO
        sbc     rLENGTH3,rZERO
                
CommandWrite_LoopEnd:

        mov     rSCRATCH,rLENGTH0
        or      rSCRATCH,rLENGTH1
        or      rSCRATCH,rLENGTH2
        or      rSCRATCH,rLENGTH3
        brne    CommandWrite_Loop

        ;
        ; Now send CRC and OK.
        ;

        rcall   ReceiveData
        mov     rPARAM1,rRETURN
        rcall   SPISendByte

        rcall   ReceiveData
        mov     rPARAM1,rRETURN
        rcall   SPISendByte

        ldi     rPARAM1,Response_Wait
        rcall   SendData
        
        ; Now read data token as response
        
        rcall   SPIReadResponse
        mov     rTEMP,rRETURN
        andi    rTEMP,0x1F              ; Mask out don't care bits
        cpi     rTEMP,0x05              ; 0x05 = OK!
        breq    PC+2
        rjmp    Main_ErrorFail          ; else fail (CRC)

CommandWrite_Wait:
        rcall   SPIReadByte             ; Read busy signal
        tst     rRETURN                 ; stays 0 until done
        breq    CommandWrite_Wait
        
        rcall   ReceiveSpace            ; Now we expect a space to finish off
        breq    PC+2
        rjmp    Main_ErrorUnk

        rjmp    Main_ResponseOK
        
;-----------------------------------------------------------------
;
; CommandErase
; 45 sg a3 a2 a1 a0 b3 b2 b1 b0 20
; Erase range of sectors or groups. Bit 0 of sg is 0 if sectors are
; to be erased, 1 if groups. The sectors are addressed using addresses
; aa and bb rather than sector numbers. All sectors within this
; address range will be erased. The address aa must be less than or
; equal to bb. If addressing individual sectors, addresses aa and bb
; must fall within the same erase group of 32 sectors.
; Returns [Wait] [OK]
; or      [Wait] [Fail]

CommandErase:
        cbi     PORTB,bLEDWRITE         ; Light "write" LED

        rcall   ReceiveData             ; Read sg flag
        clr     ZL                      ; ZL is offset for commands
        sbrc    rRETURN,0               ; if rReturn:0=0, skip
        ldi     ZL,3                    ; Offset between group and sector cmds = 3

        rcall   ReceiveData32           ; Read address AA
        
        mov     rLENGTH0,XL             ; and save
        mov     rLENGTH1,XH
        mov     rLENGTH2,YL
        mov     rLENGTH3,YH
        
        rcall   ReceiveData32           ; Read address BB
        
        rcall   ReceiveSpace
        breq    PC+2
        rjmp    Main_ErrorUnk

        ldi     rPARAM1,Response_Wait   ; Send wait
        rcall   SendData
        
        cp      XL,rLENGTH0
        cpc     XH,rLENGTH1
        cpc     YL,rLENGTH2
        cpc     YH,rLENGTH3             ; check if greater
        
        brcc    PC+2
        rjmp    Main_ErrorFail

        push    XL
        push    XH
        push    YL
        push    YH
        
        mov     XL,rLENGTH0             ; Get start sector
        mov     XH,rLENGTH1
        mov     YL,rLENGTH2
        mov     YH,rLENGTH3

        ldi     rPARAM1,0x20            ; CMD32 = Set Start Sector
        add     rPARAM1,ZL              ; CMD35 = Set Start Group
        rcall   SPISendCommand
        
        pop     YH
        pop     YL
        pop     XH
        pop     XL

        rcall   SPIReadResponse
        tst     rRETURN
        breq    PC+2
        rjmp    Main_ErrorFail          ; Failed if invalid

        ldi     rPARAM1,0x21            ; CMD33 = Set End Sector
        add     rPARAM1,ZL              ; CMD36 = Set End Group
        rcall   SPISendCommand

        rcall   SPIReadResponse
        tst     rRETURN
        breq    PC+2
        rjmp    Main_ErrorFail          ; Failed if invalid

        ldi     rPARAM1,0x26            ; CMD38 = Erase tagged sectors or groups
        rcall   SPISendCommand

        rcall   SPIReadResponse
        tst     rRETURN
        breq    PC+2
        rjmp    Main_ErrorFail          ; Failed if invalid

        rjmp    Main_ResponseOK
        
;-----------------------------------------------------------------
;
; CommandStatus
; 3F 20
; Gets extended status information in case of error.
; Returns [Data] s1 s2 [OK]

CommandStatus:
        rcall   ReceiveSpace
        breq    PC+2
        rjmp    Main_ErrorUnk

        ldi     rPARAM1,Response_Data
        rcall   SendData

        ldi     rPARAM1,0x0D            ; CMD13 = Get status
        rcall   SPISendCommand  
        
        rcall   SPIReadResponse
        mov     rPARAM1,rRETURN
        rcall   SendData
        
        rcall   SPIReadByte
        mov     rPARAM1,rRETURN
        rcall   SendData
        
        rjmp    Main_ResponseOK
        
These are the subroutines used by all of the above.

;-----------------------------------------------------------------
;
; ReceiveData
; Receives data from UART in either raw or hex form

ReceiveData:
        lds     rRETURN,Debug

        sbrs    rRETURN,0
        rjmp    uart_readcharwait

        rcall   uart_readcharwait
        mov     rTEMP,rRETURN
        subi    rTEMP,'0'
        cpi     rTEMP,0x0A
        brcs    ReceiveData_Hex1
        subi    rTEMP,7
ReceiveData_Hex1:
        andi    rTEMP,0x0F
        swap    rTEMP
        
        rcall   uart_readcharwait
        mov     rTEMP2,rRETURN
        subi    rTEMP2,'0'
        cpi     rTEMP2,0x0A
        brcs    ReceiveData_Hex2
        subi    rTEMP2,7
ReceiveData_Hex2:
        andi    rTEMP2,0x0F
        or      rTEMP,rTEMP2
        mov     rRETURN,rTEMP

        ldi     rPARAM1,'>'
        rjmp    uart_writechar

The ReceiveSpace command reads a byte of data from the UART and checks it's a space. The odd wording of the comment indicates that the return is in the Z flag, which can be checked with a breq or brne instruction.

;-----------------------------------------------------------------
;
; ReceiveSpace
; Receives a character anc checks it's a space. Returns eq if it is,
; ne if it isn't.

ReceiveSpace:
        push    rRETURN
        rcall   ReceiveData
        mov     rTEMP,rRETURN
        pop     rRETURN
        cpi     rTEMP,' '
        ret

Since there are several places where we read 32-bit values, this routine does just that.

;-----------------------------------------------------------------
;
; ReceiveData32
; Receives a 32 bit data value into YH:YL:XH:XL

ReceiveData32:  
        rcall   ReceiveData
        mov     YH,rRETURN
        rcall   ReceiveData
        mov     YL,rRETURN
        rcall   ReceiveData
        mov     XH,rRETURN
        rcall   ReceiveData
        mov     XL,rRETURN      ; Y:X is address
        ret

;-----------------------------------------------------------------
;
; SendData
; Sends data to the UART in either raw or hex form

SendData:
        lds     rRETURN,Debug

        sbrs    rRETURN,0
        rjmp    uart_writechar

        rcall   uart_writehex
        ldi     rPARAM1,' '
        rjmp    uart_writechar

WaitMicro will actually wait for 4*X clock cycles, which is actually 1.085us per loop at our 3.686MHz clock speed. If you supply X=0x0000 to start with, we will wait for 4*65536 clock cycles, which is around 71ms.

;-----------------------------------------------------------------
;
; WaitMicro
; Wait approximately X microseconds, in a tight loop.

WaitMicro:                      ; 4 cycles
        nop                     ; per loop
        dec     XL              ; at 3.686 MHz
        brne    WaitMicro       ; = about 1 uS per loop

        dec     XH
        brne    WaitMicro

        ret

;-----------------------------------------------------------------
;
; SPI/Flash routines

The card initialisation sequence is something I must thank Peter and Dan for help with - the data sheets aren't terribly helpful, but a quick glance at their code showed where I was going wrong. I was getting the reset command right, but had assumed that that was enough. Not so, an initialize command is also required.

;-----------------------------------------------------------------
;
; SPIInitialize
; Initialise card and place into SPI mode.
; rRETURN is 0 if successful, otherwise it is an error code.

SPIInitialize:
        sbi     PORTB,bCS       ; Ensure chip select high to start with
        
        ; Send 80 "high" bits to get things started
        
        ldi     rPARAM1,0xFF
        rcall   SPISendByte
        rcall   SPISendByte
        rcall   SPISendByte
        rcall   SPISendByte
        rcall   SPISendByte
        rcall   SPISendByte
        rcall   SPISendByte
        rcall   SPISendByte
        rcall   SPISendByte
        rcall   SPISendByte

        ; Now send CMD0 - go to idle state

        ldi     rPARAM1,0
        ldi     XL,0
        ldi     XH,0
        ldi     YL,0
        ldi     YH,0
        rcall   SPISendCommand
        
        rcall   SPIReadResponse
        ldi     rTEMP2,1
        ldi     rTEMP,1
        cp      rRETURN,rTEMP
        brne    SPIInitialize_Error     ; 1 expected.

        ; Now send CMD1 - initialise, and retry!
        
        clr     rLOOPJ                  ; up to 256 times
SPIInitialize_Loop:             
        ldi     rPARAM1,1
        ldi     XL,0
        ldi     XH,0
        ldi     YL,0
        ldi     YH,0
        rcall   SPISendCommand
        
        rcall   SPIReadResponse
        tst     rRETURN
        breq    SPIInitialize_OK
        
        dec     rLOOPJ
        brne    SPIInitialize_Loop

        breq    SPIInitialize_OK        ; Error code in rRETURN already

SPIInitialize_Error:
        mov     rRETURN,rTEMP2
SPIInitialize_OK:
        ret

This originally checked for specific values, but on reading further it became obvious that the MISO line is held high while the card is busy, which means that a 0xFF byte is read in this case. Any other byte indicates an actual return value.

;-----------------------------------------------------------------
;
; SPIReadResponse
; Read a response byte from the card

SPIReadResponse:
        clr     rSCRATCH                ;timeout (256 tries)

SPIReadResponse_Wait:
        rcall   SPIReadByte             ; and read into rRETURN

        mov     rTEMP,rRETURN           ; 0xFF indicates unready...
        cpi     rTEMP,0xFF
        brne    SPIReadResponse_End     ; ...so wait for anything else

;       tst     rRETURN                 ; 0... (OK)
;       breq    SPIReadResponse_End
;
;       mov     rTEMP,rRETURN
;       cpi     rTEMP,1                 ; or 1... (Idle)
;       breq    SPIReadResponse_End
;       
;       cpi     rTEMP,0xFE              ; or 0xFE... (Data token)
;       breq    SPIReadResponse_End

        dec     rSCRATCH                ; else go round again
        brne    SPIReadResponse_Wait    ; until time out

        ;
        ; Error code goes in here
        ;
        
SPIReadResponse_End:
        ret

A command to the card consists of 6 bytes, sent here. The 6th byte is a CRC check, but this is only checked on the first command, so we can hard code it, which saves using a register to pass it in all the time.

;-----------------------------------------------------------------
;
; SPISendCommand
; Send command. Command is in rPARAM1, Parameter is in X (low) and Y (high).
; CRC is a constant 0x95 - this is the CRC for the initial CMD0 which gets us
; into SPI mode, from then on the CRC is ignored.

SPISendCommand:
        sbi     PORTB,bCS       ; CS high during flushing

        push    rPARAM1
        ldi     rPARAM1,0xff
        rcall   SPISendByte

        cbi     PORTB,bCS       ; CS must be low to enable SPI mode
        
        pop     rPARAM1
        ori     rPARAM1,0x40
        rcall   SPISendByte
        
        mov     rPARAM1,YH
        rcall   SPISendByte
        mov     rPARAM1,YL
        rcall   SPISendByte
        mov     rPARAM1,XH
        rcall   SPISendByte
        mov     rPARAM1,XL
        rcall   SPISendByte
        
        ldi     rPARAM1,0x95    ; CRC for init command, ignored otherwise
        
        ;
        ; ... and drop through to SPISendByte
        ;


This was easy to write for output, but required some experimentation to get right for input. Peter and Dan's MP3 project used a 8515 microcontroller, which has a hardware SPI implementation, so no clues were forthcoming there. This was where the slow SCKDELAY really helped debugging - easy to implement and much cheaper than a digital storage oscilloscope... Bytes are read and written high bit first, accompanied by transitions on the SCK pin. The slightly odd effect of reading the bit from PINB before we do the SCK is explained by the fact that this is set up by the last SCK of the previously transmitted byte. This wasn't what I was expecting, as the timing diagrams aren't very clear on this point in the MMC Product Manual.

;-----------------------------------------------------------------
;
; SPISendByte
; Sends a byte from rPARAM1 in SPI mode to MOSI pin. At the same
; time, a byte is read from the MISO pin and returned in rRETURN.

SPISendByte:
        push    rLOOPI
        push    rPARAM1

        clr     rRETURN
        
        ldi     rLOOPI,8
SPISendByte_Loop:
        add     rRETURN,rRETURN
        add     rPARAM1,rPARAM1
        brcs    SPISendByte_1

        cbi     PORTB,bMOSI
        rjmp    SPISendByte_Clock

SPISendByte_1:
        sbi     PORTB,bMOSI

SPISendByte_Clock:      
        in      rSCRATCH,PINB

        sbi     PORTB,bSCK
        SCKDELAY

        cbi     PORTB,bSCK
        SCKDELAY
        
        sbrc    rSCRATCH,bMISO
        inc     rRETURN

        dec     rLOOPI
        brne    SPISendByte_Loop
        
        pop     rPARAM1
        pop     rLOOPI
        
        ret

To read, it is recommended that you keep MOSI high, so this is what we do here by sending a 0xFF byte.

;-----------------------------------------------------------------
;
; SPIReadByte
; Just sends an 0xFF, returns with the data as usual.

SPIReadByte:
        ldi     rPARAM1,0xFF
        rjmp    SPISendByte

;-----------------------------------------------------------------

UART Code

The file UART.ASM controls the functions of the UART, including buffering inputs on interrupt and transmitting outputs. This may be familiar from the Robot project - the code has been reused from there, although some routines were not used and have been commented out in the code (they are omitted completely here for brevity).

;-----------------------------------------------------------------
;
; Name:         uart.asm
; Title:        AVR Uart services
; Version:      1.0
; Last updated: 2001.02.03
; Target:       AT90Sxxxx (All devices)
;
;-----------------------------------------------------------------
;
; DESCRIPTION
;
; UART service which uses interrupts to read characters
; from the serial port and buffer them, but which handles trans-
; mission synchronously, without interrupts.
; 
;-----------------------------------------------------------------

.dseg

These declarations define the buffer which will be used for storing incoming characters from the UART. uart_rhead and uart_rtail are the adresses in the buffer of the head (where a new character will be written by the UART) and tail (where a character will be read from by the program) of a circular buffer. The number of characters currently in the buffer is uart_rsize, and any errors are reported with a non-0 value in uart_rerror.

uart_rhead:     .byte   1
uart_rtail:     .byte   1
uart_rsize:     .byte   1
uart_rerror:    .byte   1

The buffer itself needs to be a specific size and at a specific position.

.equ    UART_BUFSIZE    =$40    ; must be power of 2
.equ    UART_BUFFER     =$80    ; must be on a UART_BUFSIZE boundary

And here's the standard calculation of baud rate register values. The otherwise apparently odd frequency of crystal we selected is explained when you do this calculation - UART_BAUDK comes out as exactly 1, with no rounding error.

.equ    UART_BAUDRATE   =115200
.equ    UART_BAUDK      =(CLOCKRATE/(16*UART_BAUDRATE))-1

;-----------------------------------------------------------------

.cseg

;-----------------------------------------------------------------
;
; Reset vector - setup

The reset routine just sets up the head and tail, and enables the UART for interrupt-driven receive and polled transmit.

uart_reset:
          ldi       rTEMP,UART_BUFFER
          sts       uart_rhead,rTEMP
          sts       uart_rtail,rTEMP
          
          clr       rTEMP
          sts       uart_rsize,rTEMP
          sts       uart_rerror,rTEMP

          ldi       rTEMP,UART_BAUDK
          out       UBRR,rTEMP          ; load baudrate
          
          ldi       rTEMP,(1<<TXEN)|(1<<RXEN)|(1<<RXCIE)    ; enable transmit/receive, enable rx int.
          out       UCR,rTEMP

          ret

;-----------------------------------------------------------------
;
; Receive vector - buffer input

This is the receive interrupt vector. It checks for errors and then writes the character into the buffer and updates the head pointer.

uart_receive:
          in        rISREGSAVE,SREG
          mov       rISCRATCH4,XL

          sbic      USR,FE              ; check for
          rjmp      uart_rxerr_fe       ; framing error

          lds       rITEMP,uart_rsize
          cpi       rITEMP,UART_BUFSIZE
          breq      uart_rxerr_bf       ; buffer full

          inc       rITEMP
          sts       uart_rsize,rITEMP

          lds       XL,uart_rhead
          in        rITEMP,UDR
          st        X+,rITEMP

          andi      XL,(UART_BUFSIZE-1)
          ori       XL,UART_BUFFER
          sts       uart_rhead,XL

          sbic      USR,OR              ; check for
          rjmp      uart_rxerr_or       ; overrun

uart_rxend:
          mov       XL,rISCRATCH4
          out       SREG,rISREGSAVE
          reti

uart_rxerr_fe:
uart_rxerr_bf:
          in        rITEMP,UDR                    ; dummy read to clear RxC
uart_rxerr_or:
          ldi       rITEMP,1            ; overflow, framing error
          sts       uart_rerror,rITEMP

          rjmp      uart_rxend

;-----------------------------------------------------------------
;
; Read from receiver buffer char=rRETURN, wait until character ready

This reads a character from the buffer. In this version, it first checks to see if any characters are ready. If not, then it will loop until one comes in before falling through to read it.

uart_readcharwait:
          rcall     uart_charready
          breq      uart_readcharwait

This reads a character from the buffer. If no characters are ready, then it will perform incorrectly.

uart_readchar:
          push      XL
          
          lds       XL,uart_rtail
          ld        rRETURN,x+
          andi      XL,(UART_BUFSIZE-1)
          ori       XL,UART_BUFFER
          sts       uart_rtail,XL

          cli
          lds       rSCRATCH,uart_rsize
          dec       rSCRATCH
          sts       uart_rsize,rSCRATCH
          sei

          pop       XL

          ret
        
;-----------------------------------------------------------------
;
; Ask if a character is ready from the UART (eq=no, ne=yes)

This is simple to find out - if there is a character, then the number of characters in the buffer is non-0.

uart_charready:
          lds       rSCRATCH,uart_rsize
          and       rSCRATCH,rSCRATCH
          ret       

;-----------------------------------------------------------------
;
; Write to transmitter char=rPARAM1

This is a simple, polled write character routine. It waits until the UDRE bit is set (UART Data Register Empty) which indicates that it is safe to transmit another character.

uart_writechar:
          sbis      USR,UDRE
          rjmp      uart_writechar
          out       UDR,rPARAM1
          ret

;-----------------------------------------------------------------
;
; Write to transmitter hex value=rPARAM1

This converts the value in rPARAM1 into two hex digits (0..9, A..F) and outputs them to the serial port.

uart_writehex:
          push      rPARAM1
          swap      rPARAM1
          rcall     uart_writehexit
          pop       rPARAM1
          
uart_writehexit:
          andi      rPARAM1,$0F
          subi      rPARAM1,-48
          cpi       rPARAM1,$3A
          brcs      uart_writechar
          subi      rPARAM1,-7
          rjmp      uart_writechar

;-----------------------------------------------------------------

Build Process

All of these files should be loaded into an AVR studio project, with MMCSERIAL.ASM being the main target file. The build process should complete with no errors or warnings, and is then ready to be uploaded into the 2313 using whatever programming hardware is appropriate. I used the STK500, since its programming voltage can be set to 3V to match the voltage of the target board.