Project

General

Profile

Example Driver in C++

For this driver, we'll define imaginary hardware: four, 16-bit registers residing in VME A16 space representing a simple ADC. The first register (offset 0) is an ADC reading that updates at 100Hz. The second register is R/W and configures the hardware. Bit 0 controls whether the ADC is running. When the ADC is active, an interrupt is provided to notify when a new reading is available. The third register sets the interrupt vector number. The last register is read-only and is the board ID: 0xadc. The board has an 8-switch DIP to set the offset in A16 space.

Building the Project

The definition of the class will be in example.h. Any functions of the class that need to be defined will be placed in example.cpp. The MOOC interface is found in mooc_class.cpp. The following Makefile will build a loadable VxWorks module called ex_adc-1.0.out. The driver will use the VWPP Concurrency library, so we link with the library libvwpp-2.4.a.

VID = 1.0
PRODUCT = 1

SUPPORTED_VERSIONS = 64 67

MOD_TARGETS = example.out

include ${PRODUCTS_INCDIR}frontend.mk

ex_adc.out : example.o mooc_class.o ${PRODUCTS_LIBDIR}libvwpp-2.4.a
        ${make-mod}

Writing the Driver

VxWorks is a multitasking operating system and MOOC uses this feature to delegate its services across tasks. Each driver has an associated mutex which MOOC uses to serialize access. This keeps GETS32, SETS32, FTPMAN and alarm scan tasks from executing code in the driver simultaneously, possibly corrupting its state1. For simple drivers, this may be fine. But as the driver gets more complicated, it may become apparent that only a core of the driver needs to be serialized and that MOOC's driver mutex is too coarse in providing access. Although this example is simple and could use MOOC's driver mutex with little penalty, we will be doing the serialization ourselves.

The MOOC framework defines an object model using the C programming language. As impressive as this may seem, it's not a programmer-friendly environment: if the MOOC class has instance variables, offsets and typecasts have to be carefully chosen to access the correct memory location; calling methods of a MOOC class are awkward; and the inheritance model makes these details even more difficult. Since we're writing a MOOC driver, we can't avoid all these problems, although we can reduce our exposure to them. Our approach will be to create a C++ object to hold our driver's state and interface and then store the pointer to it in MOOC's instance variable memory.

This example is using features in the beta version of MOOC. These features may change, if we find better, cleaner ways of expressing them. This document will be updated appropriately.

Detecting the Hardware

Let's start the driver with this minimal implementation. Later sections will add more functionality.

 1 // Define the offset of each register from the
 2 // hardware's base address.
 3
 4 #define OFFSET_ADC   0
 5 #define OFFSET_CTL   1
 6 #define OFFSET_INT   2
 7 #define OFFSET_ID    3
 8 
 9 namespace ExADC {
10 
11     class HW : public vwpp::Uncopyable {
12         uint16_t volatile* const baseAddr;
13 
14         static uint16_t volatile* getBoardAddr(uint8_t);
15 
16      public:
17         explicit HW(uint8_t dip) : baseAddr(getBoardAddr(dip)) {}
18     };
19 
20     // Convert a DIP switch setting into a physical address. If the
21     // address can't be determined or the board ID is not found,
22     // an exception is thrown.
23 
24     uint16_t* HW::getBoardAddr(uint8_t dip)
25     {
26         char* tmp;
27         char* const addr = reinterpret_cast<char*>(dip << 2);
28 
29         if (ERROR == sysBusToLocalAdrs(VME_AM_USR_SHORT_IO, addr, &tmp))
30             throw std::runtime_error("illegal VME A16 address");
31 
32         uint16_t volatile* const ptr = reinterpret_cast<uint16_t volatile*>(tmp);
33 
34         if (ptr[OFFSET_ID] != 0xadc)
35             throw std::runtime_error("incorrect board ID");
36         return ptr;
37     }
38 }

