05-SA8155 QNX SPI框架及代码分析-程序员宅基地

技术标签: SA8155P平台开发  resmgr_attach  SA8155  SA8155 SPI  QNX SPI  SPI  

1. 描述

本文主要描述QNX SPI Drvier的相关内容,并以SA8155P处理器为例讲解SPI框架。

2. 目录结构

2.1 HW Drivers:

路径:apps/qnx_ap/AMSS/platform/hwdrivers/wired_peripherals/spi

├── aarch64
│   ├── Makefile
│   └── so-le
├── arm
│   ├── Makefile
│   └── so-le-v7
│       └── Makefile
├── common.mk
├── device
│   ├── inc
│   │   ├── SpiDalProps.h
│   │   ├── SpiDeviceError.h
│   │   ├── SpiDevice.h
│   │   ├── SpiDeviceInternal.h
│   │   ├── SpiDeviceOsSvc.h
│   │   ├── SpiDevicePlatSvc.h
│   │   ├── SpiDeviceTransfer.h
│   │   └── SpiDeviceTypes.h
│   ├── SpiDalProps.c
│   ├── SpiDevice.c
│   ├── SpiDeviceOsSvc.c
│   ├── SpiDevicePlatSvc.c
│   └── SpiDeviceTransfer.c
├── driver
│   ├── inc
│   │   └── SpiDriverTypes.h
│   └── SpiDriver.c
├── logs
│   ├── inc
│   │   └── SpiLog.h
│   └── SpiLog.c
├── Makefile
└── public
    └── amss
        └── core

2.2. SPI Resource(SPI资源管理器)

apps/qnx_ap/AMSS/platform/resources/spi_drv


├── aarch64
│   ├── Makefile
│   └── so-le
├── arm
│   ├── Makefile
│   └── so-le-v7
│       └── Makefile
├── common.mk
├── Makefile
├── protected
│   ├── spi_devctls.h
│   └── spi_lib.h
└── spi_drv.c
 

2.3. SPI Service(SPI服务进程) 

/apps/qnx_ap/AMSS/platform/services/daemons/spi_service


├── aarch64
│   ├── Makefile
│   └── o-le
├── arm
│   ├── Makefile
│   └── o-le-v7
│       └── Makefile
├── common.mk
├── Makefile
└── src
    └── spi_service.c

2.4. API

apps/qnx_ap/AMSS/platform/qal/clients/spi_client

app/qnx_ap/qnx_bins/prebuilt_QNX700/target/qnx7/usr/include/hw/spi-master.h

├── aarch64
│   ├── Makefile
│   └── so-le
├── arm
│   ├── Makefile
│   └── so-le-v7
│       └── Makefile
├── common.mk
├── Makefile
├── pinfo.mk
├── public
│   └── amss
│       └── spi_client.h
└── src
    └── spi_client.c

3. API 

3.1 API接口

 app/qnx_ap/qnx_bins/prebuilt_QNX700/target/qnx7/usr/include/hw/spi-master.h

 * SPI API calls
 */
int	spi_open(const char *path);
int spi_close(int fd);
int spi_setcfg(int fd, uint32_t device, spi_cfg_t *cfg);
int spi_getdevinfo(int fd, uint32_t device, spi_devinfo_t *devinfo);
int spi_getdrvinfo(int fd, spi_drvinfo_t *drvinfo);
int spi_read(int fd, uint32_t device, void *buf, int len);
int spi_write(int fd, uint32_t device, void *buf, int len);
int spi_xchange(int fd, uint32_t device, void *wbuf, void *rbuf, int len);
int spi_cmdread(int fd, uint32_t device, void *cbuf, int16_t clen, void *rbuf, int rlen);
int spi_dma_xchange(int fd, uint32_t device, void *wbuf, void *rbuf, int len);
int spi_dma_xfer(int fd, uint32_t device, void *paddr, int len);

4. SPI资源管理器设计

在QNX下开发驱动程序,最主要的工作除了了解底层硬件具体工作流程外,就是建立一个能与操
作系统兼容且支持POSIX的Resource manger框架了。在任何一段程序的执行过程中一段都是从
main函数开始的,然而在操作系统中的main函数还传递了两个参数:int argc, char argv,这两个
参数是用来传递从shell命令行或者buildfile中传来对Resource manger具体参数的,使用options
(int argc, char argv);函数实现,所以这个函数在main函数中最开始的位置,可以开发的driver具有
不同可选的特性,提供使用的便利性。

