avrIO - C++11/17/20 abstractions to manipulate I/O ports of AVR8

Go To Last Post
19 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Hi folks!

 

I'm writing a C++11/17/20 library to manipulate the I/O ports of AVR-8. The purpose of the work is to raise the level of abstraction to operate I/O ports with zero-overhead in mind. It’s a header only library that doesn’t require any external dependency to be used, the only requirement is avr-gcc with at least C++11 support.

 

Optionally, C++20 Concepts are used to better error messages and code readability if the C++20 support is enabled.

 

One goal of the library is to allow the design of APIs that are flexible enough to accept pins of ports of different microcontrollers as also the possibility to adapt other abstractions that solve the same problem to operate with the concept provided by the library.

 

https://github.com/ricardocosme/avrIO

 

Contributions are very welcome :)

github.com/ricardocosme

avrIO: Operation of I/O port pins and I/O registers.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

> Contributions are very welcome

 

How about suggestions.

 

I think you have it backwards- the instance name should have the class attached to it. You then get the dot use of class things which an ide can then provide typing help and a list of available functions/things, plus you have no doubt about the source location of the class functions, and also eliminate the need to have a big set of function templates that are pin specific but are not in the pin class. Pin functions should be in a pin class, not outside the class.

 

For example, what is known about your out/high/low functions and what they allow for arguments- you cannot tell without seeking out the source (nor could you really know how many function templates with the same name are out there). Rather, if you keep the pin functions contained inside the pin class, you cannot misuse them and will not have any external name collisions, plus you eliminate the need for a number of function templates (you still get them, but they are inside the class working under the existing template parameters). Without dot access, you will be missing out on a big C++ advantage.

 

Instead of something that looks more like C -

//create friendly names

using led = pb0; //guessing that's one way to get a friendly name here

using sw = pc1;

 

//init

out(led);

in(sw, pullup);

 

//use

