100ask seven day Internet of things training camp learning notes - bare metal program framework design

1. Preface

Signed up for the seven day Internet of things smart home training camp organized by 100ask. I talked about the basics 2 hours in the morning and advanced 2 hours in the afternoon. After two days of study, I really feel more enriched. In the next few days, I will successively record the key points, difficulties and my understanding of each course.

2. Theory

2.1 throwing problems

In this class, Mr. Wei started the course with the program framework design as the starting point. Taking the embedded bare metal development as an example, many beginners usually directly call the interface for description or hardware (such as HAL Library) in the business layer code or even the main function:

void main(void)
{
    GPIO_PinState key;
    while (1)
    {
        key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
        if (key == GPIO_PIN_RESET)
            HAL_GPIO_WritePin(GPIOF, GPIO_PIN_5, GPIO_PIN_RESET);
        else
            HAL_GPIO_WritePin(GPIOF, GPIO_PIN_5, GPIO_PIN_SET);
    }
}

This will cause serious coupling between business layer code and board level code, inconvenience to subsequent software function expansion, hardware upgrade and code reuse, and also cause obstacles to business layer developers who do not understand hardware.

In order to solve this problem, we layered the program structure and separated the business logic from the hardware driver code:

// main.c
void main(void)
{
    int key;
    while (1)
    {
        key = read_key();
        if (key == UP)
            led_on();
        else
            led_off();
    }
}

// key.c
int read_key(void)
{
    GPIO_PinState key;
    key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
    if (key == GPIO_PIN_RESET)
        return 0;
    else
        return 1;
}

// led.c
void led_on(void)
{
    HAL_GPIO_WritePin(GPIOF, GPIO_PIN_5, GPIO_PIN_RESET);
}

void led_off(void)
{
    HAL_GPIO_WritePin(GPIOF, GPIO_PIN_5, GPIO_PIN_SET);
}

This solves the coupling problem between business logic code and hardware driver code, but there are still two problems to be solved:

1, Software compatibility problems caused by hardware version iteration

2, Functional scalability issues

2.2 introduction of function pointer

Here we want to solve the first problem. There are usually three methods:

  1. Macro switch

    #define HARDWARE_VER  1
    
    // key.c
    // Return value: 0 means pressed and 1 means released
    int read_key(void)
    {
        GPIO_PinState key;
    #if (HARDWARE_VER == 1)
        key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
    #else
        key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);
    #endif
        if (key == GPIO_PIN_RESET)
            return 0;
        else
            return 1;            
    }
    

    If there are more macro switches, maintenance will be a disaster.

  2. Save the hardware version number in EEPROM and call the hardware version difference interface according to the version number

    // key.c
    // Return value: 0 means pressed and 1 means released
    int read_key(void)
    {
        GPIO_PinState key;
        int ver = read_hardware_ver();
        
    	if (ver == 1)
    	    key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
    	else (ver == 2)
    	    key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);
    
        if (key == GPIO_PIN_RESET)
            return 0;
        else
            return 1;            
    }
    
    

    Similar to macro switches, it is difficult to maintain a large number.

  3. Function pointer

    // key.c
    int (*read_key)(void);
    
    // Return value: 0 means pressed and 1 means released
    int read_key_ver1(void)
    {
        GPIO_PinState key;
        key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
        if (key == GPIO_PIN_RESET)
            return 0;
        else
            return 1;            
    }
    
    // Return value: 0 means pressed and 1 means released
    int read_key_ver2(void)
    {
        GPIO_PinState key;
        key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
        if (key == GPIO_PIN_RESET)
            return 0;
        else
            return 1;            
    }
    
    void key_init()
    {
        int ver = read_hardware_ver();
        if (ver == 1)
            read_key = read_key_ver1;
        else
            read_key = read_key_ver2;
    }
    
    // main.c
    void main(void)
    {
        int key;
        
        key_init();
        
        while (1)
        {
            key = read_key();
            if (key == UP)
                led_on();
            else
                led_off();
        }
    }
    
   