4.1 Spi Service Demo进程

spi_service.c 基本没做什么,就是一个壳子,核心工作如下

  • 调用spi_drv.c资源管理初始化spi驱动spi_drv_init()
  • 启动服务spi_service(后台进程)

4.2  spi资源管理器核心spi_drv.c 

入口函数:spi_drv_init

/*===========================================================================
FUNCTION: spi_drv_init

DESCRIPTION : This function init the SPI-RM
===========================================================================*/
int spi_drv_init(void)
{
   int i = 0, idx = 0, rc = 0;
   uint64_t chip_id = 0;
   const void *fdt_paddr = 0;
   pthread_t threadID;
   DALSYSPropertyVar PropVar;
   DALSYS_PROPERTY_HANDLE_DECLARE(hDALProps);

   DALSYS_InitMod(NULL);
   DALSYS_RegisterMod(&gDALModDriverInfoList);

   fdt_paddr = fdt_get_root();
   if (!fdt_paddr) {
      SPI_SLOGE("SPI_RM: Failed to load device tree");
      return -1;
   }
   rc = fdt_foreach_subnode_byname((void*) fdt_paddr , "/chip_info",
                                   &get_chip_info, &chip_id);
   if (rc) {
      SPI_SLOGE("SPI_RM: Failed to find dt chip_info");
      return -1;
   }

   /* Create RM's for each active SPI bus */
   int ret = EOK;

   int policy;
   struct sched_param param;
   pthread_attr_t attr;

   if (waitfor_attach(QCORE_SERVICE, 5000))
   {
      SPI_SLOGE("Timed out waiting for %s to be ready", QCORE_SERVICE);
      return -1;
   }

   //线程配置
   pthread_attr_init(&attr);
   pthread_getschedparam(pthread_self(), &policy, &param);
   param.sched_priority = 100;
   pthread_attr_setschedparam(&attr, &param);

   //资源管理器创建
   for (i = 0; i < MAX_NUM_SPI_DEVS; i++)
   {
      if(DALSYS_GetDALPropertyHandle(DeviceID[i], hDALProps)==DAL_SUCCESS)
      {
         if (DAL_SUCCESS != DALSYS_GetPropertyValue(hDALProps, "SPI_ENABLED", 0, &PropVar)
             || PropVar.Val.dwVal == 0)
         {
            continue;
         }

         devs[idx] = calloc(1, sizeof(spi_dev_t));
         if (devs[idx] == NULL)
         {
            pthread_attr_destroy(&attr);
            return -1;
         }

         snprintf(devs[idx]->devname, MAX_DEVNAME_LENGTH, "/dev/spi%d", i+1);
         devs[idx]->spi_idx = i;
         devs[idx]->initialized = 0;
#ifdef SPI_LPM_TIMER
         devs[idx]->timer_created = 0;
#endif

         if (DAL_SUCCESS != DALSYS_GetPropertyValue(hDALProps, "CLOCK_SE_NAME", 0, &PropVar)
             || PropVar.Val.pszVal == 0)
         {
            pthread_attr_destroy(&attr);
            return -1;
         }

         if (!strncmp(PropVar.Val.pszVal, "scc", 3)) {
            devs[idx]->is_ssc = true;
         }

         //具体实现核心代码
         ret = pthread_create(&threadID, &attr, (void *)&spi_device_main_thread,
                              (void *)devs[idx]);

         if (ret == EOK)
         {
            pthread_setname_np(threadID, devs[idx]->devname);
            SPI_SLOGD("SPI_RM: Created RM thread for device-%d:name-%s", DeviceID[i], devs[idx]->devname);
            idx++;
         }
         else
         {
            SPI_SLOGE("Couldn't create RM thread for device-%d:name-%s:ret-%d",
            DeviceID[i], devs[idx]->devname, ret);
         }
      }
   }

   SPI_SLOGI("SPI_RM created %d threads.", idx);

   if (ID_6155 == chip_id) {
      if ((rc = spi_register_ssr())) {
         SPI_SLOGE("SPI_RM: Failed to register for SSR ret=%x\n", ret);
         return -1;
      }

      SPI_SLOGI("SPI_RM registered for SSR.");
   }

   return 0;
}

 资源管理器创建线程实现:

 标准步骤:

  1. 建立一个上下文切换句柄dpp = dispatch_create();这个东东主要用在mainloop中产生一个block特性,可以让我们等待接受消息;
  2. iofunc初始化。这一步是将自己实现的函数与POSIX层函数进行接口,解析从read、write、devctl等函数传来的消息进行解析,以实现底层与应用层函数之间的交互,通过io_funcs.read = io_read,io_funcs.write = io_write,进行函数重载;
  3. 注册设备名,使设备在命名空间中产生相应的名称,这一点是整个过程的关键了,形如 pathID = resmgr_attach (dpp, &rattr, "/dev/Null",_FTYPE_ANY, 0, &connect_funcs,&io_funcs, &ioattr),这样不仅注册了一个设备名,还让系统知道了我们实习的IO函数对应关系;
  4. 为之前创建的上下文句柄分配空间,例如ctp = dispatch_context_alloc (dpp);为了第六步使用;
  5. 通过不断循环等待dispatch_block()来调用MsgReceive()使Resource manger处于receive block状态,以接收上层发送来的消息,通过dispatch_handler (ctp)去调用我们自己定义的IO函数

