An exercise to build a C++ library for the GPIO pins. The programmer can decide what underlying mechanism is used to talk to the pins. In this second post, 3 mechanism are supported:
// #include <sysfsdevice_filehandler.h> // #include "sysfsdevice_stream.h" #include "memmapdevice.h" pin p21 = pin(21); |
Example program
In C++ gpio library for Raspberry Pi - Pt 1: Design and How To Use , I developed an OO class for Raspberry Pi GPIO handling. In that design, I created a single simple class that can set or get pin status. The handling was flexible: you could plug in a "driver" class that handles the actual pin management.
I also provided one such driver: the sysfsdevice class. It used the traditional Raspberry /sys/class/gpio filesystem to manage the pins. You could select that driver at compile time, using the C++ template mechanism.
#include "pin.h" #include "sysfsdevice.h" typedef dgpio::pin<dgpio::sysfsdevice> pin; int main() { pin p21 = pin(21); // p21.init(pin::dir::out, pin::status::off); // p21.set(pin::status::off); p21.init(pin::dir::in); pin::status status = pin::status::on; while (status != pin::status::off) { status = p21.get(); } p21.deinit(); }
That sysfsdevice used C++ streams to talk to the /sys/class/gpio files.
In this post I add 2 additional drivers:
- talk to the ARM CPU's registers, and
- use the traditional C functions to talk to the /sys/class/gpio files.
That leaves us with 3 implementations that a developer can choose from:
- dgpio::sysfsdevice_filehandler
- dgpio::sysfsdevice_stream (the original one from post 1. I renamed it to distinguish it from the traditional filehandler implementation)
- dgpio::memmapdevice
image: UML diagram
In the example, I provided a way to switch between mechanisms, with a define. You can select your desired template by uncommenting the relevant define line:
// uncomment the gpio handler that you want to use (enable one of the defines) // #define DPGIO_SYSFS_FILEHANDLER // #define DPGIO_SYSFS_STREAM #define DPGIO_MEMMAP #include "pin.h" #if defined DPGIO_SYSFS_FILEHANDLER #include <sysfsdevice_filehandler.h> typedef dgpio::pin<dgpio::sysfsdevice_filehandler> pin; #elif defined DPGIO_SYSFS_STREAM #include "sysfsdevice_stream.h" typedef dgpio::pin<dgpio::sysfsdevice_stream> pin; #elif defined DPGIO_MEMMAP #include "memmapdevice.h" typedef dgpio::pin<dgpio::memmapdevice> pin; #endif
Your code itself will not change, whatever template you plug in:
int main() { pin p21 = pin(21); // out test pin pin p23 = pin(23); // in test pin // out test p21.init(pin::dir::out); p21.set(pin::status::off); pin::status s = p21.get(); p21.set(pin::status::on); s = p21.get(); p21.set(pin::status::off); // in test p23.init(pin::dir::in); s = pin::status::on; while (s != pin::status::off) { s = p23.get(); } p21.deinit(); p23.deinit(); }
The Library Code
The Pin class code hasn't changed since post 1. The new templates just plug in. I will not repeat that classe's code here
Register plugin class memmapdriver:
header
#ifndef MEMMAPDEVICE_H_ #define MEMMAPDEVICE_H_ #include <fcntl.h> // for open #include <sys/mman.h> // for mmap #include "pin.h" namespace dgpio { class memmapdevice { public: static void init(unsigned gpio, pin<memmapdevice>::dir dir, pin<memmapdevice>::status status = pin<memmapdevice>::status::off); static void deinit(unsigned gpio) { // https://linux.die.net/man/2/close // It is probably unwise to close file descriptors while they may // be in use by system calls in other threads in the same process } static pin<memmapdevice>::status get(unsigned gpio) { return gpiomem[13] & (1 << gpio) ? pin<memmapdevice>::status::on : pin<memmapdevice>::status::off; // GPLEV0 } static void set(unsigned gpio, pin<memmapdevice>::status status) { // always set status first switch (status) { case pin<memmapdevice>::status::on: gpiomem[7] |= (1 << gpio); // GPSET0 break; case pin<memmapdevice>::status::off: gpiomem[10] |= (1 << gpio); // GPCLR0 break; } } private: static unsigned int *gpiomem; static int fdgpio; }; } // namespace dgpio #endif /* MEMMAPDEVICE_H_ */
C++
#include "memmapdevice.h" namespace dgpio { // static members to be initialised in the cpp file // memory mapped registers unsigned int *memmapdevice::gpiomem = nullptr; // gpio memory map file interface int memmapdevice::fdgpio = -1; void memmapdevice::init(unsigned gpio, pin<memmapdevice>::dir dir, pin<memmapdevice>::status status) { // inspiration // https://www.cs.uaf.edu/2016/fall/cs301/lecture/11_09_raspberry_pi.html if (fdgpio > 0) { // memmap already initialised? return; } // map gpio memory to gpiomem fdgpio=open("/dev/gpiomem", O_RDWR); if (fdgpio > 0) { gpiomem = (unsigned int *)mmap(0, 4096, PROT_READ+PROT_WRITE, MAP_SHARED, fdgpio,0); } set(gpio, status); unsigned int index = gpio / 10; // GPFSELn register to set this pin direction unsigned int offset = (gpio % 10) * 3; // lsb for this pin's 3 bit status in that register unsigned int curval = gpiomem[index]; curval &= ~0b111 << (offset); switch (dir) { case pin<memmapdevice>::dir::in: break; case pin<memmapdevice>::dir::out: curval |= 0b1 << (offset); break; } gpiomem[index] = curval; } } // namespace dgpio
Traditional filehandler plugin class sysfsdriver_filehandler:
header
#ifndef SYSFSDEVICE_FILEHANDLER_H_ #define SYSFSDEVICE_FILEHANDLER_H_ #include <string> #include "pin.h" namespace dgpio { class sysfsdevice_filehandler { public: static void init(unsigned gpio, pin<sysfsdevice_filehandler>::dir dir, pin<sysfsdevice_filehandler>::status status); static void deinit(unsigned gpio); static pin<sysfsdevice_filehandler>::status get(unsigned gpio); static void set(unsigned gpio, pin<sysfsdevice_filehandler>::status status); private: static bool exists (const unsigned gpio); static void exp(const unsigned gpio); static void unexp(const unsigned gpio); static const std::string device_path; inline static std::string directoryname(unsigned gpio) {return device_path + "/gpio" + std::to_string(gpio);} inline static std::string exportname() { return device_path + "/export";} inline static std::string unexportname() { return device_path + "/unexport";} inline static std::string directionname(unsigned gpio) { return directoryname(gpio) + "/direction";} inline static std::string valuename(unsigned gpio) { return directoryname(gpio) + "/value";} }; } // namespace dgpio #endif /* SYSFSDEVICE_FILEHANDLER_H_ */
C++
#include <filesystem> #include <string> #include <cassert> #include <fcntl.h> #include <sysfsdevice_filehandler.h> #include <unistd.h> namespace dgpio { const std::string sysfsdevice_filehandler::device_path = std::string("/sys/class/gpio"); bool sysfsdevice_filehandler::exists(const unsigned gpio) { return std::filesystem::is_symlink(directoryname(gpio)); } void sysfsdevice_filehandler::exp(const unsigned gpio) { FILE *f = fopen(exportname().c_str(), "w"); fprintf(f, std::to_string(gpio).c_str()); fclose(f); } void sysfsdevice_filehandler::unexp(const unsigned gpio) { FILE *f = fopen(unexportname().c_str(), "w"); fprintf(f, std::to_string(gpio).c_str()); fclose(f); } void sysfsdevice_filehandler::init(const unsigned gpio, const pin<sysfsdevice_filehandler>::dir dir, const pin<sysfsdevice_filehandler>::status status) { if (!exists(gpio)) { exp(gpio); } // while (!exists(gpio)) {} FILE *f = nullptr; while (f == nullptr) { // when you're not root, it takes a bit to get a valid pointer f = fopen(directionname(gpio).c_str(), "w"); } switch (dir) { case pin<sysfsdevice_filehandler>::dir::in: fprintf(f, "in"); break; case pin<sysfsdevice_filehandler>::dir::out: fprintf(f, "out"); break; } fclose(f); set(gpio, status); } void sysfsdevice_filehandler::deinit(const unsigned gpio) { if (exists(gpio)) { unexp(gpio); } } pin<sysfsdevice_filehandler>::status sysfsdevice_filehandler::get(const unsigned gpio) { pin<sysfsdevice_filehandler>::status retval = pin<sysfsdevice_filehandler>::status::off; char value_str[1]; int fd = open(valuename(gpio).c_str(), O_RDONLY); read(fd, value_str, 1); if (value_str[0] == '0') { retval = pin<sysfsdevice_filehandler>::status::off; } else if (value_str[0] == '1') { retval = pin<sysfsdevice_filehandler>::status::on; } else { assert(false); // invalid } close(fd); return retval; } void sysfsdevice_filehandler::set(const unsigned gpio, const pin<sysfsdevice_filehandler>::status status) { FILE *f = fopen(valuename(gpio).c_str(), "w"); switch (status) { case pin<sysfsdevice_filehandler>::status::off: fprintf(f, "0"); break; case pin<sysfsdevice_filehandler>::status::on: fprintf(f, "1"); break; } fclose(f); } } // namespace dgpio
And the original stream handler from post 1, renamed from sysfsdriver to sysfsdriver_stream:
header
#ifndef SYSFSDEVICE_STREAM_H_ #define SYSFSDEVICE_STREAM_H_ #include <string> #include "pin.h" namespace dgpio { class sysfsdevice_stream { public: static void init(unsigned gpio, pin<sysfsdevice_stream>::dir dir, pin<sysfsdevice_stream>::status status); static void deinit(unsigned gpio); static pin<sysfsdevice_stream>::status get(unsigned gpio); static void set(unsigned gpio, pin<sysfsdevice_stream>::status status); private: static bool exists (const unsigned gpio); static void exp(const unsigned gpio); static void unexp(const unsigned gpio); static const std::string device_path; inline static std::string directoryname(unsigned gpio) {return device_path + "/gpio" + std::to_string(gpio);} inline static std::string exportname() { return device_path + "/export";} inline static std::string unexportname() { return device_path + "/unexport";} inline static std::string directionname(unsigned gpio) { return directoryname(gpio) + "/direction";} inline static std::string valuename(unsigned gpio) { return directoryname(gpio) + "/value";} }; } // namespace dgpio #endif /* SYSFSDEVICE_STREAM_H_ */
C++
#include <sysfsdevice_stream.h> #include <filesystem> #include <fstream> #include <string> #include <cassert> #include <iostream> namespace dgpio { const std::string sysfsdevice_stream::device_path = std::string("/sys/class/gpio"); bool sysfsdevice_stream::exists(const unsigned gpio) { return std::filesystem::is_symlink(directoryname(gpio)); } void sysfsdevice_stream::exp(const unsigned gpio) { std::ofstream of(exportname()); of << std::unitbuf; // unbuffered of << gpio; of.close(); } void sysfsdevice_stream::unexp(const unsigned gpio) { std::ofstream of(unexportname()); of << std::unitbuf; // unbuffered of << gpio; of.close(); } void sysfsdevice_stream::init(const unsigned gpio, const pin<sysfsdevice_stream>::dir dir, const pin<sysfsdevice_stream>::status status) { if (!exists(gpio)) { exp(gpio); } std::ofstream of; while(!of.is_open()) { // when you're not root, it takes a bit to get a valid open file of.open(directionname(gpio)); } of << std::unitbuf; // unbuffered switch (dir) { case pin<sysfsdevice_stream>::dir::in: of << "in"; break; case pin<sysfsdevice_stream>::dir::out: of << "out"; break; } of.close(); set(gpio, status); } void sysfsdevice_stream::deinit(const unsigned gpio) { if (exists(gpio)) { unexp(gpio); } } pin<sysfsdevice_stream>::status sysfsdevice_stream::get(const unsigned gpio) { std::ifstream is(valuename(gpio)); std::string value; pin<sysfsdevice_stream>::status retval = pin<sysfsdevice_stream>::status::off; is >> value; is.close(); if (value == "0") { retval = pin<sysfsdevice_stream>::status::off; } else if (value == "1") { retval = pin<sysfsdevice_stream>::status::on; } else { assert(false); // invalid } return retval; } void sysfsdevice_stream::set(const unsigned gpio, const pin<sysfsdevice_stream>::status status) { std::ofstream of(valuename(gpio)); of << std::unitbuf; // unbuffered switch (status) { case pin<sysfsdevice_stream>::status::off: of << "0"; break; case pin<sysfsdevice_stream>::status::on: of << "1"; break; } of.close(); } } // namespace dgpio
But...
There is a 4th way to talk to the GPIOs. The sysfs method (/sys/class/gpio)is actually deprecated, and to be replaced with the userspace GPIO driver ( /dev/gpiochip#). Once I figure out how that works, it should be straightforward to write a plugin driver class for that. Let me know if you have experience with that new driver ...
also: don't miss out on the blinky! C++ gpio library for Raspberry Pi - Pt 3: Blinky!
runtime hit? There is virtually none. All constructs used here are compile time resolved. There's no dynamic lookups or virtual functions resolve. It's as if you call a C function. When you build a release version, with optimisation enabled and the non-used source files excluded from build, then only the code for the template that you selected will be in the firmware file. The example program that I made, has this firmware size for each of the mechanisms:
For baseline: the same applications, with all pin related code excluded: 8.220 bytes. Note that this baseline does not do anything with the pins. It's an indication for the individual solutions. It does not show how different the size is, confirmed to non-OO solution (because there's no standard GPIO API to compare to). |
Eclipse project and source code:
Eclipse project (contains sources and pre-built executable): device_gpio_20240106.zip
github repo: https://github.com/jancumps/cppgpio