zynq PS侧DMA驱动

linux中,驱动必然会有驱动对应的设备类型。在linux4.4版本中,其设备是以设备树的形式展现的。

PS端设备树的devicetree表示如下

324         dmac_s: [email protected] {
325             compatible = "arm,pl330", "arm,primecell";
326             reg = <0xf8003000 0x1000>;
327             interrupt-parent = <&intc>;
328             interrupt-names = "abort", "dma0", "dma1", "dma2", "dma3",
329                 "dma4", "dma5", "dma6", "dma7";
330             interrupts = <0 13 4>,
331                          <0 14 4>, <0 15 4>,
332                          <0 16 4>, <0 17 4>,
333                          <0 40 4>, <0 41 4>,
334                          <0 42 4>, <0 43 4>;
335             #dma-cells = <1>;
336             #dma-channels = <8>;
337             #dma-requests = <4>;
338             clocks = <&clkc 27>;
339             clock-names = "apb_pclk";
340         };  

<drivers/of/platform.c>这个文件根据设备树信息创建设备信息,在驱动程序注册时就可以找到该设备信息,执行probe函数。

zynq下dma的设备channel如下:

 [email protected]:/sys/class/dma# ls
dma0chan0  dma0chan2  dma0chan4  dma0chan6  dma1chan0
dma0chan1  dma0chan3  dma0chan5  dma0chan7
[email protected]:/sys/class/dma# ll
total 0
drwxr-xr-x  2 root root 0 1970-01-01 00:00 ./
drwxr-xr-x 50 root root 0 1970-01-01 00:00 ../
lrwxrwxrwx  1 root root 0 1970-01-01 00:00 dma0chan0 -> ../../devices/soc0/amba/f8003000.dmac/dma/dma0chan0/
lrwxrwxrwx  1 root root 0 1970-01-01 00:00 dma0chan1 -> ../../devices/soc0/amba/f8003000.dmac/dma/dma0chan1/
lrwxrwxrwx  1 root root 0 1970-01-01 00:00 dma0chan2 -> ../../devices/soc0/amba/f8003000.dmac/dma/dma0chan2/
lrwxrwxrwx  1 root root 0 1970-01-01 00:00 dma0chan3 -> ../../devices/soc0/amba/f8003000.dmac/dma/dma0chan3/
lrwxrwxrwx  1 root root 0 1970-01-01 00:00 dma0chan4 -> ../../devices/soc0/amba/f8003000.dmac/dma/dma0chan4/
lrwxrwxrwx  1 root root 0 1970-01-01 00:00 dma0chan5 -> ../../devices/soc0/amba/f8003000.dmac/dma/dma0chan5/
lrwxrwxrwx  1 root root 0 1970-01-01 00:00 dma0chan6 -> ../../devices/soc0/amba/f8003000.dmac/dma/dma0chan6/
lrwxrwxrwx  1 root root 0 1970-01-01 00:00 dma0chan7 -> ../../devices/soc0/amba/f8003000.dmac/dma/dma0chan7/
lrwxrwxrwx  1 root root 0 1970-01-01 00:00 dma1chan0 -> ../../devices/soc0/[email protected]/43000000.axivdma/dma/dma1chan0/

dma1chan0是xilinx AXI-VDMA IP生成的DMA控制器,其处于PL端,而dma0相关的控制器是ps端的pl330.本篇看pl330这个驱动程序的注册。

查看物理地址分布

 [email protected]:~# cat /proc/iomem
00000000-1fffffff : System RAM
  00008000-006651a3 : Kernel code
  006a2000-00700867 : Kernel data
41600000-4160ffff : /[email protected]/[email protected]
43000000-43000fff : /[email protected]/[email protected]
70e00000-70e0ffff : /[email protected]/[email protected]
75c00000-75c00fff : /[email protected]/[email protected]
77600000-77600fff : /[email protected]/[email protected]
79000000-7900ffff : /[email protected]/[email protected]
e0001000-e0001fff : xuartps
e0002000-e0002fff : /amba/[email protected]
  e0002000-e0002fff : /amba/[email protected]