The example, at this point, only defines an object that interacts with the hardware. We'll add connections to MOOC in later examples. The constructor, ExADC::HW() (line 17), takes an 8-bit parameter representing the DIP switch setting. The explicit keyword prevents automatic type conversions from occurring. The object stores the base address of the hardware in a constant field, baseAddr, which is initialized with the return value of the static function, getBoardAddr(). If getBoardAddr() cannot determine the hardware base address, it throws a run-time exception which causes the object to be destroyed and memory reclaimed. We'll see later how we'll catch the exception.

The HW class uses vwpp::Uncopyable (line 11) as a base class to prevent it from accidentally being copied. Any class that is uncopyable may only be passed by reference. This makes sense for an object that interacts with hardware; if a copy were made, both classes would be able to access the hardware and probably cause problems.

Handling an Interrupt

We define a method used by the interrupt in the private section of the class. For this simple example, we'll copy the hardware register to a field in the object. A more complex implementation might save the reading in a circular buffer so that the driver more reliably supports fast time plots.

 1 uint16_t volatile lastReading;
 2 
 3 void handleIntr()
 4 {
 5     lastReading = baseAddr[OFFSET_ADC];
 6 }

Since the field lastReading is going to be shared between tasks and an interrupt routine, we qualify it using the volatile keyword.

Attach Interrupt

In the constructor, we should also attach the interrupt routine to the method handleIntr().

 1 static void deliverIntr(HW*);
 2 
 3 explicit HW(uint8_t dip) : baseAddr(getBoardAddr(dip))
 4 {
 5     baseAddr[OFFSET_CTL] = 0;
 6     if (ERROR == intConnect(INUM_TO_IVEC(0x90), (VOIDFUNCPTR) deliverIntr,
 7                             reinterpret_cast<int>(this)))
 8         throw std::runtime_error("intConnect failed");
 9     baseAddr[OFFSET_INT] = 0x90;
10 }
11 
12 // ... outside the HW class ...
13 
14 void HW::deliverIntr(HW* obj)
15 {
16     obj->handleIntr();
17 }