SA8155平台是如何做的呢?

看下代码,基本类似。

/*===========================================================================
FUNCTION: spi_device_main_thread

DESCRIPTION : main thread to create and handle device.
===========================================================================*/
int spi_device_main_thread(spi_dev_t *dev)
{
   resmgr_connect_funcs_t connect_funcs;
   resmgr_io_funcs_t io_funcs;
   resmgr_attr_t rattr;
   iofunc_funcs_t ocb_funcs = { _IOFUNC_NFUNCS, _ocb_calloc, _ocb_free };
   iofunc_mount_t mount = { 0, 0, 0, 0, &ocb_funcs };
   int pathID;

   /*
    * Without this InterruptLock() in dalinterrupt will cause SIGSEGV
    * when calling from multiple threads
    */
   ThreadCtl(_NTO_TCTL_IO, 0);

#ifdef SPI_LPM_TIMER
   pthread_cond_init(&dev->clk_mutex, NULL);
#endif /* SPI_LPM_TIMER */
   //创建一个通讯Channel,返回chid
   dev->chid = ChannelCreate(_NTO_CHF_DISCONNECT | _NTO_CHF_UNBLOCK);
   if (dev->chid == -1) {
      SPI_SLOGE("ChannelCreate() failed, err=%d\n", errno);
      goto exit;
   }
   //1. 建立一个上下文切换句柄dpp

   /*
    * allocate and initialize a dispatch structure for use by our
    * main loop
    */
   dev->dpp = dispatch_create_channel( dev->chid, 0 );
   if (dev->dpp == NULL) {
      SPI_SLOGE("SPI_RM: couldn't dispatch_create. ");
      goto exit;
   }

   /* register internal communication channel */
   dev->int_coid = ConnectAttach(ND_LOCAL_NODE, 0 /* pid */, dev->chid, _NTO_SIDE_CHANNEL, 0);
   if (-1 == dev->int_coid) {
      SPI_SLOGE("SPI_RM internal ConnectAttach failed (%s)", strerror(errno));
      goto exit;
   }

   /*
    * set up the resource manager attributes structure, we'll
    * use this as a way of passing information to resmgr_attach().
    * For now, we just use defaults.
    */

   memset(&rattr, 0, sizeof(rattr)); /* using the defaults for rattr */
   rattr.nparts_max = 10;
   rattr.msg_max_size = (64 *4* 1024); //Max HW allowed transaction 64k

   //2. 调用iofunc_func_init初始化iofunc,connect_funcs
   /*
    * intialize the connect functions and I/O functions tables to
    * their defaults by calling iofunc_func_init().
    *
    * connect_funcs, and io_funcs variables are already declared.
    *
    */

   iofunc_func_init(_RESMGR_CONNECT_NFUNCS, &connect_funcs, _RESMGR_IO_NFUNCS,
                    &io_funcs);

   /* over-ride the connect_funcs handler for open with our io_open,
    * and over-ride the io_funcs handlers for read and write with our
    * io_read and io_write handlers
    */
   connect_funcs.open = io_open;
   io_funcs.devctl = io_devctl;
   io_funcs.write = io_write;
   io_funcs.close_ocb = io_close;

   /* initialize our device description structure
   */
   /* io_attr 其实可以想像成一个文件相关的参数,比如读写权限等等 */
   iofunc_attr_init(&dev->hdr, S_IFCHR | 0666, NULL, NULL);

   dev->hdr.mount = &mount; // so we can alloc an OCB per open

   //3. resmgr_attach注册设备,注册一个资源设备名为dev->devname
   /*
    *  call resmgr_attach to register our prefix with the
    *  process manager, and also to let it know about our connect
    *  and I/O functions.
    *
    *  On error, returns -1 and errno is set.
    */
   pathID = resmgr_attach(dev->dpp, &rattr, dev->devname, _FTYPE_ANY, 0,
                          &connect_funcs, &io_funcs, (IOFUNC_ATTR_T*)dev);
   if (pathID == -1) {
      SPI_SLOGE("SPI_RM: Couldn't attach pathname: %s", strerror(errno));
      exit(1);
   }

#ifdef SPI_LPM_TIMER
   if ((dev->pulse_code = pulse_attach(dev->dpp, MSG_FLAG_ALLOC_PULSE, 0,
                                        &spi_stop_timer, (void*)dev)) == -1) {
      SPI_SLOGE("SPI_RM: pulse_attach failed - %s", strerror(errno));
      exit(1);
   }
#endif

   //4. 为之前创建的上下文句柄分配空间
   dev->ctp = dispatch_context_alloc(dev->dpp);
   if (dev->ctp == NULL) {
      SPI_SLOGE("SPI_RM: Could't alloc resmgr context - %s", strerror(errno));
      dispatch_destroy(dev->dpp);
      exit(1);
   }

   /* Notify bmetrics this device is ready */
   int fd = open("/dev/bmetrics", O_WRONLY);
   if (fd == -1) {
      SPI_SLOGE("SPI_RM: Couldn't open /dev/bmetrics");
   } else {
      char buf[30];
      snprintf(buf, 30, "bootmarker %s ready", dev->devname);
      if (-1 == write(fd, buf, 30)) {
         SPI_SLOGE("SPI_RM: Couldn't write /dev/bmetrics");
      }
      close(fd);
   }

   /* register LPM pulses */
   if (EOK != spi_register_lpm_pulse(dev))
   {
      SPI_SLOGE("SPI_RM: Failed to register pulses for %s(%s)", dev->devname, strerror(errno));
   }

   /* register internal pulses */
   if (EOK != spi_register_timeout_pulse(dev))
   {
      SPI_SLOGE("SPI_RM: Failed to register timeout pulse for %s(%s)", dev->devname, strerror(errno));
   }

   /* Initialize Spi device */
   if (EOK != spi_hwd_init(dev))
   {
      SPI_SLOGE("SPI_RM: Failed to init spi hardware %s(%s)", dev->devname, strerror(errno));
      dispatch_destroy(dev->dpp);
      exit(1);
   }

   /**5. 通过不断循环等待dispatch_block()与dispatch_handler (ctp)执行IO
    函数处理。
dispath_block() 相当于阻塞并等待,而 dispatch_handle() 则根据不同的挂接,调用不同的回调函
数进行处理。其实在_spi_register_interface里进行了dispatch_context_alloc的操作。通过不断循环
等待dispatch_block()来调用MsgReceive()使Resource manger处于receive block状态,以接收上层
发送来的消息,通过dispatch_handler (ctp)去调用我们自己定义的IO函数。
    */

   /*Message handling*/
   while (1)
   {
      if (dispatch_block(dev->ctp))
      {
         dispatch_handler(dev->ctp);
      }
      else if (errno != EFAULT)
      {
         break;
      }
      else
      {
         /* Do nothing */
      }
   }

exit:
   return -1;
}

 4.3  API与资源管理器之间的关联

