硬汉嵌入式论坛

 找回密码
 立即注册
查看: 8376|回复: 27
收起左侧

[Cache] 言简意赅的介绍M7内核的Cache工作流程,摸爬滚打半年的经验总结

  [复制链接]

1万

主题

6万

回帖

10万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
106660
QQ
发表于 2018-11-6 00:23:46 | 显示全部楼层 |阅读模式
说明:
    初学M7的Cache时,经常是ARM的手册和ST的手册看了一遍又一遍,虽然每次看,每次都有收获,但是一直无法形成系统的认识,说到某一个知识点也明白,但是具体到读写操作的时候是怎么个流程,就懵逼了,也是心里烦躁,最近脑子开窍了些,特此分享下经验。

当前的认识能力有限,有不对的地方,欢迎批评指正。

一、引出问题:
    当前芯片厂商出的M7内核芯片基本都做了一级Cache支持,Cache又分数据缓存D-Cache和指令缓冲I-Cache,对于指令缓冲,用户不用管,这里主要说的是数据缓存D-Cache。以STM32H7为例,主频是400MHz,除了TCM和Cache以400MHz工作,其它AXI SRAM,SRAM1,SRAM2等都是以200MHz工作。数据缓存D-Cache就是解决CPU加速访问SRAM。

    如果每次CPU要读写SRAM区的数据,都能够在Cache里面进行,自然是最好的,实现了200MHz到400MHz的飞跃,实际是做不到的,因为数据Cache只有16KB大小,总有用完的时候。

对于使能了Cache的SRAM区,要分读写两种情况考虑。
读操作:
如果CPU要读取的SRAM区数据在Cache中已经加载好,这就叫读命中(Cache hit),如果Cache里面没有怎么办,这就是所谓的读Cache Miss。

写操作:
如果CPU要写的SRAM区数据在Cache中已经开辟了对应的区域(专业词汇叫Cache Line,以32字节为单位),这就叫写命中(Cache hit),如果Cache里面没有开辟对应的区域怎么办,这就是所谓的写Cache Miss。

QQ截图20181105172020.png


二、支持的Cache配置:
Cache的配置是通过MPU来设置的,通常只用到下几种方式。
QQ截图20181105122432.png

其中的TEX是用来设置Cache策略的,C是Cache,B是缓冲用来配合Cache设置的,而S是共享,用来解决多总线或者多核访问时的同步问题。MPU配置的时候,最主要的也是配置这几个参数。

Cache支持的策略有如下四种:
QQ截图20181105123637.png

有了这四种方式,就可以在正式进入本帖的主题,Cache的读写操作是如何工作的,下面分这四种情况做一 一介绍。

三、四种Cache(MPU)配置的读写操作流程说明
1、 Non-cacheable
这个最好理解,就是正常的读写操作,无Cache。

(1)对应四种MPU配置如下:
TEX = 000 C=0 B=0  S=忽略此位,强制为共享
TEX = 000 C=0 B=1  S=忽略此位,强制为共享
TEX = 001 C=0 B=0  S=0
TEX = 001 C=0 B=0  S=1

2、Write through, read allocate,no write allocate
注意,M7内核只要开启了Cache,read allocate就是开启的。

(1)使能了此配置的SRAM缓冲区写操作
    如果CPU要写的SRAM区数据在Cache中已经开辟了对应的区域,那么会同时写到Cache里面和SRAM里面;如果没有,就用到配置no write allocate了,意思就是CPU会直接往SRAM里面写数据,而不再需要在Cache里面开辟空间了。

    在写Cache命中的情况下,这个方式的优点是Cache和SRAM的数据同步更新了,没有多总线访问造成的数据一致性问题。缺点也明显,Cache在写操作上无法有效发挥性能。

(2)使能了此配置的SRAM缓冲区读操作
    如果CPU要读取的SRAM区数据在Cache中已经加载好,就可以直接从Cache里面读取。如果没有,就用到配置read allocate了,意思就是在Cache里面开辟区域,将SRAM区数据加载进来,后续的操作,CPU可以直接从Cache里面读取,从而时间加速。

    安全隐患,如果Cache命中的情况下,DMA写操作也更新了SRAM区的数据,CPU直接从Cache里面读取的数据就是错误的。

