Android 相机启动流程笔记

news2024/11/17 7:40:25

90300989cb7a97628191282627f2690b.gif

和你一起终身学习,这里是程序员Android

经典好文推荐,通过阅读本文,您将收获以下知识点:

一、Camera 框架介绍:

Camera的框架分为Kernel部分和hal部分,其中kernel部分主要有两块:

  • image sensor driver,负责具体型号的sensorid检测,上电,以及在previewcapture初始化3A等等功能设定时的寄存器配置;

  • isp driver,通过DMAsensor数据流上传;

HAL层部分主要有三部分组成:

  • imageio,主要负责数据buffer上传的pipe

  • drv,包含imgsensorisphal层控制;

  • feature io,包含各种3A等性能配置;

这篇内容主要介绍开机过程中search sensor以及上电流程等内容。

二、Camera 启动流程

1、CameraService是在开机时启动的,启动后进行searchSensor的操作,会search系统有多少camera,开机时的search操作,只进行camera支持数量的遍历,以及sensor ID的读取操作,如下是hal部分的ASTAH绘制调用流程图,对应的接口的文件路径:

  • HalSensorList:
    vendor/mediatek/proprietary/hardware/mtkcam/drv/src/sensor/common/v1/HalSensorList.enumList.cpp
    vendor/mediatek/proprietary/hardware/mtkcam/drv/src/sensor/common/v1/HalSensorList.cpp

  • SeninfDrv:
    vendor/mediatek/proprietary/hardware/mtkcam/drv/src/sensor/mt6765/seninf_drv.cpp

  • SensorDrv:
    vendor/mediatek/proprietary/hardware/mtkcam/drv/src/sensor/common/v1/imgsensor_drv.cpp

08bb682884c86c6c3fb7725e88d01f78.png

(1) 这里先看enumerateSensor_Locked完成的工作,直接看代码:

MUINT HalSensorList::searchSensors()
{
    Mutex::Autolock _l(mEnumSensorMutex);
    MY_LOGD("searchSensors");
    return  enumerateSensor_Locked();
}

MUINT HalSensorList::enumerateSensor_Locked()
{
    SensorDrv *const pSensorDrv = SensorDrv::get();
    SeninfDrv *const pSeninfDrv = SeninfDrv::createInstance();
    //初始化seninf,配置ISP相关内容
    pSeninfDrv->init();

    //将所有的clk全部打开
    pSeninfDrv->setAllMclkOnOff(ISP_DRIVING_8MA, TRUE);

    pSensorDrv->init();
    for (MUINT i = IMGSENSOR_SENSOR_IDX_MIN_NUM; i <= max_index_of_camera; i++) {
        if((ret = pSensorDrv->searchSensor((IMGSENSOR_SENSOR_IDX)i)) == SENSOR_NO_ERROR){
            //query sensorinfo
           querySensorDrvInfo((IMGSENSOR_SENSOR_IDX)i);
           //fill in metadata
           buildSensorMetadata((IMGSENSOR_SENSOR_IDX)i);
           pSensorInfo = pSensorDrv->getSensorInfo((IMGSENSOR_SENSOR_IDX)i);
           addAndInitSensorEnumInfo_Locked(
                (IMGSENSOR_SENSOR_IDX)i,
                mapToSensorType(pSensorInfo->GetType()),
                pSensorInfo->getDrvMacroName());
        }
    }     
}

(2) 下面主要看下searchSensor的流程,这里有去获取sensorList的内容:

MINT32 ImgSensorDrv::searchSensor(IMGSENSOR_SENSOR_IDX sensorIdx)
{
    GetSensorInitFuncList(&pSensorInitFunc);

    featureControl(sensorIdx, SENSOR_FEATURE_SET_DRIVER, (MUINT8 *)&idx, &featureParaLen);

    NSFeature::SensorInfoBase* pSensorInfo = pSensorInitFunc[idx].pSensorInfo;
}

GetSensorInitFuncList是获取到配置的sensorList的内容,此sensorList需要与kernel层配置的一致,不一致的话在打开camera时会出现异常:
文件位置:
vendor/mediatek/proprietary/custom/mt6765/hal/imgsensor_src/sensorlist.cpp

