应读者要求讲讲 DMA

发表于 2年以前  | 总阅读数:321 次

DMA 概念介绍

DMA 传输是由 CPU 发起的:CPU 会告诉 DMA 控制器,帮忙将 source 地方的数据搬到 dest 地方。CPU 发完指令之后,就不管了。具体怎么搬,何时搬,完全由 DMA 控制器决定。DMA 控制器搬运数据的方向有如下几种:

何时传输(DMA request lines)

因为 CPU 发起 DMA 传输的时候,并不知道当前是否具备传输条件,例如 source 设备是否有数据、dest 设备的 FIFO 是否空闲等等。那谁知道是否可以传输呢?设备!因此,需要设备和 DMA 控制器之间,有几条物理的连接线(称作DMA request,DRQ),用于通知 DMA 控制器可以开始传输了。

传输通道(DMA channels)

DMA 控制器可以同时进行的传输个数是有限的,每一个传输都需要使用到 DMA 物理通道。DMA 物理通道的数量决定了 DMA 控制器能够同时传输的任务量。

在软件上,DMA 控制器会为外设分配一个 DMA 虚拟通道,这个虚拟通道是根据 DMA request 信号来区分的。

传输参数

  • transfer size
  • transfer width
  • burst size

scatter gather

我们知道,一般情况下 DMA 传输只能处理在物理上连续的 buffer。但在有些场景下,我们需要将一些非连续的 buffer 拷贝到一个连续 buffer 中,这样的操作称作 scatter gather。

对于这种非连续的传输,大多时候都是通过软件,将传输分成多个连续的小块。但为了提高传输效率,有些 DMA controller 从硬件上支持了这种操作。

DMA 客户端设备驱动

我们先看下一个 DMA 客户端设备驱动涉及到的数据结构有哪些,然后再看下使用 dmaengine 内核框架的步骤。

数据结构

  • dma_slave_config

完成一次 DMA 传输所需要的参数:

struct dma_slave_config {
 /*
 指明传输的方向
 DMA_MEM_TO_MEM,memory到memory的传输;
    DMA_MEM_TO_DEV,memory到设备的传输;
    DMA_DEV_TO_MEM,设备到memory的传输;
    DMA_DEV_TO_DEV,设备到设备的传输。
 */
 enum dma_transfer_direction direction;
 /*
 传输方向是dev2mem或者dev2dev时,读取数据的位置(通常是固定的FIFO地址)。
 对mem2dev类型的channel,不需配置该参数(每次传输的时候会指定);
 */
 phys_addr_t src_addr;
 /*
 传输方向是mem2dev或者dev2dev时,写入数据的位置(通常是固定的FIFO地址)。
 对dev2mem类型的channel,不需配置该参数(每次传输的时候会指定);
 */
 phys_addr_t dst_addr;
 //src地址的宽度
 enum dma_slave_buswidth src_addr_width;
 //dst地址的宽度
 enum dma_slave_buswidth dst_addr_width;
 //src最大可传输的burst size
 u32 src_maxburst;
 //dst最大可传输的burst size
 u32 dst_maxburst;
 u32 src_port_window_size;
 u32 dst_port_window_size;
 u32 src_fifo_num;
 u32 dst_fifo_num;
 bool device_fc;
 /*
 外部设备通过slave_id告诉dma controller自己是谁(一般和某个request line对应)。
 很多dma controller并不区分slave,只要给它src、dst、len等信息,它就可以进行传输,因此slave_id可以忽略。
 而有些controller,必须清晰地知道此次传输的对象是哪个外设,就必须要提供slave_id了。
 */
 unsigned int slave_id;
};
  • dma_async_tx_descriptor

用于描述一次 DMA 传输:

struct dma_async_tx_descriptor {
 dma_cookie_t cookie;
 /*
 DMA_CTRL_开头的标记,包括:
 DMA_CTRL_REUSE,表明这个描述符可以被重复使用,直到它被清除或者释放;
 DMA_CTRL_ACK,如果该flag为0,表明暂时不能被重复使用。
 */
 enum dma_ctrl_flags flags; /* not a 'long' to pack with cookie */
 //该描述符的物理地址
 dma_addr_t phys;
 //对应的dma channel
 struct dma_chan *chan;
 /*
 controller driver提供的回调函数,用于把改描述符提交到待传输列表。
 通常由dma engine调用,client driver不会直接和该接口打交道。
 */
 dma_cookie_t (*tx_submit)(struct dma_async_tx_descriptor *tx);
 /*
 用于释放该描述符的回调函数。
 通常由dma engine调用,client driver不会直接和该接口打交道。
 */
 int (*desc_free)(struct dma_async_tx_descriptor *tx);
 //传输完成的回调函数(及其参数),由client driver提供。
 dma_async_tx_callback callback;
 dma_async_tx_callback_result callback_result;
 //传输完成的回调函数(及其参数),由client driver提供。
 void *callback_param;
 struct dmaengine_unmap_data *unmap;
#ifdef CONFIG_ASYNC_TX_ENABLE_CHANNEL_SWITCH
 struct dma_async_tx_descriptor *next;
 struct dma_async_tx_descriptor *parent;
 spinlock_t lock;
#endif
};