I think this method should belong to the upgraded version of the second method, which also needs to write the hardware version number EEPROM The difference is that after the function pointer is introduced, it only needs to be judged once according to the version number during power on initialization, and the corresponding interface of the version is assigned to the pointer, which does not need to make a lot of judgment calls in subsequent codes.
   

   
In this way, the first problem can be solved. Let's look at the second problem, how to solve the problem of software scalability?
   
There is a design principle in the design pattern: OCP,The principle of opening and closing roughly means that a good design needs to be open to expansion and closed to modification. In other words, only new codes are added during function expansion, and existing codes are not modified.
   

   
Back to the question, if we need to increase the number of keys, according to our previous thinking, it should be:
   
   ```c	
   // key.c
   // Return value: 0 means pressed and 1 means released
   int read_key1(void)
   {
       GPIO_PinState key;
       key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
       if (key == GPIO_PIN_RESET)
           return 0;
       else
           return 1;
   }
   
   // Return value: 0 means pressed and 1 means released
   int read_key2(void)
   {
       GPIO_PinState key;
       key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);
       if (key == GPIO_PIN_RESET)
           return 1;
       else
           return 0;
   }

or

// key.c
// Return value: 0 means pressed and 1 means released
int read_key(int which)
{
    GPIO_PinState key;
    switch (which)
    {
        case 0:
			key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
            if (key == GPIO_PIN_RESET)
                return 0;
            else
                return 1;
            break;
            
        case 1:
			key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);
            if (key == GPIO_PIN_RESET)
                return 1;
            else
                return 0;
            break;
            
    }
}

These two methods can alleviate the problem to a certain extent, but the symptoms are not the root cause. With the increase of the number of keys, the former method will become more and more chaotic at the call and difficult to maintain. In the latter method, as long as there is a key to add, our read will be modified_ The key function violates the OCP principle.

2.3 introduction of structure

New ideas are introduced here to solve the problem.

Let's first layer the program. The main function belongs to the application layer or business logic layer, and the key_manager belongs to the middle layer, and the lowest part belongs to the hardware driver layer. It realizes the management of keys through the middle layer, and decouples the business logic layer from the driver layer.

2.3.1 key_system

// key_manager.h
typedef struct key {
	char *name;
    void (*init)(struct key *k);
    int (*read)(void);
}key, *p_key;

// Initialization of all keys
void key_init(void);

// Get keys according to key name
key *get_key(char *name);
// key_manager.c
int key_cnt = 0;
key *keys[32];

void register_key(key *k)
{
	keys[key_cnt] = k;
	key_cnt++;
}

void key_init(void)
{
	k1_init();
	k2_init();
}

key *get_key(char *name)
{
	int i = 0; 
	for (i = 0; i < key_cnt; i++)
		if (strcmp(name, keys[i]->name) == 0)
			return keys[i];

	return 0;
}
// key1.c
// Return value: 0 means pressed and 1 means released
static int read_key1(void)
{
    GPIO_PinState key_status;
    key_status = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
    if (key_status == GPIO_PIN_RESET)
        return 0;
    else
        return 1;
}

static key k1 = {"k1", 0, read_key1};

void k1_init(void)
{
	register_key(&k1);
}
// key2.c
// Return value: 0 means pressed and 1 means released
static int read_key2(void)
{
    GPIO_PinState key_status;
    key_status = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);
    if (key_status == GPIO_PIN_RESET)
        return 1;
    else
        return 0;
}

static key k2 = {"k2", NULL, read_key2};

void k2_init(void)
{
	register_key(&k2);
}
// main.c
void main(void)
{
	key *k;
	
	key_init();	

	/* Use a key */
	k = get_key("k1");
	if (k == NULL)
		return;

	while (1)
	{
		if (k->read(k) == 0)
			led_on();
		else
			led_off();
	}
}

2.3.2 key_system_read_multi_key

There are still some problems in the current code, which does not decouple the business layer from the driver layer. There are specific keys in the main function, the call of read function and state judgment. At the same time, as the business layer, it is expected that the middle layer can read the status of all keys at the same time.

Then optimize the implementation of the middle layer:

// key_manager.h
typedef struct key {
	char *name;
    unsigned char id;
    void (*init)(struct key *k);
    int (*read)(void);
}key, *p_key;

#define KEY_UP		0xA
#define KEY_DOWN 	0xB

// Initialization of all keys
void key_init(void);

// Read the status of all keys
int read_key(void);
// key_manager.c
int key_cnt = 0;
key *keys[32];

void register_key(key *k)
{
	keys[key_cnt] = k;
	key_cnt++;
}

void key_init(void)
{
	k1_init();
	k2_init();
}

int read_key(void)
{
    int val;
	for (int i = 0; i < key_cnt; i++)
    {
        val = keys[i]->read();
        if (val == -1)
            continue;
        else
            return val;
    }
    return -1;
}
// key1.c
// Return value: 0 means pressed and 1 means released
#define KEY1_ID		1
static int read_key1(void)
{
    static GPIO_PinState pre_key_status;
    GPIO_PinState key_status;
    key_status = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
    
    if (key_status == pre_key_status)
        return -1;
    pre_key_status = key_status;
    if (key_status == GPIO_PIN_RESET)
        return KEY_DOWN | (KEY1_ID << 8);
    else
        return KEY_UP | (KEY1_ID << 8);
}

static key k1 = {"k1", KEY1_ID, NULL, read_key1};

void k1_init(void)
{
	register_key(&k1);
}
// key2.c
// Return value: 0 means pressed and 1 means released
#define KEY2_ID		2
static int read_key1(void)
{
    static GPIO_PinState pre_key_status;
    GPIO_PinState key_status;
    key_status = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);
    
    if (key_status == pre_key_status)
        return -1;
    pre_key_status = key_status;
    if (key_status == GPIO_PIN_RESET)
        return KEY_UP | (KEY2_ID << 8);
    else
        return KEY_DOWN | (KEY2_ID << 8);
}

static key k2 = {"k2", KEY2_ID, NULL, read_key2};

void k2_init(void)
{
	register_key(&k2);
}
// main.c
void main(void)
{
	int val;
	
	key_init();	

	while (1)
	{
		val = read_key();

		if (val == -1)
		{
			/* No keys */
		}
		else
		{
            key_status = val & 0xFF;
            key_id = (val>>8) & 0xFF:
			switch (key_status)
			{
				case KEY_UP: 
                    /* key_id release */
					break;
				case KEY_DOWN: 
                    /* key_id Press */
					break;
                default:
                    break;
            }
		}
	}
}

2.3.3 key_system_read_usr_irq

However, there are still some problems with this code. For example, the key detection now belongs to polling. If you can change the key detection to interrupt mode, you don't need to poll regularly when using RTOS, and you can wait for the interrupt to trigger.

At this time, a fifo can be introduced. The interrupt event is used as the data producer and the application layer is used as the data consumer to further decouple.

The implementation of fifo here is what I found casually from github.

// key_manager.h
typedef struct key {
	char *name;
    unsigned char id;
    void (*init)(struct key *k);
    int (*read)(void);
}key, *p_key;

#define KEY_UP		0xA
#define KEY_DOWN 	0xB

// Initialization of all keys
void key_init(void);

// Read key status
int read_key(void)

// Write a key status to fifo
void put_buffer(int val);

// Read out a key status from fifo
int read_buffer(void);
// key_manager.c
int key_cnt = 0;
key *keys[32];

// Define a Fifo buffer
static RingBufferPointer fifo;

void put_buffer(int val)
{
    ringBufferAdd(fifo, val);
}

int read_buffer()
{
    int val = -1;
    if (isRingBufferNotEmpty(fifo))
        val = ringBufferGet(fifo);
    return val;
}

void register_key(key *k)
{
	keys[key_cnt] = k;
	key_cnt++;
}

void key_init(void)
{
    fifo = getRingBufferInstance(100);
	k1_init();
	k2_init();
}

int read_key(void)
{
	return read_buffer();
}
// key0.c
#define KEY0_ID		0

static key k0 = {"k0", KEY0_ID, NULL, NULL};

void k0_init(void)
{
	register_key(&k0);
}

void key0_irq(void)
{
    int val;
    GPIO_PinState key_status;
    key_status = HAL_GPIO_ReadPin(KEY0_GPIO_Port, KEY0_Pin);
    
    if (key_status == GPIO_PIN_RESET)
        val = KEY_DOWN | (KEY0_ID << 8);
    else
        val = KEY_UP | (KEY0_ID << 8);
    put_buffer(val);
}

// key1.c
#define KEY1_ID		1

static key k1 = {"k1", KEY1_ID, NULL, read_key1};

void k1_init(void)
{
	register_key(&k1);
}

void key1_irq(void)
{
    int val;
    GPIO_PinState key_status;
    key_status = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
    
    if (key_status == GPIO_PIN_RESET)
        val = KEY_DOWN | (KEY1_ID << 8);
    else
        val = KEY_UP | (KEY1_ID << 8);
    put_buffer(val);
}
// key2.c
#define KEY2_ID		2

static key k2 = {"k2", KEY2_ID, NULL, read_key2};

void k2_init(void)
{
	register_key(&k2);
}

void key2_irq(void)
{
    int val;
    GPIO_PinState key_status;
    key_status = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);
    
    if (key_status == GPIO_PIN_RESET)
        val = KEY_DOWN | (KEY2_ID << 8);
    else
        val = KEY_UP | (KEY2_ID << 8);
    put_buffer(val);
}
// stm32f7xx_it.c
void EXTI2_IRQHandler(void)
{
  HAL_GPIO_EXTI_IRQHandler(KEY1_Pin);
}

void EXTI3_IRQHandler(void)
{
  HAL_GPIO_EXTI_IRQHandler(KEY0_Pin);
}

void EXTI15_10_IRQHandler(void)
{
  HAL_GPIO_EXTI_IRQHandler(KEY2_Pin);
}
// main.c
void main(void)
{
    int val;
    char key_status;
    char key_id;
	
	key_init();	

    // If it is rtos, the polling mode can not be applied
	while (1)
	{
        val = read_key();
        if (val != -1)
        {
            key_status = val & 0xFF;
            key_id = (val >> 8) & 0xFF;
            switch (key_status)
            {
                case KEY_DOWN:
                    if (key_id == 0)
                    {
                        HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET);
                    }
                    else if (key_id == 1)
                    {
                        HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);
                    }
                    else if (key_id == 2)
                    {                
                        HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET);
                        HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET);
                    }
                    break;
                case KEY_UP:
                    break;
            }
        }
	}
}

void HAL_GPIO_EXTI_Callback(uint16_t pin)
{
    HAL_Delay(50);
    switch (pin)
    {
        case KEY0_Pin:
            key0_irq();
            break;
        case KEY1_Pin:
            key1_irq();
            break;
        case KEY2_Pin:
            key2_irq();
            break;
        default:
            break;
    }
}

3. Experimental process

According to the above ideas, practice it on the development board.

3.1 create cubemx project

Since multiple buttons will be used, here I use the STM32F767 Apollo development board of punctual atom as the experimental platform.

After checking the schematic diagram of the development board, we know that:

  • There are 3 user keys on the development board, namely KEY0 (PH3), KEY1 (PH2) and KEY2 (PC13)
  • There are LED2 development boards, LED0 (PB1) and LED1 (PB0)

Configure the five gpios and generate code.

3.2 project structure

3.3 experimental effect

Press KEY0, LED0 will light up, press KEY1, LED1 will light up, press KEY2, LED0 and LED1 will go out at the same time. The experiment is completed.

4. Summary

In fact, according to Mr. Wei's course, the led subsystem is also established to manage the led through the middle layer, so as to reduce the coupling of the code and enhance the expansibility and reusability of the code. Since the idea is basically the same as that of the key subsystem, there is no further implementation.

Through this lesson, I learned the idea of hierarchical design of software structure, and considered the future function expansion and code robustness from the beginning of design. Improve the maintainability of the whole project by sacrificing a little operation efficiency.

Keywords: IoT stm32

Added by James Bond 007 on Wed, 16 Feb 2022 15:50:50 +0200