(3)对应两种MPU配置如下:
TEX = 000 C=1 B=0  S=1
TEX = 000 C=1 B=0  S=0

3、Write back, read allocate,no write allocate
注意,M7内核只要开启了Cache,read allocate就是开启的。

(1)使能了此配置的SRAM缓冲区写操作
    如果CPU要写的SRAM区数据在Cache中已经开辟了对应的区域,那么会写到Cache里面,而不会立即更新SRAM;如果没有,就用到配置no write allocate了,意思就是CPU会直接往SRAM里面写数据,而不再需要在Cache里面开辟空间了。

    安全隐患,如果Cache命中的情况下,此时仅Cache更新了,而SRAM没有更新,那么DMA直接从SRAM里面读出来的就是错误的。

(2)使能了此配置的SRAM缓冲区读操作
   如果CPU要读取的SRAM区数据在Cache中已经加载好,就可以直接从Cache里面读取。如果没有,就用到配置read allocate了,意思就是在Cache里面开辟区域,将SRAM区数据加载进来,后续的操作,CPU可以直接从Cache里面读取,从而时间加速。

    安全隐患,如果Cache命中的情况下,DMA写操作也更新了SRAM区的数据,CPU直接从Cache里面读取的数据就是错误的。

(3)对应两种MPU配置如下:
TEX = 000 C=1 B=1  S=1
TEX = 000 C=1 B=1  S=0

4、Write back, read allocate,write allocate
注意,M7内核只要开启了Cache,read allocate就是开启的。

(1)使能了此配置的SRAM缓冲区写操作
    如果CPU要写的SRAM区数据在Cache中已经开辟了对应的区域,那么会写到Cache里面,而不会立即更新SRAM;如果没有,就用到配置write allocate了,意思就是CPU写到往SRAM里面的数据,会同步在Cache里面开辟一个空间将SRAM中写入的数据加载进来,如果此时立即读此SRAM区,那么就会有很大的速度优势。

    安全隐患,如果Cache命中的情况下,此时仅Cache更新了,而SRAM没有更新,那么DMA直接从SRAM里面读出来的就是错误的。

(2)使能了此配置的SRAM缓冲区读操作
    如果CPU要读取的SRAM区数据在Cache中已经加载好,就可以直接从Cache里面读取。如果没有,就用到配置read allocate了,意思就是在Cache里面开辟区域,将SRAM区数据加载进来,后续的操作,CPU可以直接从Cache里面读取,从而时间加速。

    安全隐患,如果Cache命中的情况下,DMA写操作也更新了SRAM区的数据,CPU直接从Cache里面读取的数据就是错误的。

    这个配置被誉可以最大程度发挥Cache性能,不过具体应用仍需具体分析。

(3)对应两种MPU配置如下:
TEX = 001 C=1 B=1  S=1
TEX = 001 C=1 B=1  S=0

5、共享配置是个隐形的大坑
这里以STM32H7为例进行说明,STM32H7编程手册对其的描述是多核共享。


而H7的应用笔记对齐的描述是开启共享基本等同于关闭Cache。


实际测试下面四种开Cache的情况,开关共享对缓冲区的大批量数据的读操作影响很大,基本差出两倍,而写操作基本没有影响,也许这就是所谓的多总线同步读造成的。
另外共享开关仅对开启了Cache的情况下有影响,而对于关闭了Cache的情况是没有影响的,开不开没关系。

6、总结这几种方式的几个关键知识点
(1)Cortex-M7内核的L1 Cache由多行内存区组成,每行有32字节,每行都配有一个地址标签。数据缓冲DCache是每4行为一组,称为4-way set associative。而指令缓冲区ICache是2行为一组,这样节省地址标签,不用每个行都标记一个地址。

(2)对于读操作,只有在第1次访问指定地址时才会加载到Cache,而写操作的话,可以直接写到内存中(write-through模式)或者放到Cache里面,后面再写入(write-back模式)。

(3)如果采用的是Write back,Cache line会被标为dirty,等到此行被evicted时,才会执行实际的写操作,将Cache Line里面的数据写入到相应的存储区。

