一般的情况下,低功耗蓝牙芯片由于要保持低功耗工作,本身会提供一种轻度休眠模式,在这种模式下保持内核时钟继续运行以定期进行广播或交换连接数据。SDK中一般都会提供一个基于BLE内核时钟的timer,这个timer在CPU轻度休眠的时候,仍然可以继续工作,功能非常强大。但是在非BLE的CPU中,由于不需要这种低功耗的工作状态,一旦休眠,内核时钟和外设一般都会彻底停止,等待各种中断唤醒,只有RTC等有限的几个器件仍然能正常工作。
有一个新的产品使用WIFI的方案,采用GD的芯片,芯片本身在休眠的时候会彻底关闭内核时钟。除非使用RTC,否则无法像蓝牙芯片那样使用timer来定时唤醒执行任务。但是我过往的产品一般都只有一颗或者两颗纽扣电池,特别注重功耗,因此这里刚阅读完GD的datasheet,就在琢磨着怎么降低功耗。
基于CPU的设计,在需要 休眠——唤醒 的场合,必须使用RTC或者其他的外部中断,这个没得解决。但是普通的对于精度要求不高的场合,比如检测按键的长按事件,某些耗时稍微长一些的操作,比如读取温湿度,adc采集等,都有用到定时器。这里的话,如果总是使用硬件timer,对于功耗和资源都会造成额外的压力,这时候就可以利用system tick来生成一个简易的软件timer。
本timer使用systick中断来计数,如果systick中断频率太高,会影响效率,这里将timer的最小延时单位设计为1ms。
对于一个timer来讲,最重要的两点是延时时间和要执行的任务。这里先定义一个回调函数用于启动timer的时候进行注册:
typedef void (*timer_cb)(void);
接下来定义一个结构体,用于记录timer的各种信息:
typedef struct {
bool loop; //是否循环执行
uint32_t id; //唯一id,用于可能的取消操作
timer_cb m_cb; //回调函数
uint32_t target_ms; //预设的延时时间
uint32_t init_ms; //启动这个timer的时候,计时器已经走到什么值了,后边要减掉这么多的时间
volatile uint32_t acc_ms; //内部计数,相当于一个计时器,会在systick中断的时候进行累加,达到预设时间后执行回调函数,注意这个值是多线程读写需要标记为volatile类型
} TIMER_DEF;
并添加如下定义:
static uint32_t valid_timer_size=0; //已经生效的timer的个数
static TIMER_DEF timer_list[TIMER_MAX_SIZE]= {0}; //顺序表,systick中断会遍历这个列表,如果同时生效的timer数量太多,可能会造成性能和功耗上的问题
static uint32_t acc_timer_id=TIMER_ID_INVALID+1; //用于唯一标记一个timer的序号,这里不需要考虑溢出的问题
由于每一次sysick中断都会遍历timer_list,为达到最优性能,这里会在每次生效的timer的个数发生变化的时候,重新检查并清理这个列表:
static void fix_timer_list(uint32_t cur_remove_index) {
if(cur_remove_index
cur_remove_timer为当前要移除掉的timer在这个列表中的位置。 这里为了保持timer的顺序,没有使用效率更高的交换法,而是直接将被移除timer后的所有的timer都往前移动一个位置。
然后就可以开始或者取消一个timer。
uint32_t timer_start(uint32_t target_ms,bool loop,timer_cb m_cb) {
if(valid_timer_size0) {
index=valid_timer_size;
}
timer_list[index].id=acc_timer_id;
acc_timer_id++;
timer_list[index].loop=loop;
timer_list[index].m_cb=m_cb;
timer_list[index].target_ms=target_ms;
timer_list[index].acc_ms=0;
return timer_list[index].id;
}
return TIMER_ID_INVALID;
}
timer_start函数需要提供三个参数,分别是延时时间和是否循环执行以及要执行的任务,该函数返回timer的id,如果已经生效的timer达到了上限,则会返回 TIMER_ID_INVALID。下面是取消一个timer的函数,注意这里取消以后需要及时清理列表:
void timer_cancel(uint32_t timer_id) {
uint32_t m_timer_index=0;
while(m_timer_index
有了开始和取消的操作后,还需要在timer开启后进行计数并检查是否已经达到了延时时间:
static void acc_check(void) {
static uint32_t m_timer_check_index; //这个函数会被超高频调用,尽量减少局部变量的产生
const uint32_t m_acc_cnt=acc_cnt;
acc_cnt=0;
if(valid_timer_size!=0) {
m_timer_check_index=valid_timer_size-1; //从后向前遍历,根据习惯这里可以从前向后遍历
do {
timer_list[m_timer_check_index].acc_ms+=m_acc_cnt; //累加timer
if(timer_list[m_timer_check_index].init_ms!=0){
if(timer_list[m_timer_check_index].acc_ms>=timer_list[m_timer_check_index].init_ms){
timer_list[m_timer_check_index].acc_ms-=timer_list[m_timer_check_index].init_ms;
}
timer_list[m_timer_check_index].init_ms=0;
}
if(timer_list[m_timer_check_index].acc_ms>=timer_list[m_timer_check_index].target_ms) {//比较是否达到了目标延时时间
timer_cb m_cb=timer_list[m_timer_check_index].m_cb;
if(timer_list[m_timer_check_index].loop) {
timer_list[m_timer_check_index].acc_ms=0;//循环的timer,将计数清零
} else {
fix_timer_list(m_timer_check_index);//单次执行的timer,直接移除
}
if(m_cb!=NULL){
m_cb();//执行timer的回调
}
}
m_timer_check_index--;
} while(m_timer_check_index!=0);
}
}
那么,如何执行这个acc_check函数呢?作为一个合格的Javaer,当然不能让这个函数成为大路货,谁都可以用,否则会造成意外调用。
我在这里又留了个心眼,假如某一天我突然脑子抽风不想使用systick来作为timer的计时来源呢?那好,我们就来一个注册式的实现吧。
首先定义一个函数指针,用于保存acc_check函数的地址:
typedef void (*timer_acc)(void); //类型与acc_check一致
static int register_state=0; //标记是否已经注册过了,acc_check只能注册一次,否则会导致重复计时
接下来定义注册函数,该函数接收一个指向timer_acc类型的指针,用于保存acc_check函数的地址:
void register_timer_source(timer_acc *acc_method_ptr) { //acc_method_ptr:输入参数,需要提供一个指针用来保存acc_check函数的地址
if(register_state!=0) {
return;
}
register_state=1;
*acc_method_ptr=acc_check;
}
到此为止,一个简易的timer就已经设计好了。假如我们要使用systick来作为计时来源,只需要这样做即可:
volatile static uint32_t delay;
static timer_acc m_systick_acc; //保存acc_check函数的地址
void systick_config(void){ //配置systick
/* setup systick timer for 1000Hz interrupts (1ms)*/
if (SysTick_Config(SystemCoreClock / 1000U)){
/* capture error */
while (1){
}
}
/* configure the systick handler priority */
NVIC_SetPriority(SysTick_IRQn, 0x00U);
register_timer_source(&m_systick_acc); //在这里添加注册函数
}
void SysTick_Handler(void){ //systick中断
delay_decrement();
m_systick_acc(); //在这里执行acc_check函数.
}
上述timer的代码写完后没有充分测试,后来在实际使用中发现若干bug(已修复)以及systick不适用的场景,所以折腾了一会儿后,我改为使用硬件Timer作为计时来源了,亏得我前边画蛇添足做了一些无用功→_→;
实现上,添加定义,时钟频率设置为10KHz,设置一个flag来标记timer是否已经启动了,新增一个acc_cnt用于记录timer中断触发的次数(便于main loop直接累加这个值);
#define PRESCALER_10KHZ ((SystemCoreClock/10000U)-1)
static bool timer_start_flag=false;
volatile uint16_t acc_cnt=0;
void timer_cfg_init(void){
timer_parameter_struct timer_initpara;
rcu_periph_clock_enable(RCU_TIMER1);
timer_deinit(TIMER1);
nvic_priority_group_set(NVIC_PRIGROUP_PRE1_SUB3);
nvic_irq_enable(TIMER1_IRQn, 1, 1);
/* TIMER1 configuration */
timer_initpara.prescaler = PRESCALER_10KHZ;
timer_initpara.alignedmode = TIMER_COUNTER_EDGE;
timer_initpara.counterdirection = TIMER_COUNTER_UP;
timer_initpara.period = 9;//1ms
// timer_initpara.period = 999;//100ms
timer_initpara.clockdivision = TIMER_CKDIV_DIV1;
timer_initpara.repetitioncounter = 0;
timer_init(TIMER1,&timer_initpara);
/* auto-reload preload enable */
timer_auto_reload_shadow_disable(TIMER1);
timer_interrupt_enable(TIMER1,TIMER_INT_UP);
}
static void start_timer(void){
if(!timer_start_flag){
timer_start_flag=true;
timer_enable(TIMER1);
}
}
static void close_timer(void){
if(timer_start_flag){
timer_disable(TIMER1);
timer_start_flag=false;
}
}
extern void TIMER1_IRQHandler(void){
if(SET==timer_interrupt_flag_get(TIMER1,TIMER_INT_FLAG_UP)){
timer_interrupt_flag_clear(TIMER1,TIMER_INT_FLAG_UP);
acc_cnt++;
set_irq_flag(EASY_TIMER_IRQ);
}
}
启动这个timer也很简单,在main_loop开始之前,调用register_timer_source 函数注册acc_check回调,然后在main_loop中执行这个回调,主循环每走一轮就会累加一次acc_cnt的值,从而决定是否要执行某个timer对应的回调函数。
这里有个set_irq_flag,这个函数是我的框架中,专门处理中断的部分,这个函数用于标记一个中断被产生了。
文件IRQ_state_handler:
#define IRQ_NONE 0
#define BTN_IRQ_0 1
#define BTN_IRQ_1 1<<1
#define BTN_IRQ_2 1<<2
#define BTN_IRQ_3 1<<3
#define RTC_ALARM_IRQ 1<<4
#define WIFI_UART_IRQ 1<<5
#define WIFI_EXTI_IRQ 1<<6
#define EASY_TIMER_IRQ 1<<7
typedef void (*irq_handler)(void);
void set_irq_flag(uint16_t IRQ_ID);
void clear_irq_flag(uint16_t IRQ_ID);
void clear_all_non_timer_irq(void);
void check_irq_action(void);
void irq_handler_init(void);
在我的项目中,总共使用八个中断,其中四个按键中断,一个RTC闹钟中断,一个timer中断,2个串口中断。串口中断是一个UART中断,一个用于唤醒的EXTI中断,这里的EXTI中断是由于我们最初设计的休眠模式是deepsleep,但是deepsleep电流未达到要求,改为standby,standby模式下无法被EXTI唤醒,所以这个中断现在无法生效。
typedef struct{
uint16_t irq_id;
irq_handler p_handler;
}irq;//中断结构,包含一个id,一个回调函数
static irq irq_list[]={
{WIFI_EXTI_IRQ,wifi_exti_irq_handler},
{WIFI_UART_IRQ,wifi_uart_irq_handler},
{BTN_IRQ_0,btn_irq_KEY_0_handler},
{BTN_IRQ_1,btn_irq_KEY_1_handler},
{BTN_IRQ_2,btn_irq_KEY_2_handler},
{BTN_IRQ_3,btn_irq_KEY_3_handler},
{RTC_ALARM_IRQ,rtc_alarm_action},
{EASY_TIMER_IRQ,acc_check},
};//中断列表,八个中断
static volatile uint16_t irq_flag=0; //16位总共可以标记16个中断
void clear_all_irq_flag(void){
irq_flag=0;
}
void clear_all_non_timer_irq(void){//清除所有非timer中断
irq_flag=irq_flag>>7<<7;
}
void clear_irq_flag(uint16_t IRQ_ID){
irq_flag&=~IRQ_ID;
}
void set_irq_flag(uint16_t IRQ_ID){//标记一个中断
irq_flag|=IRQ_ID;
}
uint16_t get_irq_flag(uint8_t *p_list_index){//返回第一个被标记的中断,并清除标记
uint8_t irq_sz=sizeof(irq_list)/sizeof(irq_list[0]);
for(int i=0;i
然后,在main函数中进行轮询:
irq_handler_init();
while(1){
check_irq_action();
main_loop();
}
main_loop用于执行一些非中断任务,在我的项目中,则是用于进入standby模式,即当检测到当前被调起的timer数量为0的时候,就表示所有的任务都被处理完了,可以进入standby模式。
void main_loop(void) {
if(rtc_wkup){
auto_tasks();
rtc_wkup=false;
}
if(valid_timer_cnt()==0){
logs("goto deep sleep cause no tasks.\r\n");
sys_exit_with_wkup(60-2); //wkup for 60s.
}
}
}
此程序框架存在一个缺陷:如果中断回调函数耗时太久,会导致下一个中断触发后不能立即执行对应的回调函数,包括所有的timer都存在这样问题。
附:timer如何分频(prescale)以及输出频率(frequency):
timer frequency=SystemCoreClock/(TIM_Prescaler+1)
output fqcy=(TIM_Prescaler+1)* (TIM_Period+1)/SystemCoreClock