Robot OS驱动开发

简介: Robot OS中我们要定制一些自己的系统服务,比如前面文章提到的MQTT长连接服务以及机器人移动控制的运动服务,有一些自定义的音频,比如麦克风阵列还涉及到驱动开发。本文介绍在基于Android9.0系统的Robot OS中开发一个最简单的驱动示例。

image.png


1. 背景


Robot OS中我们要定制一些自己的系统服务,比如前面文章提到的MQTT长连接服务以及机器人移动控制的运动服务,有一些自定义的音频,比如麦克风阵列还涉及到驱动开发。本文介绍在基于Android9.0系统的Robot OS中开发一个最简单的驱动示例。


2. 准备工作


Robot OS使用的板子是基于瑞芯微3399 Pro处理器的开源开发板,厂家提供了开源的硬件设计和内核代码,我们下载源码压缩包解压后既可直接编译出完整系统镜像。


驱动程序的开发我们参考网上示例,实现一个最简单4字节寄存器,别人教程基于低版本的Linux内核,我们使用的是4.4版本,使用到的API上略微有些差异。


3. 定义功能


接下来我们开始编写我们的代码。在kernel/drivers下面新建demo目录,并在demo目录下新建demo.h文件:


#ifndef _DEMO_ANDROID_H_
#define _DEMO_ANDROID_H_
#include <linux/cdev.h>
#include <linux/semaphore.h>
#define DEMO_DEVICE_NODE_NAME  "demo"
#define DEMO_DEVICE_FILE_NAME  "demo"
#define DEMO_DEVICE_PROC_NAME  "demo"
#define DEMO_DEVICE_CLASS_NAME "demo"
struct demo_android_dev {
  int val;
  struct semaphore sem;
  struct cdev dev;
};
#endif


这个头文件定义了一些字符串常量宏,此外,还定义了一个字符设备结构体demo_android_dev,这个就是我们虚拟的硬件设备了,val成员变量就代表设备里面的寄存器,它的类型为int,sem成员变量是一个信号量,是用同步访问寄存器val的,dev成员变量是一个内嵌的字符设备,这是Linux驱动程序自定义字符设备结构体的标准方法。


我们还看到用到两个头文件:


  • <linux/cdev.h>:linux内核设备抽象
  • <linux/semaphore.h>:信号量


4. 功能实现


在demo目录中增加demo.c文件,作为驱动程序的实现部分。驱动程序的功能主要是向上层提供访问设备的寄存器的值,包括读和写。我们提供三种访问设备寄存器的方法:


  • 一是通过proc文件系统来访问;


  • 二是通过传统的设备文件的方法来访问;


  • 三是通过devfs文件系统来访问。


首先我们包含必要的头文件和定义三种访问设备的方法:


#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/proc_fs.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include "demo.h"
/*主设备和从设备号变量*/
static int demo_major = 0;
static int demo_minor = 0;
/*设备类别和设备变量*/
static struct class* demo_class = NULL;
static struct demo_android_dev* demo_dev = NULL;
/*传统的设备文件操作方法*/
static int demo_open(struct inode* inode, struct file* filp);
static int demo_release(struct inode* inode, struct file* filp);
static ssize_t demo_read(struct file* filp, char __user *buf, size_t count, loff_t* f_pos);
static ssize_t demo_write(struct file* filp, const char __user *buf, size_t count, loff_t* f_pos);
/*设备文件操作方法表*/
static struct file_operations demo_fops = {
  .owner = THIS_MODULE,
  .open = _open,
  .release = demo_release,
  .read = demo_read,
  .write = demo_write, 
};
/*访问设置属性方法*/
static ssize_t demo_val_show(struct device* dev, struct device_attribute* attr,  char* buf);
static ssize_t demo_val_store(struct device* dev, struct device_attribute* attr, const char* buf, size_t count);
/*定义设备属性*/
static DEVICE_ATTR(val, S_IRUGO | S_IWUSR, demo_val_show, demo_val_store);


4.1 定义传统的设备文件访问方法


这里主要是定义demo_open、demo_release、demo_read和demo_write这四个打开、释放、读和写设备文件的方法:


