首页 > 其他分享 >两种形式的dma 实现memory copy代码

两种形式的dma 实现memory copy代码

时间:2024-08-21 16:15:41浏览次数:4  
标签:dma struct sdma chan m2m memory copy class

在飞思卡尔的时候,需要用SDMA实现内存到内存memory copy的功能,需要做两部分的工作:

1:在DMA controller中加入M2M的支持。

2:写一个驱动来调用DMA controller的M2M功能。

上面的2实际上对于不同的SoC来讲,思路是一样的,有通用性,在这里总结下。

当时在实现的时候,用了两种方法:

1:cyclic, 用dma_alloc_coherent分配两段dma 内存空间, 一段做src, 一段做dst. 调用DMA controller接口来将src中的数据往dst中拷贝。因为DMA操作的是物理内地址上连续的内存空间,dma_alloc_coherent分配不了太大的连续物理地址空间,所以,仅仅能实现小批量数据的M2M拷贝。

2:sg, 用dma_alloc_coherent分配很多段dma 内存空间,一半大小的空间做src,一半大小的空间做dst.通过device_prep_dma_sg来将各自独立的src/dst空间链接起来。这个,可以将若干段分散的物理地址链接成逻辑上连续的,可以实现较大数据的拷贝。

顺便复习下dma engine的用法:
Linux/Documentation/dmaengine.txt

 13 The slave DMA usage consists of following steps:
 14 1. Allocate a DMA slave channel
 15 2. Set slave and controller specific parameters
 16 3. Get a descriptor for transaction
 17 4. Submit the transaction
 18 5. Issue pending requests and wait for callback notification

 20 1. Allocate a DMA slave channel

 27    Interface:
 28         struct dma_chan *dma_request_channel(dma_cap_mask_t mask,
 29                         dma_filter_fn filter_fn,
 30                         void *filter_param);


 48 2. Set slave and controller specific parameters

 61    Interface:
 62         int dmaengine_slave_config(struct dma_chan *chan,
 63                                   struct dma_slave_config *config)


 70 3. Get a descriptor for transaction

 86    Interface:
 87         struct dma_async_tx_descriptor *(*chan->device->device_prep_slave_sg)(
 88                 struct dma_chan *chan, struct scatterlist *sgl,
 89                 unsigned int sg_len, enum dma_data_direction direction,
 90                 unsigned long flags);
 91
 92         struct dma_async_tx_descriptor *(*chan->device->device_prep_dma_cyclic)(
 93                 struct dma_chan *chan, dma_addr_t buf_addr, size_t buf_len,
 94                 size_t period_len, enum dma_data_direction direction);
 95
 96         struct dma_async_tx_descriptor *(*device_prep_interleaved_dma)(
 97                 struct dma_chan *chan, struct dma_interleaved_template *xt,
 98                 unsigned long flags);


139 4. Submit the transaction

144    Interface:
145         dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc)


153 5. Issue pending DMA requests and wait for callback notification

163    Interface:
164         void dma_async_issue_pending(struct dma_chan *chan);


传输结束的时候可以用:

168 1. int dmaengine_terminate_all(struct dma_chan *chan)


看下面代码:

1: cyclic方式实现

#include <linux/module.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <linux/mman.h>
#include <linux/init.h>
#include <linux/dma-mapping.h>
#include <linux/fs.h>
#include <linux/version.h>
#include <linux/delay.h>
#include <mach/dma.h>

#include <linux/dmaengine.h>
#include <linux/device.h>

#include <linux/io.h>
#include <linux/delay.h>

static int gMajor; //major number of device
static struct class *dma_tm_class;
static char *wbuf;
static char *rbuf;
static dma_addr_t wpaddr;
static dma_addr_t rpaddr;

struct dma_chan *dma_m2m_chan;

struct completion dma_m2m_ok;

#define SDMA_BUF_SIZE  1024

static bool dma_m2m_filter(struct dma_chan *chan, void *param)
{
    if (!imx_dma_is_general_purpose(chan))
        return false;
    chan->private = param;
    return true;
}

