I'm building a SCPI electronics lab instrument for Linux. This post is an object oriented one: porting the PiFace Digital API to C++. If you aren't interested in object oriented design, you may want to walk away from this blog. |
API and Abstraction
I've used exactly the same level of abstraction as the PiFace official procedural API.
The OO API you talk to is the one that knows the PiFace Digital hat inputs and outputs.
That API depends on a separate more low level OO API, the one of the SPI port expander used on the hat (MCP23S17).
I've turned each of these into a class. The PiFace Digital functions are served by the PifaceDigital class.
The mcp23s17 class supports the SPI port expander's functions.
What's different is that the in the original PiFace API, all functions are static. They have no context.
In my OO API, both objects are aware of the hardware address of the PiFace Digital hat (more subtile: the mcp23s17 object knows, and the PifaceDigital object is hard linked to that object).
For the programmer, the difference is that, in the traditional API, you pass the hardware address of the board to each function.
If you have multiple hats, you pass the correct hardware address whenever you want to address a particular one.
In my OO API, you create a PifaceDigital object for each hat. In the constructor, you give the hardware address so that the object knows which hat it represents.
From then on, to talk to a particular hat, you use the object that presents that one. No need to deal with addresses anymore.
I like it when my classes represent real world things. In this case the match is 100%.
image source: the icon of one of peteroakes' youtube videos.
The hat (purple rectangle) is the PifaceDigital class. The SPI port expander (yellow rectangle) is the mcp23s17 class.
Just like in my API, a hardware hat owns exactly one port expander, and the hat routes the traffic to-and-from it.
Because the physical hat can't work without one single SPI expander (the mcp23s17s IC is soldered on it, you can't get more entangled than that) , I construct them together in the API and destruct them together too.
They share the same lifespan.
C++ Code
I attached the ARM DS-5 project to this post. Check that out for the complete code. This section contains some highlights.
The PifaceDigital class
class PifaceDigital { public: //constants declarations static const unsigned int OUTPUT; // GPIOA static const unsigned int INPUT; // GPIOB // * @param hw_addr The hardware address (configure with jumpers: JP1 and JP2). PifaceDigital(uint8_t hwAddr); virtual ~PifaceDigital(); void open(); void openNoinit(); void close(); uint8_t readReg(uint8_t reg); void writeReg(uint8_t data, uint8_t reg); uint8_t readBit(uint8_t bit_num, uint8_t reg); void writeBit(uint8_t data, uint8_t bitNum, uint8_t reg); int enableInterrupts(void); int disableInterrupts(void); int waitForInput(uint8_t *data, int timeout); protected: mcp23s17 *_mcp23s17; /* MCP23S17 SPI file descriptor. All PiFace Digitals are connected to * the same SPI bus, only need 1 fd. Keeping this hiden from novice * PiFace Digital users. * If you want to make raw SPI transactions to the device then try using the * mcp23s17 class directly. */ static int _mcp23s17_fd; private: // PiFace Digital is always at /dev/spidev0.0 static const int bus; static const int chip_select; static int pfd_count; // number of PiFace Digitals };
You can see that the class does not store the hardware address. Instead, it dynamically creates an mcp23s17 object and passes the hardware address to that.
Because our class only has one mcp23s17 object, the right commands will go to the right hat.
The connection between a PifaceDigital object and its mcp23s17 is made at construction time:
PifaceDigital::PifaceDigital(uint8_t hwAddr) { _mcp23s17 = new mcp23s17(hwAddr); }
From that moment on (and there is no earlier moment in this object's life cycle), the two objects are bound together.
The PifaceDigital object can talk to only one mcp23s17 object.
The mcp23s17 class
class mcp23s17 { public: //constants declarations static const unsigned int WRITE_CMD; static const unsigned int READ_CMD; // Register addresses static const unsigned int IODIRA; // I/O direction A static const unsigned int IODIRB; // I/O direction B // ... many more, removed in this blog static const unsigned int GPIO_INTERRUPT_PIN; mcp23s17(uint8_t hw_addr); virtual ~mcp23s17(); virtual int open(int bus, int chipSelect); virtual uint8_t readReg(uint8_t reg, int fd); virtual void writeReg(uint8_t data, uint8_t reg, int fd); virtual uint8_t readBit(uint8_t bitNum, uint8_t reg, int fd); virtual void writeBit(uint8_t data, uint8_t bitNum, uint8_t reg, int fd); virtual int enableInterrupts(); virtual int disableInterrupts(); virtual int waitForInterrupt(int timeout); private: static const uint8_t spi_mode; // ... many more, removed in this blog virtual uint8_t getSpiControlByte(uint8_t rwCmd, uint8_t hwAddr); virtual int initEpoll(void); uint8_t _hw_addr; };
In this class, you can see that the hardware address is stored for later use. Each mcp23s17 object will talk to the hat with the address that it's bound to.
Under the hood, the class will use the SPI mechanism of linux to talk to that chip.
uint8_t PifaceDigital::readReg(uint8_t reg) { return _mcp23s17->readReg(reg, _mcp23s17_fd); }
Check the attached zip for more info on the implementation.
For the programmer
The firmware developer only has to deal with the PifaceDigital class. He never talks to the mcp23s17.
Te PifaceDigital object will ideal with the downstream relation (see previous section).
Talking to a hat is easy. Create it, open it and then talk to it.
When done, dispose of the object.
PifaceDigital *pf = new PifaceDigital(0); pf->open(); pf->writeBit(1, 0, PifaceDigital::OUTPUT); pf->close(); delete pf;
In the above exampe, the program creates an object to represent (i.e.: binds to the hardware of) a PiFace Digital with address 0.
It then opens (initialises) the communication, sends a command (sets output #0 to high), then closes the communication.
It then disposes of the object. The program doesn't hold any resources to that particular hat anymore.
Under the hood, the new() command also creates an mcp23s17 object and binds it to our pf object.
All commands that we send to pf are relayed to its mcp23s17 object, until we're finihed.
Then, when we destruct pf, the mcp23s17 object gets destructed too.
not 100% pure C++
I have done half the exercise. The API from Piface is converted to objects, but in the implementation I'm still using many C constructs. The file operations jump out. Instead of using C++ streams, I'm using the typical C file handler and functions. The stdout and stderr communication has switched to C++ streams.
Some functions that aren't depending on the hardware address of the PiFace digital board could be turned into static ones. I'm using C arrays instead of the STL containers. I am an STL fan since forever. I get a lot of flack for that. fixed: I modified the original underscore_type API calls to camelCase ones, but didn't do the parameters. I did not fully analyse what functions should be virtual or not. I did not make abstract classes to define the API. There are arguments for doing that. I could have kept the original procedural API and just build a wrapper around it.
I've fully encapsulated the SPI device file handler. The original library allows power users to get the handle and manipulate it. I've closed that gate. If a power user wants to have this functionality, she can use the mcp23s17 class. That one is low level and gives explicit access to the device file handler.
fixed: Although I boast about using multiple hats, the current OO implementation doesn't handle sharing the SPI resource well in a single program. I need to my edit open() and close() methods (in particular because I broke the integrity althoug it looks ok-ish when viewing the code). It's all in my hands. The original API handles it well.
Feel free to criticise my approach. What would you change if you were to turn the API into objects? |
Top Comments