devctl:

extern int devctl(int fd, int dcmd, void *dev_data_ptr, size_t nbytes, int *dev_info_ptr);
extern int devctlv(int fd, int dcmd, int sparts, int rparts, const struct iovec *sv, const struct iovec *rv, int *dev_info_ptr);

 

 

 open:

 

 5. APP例子

#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

#include "mmdefs.h"
#include "log.h"
#include "spi_driver.h"

#ifdef __QNX__
#include "spi_client.h"
#include "SpiDriver.h"
#endif

/* SPI driver handle */
static int fd = 0;

static const char *device_name = "/dev/spi1";

/* Main Interface */
int SPI_Init()
{
    int rc = -1;

    /* *******************************************************************************
     * During application mode both SPI0 and SPI1 operate as slaves and, by default,
     * configured in mode 1 (CPOL=0, CPHA=1) and for operation at a speed of 10MHz.
     * *******************************************************************************/
    fd = spi_open(device_name);
    if(fd == -1)
    {
        LOG("spi_open failed, fd=%d\n", fd);
        fd = 0;
    }
    else
    {
        LOG("spi_open successful, fd=%d\n", fd);

        spi_cfg_t cfg;
        // need bits_per_word as LSB in spi cfg
        cfg.mode = SPI_MODE_BODER_MSB | SPI_MODE_CSHOLD_HIGH | SPI_MODE_CKPHASE_HALF | ((0 << SPI_MODE_DEASSERT_WAIT_SHFT) & SPI_MODE_DEASSERT_WAIT_MASK) | 8;
        cfg.clock_rate = 2000000;
        LOG("spi_setcfg mode %x, rate %x\n", cfg.mode, cfg.clock_rate);
        rc = spi_setcfg(fd, SPI_DEVICE_1, &cfg);
        if(rc)
        {
            LOG("spi_setcfg failed, rc=%d\n", rc);
            close(fd);
            fd = 0;
        }
        else
        {
            LOG("spi_setcfg successful, rc=%d\n", rc);
        }
    }
	return rc;
}