int sdma_open(struct inode * inode, struct file * filp)
{
    dma_cap_mask_t dma_m2m_mask;
    struct imx_dma_data m2m_dma_data = {0};


    init_completion(&dma_m2m_ok);    


    dma_cap_zero(dma_m2m_mask);
    dma_cap_set(DMA_SLAVE, dma_m2m_mask);
    m2m_dma_data.peripheral_type = IMX_DMATYPE_MEMORY;
    m2m_dma_data.priority = DMA_PRIO_HIGH;
    
    dma_m2m_chan = dma_request_channel(dma_m2m_mask, dma_m2m_filter, &m2m_dma_data);
    if (!dma_m2m_chan) {
        printk("Error opening the SDMA memory to memory channel\n");
        return -EINVAL;
    }


    wbuf = dma_alloc_coherent(NULL, SDMA_BUF_SIZE, &wpaddr, GFP_DMA);
    rbuf = dma_alloc_coherent(NULL, SDMA_BUF_SIZE, &rpaddr, GFP_DMA);


    return 0;
}

int sdma_release(struct inode * inode, struct file * filp)
{
    dma_release_channel(dma_m2m_chan);
    dma_m2m_chan = NULL;
    dma_free_coherent(NULL, SDMA_BUF_SIZE, wbuf, wpaddr);
    dma_free_coherent(NULL, SDMA_BUF_SIZE, rbuf, rpaddr);


    return 0;
}

ssize_t sdma_read (struct file *filp, char __user * buf, size_t count, loff_t * offset)
{
    int i;
    
    wait_for_completion(&dma_m2m_ok);
    for (i=0; i<SDMA_BUF_SIZE; i++) {
    printk("src_data_%d = %x\n",i, *(wbuf+i) );
    }
    for (i=0; i<SDMA_BUF_SIZE; i++) {
    printk("dst_data_%d = %x\n",i, *(rbuf+i) );
    }
    
    return 0;
}

static void dma_m2m_callback(void *data)
{
    printk("in %s\n",__func__);
    complete(&dma_m2m_ok);
    return ;
}

ssize_t sdma_write(struct file * filp, const char __user * buf, size_t count, loff_t * offset)
{
    u32 *index1;
    struct dma_slave_config dma_m2m_config;
    struct dma_async_tx_descriptor *dma_m2m_desc;
    int i;
    index1 = wbuf;
    for (i=0; i<SDMA_BUF_SIZE; i++) {
        *(index1 + i) = 0x12345678;
    }

    for (i=0; i<SDMA_BUF_SIZE; i++) {
    printk("%d : %x\n",i, *(wbuf+i) );
    }

    dma_m2m_config.direction = DMA_MEM_TO_MEM;
    dma_m2m_config.dst_addr = rpaddr;
    dma_m2m_config.src_addr = wpaddr;
    dma_m2m_config.src_addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES;
    dma_m2m_config.dst_addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES;
    dma_m2m_config.dst_maxburst = 4;
    dma_m2m_config.src_maxburst = 4;
    dmaengine_slave_config(dma_m2m_chan, &dma_m2m_config);
    dma_m2m_desc = dma_m2m_chan->device->device_prep_dma_cyclic(
                    dma_m2m_chan, NULL, SDMA_BUF_SIZE, SDMA_BUF_SIZE/2, DMA_MEM_TO_MEM);
    dma_m2m_desc->callback = dma_m2m_callback;
    dmaengine_submit(dma_m2m_desc);
    return 0;
}

struct file_operations dma_fops = {
    open:        sdma_open,
    release:    sdma_release,
    read:        sdma_read,
    write:        sdma_write,
};

int __init sdma_init_module(void)
{
#if (LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,26))
    struct device *temp_class;
#else
    struct class_device *temp_class;
#endif
    int error;


    /* register a character device */
    error = register_chrdev(0, "sdma_test", &dma_fops);
    if (error < 0) {
        printk("SDMA test driver can't get major number\n");
        return error;
    }
    gMajor = error;
    printk("SDMA test major number = %d\n",gMajor);


    dma_tm_class = class_create(THIS_MODULE, "sdma_test");
    if (IS_ERR(dma_tm_class)) {
        printk(KERN_ERR "Error creating sdma test module class.\n");
        unregister_chrdev(gMajor, "sdma_test");
        return PTR_ERR(dma_tm_class);
    }


#if (LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,28))
    temp_class = device_create(dma_tm_class, NULL,
                   MKDEV(gMajor, 0), NULL, "sdma_test");
#elif (LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,26))
    temp_class = device_create(dma_tm_class, NULL,
                   MKDEV(gMajor, 0), "sdma_test");
#else
    temp_class = class_device_create(dma_tm_class, NULL,
                         MKDEV(gMajor, 0), NULL,
                         "sdma_test");
#endif
    if (IS_ERR(temp_class)) {
        printk(KERN_ERR "Error creating sdma test class device.\n");
        class_destroy(dma_tm_class);
        unregister_chrdev(gMajor, "sdma_test");
        return -1;
    }


    printk("SDMA test Driver Module loaded\n");
    return 0;
}