/*打开设备方法*/
static int demo_open(struct inode* inode, struct file* filp) {
  struct demo_android_dev* dev;        
  /*将自定义设备结构体保存在文件指针的私有数据域中,以便访问设备时拿来用*/
  dev = container_of(inode->i_cdev, struct demo_android_dev, dev);
  filp->private_data = dev;
  return 0;
}
/*设备文件释放时调用,空实现*/
static int demo_release(struct inode* inode, struct file* filp) {
  return 0;
}
/*读取设备的寄存器val的值*/
static ssize_t demo_read(struct file* filp, char __user *buf, size_t count, loff_t* f_pos) {
  ssize_t err = 0;
  struct demo_android_dev* dev = filp->private_data;        
  /*同步访问*/
  if(down_interruptible(&(dev->sem))) {
    return -ERESTARTSYS;
  }
  if(count < sizeof(dev->val)) {
    goto out;
  }        
  /*将寄存器val的值拷贝到用户提供的缓冲区*/
  if(copy_to_user(buf, &(dev->val), sizeof(dev->val))) {
    err = -EFAULT;
    goto out;
  }
  err = sizeof(dev->val);
out:
  up(&(dev->sem));
  return err;
}
/*写设备的寄存器值val*/
static ssize_t demo_write(struct file* filp, const char __user *buf, size_t count, loff_t* f_pos) {
  struct demo_android_dev* dev = filp->private_data;
  ssize_t err = 0;        
  /*同步访问*/
  if(down_interruptible(&(dev->sem))) {
    return -ERESTARTSYS;        
  }        
  if(count != sizeof(dev->val)) {
    goto out;        
  }        
  /*将用户提供的缓冲区的值写到设备寄存器去*/
  if(copy_from_user(&(dev->val), buf, count)) {
    err = -EFAULT;
    goto out;
  }
  err = sizeof(dev->val);
out:
  up(&(dev->sem));
  return err;
}


我们看到这里面主要用到了linux系统函数:


  • container_of
  • down_interruptible
  • copy_from_user
  • copy_from_user


4.2 定义通过devfs文件系统访问方法


这里把设备的寄存器val看成是设备的一个属性,通过读写这个属性来对设备进行访问,主要是实现demo_val_show和demo_val_store两个方法,同时定义了两个内部使用的访问val值的方法__demo_get_val__demo_set_val


/*读取寄存器val的值到缓冲区buf中,内部使用*/
static ssize_t __demo_get_val(struct demo_android_dev* dev, char* buf) {
  int val = 0;        
  /*同步访问*/
  if(down_interruptible(&(dev->sem))) {                
    return -ERESTARTSYS;        
  }        
  val = dev->val;        
  up(&(dev->sem));        
  return snprintf(buf, PAGE_SIZE, "%d\n", val);
}
/*把缓冲区buf的值写到设备寄存器val中去,内部使用*/
static ssize_t __demo_set_val(struct demo_android_dev* dev, const char* buf, size_t count) {
  int val = 0;        
  /*将字符串转换成数字*/        
  val = simple_strtol(buf, NULL, 10);        
  /*同步访问*/        
  if(down_interruptible(&(dev->sem))) {                
    return -ERESTARTSYS;        
  }        
  dev->val = val;        
  up(&(dev->sem));
  return count;
}
/*读取设备属性val*/
static ssize_t demo_val_show(struct device* dev, struct device_attribute* attr, char* buf) {
  struct demo_android_dev* hdev = (struct demo_android_dev*)dev_get_drvdata(dev);        
  return __demo_get_val(hdev, buf);
}
/*写设备属性val*/
static ssize_t demo_val_store(struct device* dev, struct device_attribute* attr, const char* buf, size_t count) { 
  struct demo_android_dev* hdev = (struct demo_android_dev*)dev_get_drvdata(dev);  
  return __demo_set_val(hdev, buf, count);
}


这里我们用到了:


  • up
  • simple_strtol
  • dev_get_drvdata


4.3 定义通过proc文件系统访问方法


主要实现了demo_proc_read和demo_proc_write两个方法,同时定义了在proc文件系统创建和删除文件的方法demo_create_proc和demo_remove_proc:


