Embedded Programming Model | MVC Model

Hello everyone, I am the Mixed Content Master.

In embedded / microcontroller project development, we often choose a major software framework based on actual conditions: bare-metal systems, front-end and back-end systems, RTOS, Linux, etc. In actual development, choosing the type of software architecture is just the first step.

How do the various modules in the system work together, and how is the business logic designed? This must be carefully considered in the early stages of the project; if you just write as you think, you may face many difficulties later.

It is necessary for us to learn various programming models (design patterns), such as event-driven programming models, message-driven programming models, state machine models, etc.

Programming models do not conflict with the major software frameworks; for example, the commonly used state machine model can run on bare-metal systems, RTOS, and Linux.

In this article, we will learn about the MVC programming model together.

1. Introduction to MVC Programming Model

MVC (Model-View-Controller) is a software design pattern that divides an application into three main parts: Model, View, and Controller. This separation helps improve the maintainability, scalability, and testability of the code.

  • Model: Focuses on data management and business logic.
  • View: Responsible for presenting data to the user; it is the part of the user interface.
  • Controller: Acts as a bridge between the Model and View, receiving user input requests, calling the appropriate Model methods for data processing based on the requests, and then selecting the appropriate View to display the processing results to the user.

MVC was originally designed for web applications, but its concepts are equally applicable to embedded system development.

In embedded scenarios:

  • Model: Handles sensor data collection and processing.
  • View: Handles data display (e.g., LCD rendering, LED status indication).
  • Controller: Coordinates input (e.g., GUI input, interrupt handling, user key responses).

The core advantages of MVC:

Embedded Programming Model | MVC Model

1. Structure Diagram

Embedded Programming Model | MVC Model
  • Unidirectional Dependency: Controller operates on Model, View listens to Model.
  • Decoupled Design: View and Controller do not interact directly, Model is independent of interface logic.

2. Sequence Diagram

Embedded Programming Model | MVC Model
  • User input (e.g., button press) is passed to the Controller.
  • Controller modifies Model data (e.g., updates sensor status).
  • Model triggers View update via<span>notifyObservers()</span> callback (not directly calling View from Controller).
  • View pulls the latest data from Model and renders it (e.g., refreshes LCD display).

Examples of MVC Concept Application

Below are four examples: one is a minimal MVC demo we wrote ourselves, and the other three are projects on GitHub that use the MVC concept in embedded contexts.

1. MVC Demo

In embedded systems, products with GUI interfaces may have scenarios like this: switching GUI interfaces, where the business logic reads local configuration data and refreshes it to the corresponding GUI interface.

Below, we will write a simple example based on a Linux multithreaded application using the pthread library for this simple scenario:

  • Controller Module: Receives the user input page index, retrieves the corresponding configuration index based on the mapping relationship, and sends a request to the Model module to read the configuration.
  • Model Module: Retrieves the corresponding configuration information from the configuration array based on the received configuration index and sends it to the View module.
  • View Module: Displays the configuration information on the corresponding page after receiving it.

(1) Class Diagram

Embedded Programming Model | MVC Model
Class Definitions
  • Model class is responsible for reading and storing configuration data, containing members such as configuration array, thread, and message queue.
  • View class is responsible for displaying pages and configuration information, containing members such as current page index, thread, and message queue.
  • Controller class acts as a coordinator, holding pointers to Model and View, responsible for receiving user input and controlling their interaction.
  • Config class represents configuration information, containing configuration name and value.
Association Relationships
  • Controller has an aggregation relationship (has) with Model and View, meaning Controller holds instances of Model and View.
  • Model has a composition relationship (contains) with Config, meaning Model contains multiple Config instances.

(2) Sequence Diagram

Embedded Programming Model | MVC Model

(3) Execution

Embedded Programming Model | MVC Model
  1. The terminal inputs the page index.
  2. After receiving the input, the Controller retrieves the corresponding configuration index based on the page – configuration mapping relationship.
  3. Controller sends a READ_CONFIG message to the Model, requesting to read the specified configuration.
  4. Model retrieves the corresponding configuration information from Config.
  5. Model sends a CONFIG message to View, passing the configuration information.
  6. View displays the current page and configuration information after receiving the message.

(4) Code

Embedded Programming Model | MVC Model
#include<stdio.h>
#include"model.h"
#include"view.h"
#include"controller.h"

int main(void)
{
    Model *model = create_model();
    View *view = create_view();
    Controller *controller = create_controller(model, view);

    start_model(model);
    start_view(view);
    start_controller(controller);

    pthread_exit(NULL);

    return 0;
}
  • model.c:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include"model.h"

static void update_config(Model *model, int config_index)
{
    const Config *config = get_config(model->configs, config_index, model->num_configs);

    if (config) 
    {
        char msg[MAX_MSG_SIZE] = {0};

        snprintf(msg, sizeof(msg), "CONFIG:%s:%d", config->name, config->value);
        send_message(model->mq, msg);
    }
}

void* model_thread_func(void *arg)
{
    Model *model = (Model *)arg;
    char buf[MAX_MSG_SIZE] = {0};

    while (1) 
    {
        ssize_t bytes_received = receive_message(model->mq, buf);
        buf[bytes_received] = '\0';
        
        if (strncmp(buf, "READ_CONFIG:", 12) == 0) 
        {
            int config_index = atoi(buf + 12);
            update_config(model, config_index);
        }
    }

    return NULL;
}

Model* create_model(void)
{
    Model *model = (Model *)malloc(sizeof(Model));

    if (model == NULL) 
    {
        perror("malloc");
        exit(EXIT_FAILURE);
    }

    model->num_configs = MAX_CONFIGS;
    init_configs(model->configs, model->num_configs);
    model->mq = init_message_queue();

    return model;
}