e000a000-e000afff : /amba/[email protected]
e000b000-e000bfff : /amba/[email protected]
e000d000-e000dfff : /amba/[email protected]
e0100000-e0100fff : mmc0
f8003000-f8003fff : /amba/[email protected]
  f8003000-f8003fff : /amba/[email protected]
f8005000-f8005fff : /amba/[email protected]
f8007000-f80070ff : /amba/[email protected]
f8007100-f800711f : /amba/[email protected]
f800c000-f800cfff : /amba/[email protected]
fffc0000-ffffffff : f800c000.ocmc
[email protected]:~# 

ARM采用统一编址,其访问内存和外设的指令是一样没有差异的,linux内核并不通过物理地址直接访问外设,而是通过虚拟地址,虚拟地址经过MMU转换成物理访问外设。所以外设需要使用ioremap()对将物理地址空间转换成虚拟地址。

I/O设备使用第三种地址,总线地址。如果一个设备在MMIO(memory mapped IO内存映射地址空间)有寄存器,或者其对系统存储系统执行DMA读写操作,设备使用的就是总线地址。

在系统枚举阶段,内核知道I/O设备以及他们的MMIO空间。如果一个设备支持DMA方式,驱动通过kmalloc()申请一段内存空间,返回申请空间的首地址,设为X,虚拟地址系统将虚拟地址X映射到物理地址Y。驱动程序可以使用虚拟地址X访问设备地址空间,但是设备本身却不行,这是因为DMA本身并不是通过虚拟地址方式来工作的。

在zynq7000设备里,DMA可以直接操作物理地址,但另一些处理器使用IOMMU将DMA地址转换物理地址,

linux建议使用DMA API而不是特定总线的DMA API,比如使用dma_map_*()接口而不是pci_map_*()接口,在zynq7000中,pl330 DMA已经实现了直接调用通用的DMA框架API即可以。

DMA开发相关API

首先得包含

 ?#include <linux/dma-mapping.h>

其提供了dma_addr_t定义,其可以作为设备使用DMA的源地址或者目的地址,并且可以使用DMA_XXX相关的API。

1.使用大DMA 一致性buffer

 ?void * dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag)

分配<size>大小的一致性区域。其返回值<dma_handle>能被强制类型转换成unsigned integer,这样就是总线的宽度

 ?void * dma_zalloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag)

是对dma_alloc_coherent的封装,并且将返回的地址空间内存清零。

 ?void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t dma_handle)

释放一致性内存。

2.使用小DMA一致性buffer

需要包括如下代码

 ?#include <linux/dmapool.h>

其工作有点类似kmem_cache(),不过其使用的是DMA一致性分配器,而非__get_free_pages(),并且需要N-byte对齐。

 ?	struct dma_pool *
	dma_pool_create(const char *name, struct device *dev, size_t size, size_t align, size_t alloc);

“name”字段用于诊断,dev和size和传递给dma_alloc_coherent()类似,align以字节计,且需要是2的指数。如果设备没有跨界限制,传递0;传递4096则意味着分配的DMA空间不能跨越4KByte空间。

 ?void *dma_pool_zalloc(struct dma_pool *pool, gfp_t mem_flags, dma_addr_t *handle)
 ?	void *dma_pool_alloc(struct dma_pool *pool, gfp_t gfp_flags, dma_addr_t *dma_handle);

从pool分配内存,返回内存将会满足size和alignment要求。传递GFP_ATOMIC防止阻塞,或者如果允许阻塞,则可以传递GFP_KERNEL,和dma_alloc_coherent()类似,返回两个值,一个是CPU使用的地址,以及一个设备使用的DMA地址。

 ?void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t addr); 

释放由dma_pool_alloc()分配的内存空间到pool。

 ?void dma_pool_destroy(struct dma_pool *pool);

dma_pool_destroy()将内存池的资源释放。

part1c DMA寻址限制

 ?int dma_set_mask_and_coherent(struct device *dev, u64 mask)

检查mask是否合法,如果是跟新设备streaming以及DMA mask参数。返回0是正确。

 ?int dma_set_mask(struct device *dev, u64 mask)