设备驱动使用 dmaengine 的步骤

一个设备驱动程序使用 dmaengine 的话一般步骤如下:

  1. 申请一个 DMA channel。
  2. 根据设备(slave)的特性,配置 DMA channel 的参数。
  3. 要进行 DMA 传输的时候,获取一个用于识别本次传输(transaction)的描述符(descriptor)。
  4. 将本次传输(transaction)提交给 DMA Engine 并启动传输。
  5. 等待传输(transaction)结束。

然后,重复3~5步。下面具体看下每一步的实现和相关接口:

  • 申请 DMA channel

任何设备驱动在开始 DMA 传输之前,都要申请一个 DMA channel(由“struct dma_chan”数据结构表示),可以通过如下的 API 申请 DMA channel:

struct dma_chan *dma_request_chan(struct device *dev, const char *name);

可以用如下接口释放申请得到的 DMA channel:

void dma_release_channel(struct dma_chan *chan);

  • 配置 DMA channel 的参数

设备驱动申请到一个为自己使用的 DMA channel 之后,需要根据自身的实际情况,以及 DMA controller 的能力,对该 channel 进行一些配置。设备驱动将它们填充到一个 struct dma_slave_config 变量中后,可以调用如下 API 将这些信息告诉给 DMA 控制器:

int dmaengine_slave_config(struct dma_chan *chan, struct dma_slave_config *config)

  • 获取传输描述

DMA 传输属于异步传输,在启动传输之前,设备驱动需要将此次传输的一些信息(例如src/dst的buffer、传输的方向等)提交给 DMA 控制器,DMA 控制器确认好后,返回一个描述符。此后,设备驱动就可以以该描述符为单位,控制并跟踪此次传输。设备驱动可以使用下面三个 API 获取传输描述符:

用于在“scatter gather buffers”列表和总线设备之间进行 DMA 传输:

struct dma_async_tx_descriptor *dmaengine_prep_slave_sg( struct dma_chan *chan, struct scatterlist *sgl, unsigned int sg_len, enum dma_data_direction direction, unsigned long flags);

用于音频等场景中,在进行一定长度的dma传输(buf_addr&buf_len)的过程中,每传输一定的byte(period_len),就会调用一次传输完成的回调函数:

struct dma_async_tx_descriptor *dmaengine_prep_dma_cyclic( struct dma_chan *chan, dma_addr_t buf_addr, size_t buf_len, size_t period_len, enum dma_data_direction direction);

可进行不连续的、交叉的DMA传输,通常用在图像处理、显示等场景中:

struct dma_async_tx_descriptor *dmaengine_prep_interleaved_dma( struct dma_chan *chan, struct dma_interleaved_template *xt, unsigned long flags);

  • 启动传输

获取传输描述符之后,设备驱动可以通过 dmaengine_submit 接口将该描述符放到传输队列上,然后调用 dma_async_issue_pending 接口,启动传输。

dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc);

void dma_async_issue_pending(struct dma_chan *chan);

由这两个 API 的特征可知,内核 DMA Engine 鼓励设备驱动一次提交多个传输,然后由 DMA 控制器统一完成这些传输。

  • 等待传输结束

传输请求被提交之后,设备驱动可以通过回调函数获取传输完成的消息,当然,也可以通过 dma_async_is_tx_complete 等 API,测试传输是否完成。如果等不及了,也可以使用 dmaengine_pause、dmaengine_resume、dmaengine_terminate_xxx 等 API,暂停、终止传输。

eDMA(DMA 控制器) 驱动

DMA 控制器驱动主要负责抽象 eDMA 硬件,管理 DMA channel,以 channel 为操作对象,响应 设备驱动的传输请求,并控制 eDMA 驱动,执行传输。