void SPI_Deinit()
{
    if(fd)
    {
        spi_close(fd);
        fd = 0;
    }
}

/* SPI write functions */
int SPI_Write(uint8_t* buf, uint32_t len)
{
    int rc = 0;

    if(fd)
    {
        rc = spi_write(fd, SPI_DEVICE_1, buf, len);
        //rc = spi_cmdread(fd, SPI_DEVICE_1, buf, len, NULL, 0);
        if(rc != len)
        {
            LOG("spi_write failed, rc=%d\n", rc);
        }
    }
    else
    {
        rc = -1;
    }

    return rc;
}

/* SPI read functions */
int SPI_Read(uint8_t* buf, uint32_t len)
{
    int rc = 0;

    if(fd)
    {
        rc = spi_cmdread(fd, SPI_DEVICE_1, NULL, 0, buf, len);
        if(rc != len)
        {
            LOG("spi_read failed, rc=%d\n", rc);
        }
    }
    else
    {
        rc = -1;
    }

    return rc;
}

/* SPI write & read function */
int SPI_Write_Read(uint8_t* tx_buf, uint8_t* rx_buf, uint32_t len)
{
    int rc = 0;

    if( fd )
    {
        rc = spi_cmdread(fd, SPI_DEVICE_1, tx_buf, len, rx_buf, len);
        if( rc != len )
        {
            LOG("spi_cmdread failed, rc=%d\n", rc);
        }
    }
    else
    {
        rc = -1;
    }

    return rc;
}

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/liaochaoyun/article/details/127314018

智能推荐

第6章函数-5 使用函数求余弦函数的近似值-程序员宅基地

文章浏览阅读1.1k次。本题要求实现一个函数,用下列公式求cos(x)近似值,精确到最后一项的绝对值小于eps(绝对值小于eps的项不要加):cos(x)=0!x0​−2!x2​+4!x4​−6!x6​+...函数接口定义:funcos(eps,x),其中用户传入的参数为eps和x;函数funcos应返回用给定公式计算出来,保留小数4位。输出样例:代码长度限制16 KB时间限制400 ms内存限制..._使用函数求余弦函数的近似值

STL源码剖析-关联式容器之hash_set、hash_map、hash_multiset和hash_multimap_stl源码hash_set-程序员宅基地

文章浏览阅读431次。一、hash_set1、hash_set以hashtable为底层机制,hash_set的操作几乎都是转调用hashtable的函数而已。2、hash_set的元素没有自动排序功能。3、hash_set的使用方式与set完全相同。4、测试例子 #include #include using namespace std;_stl源码hash_set

