赞
踩
报名参加了100ask组织的七天物联网智能家居训练营,每天早上2个小时讲基础,下午2个小时讲进阶。通过两天的学习确实感到比较充实。下面几天我会陆续把每节课程的重点、难点以及自己的理解记录下来。
这节课韦老师以程序框架设计作为切入点开始了课程,以嵌入式裸机开发为例,很多初学者通常会在业务层代码甚至是main函数中,直接调用用于描述或硬件的接口(如HAL库):
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);
}
}
这会造成业务层代码与板级代码严重耦合,对后续的软件功能扩展、硬件升级及代码复用都会产生不便,同时也会对不懂硬件的业务层开发人员造成障碍。
为了解决这一问题,我们将程序结构进行分层,将业务逻辑与硬件驱动代码分离:
// 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); }
这样解决了业务逻辑代码与硬件驱动代码的耦合问题,但还有两个问题没有解决:
一、因硬件版本迭代导致的软件兼容性问题
二、功能扩展性问题
这里我们要解决第一个问题,通常有3种方法:
宏开关
#define HARDWARE_VER 1 // key.c // 返回值: 0表示被按下, 1表示被松开 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; }
宏开关如果一多,维护起来将是灾难。
在EEPROM中保存硬件版本号,根据版本号调用硬件版本差异接口
// 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); if (key == GPIO_PIN_RESET) return 0; else return 1; }
与宏开关类似,数量一多,维护起来很困难。
函数指针
// key.c int (*read_key)(void); // 返回值: 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; } // 返回值: 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; 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(); } }
我觉得这种方法应该属于第2种方法的升级版,同样需要将硬件版本号写入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; 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) return 1; else return 0; }
或
// key.c // 返回值: 0表示被按下, 1表示被松开 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; } }
这两种方式都能一定程度上缓解问题,但是治标不治本。前一种方法会随着按键数量的增多,在调用处越来越混乱,难以维护。后一种方法,只要有按键新增就会修改我们的read_key
函数,违反OCP原则。
为了用新的思路解决问题,这里就引入了新的知识点:结构体。
我们先将程序分层,main
函数属于应用层或业务逻辑层,key_manager
属于中间层,最下面属于硬件驱动层,通过中间层来实现对按键的管理,同时将业务逻辑层与驱动层解耦。
// 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]; 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 // 返回值: 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; } 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; } } } }
但是,这个代码还是有一些问题,比如现在按键的检测属于轮询,若可以将按键检测修改为中断方式,在使用RTOS
时就不需要定时进行轮询了,完全可以等待中断来触发。
此时,可以引入一个fifo
,中断事件作为数据的生产者,应用层作为数据的消费者,进一步解耦。
这里fifo
实现是我从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 // 所有按键的初始化 void key_init(void); // 读取按键状态 int read_key(void) // 向fifo写入一个按键状态 void put_buffer(int val); // 从fifo读出一个按键状态 int read_buffer(void);
// key_manager.c int key_cnt = 0; key *keys[32]; // 定义一个Fifo缓冲器 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(); // 如果是rtos就可以不适用轮询方式 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; } }
根据上面的思路,在开发板上实践一下。
经过查看该开发板原理图得知,资源分布如下:
对这5个GPIO进行配置,然后生成代码。
按下KEY0,LED0亮起,按下KEY1,LED1亮起,按下KEY2,LED0、LED1同时熄灭。实验完成。
实际按照韦老师的课程中,将led也建立了子系统,通过中间层来管理led,以降低代码的耦合,增强代码的扩展性、复用性。由于思路和key子系统基本一样,就再没有进一步实现。
通过这节课学习了软件结构分层设计的思路,从设计之初就考虑未来的功能扩展、代码的健壮。通过牺牲一点点运行效率,来提高整个工程的可维护性。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。