int dma_set_coherent_mask(struct device *dev, u64 mask

检查mask是否合法,如果是就跟新。

 ?u64 dma_get_required_mask(struct device *dev)

检查系统合法的DMA掩码。

part 1D Streaming(流式) DMA 映射

 ?dma_addr_t dma_map_single(struct device *dev, void *cpu_addr, size_t size, enum dma_data_direction direction)

将处理器的虚拟地址空间进行映射,这样让处理器可以访问外设,返回值是DMA地址。

其direction参数可选字段即意义如下:

 ?DMA_NONE		no direction (used for debugging)
DMA_TO_DEVICE		data is going from the memory to the device
DMA_FROM_DEVICE		data is coming from the device to the memory
DMA_BIDIRECTIONAL	direction isn‘t known

并不是所有的存储空间都能够使用上面的函数进行映射。内核虚拟地址也许是连续的但是其映射的物理地址却可以不一样。由于这个API不提供scatter/gather功能,如果尝试映射非连续物理存储空间。所以由该API进行映射的地址需要确保物理地址连续,如kmalloc。

设备DMA的地址内存范围必须在dma_mask,需要确保由kmalloc分配的内存空间在dma_mask所能达到的范围之内。驱动程序也许会传递各种flags以限制DMA地址范围(比如X86能够表示的地址范围在前16MB内)。

对具有IOMMU的平台,物理地址连续性和dma_mask要求也许不再适用。然而,考虑到可移植性,通常会忽略IOMMU的存在。

内存操作粒度被成为cache line 宽度,为了让这里的API映射的DMA操作成功执行,映射的区域必须在cache line边界开始和结束(目的是不发生对一个cache line出现两个独立的映射区域)。

DMA_TO_DEVICE,在软件修改内存块后程序退出控制前,需要进行同步。一个这一原语被使用,由这一原语包括的这一区域被成为只读,如果同时设备想写,需要使用DMA_BIDIRECTIONAL标志。

DMA_FROM_DEVICE 在驱动程序修改数据前先同步。内存块需要被当成只读对待。如果驱动要写功能,则需要设置DMA_BIDIRECTIONAL。

DMA_BIDIRECTIONAL需要特殊对待。这意味这驱动程序不能确定内存的改变在是否发生在将存储空间交给设备前,同时不能确定设备是否需要改变这块内存。所以,需要双向同步内存块。一次是在内存控制权移交给设置前,一次是在获取数据前。

 ?void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction direction) 

Unmap先前映射的区域,所有传递进来的参数必须和建立映射接口时的参数一致。

 ?dma_addr_t dma_map_page(struct device *dev, struct page *page, unsigned long offset, size_t size,
		    enum dma_data_direction direction)
void dma_unmap_page(struct device *dev, dma_addr_t dma_address, size_t size, enum dma_data_direction direction)

对页进行映射和逆映射。轻易不要动<size>和<offset>这两个参数。

 ?int dma_mapping_error(struct device *dev, dma_addr_t dma_addr)

在一些场景下,dma_map_single() and dma_map_page()在创建一个映射时也许会失败。调用上面的接口可以检测出错的原因。

 ?	int
	dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction)

返回segment 映射的DMA地址。

当使用scatterlist方式时,建立映射的过程如下:

 ?	int i, count = dma_map_sg(dev, sglist, nents, direction);
	struct scatterlist *sg;
	for_each_sg(sglist, sg, count, i) { hw_address[i] = sg_dma_address(sg);
		hw_len[i] = sg_dma_len(sg); }

zynq PS端pl330驱动注册

3002 static struct amba_driver pl330_driver = {
3003     .drv = {
3004         .owner = THIS_MODULE,
3005         .name = "dma-pl330",
3006         .pm = &pl330_pm,
3007     },
3008     .id_table = pl330_ids,
3009     .probe = pl330_probe,
3010     .remove = pl330_remove,
3011 };
3012
3013 module_amba_driver(pl330_driver);

见到此则知道必然是调用probe函数。注册函数调用如下:

