-
Notifications
You must be signed in to change notification settings - Fork 38
Motate::Pin
To manipulate GPIO (General-Purpose Input/Output) pins, you use Motate::Pin<>
objects.
- Creating Motate::Pin (and subclass) objects
-
Motate::Pin<pin_number>
Motate::InputPin<pin_number>
Motate::OutputPin<pin_number>
-
Motate::PWMPin<pin_number>
-
Motate::IRQPin<pin_number>
- Setting up and handling pin interrupts
- Defining and using
Motate::Pin
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> 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 toconst int8_t
will work as apin_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
k
CamelCaseName
PinNumber
for the pin number, andcamelCase
Pin
(with the first letter lowercased) for the pin object itself.
- It is recommended to use the
-
PinMode
andPinOptions
are both optional -- but since this is C++ you have to have aPinMode
value to provide aPinOptions
value. (PinMode
constantkUnchanged
is useful for the case where you're rather not change thePinMode
.) -
If
PinMode
is either missing orkUnchanged
then the pin registers will not be setup. They can, however, be setup at runtime with a call topin_name.setMode(PinMode)
with the samePinMode
options. -
Note: See the Defining and using
Motate::Pin
objects in projects section for imortant information about where to defineMotate::Pin
objects as well as how to composeMotate::Pin
objects into other objects.
void pin_object.setMode(PinMode)
- Configures the pin with the given PinMode.
- The
PinMode
constants that can be used during pin initilization or thesetMode(
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.
- The
PinOptions
constants that can be used as bit masks during pin initilization or thesetOptions(
PinOptions
)
method are as follows:-
kNormal
- Do not change anything. -
kTotem
- An alias ofkNormal
. -
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.
-
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 isfalse
. -
These two operations are equivalent:
pin_object = true; pin_object.write(true);
-
Note: Using objects in an if statement when both
operator=(bool)
andoperator 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)
returnsvoid
, making it impossible to accidentally sayif (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 thattoggle()
is be optimized to take advantage of any available hardware pin-toggling facilities.
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 forget()
-- 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()) { ... }
- See the note on
-
get()
will both attempt to determine the input/output status of the pin and return eithergetInputValue()
orgetOutputValue()
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
orMotate::InputPin
or callgetInputValue()
orgetOutputValue()
directly when you can assume that the pin is an input or output. -
Motate::OutputPin<
N
>
, hasget()
changed to always be the same asgetOutputValue()
. -
Motate::InputPin<
N
>
, hasget()
changed to always be the same asgetInputValue()
.
- 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
-
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.
-
bool pin_object.isNull()
-
You can construct a
Motate::Pin
object with with any validint8_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 theNullPin
, which has apin_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 returnfalse
. Otherwise,isNull()
will returntrue
. -
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 asif (0)
orif (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> pin_name;
Motate::InputPin<pin_number> pin_name {PinOptions};
-
Motate::InputPin
objects are subclasses ofMotate::Pin
that has the PinMode set tokInput
. There are a few notable changes:-
inputpin_object.get()
will specifically return thepin_object.getInputValue()
. - Functions that set the value have been effectively removed, including:
pin_object.write()
,pin_object.set()
,pin_object.clear()
,pin_object.toggle()
, andpin_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 ofMotate::Pin
that has the PinMode set tokOutput
. There are a few notable changes:-
outputpin_object.get()
will specifically return thepin_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> pin_name {const uint32_t frequency};
-
Motate::PWMPin
objects are subclasses of bothMotate::Pin
andMotate::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
to1.0
, aspwmpin_object = value
, orpwmpin_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 withwriteRaw(const uint16_t duty)
with an integer value between0
for off andgetTopValue()
for 100% duty cycle. - The values might be
uint16_t
oruint32_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 oftrue
if the pin is capable of being a PWM, andfalse
if it's not. - A project can be compile-time optimized with
pwmpin_object.canPWM()
in the same way it is withpin_object.isNull()
.
-
Motate::IRQPin<pin_number> pin_name;
Motate::IRQPin<pin_number> pin_name {PinOptions};
-
Motate::IRQPin
objects are always inputs. They accept the normal compliment ofPinOptions
.
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);
-
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
andkPinInterruptPriorityHigh
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
orLowLevel
interrupts, it is recommended that you callsetInterrupts()
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.
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
.
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.
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();
// ...
}
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 struct
s. 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()) { //... } }
- In the Class-as-namespace pattern, all of the members and methods of the class are
-
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 ofconstexpr
and move semantics, but the value is dubious.
-
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
- 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()) { //... } }
- In the Singleton pattern, you construct an object that you will only ever create one of.
-
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.
- This works for templated types other than
- All of one templated class with the same parameters should be considered singletons. In the exmple below,
stepper0
andstepper1
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);
- With the Templated Class pattern, you use the pin_numbers as template parameters of the new type.