Genericizing Arduino Libraries

When writing a library, pass in I2C ports to make the library more useful on different platforms.

Favorited Favorite 4

My AVC 2016 vehicle used a handful of our SAMD21 Minis. It’s an amazing board because it’s got multiple serial ports and multiple I2C ports. But as I attached things like the HMC5883L to the SAMD21, I quickly realized that most libraries assume you will be using Wire.write(). What if I want to use a library, but with a different I2C port? You have to go into the library and modify quite a bit to get it to work. I want future library writers to avoid this so I decided to write a really simple example library to show readers how write libs that can accept different hardware ports.

I’m a hack when it comes to hardcore C so I learn by example. Bill Porter’s Easy Transfer Library from 2011 (!) saved the day and was the key to cracking this problem.

I wanted to create a simple-as-possible library example so if you’re creating your own library, consider this template as a starting point:

And here's what a bit of the library looks like. Because we've passed a reference (&wirePort) as an argument to the library we use arrows instead of dots to access the member functions.

Using an underscore in front of a variable name doesn't do anything magical it just makes it easier to understand which variables are private to the library. From David's Writing a Library for Arduino: "Adding an underscore to the start of the name is a common convention to make it clear which variables are private..." The _i2cPort variable can be accessed by various functions within Memory.cpp but not by the user's main sketch.

In the same vein, libraries that use Serial.print() won’t play nice with a board like the SAMD21 that can have three serial ports. So here’s a really simple example showing a library that allows a serial stream to be passed into it.

And the library:

Checkout the full files to see how to set up your header file and cpp.

Why should you care?

Imagine the day when we can use any board with any library and it just works. It’s coming fast; we just need to write the libraries as extensible as possible!

If you decide to write your own library, David wrote a great tutorial covering the basics so start there.

There are probably ways to make these templates even better. Please let me know if you know of any additional tricks.


