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:
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
;; ---------------------------------------------- ;; 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