multi-language verification (三)SV DPI call C/C++

准备

SV可以通过DPI(Direct Programming Interface)和C/C++代码交互;DPI作为编程接口的演进过程可从🔗Verilog PLI已死( 可能), SystemVerilog DPI当立中了解,相比TF (Task / Function),ACC (Access)和VPI (Verification Procedural Interface)这类编程接口,DPI更适合C程序的直接调用,不涉及内存的来回拷贝,效率更高,部署也更方便;推荐🔗《SystemVerilog for Verifaction(Third Edition)》 Chapter12 和 🔗Cadence 《SystemVerilog DPI Engineering NoteBook》作为参考资料,IEEE协议作为查阅资料。

数据结构

DPI中C和SV之间的数据传输,本质是仿真器在C/SV两侧调度切换时,利用指针来操作同一类数据结构的内存空间,指针在被使用前,只需要在C或SV中的一侧被初始化分配内存空间即可。用户需要保证C/SV两侧的数据格式相匹配(vcs会自动生成函数原型文件vc_hdrs.h,用户可参考这个文件; xrun使用-dpiheader-dpiimpheader产生export,import函数的头文件),DPI支持的数据格式除C语言内置类型外,在$VCS_HOME/include/svdpi.h中有声明,一些函数原型(包含 获取指针,从指针位置取数据,在指针位置放数据,获取数组index,等)也在此头文件中。

数据结构映射如下:
在这里插入图片描述

svBit

SV中最小的数据格式为2值1位宽的bit,C语言最小的为8位宽的char字节;所以C中svBit对SV中bit的映射,其实是unsigned char类型。所以高位bit是可以被非法赋值的,使用时需注意。建议在C侧做mask处理。

svBitVecVal

C中svBitVecVal类型是unsigned int类型,和SV中的bit[N:0]相映射。对于SV侧传入大于32bit的packed array,C侧需要拆分为svBitVecVal类型的数组。可以调用C侧的宏SV_PACKED_DATA_ELEMS()获得拆分后的数组宽度,在利用svGetSelectBit或者memcpy从中取出数据。

open arrays

如下示例中:

svSizeOfArray 获取数组大小为多少bytes,这里为4*4=16个;

p = (int*)svGetArrayPtr(a); 获取数组的指针,并强制转换为int类型指针;

q[i]=p[i] + p[i]; 直接使用指针索引,相当于数组索引;

svGetArrElemPtr1(a,low); 从1维数组a指针中获取index=low的地址

temp_data[0] = *(int*)svGetArrElemPtr1(a,low); 第一个*为解引用操作符,根据地址取数据;(int*)将地址强制转化为int类型地址;所以*(int*)svGetArrElemPtr1(a,low)是从内存地址svGetArrElemPtr1(a,low)中取出int类型的4 bytes数据,存入temp_data[0]中。

C:

#include <stdio.h>
#include "svdpi.h"
#include "vpi_user.h"
#include "stdlib.h"

/* nbcode "addarray" start */