static void sdma_cleanup_module(void)
{
    unregister_chrdev(gMajor, "sdma_test");
#if (LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,26))
    device_destroy(dma_tm_class, MKDEV(gMajor, 0));
#else
    class_device_destroy(dma_tm_class, MKDEV(gMajor, 0));
#endif
    class_destroy(dma_tm_class);


    printk("SDMA test Driver Module Unloaded\n");
}

module_init(sdma_init_module);
module_exit(sdma_cleanup_module);


2:sg方式实现

#include <linux/module.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <linux/mman.h>
#include <linux/init.h>
#include <linux/dma-mapping.h>
#include <linux/fs.h>
#include <linux/version.h>
#include <linux/delay.h>
#include <mach/dma.h>


#include <linux/dmaengine.h>
#include <linux/device.h>


#include <linux/io.h>
#include <linux/delay.h>


static int gMajor; /* major number of device */
static struct class *dma_tm_class;
u32 *wbuf, *wbuf2, *wbuf3, *wbuf4;
u32 *rbuf, *rbuf2, *rbuf3, *rbuf4;


struct dma_chan *dma_m2m_chan;


struct completion dma_m2m_ok;


struct scatterlist sg[4], sg2[4];


#define SDMA_BUF_SIZE  1024*60






static bool dma_m2m_filter(struct dma_chan *chan, void *param)
{
if (!imx_dma_is_general_purpose(chan))
return false;
chan->private = param;
return true;
}


int sdma_open(struct inode *inode, struct file *filp)
{
dma_cap_mask_t dma_m2m_mask;
struct imx_dma_data m2m_dma_data;


init_completion(&dma_m2m_ok);


dma_cap_zero(dma_m2m_mask);
dma_cap_set(DMA_SLAVE, dma_m2m_mask);
m2m_dma_data.peripheral_type = IMX_DMATYPE_MEMORY;
m2m_dma_data.priority = DMA_PRIO_HIGH;
dma_m2m_chan = dma_request_channel(dma_m2m_mask, dma_m2m_filter,
&m2m_dma_data);
if (!dma_m2m_chan) {
printk("Error opening the SDMA memory to memory channel\n");
return -EINVAL;
}


wbuf = kzalloc(SDMA_BUF_SIZE, GFP_DMA);
if(!wbuf) {
printk("error wbuf !!!!!!!!!!!\n");
return -1;
}


wbuf2 = kzalloc(SDMA_BUF_SIZE, GFP_DMA);
if(!wbuf2) {
printk("error wbuf2 !!!!!!!!!!!\n");
return -1;
}


wbuf3 = kzalloc(SDMA_BUF_SIZE, GFP_DMA);
if(!wbuf3) {
printk("error wbuf3 !!!!!!!!!!!\n");
return -1;
}


wbuf4 = kzalloc(SDMA_BUF_SIZE, GFP_DMA);
if(!wbuf4) {
printk("error wbuf4 !!!!!!!!!!!\n");
return -1;
}


rbuf = kzalloc(SDMA_BUF_SIZE, GFP_DMA);
if(!rbuf) {
printk("error rbuf !!!!!!!!!!!\n");
return -1;
}


rbuf2 = kzalloc(SDMA_BUF_SIZE, GFP_DMA);
if(!rbuf2) {
printk("error rbuf2 !!!!!!!!!!!\n");
return -1;
}


rbuf3 = kzalloc(SDMA_BUF_SIZE, GFP_DMA);
if(!rbuf3) {
printk("error rbuf3 !!!!!!!!!!!\n");
return -1;
}


rbuf4 = kzalloc(SDMA_BUF_SIZE, GFP_DMA);
if(!rbuf4) {
printk("error rbuf4 !!!!!!!!!!!\n");
return -1;
}


return 0;
}