void start_model(Model *model)
{
    if (pthread_create(&model->thread, NULL, model_thread_func, model) != 0) 
    {
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }
}

void stop_model(Model *model)
{
    pthread_join(model->thread, NULL);
    close_message_queue(model->mq);
    free(model);
}
  • view.c:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include"view.h"

static void display_page(View *view, const char *config_name, int config_value)
{
    printf("Current page: %d\n", view->current_page);
    printf("Business configuration: %s, Value: %d\n", config_name, config_value);
}

void* view_thread_func(void *arg)
{
    View *view = (View *)arg;
    char buf[MAX_MSG_SIZE] = {0};

    while (1) 
    {
        ssize_t bytes_received = receive_message(view->mq, buf);
        buf[bytes_received] = '\0';

        if (strncmp(buf, "CONFIG:", 7) == 0) 
        {
            char config_name[32] = {0};
            int config_value = 0;
            sscanf(buf + 7, "%[^:]:%d", config_name, &config_value);
            display_page(view, config_name, config_value);
        }
    }

    return NULL;
}

View* create_view(void)
{
    View *view = (View *)malloc(sizeof(View));

    if (view == NULL) 
    {
        perror("malloc");
        exit(EXIT_FAILURE);
    }

    view->current_page = 1;
    view->mq = init_message_queue();

    return view;
}

void start_view(View *view)
{
    if (pthread_create(&view->thread, NULL, view_thread_func, view) != 0) 
    {
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }
}

void stop_view(View *view)
{
    pthread_join(view->thread, NULL);
    close_message_queue(view->mq);

    free(view);
}
  • controller.c:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<pthread.h>
#include"controller.h"
#include"message_queue.h"

#define PAGE_CONFIG_MAP_SIZE 10
static int page_config_map[PAGE_CONFIG_MAP_SIZE] = {0};

static void init_page_config_map(void)
{
    for (int i = 0; i < PAGE_CONFIG_MAP_SIZE; i++) 
    {
        page_config_map[i] = i;
    }
}

void* controller_thread_func(void *arg)
{
    Controller *controller = (Controller *)arg;
    char input[10] = {0};

    init_page_config_map();

    while (1) 
    {
        printf("Input page index (0 - %d) to display corresponding page configuration, input 'q' to exit the program: ", PAGE_CONFIG_MAP_SIZE - 1);
        fgets(input, sizeof(input), stdin);
        if (input[0] >= '0' && input[0] <= '9') 
        {
            int page_index = atoi(input);

            if (page_index >= 0 && page_index < PAGE_CONFIG_MAP_SIZE) 
            {
                controller->view->current_page = page_index;
                int config_index = page_config_map[page_index];

                char msg[MAX_MSG_SIZE] = {0};

                snprintf(msg, sizeof(msg), "READ_CONFIG:%d", config_index);
                send_message(controller->model->mq, msg);
            } 
            else
            {
                printf("Invalid page index, please re-enter.\n");
            }
        } 
        else if (input[0] == 'q') 
        {
            stop_model(controller->model);
            stop_view(controller->view);
            close_message_queue(controller->mq);
            exit(0);
        }
    }

    return NULL;
}

Controller* create_controller(Model *model, View *view)
{
    Controller *controller = (Controller *)malloc(sizeof(Controller));

    if (controller == NULL) 
    {
        perror("malloc");
        exit(EXIT_FAILURE);
    }

    controller->model = model;
    controller->view = view;
    controller->mq = init_message_queue();

    return controller;
}

void start_controller(Controller *controller)
{
    if (pthread_create(&controller->thread, NULL, controller_thread_func, controller) != 0) 
    {
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }
}

void stop_controller(Controller *controller)
{
    pthread_join(controller->thread, NULL);
    close_message_queue(controller->mq);
    free(controller);
}

Due to space limitations, other code will not be posted to avoid affecting the reading experience. Interested friends can enter the keyword: MVC Example in the chat interface of this public account to get the download link.

2. Q-Controllers

Q-Controllers is an event-driven application code framework suitable for low-end microcontrollers that cannot run an operating system but need to handle increasingly complex code structures.

https://github.com/q-iot/q-controllers

It draws on the MVC pattern and proposes a D-IO-C layered architecture (Data-IO-Controller), dividing the code into:

  • Data: Data storage and management (similar to the Model layer)
  • IO: Input and output hardware operations (e.g., sensor, display driver)
  • Controller: Event-driven business logic processing

3. IotSensorDetect

An open-source IoT gas detection project based on the MVC pattern + state design pattern.

https://github.com/Yangyuanxin/IotSensorDetect

MVC pattern:

  • Model: Sensor data collection and state management (e.g., ModelSensorHandlerTask)
  • View: Data display (via cloud platform or LCD)
  • Controller: Command processing and state coordination (e.g., ControllerTask)

4. A Calculator Example of AWTK

A calculator developed based on MVC:

https://github.com/zlgopen/awtk-patterns/tree/master/src/calculator-mvc

Q & A

Q1: Should business logic be written in the Controller or Model?

A: It should be written in the Model.

Embedded Programming Model | MVC Model
Embedded Programming Model | MVC Model

This concludes our sharing on the MVC model. If you find the article helpful, please help share it, thank you!

You might also like:

Step-by-step guide to setting up an embedded containerized development environment!

An elegant multifunctional embedded debugger!

A very lightweight embedded logging library!

A very lightweight embedded thread pool library!

Popular C language projects on GitHub!

Practical | Teach you to turn on the light via web in 10 minutes

Leave a Comment