Saphum

Function Pointers in PIC Assembler

November 11, 2007 | 7 Minute Read

Simplified the Modbus slave code by replacing the large case statements—sprinkled throughout—with function pointer tables. Compared to computed jump tables, they may seem heavyweight, but they have four significant advantages:

  • They allow arbitrary case identifiers—no need to be sequential (or even numeric!),
  • They aren’t limited to 256 cases,
  • They greatly simplify the code, even reducing its size (the overhead is amortized quickly), and
  • They may be daisy-chained, allowing derivation and polymorphism.

The last one is particularly useful in the Modbus application, since it allows users of the library to override message dispatching. Each caller can define its own command-code-to-function mapping, but fall back to the default method to inherit built-in processing and error handling.

For example, here’s a snippet from the Ifos project showing how the function pointer table is defined. This one includes some functions to handle user-defined command codes (the first three entries), as well as standard Modbus messages. The last entry, with an id of -1, points to the default handler provided by the library. This action is a catch-all that will be executed if no other case is matched.

Modbus.VTable:
   data     Ifos.kSystemAdmin, System.admin
   data     Ifos.kAnimation, Anim.control
   data     Ifos.kPortMapping, LED.map
   data     Modbus.kReadCoils, LED.readCoils
   data     Modbus.kReadDiscretes, LED.readCoils
   data     Modbus.kReadRegisters, LED.readRegisters
   data     Modbus.kReadInputs, LED.readRegisters
   . . .
   data     0xffff, Modbus.builtin
 
mainLoop:
   . . .
   SetTableBase Modbus.VTable       ; point to virtual function table
   call     Modbus.dispatchMsg      ; process message and build reply
   call     Modbus.replyMsg         ; send reply
   . . .

Modbus.dispatchMsg() does nothing more than load the command id from the received frame, then drops through to VTable.dispatch(), which searches for an associated function. In this case, the command id domain is sparse, so a computed jump table would require many dummy values. The other alternative, a series of comparisons, quickly becomes unwieldy:

movlw    Ifos.kSystemAdmin
   cpfseq   Modbus.kRxFunction
     bra    chk1
   call     System.admin
   bra      reply
 
chk1:
   movlw    Ifos.kAnimation
   cpfseq   Modbus.kRxFunction
     bra    chk2
   call     Anim.control
   bra      reply
 
chk2:
   movlw    Ifos.kPortMapping
   cpfseq   Modbus.kRxFunction
     bra     chk3
   call     LED.map
   bra      reply
 
chk3:
   movlw    Modbus.kReadCoils
   cpfseq   Modbus.kRxFunction
     bra    chkN
   call     LED.readCoils
   bra      reply
   . . .
 
reply:
   call     Modbus.replyMsg         ; send reply
   . . .

As written, this approach requires 8-bit values (since they must fit in the working register), making it inappropriate for Modbus, which uses 16-bit subfunction ids. The code is repetitive, hard to read/maintain, and scales poorly—replacing the comparisons with 16-bit versions would bloat it even more—whereas iterating over a table of function pointers follows basic DRY principles. In fact, switch statements become totally data-driven; the same table look-up routine is used for every one.

Ifos, and the Modbus library itself, also chains handlers together, effectively providing for virtual functions. Since most Modbus command implementations are application-specific, the library only supports a few commands by default. Each application defines a table of function pointers to the command handlers it provides, possibly overriding handlers provided higher up and falling back to Modbus.builtin() for everything else. Modbus.dispatchMsg() may call Modbus.builtin(), which may call Diag.diagnostics(), which may call Diag.returnQuery(), all using the same mechanism:

BuiltinVTbl:
   data     Modbus.kDiagnostics, Diag.diagnostics
   data     Modbus.kGetEventCount, Diag.getEventCount
   data     Modbus.kGetEventLog, Diag.getEventLog
   data     0xffff, Modbus.illegalFunction
 
Modbus.builtin:
   SetTableBase BuiltinVTbl
   goto     VTable.dispatch
 
DiagnosticsVTbl:
   data     Modbus.kDiagReturnQuery, Diag.returnQuery
   data     Modbus.kDiagRestartComm, Diag.restartComm
   data     Modbus.kDiagGetRegister, Diag.getRegister
   data     Modbus.kDiagSetDelim, Diag.setDelim
   data     Modbus.kDiagSetListenOnly, Diag.setListenOnly
   data     Modbus.kDiagClear, Diag.clear
   . . .
   data     0xffff, Modbus.illegalFunction
 
Diag.diagnostics:
   ; Pull the 16-bit subfunction identifier from the request.
   movff    Modbus.kRxSubFunction + 1, Util.Frame + 0
   movff    Modbus.kRxSubFunction + 0, Util.Frame + 1
 
   ; Use the id to perform a virtual function call.
   SetTableBase DiagnosticsVTbl
   goto     VTable.dispatch

The VTable.dispatch() method does a simple linear search to find the the correct function pointer. A lower-order algorithm would probably be offset by the overhead of implementing it over program memory (where the tables are stored). It would also require the table to be ordered, which isn’t desirable.

