mailto:w. v. ooijen / f. hanneman [wf@XS4ALL.NL] says:

The PIC-12 uses a banked architecture to address the internal registers (RAM).

The addressing scheme used is:

< B: 3 bank bits > < S: 1 shared/banked bit > < O: 4 bits offset >

When an instruction specifies an address it specifies only the S and O bits, the B bits are taken from the status register. It is a major challenge for a compiler (or assembler programmer) to avoid the need to change the B bits.

When an indirect address is used (FSR register) all bits are taken from the FSR register.

In Michrochip terminology each bank (as selected by the 3 bank bits) consist of a shared part (S=1) and a banked part (S=0). The shared part always addresses the same registers, independent of the B bits. The shared part contains the hardware-specific registers (port buffers etc.) and a few (8) registers that can be accessed independent of the current value of the B bits.

When a pointer to a block of data must be manipulated the addressing scheme choosen by Microchip becomes a pain in the ass.

Within each bank things are fine, the pointer can be incremented, decremented etc. and it will point to the next or previous register. But when a bank boundary is crossed (the S bit changes from 1 to 0) the pointer ends up in the shared part instead of in the next (or previous) banked part, so without precaution a pointer can not simply be manipulated and then used in the FSR to address a register.

What we need is a well-behaved pointer (+1 addresses the next register, -1 address the previous). This can be achieved either by - using a different reprentation for pointers than the one envisioned by Microchip and / or - using a different representation for +1 and -1.

My preferred option is choose a pointer representation that has S as most significant bit:

< S: 1 shared/banked bit > < B: 3 bank bits > < O: 4 bits offset >

Now any carry/borrow from the O bits directly affects the B bits. When such a pointer is moved to the FSR the S bit must be returned to where Microchip in their wisdom has put it. This is no big deal but it requires a few assembler instructions.

Another option is

I have not found any way to keep both the increment / decrement and the transfer to FSR trivial.

Eric Smith says:

I use this method for incrementing a pointer if I want a buffer to span multiple banks:
                incf    ptr
                bsf     ptr,4

Decrementing could probably be done similarly, except that W is needed:

                 decf    ptr
                 movlw   0x10
                 btfss   ptr,4
                 subwf   ptr

Normally I use the increment method in a more elaborate macro that handles wrapping the pointer in a ring buffer, and tries to special case some boundary conditions that it can optimize. It's fairly complex; here's a simplified version without all of the optimization (presented as an example; I haven't tested this version):

       ringadv macro   ptr,rbase,rend

                incf    ptr             ; increment pointer
                bsf     ptr,4           ;   advance bank if needed

                movf    ptr,w           ; is pointer at end?
                xorlw   rend
                movlw   rbase           ; prepare for worst
                btfsc   status,z
                movwf   ptr             ; yes, it was at end, store base
                endif
                endm

Note that this assumes that the pointer must always be in banked memory. The buffer can't start in unbanked memory (0x00..0x0f). Here's the macro I use to allocate the buffer (again simplified and untested):

       ringres macro   size
                local   s2,bend,bleft
        s2      set     size

                while   s2>0
        bend    set     ($&0xe0)+0x20   ; where's the end of the current bank?
        bleft   set     bend-$          ; how many words can we reserve here
                if      bleft>s2        ; more than we need?
        bleft   set     s2              ;   yes, don't go too far
                endif
                res     bleft           ; reserve the space
                org     ($|0x10)        ; skip unbanked register area
        s2      set     s2-bleft        ; update remaining size
                endw

                endm

The way I use this stuff (again simplified and untested):

       tx_ring_size    equ     16
        rx_ring_size    equ     48

                        org     0x08    ; vars must be in unbanked regs

        temp:           res     1

        tx_ring_ip:     res     1       ; transmit ring input
        tx_ring_op:     res     1       ;   and output pointers
        tx_ring_cnt:    res     1

        rx_ring_ip:     res     1       ; receive ring input
        rx_ring_op:     res     1       ;   and output pointers
        rx_ring_cnt:    res     1

                        org     0x90
        tx_ring:        ringres tx_ring_size
        tx_ring_end:

        rx_ring:        ringres rx_ring_size
        rx_ring_end:

[...]

        ; add a byte in W to the transmit buffer
        ; returns 1 in W if successful, 0 if buffer full

        xmit:   movwf   temp            ; save character

                movf    tx_ring_cnt,w   ; test for buffer already full
                xorlw   tx_ring_size
                btfsc   status,z
                retlw   0

                movf    tx_ring_ip,w    ; put character into buffer
                movwf   fsr
                movf    temp,w
                movwf   ind,w

                ringadv tx_ring_ip,tx_ring,tx_ring_end ; advance input pointer
                incf    tx_ring_cnt     ; increment count

                retlw   1

Code:

Comments: