【嵌入式Linux】嵌入式Linux驱动开发基础知识之驱动设计的思想:面向对象/分层/分离

2023-05-16

文章目录

  • 前言
  • 1、分离设计
    • 驱动程序分析---程序分层
      • 通用驱动程序---面向对象
      • 个性化驱动程序---分离
    • APP 程序分析

前言

韦东山嵌入式Linux驱动开发基础知识学习笔记
文章中大多内容来自韦东山老师的文档,还有部分个人根据自己需求补充的内容
视频教程地址:https://www.bilibili.com/video/BV14f4y1Q7ti

上一章在驱动中引入面向对象和分层的设计思想
在驱动里简单来说面向对象就是抽象一个结构体作为设备的类,然后将结构体成员作为对象
而分层设计思想简单来说就是将 内核相关的操作和硬件强相关操作分离 达到内核相关操作通用,硬件相关操作个性化目的
本节将介绍分离思想
【嵌入式Linux】嵌入式Linux驱动开发基础知识之LED驱动框架–面向对象、分层设计思想

1、分离设计

在这里插入图片描述

▲面向对象、分层、分离思想体现

将使能、操作GPIO的流程抽象成统一的接口,LED的init可以去调用这些接口,其中添加struct led_resource结构体,其中保存了LED需要的资源即GPIO的group & pin,实现初始化。一系列操作相当于封装了GPIO的寄存器相关操作供设备使用

驱动程序分析—程序分层

通用驱动程序—面向对象

led_opr.h:包含led设备结构体类

#ifndef _LED_OPR_H
#define _LED_OPR_H

struct led_operations {
	int (*init) (int which); /* 初始化LED, which-哪个LED */       
	int (*ctl) (int which, char status); /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
	int (*exit) (void);
};

struct led_operations *get_board_led_opr(void);

#endif

leddrv.c:给APP提供接口

#include <linux/module.h>

#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>

#include "led_opr.h"

#define LED_NUM 2

/* 1. 确定主设备号                                                                 */
static int major = 0;
static struct class *led_class;
struct led_operations *p_led_opr;


#define MIN(a, b) (a < b ? a : b)

/* 3. 实现对应的open/read/write等函数,填入file_operations结构体                   */
static ssize_t led_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

/* write(fd, &val, 1); */
static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
	int err;
	char status;
	struct inode *inode = file_inode(file);
	int minor = iminor(inode);
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = copy_from_user(&status, buf, 1);

	/* 根据次设备号和status控制LED */
	p_led_opr->ctl(minor, status);
	
	return 1;
}

static int led_drv_open (struct inode *node, struct file *file)
{
	int minor = iminor(node);
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	/* 根据次设备号初始化LED */
	p_led_opr->init(minor);
	
	return 0;
}