Vlog简介-程序员宅基地

文章浏览阅读995次。https://www.ifanr.com/1138470转载于:https://www.cnblogs.com/pengwang52/p/10683069.html_校园vlog简介怎么写

python稳健回归_【Stata教程】如何用stata做稳健回归-程序员宅基地

文章浏览阅读1.2k次。“社会科学中的数据可视化”第411篇推送导言大量的线性回归模型是基于最小二乘法实现的,但其仍存在一些局限性。比如说,样本点出现许多异常点时,传统的最小二乘法将不再适用,此时则可以使用稳健回归(robust regression)代替最小二乘法。操作下面的稳健回归使用的是犯罪数据,该数据来自Alan Agresti和Barbara Finlay的《社会科学统计方法》。变量包括美国各州编号(sid)、..._margins 贫困

爬虫.requests.exceptions.ConnectionErro-程序员宅基地

文章浏览阅读219次。requests.exceptions.ConnectionError: HTTPConnectionPool(host='jy-qj.com.cn', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<requests.packages.urllib3.connection.HTTPConn..._requests.exceptions.connectionerror: errno1104 getaddrinfo failed

[C++]欧几里得辗转相除求最大公约数,练习_欧几里得算法c++练习题-程序员宅基地

文章浏览阅读1.1k次。编程实现求解最大公约数的欧几里德算法,用户输入两个任意正整数,程序输出他们的最大公约数。算法如下:拆解步骤如下:步骤1: 如果p < q,则交换p和q。步骤2: 令r是p / q 的余数。步骤3: 如果r = 0,则令g = q并终止;否则令p = q, q = r并转向步骤2#include<iostream>#include<stdio.h>//编程实现求解最大公约数的欧几里德算法,用户输入两..._欧几里得算法c++练习题

随便推点

hdu 5072 Coprime 容斥原理_hdu 5072 coprime (容斥)-程序员宅基地

文章浏览阅读662次。CoprimeTime Limit: 2000/1000 MS (Java/Others) Memory Limit: 262144/262144 K (Java/Others)Total Submission(s): 1460 Accepted Submission(s): 571Problem DescriptionThere are n peopl_hdu 5072 coprime (容斥)

vue中,上传图片的流程_vue 图片上传-程序员宅基地

文章浏览阅读3.2k次。则显示"File uploaded successfully."消息,否则显示"Failed to upload file."消息。这样,当用户上传文件时,组件会自动将文件提交到服务器,并根据服务器响应显示不同的消息。语句中处理响应,并根据响应数据显示不同的消息或执行不同的操作。我们还添加了一个"Upload"按钮,用于触发文件上传操作。语句中处理响应,并根据响应数据显示不同的消息。首先,你需要在Vue组件中添加一个上传组件,例如。在这里,我们添加了一个上传组件,并将其绑定到。指令将文件绑定到组件中。_vue 图片上传

ubuntu20.04 从零到rust交叉编译环境搭建_musl-g++ installed-程序员宅基地

文章浏览阅读1.1k次。rust linux通用可执行文件与win环境exe文件生成_musl-g++ installed

rabbitmq详解第二天(消息类型一Fanout 订阅模式)_rabbittemplate fanout-程序员宅基地

文章浏览阅读841次。不明白的可以参考rabbitmq详解第一天(消息类型一回调模式)fanout(订阅) 即 fanout类型的Exchange可以将producer 发送的消息绑定到所有订阅的队列中去. 即发布/订阅机制配置文件import org.springframework.amqp.core.Binding;import org.springframework.amqp.core.BindingBuilder;import org.springframework.amqp.core.Fanout.._rabbittemplate fanout

计算机考试网上报名系统-程序员宅基地

文章浏览阅读732次,点赞27次,收藏20次。目 录(一)计算机等级考试发展状况与趋势……………………………………………………1(二)开发系统的意义………………………………………………………………………1(三)用户群及特点…………………………………………………………………………1二、系统分析………………………………………………………………………………………2(一)系统要达到的目的……………………………………………………………………2(二)系统可行性分析………………………………………………………………………2(三)业务流程分析………

linux C应用开发_linux应用开发-程序员宅基地

文章浏览阅读4.4k次,点赞2次,收藏34次。linux应用开发_linux应用开发

推荐文章

热门文章

相关标签