刚接触的时候,碰到了一个问题,就是我们的某个蓝牙接口允许用户写入一些配置,然后我会将这些配置写到flash中保存起来。很简单的操作结果出错了,fds操作没有任何回调,问题在哪儿呢?
我以为是fds的操作有问题,写了测试代码,结果测试代码跑的好好的,有回调!问题很明显,必然是执行的上下文环境有问题。
前边研究fds的源码中有提到,使用fds,初始化操作的时候,要注册一个回调:(void)fds_register(fds_evt_handler);这个注册回调的函数(fds_init)是在放在main函数中执行的,和main函数跑在同一个线程中。意思就是,fds的任何操作,其操作结果最终都会发送到main函数所在的线程。详细点就是,main线程注册的回调,main线程发起的fds操作,fds发起异步线程来执行真实的flash操作,操作结果再发送回main线程。
当fds的操作放在ble事件的回调中的时候,情况不一样了。我没有在详细的文档中发现nordic有提到其ble中断事件,其回调是执行在异步线程中的,但是确实是这样。所以当我们在蓝牙profile的写回调中执行fds操作的时候,是这么一个过程:main线程注册的回调,异步线程A(蓝牙profile写操作回调)发起fds操作,fds再发起异步线程B来执行真实的操作,那么B操作完了,结果应该发给谁呢?这里边具体的细节我后续会在fds的源码中再解读,这里的话:结果是没有发给main线程,注册的回调收不到这个消息。
为了解决这个问题,我们应该引入APP_SCHEDULER模块。这是一个调度模块,其主要作用是将某些不适合当前环境(比如中断)的操作发送到主线程中去执行。
来看看怎么使用,惯例初始化操作:
/**@brief Function for the Event Scheduler initialization.
*/
static void scheduler_init(void)
{
APP_SCHED_INIT(MAX_EVT_SIZE(APP_TIMER_SCHED_EVENT_DATA_SIZE,MAX_EVT_SIZE(PERIPHERAL_MAX_EVT_SIZE,CENTRAL_MAX_EVT_SIZE)), SCHED_QUEUE_SIZE);
}
这里需要提供一个最大的事件长度,方便分配空间。需要注意的是,app_timer也引用了app_scheduler,这里计算最大的事件长度,也需要包含进来。我们的应用中,既使用到了蓝牙主机模式,也使用到了蓝牙从机模式,所以这里两个都要看。
然后,在main loop中,需要检查是否有调度任务:
// Enter main loop.
for (;;)
{
#if (MI_BLE_ENABLE)
mible_tasks_exec();
#endif
app_sched_execute();
idle_state_handle();
}
还是很简单的,到了这里,就可以开始使用app_scheduler了。以修改温度单位为例,app端发过来需要设置的温度单位,摄氏度或者华氏度,app_scheduler将单位和真实的处理函数(将单位写到flash中保存的函数)保存起来,发送到main线程,在main loop中执行。
来看看app_scheduler将事件放入队列的函数是怎么定义的?
/**@brief Function for scheduling an event.
*
* @details Puts an event into the event queue.
*
* @param[in] p_event_data Pointer to event data to be scheduled.
* @param[in] event_size Size of event data to be scheduled.
* @param[in] handler Event handler to receive the event.
*
* @return NRF_SUCCESS on success, otherwise an error code.
*/
uint32_t app_sched_event_put(void const * p_event_data,
uint16_t event_size,
app_sched_event_handler_t handler);
这里的event data 就是事件的接口,既实际执行的是什么事件。那么首先定义一个事件接口,这里的话,就是fds写flash的函数指针和对应的参数:
typedef bool(*evt_update_unit_handler_t)(TEMP_UNITS unit);
typedef struct{
evt_update_unit_handler_t m_handler;
TEMP_UNITS temp_unit;
}update_unit_evt_data;
这里的话,fds用来更新单位的函数为:bool update_unit(TEMP_UNITS unit)。evt_update_unit_handler_t 即为指向该函数的指针,然后参数是temp_unit。
然后第三个参数,app_sched_event_handler_t,我理解为要怎么执行前边定义的事件,通常的做法是直接以temp_unit为参数执行m_handler。
static void update_unit_evt_get(void * p_event_data, uint16_t event_size)
{
update_unit_evt_data * p_update_unit_event = (update_unit_evt_data *)p_event_data;
APP_ERROR_CHECK_BOOL(event_size == sizeof(update_unit_evt_data));
p_update_unit_event->m_handler(p_update_unit_event->temp_unit);
}
这里做了一个检查,防止类型转换产生错误。
现在,我们可以发起调度了:
uint32_t evt_schedule_update_unit(evt_update_unit_handler_t update_handler,TEMP_UNITS unit){
update_unit_evt_data evt_handler;
evt_handler.m_handler=update_handler;
evt_handler.temp_unit=unit;
return app_sched_event_put(&evt_handler, sizeof(evt_handler), update_unit_evt_get);
}
直接在蓝牙读写中断中执行函数 evt_schedule_update_unit即可。
在APP_SCHED_INIT的时候,需要传入一个最大的事件长度,这里定义了多少个这样的evt,就需要计算多少个。我是这样定义的,每新增一个事件,就需要计算一次:
#define MAX_EVT_SIZE(a,b) (a>b?a:b)
#define PERIPHERAL_MAX_EVT_SIZE MAX_EVT_SIZE(sizeof(update_unit_evt_data),\
MAX_EVT_SIZE(sizeof(update_profile_series_data),\
MAX_EVT_SIZE(sizeof(update_profile_series_by_leftcnt_data),\
MAX_EVT_SIZE(sizeof(update_algorithm_flag_data),\
MAX_EVT_SIZE(sizeof(update_account_data),\
MAX_EVT_SIZE(sizeof(update_profile_id_data),\
sizeof(remove_detail_items_data)\
))))))