void add_array(int test, const svOpenArrayHandle a, const svOpenArrayHandle b)
{

 int i;
 int temp_data[4];
 int *p, *q, r[4] ;
 int length,left,right,low,high;
 /* Obtains the left index of the array */
 /* The second argument '1' corresponds to the first unpacked dimension of the array */

/*
  p = (int*)malloc(sizeof(int)*5);
  q = (int*)malloc(sizeof(int)*5);
*/

 left = svLeft(a,1);
 /* Obtains the lowest index of the array */
 low = svLow(a,1);
 /* Obtains the right index of the array */
 right = svRight(a,1);
 /* Obtains the highest index of the array */
 high = svHigh(a,1);
 /* Obtains the size of the array */
 length = svSizeOfArray(a);

 vpi_printf("Left index of array = %d\n",left);
 vpi_printf("Right index of array = %d\n",right);
 vpi_printf("Low index of array = %d\n",low);
 vpi_printf("High index of array = %d\n",high);
 vpi_printf("size of array = %d\n", length);


 if(test == 1) {
 	p = (int*)svGetArrayPtr(a);
	q = (int*)svGetArrayPtr(b);
	vpi_printf("Using normalized indices\n");
        for(i=0;i<4;i++){
        	q[i]=p[i] + p[i]; 
        	vpi_printf("a[%d] = %d\n",i,p[i]);
	        vpi_printf("b[%d] = %d\n",i,q[i]);
        }                    
 }
 else{
        /* Returns the value that corresponds to Verilog index -1 of input array */
	temp_data[0] =  *(int*)svGetArrElemPtr1(a,low);
        /* Returns the value that corresponds to Verilog index 0 of input array */
	temp_data[1] =  *(int*)svGetArrElemPtr1(a,low+1);
        /* Returns the value that corresponds to Verilog index 1 of input array */
	temp_data[2] =  *(int*)svGetArrElemPtr1(a,low+2);
        /* Returns the value that corresponds to verilog index 2 of input array */
	temp_data[3] =  *(int*)svGetArrElemPtr1(a,high);
	vpi_printf("Using verilog indices\n");
	for(i=0;i<4;i++){
		r[i]= temp_data[i] + temp_data[i]; 
		vpi_printf("a[%d] = %d\n",i,temp_data[i]);
	        vpi_printf("b[%d] = %d\n",i,r[i]);
	}
 }
}  

/* nbcode "addarray" end */  

SV:

module top; 

int input_1 [-1:2];
int output_1[-1:2];

int input_2 [-1:2];
int output_2[-1:2];

int test = 1;
int cnt  = 1;
	
// nbcode "importarray" start

import "DPI-C" function void add_array(input int test, input int i[],output int o[]);
  
// nbcode "importarray" end

	initial begin
              for (int j = -1; j < 3; j++)
                begin    
                  input_1[j] = cnt;
                  $display("input_1[%d] = %h",j,input_1[j]);  
		  cnt++;
                end 
		add_array(test,input_1,output_1);


		for (int j = -1; j < 3; j++)
                begin    
		  input_2[j] = cnt;
		  $display("input_2[%d] = %h",j,input_2[j]);
		  cnt++;
                end 

		add_array(0,input_2,output_2);
	end

endmodule

LOG:

vcs -full64 \
-kdb -lca \
-sverilog \
-l vcs.log \
./add.c \
./top.v \
-R
input_1[         -1] = 00000001
input_1[          0] = 00000002
input_1[          1] = 00000003
input_1[          2] = 00000004
Left index of array = -1
Right index of array = 2
Low index of array = -1
High index of array = 2
size of array = 16
Using normalized indices
a[0] = 1
b[0] = 2
a[1] = 2
b[1] = 4
a[2] = 3
b[2] = 6
a[3] = 4
b[3] = 8
input_2[         -1] = 00000005
input_2[          0] = 00000006
input_2[          1] = 00000007
input_2[          2] = 00000008
Left index of array = -1
Right index of array = 2
Low index of array = -1
High index of array = 2
size of array = 16
Using verilog indices
a[0] = 5
b[0] = 10
a[1] = 6
b[1] = 12
a[2] = 7
b[2] = 14
a[3] = 8
b[3] = 16

string

如下示例中:

const char *str1 = "ADD"; 字符串"ADD"是RO只读的,不可以被修改,使用const修饰;字符串是一个char类型的数组,可以使用char*表示。

strcmp(a,str1) string.h中用于比较两个字符串大小的函数,返回值为0表明相等

int operate_strings(const char *a, const char **c, int d, int e, int* f) 这里比较tricky的是,对于SV端的output string,不论是string还是string数组,C端都是char**类型,传递数组的指针,而不是数组。这样可以确保SV端获取内存处的数据。若使用const char *c SV端的打印为空,可参考VCS自动生成的vc_hdrs.h文件,会生成和SV端相匹配的C端函数原型:extern void operate_strings(/* INPUT */const char* a, /* OUTPUT */SV_STRING *c, /* INPUT */int d, /* INPUT */int e, /* OUTPUT */int *f);