MSDK_SENSOR_INIT_FUNCTION_STRUCT SensorList[] =
{
#if defined(IMX486_MIPI_RAW)
    RAW_INFO(IMX486_SENSOR_ID, SENSOR_DRVNAME_IMX486_MIPI_RAW, CAM_CALGetCalData),
#endif
//.....
}

UINT32 GetSensorInitFuncList(MSDK_SENSOR_INIT_FUNCTION_STRUCT **ppSensorList)
{
    if (NULL == ppSensorList) {
        ALOGE("ERROR: NULL pSensorList\n");
        return MHAL_UNKNOWN_ERROR;
    }
    *ppSensorList = &SensorList[0];
    return MHAL_NO_ERROR;
}

对应的MSDK_SENSOR_INIT_FUNCTION_STRUCT的结构体如下:

typedef struct
{
    MUINT32 sensorType;
    MUINT32 SensorId;
    MUINT8  drvname[32];
    NSFeature::SensorInfoBase* pSensorInfo;
    MUINT32 (*getCameraIndexMgr)(CAMERA_DATA_TYPE_ENUM CameraDataType, MVOID *pDataBuf, MUINT32 size);
    MUINT32 (*getCameraCalData)(UINT32* pGetSensorCalData);
} MSDK_SENSOR_INIT_FUNCTION_STRUCT, *PMSDK_SENSOR_INIT_FUNCTION_STRUCT;

(3) featureControlsetDriver流程:
文件位置:
vendor/mediatek/proprietary/hardware/mtkcam/drv/src/sensor/common/v1/imgsensor_drv.cpp

MINT32  ImgSensorDrv::featureControl(
    IMGSENSOR_SENSOR_IDX sensorIdx,
    ACDK_SENSOR_FEATURE_ENUM FeatureId,
    MUINT8 *pFeaturePara,
    MUINT32 *pFeatureParaLen
)
{
    //结构ACDK_SENSOR_FEATURECONTROL_STRUCT和kernel中一致
    featureCtrl.InvokeCamera = sensorIdx;
    featureCtrl.FeatureId = FeatureId;//SENSOR_FEATURE_SET_DRIVER
    featureCtrl.pFeaturePara = pFeaturePara;
    featureCtrl.pFeatureParaLen = pFeatureParaLen;

    if (ioctl(m_fdSensor, KDIMGSENSORIOC_X_FEATURECONCTROL , &featureCtrl) < 0) {
        LOG_ERR("[featureControl] Err-ctrlCode (%s)", strerror(errno));
        return -errno;
    }

    return SENSOR_NO_ERROR;
}

三、kernel 启动流程

先来看整体的框架图如下:

66ee089ccc96a81d935ae69e4cb1fe8b.png

1、set clock 设置时钟

static long imgsensor_ioctl(
    struct file *a_pstFile,
    unsigned int a_u4Command,
    unsigned long a_u4Param)
{
    case KDIMGSENSORIOC_X_SET_MCLK_PLL:
        i4RetValue = imgsensor_clk_set(
            &pgimgsensor->clk,
            (struct ACDK_SENSOR_MCLK_STRUCT *)pBuff);
        break;
        //......
}

int imgsensor_clk_set(
    struct IMGSENSOR_CLK *pclk, struct ACDK_SENSOR_MCLK_STRUCT *pmclk)
{
    if (pmclk->on) {
        clk_prepare_enable(pclk->imgsensor_ccf[mclk_index])
        ret = clk_set_parent(
            pclk->imgsensor_ccf[pmclk->TG],
            pclk->imgsensor_ccf[mclk_index]);
    } else {
        clk_disable_unprepare(pclk->imgsensor_ccf[mclk_index]);
    }
}

2、set driver

static long imgsensor_ioctl(
    struct file *a_pstFile,
    unsigned int a_u4Command,
    unsigned long a_u4Param) 
{
    case KDIMGSENSORIOC_X_FEATURECONCTROL:
        i4RetValue = adopt_CAMERA_HW_FeatureControl(pBuff);
        break;
        //......
}

static inline int adopt_CAMERA_HW_FeatureControl(void *pBuf)
{
    /* copy from user */
    switch (pFeatureCtrl->FeatureId) {
        case SENSOR_FEATURE_SET_DRIVER:
        {
            MINT32 drv_idx;

            psensor->inst.sensor_idx = pFeatureCtrl->InvokeCamera;
            drv_idx = imgsensor_set_driver(psensor);

            memcpy(pFeaturePara, &drv_idx, FeatureParaLen);
            break;
        }
    }
}