You can't call an object's method directly while inside an interrupt because calling a method requires context (i.e. the object whose method you're calling.) Interrupts call through a function pointer. Fortunately, VxWorks allows you to specify an argument to the interrupt function, so we give it the pointer to the object (lines 6 and 7.) The interrupt handler can then call into the object's method (line 16.)

Adding Serialization Resources

In the private section of the class, we add a mutex. Since this driver is so simple, we'll only need one. But a more complicated driver may have subsystems that can be accessed simultaneously and could use multiple mutexes to provide better access granularity. We don't have to do anything to initialize the mutex in the constructor; the default constructor for the mutex will be used and it does what we need to create an unowned mutex. Note if the HW constructor throws an exception, the embedded mutex will be correctly destroyed.

vwpp::Mutex mutex;

typedef vwpp::Mutex::PMLock<HW, &HW::mutex> ObjLock;

Due to constraints in C++ templates, the template for a lock of an object's mutex is ugly so I typically define a typedef to make the code earlier to read. The first parameter to the template indicates which class has the mutex and the second parameter is the address of the mutex in the class. From here on, we'll refer to it as ObjLock.

Define Hardware Primitives

Now we define building blocks to access the hardware. These are placed inside the private section but will be used by the public interface.

 1 uint16_t getLatest(ObjLock const&, vwpp::IntLock const&) const
 2 {
 3     return lastReading;
 4 }
 5 
 6 uint16_t status(ObjLock const&) const
 7 {
 8     return baseAddr[OFFSET_CTL] & 1;
 9 }
10 
11 void control(ObjLock const&, bool val)
12 {
13     baseAddr[OFFSET_CTL] = val ? 1 : 0;
14 }

There's a few things to mention here.

  • Access to hardware should be serialized. Rather than take the mutex themselves, these methods require a reference to an ObjLock. If the caller can provide a ObjLock reference, then they own the correct mutex.
  • The methods will get inlined, so the unused parameters won't cause a runtime penalty.

The Public API

This is the API that the MOOC methods will call.

 1 void enable(ObjLock const& lock, bool v)
 2 {
 3     control(lock, v);
 4 }
 5 
 6 uint16_t getStatus(ObjLock const& lock)
 7 {
 8     return status(lock);
 9 }
10 
11 uint16_t getReading(ObjLock const& lock)
12 {
13     if (status(lock) & 1) {
14         vwpp::IntLock iLock;
15 
16         return getLatest(lock, iLock);
17     } else
18         return 0;
19 }

The serialization is hidden inside the methods. Also note that, in getReading(), since we have a lock to the object's mutex (line 11), we can call the hardware access methods in any order that we find useful. In this case, we check to see if the ADC is enabled (line 13.) If it is, we get the reading. However, getting the reading is a little more restrictive because we need to disable interrupts. In the scope of the if-statement, we get an interrupt lock (line 14.) At this point, we meet all the requirements to get the reading (line 16.)

Interfacing to MOOC

This driver will define four MOOC methods: Init, Reading, Basic Status, and Basic Control. The instance variable passed to these methods is a pointer to the data. The data in our case is a pointer to the object, hence the double pointer.

static STATUS objectInit(short const oid, ExADC::v1_0::HW* const ptr, void const*, ExADC::v1_0::HW** const ivs)
{
    ALARM_GUTS& albl = reinterpret_cast<ALARM_GUTS*>(ivs)[-1];

    albl.anl_chan = 1;
    albl.anl_typ = SIGNED_SHORT_TYPE;
    albl.aread = reinterpret_cast<PMETHOD>(devReading);
    albl.anotify = 0;
    albl.dig_chan = 0;
    albl.dig_len = 0;
    albl.dstat = 0;
    albl.dnotify = 0;
    albl.oid = oid;
    albl.ivs = its;

    *ivs = ptr;
    return OK;
}

static STATUS devReading(short, RS_REQ const* const req, void* const rep, ExADC::v1_0::HW* const* const ivs)
{
    try {
        MOOC::ReadingProxy<uint16_t> reading(req, rep);
        ExADC::v1_0::HW::ObjLock lock(*ivs);

        reading = (*ivs)->getReading(lock);
    }
    catch (STATUS const sts) {
        return sts;
    }
    catch (std::exception const&) {
        return ERR_DEVICEERROR;
    }
    return NOERR;
}

static STATUS devBasicStatus(short, RS_REQ const* const req, void* const rep, ExADC::v1_0::HW* const* const ivs)
{
    try {
        MOOC::ReadingProxy<uint16_t> status(req, rep);
        ExADC::v1_0::HW::ObjLock lock(*ivs);

        status = (*ivs)->getStatus(lock);
    }
    catch (STATUS const sts) {
        return sts;
    }
    catch (std::exception const&) {
        return ERR_DEVICEERROR;
    }
    return NOERR;
}

static STATUS devBasicControl(short, RS_REQ const* const req, void*, ExADC::v1_0::HW* const* const ivs)
{
    try {
        MOOC::SettingProxy<uint16_t> cmd(req);
        ExADC::v1_0::HW::ObjLock lock(*ivs);

        switch (cmd) {
         case 1:
            (*ivs)->enable(lock, false);
            return NOERR;

         case 2:
            (*ivs)->enable(lock, true);
            return NOERR;

         default:
            return ERR_WRBASCON;
        }
    }
    catch (STATUS const sts) {
        return sts;
    }
    catch (std::exception const&) {
        return ERR_DEVICEERROR;
    }
}

Creating the Class

Now we define functions which are used in a front-end start-up script. The first function creates the MOOC class (which associates a class ID with a name and the set of methods.) In this example, we add four class methods and set the instance data size to the size of a pointer. This function is called once in a start-up script.

STATUS ExADC_create_mooc_class(uint8_t const cls)
{
    if (cls < 16) {
        printf("MOOC class codes need to be 16, or greater.\n");
        return ERROR;
    }

    static short const subCls[] = { ALRCLS };

    if (NOERR != create_class(cls, NELEMENTS(subCls), subCls, 4, sizeof(ExADC::HW*))) {
        printf("Error returned from create_class()!\n");
        return ERROR;
    }
    if (NOERR != name_class(cls, "ExADC")) {
        printf("Error trying to name the class.\n");
        return ERROR;
    }
    if (NOERR != add_class_msg(cls, Init, (PMETHOD) objectInit)) {
        printf("Error trying to add the Init handler.\n");
        return ERROR;
    }
    if (NOERR != add_class_msg(cls, rPRREAD, (PMETHOD) devReading)) {
        printf("Error trying to add the reading handler.\n");
        return ERROR;
    }
    if (NOERR != add_class_msg(cls, rPRBSTS, (PMETHOD) devBasicStatus)) {
        printf("Error trying to add the basic status handler.\n");
        return ERROR;
    }
    if (NOERR != add_class_msg(cls, sPRBCTL, (PMETHOD) devBasicControl)) {
        printf("Error trying to add the basic control handler.\n");
        return ERROR;
    }
    return NOERR;
}

This function creates an instance of the MOOC class and associates an underlying ExADC::HW object with it. The first parameter is the desired object ID (OID) and he second parameter is the setting of the hardware DIP switch. This function could be called multiple times in a startup script, if the front-end has multiple instances. Each instance should have a unique OID and a different hardware address.

STATUS ExADC_create_mooc_instance(unsigned short oid, int const dip)
{
    try {
        short const cls = find_class("ExADC");

        if (cls == -1)
            throw std::runtime_error("ExADC class is not registered with MOOC");

        std::auto_ptr<ExADC::HW> ptr(new ExADC::HW(dip));

        if (ptr.get()) {
            if (create_instance(oid, cls, ptr.get(), "ExADC") != NOERR)
                throw std::runtime_error("problem creating an instance");
            instance_is_reentrant(oid);
            printf("New instance of ExADC created. Underlying object @ %p.\n", ptr.release());
        }
        return OK;
    }
    catch (std::exception const& e) {
        printf("ERROR: %s\n", e.what());
        return ERROR;
    }
}

This function tries to create the ExADC::HW object before calling MOOC's create_instance() function. We should actually allocate the object in objectInit() but can't because MOOC doesn't correctly release resources when the Init method returns an error. We work around this deficiency by allocating the object, then calling create_instance(). Up until ptr.release() is called, the auto_ptr<> "owns" the object and any exception will cause ExADC::HW to be destroyed and its memory reclaimed. This function also calls instance_is_reentrant() so that the driver specific mutex is released (since we're doing all the serialization ourself.)