CPP:

#include <stdio.h>
#include <vpi_user.h>
#include "svdpi.h"
#include "string.h"

/* nbcode "operatestrings" start */

int operate_strings(const char *a, const char **c, int d, int e, int* f)
{
  const char *str1 = "ADD";
  const char *str2 = "SUBTRACT";
  vpi_printf("Operation to be performed = %s\n",a);
  if(strcmp(a,str1) == 0){
	*f = d + e;
	*c = "ADD operation performed";
	vpi_printf("%d + %d = %d\n",d,e,*f);
  }
  else if(strcmp(a,str2) == 0){
	*f = d - e;
	*c = "SUBTRACT operation performed";
	vpi_printf("%d - %d = %d\n",d,e,*f);
  }
  else{
	*c = "No operation performed";
  }
  return svIsDisabledState(); /* Since this task is not in disabled state, so return value is 0 */
}

/* nbcode "operatestrings" end */
  

SV:

module top();


// nbcode "importstring" start

import "DPI-C" task operate_strings(input string a, output string c,input int d, input int e, output int f);

// nbcode "importstring" end

string a,b,c;
int d,e,f;

initial
  begin
  a = "ADD";
  b = "SUBTRACT";
  d = 400;
  e = 200;
  operate_strings(a,c,d,e,f);
  $display("%s\n",c);
  operate_strings(b,c,d,e,f);
  $display("%s\n",c);
  end
endmodule

LOG:

vcs -full64 \
-kdb -lca \
-sverilog \
-l vcs.log \
./add.c \
./top.v \
-R

Operation to be performed = ADD
400 + 200 = 600
ADD operation performed

Operation to be performed = SUBTRACT
400 - 200 = 200
SUBTRACT operation performed
structure

structure类型也是使用指针的方式,参考实例:https://github.com/holdenQWER/cvt_dpi
vcs会根据SV侧的structure自动推测生成C侧的structure:

SV:
typedef struct{
   int H_resol;
   int W_resol;
   shortreal refresh_rate;
   int RB_v11;
   int RB_v11_force;
   int RB_v12;
   int film_optimized;
   int interlaced;
} argv_struct;

C:
in vc_hdrs.h
struct _vcs_dpi_argv_struct {
   int H_resol;
   int W_resol;
   float refresh_rate;
   int RB_v11;
   int RB_v11_force;
   int RB_v12;
   int film_optimized;
   int interlaced;
};

Topics

only C code

DPI只支持C代码,C++代码需要C wrapper封装;g++编译时,需要extern "C"封装;

分配内存

C代码需要手动分配内存,否则仿真遇到Segmentation fault时,难以debug。
常见的内存操作如下:
typedef struct { unsigned char cnt; } c7; c7 * c = (c7*) malloc(sizeof(c7)); : 为c7类型的句柄c调用malloc分配堆空间;
free(c); : 释放堆空间

char src[] = "sample test"; char dest[50]; memcpy(dest,src,strlen(src)+1); : 复制srcdest,字节数为strlen(src)+1

char src[] = "Hello World!"; char dest[50]; strcpy(dest,src) : 将src复制到dest;只用于字符串的复制;

CPP中:
std::vector<unsigned int> dat; dat = std::vector<unsigned int>(dat_h,(dat_h+dat_size)); : 为vector数组dat复制地址dat_hdat_h+dat_size的内存数据;
dat.clear():清空dat数组

int *p1 = new int[10]; : 使用new创建一个10元素的数组,p1指向数组首元素;
delete [] p1; :释放内存空间

C11:
unique_ptr<int> pInt(new int(5)); 使用unique_ptr,自动回收内存。

info打印

C侧调用的printf函数,打印信息只会显示在terminal上;调用io_pirntf 函数(include $VCS_HOME/include/veriuser.h),可以将打印信息放入仿真工具的log中。

chandle