2775 static int
2776 pl330_probe(struct amba_device *adev, const struct amba_id *id)
2777 {
2778     struct dma_pl330_platdata *pdat;
2779     struct pl330_config *pcfg;
2780     struct pl330_dmac *pl330;
2781     struct dma_pl330_chan *pch, *_p;
2782     struct dma_device *pd;
2783     struct resource *res;
2784     int i, ret, irq;
2785     int num_chan;
2786
2787     pdat = dev_get_platdata(&adev->dev);

上面定义了三个重要的数据结构体,它们的关系如下图:

2806     pl330->base = devm_ioremap_resource(&adev->dev, res);
2807     if (IS_ERR(pl330->base))
2808         return PTR_ERR(pl330->base);

对地址空间进行映射,并且存在了struct pl330_dmac结构体里。

2812     for (i = 0; i < AMBA_NR_IRQS; i++) {
2813         irq = adev->irq[i];
2814         if (irq) {
2815             ret = devm_request_irq(&adev->dev, irq,
2816                            pl330_irq_handler, 0,
2817                            dev_name(&adev->dev), pl330);
2818             if (ret)
2819                 return ret;
2820         } else {
2821             break;
2822         }
2823     }

每一个channel对应于一个中断,但是它们的中断服务函数是同一个。

2825     pcfg = &pl330->pcfg;
2826
2827     pcfg->periph_id = adev->periphid;
2828     ret = pl330_add(pl330);

设置pl330的configure字段。

2832     INIT_LIST_HEAD(&pl330->desc_pool);
2833     spin_lock_init(&pl330->pool_lock);
2834
2835     /* Create a descriptor pool of default size */
2836     if (!add_desc(pl330, GFP_KERNEL, NR_DEFAULT_DESC))
2837         dev_warn(&adev->dev, "unable to allocate desc\n");

设置pl330的描述符池,并且初始化16个描述符。

2842     if (pdat)
2843         num_chan = max_t(int, pdat->nr_valid_peri, pcfg->num_chan);
2844     else
2845         num_chan = max_t(int, pcfg->num_peri, pcfg->num_chan);
2846
2847     pl330->num_peripherals = num_chan;
2848
2849     pl330->peripherals = kzalloc(num_chan * sizeof(*pch), GFP_KERNEL);
2850     if (!pl330->peripherals) {

初始化channel数,并且为dma channel分配内存空间。

2856     for (i = 0; i < num_chan; i++) {
2857         pch = &pl330->peripherals[i];
2858         if (!adev->dev.of_node)
2859             pch->chan.private = pdat ? &pdat->peri_id[i] : NULL;
2860         else
2861             pch->chan.private = adev->dev.of_node;
2862
2863         INIT_LIST_HEAD(&pch->submitted_list);
2864         INIT_LIST_HEAD(&pch->work_list);
2865         INIT_LIST_HEAD(&pch->completed_list);
2866         spin_lock_init(&pch->lock);
2867         pch->thread = NULL;
2868         pch->chan.device = pd;
2869         pch->dmac = pl330;
2870
2871         /* Add the channel to the DMAC list */
2872         list_add_tail(&pch->chan.device_node, &pd->channels);
2873     }

初始化dma_pl330_chan相关字段。

2886     pd->device_alloc_chan_resources = pl330_alloc_chan_resources;
2887     pd->device_free_chan_resources = pl330_free_chan_resources;
2888     pd->device_prep_dma_memcpy = pl330_prep_dma_memcpy;
2889     pd->device_prep_dma_cyclic = pl330_prep_dma_cyclic;
2890     pd->device_tx_status = pl330_tx_status;
2891     pd->device_prep_slave_sg = pl330_prep_slave_sg;
2892     pd->device_config = pl330_config;
2893     pd->device_pause = pl330_pause;
2894     pd->device_terminate_all = pl330_terminate_all;
2895     pd->device_issue_pending = pl330_issue_pending;
2896     pd->src_addr_widths = PL330_DMA_BUSWIDTHS;
2897     pd->dst_addr_widths = PL330_DMA_BUSWIDTHS;
2898     pd->directions = BIT(DMA_DEV_TO_MEM) | BIT(DMA_MEM_TO_DEV);
2899     pd->residue_granularity = DMA_RESIDUE_GRANULARITY_SEGMENT;

初始化上图中struct dma_device的若干成员和函数。

2901     ret = dma_async_device_register(pd);
2902     if (ret) {
2903         dev_err(&adev->dev, "unable to register DMAC\n");
2904         goto probe_err3;
2905     }

注册dma设备并创建sys/class接口,会有如下显示:

 [email protected]:/sys/class/dma# ls
dma0chan0  dma0chan2  dma0chan4  dma0chan6  dma1chan0
dma0chan1  dma0chan3  dma0chan5  dma0chan7

将DMA控制器注册到DT DMA helpers。通过of_dma_list就可以找到该DMA控制器。

2907     if (adev->dev.of_node) {
2908         ret = of_dma_controller_register(adev->dev.of_node,
2909                      of_dma_pl330_xlate, pl330);

至此,DMA注册函数基本流程完毕。但是还留下了一个irq函数。

DMA中断服务函数

2720 static irqreturn_t pl330_irq_handler(int irq, void *data)
2721 {
2722     if (pl330_update(data))
2723         return IRQ_HANDLED;
2724     else
2725         return IRQ_NONE;
2726 }

该函数实际上调用了pl330_update去完成中断服务函数的请求。由于其是在中断函数中调用的,则中断函数的那些注意事项还是要遵从的。

1533 static int pl330_update(struct pl330_dmac *pl330)
1534 {
1535     struct dma_pl330_desc *descdone, *tmp;
1536     unsigned long flags;
1537     void __iomem *regs;
1538     u32 val;
1539     int id, ev, ret = 0;
1540
1541     regs = pl330->base;

该函数的base地址是经过ioremap得到的。

1545     val = readl(regs + FSM) & 0x1;
1546     if (val)
1547         pl330->dmac_tbd.reset_mngr = true;
1548     else
1549         pl330->dmac_tbd.reset_mngr = false;

首先都FSM寄存器,然后看bit0是否需要复位mngr。

1551     val = readl(regs + FSC) & ((1 << pl330->pcfg.num_chan) - 1);
1552     pl330->dmac_tbd.reset_chan |= val;
1553     if (val) {
1554         int i = 0;
1555         while (i < pl330->pcfg.num_chan) {
1556             if (val & (1 << i)) {
1557                 dev_info(pl330->ddma.dev,
1558                     "Reset Channel-%d\t CS-%x FTC-%x\n",
1559                         i, readl(regs + CS(i)),
1560                         readl(regs + FTC(i)));
1561                 _stop(&pl330->channels[i]);
1562             }
1563             i++;
1564         }
1565     }

各个通复位。

1568     val = readl(regs + ES);
1569     if (pl330->pcfg.num_events < 32
1570             && val & ~((1 << pl330->pcfg.num_events) - 1)) {
1571         pl330->dmac_tbd.reset_dmac = true;
1572         dev_err(pl330->ddma.dev, "%s:%d Unexpected!\n", __func__,
1573             __LINE__);
1574         ret = 1;
1575         goto updt_exit;
1576     }

读取事件寄存器,并判断是否出错了。出错则置reset标志。

1578     for (ev = 0; ev < pl330->pcfg.num_events; ev++) {
1579         if (val & (1 << ev)) { /* Event occurred */
1580             struct pl330_thread *thrd;
1581             u32 inten = readl(regs + INTEN);
1582             int active;
1583
1584             /* Clear the event */
1585             if (inten & (1 << ev))
1586                 writel(1 << ev, regs + INTCLR);
1587
1588             ret = 1;
1589
1590             id = pl330->events[ev];
1591
1592             thrd = &pl330->channels[id];
1593
1594             active = thrd->req_running;
1595             if (active == -1) /* Aborted */
1596                 continue;
1597
1598             /* Detach the req */
1599             descdone = thrd->req[active].desc;
1600             thrd->req[active].desc = NULL;
1601
1602             thrd->req_running = -1;
1603
1604             /* Get going again ASAP */
1605             _start(thrd);
1606
1607             /* For now, just make a list of callbacks to be done */
1608             list_add_tail(&descdone->rqd, &pl330->req_done);
1609         }
1610     }

处理相应事件。

时间: 2024-10-26 11:05:46

zynq PS侧DMA驱动的相关文章

嵌入式开发之zynq---Zynq PS侧sd驱动

http://blog.chinaunix.net/uid-29404121-id-4217026.html http://blog.chinaunix.net/uid-29709984-id-4304978.html http://blog.chinaunix.net/uid-26707720-id-3979376.html http://blog.chinaunix.net/uid-21973366-id-3970069.html http://blog.csdn.net/simonjay2

嵌入式开发之zynq---Zynq PS侧I2C驱动架构

http://blog.chinaunix.net/uid-24148050-id-120532.html http://bbs.csdn.net/topics/390538368?page=1 http://blog.csdn.net/lanyou1900/article/details/41724103 http://blog.csdn.net/lqxandroid2012/article/details/51367376 http://blog.chinaunix.net/uid-2340

Zynq PS DMA控制器应用笔记

Zynq PS DMA应用笔记 Hello,Panda Zynq-7000系列器件PS端的DMA控制器采用ARM的IP核DMA-330(PL-330)实现.有关DMA控制器的硬件细节及相关指令集.编程实例内容参考ARM官方文档: DDI0424D:dma330_r1p2_trm.pdf DAI0239A:dma330_example_programs.pdf 本文开发环境为Xilinx SDK2015.2,DMA库版本为dmaps_v2_1. 1 结构特点 DMA控制器具有以下的特点: n   

linux下DMA驱动测试代码

DMA传输可以是内存到内存.内存到外设和外设到内存.这里的代码通过dma驱动实现了内存到内存的数据传输. /* Function description:When we call dmatest_read(),it will transmit src memory data to dst memory,then print dst memory data by dma_callback_func(void) function. */ #include<linux/module.h> #incl

利用ZYNQ SOC快速打开算法验证通路(3)——PS端DMA缓存数据到PS端DDR

上篇该系列博文中讲述W5500接收到上位机传输的数据,此后需要将数据缓存起来.当数据量较大或者其他数据带宽较高的情况下,片上缓存(OCM)已无法满足需求,这时需要将大量数据保存在外挂的DDR SDRAM中. 最简单的方式是使用Xilinx的读写地址库函数Xil_In32()和Xil_Out32(),当然不仅支持32bit位宽,还包括8 16和64bit.但这种方式每次读写都要占用CPU,无法在读写的同时接收后续数据或者对之前的数据进一步处理,也就无法形成类似FPGA逻辑设计中的"流水线结构&qu

S3C2440 DMA 驱动示例

将 DMA 抽象为一个字符设备,在初始化函数中调用 void *dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp) 函数来分配两段物理地址连续的空间,一段作为源空间,一段作为目的空间. 然后将物理地址进行 ioremap 供驱动使用,最后调用 register_chrdev 来注册这个字符设备. DMA 的 regs: #define DMA0_BASE_ADDR 0x4B0

DMA驱动框架

框架入口源文件:dma.c (可根据入口源文件,再按着框架到内核走一遍) 内核版本:linux_2.6.22.6    硬件平台:JZ2440 以下是驱动框架: 以下是驱动代码  dma.c : #include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/init.h> #include <linux/delay.h> #inc

【SDK】控制PS侧GPIO

封装函数位于bsp文件下xgpiops.h void XGpioPs_SetDirection(XGpioPs *InstancePtr, u8 Bank, u32 Direction);// 设置IO的in/out方向 void XGpioPs_SetOutputEnable(XGpioPs *InstancePtr, u8 Bank, u32 OpEnable);// 设置out使能 void XGpioPs_WritePin(XGpioPs *InstancePtr, u32 Pin, u

深入浅出~Linux设备驱动之DMA

如果不曾相逢 也许 心绪永远不会沉重 如果真的失之交臂 恐怕一生也不得轻松 一个眼神 便足以让心海 掠过飓风 在贫瘠的土地上 更深地懂得风景 一次远行 便足以憔悴了一颗 羸弱的心 每望一眼秋水微澜 便恨不得 泪水盈盈 死怎能不 从容不迫 爱又怎能 无动于衷 只要彼此爱过一次 就是无憾的人生 也许 也许,永远没有那一天 前程如朝霞般绚烂 也许,永远没有那一天 成功如灯火般辉煌 也许,只能是这样 攀援却达不到峰顶 也许,只能是这样 奔流却掀不起波浪 也许,我们能给予你的 只有一颗 饱经沧桑的心 和满