Versioning Protection

One last improvement is making sure users are loading the module which matches the header file. VxWorks dynamically links modules as they get loaded. If a user compiles with a header file, there's no guarantee that they'll load the associated module at runtime, unless a function was added that they use. We can use C++ to force this condition.

Since function names can be overloaded in C++, the language "mangles" the function names to include parameter types and namespaces it resides in. If we add a nested namespace containing the version number, then we force the names to have the version cooked into them and the developer if required to load the correct module.

In our example, the Makefile sets the version ID to "1.0". This means that example-1.0.out and example-1.0.h will get installed. In the ExADC namespace, we add a v1_0 namespace and define everything inside it.

namespace ExADC {
    namespace v1_0 {

        // Define everything here.

    };
};

If a new version is released, the nested namespace should get updated.


1 Dennis confirmed that FTPMAN and SETDAT tasks don’t use the semaphore, so our serialization is broken in MOOC!

Full Source

This is the complete source to example.h.

#include <vwpp-2.4.h>

#define OFFSET_ADC   0
#define OFFSET_CTL   1
#define OFFSET_INT   2
#define OFFSET_ID    3

namespace ExADC {
    namespace v1_0 {

        class HW : public vwpp::Uncopyable {
            uint16_t* const baseAddr;
            uint16_t volatile lastReading;
            vwpp::Mutex mutex;