chandle在SV代码中代表C端的一个void*句柄;void*句柄可以向任意类型转换🔗void* 详解及应用;SV侧每次调用C侧函数,函数原有的栈空间都会在调用结束时释放,通过chandle的回传,可以保证数据,实例或者特定程序在仿真过程中持续存在:

//top.sv
import "DPI-C" function void counter7(input chandle inst,output bit[6:0] out,input bit[6:0] in,input bit reset,load);
import "DPI-C" function chandle counter7_new();

module tb;
   bit [6:0] o1,o2,i1,i2;
   bit reset,load,clk;
   chandle inst1,inst2;

   initial begin
      inst1 = counter7_new();
      inst2 = counter7_new();
      reset = 0;
      load = 0;
      i1 =120;
      i2 = 10;
      fork
         forever #10 clk = ~clk;
         forever @(posedge clk) begin
            counter7(inst1,o1,i1,reset,load);
            counter7(inst2,o2,i2,reset,load);
         end
      join_none

      @(negedge clk) load = 1;
      @(negedge clk) load = 0;
      @(negedge clk) $finish;
   end
endmodule
//counter7.c
#include <svdpi.h>
#include <malloc.h>
#include <veriuser.h>
typedef struct {
   unsigned char cnt;
} c7;

void * counter7_new() {
   c7 * c = (c7*) malloc(sizeof(c7));
   c->cnt = 0;
   return c;
}

void counter7(c7 *inst,
              svBitVecVal *count,
              const svBitVecVal *i,
              const svBit reset,
              const svBit load
      ) {
   io_printf("addr:%p",inst);
   if(reset) inst->cnt = 0;
   else if (load) inst->cnt=*i;
   else inst->cnt++;
   inst->cnt &=0x7f;
   *count = inst->cnt;
   io_printf("C:count=%d,i=%d,reset=%d,load=%d\n",*count,*i,reset,load);
}
//makefile
all: clean
	vcs -full64 \
	-kdb -lca \
	-sverilog \
	-l vcs.log \
	./counter7.c \
	./top.sv \
	-R

clean:
	-rm -rf simv* *.log csrc ucli.key ucli.key
pure and context

DPI把C函数分成 pure函数,context函数或者 generic函数。

Pure C函数:
作为pure函数,函数的结果必须仅仅依赖于通过形参传递进来的数值。Pure函数的优点在于仿真器可以执行优化以改进仿真性能。Pure函数不能使用全局或者静态变量,不能执行文件I/O操作,不能访问操作系统环境变量,不能调用来自Verilog PLI库的函数。只有没有输出或者inout的非void函数可以被指定成pure。
例如:
import "DPI-C" pure function int calc_parity(input int a);

Context C函数:
context C函数明白函数声明所在工作域的Verilog的层次。 这使得被导入的C函数能够调用来自PLI TF,ACC或者VPI库的函数, 从而DPI函数可以充分利用PLI的优势特性, 比如写仿真器的log文件以及Verilog源代码打开的文件。
如果一个import函数调用了export函数,也需要声明为context类型。
例如:
import "DPI-C“ context function int myclassfunc_func1();

Generic C函数:
那些既没有明确声明为pure,也没有声明为context的函数称为generic函数(SystemVerilog标准没有给除了pure或context之外的函数特定的称呼)。generic C函数可以作为Verilog函数或者Verilog任务导入。任务或者函数可以由输入、输出以及inout的参数。函数可以有一个返回值,或者声明为void。generic C函数不允许调用Verilog PLI函数,不能访问除了参数以外的任何数据,只能修改这些参数。

inout

DPI不支持ref修饰形参,可以使用inout代替;
例如:
import "DPI-C" context task mod_t(inout real r[10]);

export scope

SV引入了编译单元(compilation unit)这一概念,一起编译的源文件的一个组合。$unitmoduleprogrampackage都会划分编译单元;其中$unit代表全局;

对于import函数来说,SV侧调用C函数,和正常SV函数一样,只要在可见范围内,就可以被正常调用;

