• 程序架构的核心理念或需求
  • 掌握回调函数的作用
  • 掌握回调函数的程序编写
  • 掌握回调函数在产品中的应用

程序结构的核心理念和需求

很多人认为好的程序就是代码紧凑、算法精妙、执行效率高。
这个说法非常片面, 因为这些都是代码的局部特征, 局部写得好不代表代码整体上写得好。而好的架构要以 “大局” 为重, 思维不能局限于当前架构下的产品功能, 还要考虑到以后功能的增加和裁剪。
对于单片机开发来说, 一个好的程序架构至少要达到以下要求:

硬件层和应用层的程序代码分开, 相互之间的控制和通讯使用接口, 不共享全局变量或数组。目的是提升可移植性和可扩展性。

硬件层可以通过 stm32 的库函数打开单片机对应功能来调用, 应用层是产品具体功能的程序代码, 他们之间需要数据的交互(通讯)。
编写较小的项目时, 可以使用全局变量来传输数据。
但是设计较大的项目时, 大量使用全局变量使得代码的可移植性和可拓展性大大降低, 最好是通过接口(函数)来传输数据。

编写 51 单片机代码时, 用一个 .c 文件就可以完成所有功能, 包括寄存器配置、产品功能, 这是没有架构的程序。
编写 stm32 单片机代码时, 程序体量增加, 需要在工程文件中加入几个文件夹目录将硬件层和应用层代码分开, 此时会将一些不同的外设功能, 比如 LED、按键、串口等硬件层的外设文件代码分别写在不同的 .C 文件当中, 然后统一用函数接口去调用。

比如点亮一个 LED 灯, 直接在 led.c 文件中写一个驱动 LED 状态的函数交给外部程序调用。此时便成功将应用层和硬件层的代码分开, 应用层直接调用这个函数接口即可, 不会出现任何全局变量或数组。
而回调函数在这其中起到联系硬件层和应用层的作用, 使得硬件层代码与应用层代码隔离, 并让应用层能够方便地调用硬件层接口。

回调函数的作用

函数调用一般分为两种:输出型、输入型

  1. 输出型:输出型函数是由程序员主导的角色, 是以数个形参作为输入, 调用函数控制外设。
  2. 输入型:响应式函数, 比如接收串口数据, 还比如按键检测的函数, 程序不知道什么时候会按下按键, 这些就要定义成响应式函数, 而响应式函数就是回调函数在单片机领域比较常用的一种。

回调函数基本是用在输入型的处理中, 比如串口数据接收、按键检测、ADC数据采集, 它们输入的时间节点都是未知的, 这些都能够通过回调函数来处理。大大提升了移植性、实时性和封装程度。

回调函数的写法

基本的程序架构分为硬件抽象层和应用层。

硬件抽象层(硬件层):

  1. 驱动模块:这是与硬件直接交互的模块,如LED, LCD, 传感器,马达等的驱动。这些模块负责对硬件进行底层的控制,使得应用层可以通过抽象接口来使用硬件。
  2. 中断处理模块:对硬件产生的中断进行处理。例如,当硬件设备完成了某项操作或者需要CPU的注意时,它会产生一个中断信号。
  3. 操作系统模块:这通常包括任务调度、内存管理、设备管理、文件系统等。它提供一个抽象的接口,使得应用层可以不用考虑具体的硬件实现。
  4. 通讯协议模块:如SPI, I2C, UART, CAN, Ethernet等。这些模块是为了让硬件设备能够进行通讯。

应用层:

  1. 用户接口模块:例如,按钮的检测和处理,LCD的显示,声音的播放等。
  2. 业务逻辑模块:这是实现嵌入式设备具体功能的模块。例如,对于一个智能门锁,可能包含密码验证,指纹识别,远程控制等功能模块。
  3. 数据处理模块:例如,对于传感器获取的数据进行处理和分析。
  4. 网络通讯模块:例如,通过Wi-Fi或蓝牙与其他设备进行通讯,或者实现OTA(Over-the-Air Technology)远程升级等。

这是一种常见的模块划分方法,但具体的划分方式取决于具体的产品需求和设计。例如,某些产品可能不需要网络通讯,或者需要额外的安全防护模块等。

而回调函数是数据在硬件层与应用层之间传输的关键。

key.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef _KEY_H_
#define _KEY_H_
typedef enum
{// 按键
KEY1,
KEY2,
}KEY_ID_TYPEDEF;
typedef enum
{// 按键状态
KEY_IDLE, //按键空闲
KEY_PRESS, //按键短按
KEY_LONG_PRESS, //按键长按
KEY_RELEASE, //按键释放
}KEY_STATE_TYPEDEF;

typedef void (*pKeyScanCallback) (KEY_ID_TYPEDEF keyID, KEY_STATE_TYPEDEF keySta); // 按键扫描回调函数

void keyInit();
void keyPoll();
void keyScanCBSRegister(pKeyScanCallback pCBS);
void keyScanHandle(KEY_ID_TYPEDEF keyID, KEY_STATE_TYPEDEF keySta);

#endif

key.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include "iostream"
#include "key.h"

KEY_ID_TYPEDEF keyVal;
KEY_STATE_TYPEDEF keySta;
pKeyScanCallback pKeyScanCBS;

void keyInit()
{//按键初始化
keyVal = KEY1;
keySta = KEY_IDLE;
pKeyScanCBS = 0;
}

void keyScanCBSRegister(pKeyScanCallback pCBS)
{
if (pKeyScanCBS == 0)
{
pKeyScanCBS = pCBS;
}
}

void keyPoll()
{//按键轮询
printf("Please enter key value:");
if(scanf("%d",&keyVal) == 1)
{
printf("\r\nPlease enter key state:");
if (scanf("%d",&keySta) == 1)
{
if (pKeyScanCBS != 0)
{
pKeyScanCBS(keyVal,keySta);
}
}
}
}

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "stdio.h"
#include "key.h"
#include "key.cpp"
void keyScanHandle(KEY_ID_TYPEDEF keyID,KEY_STATE_TYPEDEF keySta)
{
// if(keyID == KEY2)
// {
// if(keySta == KEY_PRESS)
// {
printf("keyID=%d, keySta=%d\r\n",keyID,keySta);
// }
// }
}

int main()
{
keyInit();
keyScanCBSRegister(&keyScanHandle);
keyPoll();
return 0;
}