Hi,各位小夥伴,DIY 活動已經來到了第三周!前兩周的任務大家都完成了嗎?本周將會迎來新的挑戰——文件系統。本文將從 SPI Flash 和 SD Card 兩方面給大家講解如何使用文件系統,以及針對本次 DIY 做出的一些優化,會大大增強系統性能,一起來看看吧~
我們來看一下第三周的任務:
了解 RT-Thread 文件系統,在接收節點中使用文件系統,存放來自發送節點發送過來的數據。
上述任務比較單一,只是文件系統而已。不過,能巧妙靈活的把文件系統用好用對,可不是一件輕鬆的事情。
DFS 是 RT-Thread 提供的虛擬文件系統組件,全稱為 Device File System,即設備虛擬文件系統,文件系統的名稱使用類似 UNIX 文件、文件夾的風格。
RT-Thread DFS 組件的主要功能特點有:
為應用程式提供統一的 POSIX 文件和目錄操作接口:read、write、poll/select 等。
支持多種類型的文件系統,如 FatFS、RomFS、DevFS 等,並提供普通文件、設備文件、網絡文件描述符的管理。
支持多種類型的存儲設備,如 SD Card、SPI Flash、Nand Flash 等。
DFS 的層次架構如下圖所示,主要分為 POSIX 接口層、虛擬文件系統層和設備抽象層。如下圖:
DFS 的更多內容,請在 RT-Thread 文檔中心中查看:https://www.rt-thread.org/document/site/ (由於微信無法插入外部連結,請將以上連結複製至外部瀏覽器打開)以正點原子的潘多拉開發板 (Iot Board) 為例,教大家在 SPI Flash 上使用文件系統。
值得一提的是,RT-Thread 已經將 libc 那套文件系統接口對接到 DSF 上了,在 env 工具中開啟 libc 和 DFS 即可,本次教程使用 libc 的那套接口進行文件的打開/關閉、讀取/寫入。
在 menuconfig 中開啟 libc:
1RT-Thread Components --->
2 POSIX layer and C standard library --->
3 [*] Enable libc APIs from toolchain
1RT-Thread Components --->
2 Device virtual file system --->
3 [*] Using device virtual file system
4 [*] Enable elm-chan fatfs
當然,不要忘記在 meunconfig 中開啟 SPI Flash:
1Hardware Drivers Config --->
2 Onboard Peripheral Drivers --->
3 [*] Enable QSPI FLASH (W25Q128 qspi1)
潘多拉開發板上的 SPI Flash 使用的是 QSPI 接口,還需要在 meunconfig 中把 QSPI 接口開啟:
1Hardware Drivers Config --->
2 On-chip Peripheral Drivers --->
3 [*] Enable QSPI BUS
退出 menuconfig 後需要輸入 「scons --target=mdk5」 更新工程。
本次 DIY 使用的文件系統是 elmfatfs,elmfatfs 需要在塊設備上才能進行文件操作。潘多拉板子上的 SPI Flash 是 W25Q128,我們需要將 W25Q128 註冊成塊設備,才能使用 elmfatfs 進行文件操作。如下示例代碼:
1static int rt_hw_qspi_flash_with_sfud_init(void)
2{
3 stm32_qspi_bus_attach_device("qspi1", "qspi10", RT_NULL, 4, w25qxx_enter_qspi_mode, RT_NULL);
4 if (RT_NULL == rt_sfud_flash_probe("W25Q128", "qspi10"))
5 return -RT_ERROR;
6 return RT_EOK;
7}
8INIT_COMPONENT_EXPORT(rt_hw_qspi_flash_with_sfud_init);
在 FinSH 中輸入 「list_decive」,即可看到 W25Q128 註冊成了塊設備了,並掛載在 QSPI 上:
W25Q128 註冊成了塊設備後,就能將 elmfatfs 這個文件系統掛載到 RT-Thread 的 DFS 上了,如下示例代碼:
1dfs_mount("W25Q128", "/", "elm", 0, 0)
到此為止,我們就可以使用 libc 的接口進行文件操作了,將接收到的數據以文件方式存放到 W25Q128 裡面去,舉個簡單的例子,如下示例代碼:
1FILE *recvdata_p0;
2recvdata_p0 = fopen("recvdata_p0.csv", "a+");
3if (recvdata_p0 != RT_NULL)
4{
5 fputs((char *)RxBuf_P0, recvdata_p0);
6 fputs("\n", recvdata_p0);
7 fclose(recvdata_p0);
8}
在 Finsh 中輸入 「ls」 可以查看當前文件系統中的文件目錄,如下圖:
輸入 「cat XXX」 可以查看文件內容,如下圖:
簡單的幾步就可以進行文件操作了,RT-Thread 的文件系統還是相當易用的。
以正點原子的潘多拉開發板 (Iot Board) 為例,教大家在 SD Card 上使用文件系統。
和上面的 SPI Flash 一樣,在 menuconfig 中開啟相關選項:SD Card,SPI(潘多拉板子的 SD 卡是用 SPI 驅動的而不是 SDIO),libc,DFS,elmfatfs。
4.2 文件系統的掛載與 SPI Flash 一樣,需要將 SD Card 註冊成塊設備,才能掛載文件系統。如下示例代碼:
1static int rt_hw_spi1_tfcard(void)
2{
3 __HAL_RCC_GPIOC_CLK_ENABLE();
4 rt_hw_spi_device_attach("spi1", "spi10", GPIOC, GPIO_PIN_3);
5 return msd_init("sd0", "spi10");
6}
7INIT_DEVICE_EXPORT(rt_hw_spi1_tfcard);
在 FinSH 中輸入 「list_decive」,即可看到 SD Card 註冊成了塊設備了,並掛載在 SPI 上:
SD Card 註冊成了塊設備後,就能將 elmfatfs 這個文件系統掛載到 RT-Thread 的 DFS 上了,如下示例代碼:
1dfs_mount("sd0", "/", "elm", 0, 0)
與 SPI Flash 一樣,可以直接使用 libc 的接口進行文件操作,如下示例代碼:
1FILE *recvdata_p0;
2recvdata_p0 = fopen("recvdata_p0.csv", "a+");
3if (recvdata_p0 != RT_NULL)
4{
5 fputs((char *)RxBuf_P0, recvdata_p0);
6 fputs("\n", recvdata_p0);
7 fclose(recvdata_p0);
8}
將文件系統用起來,進行文件操作,是一件相對比較容易的事情。不過當將文件系統運用到實際項目中的時候,往往會因為一些需求或者說是其他因素,導致事情不那麼好辦。就拿這個 DIY 來說,如果就像上面的示例代碼這麼用文件系統,雖然系統能正常工作,但是會帶來一些問題:
眾所周知,文件的操作是需要佔用大量時間和資源的,通俗來說就是慢,像文件的讀,寫,打開,創建等,都是比較慢的。如果發送節點發數據過來,接收節點每收到一條數據,就用文件系統記錄這個數據,這樣會導致系統性能下降。如何保證減少文件操作次數,提高系統性能,又能保證每條數據都不丟失呢?這裡使用 ringbuffer 來避免這個問題。
ringbuffer 是一種先進先出的 FIFO 環形緩衝區,DIY 的接收節點工程中,我們創建了兩個線程去工作,一個是 nrf24l01_thread 線程,用於接收來自發送節點的數據,另一個是 DFS_thread 線程,用於利用文件系統保存數據的。並且創建一個 4KB 大小的一個 ringbuffer:
1static struct rt_ringbuffer *recvdatabuf;
2recvdatabuf = rt_ringbuffer_create(4069);
每當 nrf24l01_thread 線程接收到一條數據,就存放到 ringbuffer 中去:
1rt_ringbuffer_put(recvdatabuf, (rt_uint8_t *)str_data, strlen(str_data));
在DFS_thread 線程中,我們設置一個 ringbuffer 的閾值,這裡我將閾值設置成了 ringbuffer 大小的一半,當寫入的數據達到了 ringbuffer 的閾值之後,就將 ringbuffer 中所有的數據統統寫入文件中去:
1
2if (rt_ringbuffer_data_len(recvdatabuf) > (4096 / 2))
3{
4
5 recvdatafile_p0 = fopen("recvdata_p0.csv", "ab+");
6 if (recvdatafile_p0 != RT_NULL)
7 {
8 while(rt_ringbuffer_data_len(recvdatabuf))
9 {
10 size = rt_ringbuffer_get(recvdatabuf, (rt_uint8_t *)writebuffer, (4096 / 2));
11 fwrite(writebuffer, 1, size, recvdatafile_p0);
12 }
13 fclose(recvdatafile_p0);
14 }
15}
這麼做,就可以儘可能的減少了文件的操作,提高了系統的性能,同時又保證每一條數據都不會丟失。
但是,還有一個問題:
如果發送節點很久很久才發數據過來,或者說是接收節點很久很久才收到數據,那麼 ringbuffer 要很久很久才能到閾值。如果這時候,已經寫了整整一天的數據進 ringbuffer 中了,只差一點點就要到閾值了,很快就可以將數據寫入到文件中去了,這時候偏偏斷電了!整整一天的數據白白丟失了,心痛嗎?當然,掉電丟數據這種情況是不可以避免的,但是我們可以通過一些算法優化(姑且叫它算法吧),儘可能的減少丟失數據的可能。
解決思路是:定個固定時間,計時,如果時間一到,此時數據還沒寫滿 ringbuffer 的閾值,這時候就不管數據到沒到閾值了,直接將 ringbuffer 裡的數據全部寫入文件中去。要實現這個思路需要搭配事件集 (event) 使用。
在 nrf24l01_thread 線程中,每收到一個數據,就發送一個事件:
1while (1)
2{
3 if (!rx_pipe_num_choose())
4 {
5
6 if(sscanf((char *)RxBuf_P0, "%d,+%f", &buf.timestamp, &buf.temperature) != 2)
7 {
8
9 if(sscanf((char *)RxBuf_P0, "%d,-%f", &buf.timestamp, &buf.temperature) != 2)
10 {
11 continue;
12 }
13 buf.temperature = -buf.temperature;
14 }
15 sprintf(str_data, "%d,%f\n", buf.timestamp, buf.temperature);
16
17 rt_ringbuffer_put(recvdatabuf, (rt_uint8_t *)str_data, strlen(str_data));
18
19 rt_event_send(recvdata_event, WRITE_EVENT);
20 }
21 rt_thread_mdelay(30);
22}
在DFS_thread 線程中,通過接收兩次事件,並設置接收事件的超時時間,達到計時的目的:
1\while (1)
2{
3
4 if (rt_event_recv(recvdata_event, WRITE_EVENT, RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR, RT_WAITING_FOREVER, &set) != RT_EOK)
5 continue;
6 do
7 {
8
9 if (rt_event_recv(recvdata_event, WRITE_EVENT, RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR, rt_tick_from_millisecond(1000), &set) == RT_EOK)
10 {
11
12 if (rt_ringbuffer_data_len(recvdatabuf) > THRESHOLD)
13 {
14
15 recvdatafile_p0 = fopen("recvdata_p0.csv", "ab+");
16 if (recvdatafile_p0 != RT_NULL)
17 {
18 while(rt_ringbuffer_data_len(recvdatabuf))
19 {
20 size = rt_ringbuffer_get(recvdatabuf, (rt_uint8_t *)writebuffer, THRESHOLD);
21 fwrite(writebuffer, 1, size, recvdatafile_p0);
22 }
23 fclose(recvdatafile_p0);
24 }
25 }
26
27 continue;
28 }
29
30 recvdatafile_p0 = fopen("recvdata_p0.csv", "ab+");
31 if (recvdatafile_p0 != RT_NULL)
32 {
33 while(rt_ringbuffer_data_len(recvdatabuf))
34 {
35 size = rt_ringbuffer_get(recvdatabuf, (rt_uint8_t *)writebuffer, THRESHOLD);
36 fwrite(writebuffer, 1, size, recvdatafile_p0);
37 }
38 fclose(recvdatafile_p0);
39 }
40 } while(0);
41}
第三周的源碼中,只上傳了兩個 demo 工程,均是本次 DIY 中接收節點的代碼
發送節點的代碼,在第二周的 demo 工程中有,這裡不再重複上傳相同 demo 工程
SPI Flash 的 sector 大小為 4096 字節,需要在 menuconfig 中修改:
SD Card 的 sector 大小為 512 字節,需要在 menuconfig 中修改:
#題外話# 喜歡RT-Thread不要忘了在GitHub上留下你的STAR哦,你的star對我們來說非常重要!連結地址:https://github.com/RT-Thread/rt-thread
你可以添加微信18917005679為好友,註明:公司+姓名,拉進 RT-Thread 官方微信交流群
RT-Thread
讓物聯網終端的開發變得簡單、快速,晶片的價值得到最大化發揮。Apache2.0協議,可免費在商業產品中使用,不需要公布源碼,無潛在商業風險。
點擊閱讀原文進入GitHub