DFU移植方式详解
为方便用户将GR5xx提供的DFU方案应用到自定义的工程中,本章以ble_app_template_freertos示例工程为例(工程位于SDK_Folder\projects\ble\ble_peripheral\ble_app_template_freertos\Keil_5),介绍为该工程移植DFU功能的操作。移植过程主要包括以下步骤。
- DFU初始化
- 添加DFU调度器
- DFU服务初始化(使用低功耗蓝牙通讯方式)及数据接收处理
- 添加DFU组件
- 添加OTA Profile
具体操作步骤如下:
- 初始化DFU,代码位于user_periph_setup.c文件的app_periph_init函数中,如下所示:
#include “dfu_port.h” void app_periph_init(void) { dfu_uart_init(); dfu_uart_ctrl_pin_init(); dfu_port_init(uart_send_data, DFU_FW_SAVE_ADDR, &dfu_pro_call); }
dfu_uart_init()是串口初始化函数,当使用串口通讯方式时,需要初始化串口,以GR551x为例,使用UART1进行数据传输,初始化代码如下所示:
static app_uart_params_t dfu_uart_param; #define DFU_UART_RX_BUFF_SIZE 0x400 #define DFU_UART_TX_BUFF_SIZE 0x400 static uint8_t s_dfu_uart_rx_buffer[DFU_UART_RX_BUFF_SIZE]; static uint8_t s_dfu_uart_tx_buffer[DFU_UART_TX_BUFF_SIZE]; static void dfu_uart_init(void) { app_uart_tx_buf_t uart_buffer; uart_buffer.tx_buf = s_dfu_uart_tx_buffer; uart_buffer.tx_buf_size = DFU_UART_TX_BUFF_SIZE; dfu_uart_param.id = APP_UART1_ID; dfu_uart_param.init.baud_rate = APP_UART_BAUDRATE; dfu_uart_param.init.data_bits = UART_DATABITS_8; dfu_uart_param.init.stop_bits = UART_STOPBITS_1; dfu_uart_param.init.parity = UART_PARITY_NONE; dfu_uart_param.init.hw_flow_ctrl = UART_HWCONTROL_NONE; dfu_uart_param.init.rx_timeout_mode = UART_RECEIVER_TIMEOUT_ENABLE; dfu_uart_param.pin_cfg.rx.type = APP_UART1_RX_IO_TYPE; dfu_uart_param.pin_cfg.rx.pin = APP_UART1_RX_PIN; dfu_uart_param.pin_cfg.rx.mux = APP_UART1_RX_PINMUX; dfu_uart_param.pin_cfg.rx.pull = APP_UART_RX_PULL; dfu_uart_param.pin_cfg.tx.type = APP_UART1_TX_IO_TYPE; dfu_uart_param.pin_cfg.tx.pin = APP_UART1_TX_PIN; dfu_uart_param.pin_cfg.tx.mux = APP_UART1_TX_PINMUX; dfu_uart_param.pin_cfg.tx.pull = APP_UART_TX_PULL; app_uart_init(&dfu_uart_param, dfu_uart_evt_handler, &uart_buffer); app_uart_receive_async(APP_UART1_ID, s_dfu_uart_rx_buffer, sizeof(s_dfu_uart_rx_buffer)); }
在使用UART升级时,除了初始化串口的TX和RX引脚以外,还需初始化一个控制引脚,该控制引脚的作用:唤醒正在睡眠的设备;启动已停止的DFU任务。因此,如果用户的设备采用了睡眠机制,必须选择AON类型的引脚作为控制引脚。以GR551x为例,选择AON_GPIO_PIN_1作为控制引脚,初始化代码如下所示:
#define DFU_UART_CTRL_PIN AON_GPIO_PIN_1 void dfu_uart_ctrl_pin_init(void) { app_io_init_t io_init = APP_IO_DEFAULT_CONFIG; io_init.pull = APP_IO_PULLUP; io_init.mode = APP_IO_MODE_IT_FALLING; io_init.pin = DFU_UART_CTRL_PIN; io_init.mux = APP_IO_MUX; app_io_init(APP_IO_TYPE_AON, &io_init); app_io_event_register_cb(APP_IO_TYPE_AON, &io_init, app_io_event_handler, "DFU uart ctrl pin interrupt"); }
在应用固件中,DFU_FW_SAVE_ADDR表示当进行后台双区升级时,固件拷贝的起始地址,其值设置为:
#define DFU_FW_SAVE_ADDR (FLASH_START_ADDR + 0x60000)
说明:该值不是固定的,设置时请注意不能和App bootloader固件以及Bank0固件的地址冲突。
uart_send_data是串口发送数据函数,使用串口通讯方式升级时,需要注册该接口,该函数定义如下所示:
static void uart_send_data(uint8_t *data, uint16_t size) { app_uart_transmit_async(APP_UART1_ID, data, size); }
dfu_pro_call是升级过程中升级进度打印的回调函数,详细代码如下所示:
static void dfu_program_start_callback(void); static void dfu_programing_callback(uint8_t pro); static void dfu_program_end_callback(uint8_t status); static dfu_pro_callback_t dfu_pro_call = { .dfu_program_start_callback = dfu_program_start_callback, .dfu_programing_callback = dfu_programing_callback, .dfu_program_end_callback = dfu_program_end_callback, }; static void dfu_program_start_callback(void) { APP_LOG_DEBUG("Dfu start program"); } static void dfu_programing_callback(uint8_t pro) { APP_LOG_DEBUG("Dfu programing---%d%%", pro); } static void dfu_program_end_callback(uint8_t status) { APP_LOG_DEBUG("Dfu program end"); if (0x01 == status) { APP_LOG_DEBUG("status: successful"); } else { APP_LOG_DEBUG("status: error"); } }
- 创建DFU信号量及DFU调度任务。
#include “dfu_port.h” #define DFU_TASK_STACK_SIZE ( 1024 * 2 ) TaskHandle_t dfu_task_handle; SemaphoreHandle_t xDfuSemaphore; uint8_t start_dfu_task_flag = 0; int main(void) { …… xDfuSemaphore = xSemaphoreCreateBinary(); xTaskCreate(vStartTasks, "create_task", 512, NULL, 0, NULL); vTaskStartScheduler(); for(;;); } static void vStartTask(void *arg) { …… XTaskCreate(dfu_schedule_task, "dfu_schedule_task", DFU_TASK_STACK_SIZE, NULL, configMAX_PRIORITIES - 2, &dfu_task_handle); …… vTaskDelete(NULL); } static void dfu_schedule_task(void *p_arg) { while (1) { if (!start_dfu_task_flag) { xSemaphoreTake(xDfuSemaphore, portMAX_DELAY); } dfu_schedule(); } }
实际应用中,在主机端与设备端不进行数据交互时,需要暂停DFU任务的运行,以降低功耗。GR5xx提供的方案是为DFU任务添加超时机制及信号量。如上述代码所示,DFU任务创建后,即在等待信号量,获取到信号量后,开始运行DFU任务。DFU信号量在不同通讯方式下释放方式不同,低功耗蓝牙通过Control Point特性下发DFU Enter命令释放信号量;UART通过控制引脚触发外部中断的形式释放信号量。
当设备端超过一定时间没有接收到主机下发的数据时,则判定为当前DFU任务已结束,在超时回调函数里将DFU任务开始标志位复位,停止DFU任务的运行,继续等待DFU信号量。超时机制通过app_timer软件定时器组件实现。在使用app_timer时需要包含#include “app_timer.h”,定时器初始化函数定义如下,该函数需要在app_periph_init()函数内进行调用。
static app_timer_id_t s_cmd_wait_timer_id; static void dfu_timer_init(void) { app_timer_create(&s_cmd_wait_timer_id, ATIMER_REPEAT, cmd_wait_timeout_handler); }
超时回调函数的定义如下所示:
static uint16_t s_dfu_last_count; static uint16_t s_dfu_curr_count; extern uint8_t start_dfu_task_flag; static void cmd_wait_timeout_handler(void* p_arg) { if (s_dfu_last_count < s_dfu_curr_count) { s_dfu_last_count = s_dfu_curr_count; } else { s_dfu_curr_count = 0; s_dfu_last_count = 0; dfu_cmd_parse_state_reset(); fast_dfu_state_machine_reset(); start_dfu_task_flag = 0; dfu_timer_stop(); } }
由上述代码可知,当主机端超过一定时间没有发送数据至设备端后,会触发超时函数,超时函数会调用dfu_cmd_parse_state_reset函数,该函数由SDK内部实现,可直接调用,具体作用是复位DFU命令解析的状态机,以免影响下次升级时数据解析。fast_dfu_state_machine_reset函数也需要在超时函数里进行调用,作用是复位快速模式的状态机,以免影响下次升级时主机端与设备端的数据传输。GR551x SDK V2.0.1和GR5525 SDK V0.8.0的dfu_port.c里目前没有实现fast_dfu_state_machine_reset接口,需要自行实现,实现代码如下所示:
void fast_dfu_state_machine_reset(void) { s_fast_dfu_mode = 0; s_program_end_flag = 0; s_fast_dfu_state = FAST_DFU_INIT_STATE; }
然后将DFU任务开始标志位复位,最后停止DFU软件定时器。软件定时器停止函数定义如下:
static void dfu_timer_stop(void) { app_timer_stop(s_cmd_wait_timer_id); }
- 服务初始化及数据接收处理。当使用低功耗蓝牙通讯方式进行DFU时,在user_app.c文件的service_init函数中初始化DFU相关服务,并添加头文件#include “dfu_port.h” ,如下所示:
static void services_init(void) { dfu_service_init(dfu_enter); }
dfu_enter函数定义默认为空,用户可在函数内部添加DFU任务触发代码,另一方面,为了实现DFU的超时机制,在dfu_enter函数里调用定时器开始函数。当设备端收到DFU Enter指令后,释放DFU信号量、将DFU任务开始标志位置位,开启DFU超时机制的软件定时器。添加的代码如下所示:
extern SemaphoreHandle_t xDfuSemaphore; extern uint8_t start_dfu_task_flag; static void dfu_enter(void) { if (!start_dfu_task_flag) { start_dfu_task_flag = 1; xSemaphoreGive(xDfuSemaphore); dfu_timer_start(); } }
dfu_timer_start的代码实现如下所示:
#define DFU_CMD_WAIT_TIMEOUT 4000 void dfu_timer_start(void) { app_timer_start(s_cmd_wait_timer_id, DFU_CMD_WAIT_TIMEOUT, NULL); }
如果是以UART方式进行通讯,则没有dfu_enter命令,也没有对应的dfu_enter回调函数。UART通讯方式的DFU任务唤醒方法使用在步骤1初始化的控制引脚的中断服务函数里完成,中断服务函数定义如下:
extern SemaphoreHandle_t xDfuSemaphore; extern uint8_t start_dfu_task_flag; void app_io_event_handler(app_io_evt_t *p_evt) { app_io_pin_state_t pin_level = APP_IO_PIN_RESET; if (p_evt->pin == DFU_UART_CTRL_PIN) { pin_level = app_io_read_pin(APP_IO_TYPE_AON, DFU_UART_CTRL_PIN); if (pin_level == APP_IO_PIN_RESET) { do { pin_level = app_io_read_pin(APP_IO_TYPE_AON, DFU_UART_CTRL_PIN); } while(pin_level == APP_IO_PIN_SET); if (!start_dfu_task_flag) { start_dfu_task_flag = 1; xSemaphoreGive(xDfuSemaphore); dfu_timer_start(); } } } }
开启DFU软件定时器后,需要在数据接收位置调用count累加的函数,count累加函数定义如下所示:
void dfu_rev_cmd_count(void) { s_dfu_curr_count++; }
当以低功耗蓝牙通讯方式进行DFU时,在低功耗蓝牙接收数据处调用dfu_rev_cmd_count函数,具体位置为dfu_port.c文件的otas_evt_process(otas_evt_t *p_evt)函数内,dfu的数据接收函数也在此添加,具体代码如下所示:
static void otas_evt_process(otas_evt_t *p_evt) { switch(p_evt->evt_type) { ...... case OTAS_EVT_RX_RECEIVE_DATA: dfu_rev_cmd_count(); if (!s_fast_dfu_mode || s_program_end_flag) { dfu_ble_receive_data_process(p_evt->p_data, p_evt->length); } else { s_fast_dfu_state = FAST_DFU_PROGRAM_FLASH_STATE; fast_dfu_write_data_to_buffer(p_evt->p_data, p_evt->length); } break; ...... } }
当以UART方式进行DFU时,在UART接收数据处调用dfu_rev_cmd_count函数,即在dfu_uart_evt_handler函数内进行调用,dfu的数据接收函数也在此进行添加,具体代码如下所示:
void dfu_uart_evt_handler(app_uart_evt_t * p_evt) { switch(p_evt->evt_type) { ...... case APP_UART_EVT_RX_DATA: dfu_rev_cmd_count(); dfu_uart_receive_data_process(s_dfu_uart_rx_buffer, p_evt->data.size); app_uart_receive_async(APP_UART1_ID, s_dfu_uart_rx_buffer, DFU_UART_RX_BUFF_SIZE); break; ...... } }
说明:在手机端当前连接设备的DFU任务尚处于停止状态时,点击升级按钮前,若需通过 GRToolbox 获取固件信息,可通过写控制点的方式手动下发唤醒DFU任务的命令,具体操作请参考固件升级。
- 在gr_libraries和gr_profiles工程文件夹下分别添加dfu_port.c和otas.c文件,如下图所示。
说明:
dfu_port.c和otas.c文件分别位于SDK_Folder\components\libraries\dfu_port和SDK_Folder\components\profiles\otas。
图 47 Keil工程列表 - 在custom.config.h中设置APP_CODE_LOAD_ADDR和APP_CODE_RUN_ADDR为FLASH_START_ADDR + 0x30000,使其和boot固件不会产生覆盖的情况。因此,以GR551x为例,APP_CODE_RUN_ADDR和APP_CODE_LOAD_ADDR设置为0x01030000。
// <o> Code load address // <i> Default: 0x01002000 #define APP_CODE_LOAD_ADDR 0x01030000 // <o> Code run address // <i> Default: 0x01002000 #define APP_CODE_RUN_ADDR 0x01030000
- 为使App bootloader固件能正确跳转至应用固件,需匹配两者的COMMENTS。App bootloader固件默认COMMENTS为"ble_app_temp",因此在custom.config.h中设置ble_app_template_freertos固件的COMMENTS如下:
#define APP_FW_COMMENTS "ble_app_temp"