static int led_drv_close (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

/* 2. 定义自己的file_operations结构体                                              */
static struct file_operations led_drv = {
	.owner	 = THIS_MODULE,
	.open    = led_drv_open,
	.read    = led_drv_read,
	.write   = led_drv_write,
	.release = led_drv_close,
};

/* 4. 把file_operations结构体告诉内核:注册驱动程序                                */
/* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 */
static int __init led_init(void)
{
	int err;
	int i;
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	major = register_chrdev(0, "100ask_led", &led_drv);  /* /dev/led */


	led_class = class_create(THIS_MODULE, "100ask_led_class");
	err = PTR_ERR(led_class);
	if (IS_ERR(led_class)) {
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(major, "led");
		return -1;
	}

	for (i = 0; i < LED_NUM; i++)
		device_create(led_class, NULL, MKDEV(major, i), NULL, "100ask_led%d", i); /* /dev/100ask_led0,1,... */
	
	p_led_opr = get_board_led_opr();
	
	return 0;
}

/* 6. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数           */
static void __exit led_exit(void)
{
	int i;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

	p_led_opr->exit();

	for (i = 0; i < LED_NUM; i++)
		device_destroy(led_class, MKDEV(major, i)); /* /dev/100ask_led0,1,... */

	device_destroy(led_class, MKDEV(major, 0));
	class_destroy(led_class);
	unregister_chrdev(major, "100ask_led");
}


/* 7. 其他完善:提供设备信息,自动创建设备节点                                     */

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");

个性化驱动程序—分离

board_A_led:包含LED所需要的资源信息

#include "led_resource.h"

static struct led_resource led_resources = {
	.num = 2,
	/* PA10 green */
	.pin[0] = GROUP_PIN(0,10),
	/* PG8 yellow */
	.pin[1] = GROUP_PIN(6,8),
};

struct led_resource *get_led_resouce(void)
{
	return &led_resources;
}

chip_demo_gpio.c:包含GPIO相关操作、使能函数,LED设备对象从这里获得接口

#include <linux/module.h>

#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <asm/io.h>

#include "led_opr.h"
#include "led_resource.h"

/* registers */
// RCC_PLL4CR地址:0x50000000 + 0x894
static volatile unsigned int *RCC_PLL4CR;

// RCC_MP_AHB4ENSETR 地址:0x50000000 + 0xA28
static volatile unsigned int *RCC_MP_AHB4ENSETR;

// GPIOA_MODER 地址:0x50002000 + 0x00
static volatile unsigned int *GPIOA_MODER;

// GPIOA_BSRR 地址: 0x50002000 + 0x18
static volatile unsigned int *GPIOA_BSRR;

// GPIOG_MODER 地址:0x50008000 + 0x00
static volatile unsigned int *GPIOG_MODER;

// GPIOG_BSRR 地址: 0x50008000 + 0x18
static volatile unsigned int *GPIOG_BSRR;
/* registers end */

struct led_resource *led_rsc;

static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */	   
{
	if (!led_rsc)
	{
		led_rsc = get_led_resouce();
	}
	/* 寄存器映射到变量 */
	if (!RCC_PLL4CR)
	{
		// RCC_PLL4CR地址:0x50000000 + 0x894
		RCC_PLL4CR = ioremap(0x50000000 + 0x894, 4);
		
		// RCC_MP_AHB4ENSETR 地址:0x50000000 + 0xA28
		RCC_MP_AHB4ENSETR = ioremap(0x50000000 + 0xA28, 4);
		
		// GPIOA_MODER 地址:0x50002000 + 0x00
		GPIOA_MODER = ioremap(0x50002000 + 0x00, 4);
		
		// GPIOA_BSRR 地址: 0x50002000 + 0x18
		GPIOA_BSRR = ioremap(0x50002000 + 0x18, 4);

		// GPIOG_MODER 地址:0x50008000 + 0x00
		GPIOG_MODER = ioremap(0x50008000 + 0x00, 4);

		// GPIOG_BSRR 地址: 0x50008000 + 0x18
		GPIOG_BSRR = ioremap(0x50008000 + 0x18, 4);
	}

	switch(GROUP(led_rsc->pin[which]))
	{
		case 0:
		{
			printk("init pin of group A ...\n");
			/* enalbe PLL4, it is clock source for all gpio */
			*RCC_PLL4CR |= (1<<0);
			while ((*RCC_PLL4CR & (1<<1)) == 0);
			
			/* enable gpio */
			*RCC_MP_AHB4ENSETR |= (1<<GROUP(led_rsc->pin[which]));
			
			/*
			* configure gpio as output 
			*/
			*GPIOA_MODER &= ~(3 << (PIN(led_rsc->pin[which])*2) );
			*GPIOA_MODER |= (1<< (PIN(led_rsc->pin[which])*2) );
			/* ... */
			break;
		}
		case 6:
		{
			printk("init pin of group G ...\n");
			/* enalbe PLL4, it is clock source for all gpio */
			*RCC_PLL4CR |= (1<<0);
			while ((*RCC_PLL4CR & (1<<1)) == 0);
			
			/* enable gpio */
			*RCC_MP_AHB4ENSETR |= (1<<GROUP(led_rsc->pin[which]));

			/*
			* configure gpg8 as gpio
			* configure gpio as output 
			*/
			*GPIOG_MODER &= ~((PIN(led_rsc->pin[which])*2));
			*GPIOG_MODER |= ((PIN(led_rsc->pin[which])*2));
			break;
		}
		default:
			printk("not support %d\n", led_rsc->pin[which]);
	}
	
	return 0;
}

static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
{
	//printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
	printk("set led %s: group %d, pin %d\n", status ? "on" : "off", GROUP(led_rsc->pin[which]), PIN(led_rsc->pin[which]));

	switch(GROUP(led_rsc->pin[which]))
	{
		case 0:
		{
			printk("set pin of group A ...\n");
			*GPIOA_BSRR = ( 1<< (16*status + PIN(led_rsc->pin[which])));
			break;
		}
		case 6:
		{
			printk("set pin of group G ...\n");
			*GPIOG_BSRR = ( 1<< (16*status + PIN(led_rsc->pin[which])));
			break;
		}
		default:
		printk("not support %d\n", led_rsc->pin[which]);
	}

	return 0;
}

static int board_demo_led_exit (void)
{
	if(RCC_PLL4CR)
	{
		iounmap(RCC_PLL4CR);
		iounmap(RCC_MP_AHB4ENSETR);
		iounmap(GPIOA_MODER);
		iounmap(GPIOA_BSRR);
		iounmap(GPIOG_MODER);
		iounmap(GPIOG_BSRR);
	}

	return 0;
}

static struct led_operations board_demo_led_opr = {
	.init = board_demo_led_init,
	.ctl  = board_demo_led_ctl,
	.exit = board_demo_led_exit,
};

struct led_operations *get_board_led_opr(void)
{
	return &board_demo_led_opr;
}

APP 程序分析

ledtest.c


#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

/*
 * ./ledtest /dev/100ask_led0 on
 * ./ledtest /dev/100ask_led0 off
 */
int main(int argc, char **argv)
{
	int fd;
	char status;
	
	/* 1. 判断参数 */
	if (argc != 3) 
	{
		printf("Usage: %s <dev> <on | off>\n", argv[0]);
		return -1;
	}

	/* 2. 打开文件 */
	fd = open(argv[1], O_RDWR);
	if (fd == -1)
	{
		printf("can not open file %s\n", argv[1]);
		return -1;
	}

	/* 3. 写文件 */
	if (0 == strcmp(argv[2], "on"))
	{
		status = 1;
		write(fd, &status, 1);
	}
	else
	{
		status = 0;
		write(fd, &status, 1);
	}
	
	close(fd);
	
	return 0;
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【嵌入式Linux】嵌入式Linux驱动开发基础知识之驱动设计的思想:面向对象/分层/分离 的相关文章

  • 机器人地面站-[QGroundControl源码解析]-[9]-[Camera]

    目录 前言 一 QGCCameraManager 二 QGCCameraIO 三 QGCCameraControl 前言 本篇介绍Camera文件夹下的内容 xff0c 该文件夹下又三个类文件 xff0c 分别是QGCCameraManag
  • C++开发精髓 阅读笔记

    第三章 pstack的使用 将C 43 43 类对象实例指针作为线程函数的参数 bind函数 span class token keyword auto span newCallable span class token operator
  • js逆向工具-油猴Tampermonkey脚本hook案例

    目录 一 油猴下载与安装二 油猴脚本免费使用网站三 油猴脚本编写介绍1 添加新脚本2 油猴脚本注释内容解释3 编写油猴脚本的基本步骤4 油猴脚本调试测试 四 hook之js逆向案例1 hook之window属性案例2 hook之cookie
  • js逆向案例-rus4逻辑学习

    目录 一 RS4特点二 网站请求流程特点三 网站加载js的顺序四 正式逻辑的研究1 cookie如何定位 xff1f 2 扣函数缺啥补啥注意事项 xff1f 3 如何获取 96 ts动态变量 96 值 xff1f 4 如何定位 96 MmE
  • js逆向案例-初学signature

    一 反爬点 案例网址 xff0c 向下滚动加载页面时的请求参数反爬signature xff0c 明显的特征 xff1a acrawler js以及window jsvmprt 34 二 反爬分析 参考文章1 xff0c 参考文章2 xff
  • Android逆向基础入门

    目录 一 基础App数据抓取1 App常用抓包工具2 fiddler抓包工具3 mitmdump抓包工具4 charles抓包工具5 uiautomatorviewer appium自动化工具6 Airtest自动化工具 二 js逆向与ap
  • 自动化测试工具-Airtest

    目录 一 Airtest介绍与安装二 Airtest基于图像识别自动控制手机App流程三 Airtest基于Poco的UI组件自动化控制App流程四 Airtest实现手机群控操作 一 Airtest介绍与安装 主要介绍区别 xff0c 以
  • Docker桌面版安装与使用(windows)

    目录 一 Docker概念二 下载安装三 docker镜像安装与操作四 制作自己的python镜像容器五 目录挂载六 多容器通信七 Docker Compose管理多个容器运行八 发布和部署九 备份数据迁移 一 Docker概念 1 Doc
  • Gradio 机器学习和数据科学开源 Python 库

    Gradio是一个开源的 Python 库 xff0c MIT的开源项目 xff0c 用于构建机器学习和数据科学演示和 Web 应用 Gradio的定位类似于Streamlit xff0c 但是更轻量 xff0c 因为它推荐的应用场景都是对
  • k8s安装

    目录 一 K8s概念二 minikube安装三 部署应用到集群中四 Service五 k8s部署爬虫1 意义 一 K8s概念 参考文章 xff0c 参考视频它是一个为 容器化 应用提供集群部署和管理的开源工具 xff0c 由 Google
  • 编程工具-GPT来AI编程代码

    一 安装介绍 1 https www cursor so 下载安装 xff0c 重要的说三遍 xff08 目前免费 xff01 免费 xff01 免费 xff01 xff09 xff0c 支持多平台 Mac Windows Linux xf
  • 某wipo专利_六宫格/cookie/css

    这里写目录标题 一 案例分析二 六宫格验证码三 列表页搜索式302四 详情页css链接cookie刷新 一 案例分析 案例网址 xff1a 案例网址 反爬点 xff1a 六宫格验证码 cookie反爬 时间强制等待 session数据强绑定
  • Python之urlencode()使用

    urlencode 传入参数类型 xff1a 字典功能 xff1a 将存入的字典参数编码为URL查询字符串 xff0c 即转换成以key1 61 value1 amp key2 61 value2的形式导入 xff1a from urlli
  • Python之Md5使用等加密

    目录 一 Python之md5使用二 Python之sha1使用三 Python之base64使用四 Python之rsa使用五 Python之aes des 一 Python之md5使用 功能 xff1a MD5签名是一个哈希函数 xff
  • Python之quote()使用

    quote 传入参数类型 xff1a 字符串功能 xff1a 将单个字符串编码转化为 xx 的形式导入 xff1a from urllib parse import quotePs xff1a url多个字符串编码用urlenocde 函数
  • ( 数组和矩阵) 645. 错误的集合 ——【Leetcode每日一题】

    645 错误的集合 难度 xff1a 简单 集合 s 包含从 1 到 n 的整数 不幸的是 xff0c 因为数据错误 xff0c 导致集合里面某一个数字复制了成了集合里面的另外一个数字的值 xff0c 导致集合 丢失了一个数字 并且 有一个
  • Kalman滤波(Part-1:信号模型基础)

    Kalman Filters Dynamical Signal Models 一阶高斯 马尔可夫过程 first order Gauss Markov process 描述采样点之间 xff08 相邻 xff09 的相关性 xff1a s
  • 【STM32】关于keil5中下载按钮灰色及出现#error “Please select first the target STM32F4xx devic....错误的解决方法

    解决首次运行报错问题 近期在网上购买了一个WIFI模组 xff0c 例程是使用HAL库编写的 xff0c 首次编译的时候出现USER stm32f4xx h 193 error 35 error directive 34 Please se
  • 【STM32】F429单片机的时钟系统浅析

    先把429的时钟树附上 xff1a 乍一看是不是很懵逼 xff0c 我也很懵逼 一堆乱七八糟的玩意 xff0c ST公司是真的狗 本文是基于库函数SetSysClock 展开的 xff0c 配合该函数使用时钟树效果更佳O O 讲解之间说一个
  • x-easypdf 基于pdfbox/fop 封装的框架

    x easypdf 是一个基于 pdfbox fop 二次封装的框架 xff0c 目前拥有两大模块 xff1a pdfbox 模块 与 fop 模块 pdfbox 模块 主打 pdf 编辑功能 xff0c 以组件化的形式进行 pdf 的构建

随机推荐

  • 【STM32】利用定时器实现最基本的定时功能(HAL)

    定时器简介 STM32F429单片机有许多定时器Timer xff0c 主要分成三类 xff0c 基本定时器 xff1b 通用定时器和高级定时器 三种类型的定时器功能由少变多 xff0c 高级定时器包含了低级定时器的功能 基本定时器 xff
  • 【STM32】通俗易懂的讲解回调函数

    转载 xff1a https blog csdn net vincent040 article details 50832955 回调函数在程序开发中是一个非常重要的概念 xff0c 所谓的回调其实就是不同程序模块之间的接口和约定 xff0
  • 【树莓派】设置树莓派开机自动运行python脚本

    近期因科研需要 xff0c 需命令树莓派开机后无需进行任何操作自动运行一个python脚本 xff0c 经查阅部分资料后实现了该功能 xff0c 为方便以后查看特此记录一下 1 打开cmd xff0c 输入 sudo vim span cl
  • 部分机器人领域顶会顶刊官网及22年顶会召开时间

    顶刊 Science Robotics AAAS The International Journal of Robotics Research IJRR Journal of Forestry Research JFR IEEE Trans
  • 服务器非root下安装Python cyglfw3库

    服务器非root下安装Python cyglfw3库 在服务器中跑PVNet的代码时遇到的 xff0c 废了比较大的劲才解决 xff0c 特意记录一下解决过程 如果你有sudo权限直接使用sudo apt get install libgl
  • FFB6D搭建环境

    文章目录 FFB6D搭建环境搭建前的准备 根据需要选择性浏览 安装apex安装normal speed安装opencv3 安装pybind11安装normal speed 安装RandLA运行代码前的准备运行代码有疑问多多交流 xff0c
  • win32Day06:控件

    1 什么是控件 xff1f 控件是具备特殊功能的窗口 xff0c 并且有模板 控件的特性 xff1a 一定是子窗口 和创建主窗口一样 xff0c 都是使用CreateWindow函数来创建 xff08 控件这种 xff09 子窗口和主窗口的
  • SIFT3D点云关键点提取详细介绍

    1 引言 SIFT3D的理论基础完全是从图像特征SIFT2D中迁移类比过来的 xff0c 类似的还有Harris3D和Harris6D的理论也是来源于Harris2D的 xff0c 这些点云特征在PCL库中都有具体的实现 Harris3D和
  • 【视觉注意力机制集锦】引言

    视觉注意力机制集锦之引言 1 注意力机制 1 1 注意力机制简介 卷积神经网络具有很强的拟合数据的能力 xff0c 但是由于优化算法和计算能力的限制 xff0c 在实践中 xff0c 卷积网络很难达到通用近似的能力 特别是在处理规模较大的输
  • 期刊论文发表一定要有老师吗

    期刊论文发表一定要有老师吗 不一定 xff0c 具体要看论文的情况以及各方面的要求 xff0c 一般研究生和博士生论文很多都有老师 xff0c 本科论文很多是毕业生自己完成即可 xff0c 大多也是可以自己完成的 xff0c 如果要求有老师
  • QT菜单样式Ribbon Control for Qt, Office ribbon control

    基于Qt xff08 最低要求Qt5 xff0c 支持C 43 43 11的编译器 xff09 开发的一个轻量级的Ribbon控件 Office样式UI 使用Qt Creator直接打开SARibbon pro xff0c 并编译即可 xf
  • C/C++:基本语法看这一篇就够了

    前言 本文来自菜鸟教程的C语言教程和C 43 43 教程的学习笔记 xff0c 对其中的示例有所删减与变更 xff0c 建议以以下两个链接为准 虽说C 43 43 是C的扩展 xff0c 但貌似二者存在差集 xff0c 而本文只展示了兼容部
  • Qt之简易版网络调试助手

    简述 qt中为网络编程提供了一个QtNetwork类 xff0c 使用QtNetwork可以实现ftp文件传输 xff0c 基于tcp ip协议的客户端和 xff08 服务端 xff09 开发等等 xff0c 本文基于QtNetwork中的
  • ubuntu18.04安装docker以及过程中遇到的问题

    一 在ubuntu18 04上安装 1 更新索引包和安装相关依赖 xff1a sudo apt update sudo apt install apt span class token operator span transport spa
  • # 解决onos localhost 登陆onos后台时 需要输入密码问题

    登陆后台要求输入密码 xff0c 但尝试各种密码均无法登陆 一般从网上拷贝来的onos安装包 xff0c 使用时容易出现此类问题 xff1a 登录后台时会要求输入密码 xff0c 但一般各种密码都无效 问题解决 onos命名其实是一个she
  • visual c++与visual studio有什么区别

  • 【基于pyAudioKits的Python音频信号处理(八)】语音增强:谱减法、维纳滤波和卡尔曼滤波

    pyAudioKits是基于librosa和其他库的强大Python音频工作流支持 API速查手册 通过pip安装 xff1a pip install pyAudioKits 本项目的GitHub地址 xff0c 如果这个项目帮助到了你 x
  • 3.K8S网络之pod中容器到容器之间的通信

    文章目录 1 引入1 1 创建一个网络名称空间1 2 root网络名称空间1 3 docker网络名称空间 4 pod容器到容器之间的通信通信的原理 1 引入 在Linux中 xff0c 网络命名空间提供了一个逻辑的网络堆栈 xff0c 包
  • C++基础(八)--空指针

    在C和C 43 43 中 xff0c 用0和NULL都可以表示空指针 声明指针之后 xff0c 对指针赋值之前 xff0c 让它指空 xff0c 表示没有指向任何地址 使用空指针的后果 xff08 1 xff09 如果对空指针进行解引用 x
  • 【嵌入式Linux】嵌入式Linux驱动开发基础知识之驱动设计的思想:面向对象/分层/分离

    文章目录 前言1 分离设计驱动程序分析 程序分层通用驱动程序 面向对象个性化驱动程序 分离 APP 程序分析 前言 韦东山嵌入式Linux驱动开发基础知识学习笔记 文章中大多内容来自韦东山老师的文档 xff0c 还有部分个人根据自己需求补充