内核的 dmaengine framework 提供了一套 DMA 控制器的开发框架,如下图所示:

  1. 使用 struct dma_device 抽象 eDMA,eDMA 驱动只要填充该结构中必要的字段。
  2. 使用struct dma_chan(即图中的 DCn)抽象物理的 DMA channel(即图中的 CHn),DCn 和 CHn 一一对应。
  3. 基于物理的 DMA channel,使用 struct virt_dma_chan 抽象出虚拟的 dma channel(即图中的 VCx)。多个虚拟 channel 可以共享一个物理 channel,并在这个物理 channel 上进行分时传输。

下面我们看下 dma_device,dma_chan,virt_dma_chan 的数据结构。

数据结构

  • dma_device

主要用于抽象 eDMA:

struct dma_device {
 //一个链表头,用于保存该controller支持的所有dma channel
 //在初始化的时候,dma controller driver首先要调用 INIT_LIST_HEAD 初始化它,然后调用 list_add_tail 将所有的 channel 添加到该链表头中。
 unsigned int chancnt;
 unsigned int privatecnt;
 struct list_head channels;
  ......
 /*
 一个bitmap,用于指示该dma controller所具备的能力
    DMA_MEMCPY,可进行memory copy;
    DMA_MEMSET,可进行memory set;
    DMA_SG,可进行 scatter list 传输;
    DMA_CYCLIC,可进行cyclic类的传输;
    DMA_INTERLEAVE,可进行交叉传输;
 */
 dma_cap_mask_t  cap_mask;
  ......
 int dev_id;
 struct device *dev;

 //表示该controller支持哪些宽度的src类型。具体可参考 enum dma_slave_buswidth 的定义
 u32 src_addr_widths;
 //表示该controller支持哪些宽度的dst类型。具体可参考 enum dma_slave_buswidth 的定义
 u32 dst_addr_widths;
 /*
 表示该controller支持哪些传输方向
 包括DMA_MEM_TO_MEM、DMA_MEM_TO_DEV、DMA_DEV_TO_MEM、DMA_DEV_TO_DEV,具体可参考enum dma_transfer_direction的定义
 */
 u32 directions;
 //支持的最大的burst传输的size
 u32 max_burst;
 //指示该controller的传输描述可否可重复使用
 bool descriptor_reuse;
 enum dma_residue_granularity residue_granularity;

 //申请/释放 dma channel 的时候,dmaengine会调用dma controller driver相应的alloc/free回调函数,以准备相应的资源。
 int (*device_alloc_chan_resources)(struct dma_chan *chan);
 void (*device_free_chan_resources)(struct dma_chan *chan);

 //设备驱动通过 dmaengine_prep_xxx API 获取传输描述符的时候,damengine则会直接回调 eDMA 驱动相应的 device_prep_dma_xxx 接口。
 struct dma_async_tx_descriptor *(*device_prep_dma_memcpy)(
  struct dma_chan *chan, dma_addr_t dst, dma_addr_t src,
  size_t len, unsigned long flags);
  ......
 struct dma_async_tx_descriptor *(*device_prep_dma_imm_data)(
  struct dma_chan *chan, dma_addr_t dst, u64 data,
  unsigned long flags);

 //设备驱动调用 dmaengine_slave_config 配置 dma channel 的时候,dmaengine 会调用该回调函数,交给 eDMA 驱动处理。
 int (*device_config)(struct dma_chan *chan,
        struct dma_slave_config *config);

 //设备驱动调用 dmaengine_pause、dmaengine_resume、dmaengine_terminate_xxx 等API的时候,dmaengine 会调用相应的回调函数,交给 eDMA 驱动处理。
 int (*device_pause)(struct dma_chan *chan);
  ......
 void (*device_synchronize)(struct dma_chan *chan);

 enum dma_status (*device_tx_status)(struct dma_chan *chan,
         dma_cookie_t cookie,
         struct dma_tx_state *txstate);
 //设备驱动调用 dma_async_issue_pending 启动传输的时候,会调用调用该回调函数。
 void (*device_issue_pending)(struct dma_chan *chan);
};
  • dma_chan

主要用于抽象物理的 DMA channel:

struct dma_chan {
 //指向该channel所在的dma controller
 struct dma_device *device;
 //设备驱动以该 channel 为操作对象获取传输描述符时,eDMA 驱动返回给设备的最后一个cookie。
 dma_cookie_t cookie;
 //在这个 channel 上最后一次完成的传输的 cookie。eDMA 驱动可以在传输完成时调用辅助函数 dma_cookie_complete 设置它的 value。
 dma_cookie_t completed_cookie;