;; ---------------------------------------------------------------------------
;;
;;  PIC Framework
;;  Copyright (C) 2006-8  Peter Heinrich
;;
;;  This program is free software: you can redistribute it and/or modify
;;  it under the terms of the GNU General Public License as published by
;;  the Free Software Foundation, either version 3 of the License, or
;;  (at your option) any later version.
;;
;;  This program is distributed in the hope that it will be useful,
;;  but WITHOUT ANY WARRANTY; without even the implied warranty of
;;  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;  GNU General Public License for more details.
;;
;;  You should have received a copy of the GNU General Public License
;;  along with this program.  If not, see <http://www.gnu.org/licenses/>.
;;
;;  $URL: svn://saphum.com/PIC/framework/trunk/vtable.asm $
;;  $Revision: 360 $
;;
;; ---------------------------------------------------------------------------
;;  $Author: peter $
;;  $Date: 2008-08-25 17:11:46 +0000 (Mon, 25 Aug 2008) $
;; ---------------------------------------------------------------------------
 
 
 
   #include "private.inc"
 
   ; Public Methods
   global   VTable.dispatch
 
   ; Dependencies
   extern   Math.compare16
   extern   Util.Frame
 
 
 
;; ---------------------------------------------------------------------------
.vtable                 code
;; ---------------------------------------------------------------------------
 
;; ----------------------------------------------
;;  void VTable.dispatch( frame[0..1] funcKey, TBLPTR vtable )
;;
;;  Iterates over a list of key-value entries stored in program memory,
;;  searching for a function pointer that matches the key specified.  Each
;;  entry in the key-value list starts with a 16-bit identifier, followed by
;;  the lower 16 bits of a program memory address in little-endian format (the
;;  upper 5 bits are always treated as 0).
;;
;;  This method searches linearly through the list until a matching id or -1
;;  is found.  The ids don't have to be in any particular numerical order, al-
;;  though sorting the most-used entries to the beginning of the list will
;;  improve performance.  If a match isn't found, the vector associated with
;;  the -1 identifier will be used to simulate a "missing_method" call, else
;;  program execution will be transferred to the matching address.
;;
;;  Here's an example of how a vtable might be declared.  Each numerical value
;;  indicates a function id, while the symbols following refer to program
;;  memory locations:
;;
;;     .mySection code
;;     myVTbl:
;;       data     1,  readCoils
;;       data     2,  readDiscretes
;;       data     4,  readInputs
;;       data     15, writeCoils
;;       data     16, writeRegisters
;;       data     20, readFileRecord
;;       data     0xffff, unsupported      ; required terminator/default handler
;;
VTable.dispatch:
   ; Back up the pointer two bytes to account for the first iteration.
   tblrd*-
   tblrd*-
 
lookup:
   ; Find the correct function pointer, based on the id specified.
   tblrd*+                          ; skip the function pointer from the last entry
   tblrd*+
 
   tblrd*+                          ; read the low byte of the id
   movff    TABLAT, Util.Frame + 2
   tblrd*+                          ; read the high byte of the id
   movff    TABLAT, Util.Frame + 3
 
   ; Compare the two-byte id to the first parameter.
   call     Math.compare16
   bnz      noMatch
 
vecJump:
   ; Dispatch to the correct method.
   clrf     PCLATU                  ; always 0 for chips with < 64k
   tblrd*+
   movf     TABLAT, W               ; stash low byte to be written last
   tblrd*+
   movff    TABLAT, PCLATH          ; write high byte of new PC
   movwf    PCL                     ; write low byte of new PC
 
noMatch:
   movf     Util.Frame + 2, W
   cpfseq   Util.Frame + 3          ; are both id bytes equal?
     bra    lookup                  ; no, so can't be -1 (0xffff)
   comf     WREG, F                 ; yes, complement one of them
   bz       vecJump                 ; 0 => id = -1, so we're done
   bra      lookup                  ; otherwise, keep looking
 
 
 
   end

The method above depends on a pseudo-stack frame provided by the Util module, but it amounts to nothing more than 4 bytes of storage. It also requires a simple 16-bit comparison routine:

;; ----------------------------------------------
;;  STATUS<C,Z> Math.compare16( frame[0..1] value, frame[2..3] comparand )
;;
;;  Compares two 16-bit (unsigned) numbers passed on the pseudo-stack, setting
;;  the status flags as appropriate:
;;
;;    C    Z    comparison
;;    X    1    value == comparand
;;    0    X    value  > comparand
;;    1    X    value <= comparand
;;
Math.compare16
   ; Save the working register.
   movff    WREG, Util.Save
 
   ; Compare the high words.
   movf     Util.Frame + 1, W
   subwf    Util.Frame + 3, W
   bnz      cmp16Done
 
   ; Compare the low words.
   movf     Util.Frame + 0, W
   subwf    Util.Frame + 2, W
 
cmp16Done:
   ; Restore the working register, but preserve status flags.
   movff    Util.Save, WREG
   return