(4)Cache命中是访问的地址落在了给定的Cache Line里面,所以硬件需要做少量的地址比较工作,以检查此地址是否被缓存。如果命中了,将用于缓存读操作或者写操作。如果没有命中,则分配和标记新行,填充新的读写操作。如果所有行都分配完毕了,Cache控制器将支持eviction操作。根据Cache Line替换算法,一行将被清除Clean,无效化Invalid或者重新配置。数据缓存和指令缓存是采用的伪随机替换算法。

(5)Cache支持的4种基本操作,使能,禁止,清空和无效化。Clean清空操作是将Cache Line中标记为dirty的数据写入到内存里面,而无效化Invalid是将Cache Line标记为无效,即删除操作。


四、面对这种繁冗复杂的Cache配置,推荐方式和安全隐患解决如下(以H7为例):
(1)推荐使用128KB的TCM作为主RAM区,其它的专门用于大缓冲和DMA操作等。
(2)Cache问题主要是CPU和DMA都操作这个缓冲区时容易出现,使用时要注意。
(3)Cache配置的选择,优先考虑的是WB,然后是WT和关闭Cache,其中WB和WT的使用中可以配合ARM提供的如下几个函数解决上面说到的隐患问题。但不是万能的,在不起作用的时候,直接暴力选择函数SCB_CleanInvlaidateDCache解决。关于这个问题,在分别配置以太网MAC的描述符缓冲区,发送缓冲区和接收缓冲区时尤其突出。
QQ截图20181105134811.png










评分

参与人数 3金币 +60 收起 理由
2BAM + 20 很给力!
suozhang + 20 很给力!
byccc + 20 666

查看全部评分

回复

使用道具 举报

32

主题

262

回帖

363

积分

高级会员

积分
363
发表于 2018-11-6 07:53:25 | 显示全部楼层
很详细,,,
回复

使用道具 举报

1

主题

13

回帖

16

积分

新手上路

积分
16
发表于 2018-11-6 10:56:05 | 显示全部楼层
回复

使用道具 举报

36

主题

2039

回帖

2147

积分

至尊会员

积分
2147
发表于 2018-11-6 13:51:41 | 显示全部楼层


收藏。
Ever tried. Ever failed. No matter. Try Again. Fail again. Fail better.
回复

使用道具 举报

39

主题

928

回帖

1050

积分

至尊会员

积分
1050
发表于 2018-11-6 14:09:09 | 显示全部楼层
回复

使用道具 举报

5

主题

196

回帖

211

积分

高级会员

积分
211
发表于 2018-11-6 14:27:37 | 显示全部楼层
本帖最后由 alexyzhov 于 2018-11-6 14:29 编辑

M7暴露了很多体系结构的知识给MCU程序员,而MCU程序员往往对现代计算机的体系结构知之甚少。。推荐阅读《计算机组成与设计:硬件软件接口》,补充一些how it works的原理性知识,对认识现在越来越复杂的MCU、MPU系统也有好处

当然,对于更硬核的开发者,比如对CPU设计感兴趣的工程师,还有编译器程序员,《计算机体系结构:量化研究方法》是上一本的进阶读物…
回复

使用道具 举报

6

主题

14

回帖

32

积分

新手上路

积分
32
发表于 2018-11-28 13:05:43 | 显示全部楼层
你好,请问如果设置cache属性为WT的,那么在写入得时候即写到cache又写到主存储器中,那么对写来说,性能是没有提升的,是这样么?
回复

使用道具 举报

1万

主题

6万

回帖

10万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
106660
QQ
 楼主| 发表于 2018-11-29 11:38:33 | 显示全部楼层
laogan666 发表于 2018-11-28 13:05
你好,请问如果设置cache属性为WT的,那么在写入得时候即写到cache又写到主存储器中,那么对写来说,性能是 ...

对。
回复

使用道具 举报

18

主题

321

回帖

375

积分

高级会员

积分
375
发表于 2019-5-5 13:47:54 | 显示全部楼层
硬汉哥,请问一下你这个表是哪里找到的?
回复

使用道具 举报

1万

主题