int sdma_release(struct inode * inode, struct file * filp)
{
dmaengine_terminate_all(dma_m2m_chan);
dma_release_channel(dma_m2m_chan);
dma_m2m_chan = NULL;
kfree(wbuf);
kfree(wbuf2);
kfree(wbuf3);
kfree(rbuf);
kfree(rbuf2);
kfree(rbuf3);
return 0;
}


ssize_t sdma_read (struct file *filp, char __user * buf, size_t count,
loff_t * offset)
{
int i;
#if 0
for (i=0; i<SDMA_BUF_SIZE/4; i++) {
printk("dst data_%d : %x\n", i, *(rbuf+i));
}


for (i=0; i<SDMA_BUF_SIZE/4; i++) {
printk("dst data2_%d : %x\n", i, *(rbuf2+i));
}


for (i=0; i<SDMA_BUF_SIZE/4; i++) {
printk("dst data3_%d : %x\n", i, *(rbuf3+i));
}
#endif


for (i=0; i<SDMA_BUF_SIZE/4; i++) {
if (*(rbuf+i) != *(wbuf+i)) {
printk("buffer 1 copy falled!\n");
return 0;
}
}
printk("buffer 1 copy passed!\n");


for (i=0; i<SDMA_BUF_SIZE/4; i++) {
if (*(rbuf2+i) != *(wbuf2+i)) {
printk("buffer 2 copy falled!\n");
return 0;
}
}
printk("buffer 2 copy passed!\n");


for (i=0; i<SDMA_BUF_SIZE/4; i++) {
if (*(rbuf3+i) != *(wbuf3+i)) {
printk("buffer 3 copy falled!\n");
return 0;
}
}
printk("buffer 3 copy passed!\n");


for (i=0; i<SDMA_BUF_SIZE/4; i++) {
if (*(rbuf4+i) != *(wbuf4+i)) {
printk("buffer 4 copy falled!\n");
return 0;
}
}
printk("buffer 4 copy passed!\n");


return 0;
}


static void dma_m2m_callback(void *data)
{
complete(&dma_m2m_ok);
return ;
}


ssize_t sdma_write(struct file * filp, const char __user * buf, size_t count,
loff_t * offset)
{
u32 *index1, *index2, *index3, *index4, i, ret;
struct dma_slave_config dma_m2m_config;
struct dma_async_tx_descriptor *dma_m2m_desc;
index1 = wbuf;
index2 = wbuf2;
index3 = wbuf3;
index4 = wbuf4;
struct timeval end_time;
unsigned long end, start;
for (i=0; i<SDMA_BUF_SIZE/4; i++) {
*(index1 + i) = 0x12121212;
}


for (i=0; i<SDMA_BUF_SIZE/4; i++) {
*(index2 + i) = 0x34343434;
}


for (i=0; i<SDMA_BUF_SIZE/4; i++) {
*(index3 + i) = 0x56565656;
}


for (i=0; i<SDMA_BUF_SIZE/4; i++) {
*(index4 + i) = 0x78787878;
}


#if 0
for (i=0; i<SDMA_BUF_SIZE/4; i++) {
printk("input data_%d : %x\n", i, *(wbuf+i));
}


for (i=0; i<SDMA_BUF_SIZE/2/4; i++) {
printk("input data2_%d : %x\n", i, *(wbuf2+i));
}


for (i=0; i<SDMA_BUF_SIZE/4; i++) {
printk("input data3_%d : %x\n", i, *(wbuf3+i));
}
#endif
dma_m2m_config.direction = DMA_MEM_TO_MEM;
dma_m2m_config.dst_addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES;
dmaengine_slave_config(dma_m2m_chan, &dma_m2m_config);


sg_init_table(sg, 4);
sg_set_buf(&sg[0], wbuf, SDMA_BUF_SIZE);
sg_set_buf(&sg[1], wbuf2, SDMA_BUF_SIZE);
sg_set_buf(&sg[2], wbuf3, SDMA_BUF_SIZE);
sg_set_buf(&sg[3], wbuf4, SDMA_BUF_SIZE);
ret = dma_map_sg(NULL, sg, 4, dma_m2m_config.direction);


sg_init_table(sg2, 4);
sg_set_buf(&sg2[0], rbuf, SDMA_BUF_SIZE);
sg_set_buf(&sg2[1], rbuf2, SDMA_BUF_SIZE);
sg_set_buf(&sg2[2], rbuf3, SDMA_BUF_SIZE);
sg_set_buf(&sg2[3], rbuf4, SDMA_BUF_SIZE);
ret = dma_map_sg(NULL, sg2, 4, dma_m2m_config.direction);


dma_m2m_desc = dma_m2m_chan->device->
device_prep_dma_sg(dma_m2m_chan, sg2, 4, sg, 4, 0);
dma_m2m_desc->callback = dma_m2m_callback;
//printk("1111111111111\n");
do_gettimeofday(&end_time);
start = end_time.tv_sec*1000000 + end_time.tv_usec;


dmaengine_submit(dma_m2m_desc);
dma_async_issue_pending(dma_m2m_chan);


wait_for_completion(&dma_m2m_ok);
//printk("2222222222222\n");
do_gettimeofday(&end_time);
end = end_time.tv_sec*1000000 + end_time.tv_usec;
printk("end - start = %d\n", end - start);
dma_unmap_sg(NULL, sg, 4, dma_m2m_config.direction);
dma_unmap_sg(NULL, sg2, 4, dma_m2m_config.direction);


return 0;
}