/*读取设备寄存器val的值,保存在page缓冲区中*/
static ssize_t demo_proc_read(char* page, char** start, off_t off, int count, int* eof, void* data) {
  if(off > 0) {
    *eof = 1;
    return 0;
  }
  return __demo_get_val(demo_dev, page);
}
/*把缓冲区的值buff保存到设备寄存器val中去*/
static ssize_t demo_proc_write(struct file* filp, const char __user *buff, unsigned long len, void* data) {
  int err = 0;
  char* page = NULL;
  if(len > PAGE_SIZE) {
    printk(KERN_ALERT"The buff is too large: %lu.\n", len);
    return -EFAULT;
  }
  page = (char*)__get_free_page(GFP_KERNEL);
  if(!page) {                
    printk(KERN_ALERT"Failed to alloc page.\n");
    return -ENOMEM;
  }        
  /*先把用户提供的缓冲区值拷贝到内核缓冲区中去*/
  if(copy_from_user(page, buff, len)) {
    printk(KERN_ALERT"Failed to copy buff from user.\n");                
    err = -EFAULT;
    goto out;
  }
  err = __demo_set_val(demo_dev, page, len);
out:
  free_page((unsigned long)page);
  return err;
}
/*创建/proc/demo*/
static void demo_create_proc(void) {
  struct proc_dir_entry* entry;
  entry = create_proc_entry(DEMO_DEVICE_PROC_NAME, 0, NULL);
  if(entry) {
    entry->owner = THIS_MODULE;
    entry->read_proc = demo_proc_read;
    entry->write_proc = demo_proc_write;
  }
}
/*删除/proc/demo*/
static void demo_remove_proc(void) {
  remove_proc_entry(DEMO_DEVICE_PROC_NAME, NULL);
}


这是低版本内核的代码,在我们环境中直接编译时报错,发现4.4版本有些API做了修改:


create_proc_entry()函数已经被proc_create()函数取代,在proc_fs.h头文件里也没有此函数(proc_create是在kernel 3.10以及之后的版本中新增的),我们使用proc_create()函数替换create_proc_entry:


#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/kernel.h>   
#include <linux/proc_fs.h>
#include <asm/uaccess.h>
#define BUFSIZE  100
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Liran B.H");
static int irq=20;
module_param(irq,int,0660);
static int mode=1;
module_param(mode,int,0660);
static struct proc_dir_entry *ent;
static ssize_t mywrite(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos) 
{
    int num,c,i,m;
    char buf[BUFSIZE];
    if(*ppos > 0 || count > BUFSIZE)
        return -EFAULT;
    if(copy_from_user(buf, ubuf, count))
        return -EFAULT;
    num = sscanf(buf,"%d %d",&i,&m);
    if(num != 2)
        return -EFAULT;
    irq = i; 
    mode = m;
    c = strlen(buf);
    *ppos = c;
    return c;
}
static ssize_t myread(struct file *file, char __user *ubuf,size_t count, loff_t *ppos) 
{
    char buf[BUFSIZE];
    int len=0;
    if(*ppos > 0 || count < BUFSIZE)
        return 0;
    len += sprintf(buf,"irq = %d\n",irq);
    len += sprintf(buf + len,"mode = %d\n",mode);
    if(copy_to_user(ubuf,buf,len))
        return -EFAULT;
    *ppos = len;
    return len;
}
static struct file_operations myops = 
{
    .owner = THIS_MODULE,
    .read = myread,
    .write = mywrite,
};
static int simple_init(void)
{
    ent=proc_create("mydev",0666,NULL,&myops);
    printk(KERN_ALERT "demo...\n");
    return 0;
}
static void simple_cleanup(void)
{
    proc_remove(ent);
    printk(KERN_WARNING "bye ...\n");
}


5. 定义模块加载和卸载方法


这里配置执行设备注册和初始化操作:


