To finish last week’s discussion about C++ classes for I2C buses and devices – here’s the nasty bit… syntax!
The PortI2C class is defined as follows in Ports.h (I’ve omitted some of the internal details):
class PortI2C : public Port {
...
public:
enum { KHZMAX = 1, KHZ400 = 2, KHZ100 = 9 };
PortI2C (uint8_t num, uint8_t rate =KHZMAX);
uint8_t start(uint8_t addr) const;
void stop() const;
uint8_t write(uint8_t data) const;
uint8_t read(uint8_t last) const;
};
Couple of things to note:
- PortI2C is a subclass of Port (defined here), which handles raw I/O for one port (1..4 or 0)
- there’s an “enum” which defines some constants, specifically for PortI2C use
- there’s a “constructor” which takes two arguments (the second one is optional)
- there are four member functions available to any instance of class PortI2C
But that’s not all. Since PortI2C is a subclass of public Port, all the members of the Port class are also available to PortI2C instances. So even when using a PortI2C instance as I2C bus, you could still control the IRQ pin on it (mode3(), digiWrite3(), etc). An I2C port is a regular port with I2C extensions.
Note this line:
PortI2C (uint8_t num, uint8_t rate =KHZMAX);
This is the constructor of the PortI2C class, since it has the same name as the class. You never call it directly, it gets called automatically whenever a new instance of PortI2C is declared.
This constructor takes one or two arguments: the last argument can be omitted, in which case it will get a default value of KHZMAX, which is the constant 1, as defined in the preceding enum.
Note that the first argument is required. The following instance declaration will generate a compile error:
PortI2C myPort; // compile-time error!
There’s no way to create an instance of a port without specifying its port number (an int from 1 to 4, or 0). Instead, you have to use either one of the following lines:
PortI2C myPort (3); // this defaults to KHZMAX ...
PortI2C myPort (3, PortI2C::KHZ400); // ... or you can specify the rate
And this is where things get nasty: PortI2C is a subclass of Port, which also has a constructor requiring a port number. So the PortI2C constructor somehow has to pass this information to the Port constructor. To see how this is done, look at the PortI2C constructor function, defined in Ports.cpp:
PortI2C::PortI2C (uint8_t num, uint8_t rate)
: Port (num), uswait (rate)
{
sdaOut(1);
mode2(OUTPUT);
sclHi();
}
This is the PortI2C constructor, and welcome to the murkier side of C++:
- “PortI2C::PortI2C” is the way to refer to PortI2C’s constructor function
- “blah::blah (…) : Port (…) { … }” is how the Port subclass constructor gets called
- “blah::blah (…) : uswait (…) { … }” is used to initialize the “uswait” member variable
- note that the “=KHZMAX” optional value for the 2nd arg is not repeated in this code
To summarize: the above code is called when you create a instance (such as “PortI2C myPort (3);”), and what it’ll do (in that order), is:
- initialize the included Port instance it is based on, passing it the 1st arg
- set the uswait member variable (which is part of the PortI2C instance) to the 2nd arg
- call sdaOut(), mode2(), and sclHi(), all defined in the Port and PortI2C classes
It gets worse. Let’s have a look at the definition of class DeviceI2C:
class DeviceI2C {
const PortI2C& port;
uint8_t addr;
public:
DeviceI2C(const PortI2C& p, uint8_t me)
: port (p), addr (me << 1) {}
bool isPresent() const;
uint8_t send() const { ... }
uint8_t receive() const { ... }
void stop() const { ... }
uint8_t write(uint8_t data) const { ... }
uint8_t read(uint8_t last) const { ... }
uint8_t setAddress(uint8_t me) { ... }
};
DeviceI2C is not a subclass, but it does need to refer to the PortI2C instance specified as 1st argument and remember the bus address. The way this is done is through member variables “port” and “addr”. These are defined at the top of the class, and initialized in the DeviceI2C constructor.
The reason we can’t use subclassing here, is that a device is not a port, it’s merely associated with a port and I2C bus, since multiple devices can coexist on the bus. The “&” notation is a bit like the “*” pointer notation in C, I’ll refer you to C++ language documentation for an explanation of the difference. It’s not essential here.
Not being a subclass of PortI2C, means we can’t simply send I2C packets via send(), write(), etc. Instead, we have to go through the “port” variable. Here’s the above write() member function in more detail:
uint8_t write(uint8_t data) const { return port.write(data); }
In other words, instead of simply calling “write()”, we have to call “port.write()”. No big deal.
So much for the intricacies of C++ – I hope this’ll allow you to better read the source code inside JeeLib.
Isn’t the constant KHZMAX 1 instead of 9? So the constructor of PortI2C has to get a default value of 1 instead of 9 right?
Eagle eyes! Fixed, thx.