struct file_operations dma_fops = {
open: sdma_open,
release: sdma_release,
read: sdma_read,
write: sdma_write,
};


int __init sdma_init_module(void)
{
#if (LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,26))
struct device *temp_class;
#else
struct class_device *temp_class;
#endif
int error;


/* register a character device */
error = register_chrdev(0, "sdma_test", &dma_fops);
if (error < 0) {
printk("SDMA test driver can't get major number\n");
return error;
}
gMajor = error;
printk("SDMA test major number = %d\n",gMajor);


dma_tm_class = class_create(THIS_MODULE, "sdma_test");
if (IS_ERR(dma_tm_class)) {
printk(KERN_ERR "Error creating sdma test module class.\n");
unregister_chrdev(gMajor, "sdma_test");
return PTR_ERR(dma_tm_class);
}


#if (LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,28))
temp_class = device_create(dma_tm_class, NULL,
  MKDEV(gMajor, 0), NULL, "sdma_test");
#elif (LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,26))
temp_class = device_create(dma_tm_class, NULL,
  MKDEV(gMajor, 0), "sdma_test");
#else
temp_class = class_device_create(dma_tm_class, NULL,
    MKDEV(gMajor, 0), NULL,
    "sdma_test");
#endif
if (IS_ERR(temp_class)) {
printk(KERN_ERR "Error creating sdma test class device.\n");
class_destroy(dma_tm_class);
unregister_chrdev(gMajor, "sdma_test");
return -1;
}


printk("SDMA test Driver Module loaded\n");
return 0;
}


static void sdma_cleanup_module(void)
{
unregister_chrdev(gMajor, "sdma_test");
#if (LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,26))
device_destroy(dma_tm_class, MKDEV(gMajor, 0));
#else
class_device_destroy(dma_tm_class, MKDEV(gMajor, 0));
#endif
class_destroy(dma_tm_class);


printk("SDMA test Driver Module Unloaded\n");
}


module_init(sdma_init_module);
module_exit(sdma_cleanup_module);


当时测出大约1秒钟可以拷贝50M的数据,但是客户还觉得不满意。
                      
原文链接:https://blog.csdn.net/u012769691/article/details/46814305

标签:dma,struct,sdma,chan,m2m,memory,copy,class
From: https://www.cnblogs.com/zhanyaowang/p/18371909