Comments 17 comments

  • denbo68 / about 8 years ago / 2

    Hi,

    You wrote, "Because we’ve passed a pointer (&wirePort) to the library we use arrows instead of dots to access the member functions."

    That isn't correct.

    The definition of your begin method is bool Memory::begin(TwoWire &wirePort), which is taking a reference to the Wire object as an argument, not a pointer.

    To pass a pointer your begin method would have looked like this: bool Memory::begin(TwoWire *wirePort). You would have used * instead of an ampersand (&).

    The reason you need -> for accessing methods with _i2cPort is because you assign the address (pointer) to the Wire object in this line: _i2cPort = &wirePort;

    "&wirePort" is the address (pointer) to the Wire object.

    A different way to achieve this and not use the -> is to treat everything as a reference. Declare _i2cPort in Memory.h as TwoWire& _i2cPort

    Then you can assign _i2cPort to the Wire object simply assigning it without the & (_i2cPort = wirePort;). All Wire methods after that would be with the normal dot (.) notation: _i2cPort.write();

    • Got it. Does this read better "Because we’ve passed a reference (&wirePort) as an argument.."?

      Then you can assign i2cPort to the Wire object simply assigning it without the & (i2cPort = wirePort;). All Wire methods after that would be with the normal dot (.) notation: _i2cPort.write();

      Hmm. I like that. It would read better and feel a bit more normal to first-time library writers. Is there any downside to treating everything as a reference?

      • denbo68 / about 8 years ago / 1

        Yes there are some downsides where you cannot use references. References can't be set to NULL nor is there such a thing as a "void" reference as there is a void pointer. Passing an array is basically passing a pointer to the first element of the array so that makes no sense to make as a reference.

        Also the reference must be set during its instantiation and cannot be changed afterwards unlike a pointer.

        Many C++ books have different philosophies on when to use references. Some say use them for passing objects only; others say use them until you can't.

        I am a bit 'old school' and tend to still use pointers most of the time.

  • ShapeShifter / about 8 years ago / 2

    Nice post! It's a little more work to create a library that's universally useful, but well worth it in the long run. I wish more library writers would take this idea to heart.

    I particularly like how you used the base Stream class in your "Laser" example, as there are many other communications channels besides HardwareSerial and SoftwareSerial ports. I do a fair amount of work on the Arduino Yun, and both the network connections and the Process class that lets you run and communicate with Linux processes are derived from Stream. So your Laser library could just as easily communicate over a network socket or with a Linux process, as easily as it could talk to a serial port. That's the beauty of making it generic, it allows the library to be used in situations that the author may have never considered.

    However, I do have to take exception to the use of a leading underscore for local private names. In C and C++ (the Arduino language is really C++) all names that start with an underscore are reserved for use by the compiler. You may get away with doing that most of the time, but eventually it could cause you problems.

    • denbo68 / about 8 years ago / 1

      You wrote "In C and C++ (the Arduino language is really C++) all names that start with an underscore are reserved for use by the compiler"

      What conflict? Given that local private variables are local to the class in scope there shouldn't be any conflict.

      Also realize that most registers on Arduoino have no _ in front of them. Imagine if he had declared a variable called TWAR.

    • Hmm. Good to know. Is there a different or better "standard" that libraries use to differentiate private variables? I'll bring it up with David as well.

      • While as mentioned the fact that it's defined local to the class it won't have a conflict, but it might be a little confusing. One standard notation for a class member variable is to use m_ prefix. For example m_i2cPort.

  • dksmall / about 8 years ago / 1

    Maybe I missed something not so obvious, but now that I had a chance to go over the library example, it appears this just touches on some of the additions you would need to make, in this case the pin assignments. For complete support of the other serial ports you would need interrupt handlers and ring buffers. Is this all handled by the library as shown? I didn't see anything that cover this part of the code, but I'm still learning C++, so I may have missed it.

    • If you pass in a stream such as Serial or SerialUSB all the ints and ring buffers are handled by their libs. Your library doesn't need to re-define them.

  • Constants like EEPROM_ADDRESS are also good candidates to become input parameters. In this case, making the address an initializer/constructor parameter would allow you to support multiple memory chips on the same IIC bus.

    • Really good point. I often deal with sensors that have a s set I2C address but if it's configurable (and more and more devices do) the I2C address should be passed in rather than hard-coded.

  • dksmall / about 8 years ago / 1

    This is good timing. After my poor GPS based runs at AVC, I've been looking into a dead reckoning approach using something like the SAMD21, the MPC-2950, and some sonar. Nate, we spoke in the AVC pit area, I'm Kelly from Phoenix. I didn't know you had an AVC entry, would have liked to have seen what you brought.

    • Member #2443 / about 8 years ago / 1

      I would combine GPS and dead reckoning if you can. The GPS will give you the general direction and speed but it is very rough in terms of absolute position (3-4m, 9-12' radius in good conditions). Wheel encoders are good but can't handle slipage very well. An accelerometer and compass information will give you a pretty good heading for dead reckoning and using the GPS data to give you a direction to objective. Fine tuning with a camera or other sensors to avoid objects and find the actual object once you are in the vicinity if that is the goal.

      I suspect that any of the ardupilot systems do this in their software so you could take a look at their libraries for examples or maybe use one of them as the basis for your control system.

      • dksmall / about 8 years ago / 1

        That pretty much outlines my intentions, but rather then use a GPS, I would start with encoders and fine tune with obstacle avoidance. If the AVC course remains similar to what it was, you have more of a maze running algorithm, rather then the need to go from one exact spot to another. Some like "drive at this heading until you see an opening on the right", then turn to this new heading. And while on that heading, use side sensors to stay centered between the bales.

  • FMz / about 8 years ago / 1

    This is a great post. I came across couple of Adafruit's Arduino Library where they did it. It can be overwhelming at first but it is worth the hassle!

  • This was a really good post. It shows the value of passing by reference and how it makes the libraries more useful and flexible.

Related Posts

Recent Posts

Why L-Band?

Tags


All Tags