/*初始化设备*/
static int  __demo_setup_dev(struct demo_android_dev* dev) {
  int err;
  dev_t devno = MKDEV(demo_major, demo_minor);
  memset(dev, 0, sizeof(struct demo_android_dev));
  cdev_init(&(dev->dev), &demo_fops);
  dev->dev.owner = THIS_MODULE;
  dev->dev.ops = &demo_fops;        
  /*注册字符设备*/
  err = cdev_add(&(dev->dev),devno, 1);
  if(err) {
    return err;
  }        
  /*初始化信号量和寄存器val的值*/
  init_MUTEX(&(dev->sem));
  dev->val = 0;
  return 0;
}
/*模块加载方法*/
static int __init demo_init(void){ 
  int err = -1;
  dev_t dev = 0;
  struct device* temp = NULL;
  printk(KERN_ALERT"Initializing demo device.\n");        
  /*动态分配主设备和从设备号*/
  err = alloc_chrdev_region(&dev, 0, 1, DEMO_DEVICE_NODE_NAME);
  if(err < 0) {
    printk(KERN_ALERT"Failed to alloc char dev region.\n");
    goto fail;
  }
  demo_major = MAJOR(dev);
  demo_minor = MINOR(dev);        
  /*分配demo设备结构体变量*/
  demo_dev = kmalloc(sizeof(struct demo_android_dev), GFP_KERNEL);
  if(!demo_dev) {
    err = -ENOMEM;
    printk(KERN_ALERT"Failed to alloc demo_dev.\n");
    goto unregister;
  }        
  /*初始化设备*/
  err = __demo_setup_dev(demo_dev);
  if(err) {
    printk(KERN_ALERT"Failed to setup dev: %d.\n", err);
    goto cleanup;
  }        
  /*在/sys/class/目录下创建设备类别目录demo*/
  demo_class = class_create(THIS_MODULE, DEMO_DEVICE_CLASS_NAME);
  if(IS_ERR(demo_class)) {
    err = PTR_ERR(demo_class);
    printk(KERN_ALERT"Failed to create demo class.\n");
    goto destroy_cdev;
  }        
  /*在/dev/目录和/sys/class/demo目录下分别创建设备文件demo*/
  temp = device_create(demo_class, NULL, dev, "%s", DEMO_DEVICE_FILE_NAME);
  if(IS_ERR(temp)) {
    err = PTR_ERR(temp);
    printk(KERN_ALERT"Failed to create demo device.");
    goto destroy_class;
  }        
  /*在/sys/class/demo/demo目录下创建属性文件val*/
  err = device_create_file(temp, &dev_attr_val);
  if(err < 0) {
    printk(KERN_ALERT"Failed to create attribute val.");                
    goto destroy_device;
  }
  dev_set_drvdata(temp, demo_dev);        
  /*创建/proc/demo文件*/
  demo_create_proc();
  printk(KERN_ALERT"Succedded to initialize demo device.\n");
  return 0;
destroy_device:
  device_destroy(demo_class, dev);
destroy_class:
  class_destroy(demo_class);
destroy_cdev:
  cdev_del(&(demo_dev->dev));
cleanup:
  kfree(demo_dev);
unregister:
  unregister_chrdev_region(MKDEV(demo_major, demo_minor), 1);
fail:
  return err;
}
/*模块卸载方法*/
static void __exit demo_exit(void) {
  dev_t devno = MKDEV(demo_major, demo_minor);
  printk(KERN_ALERT"Destroy demo device.\n");        
  /*删除/proc/demo文件*/
  demo_remove_proc();        
  /*销毁设备类别和设备*/
  if(demo_class) {
    device_destroy(demo_class, MKDEV(demo_major, demo_minor));
    class_destroy(demo_class);
  }        
  /*删除字符设备和释放设备内存*/
  if(demo_dev) {
    cdev_del(&(demo_dev->dev));
    kfree(demo_dev);
  }        
  /*释放设备号*/
  unregister_chrdev_region(devno, 1);
}
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("First Android Driver");
module_init(demo_init);
module_exit(demo_exit);


这里面,在 2.6.37 之后的 Linux 内核中, init_mutex 已经被废除了, 新版本使用 sema_init 函数,init_MUTEX(&sem);修改为sema_init(&sem, 1);


6. 配置编译环境


我们的模块写好了,需要在内核编译的时候编译上我们的代码,需要在做一些配置:


在demo目录中新增Kconfig和Makefile两个文件,其中Kconfig是在编译前执行配置命令make menuconfig时用到的,而Makefile是执行编译命令make是用到的:


Kconfig文件的内容


config DEMO
       tristate "First Android Driver"
       default n
       help
       This is the first android driver.


tristate表示编译选项DEMO支持在编译内核时,demo模块支持以模块、内建和不编译三种编译方法,默认是不编译,因此,在编译内核前,我们还需要执行make menuconfig命令来配置编译选项,使得demo可以以模块或者内建的方法进行编译。


Makefile文件的内容


obj-$(CONFIG_DEMO) += demo.o


修改drivers/kconfig文件,在menu "Device Drivers"和endmenu之间添加一行(2.6.25旧版本还需要修改arch/arm/Kconfig,arch/arm/Kconfig中含有Drivers里Kconfig内容的一个复本,只对drivers/kconfig修改会导致无效):


