Function Pointers in PIC Assembler
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