6万

回帖

10万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
106660
QQ
 楼主| 发表于 2019-5-5 16:07:49 | 显示全部楼层
王海靖 发表于 2019-5-5 13:47
硬汉哥,请问一下你这个表是哪里找到的?

编程手册。
回复

使用道具 举报

609

主题

3047

回帖

4894

积分

至尊会员

积分
4894
发表于 2019-5-5 17:14:07 | 显示全部楼层
东西太多,貌似看着头晕
回复

使用道具 举报

10

主题

140

回帖

170

积分

初级会员

积分
170
发表于 2019-7-23 08:24:47 | 显示全部楼层
其实看完了还是一头雾水,因为没有结合HAL库代码讲解,根本不知道各个模式如何去设置。
回复

使用道具 举报

1万

主题

6万

回帖

10万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
106660
QQ
 楼主| 发表于 2019-7-23 08:36:52 | 显示全部楼层
lvehe 发表于 2019-7-23 08:24
其实看完了还是一头雾水,因为没有结合HAL库代码讲解,根本不知道各个模式如何去设置。

初学是这样的,我当年搞了半年才折腾明白了一丢丢。

看HAL的配置没用,就是简单的API配置而已,只会让人越看越懵逼,不信你试试。
回复

使用道具 举报

23

主题

1403

回帖

1472

积分

至尊会员

积分
1472
发表于 2019-7-23 08:47:29 | 显示全部楼层
eric2013 发表于 2019-7-23 08:36
初学是这样的,我当年搞了半年才折腾明白了一丢丢。

看HAL的配置没用,就是简单的API配置而已,只会让 ...

这四个补上就跟HAL对应上了。

B = MPU_InitStruct.IsBufferable;
C=  MPU_InitStruct.IsCacheable;
S = MPU_InitStruct.IsShareable;
TEX = MPU_InitStruct.TypeExtField;
代码不规范,亲人两行泪!
回复

使用道具 举报

1万

主题

6万

回帖

10万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
106660
QQ
 楼主| 发表于 2019-7-23 08:49:37 | 显示全部楼层
missfox 发表于 2019-7-23 08:47
这四个补上就跟HAL对应上了。

B = MPU_InitStruct.IsBufferable;

这个得看教程,在教程MPU章节呢
http://www.armbbs.cn/forum.php?mod=viewthread&tid=86980&extra=page%3D1

2019-07-23_091400.jpg
回复

使用道具 举报

4

主题

139

回帖

151

积分

初级会员

积分
151
QQ
发表于 2019-10-12 09:59:46 | 显示全部楼层
c,s的描述看明白了,b(缓冲)能再讲讲吗?目前看介绍是开启wb时要开启缓冲,但是他缓冲的是什么东西那?
回复

使用道具 举报

4

主题

139

回帖

151

积分

初级会员

积分
151
QQ
发表于 2019-10-12 10:29:56 | 显示全部楼层
s的意思是不是
cpu在读内存的时候,Cache控制器会去看一下。其他cpu有没有写过这块内存,而没有刷cache,导致开启share比关闭会慢那么多?
回复

使用道具 举报

1万

主题

6万

回帖

10万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
106660
QQ
 楼主| 发表于 2019-10-12 12:22:06 | 显示全部楼层
qgyhd1234 发表于 2019-10-12 09:59
c,s的描述看明白了,b(缓冲)能再讲讲吗?目前看介绍是开启wb时要开启缓冲,但是他缓冲的是什么东西那?

b是配合c使用的。
回复

使用道具 举报

1万

主题

6万

回帖

10万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
106660
QQ
 楼主| 发表于 2019-10-12 12:23:08 | 显示全部楼层
qgyhd1234 发表于 2019-10-12 10:29
s的意思是不是
cpu在读内存的时候,Cache控制器会去看一下。其他cpu有没有写过这块内存,而没有刷cache, ...

一般情况下关闭S就行。

一些手册中对S的理解就是:开了S就相当于关闭了C,也就是关闭了Cache
回复

使用道具 举报

0

主题

84

回帖

84

积分

初级会员

积分
84
发表于 2019-10-14 09:37:27 | 显示全部楼层
学习。。。
回复

