程序框架设计示例1_裸机day2

程序框架设计示例1_裸机

1.前言

报名参加了七天物联网训练营,收获甚多,根据韦东山老师的资料,将一些学到的知识总结下来用于后期的学习。

2. 框架设计

在《代码大全》第5章中,把程序设计分为这几个层次:

  • 第1层:软件系统,就是整个系统、整个程序

  • 第2层:分解为子系统或包。比如我们可以拆分为:输入子系统、显示子系统、业务系统

  • 第3层:分解为类。在C语言里没有类,可以使用结构体来描述子系统。

  • 第4层:分解成子程序:实现那些结构体(结构体中有函数指针)。

    使用按键控制 LED例子来详细介绍一下代码思路:告诉我们为什么要引入结构体。

3. 按键控制LED

在这里插入图片描述

使用按键控制K1控制LED,功能很简单,但是可以探讨很多东西。

提出一些问题:

2.1 耦合太严重

下列代码有很多缺点:

  • 暴露了太多细节:需要结合原理图、需要有硬件知识、需要有HAL库知识,才能理解这个程序
  • 代码无法复用:换个芯片、换个引脚,代码要全部修改
  • 无法扩展:比如想实现长按点灯,"长按"这个动作就不容易扩展
void main(void)
{
    GPIO_PinState key;  //读取按键
    while (1)
    {
        key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6); //读F组引脚
        if (key == GPIO_PIN_RESET)  //如果等于零的话
            HAL_GPIO_WritePin(GPIOF, GPIO_PIN_5, GPIO_PIN_RESET);
        	//设置LED引脚输出低电平
        else
            HAL_GPIO_WritePin(GPIOF, GPIO_PIN_5, GPIO_PIN_SET);
        	//设置LED引脚输出高电平
    }
}

这个程序很容易理解,因为你会看原理图,但是有些人不会看原理图,不同部门的人可能会看不明白。这相当于你把硬件锁死了,你换个引脚换个灯程序全部要改。这会造成业务层代码与板级代码严重耦合,对后续的软件功能扩展、硬件升级及代码复用都会产生不便,同时也会对不懂硬件的业务层开发人员造成障碍。

因此,需要将程序结构进行分层,将业务逻辑代码与硬件驱动代码分离:这就提出将一些功能提取出来写一个子函数,这样子更改板子或者业务可以直接调用。

2.2 使用子函数

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

// 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);
}

写成子函数解决了耦合问题,但是还有如下缺点:

  • 按键无法扩展:想支持多个按键怎么办?想支持长按、短按、双击等等,怎么办?

  • LED无法扩展:想支持多个LED怎么办?

    引出面向对象的程序设计方法:

2.3 面向对象

2.3.1 函数指针的引入

假设你们公司产品升级了,按键发生了变化:

在这里插入图片描述

换了个引脚设计led,怎么写出一个支持多个版本的read_key函数?

  • 方法1:宏开关,在代码里指定使用哪套代码
    • 缺点:只能支持一个版本,如果宏开关过多,无法进行维护。当你版本过多就要写多个分支。
#define HARDWARE_VER  1	//定义宏等于1.
//若是你想支持版本2,写成	#define HARDWARE_VER  2
//但是它没法及支持版本1又支持版本2.

// key.c
// 返回值: 0表示被按下, 1表示被松开
int read_key(void)
{
    GPIO_PinState key;
#if (HARDWARE_VER == 1)
    key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);//版本1
#else
    key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);//版本2
#endif
    if (key == GPIO_PIN_RESET)
        return 0;
    else
        return 1;            
}
  • 方法2:读取硬件版本,根据硬件版本调用对应代码,可以同时支持多个版本

    • 缺点:如果版本很多,效率很低,太多仍然不好维护。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kl3YPzx3-1648727365352)(程序框架设计示例1_裸机.assets/2.png)]

      使用E2PROM来存储引脚信息,比如外接有三引脚,000代表版本1,001代表版本2,…总共有2三次方个版本信息。你可以写一个函数read_hardware_ver()来实现。