遍历CONFIG_CUSTOM_KERNEL_IMGSENSOR的内容,然后看sensorList是否对应,并获取对应的下标,调用imgsensor_check_is_alive进行上下电并读取ID 的操作:

struct IMGSENSOR_INIT_FUNC_LIST kdSensorList[MAX_NUM_OF_SUPPORT_SENSOR] = {
#if defined(XXXXXX_MIPI_RAW)
        {XXXXXX_SENSOR_ID,
        SENSOR_DRVNAME_XXXXXX_MIPI_RAW,
        XXXXXX_MIPI_RAW_SensorInit},
#endif
    //......
}

int imgsensor_set_driver(struct IMGSENSOR_SENSOR *psensor)
{
    struct IMGSENSOR_SENSOR_INST    *psensor_inst = &psensor->inst;
    struct IMGSENSOR_INIT_FUNC_LIST *pSensorList  = kdSensorList;
    //获取config的size
    char *sensor_configs = STRINGIZE(CONFIG_CUSTOM_KERNEL_IMGSENSOR);
    imgsensor_i2c_init(&psensor_inst->i2c_cfg,
    imgsensor_custom_config[psensor->inst.sensor_idx].i2c_dev);

    memcpy(psensor_list_config, sensor_configs+1, strlen(sensor_configs)-2);
    //对应config字符串进行按空格进行拆解
    driver_name = strsep(&psensor_list_config, " \0");

    while (driver_name != NULL) {
        for (j = 0; j < MAX_NUM_OF_SUPPORT_SENSOR; j++) {
            //判断对应的init函数是否存在
            if (pSensorList[j].init == NULL)
                break;
            else if (!strcmp(driver_name, pSensorList[j].name)) {
                //如果在config中和sensorlist中同时有定义进行赋值
                orderedSearchList[i++] = j;
                break;
            }
        }
        driver_name = strsep(&psensor_list_config, " \0");
    }

    for (i = 0; i < MAX_NUM_OF_SUPPORT_SENSOR; i++) {
        //上面获取到的sensorlist的下标
        drv_idx = orderedSearchList[i];
        if (pSensorList[drv_idx].init) {
            //调用对应驱动的init函数
            pSensorList[drv_idx].init(&psensor->pfunc);
            if (psensor->pfunc) {
                psensor_inst->psensor_name =
                    (char *)pSensorList[drv_idx].name;
                    //到这里是重点,进行上电读取ID的操作
                if (!imgsensor_check_is_alive(psensor)) {
                    ret = drv_idx;
                }
            }
        }
    }
}

下面看对应的上下电以及读取ID的操作:

static inline int imgsensor_check_is_alive(struct IMGSENSOR_SENSOR *psensor)
{
    struct IMGSENSOR_SENSOR_INST  *psensor_inst = &psensor->inst;
    //上电
    err = imgsensor_hw_power(&pgimgsensor->hw,
                psensor,
                psensor_inst->psensor_name,
                IMGSENSOR_HW_POWER_STATUS_ON);
    //读取ID
    imgsensor_sensor_feature_control(
            psensor,
            SENSOR_FEATURE_CHECK_SENSOR_ID,
            (MUINT8 *)&sensorID,
            &retLen);

    if (sensorID == 0 || sensorID == 0xFFFFFFFF) {
        pr_info("Fail to get sensor ID %x\n", sensorID);
        err = ERROR_SENSOR_CONNECT_FAIL;
    } else {
        pr_info(" Sensor found ID = 0x%x\n", sensorID);
        err = ERROR_NONE;
    }
    //下电
    imgsensor_hw_power(&pgimgsensor->hw,
        psensor,
        psensor_inst->psensor_name,
        IMGSENSOR_HW_POWER_STATUS_OFF);

    return err ? -EIO:err;
}

3、上电相关

上电时序配置:

struct IMGSENSOR_HW_POWER_INFO {
    enum IMGSENSOR_HW_PIN       pin;
    enum IMGSENSOR_HW_PIN_STATE pin_state_on;
    u32  pin_on_delay;
    enum IMGSENSOR_HW_PIN_STATE pin_state_off;
    u32  pin_off_delay;
};