使用道具 举报

4

主题

139

回帖

151

积分

初级会员

积分
151
QQ
发表于 2019-12-13 13:20:09 | 显示全部楼层
eric2013 发表于 2019-10-12 12:23
一般情况下关闭S就行。

一些手册中对S的理解就是:开了S就相当于关闭了C,也就是关闭了Cache

E:代表独占(Exclusive)
S:代表共享(Shared)

无论是独占状态还是共享状态,缓存里面的数据都是“干净”的。这个“干净”,自然对应的是前面所说的“脏”的,也就是说,这个时候,Cache Block 里面的数据和主内存里面的数据是一致的。
那么“独占”和“共享”这两个状态的差别在哪里呢?这个差别就在于,在独占状态下,对应的 Cache Line 只加载到了当前 CPU 核所拥有的 Cache 里。其他的 CPU 核,并没有加载对应的数据到自己的 Cache 里。这个时候,如果要向独占的 Cache Block 写入数据,我们可以自由地写入数据,而不需要告知其他 CPU 核。
在独占状态下的数据,如果收到了一个来自于总线的读取对应缓存的请求,它就会变成共享状态。这个共享状态是因为,这个时候,另外一个 CPU 核心,也把对应的 Cache Block,从内存里面加载到了自己的 Cache 里来。
而在共享状态下,因为同样的数据在多个 CPU 核心的 Cache 里都有。所以,当我们想要更新 Cache 里面的数据的时候,不能直接修改,而是要先向所有的其他 CPU 核心广播一个请求,要求先把其他 CPU 核心里面的 Cache,都变成无效的状态,然后再更新当前 Cache 里面的数据。这个广播操作,一般叫作 RFO(Request For Ownership),也就是获取当前对应 Cache Block 数据的所有权。

在看MESI协议的时候看到了共享的解释,不过好像和H7的策略不一样,MESI协议在写时会有影响,但是读很快,而H7刚好相反
回复

使用道具 举报

3

主题

1222

回帖

1231

积分

至尊会员

积分
1231
发表于 2019-12-14 18:40:18 | 显示全部楼层
  1.         /* 配置以太网收发描述符部分为Device */
  2.         MPU_InitStruct.Enable = MPU_REGION_ENABLE;
  3.         MPU_InitStruct.BaseAddress = 0x30040000;
  4.         MPU_InitStruct.Size = MPU_REGION_SIZE_256B;
  5.         MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
  6.         MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
  7.         MPU_InitStruct.IsCacheable  = MPU_ACCESS_NOT_CACHEABLE;
  8.         MPU_InitStruct.IsShareable  = MPU_ACCESS_SHAREABLE;
  9.         MPU_InitStruct.Number = MPU_REGION_NUMBER2;
  10.         MPU_InitStruct.SubRegionDisable = 0x0;
  11.         MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
  12.         MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;

  13.         HAL_MPU_ConfigRegion(&MPU_InitStruct);
复制代码

  1.         /* 配置FMC扩展IO的MPU属性为Device或者Strongly Ordered */
  2.         MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
  3.         MPU_InitStruct.BaseAddress      = 0x60000000;
  4.         MPU_InitStruct.Size             = ARM_MPU_REGION_SIZE_64KB;       
  5.         MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
  6.         MPU_InitStruct.IsBufferable     = MPU_ACCESS_BUFFERABLE;
  7.         MPU_InitStruct.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE;        /* 不能用MPU_ACCESS_CACHEABLE;会出现2次CS、WE信号 */
  8.         MPU_InitStruct.IsShareable      = MPU_ACCESS_NOT_SHAREABLE;
  9.         MPU_InitStruct.Number           = MPU_REGION_NUMBER1;
  10.         MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL0;
  11.         MPU_InitStruct.SubRegionDisable = 0x00;
  12.         MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_ENABLE;
  13.        
  14.         HAL_MPU_ConfigRegion(&MPU_InitStruct);
复制代码


这两块配置就是一个共享了,一个没有共享, 这个在H7上面写驱动, 有没有一个标准供初学者参考?
回复

使用道具 举报

1万

主题

6万

回帖

