Go Down

Topic: (Re)-Wiring (Read 763 times) previous topic - next topic

Matteo Italia

Nov 21, 2010, 02:38 pm Last Edit: Nov 21, 2010, 02:49 pm by MItaly Reason: 1
Hi all,
in the project I'm starting the digital IO pins of Arduino seemed to be not enough for my purposes (Ethernet/SD shield+LCD+1 relay+some digital inputs literally eat IO pins), so I decided to work with shift registers.

However, I immediately noticed that most libraries (including LiquidCrystal) do not work directly with shift registers, since they ask for "normal" IO pin numbers, even if they could work without problems using a shift register in the middle.
Hence my idea of developing a library to make IO more flexible.

The basic idea is that each object that drives a peripheral uses an "IO manager" object to perform the IO. This object provides the usual digitalWrite, pinMode, digitalRead, ... methods, to make easy adapting existing libraries.
The most important IO manager is the DefaultIOManager, which just forwards the calls to its method to the "normal" Wiring IO functions; there's a global instance of such object, namely RW::ArdIOPorts. This object is used if all it's needed is to just do IO on the "normal" arduino pins.
Other IO managers act as building blocks: for example, I've implemented ShiftOutIOManager, an IO manager that handles the boring task of managing a shift register like the 74HC595. The magic of such class is that, from the class that uses it, it's just like writing on "normal" IO ports: it provides digitalWriteBuffered (just update the internal buffer), flushBuffer (actually write it on the shift register) and the "normal" digitalWrite (digitalWriteBuffered+flushBuffer). If the user class uses digitalWrite, it will just work with the DefaultIOManager as well as with the ShiftOutIOManager.
In turn, the ShiftOutIOManager will use another IO manager to do it's own IO to the shift register, so in theory one could even chain shift out registers and any thing that can be handled with an IO Manager class, all without having to apply modifications to the device driver classes (as far as timing is not a big issue, obviously).

To develop this library, an important design choice has to be made.
There are fundamentally two alternatives: the "classic OOP" model and the "generic programming" model.
The classic OOP model would imply having several abstract base classes to describe the capabilities of a particular IO manager (OutputIOManager would provide virtual digitalWrite, InputIOManager virtual digitalRead, etc), and the actual IO managers would be implemented using inheritance and implementing the virtual functions.
The device-driver classes would then specify the capabilities needed for the IO manager by specifying the abstract base class that best summarizes their needs.
Although this approach would be perfectly fine on a PC, I don't think that on Arduino it would work fine.
In facts, for each e.g. digitalWrite call a virtual call would be needed for each IO manager involved; not only virtual calls are in general slower than "normal" calls, but they are also a lot more difficult to optimize, so I think that this slowdown would be tangible on such a slow machine.
Moreover, virtual calls require a vtable, which means that each IO manager would take more precious bytes of RAM to hold its vtable.

The other approach, instead, is the generic programming one (which, incidentally, is the one I chose, at least for now). The idea is that all the device-driver classes should be template classes accepting any type as IO manager; the background concept here is duck typing: if such class provides the required methods (e.g. digitalOut, pinMode), then it's fine to use it, without all the formalities associated with classic OOP. If the class passed as IO manager doesn't provide a needed method (e.g. it's an output-only IO manager that do not provide digitalRead, but the driver class tries to do a digitalRead) the compilation will fail, thus avoiding strange issues at runtime.
This has a big advantage: optimization. Since all the classes involved in the process are perfectly known at compile time, the compiler can optimize aggressively, often removing all the useless (from a computer perspective) abstraction layers and inlining whenever it's convenient.
On the disadvantages side, there's the verbosity of template classes declarations (that however can be reduced with typedefs and with newer C++0x features) and of the error messages if something goes wrong in the template.

To illustrate the idea, here's a little example:
without the 74HC595:
Code: [Select]

#include <ReWiring.h>
#include <RW-LiquidCrystal.h>

// Create the device driver for the LCD; it's connected to the pins 2, 3, 4, 5,
// 11, 12 of the ArdIOPorts, which is actually just a forwarder to the "normal"
// Wiring functions
RW::LiquidCrystal<RW::DefaultIOManager> lcd(12, 11, 5, 4, 3, 2, RW::ArdIOPorts);

void setup() {
 // set up the LCD's number of columns and rows:
 lcd.begin(16, 2);
}

// just a random example from the LiquidCrystal library
void loop() {
 // set the cursor to (0,0):
 lcd.setCursor(0, 0);
 // print from 0 to 9:
 for (int thisChar = 0; thisChar < 10; thisChar++) {
  lcd.print(thisChar);
  delay(100);
 }

 // set the cursor to (16,1):
 lcd.setCursor(16,1);
 // set the display to automatically scroll:
 lcd.autoscroll();
 // print from 0 to 9:
 for (int thisChar = 0; thisChar < 10; thisChar++) {
   lcd.print(thisChar);
   delay(100);
 }
 // turn off automatic scrolling
 lcd.noAutoscroll();
 
 // clear screen for the next loop:
 lcd.clear();
}


with the 74HC595:
Code: [Select]

#include <ReWiring.h>
#include <RW-LiquidCrystal.h>

// Create the IO manager for the shiftout device; it's attached to the ports
// 2, 3, 4 of the RW::ArdIOPorts device manager (which is actually linked
// to the normal Arduino IO pins)
typedef RW::ShiftOutIOManager<RW::DefaultIOManager> soim_type;
soim_type soim(2, 3, 4, RW::ArdIOPorts, RW::MSBFirst);

// Create the device driver for the LCD; it's connected to the pins 2, 3, 4, 5,
// 6 of the shiftout, but the LiquidCrystal class just has to do "normal"
// digitalOut calls, it's the ShiftOutIOManager that handles the translation
// to commands for the shift out
RW::LiquidCrystal<soim_type> lcd(7, 6, 5, 4, 3, 2, soim);

void setup() {
 // set up the LCD's number of columns and rows:
 lcd.begin(16, 2);
}

// just a random example from the LiquidCrystal library
void loop() {
 // set the cursor to (0,0):
 lcd.setCursor(0, 0);
 // print from 0 to 9:
 for (int thisChar = 0; thisChar < 10; thisChar++) {
  lcd.print(thisChar);
  delay(100);
 }

 // set the cursor to (16,1):
 lcd.setCursor(16,1);
 // set the display to automatically scroll:
 lcd.autoscroll();
 // print from 0 to 9:
 for (int thisChar = 0; thisChar < 10; thisChar++) {
   lcd.print(thisChar);
   delay(100);
 }
 // turn off automatic scrolling
 lcd.noAutoscroll();
 
 // clear screen for the next loop:
 lcd.clear();
}

You'll notice that from the point of view of the RW::LiquidCrystal<> class (which is just the stock LiquidCrystal class with minor changes to make it use IO managers instead of Wiring functions) the addition of the shift register is transparent: all it has to do is to do IO with the digitalWrite functions provided by the given IO manager.

Naturally the scope of this thing is not limited to shift registers: one can create IO manager classes that work also over a serial communication line or over a network, or use them to remap outputs to go partially on real ports and partially on shift registers, ...


My question is: is anyone actually interested in this stuff? Do you think that it would be useful/nicer to use than what's already available, and that this is an idea worth developing? Do you think that there are better approaches to solve this problem?

Go Up