2024年7月

摘要:本次实验学习记录主题为“FIFO_IP核实现算术求和”,主要内容是上位机通过串口向FPGA发送一定规格的数字矩阵,FPGA对矩阵处理,按规定逻辑实现求和运算,将结果返回串口转发至上位机。

芯片型号:cyclone Ⅳ EP4CE10F17C8

平台工具:Quartus II 15.0 (64-bit)、Modelsim SE-64 10.4

最终框图:

image


【FIFO IP核概述及调用】

FIFO(First In First Out,先入先出) IP核作为
数据缓冲区
,能临时存储从数据源接收的数据,直到数据被其他处理单元再次读取。FIFO IP核通常用于多比特数据的
跨时钟域处理
以及前后
带宽不同步
情况,平衡数据源和处理单元之间的速度差异,同时减少因速率不匹配而导致的等待时间或数据丢失。

FIFO IP核支持
同步(SCFIFO)和异步(DCFIFO)
操作模式,在同步模式下,读写操作在同一时钟域下进行。其支持可配置的参数(如数据宽度、深度等,调整以适应不同的需求。针对不同模式的选择,需要考虑方面包括时钟源、存取位宽和深度、以及一系列辅助设计的标志信号和操作信号。

下图为Quartus Ⅱ构建IP核能产生的全部接口,同步模式下,除了基本的外接口如数据位、时钟、写标志和读标志、计数位
usedw
外,还有清零操作(同步
sclr
/异步
aclr
)、满/近满/空/近空/校检
eccstatus
信号。而异步模式下,对于入栈和出栈辅助设计的分为了两批,具体结构如下图。

image

访问IP Catalog
:在Quartus Ⅱ的菜单栏中,点击“Tools”选项,然后选择“IP Catalog”或者“MegaWizard Plug-In Manager”,打开“fifo”选项即可。FIFO配置流程分为三部分:parameter settings、EDA和summary。如下图的配置界面,在其左上可以实时看到配置产生的接口,左下角看到FIFO在FPGA所产生的资源消耗。

image

在配置完基本参数后,FIFO还支持功能等设置趋向,
rdreq
读取驱动:信号作为请求,数据滞后一个时钟周期输出;信号作为确认,数据同时输出。存储方式和最大深度选择自动匹配即可。FIFO性能支持最大速度和最小消耗资源空间两种,可根据具体工程需求选定。黄色方框内是上级检测和下级检测保护电路,即存储栈满和栈空情况下的继续操作保护,最小面是存储空间位置选择,这里默认选定内部存储块即可。

image

异步模式下,还需配置速度和稳定性的优化方式,一是保持最低延迟,但需要同步时钟,没有亚稳态保护,占用资源空间最小,提供良好性能;二是具备两个同步阶段和良好的亚稳态保护,资源空间消耗中等;三是提高最佳的亚稳态保护,具有三个或更多同步阶段。

image

【IP核的同步、异步调用及仿真验证】

首先,构建一个同步FIFO_IP核,具体配置如下:

almost_empty_value = 20,	//近空阈值
almost_full_value = 220,	//近满阈值
intended_device_family = "Cyclone IV E",	//FPGA IP核型号
lpm_numwords = 256,			//FIFO深度
lpm_showahead = "OFF",		//rdreq模式选择
lpm_type = "scfifo",		//FIFO工作模式(同步,单时钟模式)
lpm_width = 8,				//时钟源同步下,进入FIFO位宽
lpm_widthu = 8,				//计数位宽

IP的直接调用inst.v模块文件即可,实例化应用后,通过一个简单的录入核/退出核仿真(如下两图)。可以看到,程序启动,持续向核内写入256个8bit数据,仿真设定,写入周期是读入周期的四倍。

计数到20时,退出近空阈值,近空信号拉低;计数到220,达到近满阈值,近满信号拉高,等到写入完毕(这里计数单元
usedw_sig
溢出,显示8'h00),满信号拉高。下一周期,读标志拉高,读取一个8bit数据后,满信号拉低,持续读取完毕。

image

构建一个异步混合FIFO_IP核,具体配置如下:

add_usedw_msb_bit = "ON",	//为计数位扩充一位,避免溢出
intended_device_family = "Cyclone IV E",	//FPGA IP核型号
lpm_numwords = 256,			//FIFO深度
lpm_showahead = "OFF",		//rdreq模式选择
lpm_type = "dcfifo_mixed_widths",	//混合异步fifo模式,意思是录入核和退出核位宽不一致
lpm_width = 8,				//录入核位宽
lpm_widthu = 9,				//计数位宽+1 = 9
lpm_widthu_r = 8,			//读取退出核位宽
lpm_width_r = 16,			//读取退出核计数位宽

异步模式,需要关注时序上的同步(打了两拍),50MHz的写时钟wrclk,25MHz的读时钟rdclk。这里由于写位宽和读位宽的不同,要区别写计数和读计数的计数方式。

image

【调用FIFO实现求和运算】

调用Quartus Ⅱ的IP核实现普通求和运算(便于Sobel算法FPGA学习),左边是求和模块的框图,需要复用两个相同位宽及深度的FIFO IP核,以m x n(5x4)矩阵为例,先对上三行求运算后,持续向下降一行运算,形成一个新的矩阵(m-2) x n形式。

image

FPGA运算:pi_data持续接入数据,先将第一、二行数据分布存入FIFO 1核和2核内,在第三行数据开始,同步读取两核一个数据,并对其作求和运算,通过po_data输出。求和的同时,将FIFO 2核内数据写入1核(1、2核此时为空),即第二行充当原先的第一行。第三行写入2核,第四行持续运算.......

时序图如下,
pi_flag

pi_data
是串口rx模块接收上位机处理后的数据,录入此fifo_disp模块。矩阵的列和行计数器
cnt_row

cnt_rol
作为的顺序标志,方便确认求和准备。
dout_flag
条件(wr_en2)&&(rd_en),标志建立用于1核数据再次写入。借入标志信号
sum_flag
,触发求和
po_data
=
data_out1
+
data_out2
+
pi_data

image

对应的各信号时序条件处理,代码如下:

always@(posedge sys_clk or negedge sys_rst)begin	//dispose cnt_row counter
    if(!sys_rst)	cnt_row <=  8'd0;
    else    if((cnt_row == CNT_ROW_MAX)&&(pi_flag))	cnt_row <=  8'd0;
    else    if(pi_flag)	cnt_row <=  cnt_row + 1'b1;
end

always@(posedge sys_clk or negedge sys_rst)begin	//dispose cnt_col counter
    if(!sys_rst)	cnt_col <=  8'd0;
    else    if((cnt_col == CNT_COL_MAX)&&(pi_flag)&&(cnt_row == CNT_ROW_MAX))
        cnt_col <=  8'd0;
    else    if((cnt_row == CNT_ROW_MAX)&&(pi_flag))cnt_col <=  cnt_col + 1'b1;
end

always@(posedge sys_clk or negedge sys_rst)begin	//dispose wr_en1 drive
    if(!sys_rst)	wr_en1  <=  1'b0;
    else    if((cnt_col == 8'd0) && (pi_flag))	wr_en1  <=  1'b1;
    else	wr_en1  <=  dout_flag;
end

always@(posedge sys_clk or negedge sys_rst)begin	//dispose data_in1 sequence
    if(!sys_rst)	data_in1  <=  8'd0;
    else    if((pi_flag)&&(cnt_col == 8'd0))	data_in1  <=  pi_data;
    else    if(dout_flag == 1'b1)	data_in1  <=  data_out2;
    else	data_in1  <=  data_in1;
end

always@(posedge sys_clk or negedge sys_rst)begin	//dispose wr_en2 drive
    if(!sys_rst)	wr_en2  <=  1'b0;
    else    if((cnt_col >= 8'd1)&&(cnt_col <= CNT_COL_MAX - 1'b1)&&(pi_flag))
        wr_en2  <=  1'b1;
    else	wr_en2  <=  1'b0;
end

always@(posedge sys_clk or negedge sys_rst)begin	//dispose data_in2 sequence
    if(!sys_rst)	data_in2  <=  8'b0;
    else    if((pi_flag)&&(cnt_col >= 8'd1)&&(cnt_col <= (CNT_COL_MAX - 1'b1)))
        data_in2  <=  pi_data;
    else	data_in2  <=  data_in2;
end

always@(posedge sys_clk or negedge sys_rst)begin	//dispose rd_en drive
    if(!sys_rst)	 rd_en <=  1'b0;
    else    if((pi_flag)&&(cnt_col >= 8'd2)&&(cnt_col <= CNT_COL_MAX)) rd_en <=  1'b1;
    else	rd_en <=  1'b0;
end

always@(posedge sys_clk or negedge sys_rst)begin	//dispose dout_flag sequence
    if(!sys_rst)	dout_flag <=  0;
    else    if((wr_en2)&&(rd_en))	dout_flag <=  1'b1;
    else	dout_flag <=  1'b0;
end

always@(posedge sys_clk or negedge sys_rst)begin	//dispose sum_flag sequence
    if(!sys_rst)	sum_flag <=  1'b0;
    else    if(rd_en)	sum_flag <=  1'b1;
    else    sum_flag <=  1'b0;
end

always@(posedge sys_clk or negedge sys_rst)begin	//dispose po_data result
    if(!sys_rst)	po_data  <=  8'b0;
    else    if(sum_flag)	po_data  <=  data_out1 + data_out2 + pi_data;
    else	po_data  <=  po_data;
end

always@(posedge sys_clk or negedge sys_rst)begin	//dispose po_flag sequence
    if(!sys_rst)	po_flag <=  1'b0;
    else 	po_flag <=  sum_flag;
end

仿真分析:
很明显,仿真图与上面的时序图一致,tx、rx模块在之前的实验经过仿真验证了。

image

最后,将程序下载至开发板,得到的数据与仿真结果一样,简单做了两次测试,结果都正确。

image

文献参考:

[1] FIFO求和实验
野火FPGA Verilog开发实战指南——基于Altera EP4CE10 征途Pro开发板 文档 (embedfire.com)
;

[2]
掰开揉碎讲 FIFO(同步FIFO和异步FIFO) - Doreen的FPGA自留地 - 博客园 (cnblogs.com)


本篇文章中使用的Verilog程序模块,若有需见网页左栏Gitee仓库链接:
https://gitee.com/silly-big-head/little-mouse-funnyhouse/tree/FPGA-Verilog/

一、
起因

最近毕业在家:),准备筹划社区运营和IoTBrowser升级的事务,遇到了一系列物业管理上的问题,本来出于好心提醒物业人员,结果反被误认为是打广告推销的,当时被激怒一下,后面一想也许这也是一个普遍存在的问题,正好IoTBrowser缺少落地的应用场景,遂又撸起袖子搞了一个AI工具。以下是本人所在小区发生的几件奇葩事件:

案例一、小狗闯入小朋友游玩区排便

利用AI监测可以根据小狗的特征实时抓拍分析,一旦有小狗进入特定区域及时提醒物管人员进行人为干预。

案例二、几十起
车胎被

事件

短短几个月内,已经有几十起轮胎被扎事件;几个月时间过去了,还没有找到任何嫌疑人,目前的解决方案是保安不定期人工巡查。轮胎被钉子扎是很严重的问题,轻则补胎换胎,严重的情况在高速上才发现会有车祸的风险。如果利用AI识别,可以训练特定的姿势数据从海量回放视频中快速找到嫌疑人。


案例三、有人进入车库偷窃、偷电瓶

一件电瓶偷窃事件、一起拉车门偷窃事件。物业管理面对以上事件给业主的信誉度和服务质量打上了非常差劲的评价。目前仅地下车库就有100多个摄像头,没有专门的技术人员和工具,无法快速定位事件的源头,给物业工作带来了十分庞大的管理问题。

二、智慧眼介绍

智慧眼(AIEye)是一款解决监控直播、视频、图片的视觉风险监测系统,单机版支持任何Windows系统上运行,基于Yolo v5智能算法开发。基于IoTBrowser开发框架,利用丰富的插件和成熟的web技术,在不到两周时间就完成了开发和发布。系统支持主流摄像头实时监测、视频回放监测、80多种物体识别、微信预警推送等功能,还可以定制训练特定的物体识别。

二、挖掘潜力

开发的初衷是解决物业管理中减少人工成本、降低无人值守中的盲点、快速追踪物体,如果发现工具可以在其他领域发挥其价值的朋友,欢迎一起探讨!

liwen01 2024.07.07

前言

yaffs 是专为nand flash 设计的一款文件系统,与jffs 类似,都是属于日志结构文件系统。与jffs 不同的是,yaffs 文件系统利用了nand flash 一些特有属性,所以在数据读写擦除和回收上都有较大的差异。

关于jffs2文件系统的介绍可以查看

文件系统(八):Linux JFFS2文件系统工作原理、优势与局限

这里先介绍一下nand flash的一些基础知识,有助于后面理解yaffs的设计原理。

(一)flash 基础

flash分为
nor flash

nand flash
两类:

nor flash
: 成本较高,容量较小,优点是读写数据不容易出错,比较适用于存储关键数据,比如程序固件、配置参数等。

nand flash
:成本较低,相对便宜,容量较大,但是数据比较容易出错,所以一般都需要有对应的软件或者硬件的校验算法(ECC),比较适合用来储存大容量且数据安全要求不是非常严格的数据,比如照片、视频等。

(1)nand flash 数据存储单元

nand flash数据存储单元从概念上来说,由大到小有:

Nand Flash(Package) -> Chip(Die) -> Plane -> Block -> Page(Chunk) -> OOB(Spare data)

其中有些存储单元,在一些不同的资料上它们的叫法不太一样,比如page(页),、有些资料上介绍的是Chunk,在有些软件编程中,也有可能被介绍为扇区sector

Nand Flash
:也叫
Package
,这是我们在
PCBA
上看到的已经封装好的整科芯片,带有封装有IO引脚,可以直接焊接到PCB上使用。

Chip
:也叫
Die
(裸片),这是独立的硅片,包含存储单元和控制电路,一个Package 中可以包含多个Die。

Plane
: Plane是die内部的一个逻辑分区。每个die通常被划分为多个plane,以实现并行操作。每个plane有独立的寄存器和数据缓存,因此可以同时进行多个操作(如读取、写入、擦除),从而提高性能。

Block
:
NAND Flash
存储的基本单位。

Page
:也叫
chunk
,NAND Flash中最小的可编程单元。

OOB(
Out

-Of-Band
)

:也叫
Spare data
,OOB区域是每个page中额外的存储空间,用于存储元数据,例如错误校正码(ECC)、坏块标记和其他管理信息.

(2)nand flash 特性

nand flash 有一些特殊的属性,也是因为这些特殊的属性才有了yaffs文件系统的特殊设计

  • 数据读写的最小单位是page(chunk)
  • 数据写入之前,写入位置需要是被擦除过了的
  • 数据擦除的最小单位是block
  • block里面的page,只能按顺序写入,不能任意page写入
  • oob的数据是随着page(chunk)的数据一同被写入
  • nand flash有 编程干扰读取干扰配对页面 等问题,会引起自身或是配对页面的位翻转。

(3)数据存储

结合nand flash的特性,从应用软件编程的角度来看,整个nand flash空间是由各page(chunk)组成,每个page(chunk)后面跟随一个与之对应的oob.

不同型号不同厂家生产的nand flash,它的block、page、oob等大小有可能不一样,在软件开发或是制作yaffs文件系统时,首先需要确认nand flash的参数。

(二)yaff2 数据格式

yaffs 有两个版本,yaffs1与yaffs2,主要区别是yaffs2可以支持比512Byte更大的chunk。它发布于2003年,比jffs2晚一两年被设计,但距今也二十多年了。

下面内容,yaffs 是代指yaffs1和yaffs2。关于yaffs文件系统的详细介绍,可以从官方网站下载到最新的代码和说明文档:https://www.aleph1.co.uk/gitweb/

(1)yaffs2 数据打包

  1. 创建4个测试目录,每个目录各创建一个测试文件,里面写有少量字符数据:
biao@ubuntu:~/test/yaffs/yaffs2_fs$ tree
.
├── test1
│   └── file1
├── test2
│   └── file2
├── test3
│   └── file3
└── test4
    └── file4

4 directories, 4 files

在制作成yaffs2
镜像文件
之前,4个目录和文件的大小如下:

biao@ubuntu:~/test/yaffs$ du yaffs2_fs
8       yaffs2_fs/test3
8       yaffs2_fs/test2
8       yaffs2_fs/test1
8       yaffs2_fs/test4
36      yaffs2_fs
biao@ubuntu:~/test/yaffs$
  1. 下载最新yaffs源码,在yaffs2/utils 目录执行make,编译生成mkyaffs2image打包程序
  2. 使用默认参数对测试目录进行打包
biao@ubuntu:~/test/yaffs$ ./mkyaffs2image yaffs2_fs yaffs2_fs.img
mkyaffs2image: image building tool for YAFFS2 built Jul  7 2024
Processing directory yaffs2_fs into image file yaffs2_fs.img
Object 257, yaffs2_fs/test3 is a directory
Object 258, yaffs2_fs/test3/file3 is a file, 1 data chunks written
Object 259, yaffs2_fs/test2 is a directory
Object 260, yaffs2_fs/test2/file2 is a file, 1 data chunks written
Object 261, yaffs2_fs/test1 is a directory
Object 262, yaffs2_fs/test1/file1 is a file, 1 data chunks written
Object 263, yaffs2_fs/test4 is a directory
Object 264, yaffs2_fs/test4/file4 is a file, 1 data chunks written
Operation complete.
16 objects in 5 directories
12 NAND pages
biao@ubuntu:~/test/yaffs$

查看yaffs2_fs.img镜像文件信息:

biao@ubuntu:~/test/yaffs$ stat yaffs2_fs.img
  File: yaffs2_fs.img
  Size: 135168          Blocks: 264        IO Block: 4096   regular file
Device: 801h/2049d      Inode: 7874075     Links: 1
Access: (0600/-rw-------)  Uid: ( 1000/    biao)   Gid: ( 1000/    biao)
Access: 2024-07-07 23:12:18.195919283 +0800
Modify: 2024-07-07 23:10:19.798582920 +0800
Change: 2024-07-07 23:10:19.798582920 +0800
 Birth: -
biao@ubuntu:~/test/yaffs$

从yaffs2_fs.img镜像文件中我们看到,打包后的镜像文件比我们原来的目录文件要大很多,打包前是36KByte,打包后是132KByte,这是为什么呢?

(2)yaffs 数据分析

使用hexdunp命令直接查看yaffs2_fs.img镜像文件数据:

biao@ubuntu:~/test/yaffs$ hexdump -C yaffs2_fs.img
00000000  03 00 00 00 01 00 00 00  ff ff 74 65 73 74 33 00  |..........test3.|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
.........
.........
*
00000840  01 00 00 00 01 01 00 00  ff ff 66 69 6c 65 33 00  |..........file3.|
00000850  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000940  00 00 00 00 00 00 00 00  00 00 ff ff b4 81 00 00  |................|
00000950  e8 03 00 00 e8 03 00 00  f4 45 85 66 7e e5 70 66  |.........E.f~.pf|
00000960  43 45 85 66 1d 00 00 00  ff ff ff ff ff ff ff ff  |CE.f............|
00000970  ff ff ff ff ff ff ff ff  ff ff ff ff ff ff ff ff  |................|
*
.........
.........
*
00001080  63 63 63 63 63 63 63 63  63 63 63 63 63 63 63 63  |cccccccccccccccc|
00001090  63 63 63 63 63 63 63 63  63 63 63 63 0a ff ff ff  |cccccccccccc....|
000010a0  ff ff ff ff ff ff ff ff  ff ff ff ff ff ff ff ff  |................|
*
.........
.........

从hex数据中我们可以直观的看到文件名信息和文件里面的数据,也就是说文件名和文件里面的数据都是未压缩的。

我们对mkyaffsimage.c的源码进行分析,在默认参数下mkyaffsimage打包的镜像文件,它的chunk、spare、block大小信息如下:

#define chunkSize 2048
#define spareSize 64
#define pagesPerBlock 64

yaffs2的镜像文件是由object_header、data、yaffs_spare 三个部分组成,每个object_header、data 至少占用一个chunk,yaffs_spare 实际上也就是oob数据,是存储在spare空间。

(3)yaffs2 目录

我们对上面yaffs2_fs.img的镜像文件进行分析,先看最开始的数据,是test3目录obj

00000000  03 00 00 00 01 00 00 00  ff ff 74 65 73 74 33 00  |..........test3.|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000100  00 00 00 00 00 00 00 00  00 00 ff ff fd 41 00 00  |.............A..|
00000110  e8 03 00 00 e8 03 00 00  f4 45 85 66 7e e5 70 66  |.........E.f~.pf|
00000120  43 45 85 66 ff ff ff ff  ff ff ff ff ff ff ff ff  |CE.f............|
00000130  ff ff ff ff ff ff ff ff  ff ff ff ff ff ff ff ff  |................|
*
000001c0  ff ff ff ff ff ff ff ff  ff ff ff ff 00 00 00 00  |................|
000001d0  ff ff ff ff ff ff ff ff  ff ff ff ff ff ff ff ff  |................|
*
00000800  00 10 00 00 01 01 00 00  00 00 00 00 ff ff 00 00  |................|
00000810  25 00 00 00 00 00 00 00  ff ff ff ff ff ff ff ff  |%...............|
00000820  ff ff ff ff ff ff ff ff  ff ff ff ff ff ff ff ff  |................|
*

0~0x800 地址的数据是object_header数据结构,后面是oob的数据结构,详细解析数据如下:

从目录解析表格中我们可以看到如下信息:
  1. 未填写区域是数据0xFF,也就是未写入数据
  2. object_header大小为512Byte
  3. oob 大小为64Byte,与上面代码设置的相同
  4. 这里file_size_low为0xFF,表示不携带实际数据,实际也是没有data段
  5. obj_id 是从0x100(256)开始,在整个文件系统中,obj_id是不重复的,chunk更新的时候,obj_id保持不变

(3)yaffs2 文件

下面数据是file3的数据结构

00000840  01 00 00 00 01 01 00 00  ff ff 66 69 6c 65 33 00  |..........file3.|
00000850  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000940  00 00 00 00 00 00 00 00  00 00 ff ff b4 81 00 00  |................|
00000950  e8 03 00 00 e8 03 00 00  f4 45 85 66 7e e5 70 66  |.........E.f~.pf|
00000960  43 45 85 66 1d 00 00 00  ff ff ff ff ff ff ff ff  |CE.f............|
00000970  ff ff ff ff ff ff ff ff  ff ff ff ff ff ff ff ff  |................|
*
00000a00  ff ff ff ff ff ff ff ff  ff ff ff ff 00 00 00 00  |................|
00000a10  ff ff ff ff ff ff ff ff  ff ff ff ff ff ff ff ff  |................|
*
00000a30  00 00 00 00 ff ff ff ff  ff ff ff ff ff ff ff ff  |................|
00000a40  ff ff ff ff ff ff ff ff  ff ff ff ff ff ff ff ff  |................|
*
00001040  00 10 00 00 02 01 00 00  00 00 00 00 ff ff 00 00  |................|
00001050  26 00 00 00 00 00 00 00  ff ff ff ff ff ff ff ff  |&...............|
00001060  ff ff ff ff ff ff ff ff  ff ff ff ff ff ff ff ff  |................|
*
00001080  63 63 63 63 63 63 63 63  63 63 63 63 63 63 63 63  |cccccccccccccccc|
00001090  63 63 63 63 63 63 63 63  63 63 63 63 0a ff ff ff  |cccccccccccc....|
000010a0  ff ff ff ff ff ff ff ff  ff ff ff ff ff ff ff ff  |................|
*
00001880  00 10 00 00 02 01 00 00  01 00 00 00 1d 00 00 00  |................|
00001890  00 00 00 00 08 00 00 00  08 00 00 00 ff ff ff ff  |................|
000018a0  ff ff ff ff ff ff ff ff  ff ff ff ff ff ff ff ff  |................|
*

file3是一个文件,其中包括2个chunk:一个是Object,另外一个是data,其中每个chunk后面有一个与之对应的oob

与目录相比,文件有file_size_low,chunk_id,还有data chunk。我们看file3实际数据:

biao@ubuntu:~/test/yaffs$ stat yaffs2_fs/test3/file3 
  File: yaffs2_fs/test3/file3
  Size: 29              Blocks: 8          IO Block: 4096   regular file
Device: 801h/2049d      Inode: 7874095     Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/    biao)   Gid: ( 1000/    biao)
Access: 2024-07-07 23:57:37.355671911 +0800
Modify: 2024-07-07 23:40:14.962499985 +0800
Change: 2024-07-07 23:34:11.067767029 +0800
 Birth: -
biao@ubuntu:~/test/yaffs$ cat yaffs2_fs/test3/file3 
cccccccccccccccccccccccccccc
biao@ubuntu:~/test/yaffs$

对比发现data chunk中存储的数据,就是file3文件里面的实际数据。

(三)工作原理

(1)yaffs2 挂载

上面我们分析了目录和文件obj的数据结构,实际yaffs还支持其它的文件类型:

enum yaffs_obj_type {
	YAFFS_OBJECT_TYPE_UNKNOWN,
	YAFFS_OBJECT_TYPE_FILE,
	YAFFS_OBJECT_TYPE_SYMLINK,
	YAFFS_OBJECT_TYPE_DIRECTORY,
	YAFFS_OBJECT_TYPE_HARDLINK,
	YAFFS_OBJECT_TYPE_SPECIAL
};

从obj 类型结构体中我们可以看到,还支持软连接、硬连接和特殊文件类型。它们与常规的文件、目录一样,都有object_header 结构,关键的元数据信息都是存储在oob中。

实际yaffs文件系统在挂载的时候,并不需要像jffs2一样扫描整个flash空间。在yaffs文件系统中,只需要先扫描oob里面的数据就可以构建出文件、目录与chunk之间的关系,再结合object_header信息就可以构建出整个文件系统的信息。所以yaffs2在同等大小的文件系统中,挂载速度是会比jffs2快的。

(2)yaffs2数据更新

回顾我们前面介绍的nand flash特性:

  • 数据读写的最小单位是page(chunk)
  • 数据写入之前,写入位置需要是被擦除过了的
  • 数据擦除的最小单位是block
  • block里面的page,只能按顺序写入,不能任意page写入
  • oob的数据是随着page的数据一同被写入

对于我们上面介绍的file3文件,如果我们要对它进行修改或是删除,在flash中是需要怎么操作的呢?

  1. 首先找到要修改的chunk,将数据读取到内存中,再对其数据进行修改,最后将修改后的数据写入到一个新的chunk
  2. 新的数据写入新chunk的同时,与它对应的oob数据也会被一同写入新chunk对应的oob区域

oob的数据是随着chunk的数据写入flash中的,但是nand flash 的擦除又是按block进行擦除,如果不擦除,数据又不能重新被写入,那要怎么标记file3 存储原来数据的chunk为无效呢?

在yaffs2中,它是通过oob中的obj id来标记是否同一个数据chunk,通过seq_number来标记哪个chunk的数据是最新的,如果不是最新的,那就是无效的了。

比如在文件系统中,有多个chunk它们有相同的obj id,说明这些chunk都是这个obj id 的不同修改版本的同一组数据,seq_number值最大的是最新的数据,其它的则都是无效数据。每一次修改,seq_number就会增加1。

这里是通过软件方法来标记数据无效,实际物理数据是没有做无效标记的,数据也没有被清除。物理上的标记无效和数据擦除,是需要等到垃圾回收的时候再对整个block进行擦除操作,这个时候标记的其实不是数据无效,而是chunk未使用.

在数据更新的操作中,核心的参数是
obj id

seq_number

(3)垃圾回收机制

从上面数据更新原理上我们知道,一个旧的数据,或是数据结构,在yaffs2文件系统中并不会标记它为无效,因为写入标志同样需要擦除再写入。在yaffs2文件系统中,是通过seq_number来标记数据版本的新旧,旧的则为无效数据。

在yaffs2的垃圾回收中,有两种方式:主动回收和被动回收:

主动回收
:一个block中的绝大部分chunk数据都是无效的,文件系统会触发主动回收

被动回收
:flash 已经没有干净的chunk可以继续使用,此时需要立即执行垃圾回收以释放空间。这里会把几个block中的有效数据合并到一块,腾出至少一个无效数据block以便进行整块擦除回收。

yaffs2文件系统中,为了平衡性能与回收功能,它的垃圾回收有两个特性:

  1. 尽可能地延迟进行垃圾回收
  2. 一次只处理一个块

(四)优缺点

(1)优点

  • 启动较快:与jffs2相比,它不需要全盘扫描flash空间,所以挂载所花费的时间相对较短。
  • 日志结构:采用日志结构的设计,在异常断电等情况下比较容易保持文件系统的一致性。
  • 磨损均衡:block内的chunk是按序写入,加上日志结构设备使yaffs自带磨损平衡。但是在垃圾回收的时候,并没有提供专门的算法,所以不是严格的磨损平衡,带有一些随机性。

(2)缺点

  • 无压缩功能:从上面我们对file3文件的分析可以看到,文件数据和元数据都未进行压缩,这个在对成本敏感的嵌入式设备中,是个劣势。
  • 元数据开销大: 每个obj都至少需要一个chunk存储object_header,元数据的开销大,浪费存储空间。
  • 扩展性差:不适合大容量的存储设备,管理大规模数据时性能可能下降。

(3)yaffs2与jffs2

yaffs2 文件系统与 jffs2 文件系统非常相似,都是基于裸flash设计的文件系统,jffs2 更常用于nor flash ,而yaffs2 是专为nand flash 而设计。它们都是日志结构文件系统,都有磨损平衡功能,但也都是随机磨损平衡。

它们都适合比较小容量的存储设备,因为jffs2挂载的时候需要全盘扫描flash查找元数据构建文件目录结构,所以jffs2在大容量存储设备中数据存储比较多时,挂载所需要的时间会比较长,耗用的内存也会比较多。

yaff2 是将关键元数据存储在oob中,nand flash的oob区域是固定的。挂载的时候只需要扫描oob区域数据就可以了,所以相比较jffs2,yaffs2的挂载启动速度会比较快一些。

jffs2的数据和元数据都是压缩的,并且支持多种压缩算法,这些yaffs2都没有,所以空间利用率yaffs2并没有jffs2高。

在产品功能没有明显优势的前提下,能把产品价格做低其实也是一个非常大的优势,所以nand flash的应用也越发的普及。但目前nand flash 使用比较多的是集成到FTL(Flash Translation Layer)设备中,比如TF卡,SD卡、SSD、U盘等。

jffs2和yaffs2文件系统,都是基于裸的flash来使用,它们并不适用于FTL设备,FTL设备使用比较多的文件系统是:FAT32,exFAT、NTFS、ext3、ext4等

关于存储介质和其它文件系统原理的介绍,可以查看前面文章:

文件系统(一):存储介质、原理与架构
文件系统(二):分区、格式化数据结构
文件系统(三):嵌入式、计算机系统启动流程与步骤
文件系统(四):FAT32文件系统实现原理
文件系统(五):exFAT 文件系统原理详解
文件系统(六):一文看懂linux ext4文件系统工作原理
文件系统(七):文件系统崩溃一致性、方法、原理与局限
文件系统(八):Linux JFFS2文件系统工作原理、优势与局限

结尾

yaffs2目前在嵌入式设备中使用率还是比较高,了解它的工作原理,有助于更好地使用它。另外从官方资料上看,yaffs 是需要授权收费的,如果有使用yaffs2文件系统的设备,需要考虑是否存在版权法律风险。

【如果你觉得文章内容对你有帮助,那就点个赞、关注一下吧】

------------------End------------------
如需获取更多内容
请关注 liwen01 公众号

1、引言

在处理JSON数据时,我们常常需要提取、筛选或者变换数据。手动编写这些操作的代码不仅繁琐,而且容易出错。Python作为一个功能强大的编程语言,拥有丰富的库和工具来处理这些数据。今天,将介绍一个实用的Python库——JMESPath,它为提取JSON数据提供了简洁而强大的语法。

2、JMESPath介绍

Jmespath(JSON Messaging Path)是一种用于查询和转换JSON数据的查询语言,专为JSON数据设计。它允许开发者使用简洁的表达式来提取、筛选和转换JSON数据中的元素,使得开发者能够轻松地提取JSON数据中的元素,并执行各种转换操作。与XPath类似,JMESPath的语法直观易懂,学习成本较低。

3、JMESPath安装

在使用Python Jmespath之前,需要先通过pip安装该库:

pip install jmespath

4、JMESPath用途

JMESPath在Python中的用途广泛,尤其适用于以下场景:

  • 接口自动化测试
    :在测试API接口时,经常需要从响应的JSON数据中提取待验证的字段值。使用JMESPath,可以轻松编写查询表达式,快速定位并提取所需数据。
  • 数据处理与分析
    :在处理和分析大量JSON数据时,JMESPath可以帮助开发者快速筛选和转换数据,提高数据处理效率。
  • 日志分析与监控
    :在日志管理和监控系统中,JSON格式的日志数据非常常见。使用JMESPath,可以轻松从日志数据中提取关键信息,实现实时监控和告警。

5、JMESPath的特性

JMESPath具有以下特性:

  • 声明式语法
    :JMESPath使用声明式语法,使得表达式易于阅读和编写,通过点(.)和方括号([])操作符即可轻松访问JSON对象中的属性和数组元素。
  • 强大的功能
    :支持多种运算符、函数和语法结构,可以处理各种复杂的数据结构。
  • 跨语言跨平台支持
    :JMESPath不仅支持Python,还有其他编程语言的实现,如JavaScript、Java等。
  • 可扩展性
    :可以自定义函数,以满足特定的数据处理需求。

6、使用示例

假设有以下JSON数据:

{  
  "name": "张三",  
  "age": 26,  
  "grade": {  
    "Chinese": 96,  
    "Math": 99  
  },  
  "records": [  
    {"Chinese": 95, "Math": 100},  
    {"Chinese": 98, "Math": 98}  
  ]  
}

使用Jmespath提取name和records中的第一个元素:

import jmespath  
  
data = {  
  "name": "张三",  
  "age": 26,  
  "grade": {  
    "Chinese": 96,  
    "Math": 99  
  },  
  "records": [  
    {"Chinese": 95, "Math": 100},  
    {"Chinese": 98, "Math": 98}  
  ]  
}  
  
search_name = 'name'  
res_name = jmespath.search(search_name, data)  
print(res_name)  # 输出: 张三  
  
search_records = 'records[0]'  
res_records = jmespath.search(search_records, data)  
print(res_records)  # 输出: {'Chinese': 95, 'Math': 100}

使用切片和通配符提取所有学生的名字:

import jmespath  
  
data = {  
  "students": [  
    {"name": "Alice", "age": 20},  
    {"name": "Bob", "age": 22},  
    {"name": "Charlie", "age": 21}  
  ]  
}  
  
search_names = 'students[*].name'  
res_names = jmespath.search(search_names, data)  
print(res_names)  # 输出: ['Alice', 'Bob', 'Charlie']

使用管道符将多个查询操作串联起来,如先筛选出年龄大于21的学生,然后提取他们的名字:

import jmespath  
  
data = {  
  "students": [  
    {"name": "Alice", "age": 20},  
    {"name": "Bob", "age": 22},  
    {"name": "Charlie", "age": 21}  
  ]  
}  
  
search_expr = 'students[?age > `21`].name'  
res_filtered_names = jmespath.search(search_expr, data)  
print(res_filtered_names)  # 输出: ['Bob']

7、结论

Python Jmespath库为处理JSON数据提供了一种简洁而强大的解决方案。其简洁的语法、强大的功能以及跨平台的支持使得它成为处理复杂JSON数据的理想选择。无论是接口自动化测试、数据处理还是数据分析,JMESPath都能发挥重要作用。

通过上述内容的学习,希望能够帮助大家更好地理解和使用Jmespath库,从而更高效地处理JSON数据。

前言

前面使用 Admin.Core 的代码生成器生成了通用代码生成器的基础模块 分组,模板,项目,项目模型,项目字段的基础功能,本篇继续完善,实现最核心的模板生成功能,并提供生成预览及代码文件压缩下载

准备

首先清楚几个模块的关系,如何使用,简单画一个流程图

image.png
前面完成了基础的模板组,模板管理,项目,模型,字段管理,都是由 Admin.Core 框架的代码生成器完成,感兴趣的可参考前篇使用,文末也会给出仓库地址,有问题欢迎交流

本文主要分享项目代码的生成,先放出效果图

  • 项目生成管理,支持多模板组

  • 模板生成预览,预览页可以直接编辑模板

  • 模板生成,将生成压缩包并下载

实现

需要实现上面效果的代码生成器,基础的增删改查都可以借由代码生成器生成,也写过几篇了这里就不再赘述,我们只需要关注核心部分:如何生成?如何预览?如何下载?

根据模板生成对应项目的代码文件

当我们有了一个模板(模板内容),也有了对应的配置项(项目&项目模型&项目字段),中间肯定是需要一个模板引擎来将模板根据配置项解析出来的。

市面上有很多模板引擎,比如 ejs,art 等,但既然是搞C#的,那不妨试试看 Razor (之前的Admin.Core代码生成器也是基于razor模板引擎,也可以换其他的或者支持多种模板引擎),C#的语法写起来还是挺舒服的,并且其实还可以新建一个.net core 的项目添加页面后可以复制模板和模型到页面为自己的模板增加智能提示

模板引擎的使用

  • 项目中引用包:RazorEngine.NetCore


    • 原版只支持 framework,大佬打包的 netcore 版本
<ItemGroup>
  <PackageReference Include="RazorEngine.NetCore" Version="3.1.0" />
</ItemGroup>
  • 使用方式:指定模型,内容,模板名称即可
var code="模板内容";
var key="模板名";
//模型名称
var model=new DevProjectRazorRenderModel();
RazorEngine.Engine.Razor.RunCompile(new LoadedTemplateSource(code), key, model.GetType(), model);
  • 模板内容的写法


    • 我这里定义了固定模型,所以需要先什么模型的变量,这里指定了 gen
    • 模板语法文档
@{
var gen = Model as ZhonTai.Module.Dev.DevProjectRazorRenderModel;
}
  • 这里定义了一个公共的项目模型,后续增加项目模型字段的信息都无需更搞代码,生成即可,另外也可以再属性模型中添加字典等属性,即可灵魂的再模板中使用动态配置了
//模型渲染
var gen = new DevProjectRazorRenderModel()
{
    Project = Mapper.Map<DevProjectGetOutput>(project),
    Model = Mapper.Map<DevProjectModelGetOutput>(model),
    Fields = Mapper.Map<List<DevProjectModelFieldGetOutput>>(modelFields),
};

模板内容的生成

  • 这里分为了两部分,一个是文件路径,一个是文件内容
  • 因为要生成的路径可能也会包含一些模块或者模型的信息,所以可以将模板路径也使用模板生成,这里直接拼接一个模板即可
var pathCodeText = @"
@{
var gen = Model as ZhonTai.Module.Dev.DevProjectRazorRenderModel;
}
" + outPath;
//转换路径
var outPath = RazorCompile(gen, $"{project.Code}_{model.Code}_{tpl.Name}_Path.tpl", pathCodeText).Trim();

文末附完整代码

内容文件的下载

  • 因为是多模板组多模板,所以每次生成项目代码都基本是多个文件,一个个下载很明显不合理,所以可以将所有代码内容文件打包成压缩包进行下载,下面是核心压缩代码,无需引用包,暂时只在Windows中测试使用
  • 完整代码如下,做了一些文件和文件夹的判断处理,可自行封装
  • 通过GenerateAsync获取到文件信息后写入文件,再将目录进行打包返回即可
/// <summary>
/// 下载
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost]
public async Task<ActionResult> DownAsync(DevProjectGenGenerateInput input)
{
    var path = Path.Combine(AppContext.BaseDirectory, "DownCodes", DateTime.Now.ToString("yyyyMMddHHmmss"));
    var zipFileName = $"源码{DateTime.Now.ToString("yyyyMMddHHmmss")}.zip";
    var zipPath = Path.Combine(AppContext.BaseDirectory, "DownCodes", zipFileName);
    try
    {
        if (!Directory.Exists(path))
        {
            Directory.CreateDirectory(path);
        }
        //获取内容信息
        var codes = await GenerateAsync(input);
        foreach (var code in codes)
        {
            var codePath = Path.Combine(path, code.Path);
            var directory = Path.GetDirectoryName(codePath);
            if (!Directory.Exists(directory))
            {
                Directory.CreateDirectory(directory);
            }
            if (!File.Exists(codePath))
            {
                using (var fs = File.Open(codePath, FileMode.Create, FileAccess.ReadWrite))
                {
                    await fs.WriteAsync(Encoding.UTF8.GetBytes(code.Content));
                }
            }
        }
        ZipFile.CreateFromDirectory(path, zipPath);
        var bytes = await File.ReadAllBytesAsync(zipPath);
        return new FileContentResult(bytes, "application/zip")
        {
            FileDownloadName = zipFileName
        };
    }
    finally
    {
        if (Directory.Exists(path))
        {
            Directory.Delete(path, true);
        }
        if (File.Exists(zipPath))
        {
            File.Delete(zipPath);
        }
    }
}

前端对应需要支持文件下载,可以修改为生成对应下载URL,用GET请求直接打开新窗口进行下载

项目中前端页面的重点

整个框架主要使用者肯定是.net开发,如果没有写过 vue 项目,写起来的时候可能会有一些吃力,但因为现在有了代码生成器,大部分代码都可以生成,也可以做参考,所以这里只做一些关键点的说明

框架页面菜单的添加说明:具体可参考前文进行创建

  • 系统管理-添加视图->指定 vue 页面文件的路径,可再权限菜单中复用这个视图生成不同的路由地址
  • 接口管理-同步接口->将后端服务映射为权限点,对应后端功能权限
  • 权限管理-添加权限->添加菜单,功能点
  • 用户-角色->通过角色分配角色权限,用户管理角色

Vue 文档

有时间的话至少过一遍再开始,磨刀不误砍柴工

生命周期的一定花时间看看:
官方文档

页面中获取路由信息与页面跳转

以项目生成页面调整到预览页面为例

  • 项目生成页和预览页引入定义
//引入路由
import { useRoute, useRouter } from 'vue-router'

//路由信息
const route = useRoute()
//路由跳转
const router = useRouter()
  • 跳转到预览页
  router.push({
    path: '/dev/dev-project-gen/preview', query: {
      projectId: row.projectId,
      groupIds: row.groupIds_Values
    }
  })
  • 预览页获取生成列表传递的参数
//从路由中获取query参数
onMounted(() => {
  state.filter.projectId = route.query.projectId
  state.filter.groupIds = route.query.groupIds
})

左侧树/列表右侧预览实现

将左侧的列表列封装为一个组件,右侧如果简单可封装,也可以直接写到预览页面

这里参考框架中用户列表做的

<my-layout class="my-layout">
    <pane size="30" min-size="20" max-size="35">
      <div class="my-flex-column w100 h100">
        <group-template-menu :groupIds="state.filter.groupIds" :projectId="state.filter.projectId"
          @node-click="onNodeClick" select-first-node></group-template-menu>
      </div>
    </pane>
    <pane size="70" v-loading="state.loading">
      <div class="my-flex-column w100 h100">
          内容预览部分
      </div>
    </pane>
  </my-layout>

当然,封装了组件记得引入用到的组件

const GroupTemplateMenu = defineAsyncComponent(() => import('./components/dev-group-template-menu.vue'))
const MyLayout = defineAsyncComponent(() => import('/@/components/my-layout/index.vue'))

组件中获取传递的参数示例

interface Props {
  modelValue: number[] | null | undefined
  selectFirstNode: boolean,
  projectId: number,
  groupIds: number[]
}

const props = withDefaults(defineProps<Props>(), {
  modelValue: () => [],
  selectFirstNode: false,
  projectId: 0,
  groupIds: () => []
})

在预览左侧菜单中,我们可以看到一个标记的小图标,用来直接编辑模板

这个弹窗其实是直接引用了编辑模板的组件

<template>
...
<dev-template-form ref="devTemplateFormRef" :title="'编辑模板'"></dev-template-form>
...
</template>
<script>
...
// 引入组件
const DevTemplateForm = defineAsyncComponent(() => import('../../dev-template/components/dev-template-form.vue'))
//使用
const devTemplateFormRef = ref()
const editTemplate = (node, data) => {
  devTemplateFormRef.value.open({
    id: data.id
  })
}
...
</script>

defineExpose({
  open,
})

如上可以看到我们使用和组件同名的 const devTemplateFormRef = ref() 即可获取到组件引用

另外调用的方法可以查看 dev-template-form.vue 是开放了方法的

defineExpose({
  open,
})

下载压缩包文件

首先需要后端返回文件流,然后调用对应接口的时候指定format格式为blob,并创建下载连接点击即可

const genCode = async (row: DevProjectGenGetOutput) => {
  new DevProjectGenApi().down({ projectId: row.projectId, groupIds: row.groupIds_Values?.map(s => Number(s)) }, {
    loading: false,
    showErrorMessage: false,
    format: 'blob'
  })
    .then((res) => {
      const a = document.createElement('a');
      a.href = URL.createObjectURL(res as Blob);
      a.download = '源码.zip';
      a.click();
    });
}

后语

本文所有代码皆在
yimogit/Emo.Dev
仓库中可以找到,觉得有用的来个 Star 吧

基于前面代码生成器生成的功能模块上,周末花了两天完善项目生成,终于算是搞定

后面还需要逐步完善生成器,欢迎点个赞,留个言,交流指点一二

相关文档