10万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
106660
QQ
 楼主| 发表于 2019-12-14 21:01:40 | 显示全部楼层
morning_enr6U 发表于 2019-12-14 18:40
这两块配置就是一个共享了,一个没有共享, 这个在H7上面写驱动, 有没有一个标准供初学者参考?:l ...

V7用户手册对应章节。
回复

使用道具 举报

14

主题

99

回帖

141

积分

初级会员

积分
141
发表于 2020-3-1 17:23:12 | 显示全部楼层
初学者还是关闭了吧,懵逼
回复

使用道具 举报

1万

主题

6万

回帖

10万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
106660
QQ
 楼主| 发表于 2020-3-1 18:08:48 | 显示全部楼层
LR215 发表于 2020-3-1 17:23
初学者还是关闭了吧,懵逼

上点心就会了。
回复

使用道具 举报

0

主题

6

回帖

6

积分

新手上路

积分
6
发表于 2024-2-6 21:51:54 | 显示全部楼层
个人理解:
- C=0时等同于Non-cacheable。
- 启用缓存时(C=1), write through/write back是由B(Bufferable)位决定的,B=0时用write through, B=1用write back,两个策略的区别在于前者CPU写的SRAM数据如果在Cache有开辟对应的区域(cache hit),那么写进Cache的时候同时也会写到SRAM,后者不会,因此后者在读cache的时候如果dma有更新SRAM,数据就会不一致。
- read/write allocate的用处是在CPU cache miss,与SRAM交互数据(写入SRAM或者从SRAM读)时,决定数据是否也同步到Cache的,如果No allocate就不会同步回Cache,C=1下的四种策略均有read allocate, 而write allocate对应TEX,TEX=000为no write allocate, TEX=001为write allocate.
- 看表格,write through(B=0)和write allocate(TEX=001)似乎是不兼容的(即Implementation defined attributes那一行),一个是CPU在cache hit时写cache同时更新SRAM,另一个是在CPU cache miss时写SRAM同时更新到cache,  这个不知道怎么理解

回复

使用道具 举报

0

主题

6

回帖

6

积分

新手上路

积分
6
发表于 2024-2-6 21:55:34 | 显示全部楼层
对四种策略(write through, write back, read allocate, write allocate)的个人理解:

- C=0时等同于Non-cacheable。
- 启用缓存时(C=1), write through/write back是由B(Bufferable)位决定的,B=0时用write through, B=1用write back,两个策略的区别在于前者CPU写的SRAM数据如果在Cache有开辟对应的区域(cache hit),那么写进Cache的时候同时也会写到SRAM,后者不会,因此后者在读cache的时候如果dma有更新SRAM,数据就会不一致。
- read/write allocate的用处是在CPU cache miss,与SRAM交互数据(写入SRAM或者从SRAM读)时,决定数据是否也同步到Cache的,如果No allocate就不会同步回Cache,C=1下的四种策略均有read allocate, 而write allocate对应TEX,TEX=000为no write allocate, TEX=001为write allocate.
- 看表格,write through(B=0)和write allocate(TEX=001)似乎是不兼容的,一个是CPU在cache hit时写cache同时更新SRAM,另一个是在CPU cache miss时写SRAM同时更新到cache, 个人理解如果两个属性都有,cache初始是空的,系统第一次写肯定cache miss,这时候是写SRAM再同步到cache,后面写就都是cache hit了,如果write through的话就会写cache同时也会写到SRAM,这样write allocate反而就没什么用了。

回复

使用道具 举报

0

主题

6

回帖

6

积分

新手上路

积分
6
发表于 2024-2-7 09:42:42 | 显示全部楼层
二五 发表于 2024-2-6 21:55
对四种策略(write through, write back, read allocate, write allocate)的个人理解:

- C=0时等同于N ...

多发了一楼,问下怎么删除
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|小黑屋|Archiver|手机版|硬汉嵌入式论坛

GMT+8, 2024-4-29 06:13 , Processed in 0.331914 second(s), 32 queries .

Powered by Discuz! X3.4 Licensed

Copyright © 2001-2023, Tencent Cloud.

快速回复 返回顶部 返回列表