Thursday 13 February 2020

Custom Flight Controller Part 2.1: Software Overview and Architecture


In part 2 I am going to document how I implement various components of the flight controller software. The detailed code can be found in my Github.

At its bare minimum, a flight controller software should perform the following tasks

  • Read measurement data from sensor and user command from RC receiver
  • Perform sensor fusion to calculate quadcopter's attitude and other states
  • Execute control loops to command quadcopter to follow user command. 
Despite quadcopter's small size, it actually involves almost three out of four parts of a complete robot system, perception, sensor fusion and control. Of course planning could be added if you want the quadcopter to navigate autonomously. But this is not part of the goal of this project.

To implement the software in a structured way, I make use of a layered software architecture. With this architecture, I am able to define all components required and implement them in modular and object-oriented way.

On the top-most level, the software is divided into five layers. Ranking from top to bottom, they are Application layer -> Service Layer -> HAL Layer -> Driver Layer -> Library Layer. They also define the folder structure of my code. 

Library layer lies at the bottom-most level because a library can be used by any other component. In my case, my library layer contains the following libraries:

Driver layer contains modules that handle interactions to various hardware. For example, PWM driver will expose a function called PWM_SetDutyCycle(). As a result, an user could pass in clearly-defined arguments instead of having to set register values. My driver cotains the following modules:

HAL layer tries to provide a further level of abstraction by hiding away hardware specific names like I2C and SBUS. This makes sense because if someone who knows nothing about embedded system tries to write flight controller code, he would expect to communicate with modules named "IMU" and "Receiver" instead of "I2C" and "SBUS". HAL layer provides exactly that. For example, the receiver module would expose function called ReceiverStatus GetCmd(FCCmdType& cmd) which can be easily understood by top-level application.

Service layer defines various services the main application can use. For flight controller software, the basic components include cmd_listener which listens to commands from receiver, controller which defines the whole control system, sensor_reader which read data from IMU and other sensors and lastly state_estimator which estimates states by performing sensor fusion. 

Lastly, app layer includes top-most applications which are also the entry points of the whole program. Other than the main flight controller app, I also wrote several test applications that instead of flying the quadcopter, perform various tests for debugging purpose. 

Note that STM32CubeMX will automatically generate initialization code for the project. The auto-generated code has its own structure which does not always conform to my intended design principle. For example, the auto-generated code puts all initialization in main.c while if I were to write the whole code from scratch, I would put initialization code for PWM into drivers/pwm module and I2C into drivers/i2c module and so on. Despite the difference, I chose not to change auto-generated code as otherwise, it would be very difficult to change settings in STM32CubeMX and generate code again. 

As a result, codes which initializes peripherals remain in main.c auto-generated by STM32CubeMX. To run my flight controller main application, I merely calls MainApp() from main() as shown below.
int main(void)
{
    /* USER CODE BEGIN 1 */

    /* USER CODE END 1 */

    /* MCU Configuration--------------------------------------------------------*/

    /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
    HAL_Init();

    /* USER CODE BEGIN Init */

    /* USER CODE END Init */

    /* Configure the system clock */
    SystemClock_Config();

    /* USER CODE BEGIN SysInit */

    /* USER CODE END SysInit */

    /* Initialize all configured peripherals */
    MX_GPIO_Init();
    MX_DMA_Init();
    MX_I2C1_Init();
    MX_TIM1_Init();
    MX_USART3_UART_Init();
    MX_USART2_UART_Init();
    /* USER CODE BEGIN 2 */
    HAL_Delay(1000);
    MainApp(); //runs my flight controller app
    // TestMadgwickNoMag_Main();
    /* USER CODE END 2 */

    /* Infinite loop */
    /* USER CODE BEGIN WHILE */
    while (1)
    {
        /* USER CODE END WHILE */

        /* USER CODE BEGIN 3 */
    }
    /* USER CODE END 3 */
}
Furthermore, in multiple software modules, I had to declare external variables to use peripheral initialized. For example, in PWM driver module, TIM_HandleTypeDef htim1 is declared to be extern because it is defined in main.c by code auto-generated from STM32CubeMX.

For most of the software modules, implementation is quite straight-forward and therefore they will not be covered in this blog series. Instead, the next posts will focus on those software modules that are tricky to implement and requires particular attentions.  

No comments:

Post a Comment