此文有重大BUG,稍後有空了更新 -_-||
12月20日已修復所有已知問題。
一般的情況下,低功耗藍牙芯片由於要保持低功耗工作,本身會提供一種輕度休眠模式,在這種模式下保持內核時鐘繼續運行以定期進行廣播或交換連接數據。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<valid_timer_size-1) {
memmove(&timer_list[cur_remove_index],&timer_list[cur_remove_index+1],sizeof(TIMER_DEF)*(valid_timer_size-(cur_remove_index+1)));
valid_timer_size--;
}
}
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_size<TIMER_MAX_SIZE) {
uint32_t index=0;
if(valid_timer_size>0) {
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<valid_timer_size) {
if(timer_list[m_timer_index].id==timer_id) {
fix_timer_list(m_timer_index); //清理掉無效的timer
return;
}
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<irq_sz;i++){
if(irq_flag&irq_list[i].irq_id){
clear_irq_flag(irq_list[i].irq_id);
if(p_list_index!=NULL){
*p_list_index=i;
}
return irq_list[i].irq_id;
}
}
return IRQ_NONE;
}
void check_irq_action(void){
static uint8_t m_list_index;
while((get_irq_flag(&m_list_index))!=IRQ_NONE){//輪詢中斷標記,執行對應的回調函數,並清除中斷標記
irq_list[m_list_index].p_handler();
}
}
void irq_handler_init(void){
irq_flag=0;
}
然後,在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