            typedef vwpp::Mutex::PMLock<HW, &HW::mutex> ObjLock;

            void handleIntr()
            {
                lastReading = baseAddr[OFFSET_ADC];
            }

            static void deliverIntr(HW*);
            static uint16_t* getBoardAddr(uint8_t);

            uint16_t getLatest(ObjLock const&, vwpp::IntLock const&) const
            {
                return lastReading;
            }

            uint16_t status(ObjLock const&) const
            {
                return baseAddr[OFFSET_CTL] & 1;
            }

            void control(ObjLock const&, bool val)
            {
                baseAddr[OFFSET_CTL] = val ? 1 : 0;
            }

         public:
            explicit HW(uint8_t dip) : baseAddr(getBoardAddr(dip))
            {
                baseAddr[OFFSET_CTL] = 0;
                if (ERROR == intConnect(INUM_TO_IVEC(0x90), (VOIDFUNCPTR) deliverIntr,
                                        reinterpret_cast<int>(this)))
                    throw std::runtime_error("intConnect failed");
                baseAddr[OFFSET_INT] = 0x90;
            }

            void enable(ObjLock const& lock, bool v)
            {
                control(lock, v);
            }

            uint16_t getStatus(ObjLock const& lock)
            {
                return status(lock);
            }

            uint16_t getReading(ObjLock const& lock)
            {
                if (status(lock) & 1) {
                    vwpp::IntLock iLock;

                    return getLatest(lock, iLock);
                } else
                    return 0;
            }
        };
    }
}

This is the complete source to example.cpp.

#include "example.h" 

namespace ExADC {
    namespace v1_0 {

        // Convert a DIP switch setting into a physical address. If the
        // address can't be determined or the board ID is not found,
        // an exception is thrown.

        uint16_t* HW::getBoardAddr(uint8_t dip)
        {
            char* tmp;
            char* const addr = reinterpret_cast<char*>(dip << 2);

            if (ERROR == sysBusToLocalAdrs(VME_AM_USR_SHORT_IO, addr, &tmp))
                throw std::runtime_error("illegal VME A16 address");

            uint16_t* const ptr = reinterpret_cast<uint16_t*>(tmp);

            if (ptr[OFFSET_ID] != 0xadc)
                throw std::runtime_error("incorrect board ID");
            return ptr;
        }

        // Routes the interrupt into our object method.

        void HW::deliverIntr(HW* obj)
        {
            obj->handleIntr();
        }
    }
}

This is the complete source to mooc_class.cpp.

#include <vxWorks.h>
#include <sysLib.h>
#include <intLib.h>
#include <config.h>
#include <memory>
#include <vwpp-2.4.h>
#include <mooc++-4.8.h>
#include "example.h" 

extern "C" {
    STATUS ExADC_create_mooc_instance(unsigned short, int);
    STATUS ExADC_create_mooc_class(uint8_t);
};

static STATUS objectInit(short, ExADC::HW*, void const*, ExADC::HW**);
static STATUS devReading(short, RS_REQ const*, void*, ExADC::HW* const*);
static STATUS devBasicStatus(short, RS_REQ const*, void*, ExADC::HW* const*);
static STATUS devBasicControl(short, RS_REQ const*, void*, ExADC::HW* const*);

// ----------- MOOC Methods -----------

static STATUS objectInit(short const oid, ExADC::v1_0::HW* const ptr, void const*, ExADC::v1_0::HW** const ivs)
{
    ALARM_GUTS& albl = reinterpret_cast<ALARM_GUTS*>(ivs)[-1];

    albl.anl_chan = 1;
    albl.anl_typ = SIGNED_SHORT_TYPE;
    albl.aread = reinterpret_cast<PMETHOD>(devReading);
    albl.anotify = 0;
    albl.dig_chan = 0;
    albl.dig_len = 0;
    albl.dstat = 0;
    albl.dnotify = 0;
    albl.oid = oid;
    albl.ivs = its;

    *ivs = ptr;
    return OK;
}

