报名参加了七天物联网训练营,收获甚多,根据韦东山老师的资料,将一些学到的知识总结下来用于后期的学习。
在《代码大全》第5章中,把程序设计分为这几个层次:
第1层:软件系统,就是整个系统、整个程序
第2层:分解为子系统或包。比如我们可以拆分为:输入子系统、显示子系统、业务系统
第3层:分解为类。在C语言里没有类,可以使用结构体来描述子系统。
第4层:分解成子程序:实现那些结构体(结构体中有函数指针)。
使用按键控制 LED例子来详细介绍一下代码思路:告诉我们为什么要引入结构体。
使用按键控制K1控制LED,功能很简单,但是可以探讨很多东西。
提出一些问题:
下列代码有很多缺点:
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引脚输出高电平}
}
这个程序很容易理解,因为你会看原理图,但是有些人不会看原理图,不同部门的人可能会看不明白。这相当于你把硬件锁死了,你换个引脚换个灯程序全部要改。这会造成业务层代码与板级代码严重耦合,对后续的软件功能扩展、硬件升级及代码复用都会产生不便,同时也会对不懂硬件的业务层开发人员造成障碍。
因此,需要将程序结构进行分层,将业务逻辑代码与硬件驱动代码分离:这就提出将一些功能提取出来写一个子函数,这样子更改板子或者业务可以直接调用。
// 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怎么办?
引出面向对象的程序设计方法:
假设你们公司产品升级了,按键发生了变化:
换了个引脚设计led,怎么写出一个支持多个版本的read_key函数?
#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,开闭原则,大致意思是好的设计需要对扩展开放,对修改关闭。用人话说就是做功能扩展时只新增代码,不对已有代码做修改。
假设有两个按键,它们的操作完全不同,如何实现两个按键同时按下。
你需要实现两个读函数:
// 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—>>可以使用结构体。
缺点:
定义一个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};
那么怎么用起来呢?引入概念,分层。
怎么管理这些按键?
需要增加一个管理层:.比如说注册:底层的代码放在上面的数组key *key[32];(比如说有32个按键)/列表中,把底层的key1、key2、key3放在数组中来。面向对象的思想,按键1抽象出一个结构体,注册到某一个数组/列表。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-88WoHSAw-1648727365354)(程序框架设计示例1_裸机.assets/4.png)]
怎么写程序?我们先将程序分层,main
函数属于应用层或业务逻辑层,key_manager
属于中间层,最下面属于硬件驱动层,通过中间层来实现对按键的管理,同时将业务逻辑层与驱动层解耦。
划分为按键系统、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;}}}
}
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;}}}
}
HAL库是不是经常使用开关宏来做&#xff1f; &#xff08;对得&#xff09;不需要支持M3或者M4。
哪些函数放在驱动层&#xff0c;哪些函数放在应用层&#xff1a;APP&#xff08;不用懂硬件。&#xff09; 和驱动程序&#xff08;硬件操作给封装好函数&#xff0c;让别人不懂硬件也可以使用。&#xff09;
RAM资源比较紧张的话&#xff0c;不用这样子写&#xff0c;这样子写会浪费资源。