背景
在一个硬件系统里面,像蜂鸣器,LED指示灯,震动马达等,如果每一种提示信号都写一个单独的逻辑或者创建一个任务,那有些代码功能就会重复,对于一个MCU,RAM不太大的情况就需要占更多的空间。
而且有的地方,会用到不同的提示元器件,且逻辑不太相同,如果针对每一种场景,单独写逻辑的话,这样方式来实现提示,可能新一加个逻辑就需要改动较多的代码,且内部的判断逻辑会容易混淆。
如果碰到不同任务去使用的时候,且提示信号还有优先级要考虑的话,代码判断逻辑就又要多一层了。如果需要对提示元器件的使能进行控制,代码分散在各个地方也是一个比较麻烦的事情。
像蜂鸣器这类提示元件的工作方式都挺类似,像持续ON,响几次(周期性动作),响几次结束,长周期提示,这些动作可以覆盖基本的提示方式,当然一个独特的方式还是需要单独使用一些代码来实现,也可以在具体的响应中,去调用统一的提示信号的操作,去实现想要的效果。
抽象
工作类型
一般来说,可以将提示信号的状态,分为无,连续,顺序,周期
无,即没有动作,或者说是关闭的状态
连续的,主要指的是持续地开启,蜂鸣器一直响
顺序的,响几声就关闭,类似于开,关,开,关
周期的,以一种顺序,一直按照设置往复运动,直到接收到其他指令
typedef enum{PTYPE_NONE = 0,PTYPE_CONTINUOUS,PTYPE_DURATION_SEQ,PTYPE_PERIODIC_BURST,PYTPE_MAX
}pattern_type_t;typedef struct pattern_t{pattern_type_t type;// for SEQconst uint32_t *durations_ms; // point to array [on, off, on ,off]uint8_t n_durations;uint16_t repeats; // 0 => forever// for PERIODIC_BURSTuint16_t burst_conut;uint32_t burst_on_ms;uint32_t burst_off_ms;uint32_t period_ms;//priority hintuint8_t default_prioriry;
}pattern_t;
定义提示信号的类型及对每种信号的工作方式抽象到pattern_t这个结构体中,pattern_t中的成员包含所有情况,但针对每一种模式,只使用相关的成员变量即可,不需要使用全部成员变量
定义示例
//连续工作
static const pattern_t PAT_BUZZER_CONTINUOUS = {.type = PTYPE_CONTINUOUS,.default_prioriry = 3,
};//顺序工作,开关2次,开,关,开,关,间隔200ms
static const uint32_t buzzer_once_seq[] = {200, 200};
static pattern_t pat_buzzer_twinkle_freq = {.type = PTYPE_DURATION_SEQ,.durations_ms = buzzer_twinkle_freq_seq,.n_durations = 2,.repeats = 2,.default_prioriry = 3,
};//周期工作 on off ... on period,最后一个周期只有ON,没有OFF
static const pattern_t PAT_BUZZER_PERIODIC = {.type = PTYPE_PERIODIC_BURST,.burst_conut = 3,.burst_on_ms = 100,.burst_off_ms = 80,.period_ms = 2000,.default_prioriry = 3,
};
操作类型
提示操作类型,主要可分为,运行定义的提示操作,停止设备(当前周期运行完成),强行停止设备,修改设备的开关状态标志
应用任务中,在需要的时候,给提示管理器,发送相应的操作命令,来改变提示器的提醒状态,如果同优级则覆盖当前的操作,否则将被忽略。
typedef enum{CMD_PLAY_PATTERN = 0,CMD_STOP_DEVICE,CMD_FORCE_STOP_DEVICE,
}cmd_type_t;
每次调用周期先判断是否有相关的指令接收,如果有指令就对接令进行处理,转换当前的提示设备状态,或者停止当前的提示状态
根据时间片轮询更新提示设备的状态,遍历每个设备的相关状态
实例代码
这里是使用FreeRTOS实现,裸机也是可以,只要定时轮咨整个提示信号状态就可以
定义相关参数类型
typedef enum{DEV_BUZZER = 0,DEV_LED_RED,DEV_LED_GREEN,DEV_VIBRATOR,DEV_MAX
}device_id_t;typedef struct notifier_req_t{cmd_type_t type;device_id_t dev;const pattern_t *pattern;uint8_t priority;
}notifier_req_t;typedef struct device_state_t{uint8_t active;const pattern_t *pat;uint8_t prioriry;//runtime for DURATION_SEQuint8_t seg_idx;uint32_t seg_remaining;uint16_t repeats_left;uint8_t output_on;//runtime for PERIODIC_BURSTuint16_t burst_remaining;uint32_t burst_segment_timer;uint32_t period_timer;uint8_t stop_pending;
}device_state_t;
关联设备控制
使用分配函数指针的形式,将提示信号统一管理与实际设备控制进行相互关联
typedef void (*device_control_fn_t)(uint8_t);
static device_control_fn_t dev_control[DEV_MAX] = {0};
static void device_control_init(void)
{for(int i = 0; i < DEV_MAX; i++)dev_control[i] = NULL;dev_control[DEV_BUZZER] = app_buzzer_dev_control;dev_control[DEV_LED_RED] = app_led_red_control;dev_control[DEV_LED_GREEN] = app_led_green_control;
}
命令操作响应
根据重新接收到的信号去初始化,或者调整当前的设备状态,在下一轮设备状态轮询去改变设备状态
static void notifier_cmd_response(void)
{notifier_req_t req;device_id_t dev;while (xQueueReceive(notifier_queue, &req, 0) == pdTRUE){switch (req.type){case CMD_PLAY_PATTERN:if(req.dev > DEV_MAX)return;if(notify_priority_override(&dev_state[req.dev], req.priority)){device_start_pattern(&dev_state[req.dev], req.pattern, req.priority);device_control(req.dev, dev_state[req.dev].output_on);}break;case CMD_STOP_DEVICE:dev = req.dev;if(dev < DEV_MAX)dev_state[dev].stop_pending = 1; break;case CMD_FORCE_STOP_DEVICE:dev = req.dev;if(dev < DEV_MAX){memset(&dev_state[dev], 0, sizeof(dev_state[dev]));device_control(req.dev, dev_state[req.dev].output_on);}break;default:break;}}
}
提示信号动作初始化
根据当前的信号指令,初始化相关提示逻辑跳转的初始状态
static void device_start_pattern(device_state_t *st, const pattern_t *pat, uint8_t prio)
{memset(st, 0, sizeof(*st));st->active = 1;st->pat = pat;st->prioriry = prio ? prio : pat->default_prioriry;st->output_on = 0;st->stop_pending = 0;if(pat->type == PTYPE_CONTINUOUS){st->output_on = 1;st->seg_remaining = 0xFFFFFFFFu;}else if(pat->type == PTYPE_DURATION_SEQ){st->seg_idx = 0;st->output_on = 1;st->seg_remaining = pat->durations_ms[0];st->repeats_left = (pat->repeats == 0) ? 0xFFFFu : pat->repeats;}else if(pat->type == PTYPE_PERIODIC_BURST){st->burst_remaining = pat->burst_conut;st->burst_segment_timer = pat->burst_on_ms;st->output_on = 1;st->period_timer = pat->period_ms;}
}
整体任务及各设备根据当前类型进行逻辑判断及跳转
static void notifier_task(void *pParameter)
{TickType_t last_wake = xTaskGetTickCount();const TickType_t tick_ticks = pdMS_TO_TICKS(NOTIFIER_TICK_MS);while(1){notifier_cmd_response();for(device_id_t d = 0; d < DEV_MAX; d++){device_state_t *p_dev_state = &dev_state[d];if((!p_dev_state->active) || (p_dev_state->pat == NULL))continue;switch(p_dev_state->pat->type){case PTYPE_CONTINUOUS:if(p_dev_state->stop_pending)device_pending_stop(d);else{p_dev_state->output_on = 1;device_control(d, p_dev_state->output_on);}break;case PTYPE_DURATION_SEQ:if(p_dev_state->seg_remaining >= (uint32_t)NOTIFIER_TICK_MS)p_dev_state->seg_remaining -= NOTIFIER_TICK_MS;else{p_dev_state->seg_idx++;if(p_dev_state->seg_idx < p_dev_state->pat->n_durations){p_dev_state->output_on = p_dev_state->output_on ^ 0x01;p_dev_state->seg_remaining = p_dev_state->pat->durations_ms[p_dev_state->seg_idx];;device_control(d, p_dev_state->output_on);}else{if(p_dev_state->stop_pending)device_pending_stop(d);else if(p_dev_state->repeats_left == 0xFFFFu){p_dev_state->seg_idx = 0;p_dev_state->seg_remaining = p_dev_state->pat->durations_ms[0];p_dev_state->output_on = 1;device_control(d, p_dev_state->output_on);}else if(p_dev_state->repeats_left > 1){p_dev_state->repeats_left--;p_dev_state->seg_idx = 0;p_dev_state->seg_remaining = p_dev_state->pat->durations_ms[0];p_dev_state->output_on = 1;device_control(d, p_dev_state->output_on);}else{p_dev_state->active = 0;p_dev_state->pat = NULL;p_dev_state->output_on = 0;device_control(d, p_dev_state->output_on);}}}break;case PTYPE_PERIODIC_BURST:if(p_dev_state->output_on){if(p_dev_state->burst_segment_timer >= (uint32_t)NOTIFIER_TICK_MS)p_dev_state->burst_segment_timer -= NOTIFIER_TICK_MS;else{if(p_dev_state->burst_remaining > 1){p_dev_state->burst_remaining--;p_dev_state->output_on = 0;p_dev_state->burst_segment_timer = p_dev_state->pat->burst_off_ms;device_control(d, p_dev_state->output_on);}else{p_dev_state->burst_remaining = 0;p_dev_state->output_on = 0;p_dev_state->period_timer = p_dev_state->pat->period_ms;device_control(d, p_dev_state->output_on);}}}else{if(p_dev_state->burst_remaining > 0){if(p_dev_state->burst_segment_timer >= (uint32_t)NOTIFIER_TICK_MS)p_dev_state->burst_segment_timer -= NOTIFIER_TICK_MS;else{p_dev_state->output_on = 1;p_dev_state->burst_segment_timer = p_dev_state->pat->burst_on_ms;device_control(d, p_dev_state->output_on);}}else{if(p_dev_state->period_timer >= (uint32_t)NOTIFIER_TICK_MS)p_dev_state->period_timer -= NOTIFIER_TICK_MS;else{if(p_dev_state->stop_pending)device_pending_stop(d);else{//periodic restartp_dev_state->burst_remaining = p_dev_state->pat->burst_conut;p_dev_state->output_on = 1;p_dev_state->burst_segment_timer = p_dev_state->pat->burst_on_ms;device_control(d, p_dev_state->output_on);}}}}break;default:break;}}vTaskDelayUntil(&last_wake, tick_ticks);}
}
发送指令
void app_notification_send_cmd(device_id_t dev, const pattern_t *pattern, uint8_t priority)
{if(dev >= DEV_MAX || pattern == NULL)return;notifier_req_t req;req.type = CMD_PLAY_PATTERN;req.dev = dev;req.pattern = pattern;req.priority = priority;xQueueSend(notifier_queue, &req, 10);
}//示例,蜂鸣响一次
app_notification_send_cmd(DEV_BUZZER, &PAT_BUZZER_ONCE, 2);
优先级
每当新接收提示信号指令,先判定接收信号的优先级,如果高于或等于当前提示操作,就执行新的提示操作,否则忽略。
这样可以很好地,执行优先较高的提示信号,且应用时的代码不需要对提示信号做相应的判断调整修改,只要重要的任务执行高优先的提示信号就可以进行切换
提示的时候比较好地区分优先级,不同单独再去考虑是否打断的问题,省去新的判定逻辑
static uint8_t notify_priority_override(device_state_t *cur_st, uint8_t new_prio)
{if(!cur_st->active)return 1;return new_prio >= cur_st->prioriry;
}
DURATION_SEQ , PERIODIC两种机制
PERIODIC循环机制,其实用SEQ,将repeats设置为0,也可以达到一直循环的效果,从实现的角度来说,可以把PERIODIC机制并入到DURATION_SEQ模式中
但两种的语义并不同
DURATION_SEQ 按键一次顺序完成一系列动作
PERIODIC 间隔多长时间做一系列操作
保留两种实现方式比较好,多一种模式可以让使用的时候的接口,有多一个选择,且更好理解一些。
还有一点不同,等单个小循环结束再停止的机制,有一点细微的差别
总结
所有设备操作都在一个任务里面,不会因为不同任务,导致IO的控制的失控,且相对节省一定的任务空间
添加新的设备可以常规的功能,可以直接复用当前提示信号的管理代码,不需要重新添加逻辑
有这样的统一管理机制,为提示信号控制提供很大的帮助与便利。