struct IMGSENSOR_HW_POWER_SEQ sensor_power_sequence[] = {
    //……
    #if defined(XXXXXX_MIPI_RAW)
        {
            SENSOR_DRVNAME_XXXXXX_MIPI_RAW,
            {
                {RST, Vol_Low, 0},
                {DVDD, Vol_1100, 1},
                {AVDD, Vol_2800, 1},
                {DOVDD, Vol_1800, 1},
                {RST, Vol_High, 1},
                {SensorMCLK, Vol_High, 0},
            },
        },
    #endif
}

对应的控制的流程如下:

static enum IMGSENSOR_RETURN imgsensor_hw_power_sequence(
    struct IMGSENSOR_HW             *phw,
    enum   IMGSENSOR_SENSOR_IDX      sensor_idx,
    enum   IMGSENSOR_HW_POWER_STATUS pwr_status,
    struct IMGSENSOR_HW_POWER_SEQ   *ppower_sequence,
    char *pcurr_idx)
{
    ppwr_info = ppwr_seq->pwr_info;
    // 上电
    while (ppwr_info->pin != IMGSENSOR_HW_PIN_NONE &&
        ppwr_info < ppwr_seq->pwr_info + IMGSENSOR_HW_POWER_INFO_MAX) {

        if (pwr_status == IMGSENSOR_HW_POWER_STATUS_ON &&
           ppwr_info->pin != IMGSENSOR_HW_PIN_UNDEF) {
            pdev = phw->pdev[psensor_pwr->id[ppwr_info->pin]];
            if (pdev->set != NULL)
                //调用GPIO或者regulator的set 电压操作,这里的pdev在imgsensor_probe中已经设置
                pdev->set( 
                    pdev->pinstance,
                    sensor_idx,
                    ppwr_info->pin,
                    ppwr_info->pin_state_on);

            mdelay(ppwr_info->pin_on_delay);
        }
        // 从上到下依次上电
        ppwr_info++;
        pin_cnt++;
    }

    // 下电操作
    if (pwr_status == IMGSENSOR_HW_POWER_STATUS_OFF) {
        while (pin_cnt) {
            //从下到上依次下电
            ppwr_info--;
            pin_cnt--;

            if (ppwr_info->pin != IMGSENSOR_HW_PIN_UNDEF) {
                pdev =
                    phw->pdev[psensor_pwr->id[ppwr_info->pin]];
                mdelay(ppwr_info->pin_on_delay);

                if (pdev->set != NULL)
                    pdev->set(
                        pdev->pinstance,
                        sensor_idx,
                        ppwr_info->pin,
                        ppwr_info->pin_state_off);
            }
        }
    }

    /* wait for power stable */
    if (pwr_status == IMGSENSOR_HW_POWER_STATUS_ON)
        mdelay(5);
    return IMGSENSOR_RETURN_SUCCESS;
}

四、总结

通过上面的代码流程,可以知道上开机时,camera模块先会将所有的MCLK打开,然后对依次对对应的sensor进行上电,读取ID(判断I2C是否正常通讯)。这部分调试过程中遇到的问题总结如下:

1、ID读取不到,I2C不通

  • 检查上电时序,3项电压(AVDD/DVDD/IOVDD)是否正确;

  • I2C地址及通道设置是否正确;

  • 检查cfg_setting_imgsensor.cppMCLKHW链接配置是否正确;

2、Camera 启动时间过长

  • 检查Sensor上电时序要求的延时,是否有偏长的情况;

  • 去掉多余的I2C地址,因为大部分驱动会多添加一些地址;

  • OTP的加载调整到每次开机时第一次打开加载,之后不加载;

  • sensorInit如果时间过长,可以调节I2C speed(400->1000)

3、preview 阶段耗时

  • 检查streamOn/Off的耗时;

  • preview_init是否有较长时间的耗时

  • 以及延时操作使用mdelay代替msleep

  • pre_delay_frame/cap_delay_frame丢帧操作是否合适;

4、低电流、功耗相关问题

  • 检查电压是否都有下电成功,防止漏电;

  • 对于共pinsensor,在操作时是否有做好workaround

  • I2C寄存器单个读写,调整为连续读写的方式也有一定优化;

  • sensorPIN是否有被其他模块占用,异常操作的行为;