但是对于export函数,因为是C侧调用,SV侧定义,所以C侧调用需要显示指明context上下文,就是通过svSetScope指明SV侧被调用函数定义的位置;

函数含义
svGetScope获取当前C侧的svScope
svGetScopeFromName将string类型的作用域转化为svScope类型
svGetNameFromScopesvScope类型转化为string类型的作用域
svSetScope在调用export函数之前,设定export函数的context上下文

示例1:
save_my_scopeblock中调用,my_scope = svGetScope();获得当前blockmodule的scope:top.b1
block先调用c_display,打印当前调用的scope,然后为sv_display设置scope,使得C端调用block中定义的sv_diaplay函数;
top之后调用c_display,打印当前调用的scope,然后为sv_display设置scope(top.b1),使得C端调用block中定义的sv_diaplay函数;

svSetScope(my_scope);用于设置调用的export函数sv_diaplay是定义在top中还是block中。
对于import函数c_display(),都是调用C侧唯一的一个函数;但是因为blocktop是独立的两个作用域空间,所以需要分别import一次。

打印log:

C: c_display called from scope top.b1
C: calling top.b1.sv_display
SV: In top.b1.sv_display

C: c_display called from scope top
C: calling top.b1.sv_display
SV: In top.b1.sv_display

//top.sv
module block;
   import "DPI-C" context function void c_display();
   import "DPI-C" context function void save_my_scope();
   export "DPI-C" function sv_display;

   function void sv_display();
      $display("SV: In %m");
   endfunction

   initial begin
      save_my_scope();
      c_display();
   end

endmodule

module top;
   import "DPI-C" context function void c_display();
   export "DPI-C" function sv_display;

   function void sv_display();
      $display("SV: In %m");
   endfunction

   block b1();

   initial #1 c_display();

endmodule
//test.c
#include <svdpi.h>
#include "veriuser.h"

extern void sv_display();
svScope my_scope;

void save_my_scope() {
   my_scope = svGetScope();
}

void c_display() {
   io_printf("\nC: c_display called from scope %s\n",svGetNameFromScope(svGetScope()));

   svSetScope(my_scope);
   io_printf("C: calling %s.sv_display\n",svGetNameFromScope(svGetScope()));
   sv_display();
}

//makefile
all: clean
	vcs -full64 \
	-kdb -lca \
	-sverilog \
	-l vcs.log \
	./test.c \
	./top.sv \
	-R

clean:
	-rm -rf simv* *.log csrc ucli.key ucli.key

示例2:
如果注释掉scope_name = svGetScopeFromName("$unit");,则会报如下错误:

Error-[DPI-DXFNF] DPI export function not found The DPI export function/task ‘store_in_fifo’ called from a user/external
C/C++/DPI-C code originated from import DPI function ‘store_output_data’ at file ‘./top.sv’(line 75) is not defined or visible.
Please check the called DPI export function/task is defined in the mentioned module, or check if the DPI declaration of the DPI import function/task which invokes that DPI export function/task is made with ‘context’. Another work-around is using svGetScopeFromName/svSetScope to explicitly set the scope to the module which contains the definition of the DPI export function/task.

因为store_in_fifo放在SV测的$unit作用域;store_output_datatop中被调用,默认处于top作用域;需要重新设定scop为$unit,才可以调用store_in_fifo

//top.sv
typedef struct {
               bit enable;
               int cnt;
               int op;
               } packet;
  
  parameter MAX_CNT = 5;
  parameter FIFO_SIZE = 10;

  packet p;
  int a, b, out, ret, fifo_ptr, enable;
  bit[1:0] rand_data;

  int result_fifo[FIFO_SIZE];

// nbcode "formalarg" start
  import "DPI-C" context function int math_operation(inout packet p, input int a, input int b, output int out);
// nbcode "formalarg" end

  export "DPI-C" function valid_operation;

