热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

程序框架设计示例1_裸机1.前言报名参加了七天物联网训练营,收获甚多,根据韦东山老师的资料,将一些学到的知识总结下来用于后期的学习。

程序框架设计示例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引脚输出低电平elseHAL_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(); //打开ledelseled_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;elsereturn 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
#elsekey = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);//版本2
#endifif (key == GPIO_PIN_RESET)return 0;elsereturn 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;elsereturn 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;elsereturn 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;elsereturn 1;
}//初始化
void key_init()
{int ver = read_hardware_ver();if (ver == 1)read_key = read_key_ver1; //函数指read_key针指向read_key_ver1elseread_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();elseled_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;elsereturn 1;
}// 返回值: 0表示被按下, 1表示被松开
int read_key2(void)
{GPIO_PinState key;key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);if (key == GPIO_PIN_RESET)//读到零返回1return 1;elsereturn 0;
}

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

我们当然可以改进:

// key.c
// 返回值: 0表示被按下, 1表示被松开
//读哪个按键必须传入一个参数。
//在读引脚时要包含一堆的头文件。看着很别扭很麻烦。
#include
#include
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;elsereturn 1;break;//传入1表示想读按键1,走这个分支。 case 1:key = HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);if (key == GPIO_PIN_RESET)return 1;elsereturn 0;break;//假设是网络数据,虚拟成按键。case 2:read_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 &#61; 0; for (i &#61; 0; i < key_cnt; i&#43;&#43;)if (strcmp(name, keys[i]->name) &#61;&#61; 0)return keys[i];return NULL;
}

// key1.c
//我们构造了一个结构体。这个结构体是什么呢&#xff0c;可以看key_manager.h里
// 返回值: 0表示被按下, 1表示被松开
//按键的读函数
static int read_key1(void)
{GPIO_PinState key_status;key_status &#61; HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);if (key_status &#61;&#61; GPIO_PIN_RESET)return 0;elsereturn 1;
}//构造出的结构&#xff1a;key k1&#xff0c;。内容是 {"k1", 0, read_key1}
//要告诉中间层&#xff0c;你以后可以使用我了&#xff0c;怎么告诉呢有个初始化函数k1_init(void)&#xff0c;有个注册函数register_key(&k1)——>注册按键是什么意思呢&#xff0c;在中间层有个按键指针数组来注册。
static key k1 &#61; {"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 &#61; HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);if (key_status &#61;&#61; GPIO_PIN_RESET)return 1;elsereturn 0;
}static key k2 &#61; {"k2", NULL, read_key2};void k2_init(void)
{register_key(&k2);
}

// main.c
void main(void)
{key *k;key_init(); /* 使用某个按键 */k &#61; get_key("k1");if (k &#61;&#61; NULL)return;while (1){if (k->read(k) &#61;&#61; 0)//如果读取的按键等于零&#xff0c;表示已经按下。led_on();elseled_off();}
}

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

再对中间层实现进行优化&#xff1a;&#xff08;读取多个按键。有轮询方式&#xff0c;&#xff09;

// 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)&#xff1b;

// key_manager.c
int key_cnt &#61; 0;
key *keys[32];void register_key(key *k)
{keys[key_cnt] &#61; k;key_cnt&#43;&#43;;
}void key_init(void)
{k1_init();k2_init();
}int read_key(void)
{int val;for (int i &#61; 0; i < key_cnt; i&#43;&#43;){val &#61; keys[i]->read();if (val &#61;&#61; -1)continue;elsereturn 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 &#61; HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_6);if (key_status &#61;&#61; pre_key_status)return -1;pre_key_status &#61; key_status;if (key_status &#61;&#61; GPIO_PIN_RESET)return KEY_DOWN | (KEY1_ID << 8);elsereturn KEY_UP | (KEY1_ID << 8);
}static key k1 &#61; {"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 &#61; HAL_GPIO_ReadPin(GPIOF, GPIO_PIN_7);if (key_status &#61;&#61; pre_key_status)return -1;pre_key_status &#61; key_status;if (key_status &#61;&#61; GPIO_PIN_RESET)return KEY_UP | (KEY2_ID << 8);elsereturn KEY_DOWN | (KEY2_ID << 8);
}static key k2 &#61; {"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 &#61; read_key();if (val &#61;&#61; -1){/* 没有按键 */}else{key_status &#61; val & 0xFF;key_id &#61; (val>>8) & 0xFF:switch (key_status){case KEY_UP: /* key_id 松开 */break;case KEY_DOWN: /* key_id 按下 */break;default:break;}}}
}

4.一些提问的问题

HAL库是不是经常使用开关宏来做&#xff1f; &#xff08;对得&#xff09;不需要支持M3或者M4。

哪些函数放在驱动层&#xff0c;哪些函数放在应用层&#xff1a;APP&#xff08;不用懂硬件。&#xff09; 和驱动程序&#xff08;硬件操作给封装好函数&#xff0c;让别人不懂硬件也可以使用。&#xff09;

_key(&k2);
}


&#96;&#96;&#96;c
// main.c
void main(void)
{int val;key_init(); while (1){val &#61; read_key();if (val &#61;&#61; -1){/* 没有按键 */}else{key_status &#61; val & 0xFF;key_id &#61; (val>>8) & 0xFF:switch (key_status){case KEY_UP: /* key_id 松开 */break;case KEY_DOWN: /* key_id 按下 */break;default:break;}}}
}

4.一些提问的问题

HAL库是不是经常使用开关宏来做&#xff1f; &#xff08;对得&#xff09;不需要支持M3或者M4。

哪些函数放在驱动层&#xff0c;哪些函数放在应用层&#xff1a;APP&#xff08;不用懂硬件。&#xff09; 和驱动程序&#xff08;硬件操作给封装好函数&#xff0c;让别人不懂硬件也可以使用。&#xff09;

RAM资源比较紧张的话&#xff0c;不用这样子写&#xff0c;这样子写会浪费资源。


推荐阅读
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • 本文介绍了一种划分和计数油田地块的方法。根据给定的条件,通过遍历和DFS算法,将符合条件的地块标记为不符合条件的地块,并进行计数。同时,还介绍了如何判断点是否在给定范围内的方法。 ... [详细]
  • 本文介绍了P1651题目的描述和要求,以及计算能搭建的塔的最大高度的方法。通过动态规划和状压技术,将问题转化为求解差值的问题,并定义了相应的状态。最终得出了计算最大高度的解法。 ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • VScode格式化文档换行或不换行的设置方法
    本文介绍了在VScode中设置格式化文档换行或不换行的方法,包括使用插件和修改settings.json文件的内容。详细步骤为:找到settings.json文件,将其中的代码替换为指定的代码。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
author-avatar
梁梁庆新
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有