source "drivers/demo/Kconfig"


在drivers/Makefile中增加一行:


obj-$(CONFIG_DEMO) += demo/


回到kernel目录执行make menuconfig会弹出U操作界面:


选择Device Drivers First Android Driver:


image.png


选择我们自己实现的驱动:


image.png


First Android Driver有两个选项可选:


  • M:作为module。.config中就会多一行CONFIG_DEMO = m,然后保存配置,执行make命令,就可以看到 CC [M] drivers/demo/demo.o 的log了,demo目录里生成了demo.o demo.ko的等文件
  • y:编进linux内核,.config中就会多一行CONFIG_DEMO = y


如果配置First Android Driver为M,drivers/Makefileobj-$(CONFIG_DEMO) += demo/就变成了obj-m +=demo/,在执行make命令时,便会进入demo目录里找makefile,MakeFile内容obj-$(CONFIG_DEMO) += demo.o 变成了obj-m +=demo.o,所以demo.c就被编译成模块了。


如果是模块的方式,我们将编译出的demo.ko文件通过adb push到设备SD卡,然后root方式进入adb,执行insmod demo.ko即可以调试的方式加载我们刚刚的驱动。


如果是内嵌内核的方式的话,这块板子的内核需要将make menuconfig保存下来的文件(如.config)拷贝到arch/arm64/configs/firefly_defconfig下再编译内核才能生效。


7. 验证


编译完成刷机后,可以在/dev//proc/sys/class下看到demo文件或目录了。通过cat demo查看虚拟寄存器值,通过echo num > demo`向寄存器写入值。


/sys/class/demo/下可以看到val文件,我们可以用类似的方式操作val。


8. 总结


本文介绍了Android9.0开发内核驱动的一般流程,以及开发一个4字节寄存器用到的linux函数,并且列出Linux内核4.4及之前版本接口上的一些区别。后续将完整代码放出,并录制一个完整的流程。

