为什么要写这篇
程序需要移植,移植一些底层函数调用接口就需要修改,其中遇到个灯闪烁函数,本以为三下五除二就能搞定,小case。没想到结果让我自己都惊讶。
什么闪烁函数这么牛
函数接口如下:设置灯闪烁行为的函数
有四个参数:idx(LED的下标) blink_time_ms (LED闪烁时间,亮时间 = 灭时间 = blink_time_ms/2) rest_ms (最后等待时间) times(LED闪烁次数, 如果为0意味着一直)
举例来说blink_time_ms = 400ms , rest_ms = 500ms, times = 5, 那么200ms后灯亮,再经过200ms灯灭,等待500ms ,为一个周期。下个周期仍为200ms后灯亮,再经过200ms灯灭,等待500ms。执行五个周期,灯不再闪烁。
我的分析,我要用定时器实现,不阻塞其它任务功能。idx LED的下标,没有实际意义,砍掉,返回值,没啥实际意义,砍掉。然后就成了这样
工欲善其事必先利其器,什么器?定时器,我之前用过的软件定时器MultiTimer ,为了用起来更顺手一些,更方便移植一些,给其封装了一下。大概用了36行。
typedef struct Multi_Timer Soft_Timer;
typedef void (*Timeout_Cb)(void);
/**
* software_timer_set
* @brief 软件定时器设置
* @param handle 参数描述: 定时器句柄标识
* @param callback_func 参数描述: 回调函数指针
* @param timeout_ms 参数描述: 定时timeout_ms 毫秒
* @param is_repeat 参数描述: 是否重复开启定时
* @note 由于底层没有留出回调函数参数传入,导致无法实现数据传入
*
*/
void
software_timer_set(Soft_Timer* handle, Timeout_Cb callback_func, uint32_t timeout_ms, _Bool is_repeat)
{
if(is_repeat)
{
multi_timer_init(handle, callback_func, timeout_ms, timeout_ms);
multi_timer_start(handle);
}else
{
multi_timer_init(handle, callback_func, timeout_ms, 0);
multi_timer_start(handle);
}
}
/**
* software_timer_stop
* @brief 软件定时器停止
* @param handle 参数描述: 定时器句柄标识
*/
void
software_timer_stop(Soft_Timer* handle)
{
multi_timer_stop(handle);
}
1. 为什么要封装?为了方便移植,每个芯片底层函数都有定时器功能实现,但不见得有软件定时器函数,没有的话就用Multi_Timer替代,有的话,直接将底层函数函数替换掉就好了。
2. 为什么要写注释?因为我没有谜之自信,函数介绍一下功能,说明一下参数,变量说明下作用,添加点儿备注都是给人看的,真正移植别人写的代码时就能体会到要是有些注释那该有多好哇。而且尽最大可能不要在函数内部写单条语句的注释, 失之毫厘,差之千里。没有功能介绍,零散的注释,一旦注释未来得及修正,让移植的人更加头大。
进入正题,如何实现
1. 对于该函数来说,每次执行都该是一次重新开始,如何重新开始,停止闪烁(关闭定时器),指示灯设置为灭。

