100ask七天物联网训练营学习笔记 - 裸机程序框架设计
智能家居物联网学习
1. 前言
报名参加了100ask组织的七天物联网智能家居训练营,每天早上2个小时讲基础,下午2个小时讲进阶。通过两天的学习确实感到比较充实。下面几天我会陆续把每节课程的重点、难点以及自己的理解记录下来。
2. 理论
2.1 抛出问题
这节课韦老师以程序框架设计作为切入点开始了课程,以嵌入式裸机开发为例,很多初学者通常会在业务层代码甚至是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);
}
这样解决了业务逻辑代码与硬件驱动代码的耦合问题,但还有两个问题没有解决:
一、因硬件版本迭代导致的软件兼容性问题
二、功能扩展性问题
2.2 引入函数指针
这里我们要解决第一个问题,通常有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原则。
2.3 引入结构体
为了用新的思路解决问题,这里就引入了新的知识点:结构体。

我们先将程序分层,main函数属于应用层或业务逻辑层,key_manager属于中间层,最下面属于硬件驱动层,通过中间层来实现对按键的管理,同时将业务逻辑层与驱动层解耦。
2.3.1 key_system
// 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();
}
}
2.3.2 key_system_read_multi_key
目前的代码还有一些问题,没有将业务层与驱动层解耦,在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;
}
}
}
}
2.3.3 key_system_read_usr_irq
但是,这个代码还是有一些问题,比如现在按键的检测属于轮询,若可以将按键检测修改为中断方式,在使用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;
}
}
3. 实验过程
根据上面的思路,在开发板上实践一下。
3.1 创建cubemx工程

经过查看该开发板原理图得知,资源分布如下:
- 开发板共有用户按键3个,分别是KEY0(PH3)、KEY1(PH2)、KEY2(PC13)
- 开发板共有LED2个,分别是LED0(PB1)、LED1(PB0)

对这5个GPIO进行配置,然后生成代码。
3.2 项目结构

3.3 实验效果
按下KEY0,LED0亮起,按下KEY1,LED1亮起,按下KEY2,LED0、LED1同时熄灭。实验完成。
4. 总结
实际按照韦老师的课程中,将led也建立了子系统,通过中间层来管理led,以降低代码的耦合,增强代码的扩展性、复用性。由于思路和key子系统基本一样,就再没有进一步实现。
通过这节课学习了软件结构分层设计的思路,从设计之初就考虑未来的功能扩展、代码的健壮。通过牺牲一点点运行效率,来提高整个工程的可维护性。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)