-
Notifications
You must be signed in to change notification settings - Fork 0
Defining Subsystems
A Subsystem is collection of functionally-related hardware geared towards a particular purpose. Examples of Subsystems are a drivetrain, a 4-bar lift, a claw or a rotating intake. Decomposing a single robot into discrete Subsystems is a key design decision. It some cases, the decomposition is obvious. In others, there are multiple acceptable answers and the decision is mostly a matter of taste or convenience.
In VexOS, Subsystems are implemented as singletons and are visible to the user program as variables of type Subsystem
. These variables are declared in Robot.h
using pre-processor macros, so they can used throughout the robot program.
For each Subsystem in your robot, entries must be made in Robot.h
that:
- Declare the existence of the Subsystem variable
- Declare prototypes for any public methods (functions) that operate on that Subsystem
- Declare any data types (such as enums) used by the methods of the Subsystem.
Example of a simple Drivetrain declaration in Robot.h
is given below:
DeclareSubsystem(Drive);
void Drive_setPower(Power left, Power right);
The DeclareSubsystem(Drive)
statement is the pre-processor macro, which creates a extern
variable of type struct Subsystem
with the name Drive
. Following the declaration is a single public method to set the power of the motors on each side of the drivetrain. The VexOS convention is to prefix the method with the name of the class/singleton being operated on, then a underscore, then the name of the method in lower CamelCase ("camel-back" notation, similar to Java method names). Note, unlike most VexOS system calls, a pointer to a Drive
instance is not needed as the first argument of the Drive_setPower(...)
method, since there is only one Drive instance (because it is a singleton), so the method will know what you are talking about.
A slightly more complex Subsystem declaration, one containing a custom enumeration type is given below. Enumerations are useful for eliminating "magic numbers" in code and making things more readable without heavy reliance on #define
macros.
DeclareSubsystem(Intake);
typedef enum {
IntakeDirection_Stop,
IntakeDirection_Suck,
IntakeDirection_Blow
} IntakeDirection;
IntakeDirection Intake_getDirection();
void Intake_setDirection(IntakeDirection dir);
Note that because enum value names are global, each choice is prefixed with the name of the enumeration type, in this case IntakeDirection
.
Once the Subsystem has been declared in Robot.h
, you must create the actual implementation. By convention, each Subsystem should be defined in its own file. If using easyC, the file should be titled sys__NAME_.c
, where NAME is the name of the Subsystem, in CamelCase. If not using easyC, then the suggested practice is to make a subsystems/
directory in your robot project and place NAME.c files (without a prefix) in that directory.
The example below, defining the Drive
class declared above demonstrates the key components of a Subsystem definition:
//
// sys_Drive.c
//
#include "Subsystem.h"
#include "Hardware.h"
#include "Robot.h"
/********************************************************************
* Class Definition *
********************************************************************/
DefineSubsystem(Drive);
static MotorGroup* leftDrive;
static MotorGroup* rightDrive;
static void constructor() {
leftDrive = MotorGroup_new("left drive");
MotorGroup_add(leftDrive, "left top", PWMPort_1, MotorType_393_HS, false);
MotorGroup_add(leftDrive, "left bottom", PWMPort_2, MotorType_393_HS, true);
rightDrive = MotorGroup_new("right drive");
MotorGroup_add(rightDrive, "right top", PWMPort_10, MotorType_393_HS, true);
MotorGroup_add(rightDrive, "right bottom", PWMPort_9, MotorType_393_HS, false);
}
static void initialize() {
setDefaultCommand(Command_new(&DriveWithJoystick));
}
/********************************************************************
* Public API *
********************************************************************/
void Drive_setPower(Power left, Power right) {
MotorGroup_setPower(leftDrive, left);
MotorGroup_setPower(rightDrive, right);
}
A Subsystem will always include Subsystem.h
, as this header contains all of the magic needed to create the Subsystem variable. Subsystem.h
should only be include in Subsystem definition C files and should not ne included in any other type of file. You will also include Hardware.h
since the main function of a Subsystem is to define hardware. Both of these headers are part of VexOS and should have been imported into your project previously, see Using VexOS in easyC. The final include is Robot.h
, which is your project-specific robot declaration header. The order of the includes is not important.
The next component is the DefineSubsystem(name)
pre-processor macro. This is what actually creates the variable of type Subsystem and links it up to everything else defined in the C file. The only argument to this macro is the name of the Subsystem. This must exactly match the name given to DeclareSubsystem(name)
in the header file.
Next are any private variables used by your Subsystem. These are generally pointers to hardware objects. This section is exactly the same as the private variables section described in Defining the Robot. The declarations are for pointers, since VexOS objects are never used directly.
While it is possible to declare public variables in Subsystems, you should never do so. Like in Java, it is considered bad form to create public fields. Instead, public methods should be declared and used to alter the fields (e.g. _getValue()
and _setValue(val)
).
The subsystem constructor is a function that is called when the Subsystem is being created (by VexOS, during "boot up" of the Cortex controller). The function is called only once, immediately after the Cortex powers up or is reset. The constructor function is declared static because it is never called directly by anything outside of the Robot.c file. In this case, it means the same thing as the static used on private variables.
The main purpose of the constructor is initialize the hardware variables described above and to initialize that hardware with any required configuration. In our example, a MotorGroup
is created for each side of the drivetrain and motors are added to it. This drivetrain has two motors ganged together per side, a typical configuration. For more details on using the VexOS hardware abstraction layer (HAL), please read Working with Hardware.
Subsystem constructors are called before the robot constructor. They are called in the same order as the Subsystems are listed in the DefineRobot(...systems...)
statement. For more details, see VexOS Execution Cycle.
The Subsystem initializer is called during the initialize phase of robot start-up, immediately before the robot initializer. The typical uses of the initializer are to set-up the default Command and to perform hardware initialization, such as starting PID loops.
Subsystems do not need to have a default command. It is a convenience that allows the other Commands to be simpler and guarantees a certain degree of safety, since the default command typically puts a Subsystem into a well-defined configuration, such as stopping all motors. Even if your Subsystem does not require a default command, the initialize()
method must still be defined. In that case, use an empty method:
static void initialize() { }
The last mandatory part of the Subsystem definition is that you must define all of the public methods that were declared in Robot.h
. Because they are publicly visible, these methods are declared without the static
qualifier. In this example, it is only the Drive_setPower(left, right)
method. Note that the name and parameter types must match the prototype in the header exactly, or a compiler error will be generated.
Public methods can be thought of as the "knobs and levers" of your Subsystem. The Subsytem provides the functionality and typically hides all hardware from the user (for example, you shouldn't need to know that the Drive subsystem has 4 motors from the outside... if you later change it to 6 motors, all other code should remain unchanged and still work). The public methods are the sole way that the external world (the Commands) interact with the Subsystem.
By default, the constructor is the only method in a Subsystem that is called automatically. The rest of the public methods are either called by Commands or are called manually in the initialize()
method of the Robot class. Should your Subsystem need to be updated periodically or once every scheduler loop, the easiest way to achieve that is with events. The VexOS Events page summarizes how to use them.
Sometimes you have common functionality that is required for multiple public methods, such as checking a switch. Rather repeating code in each public method, the best approach is to make additional private static
methods for use only within the subsystem. An example:
// private method //
static bool checkSwitch() {
return DigitalIn_get(mySwitch);
}
// public methods //
void Drive_setPower(Power left, Power right) {
if(!checkSwitch()) return;
...
}
void Drive_turn(int degrees) {
if(!checkSwitch()) return;
...
}
Here, the checkSwitch()
method is used internally, but because of the static
qualifier, is not visible outside of the Subsystem. Because the methods are private, they do not need to be prefixed with the name of the Subsystem.