// key.c
// 返回值: 0表示被按下, 1表示被松开
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);
    
    .....     ///假如有无数个。
        
	else (ver == n)
	    key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_n);
    if (key == GPIO_PIN_RESET)
        return 0;
    else
        return 1;            
}
  • 方法3:使用函数指针

    什么是函数指针,

    int a;  //整数
    int *a; //指针
    void read_key(); //函数
    void*read_key)(); 
    //指针,但是要注意,*是描述返回值,还是描述函数指针,因此加一个括号
    
int (*read_key)(void); //函数指针,到底指向那个函数需要判断。

//版本1
// 返回值: 0表示被按下, 1表示被松开
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;            
}

//版本2
// 返回值: 0表示被按下, 1表示被松开
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; //函数指read_key针指向read_key_ver1
    else
        read_key = read_key_ver2; //函数指read_key针指向read_key_ver2
}

// main.c
void main(void)
{
    int key;
    
    key_init();  //初始化按键
    
    while (1)
    {
        key = read_key(); //不用像以前一样每次调用KEY都要判断版本
        if (key == UP)
            led_on();
        else
            led_off();
    }
}

需要将硬件版本号写入EEPROM中,在软件中进行判断,但是区别在于引入函数指针后,只需要在上电初始化过程中根据版本号判断一次,将版本对应接口赋值给指针,不需要在后续的代码中进行大量的判断调用。

第二个问题: 如何解决软件扩展性问题?

设计模式中有一个设计原则:OCP,开闭原则,大致意思是好的设计需要对扩展开放,对修改关闭。用人话说就是做功能扩展时只新增代码,不对已有代码做修改。

2.3.2 怎么处理多个按键(解决扩展性问题)

假设有两个按键,它们的操作完全不同,如何实现两个按键同时按下。

在这里插入图片描述

你需要实现两个读函数:

// key.c
// 返回值: 0表示被按下, 1表示被松开
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;
}

// 返回值: 0表示被按下, 1表示被松开
int read_key2(void)
{
    GPIO_PinState key;
    key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);
    if (key == GPIO_PIN_RESET)//读到零返回1
        return 1;
    else
        return 0;
}

能否用一个函数,既可以读按键1,也可以读按键2呢?

我们当然可以改进:

// key.c
// 返回值: 0表示被按下, 1表示被松开
//读哪个按键必须传入一个参数。
//在读引脚时要包含一堆的头文件。看着很别扭很麻烦。
#include <hal...h>
#include <wifi...h>
int read_key(int which)
{
    GPIO_PinState key;
    switch (which)
    {
        //传入0表示想读按键0,走这个分支。
        case 0:
			key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);
            if (key == GPIO_PIN_RESET)
                return 0;
            else
                return 1;
            break;
       //传入1表示想读按键1,走这个分支。     
        case 1:
			key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);
            if (key == GPIO_PIN_RESET)
                return 1;
            else
                return 0;
            break;
        //假设是网络数据,虚拟成按键。
        case 2read_net_data();
            if (data==xxx)
                return  xxx;
    }
}

上述函数中,K1、K2的代码掺和在一起,不好扩展。比如我们想增加K3时,可能并不是用按键触发的,只是一个虚拟的按键,例如是一种网络,并不想、也不需要顾虑K1、K2的存在。怎么办?我想要让K1独立,K2独立,K3独立,我们可以写一个k1.c、k2.c、k3.c—>>可以使用结构体。

缺点:

2.3.3 结构体的引入

定义一个key结构体:

//定义一个结构体
typedef struct key {
	char *name;
    void (*init)(struct key *k);
    int (*read)(struct key *k);
}key, *p_key;

每个按键都实现一个key结构体:

// key1.c
key k1 = {"k1", NULL, read_key1}; //read_key1只读这个函数。

// key2.c
key k2 = {"k2", NULL, read_key2};

// key_net.c
key k_net = {"net", NULL, read_key_net};

那么怎么用起来呢?引入概念,分层。
在这里插入图片描述

2.3.4 管理层/接口层的引入(分层)

怎么管理这些按键?

需要增加一个管理层:.比如说注册:底层的代码放在上面的数组key *key[32];(比如说有32个按键)/列表中,把底层的key1、key2、key3放在数组中来。面向对象的思想,按键1抽象出一个结构体,注册到某一个数组/列表。