while(1){ if( is_high(sw) low(led); else high(led); }

 

you could have the pin functions attached to the instance-

//friendly names created + init

Pin<B0>          led { OUTPUT };

Pin<C1, LOWISON> sw  { PULLUP };

 

//use

while(1){ led.on( sw.isOn() ); }

 

 

 

Simple example for a mega328-

https://godbolt.org/z/d1h7W4

its all there and is minimal so is easy to read, and the only thing that would normally be different is having an mcu specific PINS enum in an mcu header. Although a template is in use, the word template occurs only once, and I would suspect someone not familiar with C++ could read this and mostly understand what is going on. It would probably be easier for a C coder to read this than its C equivalent.

 

 

Just a suggestion.

 

 

 

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

rcosme wrote:
... as also the possibility to adapt other abstractions that solve the same problem to operate with the concept provided by the library.
fyi

Tools and Tips | The Embedded Muse 412 by Jack Ganssle

[last topic]

Jan Ciger has an alternative to regbits, which I mentioned in the last issue:

[Kvasir]

"Dare to be naïve." - Buckminster Fuller

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

>How about suggestions.

Sure. Thank you for your time. Let's see what you bring to the table.

 

>I think you have it backwards- the instance name should have the class attached to it. You then get the dot use of class things which an ide can then provide typing help and a list of available functions/things, plus you have no doubt about the source location of the class functions, and also eliminate the need to have a big set of function templates that are pin specific but are not in the pin class. Pin functions should be in a pin class, not outside the class.

 

Right. The library was using the dot notation before the implementation of the mechanism that allows adaptability to other models. One of these models that I usually see is integer(int). We can't have something like 'int{}.high()'. I don't think that a representation like integer is a good one to abstract a pin, but this is the reality to libraries like avr-libc and, AFAIK, the Arduino. I wouldn't like to offer an abstraction that wants to monopolize the handling on I/O pins. I think that HALs which are offering a rigid solution are good options to start a project from zero, but it can be a pain to insert or mix it with something that is legacy. I lost how many times I was forced to use an avr-libc macro like PB0 to use some component in my project that I don't have one similar using a higher level of abstraction. If we invert this we can realize that someone would like to use a library like https://github.com/ricardocosme/... in a project that avr-libc is predominant. Instead of to be forced to use the model 'avr::io::pxn', the user can only write once an extension to call the above library using the PXN macros of avr-libc.

 

The model avr::io::pxn can have methods that dispatches the calls to the free functions: no problem. But, these methods can't be part of the concept avr::io::Pin because there are models to the concept which aren't classes, like an int. In the end, your demo below can use methods but an API should use free functions to be more flexible.

 

It sounds too much strong to me when you say "Pin functions should be in a pin class, not outside the class.". This is a danger in C++. One thing that many programmers don't realize is that C++ is not an OO language, actually it's a multi paradigm language in the sense that the better of C++ is when you are using the best paradigm to a specific part of your project. One big problem with this type of sentence is that you can tight algorithms to classes(models) that doesn't need to be in the class, and this is awful to code reusability and dependency. A function must be in the class when there are invariants to be protected. Otherwise, the decision is more flexible.

 

>For example, what is known about your out/high/low functions and what they allow for arguments- you cannot tell without seeking out the source (nor could you really know how many function templates with the same name are out there). Rather, if you keep the pin functions contained inside the pin class, you cannot misuse them and will not have any external name collisions, plus you eliminate the need for a number of function templates (you still get them, but they are inside the class working under the existing template parameters).

 

We can have both and use free functions to implement APIs.

 

>Without dot access, you will be missing out on a big C++ advantage.

 

Hmm, I would say that we don't need to be too much strong here.

 

>//friendly names created + init
>Pin<B0>          led { OUTPUT };
>Pin<C1, LOWISON> sw  { PULLUP };
This is good and don't collide with I want to offer.

>https://godbolt.org/z/d1h7W4

 

Good job! I didn't get the choice of a pair on/off() inside the class Pin. If I'm not loosing anything, I see that these actions are welcome to a device like a led but it doesn't make sense to a generic I/O port.

 

In the end I made some modifications based on ideas that your post and code brought to me:
1. {high,low}(pin) to {high,low}(pin, bool)
2. Functions isolated at avr/io/functions.hpp, concepts at avr/io/concepts.hpp and traits at avr/io/traits.hpp.
3. Constructor to the model pxn to allow initialization of a mode like input, pullup or output.
4. Renames the types pxn_t to Pxn. The user can use the global pxn or he can instantiate objects of the type Pxn to use the constructor to set the mode of the pin.
5. The model Pxn gains methods to the basic functions to allow a dot notation.
6. I took the liberty to replace my demo/basic.cpp to a demo/led.cpp based on yours, and, I also wrote a demo/{C++20/}api.cpp.

 

Your demo can now be written as:
 

#include <avr/io.hpp>
using namespace avr::io;
int main() {
    Pb0 led{output};
    Pc1 sw{pullup};
    while(true) {
        led.high(sw.is_low());
    }
}

 

github.com/ricardocosme

avrIO: Operation of I/O port pins and I/O registers.

Last Edited: Thu. Feb 4, 2021 - 07:16 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

rcosme wrote:
I'm writing a C++11/17/20 library to manipulate the I/O ports of AVR-8. The purpose of the work is to raise the level of abstraction to operate I/O ports with zero-overhead in mind

I am always shocked at that amount of programming language effort that be made for simple tasks. That's even a prime example of overhead in mind and an unsuccessful attempt to simplify things.

Last Edited: Thu. Feb 4, 2021 - 05:37 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

>I am always shocked at that amount of programming language effort that be made for simple tasks. That's even a prime example of overhead in mind and an unsuccessful attempt to simplify things.

 

Good point: simplicity. I would like to know more about your observation. We can use the demo that was presented in this topic to pursue a simpler one:

#include <avr/io.hpp>
using namespace avr::io;
int main() {
    Pb0 led{output};
    Pc1 sw{pullup};
    while(true) {
        led.high(sw.is_low());
    }
}

The following can be used to compile:

avr-g++ -std=c++17 -Os -mmcu=atmega328p -I$AVRIO_PATH/include led.cpp

The AVRIO_PATH points to the path that has the package of the avrIO.

 

How do you write this demo?

github.com/ricardocosme

avrIO: Operation of I/O port pins and I/O registers.

Last Edited: Thu. Feb 4, 2021 - 07:16 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

GermanFranz wrote:
an unsuccessful attempt to simplify things.

That's often the trouble with libraries (eg, ASF) - it can take as long to understand & get to grips with using the library as it would have done to just understand & get to grips with the "bare" hardware...

 

frown

Top Tips:

  1. How to properly post source code - see: https://www.avrfreaks.net/comment... - also how to properly include images/pictures
  2. "Garbage" characters on a serial terminal are (almost?) invariably due to wrong baud rate - see: https://learn.sparkfun.com/tutorials/serial-communication
  3. Wrong baud rate is usually due to not running at the speed you thought; check by blinking a LED to see if you get the speed you expected
  4. Difference between a crystal, and a crystal oscillatorhttps://www.avrfreaks.net/comment...
  5. When your question is resolved, mark the solution: https://www.avrfreaks.net/comment...
  6. Beginner's "Getting Started" tips: https://www.avrfreaks.net/comment...
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

You're trying to solve a difficult problem, if you are trying to write something that is useful to a wider audience than just your own projects.  While many use cases can be handled with abstractions for single pins, some IO requires modifying multiple pins at once.  One example would be setting 4 or all 8 bits on a port for interfacing with a HD44780.

 

My conclusion has been that universal cross-platform IO abstraction library is an unattainable goal.  I've had the best luck with some basic macros or inline C functions for GPIO, which get customized for the target platform.  This is how I've take code that I initially wrote to run on classic AVRs, and ported it to 8051 with the SDCC compiler.  If I had developed a sophisticated template-based abstraction library for the AVR, I would've had to start over with SDCC since it doesn't support C++.  For ARM Cortex-M targets, although it's not quite what I would do myself, I've settled on libopencm3 since the API is "good enough" and supports targets from STM32 to the SAMD.

 

I have no special talents.  I am only passionately curious. - Albert Einstein

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

>You're trying to solve a difficult problem, if you are trying to write something that is useful to a wider audience than just your own projects.

 

I'm trying to write something useful to anyone that who needs to manipulate single bits in the user application as also in the development of a library. For sure this work is useful to my projects.

 

>While many use cases can be handled with abstractions for single pins, some IO requires modifying multiple pins at once.  One example would be setting 4 or all 8 bits on a port for interfacing with a HD44780.

 

Yes, I agree with you and I have thought about this when I began the project.

 

github.com/ricardocosme

avrIO: Operation of I/O port pins and I/O registers.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

>One example would be setting 4 or all 8 bits on a port for interfacing with a HD44780.

 

Using up 8 pins (+2 or 3) to run an lcd is not worth giving up the pins for such a low bandwidth device. Using 4 data pins is easily done without the need to make them port consecutive and has the big advantage of being able to use any available pin where you can avoid using pins required for other peripherals. 8 pins could also be done this way, but why give up 4 pins.

 

//HD44780 driver, 4bit mode

//(write only, rw would be better so can read busy flag)

// [E,RS, D4,D5,D6,D7]
template<const PINS::PIN (&Pins_)[6]>
struct LcdChar_4bit_E_RS_D4D7 {

 

    //...

 

    /*--------------------------------------------------------------------------

        no read capability here, so cannot read busy flag, so...
        assume fastest possible clock for timing (20MHz), and highest times
        from lcd datasheet (lower voltage = higher times), so can just have
        fixed times to satisfy all possible clocks and voltages
    --------------------------------------------------------------------------*/
SA  write4bits      (u8 v) {                        //100ns+ (setup v, rcall)
                        E_.on();                    //(RS->EN tAS=60ns satisfied)
                        D4_.off();                  //50ns
                        D5_.off();                  //100ns
                        D6_.off();                  //150ns
                        D7_.off();                  //200ns
                        if( v bitand 1 ) D4_.on();  //300ns
                        if( v bitand 2 ) D5_.on();  //400ns
                        if( v bitand 4 ) D6_.on();  //500ns (EN tPW satisfied)
                        if( v bitand 8 ) D7_.on();  //600ns
                        //tDSW = 195ns = 4clks@20MHz
                        asm("rjmp . \n rjmp .");
                        E_.off();                   //800ns
                        //(EN tCYCE/1000ns will be easily satisfied)
                    }

 

    //...

}

 

using namespace PINS;
//[E,RS, D4,D5,D6,D7]
static constexpr PIN LcdPins[]{ PB1,PB0, PA7,PA6,PA5,PA4 };

//Lcd_HD44780< some driver(4bit, twi, i2c bitbang, whatever your lcd requires) >

Lcd_HD44780< LcdChar_4bit_E_RS_D4D7<LcdPins> > lcd; //4bit mode

 

Print( lcd, "Hello World\n\n" );

 

 

Not a lot of things require consecutive port pins but if needed you can still access a port just as easy as a pin. Partial port access is doable but way more work than benefit.

 

For something like an stm32 (and any other mcu), you can split the port into its own class so you can deal with the port specific things like clocks, lock, sleep, and also have access to the port registers in their full width-

https://godbolt.org/z/hK3hT1

but is not a requirement as there are a number of easy ways to deal with ports inside a pin class also (you already have access to the registers). Generic port wide use is just not needed much since we are in a serial era now. If speed is needed, you will have a peripheral designed for the job and will not be using generic pins for the task.

 

As a contrast to the above stm32 example, I had put up a C equivalent where I extracted the needed defines from a ~half dozen files to get a LL HAL gpio example to compile online-

https://godbolt.org/z/cjTexe

that's the kind of mcu world I prefer to avoid if possible, and C++ can help but you are on your own. You can compare what would be the end user code- lines 285-end for the C version, and lines 287-302 for the C++ version. Even when digging into the source, the C++ version is in one file (header only), one class, where the C version makes you do treasure hunts to find the nugget you are looking for on the trail of defines and source files.

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

rcosme wrote:
I'm trying to write something useful to anyone that who needs to manipulate single bits in the user application as also in the development of a library.
But this is the thing with HALs. Say I'm an end user and I want to program a mega328 (many do!) then do I spend the time I have available to simply learn the "traditional" way to set and rest IO bits:

PORTB |= (1 << 3);
PORTB &= ~(1 << 3);

or do I use that time to learn some "niche" API with C++ templates etc etc? 

 

As a consumer what would draw me to the "fancy" solution when the alternative is so easy anyway?

 

Yes, I understand that HAL is exactly that - Hardware Abstraction - but as we learn here so often many (most?) working with micros simply pick one family, perhaps even one single model then always try to shoehorn whatever it is they want to achieve into that single model because that's the one they know.

 

So how do you propose to break people out of that mindset?

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

clawson wrote:

But this is the thing with HALs. Say I'm an end user and I want to program a mega328 (many do!) then do I spend the time I have available to simply learn the "traditional" way to set and rest IO bits:

PORTB |= (1 << 3);
PORTB &= ~(1 << 3);

or do I use that time to learn some "niche" API with C++ templates etc etc?


First things first. The ideal is to first learn the "traditional" way. This gives the foundation that enhances the quality of any work and also it brings confidence and efficiency to solve nasty problems when using high levels of abstraction. The second step is to consider other layers of abstraction.

clawson wrote:

As a consumer what would draw me to the "fancy" solution when the alternative is so easy anyway?

Basically, you're asking yourself what is the purpose of software engineering. Abstraction and composition is an essential pair to solve real problems, inside or outside to electronics or software. A vehicle is builded with this in mind, for example. It's not always feasible to do all the things from the scratch all the time. I believe that you already noticed how error-prone is to write an async program implementing state machines by hand with more than two or three devices being manipulated. It's all about what are your expectations to handle the problem: Do you I want to write the same thing that you already wrote many times in a way less error-prone, more expressive, more concise, more reusable to be more resilient if your requirements changes? Yes? High levels of abstraction is welcome. No? It's okay, the "traditional" way is the best in this case. There isn't anything like this is the best choice always and the other is the bad guy.

clawson wrote:

So how do you propose to break people out of that mindset?

I believe that I have already said what is the most interesting approach. I love the low level and I really don't believe in quality(and also in fun :D ) without it, but I don't like to repeat myself in code, I don't like to program something in the layer of application user that is fragile to changes(low level of maintainability) and so on to all those things that defines software engineering. When you rolls out a product (software and/or hardware) and you have to maintain it over years, as an engineer, we don't have the choice of ignore high level of abstractions.

 

github.com/ricardocosme

avrIO: Operation of I/O port pins and I/O registers.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Unless the framework includes most, if not all chips such as the ASF framework it would not serve any useful purpose, except maybe to one that limits there development to the targeted chips.

 

I prefer to go as close to the hardware as possible, cutting out any framework bugs and/or design restrictions.

Happy Trails,

Mike

JaxCoder.com

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Mike wrote:

Unless the framework includes most, if not all chips such as the ASF framework it would not serve any useful purpose, except maybe to one that limits there development to the targeted chips.

Frameworks are another beast. This is a single component that shouldn't compromise your other options, as you can see, the library is trying to provide some level of adaptability to allow you to mix it with your environment. The most important aspect to me related to this point is if the abstraction is powerful enough to accept other chips that are from the supported architecture, in this case: AVR8.

Mike wrote:

I prefer to go as close to the hardware as possible, cutting out any framework bugs and/or design restrictions.

I understand you. I don't like frameworks in general. I believe that to use one is a good ideia to have a good comprehension and control of what is being manipulated by the beast.

 

github.com/ricardocosme

avrIO: Operation of I/O port pins and I/O registers.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

rcosme wrote:

Basically, you're asking yourself what is the purpose of software engineering.

Nope I am really talking about your average Freaks contributor. They really do tend to pick one chip (or certainly "family") and then make everything fit. "I can happily take my mega88 UART code and rebuild it for a mega168 or a mega328 as that's the family I choose to use". You only need abstraction if you were try to be the same high level code on top of a mega168 and a mega4809. In that case I agree the UARTs are quite different and having an abstracted view is an advantage (its the same init(9600) / serial_sendchar('A') whatever the underlying hardware) but my point is that most reader here just don't hop about. What's more is that if the hop were to be from a mega168 to a STM Cotrtex M3 or something I think they'd be even less likely to expect to re-use the same code.

 

OTOH I suppose Arduino does prove that HALs work to a certain extent ;-)

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