目录
相关文章
|
6月前
|
人工智能 搜索推荐 API
🚀 2小时极速开发!基于DeepSeek+智体OS的AI社交「头榜」震撼上线!
基于DeepSeek大模型与DTNS协议的革命性AI社交平台「头榜」震撼上线!仅需2小时极速开发,即可构建完整社交功能模块。平台具备智能社交网络、AI Agent生态、Prompt市场、AIGC创作等六大核心优势,支持低代码部署与个性化定制。开发者可快速接入DeepSeek API,体验去中心化架构与数据自主权。官网:[dtns.top](https://dtns.top),立即开启你的AI社交帝国!#AI社交 #DeepSeek #DTNS协议
223 4
|
9月前
|
存储 人工智能 JavaScript
Harmony OS开发-ArkTS三
本文介绍了ArkTS的基础语法,包括常量、命名规则、数组及其常用函数,以及函数的定义与使用,涵盖匿名函数和箭头函数的区别。通过具体示例,帮助读者快速掌握ArkTS编程技巧,踏上Harmony OS开发之旅。君志所向,一往无前!
531 1
Harmony OS开发-ArkTS三
|
4月前
|
监控 Linux 开发者
理解Linux操作系统内核中物理设备驱动(phy driver)的功能。
综合来看,物理设备驱动在Linux系统中的作用是至关重要的,它通过与硬件设备的紧密配合,为上层应用提供稳定可靠的通信基础设施。开发一款优秀的物理设备驱动需要开发者具备深厚的硬件知识、熟练的编程技能以及对Linux内核架构的深入理解,以确保驱动程序能在不同的硬件平台和网络条件下都能提供最优的性能。
270 0
|
安全 搜索推荐 Android开发
移动应用与系统:探索开发趋势与操作系统优化策略####
当今数字化时代,移动应用已成为日常生活不可或缺的一部分,而移动操作系统则是支撑这些应用运行的基石。本文旨在探讨当前移动应用开发的最新趋势,分析主流移动操作系统的特点及优化策略,为开发者提供有价值的参考。通过深入剖析技术创新、市场动态与用户需求变化,本文力求揭示移动应用与系统协同发展的内在逻辑,助力行业持续进步。 ####
245 9
|
7月前
|
网络协议 Linux 网络安全
Palo Alto PAN-OS 11.2.5 for KVM - ML 驱动的 NGFW
Palo Alto PAN-OS 11.2.5 for KVM - ML 驱动的 NGFW
298 10
Palo Alto PAN-OS 11.2.5 for KVM - ML 驱动的 NGFW
|
10月前
|
存储 人工智能 JavaScript
Harmony OS开发-ArkTS语言速成二
本文介绍了ArkTS基础语法,包括三种基本数据类型(string、number、boolean)和变量的使用。重点讲解了let、const和var的区别,涵盖作用域、变量提升、重新赋值及初始化等方面。期待与你共同进步!
510 47
Harmony OS开发-ArkTS语言速成二
|
9月前
|
存储 人工智能 编译器
【03】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-测试hello word效果-虚拟华为手机真机环境调试-为DevEco Studio编译器安装中文插件-测试写一个滑动块效果-介绍诸如ohos.ui等依赖库-全过程实战项目分享-从零开发到上线-优雅草卓伊凡
【03】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-测试hello word效果-虚拟华为手机真机环境调试-为DevEco Studio编译器安装中文插件-测试写一个滑动块效果-介绍诸如ohos.ui等依赖库-全过程实战项目分享-从零开发到上线-优雅草卓伊凡
557 10
【03】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-测试hello word效果-虚拟华为手机真机环境调试-为DevEco Studio编译器安装中文插件-测试写一个滑动块效果-介绍诸如ohos.ui等依赖库-全过程实战项目分享-从零开发到上线-优雅草卓伊凡
|
9月前
|
前端开发 JavaScript 开发工具
【04】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-正确安装鸿蒙SDK-结构目录介绍-路由介绍-帧动画(ohos.animator)书写介绍-能够正常使用依赖库等-ArkUI基础组件介绍-全过程实战项目分享-从零开发到上线-优雅草卓伊凡
【04】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-正确安装鸿蒙SDK-结构目录介绍-路由介绍-帧动画(ohos.animator)书写介绍-能够正常使用依赖库等-ArkUI基础组件介绍-全过程实战项目分享-从零开发到上线-优雅草卓伊凡
580 5
【04】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-正确安装鸿蒙SDK-结构目录介绍-路由介绍-帧动画(ohos.animator)书写介绍-能够正常使用依赖库等-ArkUI基础组件介绍-全过程实战项目分享-从零开发到上线-优雅草卓伊凡
|
9月前
|
安全 前端开发 开发工具
【01】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-项目开发实战-优雅草卓伊凡拟开发一个一站式家政服务平台-前期筹备-暂定取名斑马家政软件系统-本项目前端开源-服务端采用优雅草蜻蜓Z系统-搭配ruoyi框架admin后台-全过程实战项目分享-从零开发到上线
【01】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-项目开发实战-优雅草卓伊凡拟开发一个一站式家政服务平台-前期筹备-暂定取名斑马家政软件系统-本项目前端开源-服务端采用优雅草蜻蜓Z系统-搭配ruoyi框架admin后台-全过程实战项目分享-从零开发到上线
471 5
【01】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-项目开发实战-优雅草卓伊凡拟开发一个一站式家政服务平台-前期筹备-暂定取名斑马家政软件系统-本项目前端开源-服务端采用优雅草蜻蜓Z系统-搭配ruoyi框架admin后台-全过程实战项目分享-从零开发到上线
|
9月前
|
JavaScript 编译器 开发工具
【02】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-项目开发实战-准备工具安装-编译器DevEco Studio安装-arkts编程语言认识-编译器devco-鸿蒙SDK安装-模拟器环境调试-hyper虚拟化开启-全过程实战项目分享-从零开发到上线-优雅草卓伊凡
【02】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-项目开发实战-准备工具安装-编译器DevEco Studio安装-arkts编程语言认识-编译器devco-鸿蒙SDK安装-模拟器环境调试-hyper虚拟化开启-全过程实战项目分享-从零开发到上线-优雅草卓伊凡
495 2
【02】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-项目开发实战-准备工具安装-编译器DevEco Studio安装-arkts编程语言认识-编译器devco-鸿蒙SDK安装-模拟器环境调试-hyper虚拟化开启-全过程实战项目分享-从零开发到上线-优雅草卓伊凡

热门文章

最新文章

推荐镜像

更多
下一篇
oss云网关配置