// nbcode "disablep" start
  function int valid_operation(input int cnt);
    if(cnt > MAX_CNT) begin
      $display("The number of valid operations has exceeded the limit.");
      $display("The store_output_data function will be disabled");
      enable = 0;
      disable store_output_data;
    end
    else begin
      $display("The number of valid operations has not reached its upper limit.");
      $display("More operations can be performed"); 
    end
  endfunction
// nbcode "disablep" end

  function int fifo_full();
    if(fifo_ptr > FIFO_SIZE)
      return 1;
    else
      return 0;
  endfunction

  import "DPI-C" context task store_output_data(inout packet p);

  export "DPI-C" task store_in_fifo;
  task store_in_fifo();
    if(!fifo_full()) begin
      ret = math_operation(p,a,b,out);
      if(enable) begin
        fifo_ptr++;
        result_fifo[fifo_ptr] = out;
      end
    end
    else
      $display("FIFO is already full."); 
  endtask

// nbcode "compileunit" end

module top();

  initial
  begin
    a = 400;
    b = 200;
    fifo_ptr = 0;
    p.enable = 1'b1;
    enable   = 1;
    for(int i = 1; i < 7; i++) begin
      if(std::randomize(rand_data)) begin
        p.cnt = i;
        p.op  = rand_data; 
        store_output_data(p);
      end
    end
  end

endmodule 

//calc.c
#include <stdio.h>
#include "svdpi.h"
#include "vpi_user.h"

/* nbcode "structlayout" start */
typedef struct {
  char enable;
  int cnt;
  int op;
} data;
/* nbcode "structlayout" end */
extern int valid_operation (int _a1);
extern int store_in_fifo();

int ret;
/* nbcode "disableack" start */
int store_output_data(data *p)
{
  svScope scope_name;
  scope_name = svGetScope();
  vpi_printf("KKK1 scope_name:%s\n",svGetNameFromScope(scope_name));

  scope_name = svGetScopeFromName("$unit");
  svSetScope(scope_name);
  vpi_printf("KKK2 scope_name:%s\n",svGetNameFromScope(scope_name));

  if(p->enable == 1){
    store_in_fifo();
  }
  /* The following API is used to check the whether the import task 
     has been disabled by export task or not*/

  if(svIsDisabledState() == 1) {
    vpi_printf("The imported task store_output_data has been disabled\n");
  }
  return (svIsDisabledState());
}
/* nbcode "disableack" end */ 

/* nbcode "formalpointer" start */
/* The import task returns an int */
int math_operation(data *p, int a, int b, int *c)
{
  /* nbcode "formalpointer" end */
  /*The export task returns an int */
  ret = valid_operation(p->cnt);

  /* The following API is used to check the whether the import task 
     has been disabled by export task or not*/

  if(svIsDisabledState() == 1) {
    vpi_printf("The imported function math_operation has been disabled\n");
    svAckDisabledState();
  }
  else {
    switch(p->op) {
    case 0 : /* ADD */
             *c = a + b;
             printf(" Sum             of %d and %d is %d\n", a, b, *c);
             break;

    case 1 : /* SUB */
             *c = a - b;
             printf(" Difference      of %d and %d is %d\n", a, b, *c);
             break;

    case 2 : /* MUL */
             *c = a * b;
             printf(" Multliplication of %d and %d is %d\n", a, b, *c);
             break;

    case 3 : /* DIV */
             if(b != 0)
               *c = a / b;
               printf(" Division        of %d and %d is %d\n", a, b, *c);
             break;

    default: break; 
    }
  }
  return *c;
}
//makefile
all: clean
	vcs -full64 \
	-kdb -lca \
	-sverilog \
	-l vcs.log \
	./calc.c \
	./top.sv \
	-R

clean:
	-rm -rf simv* *.log csrc ucli.key ucli.key
  • 18
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
该资源内项目源码是个人的课程设计、毕业设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。 该资源内项目源码是个人的课程设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。
该资源内项目源码是个人的课程设计、毕业设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。 该资源内项目源码是个人的课程设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

劲仔小鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值