2. 进入定时回调函数的时间如何确定?定时回调函数的首要功能是计时,然后才是执行功能。时间难道不是一个固定值吗?no no no ... 应该随着输入的变化而变化,还是假设blink_time_ms = 400ms , rest_ms = 500ms, 设置什么值最合适呢?100ms, 因为400/2 和 500能被整除的最大数为100,即最大公约数。此处用去66行代码
/**
* pow_func
* @brief 幂函数
* @param num1 参数描述: 无
* @param num2 参数描述: 无
* @retval uint16_t 返回值描述: 指数值
*/
uint16_t pow_func(uint16_t num1, uint8_t num2)
{
uint16_t need_val = 1;
for (uint8_t i = 0; i < num2; i++)
{
need_val *= num1;
}
}
/**
* greatest_common_divisor
* @brief 获取两数的最大公约数
* @param num1 参数描述: 无
* @param num2 参数描述: 无
* @retval uint16_t 返回值描述: 获取两数的最大公约数
* @note 1.两数是否有特殊关系,相等或成倍数关系
* @note 2.若都是10的倍数,化简
* @note 3.从两数中较小的数开始递减,直到被两数整除
*/
uint16_t greatest_common_divisor(uint16_t num1, uint16_t num2)
{
uint16_t max_val, min_val;
uint8_t index = 0;
if ((num1 == num2) || (num2 % num1 == 0))
{
return num1;
}
if (num1 % num2 == 0)
{
return num2;
}
while ((num1 % 10 == 0) && (num2 % 10 == 0))
{
num1 /= 10;
num2 /= 10;
index++;
}
if (num1 < num2)
{
min_val = num1;
max_val = num2;
}
else
{
min_val = num2;
max_val = num1;
}
for (uint16_t i = min_val; i > 0; i--)
{
if ((max_val % i == 0) && (min_val % i == 0))
{
return i * pow_func(10, index);
}
}
}
1. 最大公倍数怎么得来的?最大公倍数不可能比两数中较小的那个数大。因此从较小的那个数递减开始,直到找到第一个可以被两数都整除的数,即最大公倍数。根据设定时间的常规操作,对执行做一些简化,避免求最大公倍数时间过长。
2. 为什么要写一个幂函数?芯片对标准函数库中的函数 srand 和 rand都能重写,那我还会觉得math.h中的函数还是原装原味的吗。干脆自己写一个。
用到的变量不少,封装成一个结构体。
flag_invoke_blink_func 调用func_LED_blink 函数时,重新开始执行,那么回调函数中的静态变量就都该清0,还不是每次进入回调函数就清零,因此需要一个变量。
flag_finish_blink 只有在执行完闪烁后才会计时rest_ms时间,因此需要一个变量。
blink_times 闪烁时间是进入回调时间的整数倍,需要变量来确定第几次进入回调函数时执行操作。
rest_times 同理。
blink_cycle_times 要求执行的闪烁次数。占用8行代码
struct led_indicate
{
uint8_t flag_invoke_blink_func; //调用aiqi_LED_blink 函数标志
uint8_t flag_finish_blink; //完成一次闪烁标志
uint16_t blink_times; //完成闪烁时间需要进入回调函数的次数
uint16_t rest_times; //完成间隔时间需要进入回调函数的次数
uint16_t blink_cycle_times; //需要执行闪烁的次数
} twinkle;
设置结构体中变量的函数如下,占用30行代码
/**
* func_LED_blink
* @brief 重写该函数,之前是调用工程底层接口实现,为移植改成仅用定时器就能实现
* @param blink_time_ms 参数描述: 闪烁周期,亮灭总时间
* @param rest_ms 参数描述: 间隔时间
* @param times 参数描述: 闪烁次数,设置为0则一直闪烁
*/
void func_LED_blink(uint16_t blink_time_ms, uint16_t rest_ms, uint8_t times)
{
uint16_t need_value; //进入回调函数的时间
software_timer_stop(&timer_indicate_led);
indicator_led_off();
if (rest_ms != 0)
{
need_value = greatest_common_divisor(blink_time_ms / 2, rest_ms);
}
else
{
need_value = blink_time_ms / 2;
}
twinkle.blink_times = blink_time_ms / 2 / need_value;
twinkle.rest_times = rest_ms / need_value;
twinkle.blink_cycle_times = times;
twinkle.flag_invoke_blink_func = 1;
twinkle.flag_finish_blink = 0;
software_timer_set(&timer_indicate_led, indicate_led_callback, need_value, true);
}
回调函数中有哪些变量?into_times 记录是第几次进入回调函数 ; cycle_times 记录是第几个周期。 占用64行代码
代码逻辑和注释条理感觉还是比较清晰的。
/**
* indicate_led_callback
* @brief 指示灯回调函数
* @note 1.针对间隔时间为0,进入回调函数的时间为blink_time_ms / 2,直到执行完闪烁次数后关闭定时器
* @note 2.间隔时间不为0,通过最大公约数求得进入回调函数的时间,直到执行完闪烁次数后关闭定时器
*/
void indicate_led_callback(void)
{
static uint16_t into_times = 0; //进入该回调函数次数
static uint16_t cycle_times = 0; //闪烁循环次数
if (twinkle.flag_invoke_blink_func)
{
twinkle.flag_invoke_blink_func = 0;
into_times = 0;
cycle_times = 0;
}
into_times++;
if (twinkle.rest_times == 0)
{
if (into_times % 2)
{
indicator_led_toggle(); //indicator_led_on();
}
else
{
into_times = 0;
cycle_times++;
indicator_led_toggle(); //indicator_led_off();
if ((twinkle.blink_cycle_times > 0) && (cycle_times >= twinkle.blink_cycle_times))
{
cycle_times = 0;
software_timer_stop(&timer_indicate_led);
}
}
}
else
{
if (into_times == twinkle.blink_times)
{
indicator_led_toggle(); //indicator_led_on();
}
else if (into_times == 2 * twinkle.blink_times)
{
indicator_led_toggle(); //indicator_led_off();
twinkle.flag_finish_blink = 1;
}
else if (twinkle.flag_finish_blink)
{
if (into_times >= 2 * twinkle.blink_times + twinkle.rest_times)
{
into_times = 0;
twinkle.flag_finish_blink = 0;
cycle_times++;
if ((twinkle.blink_cycle_times > 0) && (cycle_times >= twinkle.blink_cycle_times))
{
cycle_times = 0;
software_timer_stop(&timer_indicate_led);
}
}
}
}
}
代码总行数:36 + 66 + 8 + 30 + 64 = 204。 对了,其中还没算上indicator_led_on , indicator_led_off 和 indicator_led_toggle 的实现代码。
是不是还觉得不可思议,但不觉得我是在开玩笑了吧?