持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第26天,点击查看活动详情
本文介绍在 BLE 协议栈下如何读写flash。
介绍
nRF52832 有内部 Flash。官方 SDK 提供两种方式,一种是 FStorage 方式,较简单且底层,另一种是在 FStorage 基础上实现的的 FDS 方式。本文只涉及前者,因为在应用中已经足够了。
FStorage 访问 Flash 是异步的,需要设置回调函数。具备写、读、擦除等操作。本文使用自定义的函数接口对 FStorage 进行封装。
实践
经验
在 BLE 协议栈使能情况下,有可能写Flash会失败,可以加大下面的宏定义:
NRF_FSTORAGE_SD_QUEUE_SIZE
NRF_FSTORAGE_SD_MAX_RETRIES
NRF_FSTORAGE_SD_MAX_WRITE_SIZE
操作 Flash 和蓝牙协议栈工作(如扫描、广播、收发数据)可能会冲突,笔者实践发现,基本上都会冲突,具体解决方法见下文代码。
Flash常识
Flash 需要先擦除才能写成功,擦除是将0改为1,写是将1改为0。写入的内容需要4字节对齐,写地址本身也要4字节对齐。以下函数调用为例:
flash_write(flash_addr, p_str, len)
p_str为数据缓冲区指针,该针对需要4字节对齐,len为要写的数据,需为4的整数倍。
nRF52832 Flash 大小为 512K 字节,分为128页,每页大小为4K,每页分为8个块,每块大小512字节。Flash 的擦除单位为 4KB,官方提供的 API 接口,就是按页擦除的。
经测试,手上板子 flash 信息如下:
00> <info> app: ========| flash info |========
00> <info> app: erase unit: 4096 bytes
00> <info> app: program unit: 4 bytes
00> <info> app: ==============================
对外接口
为配合笔者硬件驱动库,设计如下接口:
- flash_init
- flash_write
- flash_read
- flash_erase
使用者根据不同的应用目的,直接调用上述接口即可,至于 FStorage 的实现细节,已经隐藏了。
代码
官方提供了 Flash 的操作例程,有如下几个:
examples\peripheral\flash_fds
examples\peripheral\flash_fstorage
examples\peripheral\flashwrite
笔者使用 flash_fstorage 例程进行改写。使用 FStorage 时,须使能宏定义NRF_FSTORAGE_ENABLED
。实现代码如下:
#if NRF_FSTORAGE_ENABLED == 1
static void fstorage_evt_handler(nrf_fstorage_evt_t * p_evt);
NRF_FSTORAGE_DEF(nrf_fstorage_t fstorage) =
{
/* Set a handler for fstorage events. */
.evt_handler = fstorage_evt_handler,
/* These below are the boundaries of the flash space assigned to this instance of fstorage.
* You must set these manually, even at runtime, before nrf_fstorage_init() is called.
* The function nrf5_flash_end_addr_get() can be used to retrieve the last address on the
* last page of flash available to write data. */
.start_addr = 0x3e000,
.end_addr = 0x3ffff,
};
/**@brief Helper function to obtain the last address on the last page of the on-chip flash that
* can be used to write user data.
*/
static uint32_t nrf5_flash_end_addr_get()
{
uint32_t const bootloader_addr = BOOTLOADER_ADDRESS;
uint32_t const page_sz = NRF_FICR->CODEPAGESIZE;
uint32_t const code_sz = NRF_FICR->CODESIZE;
return (bootloader_addr != 0xFFFFFFFF ?
bootloader_addr : (code_sz * page_sz));
}
static void fstorage_evt_handler(nrf_fstorage_evt_t * p_evt)
{
if (p_evt->result != NRF_SUCCESS)
{
NRF_LOG_INFO("--> Event received: ERROR while executing an fstorage operation.");
return;
}
switch (p_evt->id)
{
default:
break;
}
}
// static void print_flash_info(nrf_fstorage_t * p_fstorage)
// {
// NRF_LOG_INFO("========| flash info |========");
// NRF_LOG_INFO("erase unit: \t%d bytes", p_fstorage->p_flash_info->erase_unit);
// NRF_LOG_INFO("program unit: \t%d bytes", p_fstorage->p_flash_info->program_unit);
// NRF_LOG_INFO("==============================");
// }
void wait_for_flash_ready(nrf_fstorage_t const * p_fstorage)
{
/* While fstorage is busy, sleep and wait for an event. */
while (nrf_fstorage_is_busy(p_fstorage))
{
(void) sd_app_evt_wait();
}
}
int flash_init(void)
{
ret_code_t ret = NRF_SUCCESS;
nrf_fstorage_api_t * p_fs_api;
p_fs_api = &nrf_fstorage_sd;
ret = nrf_fstorage_init(&fstorage, p_fs_api, NULL);
if (ret != NRF_SUCCESS)
{
return ret;
}
// print_flash_info(&fstorage);
return ret;
}
static uint32_t round_up_u32(uint32_t len)
{
if (len % sizeof(uint32_t))
{
return (len + sizeof(uint32_t) - (len % sizeof(uint32_t)));
}
return len;
}
int flash_write(uint32_t addr, uint8_t* data, uint32_t len)
{
ret_code_t ret = NRF_SUCCESS;
// 保证4字节对齐
uint32_t len_w = round_up_u32(len);
ret = nrf_fstorage_write(&fstorage, addr, data, len_w, NULL);
if (ret != NRF_SUCCESS)
{
return ret;
}
wait_for_flash_ready(&fstorage);
NRF_LOG_INFO(" Write flash Done.");
return ret;
}
int flash_read(uint32_t addr, uint8_t* data, uint32_t len)
{
return nrf_fstorage_read(&fstorage, addr, data, len);
}
int flash_erase(uint32_t addr, uint32_t pages_cnt)
{
return nrf_fstorage_erase(&fstorage, addr, pages_cnt, NULL);
}
#endif
下面给出用法示例的主要代码:
// 连接参数等
typedef struct {
uint16_t status; // 0xaa55 表示数值有效
uint16_t conn_min_ms;
uint16_t conn_max_ms;
uint16_t conn_latency;
uint16_t conn_sup_timeout;
uint16_t padding;
} app_ble_param_t;
static uint8_t s_flash_write = 0; // 写 flash 标志
static app_ble_param_t s_ble_param = {0};
static void dts_data_handler(ble_dts_evt_t * p_evt)
{
if (p_evt->type != BLE_DTS_EVT_TX_RDY)
NRF_LOG_INFO("got dts event %d", p_evt->type);
// 收到主机发来的NUS数据
if (p_evt->type == BLE_DTS_EVT_RX_DATA)
{
dev_led_set(LED_3, OFF);
// uint32_t err_code;
const uint8_t* p_data = p_evt->params.rx_data.p_data;
uint16_t length = p_evt->params.rx_data.length;
NRF_LOG_INFO("Received data from BLE NUS, cnt: %d p_data[0]: 0x%x", length, p_data[0]);
if (p_data[0] != 0xAA)
{
return;
}
/* 简单协议 0xaa开头
支持的:
aa 01 <发送次数> <延时毫秒数> -- 适用于button3一次发送的参数
aa 02 55 aa <最小> <最大> 00 00 <超时> -- 示例:最小最大为7.5ms,超时为4s:aa 02 55 aa 06 00 06 00 00 00 90 01
// more...
*/
// NRF_LOG_INFO("p_data: %x len: %d", p_data, length);
NRF_LOG_HEXDUMP_INFO(p_data, length);
app_ble_param_t ble_param = {0};
// 直接用结构体转,发来的数据必须和结构体一一对应
ble_param = *(app_ble_param_t*)(p_data+2);
NRF_LOG_INFO("%d %d %d %d pstr: 0x%x",
ble_param.conn_min_ms, ble_param.conn_max_ms,
ble_param.conn_latency, ble_param.conn_sup_timeout, &ble_param);
memcpy(&s_ble_param, &ble_param, sizeof(app_ble_param_t));
s_flash_write = 1; // 经测试,不能在回调函数中写flash 作标志
}
}
int main(void)
{
// 服务初始化...
....
// 读参数
flash_init();
ret_code_t ret;
// app_ble_param_t ble_param = {0};
memset(&s_ble_param, '\0', sizeof(app_ble_param_t));
ret = flash_read(FLASH_START, (uint8_t*)&s_ble_param, sizeof(app_ble_param_t));
for (;;)
{
// 这是临时的方法
// 不能在BLE事件中写flash,作标志,在主循环中做
// 猜测:BLE事件为回调函数,此时未完成BLE事件,故无法写flash,待退出事件结束后方可
if (s_flash_write)
{
NRF_LOG_INFO("write flash...");
s_flash_write = 0;
flash_erase(FLASH_START, 1);
flash_write(FLASH_START, (uint8_t*)&s_ble_param, sizeof(app_ble_param_t));
}
idle_state_handle();
}
}
在主函数中初始化 Flash,再读 flash,在主循环中判断写 Flash 标志 s_flash_write,有则先擦除再写。代码中的 dts_data_handler 函数为接收蓝牙主机数据的回调函数,当接收到数据时,设置标志变量 s_flash_write。
小结
关于 BLE 事件和 Flash 可能冲突的问题,SDK 中有提到,但还是测试了一下,经测试,如果在BLE事件中写flash,就会死机。因此设置标志,在主循环中再写 Flash。个人认为,BLE事件实际是回调函数,在回调函数中还未完成BLE事件,故无法写flash,须退出事件,结束后方可。
对于产品设计,可以在成功写入flash后,闪烁一下某个LED,方便观察状态。