clawson wrote:

Nope I am really talking about your average Freaks contributor. They really do tend to pick one chip (or certainly "family") and then make everything fit. "I can happily take my mega88 UART code and rebuild it for a mega168 or a mega328 as that's the family I choose to use".

I only have until now ATtiny85, ATtiny13A and ATmega328P. I need to start from something, don't you agree? Or, do you believe that I need a whole family or families to start an abstraction?

clawson wrote:

You only need abstraction if you were try to be the same high level code on top of a mega168 and a mega4809.

I completely disagree with this. Doesn't make any sense. Abstractions aren't only useful to navigate between N platforms.

clawson wrote:

(...) my point is that most reader here just don't hop about. What's more is that if the hop were to be from a mega168 to a STM Cotrtex M3 or something I think they'd be even less likely to expect to re-use the same code.

Maybe it's a good time to stop sharing my abstractions here.

clawson wrote:

OTOH I suppose Arduino does prove that HALs work to a certain extent ;-)

Yes, sure :)

 

github.com/ricardocosme

avrIO: Operation of I/O port pins and I/O registers.

Last Edited: Fri. Feb 5, 2021 - 06:46 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

rcosme wrote:

Maybe it's a good time to stop sharing my abstractions here.

 

Please, don't. It is not the first time I see posts on avrfreaks with very interesting C++ solutions (like this one) followed by this kind of "resistance". Ignore them.