友情推荐:

Android 开发干货集锦

至此,本篇已结束。转载网络的文章,小编觉得很优秀,欢迎点击阅读原文,支持原创作者,如有侵权,恳请联系小编删除,欢迎您的建议与指正。同时期待您的关注,感谢您的阅读,谢谢!

03b38985121e177bac9804493fb63001.jpeg

点个在看,方便您使用时快速查找!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1467349.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

并发锁机制之深入理解synchronized

并发锁机制之深入理解synchronized Synchronized基础知识 i的JVM字节码指令 getstatic i // 获取静态变量i的值 iconst_1 // 将int常量1压入操作数栈 iadd // 自增 i–的JVM字节码指令 getstatic i // 获取静态变量i的值 iconst_1 // 将int常量1压入操作数栈 isub // 自减…

性能优化——canvas 加载海量图

背景 公司的在线设计稿平台的画板列表页开发时由于数据量不足&#xff0c;未能测出关于画板列表页性能问题&#xff0c;在经过用户一段时间的使用后出现了关于初始化卡顿、缩放卡顿等问题&#xff0c;画板列表页采用了vue-konva 原因 关于画板列表为何卡顿有如下几点原因 1、…

2月24日(周六)比赛前瞻:曼联 VS 富勒姆、拜仁 VS 莱比锡

大家好&#xff0c;博主将持续更新胜负14场前瞻&#xff0c;此处每日赛事间歇更新&#xff0c;胃信号每日更新。 精选赛事&#xff1a;曼联 VS 富勒姆 曼联近期状态显著提升&#xff0c;上一轮联赛客场2-1战胜卢顿&#xff0c;连续7场正赛取得6胜1平的成绩&#xff0c;保持不败…

6.网络游戏逆向分析与漏洞攻防-游戏网络架构逆向分析-通过逆向分析确定游戏明文发送数据过程

内容参考于&#xff1a;易道云信息技术研究院VIP课 上一个内容&#xff1a;测试需求与需求拆解 在开始之前要了解一个小知识&#xff0c;在逆向开始之前要很清楚知道要找的东西是什么&#xff0c;大概长什么样子&#xff0c;只有这样才能看到它第一眼发现它&#xff0c;现在我…

Qt+VTK鼠标拾取点生成拉伸闭合三维体

程序示例精选 QtVTK鼠标拾取点生成拉伸闭合三维体 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对《QtVTK鼠标拾取点生成拉伸闭合三维体》编写代码&#xff0c;代码整洁&#xff0c;规则&…

ES6内置对象 - Map

Map&#xff08;Map对象保存键值对&#xff0c;键值均不限制类型&#xff09; 特点&#xff1a; 有序&#xff08;Set集合是无序的&#xff09;&#xff1b;键值对&#xff08;键可以是任意类型&#xff09;&#xff1b;键名不能重复&#xff08;如果重复&#xff0c;则覆盖&…

从ViT到MAE,transformer架构改造Autoencoder

Vision Transformer (ViT) 论文出处[2010.11929] An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale (arxiv.org) 传统的卷积神经网络&#xff08;CNN&#xff09;在图像分类、目标检测等任务上表现出色&#xff0c;但其局限性也逐渐显露&#xf…

【大数据】Flink 内存管理(二):JobManager 内存分配(含实际计算案例)

Flink 内存管理&#xff08;二&#xff09;&#xff1a;JobManager 内存分配 1.分配 Total Process Size2.分配 Total Flink Size3.单独分配 Heap Size4.分配 Total Process Size 和 Heap Size5.分配 Total Flink Size 和 Heap Size JobManager 是 Flink 集群的控制元素。它由三…

virtualenv env_name 使用 virtualenv 创建 python 虚拟环境

为什么要用这个 win7 32 环境下 pycharm 只能用低版本的&#xff0c;比如 2016,2018 此时pycharm 图形界面创建的 虚拟环境版本很低&#xff0c;有些包不兼容&#xff0c;因此用 virtualenv 模块&#xff0c;可以创建 20 版本以上的虚拟环境 virtualenv env_name官方文档 http…

全面解析企业财务报表系列之四:财务报表的真实性和可靠性

