You asked, yesterday:
I responded overnight with some stuff off the top of my head. Well, today I actually built the AVRPin class I talked about, so here is a small class and some explanations to hopefully make things a bit clearer.
Enjoy.
Here’s a proper working version of using member functions in a class. Hopefully I will explain matters clearly enough for you to understand - but ask away if not.
Here are the steps I went through to generate a small class to mimic an Arduino Pin.
# Create a directory:
mkdir -p ~/SourceCode/temp/PIO_AVRPin
cd ~/SourceCode/temp/PIO_AVRPin
pio init --board uno
cd include
vi AVRPin.h
The following code should be typed/copied and pasted into include/AVRPin.h:
#ifndef AVRPIN_H
#define AVRPIN_H
#ifndef ARDUINO
#define LOW 0
#define HIGH 1
#endif
#include <stdint.h>
#include <avr/io.h>
class AVRPin {
private:
// Which is our pin's PORTx register?
volatile uint8_t *portRegister;
// Which is our pin's DDRx register?
volatile uint8_t *ddrRegister;
// Which is our pin's PINx register?
volatile uint8_t *pinRegister;
// Which bit in all registers is for our pin?
uint8_t pinBit;
public:
// To refer to a pin, we do something like
// AVRpin LED(AVRpin::AAVRPIN_D13, AVRpin::OUTPUT_PIN);
typedef enum AVRpins : uint8_t {
AVRPIN_D0 = 0,
AVRPIN_D1,
AVRPIN_D2,
AVRPIN_D3,
AVRPIN_D4,
AVRPIN_D5,
AVRPIN_D6,
AVRPIN_D7,
AVRPIN_D8,
AVRPIN_D9,
AVRPIN_D10,
AVRPIN_D11,
AVRPIN_D12,
AVRPIN_D13,
AVRPIN_D14,
AVRPIN_D15,
AVRPIN_D16,
AVRPIN_D17,
AVRPIN_D18,
AVRPIN_D19,
AVRPIN_A0 = AVRPIN_D14,
AVRPIN_A1,
AVRPIN_A2,
AVRPIN_A3,
AVRPIN_A4,
AVRPIN_A5
} AVRpin_t;
// Pin modes.
typedef enum pinModes : uint8_t {
INPUT_PIN = 0,
INPUT_PULLUP_PIN,
OUTPUT_PIN
} pinMode_t;
// Constructor.
AVRPin(const AVRpin_t pin, pinMode_t mode);
// No destructor required.
// Turn pin HIGH.
void setHigh();
// Turn pin LOW.
void setLow();
// Toggle pin.
void toggle();
// Get pin state. HIGH or LOW.
uint8_t getState();
};
#endif // AVRPIN_H
The class definition in AVRPin.h
defines the “blueprint” for our “object” - which is an Arduino digital pin. The class name can be used as a new variable type in a source program. The same as int
, long
, String
(which is a class) etc.
We can see in the public section that the pin can be constructed to mimic a specific Arduino pin and be given a mode to operate in, all done by the constructor. Once an AVRPin object exists, it can be driven HIGH or LOW and can be toggled and the current state of the pin can be read at any time. These public functions can be used to make the pin do something.
Those functions which do this are the member functions you were asking about. There’s nothing special, they are functions, which are members of a class.
The private section holds a few variables which we don’t want to be accessed from outside our class. These would relate to static
variables in a C++ source file, those are only visible inside functions defined in that source file. Private variables (and functions) can only be called or used from within the class.
The variable here hold the PORTx
, DDRx
and PINx
register addresses, so that we can manipulate the pin by directly accessing the registers. This is what the Arduino Language does in functions like pinMode()
, digitalWrite()
, digitalRead()
etc. There’s no need for these to be accessed outside of the AVRPin object(s), so they are kept private.
In this example, there are no private member functions though, all of those are public.
Next up, we need to implement the class:
cd ../src
vi AVRPin.cpp
The following code should be typed/copied and pasted into src/AVRPin.cpp:
#include "AVRPin.h"
// Constructor.
AVRPin::AVRPin(const AVRpin_t pin, pinMode_t mode) {
// Calculate the PORT, PIN and DDR from the pin.
// D0 through D7 = PORTD,
// D8 through D13 = PORTB,
// D14 (A0) through D19 (A5) = PORTC.
if (pin <= AVRPIN_D7) {
// We are in D registers.
portRegister = &PORTD;
pinRegister = &PIND;
ddrRegister = &DDRD;
// PinBit is the same as the pin.
pinBit = pin;
} else if (pin <= AVRPIN_D13) {
// We are in B registers.
portRegister = &PORTB;
pinRegister = &PINB;
ddrRegister = &DDRB;
// PinBit is the pin - 8
pinBit = pin - 8;
} else {
// We are in C registers.
portRegister = &PORTC;
pinRegister = &PINC;
ddrRegister = &DDRC;
// PinBit is the pin - 14
pinBit = pin - 14;
}
// Now we have the registers, configure the pin.
if (mode == INPUT_PIN) {
*ddrRegister &= ~(1 << pinBit);
*portRegister &= ~(1 << pinBit);
} else if (mode == INPUT_PULLUP_PIN) {
*ddrRegister &= ~(1 << pinBit);
*portRegister |= (1 << pinBit);
} else {
// OUTPUT_PIN
*ddrRegister |= (1 << pinBit);
}
}
// Turn pin HIGH.
void AVRPin::setHigh() {
*portRegister != (1 << pinBit);
}
// Turn pin LOW.
void AVRPin::setLow() {
*portRegister &= ~(1 << pinBit);
}
// Toggle pin.
void AVRPin::toggle() {
*pinRegister |= (1 << pinBit);
}
// Get pin state. HIGH or LOW.
uint8_t AVRPin::getState() {
return !!(*pinRegister & (1 << pinBit));
}
The class implementation in AVRPin.cpp
builds up the meat onto the bones of the blueprint. Every function defined in the class definition is prefixed with the class name - AVRPin::
the ::
bit is just the scope operator. It says that, for example, the implementation of the function toggle()
which follows, is a member function of the class AVRPin
. Other classes can have the same function name, but they will be scoped into their own class. There’s no conflict in using the same function or variable names in different classes.
The class defines a couple of types which are used when defining the name of a new pin and the mode that pin is to be operated in. So, a new AVRPin is defined as follows:
AVRPin D13(AVRPin::AVRPIN_D13, AVRPin::OUTPUT_PIN);
Creating a pair of enum
s in the manner i did also scopes them to the AVRPin class. It also is a sneaky manner of getting parameter validation done “for free” by the compiler. I can only supply a pin name, or corresponding number, within the correct range, so I don’t have to do any worrying about parameter validation.
I had to give the pin names and modes different names to that used in the Arduino Language to avoid conflict.
The rest of the code just implements the various member functions using direct register access to set high, set low and toggle a particular pin.
Now we need a test file to see if the class can be used or not:
vi main.cpp
The following code should be typed/copied and pasted into src/main.cpp:
#include "Arduino.h"
#include "AVRPin.h"
AVRPin D13(AVRPin::AVRPIN_D13, AVRPin::OUTPUT_PIN);
void setup() {
Serial.begin(9600);
}
void loop() {
// Toggle the pin every second.
D13.toggle();
Serial.print("The state of the pin is: ");
Serial.println(D13.getState());
delay(1000);
}
The code in main.cpp
is the ubiquitous blink sketch reduced to a couple of lines. It creates a pin attached to D13, the built in LED. The constructor converts AVRPIN_D13
from a number into PORTB
, DDRB
and PINB
registers, and also, into the value 5 for the pinBit
member variables. Once we have those, we have total control over the pin.
In the loop()
function, we toggle the pin and read the state of the pin and display it on the monitor.
This code compiled down to 704 bytes without the Serial stuff, and 1,806 bytes with the Serial stuff left in. The default blink sketch in the Arduino IDE compiles to 924 bytes plus 9 bytes of Static RAM. My code is smaller and, I think, easier to read and use? I hope!
I hope this small example clears up some of the worries you have about classes and member functions.
Cheers,
Norm.