I'm writing firmware for my Arduino project, and I'm struggling with a clean, scalable hardware mapping structure. Initially, I had the following namespace:
namespace Motherboard {
extern DRV8825 xAxisMotor;
extern MP6500 yAxisMotor;
extern DRV8876 zAxisMotor;
extern ClickEncoder encoder;
extern SSD1331 display;
extern RGBLed rgbIndicator;
extern Relay heaterRelay;
void performMainSequence();
void begin(const EepromData &initialData);
}; // namespace Motherboard
Which I initialized like this:
namespace Motherboard {
//=================== Public ====================
DRV8825 xAxisMotor = {
23, // ENABLE PIN
25, // MS0 PIN
27, // MS1 PIN
29, // MS2 PIN
31, // RESET PIN
33, // SLEEP PIN
35, // STEP PIN
37, // DIRECTION PIN
39, // FAULT PIN
MOTHERBOARD_X_AXIS_MAX_SPEED,
MOTHERBOARD_X_AXIS_MIN_POSITION,
MOTHERBOARD_X_AXIS_MAX_POSITION,
};
MP6500 yAxisMotor = {
22, // ENABLE PIN
24, // MS1 PIN
26, // MS2 PIN
28, // SLEEP PIN
30, // STEP PIN
32, // DIRECTION PIN
34, // FAULT PIN
MOTHERBOARD_Y_AXIS_MAX_SPEED,
MOTHERBOARD_Y_AXIS_MIN_POSITION,
MOTHERBOARD_Y_AXIS_MAX_POSITION,
};
DRV8876 zAxisMotor = {
44, // ENABLE PIN
36, // PHASE PIN
38, // PMODE PIN
40, // SLEEP PIN
42, // FAULT PIN
MOTHERBOARD_Z_AXIS_MAX_SPEED};
ClickEncoder encoder = {
45, // A PIN
43, // B PIN
41, // SW PIN
2, // STEPS PER NOTCH
};
SSD1331 display = {
53, // CS PIN
48, // DC PIN
49, // RESET PIN
};
RGBLed rgbIndicator = {
13, // R PIN
12, // G PIN
11, // B PIN
RGBLed::COMMON_CATHODE, // COMMON
};
Relay heaterRelay = {
47 // SWITCH PIN
};
void performMainSequence() {
//
}
void begin(const EepromData &initialData) {
//
}
//===============================================
}; // namespace Motherboard
A senior in my company saw this piece of code, suggested moving this to a class and create interfaces for hardware related classes. Therefore, I've created this:
struct MotherboardDevices {
IStepperDriver* xAxisMotor;
IStepperDriver* yAxisMotor;
IDcDriver* zAxisMotor;
ISwitch* heaterRelay;
RGBLed* rgbIndicator;
ClickEncoder* encoder;
IDisplay* display;
};
class Motherboard {
public:
MotherboardDevices devices;
Motherboard();
Motherboard(const MotherboardDevices _devices);
void motherboardIsr();
void performMainSequence();
void begin(const EepromData &initialData);
};
I was struggling with the way of initializing this class. I eventualy created separate file:
namespace Hardware {
namespace {
DRV8825 initXAxisMotor() {
DRV8825Pinout xAxisPinout = {
23, // ENABLE PIN
25, // MS0 PIN
27, // MS1 PIN
29, // MS2 PIN
31, // RESET PIN
33, // SLEEP PIN
35, // STEP PIN
37, // DIRECTION PIN
39, // FAULT PIN
};
DRV8825 xAxisMotor = {
xAxisPinout,
MOTHERBOARD_X_AXIS_MAX_SPEED,
MOTHERBOARD_X_AXIS_MIN_POSITION,
MOTHERBOARD_X_AXIS_MAX_POSITION,
};
return xAxisMotor;
}
MP6500 initYAxisMotor() {
MP6500Pinout yAxisPinout = {
22, // ENABLE PIN
24, // MS1 PIN
26, // MS2 PIN
28, // SLEEP PIN
30, // STEP PIN
32, // DIRECTION PIN
34, // FAULT PIN
};
MP6500 yAxisMotor = {
yAxisPinout,
MOTHERBOARD_Y_AXIS_MAX_SPEED,
MOTHERBOARD_Y_AXIS_MIN_POSITION,
MOTHERBOARD_Y_AXIS_MAX_POSITION,
};
return yAxisMotor;
}
DRV8876 initZAxisMotor() {
DRV8876Pinout zAxisPinout = {
44, // ENABLE PIN
36, // PHASE PIN
38, // PMODE PIN
40, // SLEEP PIN
42, // FAULT PIN
};
DRV8876 zAxisMotor = {
zAxisPinout,
MOTHERBOARD_Z_AXIS_MAX_SPEED,
};
return zAxisMotor;
}
Relay initHeaterRelay() {
Relay heaterRelay = {
47 // SWITCH PIN
};
return heaterRelay;
}
SSD1331 initDisplay() {
SSD1331 display = {
53, // CS PIN
48, // DC PIN
49, // RESET PIN
};
return display;
}
ClickEncoder initEncoder() {
ClickEncoder encoder = {
45, // A PIN
43, // B PIN
41, // SW PIN
2, // STEPS PER NOTCH
};
return encoder;
}
RGBLed initRgbIndicator() {
RGBLed rgbIndicator = {
13, // R PIN
12, // G PIN
11, // B PIN
RGBLed::COMMON_CATHODE, // COMMON
};
return rgbIndicator;
}
MotherboardDevices createDevices() {
static auto xAxisMotor = initXAxisMotor();
static auto yAxisMotor = initYAxisMotor();
static auto zAxisMotor = initZAxisMotor();
static auto heaterRelay = initHeaterRelay();
static auto rgbIndicator = initRgbIndicator();
static auto encoder = initEncoder();
static auto display = initDisplay();
return {
&xAxisMotor,
&yAxisMotor,
&zAxisMotor,
&heaterRelay,
&rgbIndicator,
&encoder,
&display,
};
}
MotherboardDevices devices = createDevices();
} // namespace
Motherboard motherboard = Motherboard(devices);
} // namespace Hardware
I feel like now it's messier than before and I could've done that better. It also creates a problem with my UI layer. I'm using ArduinoMenu library for creating UI menu system. This UI is built using macros and needs static, compile time access to hardware related methods. That means I can't use dependency injection and pass created Motherboard object to UI class. I would have to pass global Motherboard instance via extern which is ugly. It also doesn't make sense to create multiple instance of this class.
Would keeping this structure as namespace with hardware interfaces better? It would be statically available for the UI, there wouldn't be a way of creating multiple instances. I could initialize all the hardware in the related to header, cpp file. However, what I would essentially do here is create a bunch of global variables. I also don't like the idea of the possibility to modify this namespace from every place.
Edit, adding more context
Motherboard::devices would be accessed from non members, UI event handler example:
result updateZAxisSpeed(eventMask event) {
if (event == enterEvent) {
Hardware::motherboard.devices.zAxisMotor->start();
} else if (event == updateEvent) {
uint8_t updatedValue = EepromManager::currentSettings.zSpeed;
Hardware::motherboard.devices.zAxisMotor->setSpeed(updatedValue);
} else if (event == exitEvent) {
EepromManager::syncEeprom();
Hardware::motherboard.devices.zAxisMotor->stop();
}
return proceed;
}
Motherboard::begin is a late initialization method. It configures provided devices, like:
devices.encoder->setAccelerationEnabled(false);
devices.encoder->setButtonHeldEnabled(false);
devices.encoder->setDoubleClickEnabled(false);
Motherboard::performMainSequence method:
void Motherboard::performMainSequence() {
devices.heaterRelay->toggleOn();
delay(5000);
long xHalfWay = devices.xAxisMotor->getEndPosition() / 2;
devices.yAxisMotor->moveToEnd();
devices.xAxisMotor->moveTo(xHalfWay);
devices.zAxisMotor->start();
devices.xAxisMotor->moveToEnd();
delay(2000);
devices.xAxisMotor->moveTo(xHalfWay);
devices.zAxisMotor->stop();
devices.heaterRelay->toggleOff();
devices.xAxisMotor->moveToStart();
devices.yAxisMotor->moveToStart();
}
Motherboard::motherboardIsr() is an interrupt routine:
void Motherboard::motherboardIsr() {
devices.encoder->service();
}
When Motherboard was a namespace I could just directly asign service() method of my encoder to microcontroller's timer interrupt. Since interrupts needs global context and Motherboard now is a class I had to expose isr routine method and asign it globally:
Timer1.attachInterrupt([]() { Hardware::motherboard.motherboardIsr(); });
Motherboard::devices
going to be accessed directly by non-member functions? What domotherboardIsr()
,performMainSequence()
andbegin()
do? \$\endgroup\$