I will steal for sure some parts of your work, hoping I can wrap my head around C++20, but I don't expect to be able to contribute anything meaningful sad.

I myself was working on something similar for the newer attiny804 in C++17 with gcc 9.3.0 and clang 9. The freestanding build of libstdc++ in your other post is also great news, I did not know that existed.

I'm following somewhat closely the book Real-Time C++ by Kormanyos, someone on discord #include <C++> recommended it, I think you can find some good ideas there.

I also found many articles and conferences by Dan Saks on embedded C++ about abstracting more complex peripherals.

Keep up the good work!

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I lost the notification about your reply, I'm only seeing it now.

fmiz wrote:

Please, don't. It is not the first time I see posts on avrfreaks with very interesting C++ solutions (like this one) followed by this kind of "resistance". Ignore them.

Hmmm, I see, I need to be aware of this.

fmiz wrote:

I will steal for sure some parts of your work, hoping I can wrap my head around C++20, but I don't expect to be able to contribute anything meaningful sad.

Feel free to use what you want as also to do a contribution if you like. Your ideas, suggestions or questions are welcomed.

fmiz wrote:

I myself was working on something similar for the newer attiny804 in C++17 with gcc 9.3.0 and clang 9. The freestanding build of libstdc++ in your other post is also great news, I did not know that existed.

It's good to know that the post about the freestanding implementation is useful to you.
clang 9? Interesting!
I would like to see your work if you're sharing it somewhere. BTW, a PR to avrIO supporting attiny804 would be beautiful :)

fmiz wrote:

I'm following somewhat closely the book Real-Time C++ by Kormanyos, someone on discord #include <C++> recommended it, I think you can find some good ideas there.

I'll take a look at it.

fmiz wrote:

I also found many articles and conferences by Dan Saks on embedded C++ about abstracting more complex peripherals.

Yes, Dan Saks has good presentations. I have watched him "live" at the Cppcon 2020.

fmiz wrote:

Keep up the good work!

Thanks for the incentive ;)

 

github.com/ricardocosme

avrIO: Operation of I/O port pins and I/O registers.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

FYI, I've updated the work to support operations on multiple I/O port pins at once:

https://www.avrfreaks.net/forum/...

github.com/ricardocosme

avrIO: Operation of I/O port pins and I/O registers.