 /* sysfs */
 int chan_id;
 struct dma_chan_dev *dev;

 //链表 node,用于将该 channel 添加到 dma_device 的 channel 列表中
 struct list_head device_node;
 struct dma_chan_percpu __percpu *local;
 int client_count;
 int table_count;

 /* DMA router */
 struct dma_router *router;
 void *route_data;

 void *private;
};
  • virt_dma_chan

主要用于抽象一个虚拟的 DMA channel:

struct virt_dma_chan {
 //一个 struct dma_chan 类型的变量,用于和设备驱动打交道(屏蔽物理channel和虚拟channel的差异)。
 struct dma_chan chan;
 //一个 tasklet,用于等待该虚拟 channel 上传输的完成(由于是虚拟channel,传输完成与否只能由软件判断)。
 struct tasklet_struct task;
 void (*desc_free)(struct virt_dma_desc *);

 spinlock_t lock;

 /* protected by vc.lock */
 //用于保存不同状态的虚拟 channel 描述符(struct virt_dma_desc,仅仅对 struct dma_async_tx_descriptor 做了一个简单的封装)。
 struct list_head desc_allocated;
 struct list_head desc_submitted;
 struct list_head desc_issued;
 struct list_head desc_completed;

 struct virt_dma_desc *cyclic;
};

eDMA 使用 dmaengine 的接口

damengine 直接向 DMA 控制器驱动提供的API并不多,主要包括:

  • struct dma_device 变量的注册和注销接口:

int dma_async_device_register(struct dma_device *device);

void dma_async_device_unregister(struct dma_device *device);

  • cookie 有关的辅助接口:

初始化dma channel中的cookie、completed_cookie字段:

static inline void dma_cookie_init(struct dma_chan *chan)

为指针的传输描述(tx)分配一个cookie:

static inline dma_cookie_t dma_cookie_assign(struct dma_async_tx_descriptor *tx)

当某一个传输(tx)完成的时候,可以调用该接口,更新该传输所对应channel的completed_cookie字段:

static inline void dma_cookie_complete(struct dma_async_tx_descriptor *tx)

获取指定channel(chan)上指定cookie的传输状态:

static inline enum dma_status dma_cookie_status(struct dma_chan *chan, dma_cookie_t cookie, struct dma_tx_state *state)

eDMA 使用 dmaengine 的步骤

  1. 定义一个 struct dma_device 变量,并根据实际的硬件情况,填充其中的关键字段。
  2. 根据 eDMA 支持的 channel 个数,为每个 channel 定义一个 struct dma_chan 变量,进行必要的初始化后,将每个 channel 都添加到 struct dma_device 变量的 channels 链表中。
  3. 根据硬件特性,实现 struct dma_device 变量中必要的回调函数(device_alloc_chan_resources/device_free_chan_resources、device_prep_dma_xxx、device_config、device_issue_pending等等)。
  4. 调用 dma_async_device_register 将 struct dma_device 变量注册到 kernel 中。
  5. 当 DMA 客户端设备驱动申请 dma channel 时(例如通过 device tree 中的 dma 节点获取),dmaengine core 会调用 eDMA 驱动的 device_alloc_chan_resources 函数,controller driver需要在这个接口中奖该channel的资源准备好。
  6. 当 DMA 客户端设备驱动配置某个 dma channel 时,dmaengine core 会调用 eDMA 驱动的 device_config 函数,eDMA 驱动需要在这个函数中将 DMA 客户端设备驱动想配置的内容准备好,以便进行后续的传输。
  7. DMA 客户端设备驱动开始一个传输之前,会把传输的信息通过 dmaengine_prep_slave_xxx 接口交给eDMA 驱动,eDMA 驱动需要在对应的 device_prep_dma_xxx 回调中,将这些要传输的内容准备好,并返回给DMA 客户端设备驱动一个传输描述符。
  8. 然后,DMA 客户端设备驱动会调用 dmaengine_submit 将该传输提交给 eDMA 驱动,此时 dmaengine 会调用 eDMA 驱动为每个传输描述符所提供的 tx_submit 回调函数,eDMA 驱动需要在这个函数中将描述符挂到该 channel 对应的传输队列中。
  9. DMA 客户端设备驱动开始传输时,会调用 dma_async_issue_pending,eDMA 驱动需要在对应的回调函数(device_issue_pending)中,依次将队列上所有的传输请求提交给硬件。

Dynamic DMA mapping

待续...

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/FrYAlidkm3C8wk3ft_a56g

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237229次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8063次阅读
 目录