static STATUS devReading(short, RS_REQ const* const req, void* const rep, ExADC::v1_0::HW* const* const ivs)
{
    try {
        MOOC::ReadingProxy<uint16_t> reading(req, rep);
        ExADC::v1_0::HW::ObjLock lock(*ivs);

        reading = (*ivs)->getReading(lock);
    }
    catch (STATUS const sts) {
        return sts;
    }
    catch (std::exception const&) {
        return ERR_DEVICEERROR;
    }
    return NOERR;
}

static STATUS devBasicStatus(short, RS_REQ const* const req, void* const rep, ExADC::v1_0::HW* const* const ivs)
{
    try {
        MOOC::ReadingProxy<uint16_t> status(req, rep);
        ExADC::v1_0::HW::ObjLock lock(*ivs);

        status = (*ivs)->getStatus(lock);
    }
    catch (STATUS const sts) {
        return sts;
    }
    catch (std::exception const&) {
        return ERR_DEVICEERROR;
    }
    return NOERR;
}

static STATUS devBasicControl(short, RS_REQ const* const req, void*, ExADC::v1_0::HW* const* const ivs)
{
    try {
        MOOC::SettingProxy<uint16_t> cmd(req);
        ExADC::v1_0::HW::ObjLock lock(*ivs);

        switch (cmd) {
         case 1:
            (*ivs)->enable(lock, false);
            return NOERR;

         case 2:
            (*ivs)->enable(lock, true);
            return NOERR;

         default:
            return ERR_WRBASCON;
        }
    }
    catch (STATUS const sts) {
        return sts;
    }
    catch (std::exception const&) {
        return ERR_DEVICEERROR;
    }
}

// ----------- Startup script functions -----------

STATUS ExADC_create_mooc_class(uint8_t const cls)
{
    if (cls < 16) {
        printf("MOOC class codes need to be 16, or greater.\n");
        return ERROR;
    }

    static short const subCls[] = { ALRCLS };

    if (NOERR != create_class(cls, NELEMENTS(subCls), subCls, 4, sizeof(ExADC::v1_0::HW*))) {
        printf("Error returned from create_class()!\n");
        return ERROR;
    }
    if (NOERR != name_class(cls, "ExADC")) {
        printf("Error trying to name the class.\n");
        return ERROR;
    }
    if (NOERR != add_class_msg(cls, Init, (PMETHOD) objectInit)) {
        printf("Error trying to add the Init handler.\n");
        return ERROR;
    }
    if (NOERR != add_class_msg(cls, rPRREAD, (PMETHOD) devReading)) {
        printf("Error trying to add the reading handler.\n");
        return ERROR;
    }
    if (NOERR != add_class_msg(cls, rPRBSTS, (PMETHOD) devBasicStatus)) {
        printf("Error trying to add the basic status handler.\n");
        return ERROR;
    }
    if (NOERR != add_class_msg(cls, sPRBCTL, (PMETHOD) devBasicControl)) {
        printf("Error trying to add the basic control handler.\n");
        return ERROR;
    }
    return NOERR;
}

STATUS ExADC_create_mooc_instance(unsigned short oid, int const dip)
{
    try {
        short const cls = find_class("ExADC");

        if (cls == -1)
            throw std::runtime_error("ExADC class is not registered with MOOC");

        std::auto_ptr<ExADC::v1_0::HW> ptr(new ExADC::v1_0::HW(dip));

        if (ptr.get()) {
            if (create_instance(oid, cls, ptr.get(), "ExADC") != NOERR)
                throw std::runtime_error("problem creating an instance");
            instance_is_reentrant(oid);
            printf("New instance of ExADC created. Underlying object @ %p.\n", ptr.release());
        }
        return OK;
    }
    catch (std::exception const& e) {
        printf("ERROR: %s\n", e.what());
        return ERROR;
    }
}