Hi there, Wouter wrote me an email regarding xpcc and below is my original long answer with a minor edit regarding connecting GPIOs in the configuration handler. Cheers, Niklas
Hi Niklas
I stumbled on your xpcc work, which seems very interesting. After a little brouwsing of the examples, sources and documentation I have a lot of questions about the basic architecture. Do you have any blogs or other documentation about that? Eaxmples of things I would like to know
- how does the efficiency (speed, code size) compare to non-OO (plain C) code - do you use run-time objects for GPIO, LCD, extenders, etc, or do they 'vanish' at compiler time - is configurability limited to compile-time, or can you change for instance the I2C pins at run-time? - are the pins 'provided' by an extender as usable as normal GPIO pins (for instance to connect and LCD, or a cascaded extender)? - what kind of error messages does the user get when inappropriate (template) arguments are provided?
An idea of what I am doing: - https://www.youtube.com/watch?v=k8sRQMx2qUw - www.voti.nl/blog
best regards, Wouter van Ooijen
Hi Wouter, this is going to be a long reply, with the idea that with your permission I would like to post this reply on our mailing list [1] for the benefit of our other developers. [1]: https://mailman.rwth-aachen.de/mailman/listinfo/xpcc-dev, http://blog.gmane.org/gmane.comp.hardware.arm.cortex.xpcc.devel
An idea of what I am doing: - https://www.youtube.com/watch?v=k8sRQMx2qUw - www.voti.nl/blog
I’ve seen your talk, and I agree 110% with your ideas. The GPIO (and most other peripheral) implementation in xpcc is based on the same principles of static polymorphism as you described, however expands on these concepts quite extensively. We have held a talk at FOSDEM’14 [2], however, with only ~20min we could only touch the surface, and we did not explain the solutions as well as you did (we were young and dumb ;-). The talk also describes our build system for generating and customising all peripheral classes out of our meta-template library. I think it is very difficult to describe the advantages of this concept to developers who are not C++ experts or do not have a theoretical background (ie. software patterns) in software engineering. Typically computer scientists or software engineers do not program microcontroller software. [2]: https://archive.fosdem.org/2014/schedule/event/the_xpcc_microcontroller_fram...
I stumbled on your xpcc work, which seems very interesting. After a little brouwsing of the examples, sources and documentation I have a lot of questions about the basic architecture. Do you have any blogs or other documentation about that?
We have a doxygen documentation that is relatively complete [3], however, it is not the right format to describe conceptual ideas. Therefore we have a development blog [4], which goes very much into the details of the problem and presents our solutions in a more scientific way than just doxygen. An interesting derivation on the concept of static polymorphism is how we compute baudrates at compile time. This is actually an object-oriented approach, however, doesn’t even exist at runtime, neither as memory nor as code. Most embedded C developers declare this to be black magic ;-) There are several more topics that we want to describe [5] as such detailed blog posts, but of course, we are still students and we spend most of our time building robots, so the progress is slow on this xpcc “side-project”.\ [3]: http://xpcc.io/api/modules.html [4]: http://blog.xpcc.io [5]: https://github.com/roboterclubaachen/xpcc-blog/wiki
Eaxmples of things I would like to know
- how does the efficiency (speed, code size) compare to non-OO (plain C) code
This is a difficult question, since most concepts are not directly transferable to plain C code, or you wouldn’t use them like that. There is also link-time or whole-program optimizations, which can produce some amazing results. I recommend these slides [6] from for an in-depth exploration of C vs. C++ and what kind of “overhead” can be (not) expected. They are in German, I cannot find the English version. xpcc started as a API for AVRs and for a long time we used AVRs to build our Eurobot robots [7]. So there was a real-world requirement to create a very efficient API for it. Since we are one of the very few if not the only framework that supports both AVR and ARM with the same API, the efficiency also translates to the ARM implementations. In practice this means: - short functions (one-liners, such as Gpio set/reset) are forced to be inlined whenever possible, - virtual functions are not used in the platform driver API, ie. full static polymorphism, - complex computations are moved to compile time, like the aforementioned baudrate computation, and - specialisation and customisation happens through code generation from our meta-template library, using device files as source [8]. This generates very efficient code in the sense, that the code requires none to very little RAM and if very fast due to inlining and compile time computations. Also note that there are no templates involved here, since we “manually” generate the classes that the compiler would generate out of templates. For example, here are all available GPIO classes on the ATmega328 [9], or the STM32F407 [10]. This allows us to extensively specialise the peripheral drivers at zero cost. An example of this are the GPIO `connect` methods, that are generated from the information in the device drivers [11] and allow a typesafe and optimized setting of the pins alternate functions [12]. This is not possible with templates, however, especially useful when dealing with inconsistent hardware memory maps, as found on many AVRs, which do not have consistent register definitions [13]. We confirm the most timing critical implementations, such as GPIO, by counting cycles (at least on Cortex-M3s with built-in Trace Units) and/or looking at the generated assembly. The fact that we can run the same code on an ATtiny that we run on an STM32 speaks for the low memory footprint. [6]: http://aristeia.com/TalkNotes/C++_Embedded_Deutsch.pdf [7]: https://www.youtube.com/watch?feature=player_detailpage&v=K7obV0avUoQ#t=25929 [8]: https://github.com/roboterclubaachen/xpcc/blob/develop/src/xpcc/architecture... [9]: http://xpcc.io/api/group__atmega328p__gpio.html [10]: http://xpcc.io/api/group__stm32f407vg__gpio.html [11]: https://github.com/roboterclubaachen/xpcc/blob/develop/src/xpcc/architecture... [12]: http://xpcc.io/api/structxpcc_1_1stm32_1_1_gpio_output_a2.html#a922dc0b8d948... [13]: https://github.com/roboterclubaachen/xpcc/blob/develop/src/xpcc/architecture...
- do you use run-time objects for GPIO, LCD, extenders, etc, or do they 'vanish' at compiler time
GPIOs as well as almost all peripheral drivers have no run-time representation, so their “objects" do indeed “vanish” at compile time. However, communication and bus interfaces do contain static data, such as Uart [14] (which uses two atomic buffers for rx, tx) and the SpiMaster [15] and I2cMaster [16] (which manage their own resources and allow for safe multi-device access). In particular, the I2C device drivers are quite complicated under the hood, since they allow a generic protocol customization at run-time using virtual function calls [17]. You need this for this custom EEPROM driver, which performs one I2C write for the address and then another for the data, without copying the data [18]. We also have an extensive concept for cooperative, stackless multitasking, based on Dunkels Protothreads [19], which we expanded on with Resumable Functions [20] which are used in all device drivers to make communication non-blocking and allow multiple device drivers on the same bus to safely access it. This form of multithreading is very resource friendly since it does not require context switches and only the main call stack (literally 2 bytes of RAM per Protothread). However, this is quite an advanced topic and there are disadvantages to this approach compared to a full real-time operating system (power efficiency, hard real-time), such as mbed os or riot-os, however, they do not necessarily work well on AVRs. So there are are many types of “objects" and different definitions of “overhead”. Of course, using virtual functions in the I2cTransaction is an overhead, however, the complexity of the protocol demands it. Of course, the resource management requires a few more bytes (literally), however, the nature of multitasking demands it. Of course, the protothreads are polling based, however, there is no real alternative for light-weight multitasking the AVR. [14]: http://xpcc.io/api/classxpcc_1_1_uart.html [15]: http://xpcc.io/api/classxpcc_1_1_spi_master.html [16]: http://xpcc.io/api/classxpcc_1_1_i2c_master.html [17]: http://xpcc.io/api/classxpcc_1_1_i2c_transaction.html [18]: https://github.com/roboterclubaachen/xpcc/blob/develop/src/xpcc/driver/stora... [19]: http://xpcc.io/api/group__protothread.html#details [20]: http://xpcc.io/api/group__resumable.html#details
- is configurability limited to compile-time, or can you change for instance the I2C pins at run-time?
In general no, but if the hardware allows for multiple I2C pins at the same I2cMaster then yes. So for example, the SoftwareI2cMaster class [21] bitbangs the protocol on two pins as template parameters, which cannot be changed at runtime. Of course you could use a second SoftwareI2cMaster class for the other two pins, but you cannot move your device driver over to this bus. typedef xpcc::SoftwareI2cMaster<GpioB1, GpioB2> I2cMaster1; typedef xpcc::SoftwareI2cMaster<GpioB3, GpioB4> I2cMaster2; xpcc::Pca9535<I2cMaster1> expander; // device always uses B1, B2 This is a little different for Hardware I2C, since the pins alternate function is configured at runtime in hardware: typedef xpcc::stm32::I2cMaster1 I2cMaster1; xpcc::Pca9535<I2cMaster1> expander; // device always uses I2cMaster1 GpioB1::connect(I2cMaster1::Sda); // remember the connect method from earlier? GpioB2::connect(I2cMaster1::Scl); I2cMaster1::initialize<...>(); // expander uses pins B1, B2 I2cMaster1::reset(); // stop all transactions GpioB3::connect(I2cMaster1::Sda); GpioB4::connect(I2cMaster1::Scl); I2cMaster1::initialize<...>(); // expander uses pins B3, B4 The same is possible for SPI and UART, if the pins have the right multiplexes of course! However, there is actually a much more interesting problematic going on with this interface. What do you do if you have multiple devices on a bus, and they have different settings? In xpcc you can reconfigure the bus on the fly using configuration callbacks [22]: void fastSpeed() { SpiMaster::initialize<systemClock, MHz20>(); SpiMaster::setBitOrder(SpiMaster::BitOrder::LsbFirst); } void slowSpeed() { SpiMaster::initialize<systemClock, MHz1>(); SpiMaster::setBitOrder(SpiMaster::BitOrder::MsbFirst); } xpcc::FastSpiDevice<SpiMaster, GpioB2> fastDevice; // GpioB2 = Chip Select xpcc::SlowSpiDevice<SpiMaster, GpioB3> slowDevice; // attach configuration callbacks: fastDevice.attachConfigurationHandler(fastSpeed); slowDevice.attachConfigurationHandler(slowSpeed); Now, whenever any device accesses the bus, the SpiMaster calls its configuration handler to reconfigure itself. The same is possible for the I2cMaster. Notice how this moves the platform-specific code, namely the speed and bit order, into the user code, while still remaining generic for the device driver. EDIT: You can of course also re-connect your GPIOs in the configuration methods! void slowSpeed() { GpioB3::connect(I2cMaster1::Sda); // yes, this works! GpioB4::connect(I2cMaster1::Scl); SpiMaster::initialize<systemClock, MHz1>(); SpiMaster::setBitOrder(SpiMaster::BitOrder::MsbFirst); } [21]: https://github.com/roboterclubaachen/xpcc/blob/develop/src/xpcc/architecture... [22]: http://xpcc.io/api/classxpcc_1_1_i2c_device.html
- are the pins 'provided' by an extender as usable as normal GPIO pins (for instance to connect and LCD, or a cascaded extender)?
There is no concept for that in our code generation, however, we have had to solve this issue before, when we wanted to drive a HD44780 display [23] using a I2C IO-expander. We suddenly had the problem, that we couldn’t pass the I2C expander to the LCD driver, since we never assumed that GPIOs could be dynamic ;-) So the solution was to create a class with the same static functions as expected by the driver and wrap the I2C expander like this: extern xpcc::Pca9535<I2cMaster1> expander; // either extern or static class GpioExt4 { … static void set() { expander.set(Pin::P4); } } Let me be clear: This is not efficient, due to the I2C bus access and its blocking! But it worked. I haven’t tried passing these GPIO classes to our SoftwareI2cMaster so that we can cascade another I2C expander as you suggested in your talk, but it might work ;-) [23]: http://xpcc.io/api/classxpcc_1_1_hd44780.html
- what kind of error messages does the user get when inappropriate (template) arguments are provided?
C++ Template errors are really terrible and cryptic. I don’t know why compiler developers hate us so much. We mainly use `static_assert` to confirm some basic functionality. An example would be the width of the bus in our `GpioPort` classes [25] or directly asserting the correct bus width in the HD44780 driver [26]. And of course there is the afore-mentioned baudrate computation. Unfortunately there is no way of confirming that a Template parameter confirms to a specific interface, however, this functionality called “ConceptC++” was actually a C++0x proposal [27], but it did not make it into C++11 nor C++14. Maybe C++17? [24]: https://www.youtube.com/watch?v=mGS2tKQhdhY [25]: https://github.com/roboterclubaachen/xpcc/blob/develop/src/xpcc/architecture... [26]: https://github.com/roboterclubaachen/xpcc/blob/develop/src/xpcc/driver/displ... [27]: http://www.generic-programming.org/languages/conceptcpp/tutorial/ Kind regards, Niklas