Skip to content
Rob Giseburt edited this page Jul 19, 2014 · 80 revisions

To manipulate GPIO (General-Purpose Input/Output) pins, you use Motate::Pin<> objects.

Creating Motate::Pin (and subclass) objects

To use Motate::Pin objects include the MotatePins.h header, and declare file-global Motate::Pin objects. Then they will then be available for use in functions.

#include "MotatePins.h"

Motate::Pin<9> led1 {kOutput};
Motate::OutputPin<10> led2;
Motate::InputPin<11> button {kPullUp};

void loop() {
  if (button) {
    led1 = true;
    led2 = false;
  } else {
    led1 = false;
    led2 = true;
  }
}

Motate::Pin<pin_number> is a type in C++, like int and char. This means that, Motate::Pin<0> and Motate::Pin<1> are distinct types, and you can not cast from one Motate::Pin<pin_number> type to another.

Since Motate::Pin and subclases are all representing specific hardware objects, it is designed to be impossible to change them at run time. This also means it's impossible to create pin objects, pass pin objects as parameters in functions/methods, etc. See Defining and using Motate::Pin objects for more information.

#Motate::Pin<pin_number>

Motate::Pin<pin_number>  pin_name  {PinMode, PinOptions};
Motate::Pin<pin_number>  pin_name  {PinMode};  // PinOptions defaults to kNormal
Motate::Pin<pin_number>  pin_name; // Not recommended - doesn't fully configure the pin
  • pin_number is the name of a typedef, and must be a compile-time constant integer.

    • It is recommended to use the pin_number typedef in order to support platforms that have more than 250 pins in the future. Currenty, however, any value that the compiler can convert to const int8_t will work as a pin_number.
    • It is better to use a pin_number variable placed before the pin declaration or in a header file than to use a #define.
    // Both of the following will be compiled away:
    pin_number kRelayPinNumber = 13; // GOOD
    #define RELAY_PIN_NUMBER 13 // Not as good -- avoid this
    
    // Later, the pin_number can be used as follows:
    Motate::Pin<kRelayPinNumber>  relayPin  {kOutput};
    • As shown above, the naming convention is to use kCamelCaseNamePinNumber for the pin number, and camelCasePin (with the first letter lowercased) for the pin object itself.
  • PinMode and PinOptions are both optional -- but since this is C++ you have to have a PinMode value to provide a PinOptions value. (PinMode constant kUnchanged is useful for the case where you're rather not change the PinMode.)

  • If PinMode is either missing or kUnchanged then the pin registers will not be setup. They can, however, be setup at runtime with a call to pin_name.setMode(PinMode) with the same PinMode options.

  • Note: See the Defining and using Motate::Pin objects in projects section for imortant information about where to define Motate::Pin objects as well as how to compose Motate::Pin objects into other objects.

Pin configuration

void pin_object.setMode(PinMode)
  • Configures the pin with the given PinMode.

PinMode constants

  • The PinMode constants that can be used during pin initilization or the setMode(PinMode) method are as follows:
    • kUnchanged - Do not change the regiseters of the pin during initilization. Useful for creating an alias object of a pin that may have already been created elsewhere.
    • kOutput - The pin will be set up as an output. Any clocks or peripheral switches that are no longer needed when the pin goes from Input to Output will be disabled automatically.
    • kInput - The pin will be set up as an input. Any clocks or peripheral switches that need to be enabled for input will be enabled.

void pin_object.setOptions(PinOptions)
  • Configures the secondary options of the pin.

PinOptions constants

  • The PinOptions constants that can be used as bit masks during pin initilization or the setOptions(PinOptions) method are as follows:
    • kNormal - Do not change anything.
    • kTotem - An alias of kNormal.
    • kPullUp - Enable the pull-up resister of the pin (if available).
      • If the pin doesn't have a pull-up, then this function will do nothing.
      • If the pin is configured as an output, then this function will do nothing.

Setting pin outputs

void pin_object.operator=(const bool value)
void pin_object.write(const bool value)
  • Set the pin high if the provided value is true, and low if the value is false.

  • These two operations are equivalent:

      pin_object = true;
      pin_object.write(true);
  • Note: Using objects in an if statement when both operator=(bool) and operator bool() are defined can be dangerous, since you could accidentally set the value when you mean to compare against it. For this reason, operator=(bool) returns void, making it impossible to accidentally say if (pin=true) {...}.

void pin_object.set()
  • Set the pin to a high value. Equivaluent to pin_object.write(true).

void pin_object.clear()
  • Clears the pin to a low value. Equivaluent to pin_object.write(false).

void pin_object.toggle()
  • Inverts the value of the pin - high-to-low or low-to-high. Equivaluent to pin = !pin, except that toggle() is be optimized to take advantage of any available hardware pin-toggling facilities.

Getting pin values

operator bool()
uint32_t pin_object.get()
uint32_t pin_object.getInputValue()
uint32_t pin_object.getOutputValue()
  • The functions provide various ways to retrieve the pins value.
    • Some architectures make a destinction between input (read) pin value and output (set) pin value. These often depend on the Input or Output status of the pin.

    • operator bool is a syntactic shortcut for get() -- allowing the pin to be used directly in an if statement or other boolean context.

      • See the note on operator=(bool) about accidental assignment in if statements being prevented.
      if (button) {
        ...
      }
      // Same as:
      if (button == true) {
        ...
      }
      // Same as:
      if (button.get()) {
        ...
      }
    • get() will both attempt to determine the input/output status of the pin and return either getInputValue() or getOutputValue() appropriately.

      • On some processors, this may introduce a significant performance penalty for high-performance projects. For these cases, you should either define the pin as a Motate::OutputPin or Motate::InputPin or call getInputValue() or getOutputValue() directly when you can assume that the pin is an input or output.
      • Motate::OutputPin<N>, has get() changed to always be the same as getOutputValue().
      • Motate::InputPin<N>, has get() changed to always be the same as getInputValue().
    • getInputValue() will get the input value of the pin.

      • The value is only guaranteed to be valid for pins that are currently configured as inputs.
    • getOutputValue() will get the output value of the pin, of the value it was last set to.

      • The value is only guaranteed to be valid for pins that are currently configured to be outputs.

Getting pin information

bool pin_object.isNull()
  • You can construct a Motate::Pin object with with any valid int8_t value. If a pin specialization with that pin number hasn't been created for the current board and architecture, it will use the default specialization, which is the same as the NullPin, which has a pin_number of -1.

  • The default specialization, a.k.a. the NullPin, has the same interface as a normal pin, but with empty or minimal method definitions to satisfy the compiler. These are designed to completely go away in the optimizer.

  • If the pin is a valid pin (IOW, it has a valid specialization), isNull() will return false. Otherwise, isNull() will return true.

  • If you put a call to !pin_object.isNull() in an if statement, the value will be known at compile-time, and it will be the same as if (0) or if (1), which will either be deleted or the contents will be kept and the branch optimized away. For example:

    Motate::OutputPin<kPinThatDoesntExist> fakePin;
    Motate::OutputPin<kPinThatDoesExist> realPin;
    
    if (!fakePin.isNull()) {
      // code here will never make it to the final binary
    }
    if (!realPin.isNull()) {
      // code here will *always* make it to the final binary
      // but the "if" branch will be removed
    }

#Motate::InputPin<pin_number>

Motate::InputPin<pin_number>  pin_name;
Motate::InputPin<pin_number>  pin_name  {PinOptions};
  • Motate::InputPin objects are subclasses of Motate::Pin that has the PinMode set to kInput. There are a few notable changes:
    • inputpin_object.get() will specifically return the pin_object.getInputValue().
    • Functions that set the value have been effectively removed, including: pin_object.write(), pin_object.set(), pin_object.clear(), pin_object.toggle(), and pin_object.operator=(bool).
  • It is recommended to use a Motate::InputPin when you know that the pin will always be used as an input.

#Motate::OutputPin<pin_number>

Motate::OutputPin<pin_number>  pin_name;
Motate::OutputPin<pin_number>  pin_name  {PinOptions};
  • Motate::OutputPin objects are subclasses of Motate::Pin that has the PinMode set to kOutput. There are a few notable changes:
    • outputpin_object.get() will specifically return the pin_object.getOutputValue().
  • It is recommended to use a Motate::OutputPin when you know that the pin will always be used as an output.

#Motate::PWMPin<pin_number>

Motate::PWMPin<pin_number>  pin_name  {const uint32_t frequency};
  • Motate::PWMPin objects are subclasses of both Motate::Pin and Motate::Timer that provides a convenient interface to configure a PWM output on the pin.

    • Motate::PWMPin objects are always outputs.
    • frequency is the master frequency of the output in hz.

    void operator=(const float value)
    void pwmpin_object.write(const float value)
    • The object can be "set" as a floating-point value, from 0.0 to 1.0, as pwmpin_object = value, or pwmpin_object.write(value), to set the duty cycle.
    • The value will be scaled linearly from the maximum duty cycle (value=1.0) to off (value=0.0).

    uint16_t pwmpin_object.getTopValue()
    void pwmpin_object.writeRaw(const uint16_t duty)
    • If you wish to avoid floating point math in you project, you can obtain the maximum duty-cycle value with getTopValue(), and then set the duty cycle with writeRaw(const uint16_t duty) with an integer value between 0 for off and getTopValue() for 100% duty cycle.
    • The values might be uint16_t or uint32_t, depending on if the resolution of the timer for that pin on that architecture is higher than 16-bit.

    bool pwmpin_object.canPWM()
    • Not all pins are capable of PWM output. Since some architectures might have different pins that are capable of PWM than others, a project that runs on mutliple architectures may wish to adapt to those different capabilities. canPWM() will return a constant of true if the pin is capable of being a PWM, and false if it's not.
    • A project can be compile-time optimized with pwmpin_object.canPWM() in the same way it is with pin_object.isNull().

#Motate::IRQPin<pin_number>

Motate::IRQPin<pin_number>  pin_name;
Motate::IRQPin<pin_number>  pin_name  {PinOptions};
  • Motate::IRQPin objects are always inputs. They accept the normal compliment of PinOptions.

void pin_object.setInterrupts(const uint32_t interrupts)
  • Turn on or off the interrupts for this pin, if they are supported by the architecture and for this specific pin.

  • Use a bit-mask of the options available to set the interrupts value. For example:

    pin_object.setInterrupts(kPinInterruptOnChange|kPinInterruptPriorityHigh);

PinInterruptOptions constants

  • kPinInterruptsOff - Disable interrupts on this pin.

  • kPinInterruptOnChange - Enable interrupts on pin level changes.

  • kPinInterruptOnRisingEdge - Enable interrupts on low-to-high level.

  • kPinInterruptOnFallingEdge - Enable interrupts on high-to-low level.

  • kPinInterruptOnLowLevel - Enable interrupts on a low-level of the pin.

    • Warning: This interrupt will continue to fire as long as it is enabled and the pin has a low level, effectively preventing the processor from running other code.
  • kPinInterruptOnHighLevel - Enable interrupts on a high-level of the pin.

    • Warning: This interrupt will continue to fire as long as it is enabled and the pin has a high level, effectively preventing the processor from running other code.
  • kPinInterruptOnSoftwareTrigger - This is a special setting to enable the interrupt hardware (timers, etc), but not set a specific hardware interrupt trigger.

  • Interrupt priority level options:

    kPinInterruptPriorityHighest
    kPinInterruptPriorityHigh
    kPinInterruptPriorityMedium
    kPinInterruptPriorityLow
    kPinInterruptPriorityLowest
    • On architectures that support it, these set the level of priority of the interrupt. Higher priority interrupts will interrupt lower priority interrupts.
    • On architectures that don't support priority levels, these options have no effect.
    • On most architectures, this changes the level of interrupt priority for all interrupts that share that port.
    • On some architectures there are fewer levels, so kPinInterruptPriorityHighest and kPinInterruptPriorityHigh may indicate the same priority, for example.

MOTATE_PIN_INTERRUPT(pin_number) {
  // Insert your code here
}
  • When an interrupt for pin_number is handled, the user-defined function is executed.
  • Care should be taken that this does not interfere with other code that may be running at a lower priority level, such as normal code (which runs at a level below the "lowest"), since it will literally interrupt that code.
  • There is no return value. When this function ends (or returns) then the code that was interrupted will be handled.
  • For HighLevel or LowLevel interrupts, it is recommended that you call setInterrupts() to change or disable the interrupts, or this function will start again immediately after it exits as long as the pin is held at the high/low trigger level.

Setting up and handling pin interrupts

Some architectures have a limited number of pins that can generate pin-change interrupts. For this reason, there is a special subclass of Motate::Pin for those pins that can: Motate::IRQPin.

Defining and using Motate::Pin objects

Since Motate::Pin objects are static stateless objects that provide a convenient interface to hardware registers, it's problematic to define mutiple objects for the same pin.

For this reason, along with performance considerations, Motate::Pin objects are designed to be impossible to be fundamentally changed at run time -- you cannot make a Motate::Pin object represent a different physical pin, for example. This also means it's impossible to create pin objects, pass pin objects as parameters in functions/methods, loop over pin objects by pin number, etc.

Multi-file project usage tips

If the Motate::Pin object is to be directly used from within one file only, then you can define the object in that file (outside of any function definitions) and then you can use it in functions. You can then refer to those functions from other files as you would normally - by declaring the functions in a header and including that header in any .cpp file that uses those functions.

Motate::Pin<kRelayPinNumber>  relayPin  {kOutput};

void flipTheRelay() {
 relayPin.toggle();
}

It's not ideal, but if the Motate::Pin object is to be directly used from within multiple files, then you must define the object as extern (and without the initilizer) in a header file that is included in all the places that that the object will be used. Then, in one compiled file you place the definition without the extern.

// -- File "relay.h"
// In the header file
pin_number kRelayPinNumber = 13; // It's best to have the number be set in one place
extern Motate::Pin<kRelayPinNumber>  relayPin;
// ^^ extern is vital here
//      Do NOT include the {} intializer here ^^^

// -- File "relay.cpp"
// In exactly ONE .cpp file, we actually define and initialize it
// At the top of the file, with the includes, we pull in the header
#include "relay.h"

// Now we actually define it
// Note that "extern" is missing, and we have the {initializer}
Motate::Pin<kRelayPinNumber>  relayPin {kOutput};

// And we can, of course, use the relayPin in this file as well:
void initRelay() {
  relayPin.clear();
}


// -- File "controller.cpp"
// Now we can use the pin from any .cpp file that includes "relay.h"
#include "relay.h"

// later...
void turnTheRobot() {
  // ...
  relayPin.toggle();
  // ...
}

Using Motate::Pin objects as members of other objects

If one or more Motate::Pin objects are to be part of another object type, care must be taken to ensure that that containing object represents a distinct type. All objects of that type must be given the same care to not be multiply defined as Motate::Pin objects are.

There are three primary usage patterns: Class-as-namespace, Singleton, or Templated Class.

Note: In this case the term "Class" is used, but in the code examples we use structs. In C++, a struct is simply a class where the default access is public, where with a class the default is private. There's little value in using access restrictions in bare-metal projects.

  • Class-as-namespace

    • In the Class-as-namespace pattern, all of the members and methods of the class are static, and no actual objects of the class are ever instantiated.
    • In this pattern, the class is merely providing a "namespace" to collect the static methods and members into. At no point are objects of the class ever instatiated.
    • Warning:** If an all-static class uses other all-static classes or is nested in another class (all-static or not), then you may have issues with the optimizer incorrectly "garbage collecting" methods. Use this method sparingly.
      struct SharedButton {
        static Motate::Pin<kSharedRedButtonPinNumber> redButtonPin {kInput};
        static Motate::Pin<kSharedBlueButtonPinNumber> blueButtonPin {kInput};
        static bool areBothButtonsPushed() {
          return (redButtonPin && blueButtonPin);
        };
      };
      
      void main() {
        if (SharedButton::areBothButtonsPushed()) {
          //...
        }
      }
  • Singleton

    • In the Singleton pattern, you construct an object that you will only ever create one of.
      • Note to C++ purists: You don't have to make this type a singleton is the strict sense of making the constructor private and creating a static factory method. In fact, that will be rather difficult to do since Motate::Pin and many other Motate objects prevent copying. You could with heavy usage of constexpr and move semantics, but the value is dubious.
    • With this method, the singleton is the only object of that type, and contains a compile-time selected set of Motate::Pin objects.
      struct SharedButton_t {
        Motate::InputPin<kSharedRedButtonPinNumber> redButtonPin;
        Motate::Pin<kSharedBlueButtonPinNumber> blueButtonPin; // NO {initilizer list here}!
        
        // The initilizer has to placed in the constructor.
        // Note that we don't need an initilizer for InputPin redButtonPin.
        SharedButton_t() : blueButtonPin {kInput}
          {}; // This constructor has no body.
        // When the constructor has no body, like this one, we could make it constexpr.
        
        bool areBothButtonsPushed() {
          return (redButtonPin && blueButtonPin);
        };
      };
      
      SharedButton_t shared_button; // We create one and ONLY ONE of these.
      
      void main() {
        if (shared_button.areBothButtonsPushed()) {
          //...
        }
      }
  • Templated Class

    • With the Templated Class pattern, you use the pin_numbers as template parameters of the new type.
      • This works for templated types other than Motate::Pin as well. Just use the relevant template parameters of those types as parameters to the new type.
    • All of one templated class with the same parameters should be considered singletons. In the exmple below, stepper0 and stepper1 are both singletons that use different pins.
    • Care should be taken to not use the same pin in two or more different objects - the behavior could become unpredicatable.
      • If you wish to intentionally use the same pin in multiple singletons, you should define it outside the singleton class and simply utilize the pin as any other global. For complex cases, create a second templated type that is utilized as a group.
      • If you wish to intentially define the same pin in multiple singletons - don't.
    template<pin_number step_pin_number,
             pin_number dir_pin_number,
             pin_number enable_pin_number>
    struct stepper_t {
        Motate::Pin<step_pin_number> step_pin; // NO initilizers here!
        Motate::Pin<dir_pin_number> dir_pin;
        Motate::Pin<enable_pin_number> enable_pin;
    
        // We need to intialize them in the constructor of the object
        stepper_t() : step_pin {kOutput}, dir_pin {kOutput}, enable_pin {kOutput}
        {
            // intialization here
        }
    
        // No we can create methods that use those pins
        void enable(bool doEnable) {
            enable_pin.write(!doEnable);
        }
    
        // functions to step and set direction as well
    };
    
    // These three variables can be set in a separate header:
    pin_number kMotor0StepPinNumber = 0;
    pin_number kMotor0DirPinNumber = 1;
    pin_number kMotor0EnablePinNumber = 2;
    
    // Now we can create a stepper object and specify the pin numbers
    //  as part of the template parameters:
    stepper_t<kMotor0StepPinNumber,
              kMotor0DirPinNumber,
              kMotor0EnablePinNumber> stepper0;
    
    // Assuming kMotor1StepPinNumber, kMotor1DirPinNumber,
    //  and kMotor1EnablePinNumber are already setup somewhere else as well.
    stepper_t<kMotor1StepPinNumber,
              kMotor1DirPinNumber,
              kMotor1EnablePinNumber> stepper1;
    
    // In a function, we can now use them:
    stepper0.enable(true);
    stepper1.enable(false);