相关文章

  • A 3nm, 32.5TOPS/W, 55.0TOPS/mm2 and 3.78Mb/mm2 Fully-Digital Compute-in-Memory M
    1、强调存储密度(StorageDensity)Mb/mm2,存算一体的主要目的是减少数据搬运的开销,如果一味的堆计算单元而损失存储密度,那么虽然整体的计算吞吐率(TOPS)可以做到很大,相应的对计算密度也会有提升,但是由于需要频繁给CIMMacro刷新数据,从系统能效的角度上来说反而是下降的。这次的SRAMArr......
  • A 12nm 121-TOPS/W 41.6-TOPS/mm2 All Digital Full Precision SRAM-based Compute-in
    1b*4b的操作是通过4b或非门乘法器完成,然后再通过4b加法器两两相加。但是从真值表上来看,2个4b或非门乘法器加1个4b加法器完成的工作实际上可以通过一个由加法器和两比特IN控制的四选一Mux(或者说LUT)来完成。这样做的话可以直接节省掉21%的功耗。提出的这个并行多位输入结构下(即并......
  • Docker无法运行java虚拟机报错There is insufficient memory for the Java Runtime
    镜像导入到docker后无法启动容器的问题,但是上传到别的服务器上面又可以正常启动容器,报错信息如下:#ThereisinsufficientmemoryfortheJavaRuntimeEnvironmenttocontinue.#CannotcreateGCthread.Outofsystemresources.#Cannotsavelogfile,dumptoscree......
  • A 4nm 6163-TOPS/W/b 4790-TOPS/mm2/b SRAM Based Digital-Computing-in-Memory Macro
    SRAMarray和Localadder耦合在一起形成一个块,两个块share一个semi-global-adder,四个块再去shareGlobaladder和移位累加器。这样的floorplan使得整体结构上不存在一大块独立的巨型多级加法树,使得布局变得更加的规整。这里讨论了mix-Vt设计的问题,即混用高Vt管子和低Vt管子,高Vt......
  • An 89TOPS/W and 16.3TOPS/mm2 All-Digital SRAM-Based Full-Precision Compute-In Me
    权重是4bit的CIM结构图:激活值是4bit的做法是:以MSB-first的方式串性送入,然后通过移位加计算不同数位的和累加器就是一个移位累加结构,其中具有对符号位的处理机制,这里是补码机制。如果符号位是0,直接原码做符号位拓展加进去,如果符号位是1,取反加1原码转成补码之后加进去。减少......
  • 配置stm32cubemx采集stm32H743IIT6,通过DMA实现多通道和多模块ADC的采集,亲测有效!
     之前写到stm32cubemx通过阻塞实现单通道和多通道的ADC的采集。本文分享通过DMA实现单模块多通道和多模块多通道的ADC采集。stm32cubemx的版本6.10.0。一、DMA采集多通道ADC数据阻塞采集是每次采集adc数据,cpu死等,直到采集完或者在设定时间超时没能采集,返回到cpu。DMA采集......
  • CADMATIC许可证续费
    在数字化时代,CADMATIC软件已成为工程设计和制造业领域的核心工具。为了保持竞争优势,企业需要确保CADMATIC软件的持续使用。而CADMATIC许可证续费,则是确保软件持续稳定运行的关键。本文将为您揭示CADMATIC许可证续费的必要性,以及如何顺利完成许可证续费,帮助您在竞争激烈的市场中保......
  • OI Memory: No Regrets
    其实感觉没什么可以写的啊。我从小学四年级开始接触OI,最开始是在培训机构里学了一年半。刚起步的时候当然是学得很快的,到进入初中的时候,已经差不多把基础算法学完了。五年级的时候,我参加了ZL的提前招。那时这所学校已经在MO上颇有名气,同时也开始在OI方面崭露头角。经过一......
  • CADMATIC许可证配置文件
    在数字化时代,CADMATIC软件已成为工程设计和制造业领域的核心工具。为了充分发挥CADMATIC软件的性能并确保合规性,合理的许可证配置文件设置至关重要。本文将为您揭示CADMATIC许可证配置文件的奥秘,帮助您优化软件性能并确保合规性,提升企业的竞争力。一、CADMATIC许可证配置文件的重......
  • Copy AI——营销和内容创作
    一、CopyAI介绍CopyAI是一个基于人工智能的文案生成工具,旨在帮助用户快速生成高质量的营销内容、广告文案、社交媒体帖子、博客文章、电子邮件等。它利用自然语言处理(NLP)和深度学习技术,通过理解用户输入的关键信息,生成符合要求的文本内容。二、CopyAI核心功能多场景文......