在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-88WoHSAw-1648727365354)(程序框架设计示例1_裸机.assets/4.png)]

怎么写程序?我们先将程序分层,main函数属于应用层或业务逻辑层,key_manager属于中间层,最下面属于硬件驱动层,通过中间层来实现对按键的管理,同时将业务逻辑层与驱动层解耦。

2.4 整体改造

2.3.1 系统划分

划分为按键系统、LED系统、业务系统:

在这里插入图片描述

// key_manager.h
typedef struct key {
	char *name;     //按键的名字
    void (*init)(struct key *k); //按键的初始化函数
    int (*read)(void);   //按键的读函数
}key, *p_key;

// 所有按键的初始化
void key_init(void);

// 根据按键name获取按键
key *get_key(char *name);
// key_manager.c
int key_cnt = 0;
key *keys[32]; //32位的数组

//注册按键,结构体k放在数组里。
void register_key(key *k)
{
	keys[key_cnt] = k;
	key_cnt++;
}

//按键初始化。
void key_init(void)
{
	k1_init();
	k2_init();
}

//取出某一个按键。传一个char形的名字
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 NULL;
}
// key1.c
//我们构造了一个结构体。这个结构体是什么呢,可以看key_manager.h里
// 返回值: 0表示被按下, 1表示被松开
//按键的读函数
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;
}

//构造出的结构:key k1,。内容是 {"k1", 0, read_key1}
//要告诉中间层,你以后可以使用我了,怎么告诉呢有个初始化函数k1_init(void),有个注册函数register_key(&k1)——>注册按键是什么意思呢,在中间层有个按键指针数组来注册。
static key k1 = {"k1", 0, read_key1};

//
void k1_init(void)
{
	register_key(&k1);
}
// key2.c
// 返回值: 0表示被按下, 1表示被松开
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();	

	/* 使用某个按键 */
	k = get_key("k1");
	if (k == NULL)
		return;

	while (1)
	{
		if (k->read(k) == 0)//如果读取的按键等于零,表示已经按下。
			led_on();
		else
			led_off();
	}
}

目前的代码还有一些问题,没有将业务层与驱动层解耦,在main函数中还有具体按键read函数的调用及状态判断。同时,作为业务层期望中间层可以同时读取所有按键的状态。

再对中间层实现进行优化:(读取多个按键。有轮询方式,)

// 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

// 所有按键的初始化
void key_init(void);

// 读取所有按键的状态
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
// 返回值: 0表示被按下, 1表示被松开
#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
// 返回值: 0表示被按下, 1表示被松开
#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)
		{
			/* 没有按键 */
		}
		else
		{
            key_status = val & 0xFF;
            key_id = (val>>8) & 0xFF:
			switch (key_status)
			{
				case KEY_UP: 
                    /* key_id 松开 */
					break;
				case KEY_DOWN: 
                    /* key_id 按下 */
					break;
                default:
                    break;
            }
		}
	}
}

4.一些提问的问题

HAL库是不是经常使用开关宏来做? (对得)不需要支持M3或者M4。

哪些函数放在驱动层,哪些函数放在应用层:APP(不用懂硬件。) 和驱动程序(硬件操作给封装好函数,让别人不懂硬件也可以使用。)

_key(&k2);
}


```c
// main.c
void main(void)
{
	int val;
	
	key_init();	

	while (1)
	{
		val = read_key();

		if (val == -1)
		{
			/* 没有按键 */
		}
		else
		{
            key_status = val & 0xFF;
            key_id = (val>>8) & 0xFF:
			switch (key_status)
			{
				case KEY_UP: 
                    /* key_id 松开 */
					break;
				case KEY_DOWN: 
                    /* key_id 按下 */
					break;
                default:
                    break;
            }
		}
	}
}

4.一些提问的问题

HAL库是不是经常使用开关宏来做? (对得)不需要支持M3或者M4。

哪些函数放在驱动层,哪些函数放在应用层:APP(不用懂硬件。) 和驱动程序(硬件操作给封装好函数,让别人不懂硬件也可以使用。)

RAM资源比较紧张的话,不用这样子写,这样子写会浪费资源。

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值