Development / HOWTO / NGOS |
/ImplementController |
HOWTO Implement a NGOS flight controller
Contents
What's a NGOS flight controller
Introduction
A NGOS flight controller essentially represents the flight control algorithm, nothing more and nothing less. It takes the sensor input data and internal state data and produces 3 correction values for the 3 axes and a throttle value, which then get passed to the choosen HAL (Hardware Abstraction Layer) and then output to the actors.
NGOS makes it very simple for anyone to implement new NGOS controllers. A controller consists essentially of a single C function and an array of controller specific parameters. Furthermore in may implement it's own state storage structure, which it then can use to store flight controller state data between controller process cycles.
Output / Input / Process
The NGOS kernel essentially does something very simple: It gets triggered every millisecond by a timer. When triggered, it outputs the actor data calculated in the last cycle to the actors, then fetches the newest sensor data and calls the process function of a controller, to calculate the correction.
You may wonder why we don't do Input / Process / Output? Now, the answer is simple: When doing Output / Input / Process we assure that the Output happens exactly every millisecond. Then we immediately do Input, fetch a snapshot of the newest sensor data and pass them to the Process function of the chosen controller which then may use the rest of the millisecond to finish calculations.
The controller process function processes the controller state input structure (cs.in.*) and produces four outputs which get stored in the controller state output structure (cs.out.MN, cs.out.MR, cs.out.MY, cs.out.MT). It does this by using the flight controller code implemented in that function. The controller also has it's own state structure, where it's able to store internal state data needed for the calculation.
After the controller processed the new sensor input data, the controller state output structure (cs.out.*) gets passed to the HAL, which normally redistributes the generic correction data (cs.out.MN, cs.out.MR, cs.out.MY) to the I2C motor actor specific output variables (cs.out.M[0..15], depending on HAL's needed number of actors). A HAL also could use the 5 servo outputs on the NG hardware simultaneously to the I2C motor actors to implement a for example, a tricopter or to fly a helicopter.
After the above, the kernel controller task finishes and lets the user space code take controll and serve non time critical user space tasks (eg. the NGOS shells and their spawned commands).
Please note: NGOS has a third context, the IRQ context. The kernel process as well as the user space tasks will get interrupted by the IRQ context. NGOS runs it's "device drivers" in it's IRQ handlers and so assures that sensor data gets aquired continiously alongside the kernel and user space contexts.
Controller State
There are two globally defined structures called the controller state input structure (cs.in.*) and the controller state output state (cs.out.*). Check out src/fc/ctrl/ctrl.h for the two structures cs.in and cs.out. cs.in contains als sensor input data received, sometimes in multiple formats. cs.out contains all controller output variables.
A Controller takes the sensore values from cs.in.* and from it's internal controller state structure and calculates 3 correction factors for the three axes called Nick, Roll and Yaw (Nick stands for what most call "pitch") which then are placed cs.out.MN, cs.out.MR, cs.out.MY.
Besides the above two global structures a controller may implement it's own internal controller state structure which it then can use to store internal state data over multiple process cycles.
How do I implement a new NGOS Controller
Introduction
Implementing a controller is simple:
- Create buildsystem for the new controller:
- Create a folder src/fc/ctrl/ctrl-NAME
- Copy a Makefile.am from another controller into the new folder
- Change folder name and library name to NAME in the new Makefile.am
- Edit src/fc/ctrl/Makefile.am and add 'ctrl-NAME' to SUBDIRS
- Depending on your knowledge add 'if BUILD_CTRL_NAME' conditions and extend configure.ac with '--enable-ctrl-NAME'
- Create new controller:
- Create srcfc/ctrl/ctrl-NAME/ctrl-NAME.[ch]
- Fill them with:
- Your internal state structure for your controller
- A configuration parameter table with the parameter names, pointer to storage variables and validation functions
- A 'void ctrl_NAME_process(void)' function implementing your controller algorithm
- A 'void ctrl_NAME_state_reset(void)' function implementing zeroing of the internal state
- Add controller to NGOS
- Edit src/fc/ctrl.c
- Add #include lines for your controller's header file
- Extend the controller function table 'ctrl_func_list[]' with a row for your new controller
- Extend the controller parameter table '*ctrl_args[]' with the pointer to your controller's parameter table (Attention: Order is important and must be the same as in ctrl_func_list[]!)
- Compile your new NGOS firmware which includes now your new controller
Sample Implementation
You need some include files:
/* * needed includes for 'NAME' controller */ #include <string.h> #include <ngos/sysdefs.h> #include <ngos/conf.h> #include <ctrl/ctrl.h> #include <ctrl/ctrl-limit.h> #include <ctrl/ctrl-NAME/ctrl-NAME.h>
Controller internal state storage structure
This defines the most elementary internal state storage structure possible
/*
* controller internal state storage structure for 'NAME' controller
*/
typedef struct {
int spinup_cnt; // spinup counter
} ctrl_state_NAME_t;
ctrl_state_NAME_t xx;
Controller Parameter Table
This defines 4 controller dependant parameters. Each of them is defined by name, data type, desciption, pointer to parameter storage location in the global 'conf' structure (controllers may share parameters, allowing them to be switched mid-air, as they share the same scaled parameters) and a validation function.
/*
* configuraton parameter view for 'NAME' controller
*/
const ctrl_args_t ctrl_args_NAME[] =
{
{ "controller", "s20", "Closed-loop controller", &conf.controller, conf_validate_controller },
{ " ", "", "", 0, 0 },
{ "RC.fact.nick", "d", "RC stick factor nick", &conf.rc_fact_nick, conf_validate_integer },
{ "RC.fact.roll", "d", "RC stick factor roll", &conf.rc_fact_roll, conf_validate_integer },
{ "RC.fact.yaw", "d", "RC stick factor yaw", &conf.rc_fact_yaw, conf_validate_integer },
{ "", "", "", 0, 0 },
};
Controller Process Function
This defines an elementary controller process function. In this case it essentially does nothing most of the time and zeros the 4 controller output variables cs.out.MT', cs.out.MN', cs.out.MR', cs.out.MY'. So the copter does not turn the motors (cs.out.MT is zero) and it does not correct anything (the other three variables are zero too).
Should the flight state go to 'SPINUP', it will call a globally implemented default spinup function ctrl_process_spinup() in src/fc/ctrl/ctrl.c with a storage pointer for it's state from the internal state structure of the controller. This will let the motors spinup according to the global parameters set on the NGOS configuration. It will fill in the controller output variables, so we have to do nothing by ourselfes in our controller while it does it's magic.
When the flight state goes to 'FLYING' (and some similar states) it will output the throttle value received from the remote controller, limited and mapped to the right scale directly to the controller output structure's cs.out.MT which is the speed the motors will turn. It does does while still zeroing cs.out.MN, cs.out.MR, cs.out.MY, meaning that the copter will not try to correct any movements on these axes.
/*
* controller step
*/
void ctrl_NAME_process(void)
{
int mThrottle;
// limit throttle cs.in.rcT -> mThrottle
mThrottle = ctrl_limit_and_map_throttle(cs.in.rcT);
switch (ctrl_flightstate)
{
case CTRL_FS_LANDED:
case CTRL_FS_SPINDOWN:
// do nothing
cs.out.MT = cs.out.MN = cs.out.MR = cs.out.MY = 0;
return;
case CTRL_FS_SPINUP:
// handle spinup flight phase
ctrl_process_spinup(&xx.spinup_cnt);
return;
case CTRL_FS_FLYING:
case CTRL_FS_GPS_COMING_HOME:
case CTRL_FS_GPS_POSITION_HOLD:
default:
// handle flying flight phase
cs.out.MT = mThrottle;
cs.out.MN = cs.out.MR = cs.out.MY = 0;
return;
}
return;
}
Controller Header File
You also need a small header file for the controller:
#ifndef CTRL_NAME_H_ #define CTRL_NAME_H_ #include <ctrl/ctrl.h> extern const ctrl_args_t ctrl_args_NAME[]; void ctrl_NAME_process(void); void ctrl_NAME_state_reset(void); #endif /*CTRL_NAME_H_ */
In Reality
What you have seen above essentially represents the 'rc' controller which maps throttle to props while flying.
Tips and Tricks
Dig other controllers
Make sure to look at the other controllers. Especially the 'wolferl' controller (ctrl-wolferl) and the 'bearing hold' controller (ctrl-bh) are built very simple and easily to understand.
Make sure to look at the more complex controllers (ctrl-amir, ctrl-amir-ng) later, when you have understood the parameters in cs.in and cs.out and when you understand the flight states, parameter usage and reference and the PID controller. The more complex controllers consist of serveral files splitting the flight algorithm in modular parts. You will need a firm understanding of Kalman and DCM to understand some of those.
Make sure you handle all flight states
Make sure that you handle all flight states in your controller's process function. Always add a default statement to the flight state switch! Otherwise you must fear that your controller just switches off it's motors mid-air, when changing flight state accordingly!