全面解析企业财务报表系列之四&#xff1a;财务报表的真实性和可靠性 一、什么是会计方法二、选择会计方法三、会计方法的重要性四、会计报表常用的造假手段五、财务报表经常被遗漏的重要事件六、财务报告造假的资信敏感性七、财务报告审计的重要性八、审计报告 一、什么是会计…

nginx重新编译添加模块或去除不需要的模块

在使用nginx中&#xff0c;我们可能需要对已经安装的nginx进行添加或者删除模块 1、先查看nginx安装了哪一些模块 nginx -V2、来到nginx源码目录&#xff0c;根据如下规则&#xff0c;自行根据需求更改命令 如果要去掉nginx自带的模块&#xff0c;就是用–without做为前缀进…

Stable Diffusion 绘画入门教程(webui)-ControlNet(IP2P)

上篇文章介绍了深度Depth&#xff0c;这篇文章介绍下IP2P&#xff08;InstructP2P&#xff09;, 通俗理解就是图生图&#xff0c;给原有图加一些效果,比如下图&#xff0c;左边为原图&#xff0c;右边为增加了效果的图&#xff1a; 文章目录 一、选大模型二、写提示词三、基础参…

Siamfc论文中文翻译(详细!)

Fully-Convolutional Siamese Networks for Object Tracking 用于对象跟踪的Siamese网络 说明 建议对照siamfc&#xff08;2021版&#xff09;原文阅读&#xff0c;翻译软件翻译出来的效果不好&#xff0c;整体阅读体验不佳&#xff0c;所以我对译文重新进行了整理&#xff0…

5分钟JavaScript快速入门

目录 一.JavaScript基础语法 二.JavaScript的引入方式 三.JavaScript中的数组 四.BOM对象集合 五.DOM对象集合 六.事件监听 使用addEventListener()方法添加事件监听器 使用onX属性直接指定事件处理函数 使用removeEventListener()方法移除事件监听器 一.JavaScript基础…

悄悄话花费的时间(C语言)【二叉树各结点统计求和】

题目描述 给定一个二叉树&#xff0c;每个节点上站着一个人&#xff0c;节点数字表示父节点到该节点传递悄悄话需要花费的时间。 初始时&#xff0c;根节点所在位置的人有一个悄悄话想要传递给其他人&#xff0c;求二叉树所有节点上的人都接收到悄悄话花费的时间。 输入描述 …

element导航菜单el-menu添加搜索功能

element导航菜单-侧栏&#xff0c;自带的功能没有搜索或者模糊查询。 找了找资料 找到一个比较可行的&#xff0c;记录一下&#xff1a; //index.vue的代码 <div style"overflow:auto"><el-menu :default-active"$route.path":default-openeds&…

如何在 Tomcat 中为 Web 应用程序启用和配置缓存?

在Tomcat中为Web应用程序启用和配置缓存通常涉及到对Tomcat的连接器&#xff08;Connector&#xff09;进行配置&#xff0c;以及可能的话&#xff0c;配置Web应用程序本身以支持缓存。 1. 配置Tomcat连接器以启用缓存 Tomcat的连接器可以通过其配置来启用各种…

开源免费的NTFS for mac工具mounty

开源免费的NTFS for mac工具mounty 安装依赖 brew install gromgit/fuse/ntfs-3g-macbrew install --cask macfuse安装mounty 如果已经安装macFUSE和ntfs-3g-mac&#xff0c;可以直接点击下载的dmg安装包&#xff0c;安装升级。第一次启动mounty&#xff0c;你需要接受一系列…

Oracle迁移到mysql-导出mysql所有索引和主键

导出建库表索引等&#xff1a; [rootlnpg ~]# mysqldump -ugistar -pxxx -h192.168.207.143 --no-data -d lndb > lndb20230223-1.sql 只导出索引&#xff1a;参考&#xff1a;MYSQL导出现有库中的索引脚本_mysql 导出数据库所有表的主键和索引-CSDN博客 -- MYSQL导出现有…

国内排名比较好的ai软件有哪些?极力推荐这几款

随着人工智能技术的不断演进&#xff0c;越来越多的写作者开始借助AI写作软件来提升写作效率。在国内&#xff0c;有许多实用且易用的AI写作工具&#xff0c;让写作变得更加便捷和高效。以下是6款国内优秀的AI写作软件&#xff0c;让您的写作过程更加顺畅。 第一款&#xff1a;…