RT-Thread在STM32硬件I2C的踩坑记录

news2024/11/18 9:31:11

RT-Thread在STM32硬件I2C的踩坑记录

  • 0.前言
  • 一、软硬件I2C区别
  • 二、RT Thread中的I2C驱动
  • 三、尝试适配硬件I2C
  • 四、i2c-bit-ops操作函数替换
  • 五、Attention Please!
  • 六、总结


参考文章:
1.将硬件I2C巧妙地将“嫁接”到RTT原生的模拟I2C驱动框架
2.基于STM32F4平台的硬件I2C驱动实现笔记
3.《rt-thread驱动框架分析》- i2c驱动

0.前言

  最近打算用RT-Thread做一个小demo玩玩,其中需要用I2C通信驱动一个oled屏幕,但是找了一圈也没找到RTT中对硬件I2C的支持方式以及使用案例,好像大家都心照不宣的用这个好用又不好用的软件I2C。这里还是忍不住吐槽两句,连硬件SPI都已经支持了,甚至支持SPI DMA模式了,硬件I2C这么多年了也没适配。也希望有大佬能贡献一份力量,做出一份能让DIY玩家凑合用的第三方硬件I2C驱动也行。

一、软硬件I2C区别

  有关I2C通信协议的原理部分就不多介绍了,这个算是很常见的通信协议了,CSDN论坛一搜一大把,RT Thread文档中心也有较详细的介绍。
  软件I2C是使用GPIO的电平翻转模拟出I2C信号,它的好处是方便移植,下至51单片机,上至linux平台,只要有GPIO都能适用(当然linux下也不会有人用这个)。缺点则是速率很低,软件操作GPIO电平翻转不可避免的有时延以及毛刺,为了消除这种现象的影响,模拟的I2C信号之间就需要稍微大点的时间间隔。软件I2C的信号频率一般在30KHz ~ 50KHz,即便优化相当好的情况也差不多在这个量级。用来操作128x64的oled屏幕,帧率基本在2帧左右。
  硬件I2C则是通过操作芯片自带的寄存器进行I2C通信,缺点就是不同芯片间驱动不通用,优点则是速度更快,并且可以适配DMA模式,降低CPU负载。笔者使用的STM32RCT6,硬件I2C标准模式信号频率为100KHz,快速模式400KHz,一些性能较好的芯片还有1MHz的极速模式。400kHz情况下操作128x64的oled帧率在25帧左右,可以说是提升巨大了。

二、RT Thread中的I2C驱动

  关于RT Thread中的I2C驱动框架的实现方式,可以参考上述的第三篇参考文章,个人觉得写的很详细也好懂。RT Thread为类Linux的实时操作系统,所以I2C框架的实现方式和linux中的也比较相像:I2C驱动提供一些操作相关的ops函数,并注册到内核中,I2C设备则可以通过probe函数挂载到总线上,通过ops操作函数进行I2C通信。
在这里插入图片描述
并且在该篇文章中,该作者跳过原本的bit_ops,重新设计了一个硬件I2C的实现方式,将驱动直接挂载到内核core中,也实现了作为master设备的硬件I2C驱动。不过笔者认为这种方式对通用结构的兼容性不太好,所以又找了一些其他方式。
在这里插入图片描述

三、尝试适配硬件I2C

参考文章1和2中,通过修改I2C总线的实现函数,“嫁接”一个硬件的I2C驱动实现方式。这里就先放上代码,首先在原drv_soft_i2c.c和drv_soft_i2c.h的同级目录下,分别创建drv_hard_i2c.c和drv_hard_i2c.h:
drv_hard_i2c.h:

/*
 * Copyright (c) 2006-2018, RT-Thread Development Team
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Change Logs:
 * Date           Author       Notes
 * 2018-11-08     balanceTWK   first version
 */

#ifndef __DRV_I2C__
#define __DRV_I2C__

#include <rtthread.h>
#include <rthw.h>
#include <rtdevice.h>

#ifdef BSP_USING_HARD_I2C

/* stm32 config class */
typedef void (*pI2cConfig)(void);
struct stm32_hard_i2c_config
{
    rt_uint8_t scl;                     /* scl pin */
    rt_uint8_t sda;                     /* sda pin */
    const pI2cConfig pFunc;             /* i2c init function */
    const char* pName;                  /* i2c bus name */
    I2C_HandleTypeDef* pHi2c;           /* i2c handle */
    struct rt_i2c_bus_device i2c_bus;   /* i2c bus device */
};
/* stm32 i2c dirver class */
struct stm32_i2c
{
    struct rt_i2c_bit_ops ops;
    struct rt_i2c_bus_device i2c2_bus;
};

#define HARD_I2C_CONFIG(x)  \
{
    .scl        = BSP_I2C##x##_SCL_PIN,    \
    .sda        = BSP_I2C##x##_SDA_PIN,    \
    .pFunc      = MX_I2C##x##_Init,         \
    .pHi2c      = &hi2c##x,                 \
    .pName      = "i2c"#x,                  \
    .i2c_bus    = {
            .ops = &i2c_bus_ops,
    },
}
    
int rt_hw_i2c_init(void);

#endif

#endif /* RT_USING_I2C */

其中stm32_hard_i2c_config可以理解为i2c实例对象,属性包括scl和sda引脚、总线名称及初始化函数等。(注:在参考文章2中的总线速度、信号量及互斥锁则不需要,因为使用CubeMx生成的初始化函数中已有总线速度,HAL库中的I2C操作函数内部已有总线锁)
stm32_i2c则封装了设备操作函数及总线,用于与内核对接。
函数宏HARD_I2C_CONFIG(x)则用来后续创建I2C设备对象。

drv_hard_i2c.c:

/*
 * Copyright (c) 2006-2018, RT-Thread Development Team
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Change Logs:
 * Date           Author       Notes
 * 2018-11-08     balanceTWK   first version
 */

#include <board.h>
#include "drv_hard_i2c.h"
#include "drv_config.h"
#include<rtthread.h>
#include<rtdevice.h>

#ifdef BSP_USING_HARD_I2C

//#define DRV_DEBUG
#define LOG_TAG              "drv.i2c"
#include <drv_log.h>

static const struct stm32_hard_i2c_config hard_i2c_config[] =
{
#ifdef BSP_USING_HARD_I2C1
    HARD_I2C_CONFIG(1),
#endif
#ifdef BSP_USING_HARD_I2C2
    HARD_I2C_CONFIG(2),
#endif
#ifdef BSP_USING_HARD_I2C3
    HARD_I2C_CONFIG(3),
#endif
#ifdef BSP_USING_HARD_I2C4
    HARD_I2C_CONFIG(4),
#endif
};

static struct stm32_i2c i2c_obj[sizeof(hard_i2c_config) / sizeof(hard_i2c_config[0])];

/**
 * This function initializes the i2c pin.
 *
 * @param Stm32 i2c dirver class.
 */
static void stm32_i2c_gpio_init(struct stm32_i2c *i2c)
{
    struct stm32_soft_i2c_config* cfg = (struct stm32_soft_i2c_config*)i2c->ops.data;

    rt_pin_mode(cfg->scl, PIN_MODE_OUTPUT_OD);
    rt_pin_mode(cfg->sda, PIN_MODE_OUTPUT_OD);

    rt_pin_write(cfg->scl, PIN_HIGH);
    rt_pin_write(cfg->sda, PIN_HIGH);
}

/**
 * The time delay function.
 *
 * @param microseconds.
 */
static void stm32_udelay(rt_uint32_t us)
{
    rt_uint32_t ticks;
    rt_uint32_t told, tnow, tcnt = 0;
    rt_uint32_t reload = SysTick->LOAD;

    ticks = us * reload / (1000000 / RT_TICK_PER_SECOND);
    told = SysTick->VAL;
    while (1)
    {
        tnow = SysTick->VAL;
        if (tnow != told)
        {
            if (tnow < told)
            {
                tcnt += told - tnow;
            }
            else
            {
                tcnt += reload - tnow + told;
            }
            told = tnow;
            if (tcnt >= ticks)
            {
                break;
            }
        }
    }
}

/**
 * if i2c is locked, this function will unlock it
 *
 * @param stm32 config class
 *
 * @return RT_EOK indicates successful unlock.
 */
static rt_err_t stm32_i2c_bus_unlock(const struct stm32_soft_i2c_config *cfg)
{
    rt_int32_t i = 0;

    if (PIN_LOW == rt_pin_read(cfg->sda))
    {
        while (i++ < 9)
        {
            rt_pin_write(cfg->scl, PIN_HIGH);
            stm32_udelay(100);
            rt_pin_write(cfg->scl, PIN_LOW);
            stm32_udelay(100);
        }
    }
    if (PIN_LOW == rt_pin_read(cfg->sda))
    {
        return -RT_ERROR;
    }

    return RT_EOK;
}

/* I2C initialization function */
int rt_hw_i2c_init(void)
{
    rt_int8_t ret = RT_ERROR;
    rt_size_t obj_num = NR(hard_i2c_config);
    rt_err_t result;

    for (int i = 0; i < obj_num; i++)
    {
        //GPIO初始化
        stm32_i2c_gpio_init(&hard_i2c_config[i]);

        //检测SDA是否为低电平,低电平则通过管脚模拟9个CLK解锁
        stm32_i2c_bus_unlock(&hard_i2c_config[i]);

        //调用Hal库MX_I2Cx_Init(),配置硬件I2C
        hard_i2c_config[i].pFunc();

        //向内核注册I2C Bus设备
        if(rt_i2c_bus_device_register(&(hard_i2c_config[i].i2c_bus), hard_i2c_config[i].pName) != RT_EOK)
        {
            LOG_E("%s bus init failed!\r\n", hard_i2c_config[i].pName);
            ret |= RT_ERROR;
        }
        else
        {
            ret |= RT_EOK;
            LOG_I("%s bus init success!\r\n", hard_i2c_config[i].pName);
        }
    }
    return ret;
}
//INIT_BOARD_EXPORT(rt_hw_i2c_init);

#endif /* BSP_USING_HARD_I2C */

此文件中则主要根据宏定义开关创建I2C实例对象,并对其进行初始化。主要函数为rt_hw_i2c_init(),此函数中所需要的gpio init、delay函数等,则保留软件i2c中的初始化操作。

user_i2c.h:

/*
 * Copyright (c) 2006-2021, RT-Thread Development Team
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Change Logs:
 * Date           Author       Notes
 * 2023-08-27     14187       the first version
 */
#ifndef DRIVERS_HARD_I2C_H_
#define DRIVERS_HARD_I2C_H_

//硬件i2c宏开关
//#define     BSP_USING_HARD_I2C
#ifdef      BSP_USING_HARD_I2C
//      #define BSP_USING_HARD_I2C1
//      #define BSP_USING_HARD_I2C2
//      #define BSP_USING_HARD_I2C3
//      #define BSP_USING_HARD_I2C4

        #if defined(BSP_USING_HARD_I2C1 || BSP_USING_HARD_I2C2 || BSP_USING_HARD_I2C3 || BSP_USING_HARD_I2C4)
            //#define BSP_USING_DMA_I2C_TX
            //#define BSP_USING_DMA_I2C_RX
        #endif
#endif

#endif /* DRIVERS_HARD_I2C_H_ */

为了不在每次保存RT Thread Settings时,自己的配置被覆盖刷新,所以额外定义了一个头文件,用于保存自定义的I2C宏开关,这样每次刷新后只需要重新在board.h中包含此头文件即可。

至此自定义的硬件I2C宏开关及设备对象创建已完成,剩下的则只需要替换内核中的bit_ops操作函数即可。

四、i2c-bit-ops操作函数替换

在rt thread项目根目录下的 rt-thread/components/drivers/i2c/ 目录下,有一个i2c-bit-ops.c文件,其中则保存了i2c驱动框架中注册的ops操作函数:

static rt_size_t i2c_bit_xfer(struct rt_i2c_bus_device *bus,
                              struct rt_i2c_msg         msgs[],
                              rt_uint32_t               num)
{
    struct rt_i2c_msg *msg;
    struct rt_i2c_bit_ops *ops = (struct rt_i2c_bit_ops *)bus->priv;
    rt_int32_t i, ret;
    rt_uint16_t ignore_nack;

    if (num == 0) return 0;

    for (i = 0; i < num; i++)
    {
        msg = &msgs[i];
        ignore_nack = msg->flags & RT_I2C_IGNORE_NACK;
        if (!(msg->flags & RT_I2C_NO_START))
        {
            if (i)
            {
                i2c_restart(ops);
            }
            else
            {
                LOG_D("send start condition");
                i2c_start(ops);
            }
            ret = i2c_bit_send_address(bus, msg);
            if ((ret != RT_EOK) && !ignore_nack)
            {
                LOG_D("receive NACK from device addr 0x%02x msg %d",
                        msgs[i].addr, i);
                goto out;
            }
        }
        if (msg->flags & RT_I2C_RD)
        {
            ret = i2c_recv_bytes(bus, msg);
            if (ret >= 1)
            {
                LOG_D("read %d byte%s", ret, ret == 1 ? "" : "s");
            }
            if (ret < msg->len)
            {
                if (ret >= 0)
                    ret = -RT_EIO;
                goto out;
            }
        }
        else
        {
            ret = i2c_send_bytes(bus, msg);
            if (ret >= 1)
            {
                LOG_D("write %d byte%s", ret, ret == 1 ? "" : "s");
            }
            if (ret < msg->len)
            {
                if (ret >= 0)
                    ret = -RT_ERROR;
                goto out;
            }
        }
    }
    ret = i;

out:
    if (!(msg->flags & RT_I2C_NO_STOP))
    {
        LOG_D("send stop condition");
        i2c_stop(ops);
    }

    return ret;
}
...
static const struct rt_i2c_bus_device_ops i2c_bit_bus_ops =
{
    i2c_bit_xfer,
    RT_NULL,
    RT_NULL
};

这段代码中实现了对每个i2c设备发送对应的i2c msg流程,将其修改为硬件i2c的发送方式:

static rt_size_t i2c_xfer(struct rt_i2c_bus_device *bus,
                              struct rt_i2c_msg     msgs[],
                              rt_uint32_t           num)
{
    rt_uint32_t i;
    struct rt_i2c_msg *msg;
    struct stm32_hard_i2c_config *Pconfig = rt_container_of(bus, struct stm32_hard_i2c_config, i2c_bus);

    fot(i = 0;i < num;i++)
    {
        msg = &msgs[i];
        if(msg->flags & RT_I2C_RD)
        {
#if defined(BSP_USING_DMA_I2C_RX)
            HAL_I2C_Master_Receive_DMA(Pconfig->pHi2c, (msg->addr)<<1, msg->buf, msg->len);
            rt_hw_us_delay(100);
#else
            HAL_I2C_Master_Receive(Pconfig->pHi2c, (msg->addr)<<1, msg->buf, msg->len, 100);
#endif
        }
        else
        {
#if defined(BSP_USING_DMA_I2C_TX)
            HAL_I2C_Master_Transmit_DMA(Pconfig->pHi2c, (msg->addr)<<1, msg->buf, msg->len);
            rt_hw_us_delay(100);
#else
            HAL_I2C_Master_Transmit(Pconfig->pHi2c, (msg->addr)<<1, msg->buf, msg->len, 100);
#endif
        }
    }
    return i;
}

static const struct rt_i2c_bus_device_ops i2c_bit_bus_ops =
{
    i2c_xfer,
    RT_NULL,
    RT_NULL
};

参考软件i2c的发送方式,创建一个新的发送函数rt_size_t i2c_xfer(),并将rt_i2c_bus_device_ops 中对应的发送方式修改为此方式。

至此,硬件I2C的驱动则算是完成了一部分,可以通过与软件i2c一样的声明及挂载方式,将设备挂载到硬件I2C总线上。

五、Attention Please!

问题1:在上述的实现方式中,可以根据宏定义通过HAL_I2C_Master_Transmit()或HAL_I2C_Master_Transmit_DMA()方式发送I2C消息,但并未对是否发送成功做出判断。
问题2:ST官方的HAL库中,I2C发送消息共有三种方式,polling模式(轮询)、中断模式、DMA模式,HAL_I2C_Master_Transmit()则对应轮询模式,此模式相对于软件I2C虽然速率有所提升,但实际的提升效果其实不是特别大。而对于中断模式,则需要移植并实现对应的中断处理函数,可以按照参考文章2进行实现,不过笔者认为该篇需要注意的地方很多,比如在中断处理函数中释放信号量的操作,可能会造成一些隐患(可以直接去除信号量)。对于DMA模式,理论上也需要移植一些中断处理函数,但笔者目前没有用这种方式,所以也没有细究。所以理论上只能停留在polling模式。
问题3:在drv_hard_i2c.c中,INIT_BOARD_EXPORT(rt_hw_i2c_init);这个注册步骤,需要根据实际情况而定,如果想要使用DMA模式,则在此注册步骤之前,需要先注册MX_DMA_Init(),此函数为CubeMX生成,用来初始化DMA功能。中断模式同理。

六、总结

  目前看来,移植ST的硬件I2C驱动还是困难重重,所以笔者选则了更换平台(我逃避。。。)将oled的电路修改成了SPI模式,并更换了芯片平台,手头还有一个LPC54110和一个CH32的开发板,这两个板子的RTT BSP支持包好像有适配硬件I2C驱动,ST再见,希望下次回来有大佬适配了硬件I2C。

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

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

相关文章

flutter 上传图片并裁剪

1.首先在pubspec.yaml文件中新增依赖pub.dev image_picker: ^0.8.75 image_cropper: ^4.0.1 2.在Android的AndroidManifest.xml文件里面添加权限 <activityandroid:name"com.yalantis.ucrop.UCropActivity"android:screenOrientation"portrait"andro…

Golang网络编程

互联网协议介绍引入 1. 物理层&#xff08;Physical Layer&#xff09;&#xff1a; - 功能&#xff1a;物理层负责定义物理介质传输数据的方式和规范&#xff0c;它传输的是原始数据比特流。 - 协议&#xff1a;Ethernet、Wi-Fi、USB、光纤等。 - 例子&#xff1a;将…

[javaWeb]Socket网络编程

网络编程&#xff1a;写一个应用程序,让这个程序可以使用网络通信。这里就需要调用传输层提供的 api。 Socket套接字 传输层提供协议&#xff0c;主要是两个: UDP和TCP 提供了两套不同的 api&#xff0c;这api也叫做socket api。 UDP和 TCP 特点对比&#xff1a; UDP: 无连…

数据结构day05(单链表)

今日任务&#xff1a; 思维导图&#xff1a; 实现 代码&#xff1a;&#xff08;多文件&#xff09; head.h #ifndef __HEAD_H__ #define __HEAD_H__#include <stdio.h> #include <string.h> #include <stdlib.h> typedef int datatype;typedef struct Lin…

Nuxt3 与 Vue3 的 Server api 全栈开发之路

Nuxt Server Api 阅读时长&#xff1a;15分钟 本文内容&#xff1a; 国内关于Nuxt3的资料太少了&#xff0c;而Nuxt3又发布了没有多久&#xff0c;导致资料太少。本文浓缩讲解了&#xff0c;对于一个前端开发&#xff0c;上手使用 Nuxt3&#xff0c;并一个人承担前后端开发的所…

Hive3第六章:更换引擎

系列文章目录 Hive3第一章&#xff1a;环境安装 Hive3第二章&#xff1a;简单交互 Hive3第三章&#xff1a;DML数据操作 Hive3第三章&#xff1a;DML数据操作(二) Hive3第四章&#xff1a;分区表和分桶表 Hive3第五章&#xff1a;函数 Hive3第六章&#xff1a;更换引擎 文章目…

网络安全—0基础学习笔记(黑客)

一、前言 1.这是一条坚持的道路,三分钟的热情可以放弃往下看了. 2.多练多想,不要离开了教程什么都不会了.最好看完教程自己独立完成技术方面的开发. 3.有时多 google,baidu,我们往往都遇不到好心的大神,谁会无聊天天给你做解答. 4.遇到实在搞不懂的,可以先放放,以后再来解决. …

2023年智慧政务一网通办云平台顶层设计与建设方案PPT

导读&#xff1a;原文《2023年智慧政务一网通办云平台顶层设计与建设方案PPT》&#xff08;获取来源见文尾&#xff09;&#xff0c;本文精选其中精华及架构部分&#xff0c;逻辑清晰、内容完整&#xff0c;为快速形成售前方案提供参考。 部分内容&#xff1a; 喜欢文章&#…

Spring——RESTful Web服务

文章目录 RESTful Web 服务介绍内容概览下载 Lombok 优化代码利器RESTful Web 服务开发运行项目并测试效果 RESTful Web 服务介绍 本节我们将开发一个简单的 RESTful Web 服务。 RESTful Web 服务与传统的 MVC 开发一个关键区别是返回给客户端的内容的创建方式&#xff1a;传…

Go操作各大消息队列教程(RabbitMQ、Kafka)

Go操作各大消息队列教程 1 RabbitMQ 1.1 概念 ①基本名词 当前市面上mq的产品很多&#xff0c;比如RabbitMQ、Kafka、ActiveMQ、ZeroMQ和阿里巴巴捐献给Apache的RocketMQ。甚至连redis这种NoSQL都支持MQ的功能。 Broker&#xff1a;表示消息队列服务实体Virtual Host&#x…

感觉车载测试的这一波敏捷风快过去了

敏捷&#xff0c;算不得汽车行业的原生产物&#xff0c;几年前&#xff0c;耳边很少听到这个字眼&#xff0c;基本算是在近几年传统汽车行业开始衰落的大背景下&#xff0c;而后伴随着软件从互联网等行业传进来的。 这两年&#xff0c;大家开始把敏捷谈得风生水起&#xff0c;…

用idea查看sqlite数据库idea sqlite

1、安装Database Navigator插件 2、导入数据库并查看 3、删除数据库连接 在此做个笔记

VUE环境下 CSS3+JS 实现发牌 翻牌

创建牌容器&#xff08;关键点&#xff1a;overflow&#xff1a;hidden&#xff09;&#xff1a; <div class"popup-box"></div> .popup-box {position: absolute;width: 100vw;height: 100vh;top: 0px;left: 0;overflow: hidden; } 创建每一张牌《固…

python+TensorFlow实现人脸识别智能小程序的项目(包含TensorFlow版本与Pytorch版本)(一)

pythonTensorFlow实现人脸识别智能小程序的项目&#xff08;包含TensorFlow版本与Pytorch版本&#xff09;&#xff08;一&#xff09; 一&#xff1a;TensorFlow基础知识内容部分&#xff08;简明扼要&#xff0c;快速适应&#xff09;1、下载Cifar10数据集&#xff0c;并进行…

react17:生命周期函数

挂载时更新时 setState触发更新、父组件重新渲染时触发更新forceUpdate触发更新卸载时 react&#xff08;v17.0.2&#xff09;的生命周期图谱如下。 相较于16版本&#xff0c;17版本生命周期函数有如下变化&#xff1a; componentWillMount() componentWillUpdate() compone…

mac电脑屏幕录制Berrycast Mac屏幕录制软件

Berrycast是一款为Mac设计的优秀屏幕录制软件&#xff0c;它让屏幕录制变得简单而高效。以下是Berrycast的一些主要特点&#xff1a; 简单的用户界面&#xff1a;Berrycast拥有直观和简洁的用户界面&#xff0c;使得用户可以轻松上手。高质量的视频输出&#xff1a;Berrycast能…

电商系统架构设计系列(十):怎么能避免写出慢SQL?

上篇文章中&#xff0c;我给你留了一个思考题&#xff1a;怎么能避免写出慢SQL&#xff1f; 我们知道&#xff0c;一个慢 SQL 就可以直接让 MySQL 瘫痪。以我个人经验总结来看&#xff0c;一般情况下系统出问题&#xff0c;大多数都是因为SQL语句的问题。掌握和用好了SQL&…

软件面试题:文件上传下载测试点

目前关于云文档的业务还是挺多的&#xff0c;相信出去面试的同学&#xff0c;大多会遇到这道高频软件测试面试题&#xff1a;文件上传下载测试点。今天向大家分享下&#xff0c;希望对大家有所启发。 一、文件上传测试点 1、文件大小 一般情况下&#xff0c;系统会设定上传文…

基于OV2640/ OV5640 的图像采集显示系统

基于OV2640/ OV5640 的图像采集显示系统系列文章目录&#xff1a; &#xff08;1&#xff09;基于 OV5640 摄像头理论知识讲解-成像和采样原理 &#xff08;2&#xff09;基于 OV5640 摄像头理论知识讲解-数字接口和控制接口 &#xff08;3&#xff09;基于 OV5640 摄像头理论知…

Mac软件删除方法?如何删除不会有残留

Mac电脑如果有太多无用的应用程序&#xff0c;很有可能会拖垮Mac系统的运行速度。因此&#xff0c;卸载电脑中无用的软件是优化Mac系统运行速度的最佳方式之一。Mac卸载应用程序的方式是和Windows有很大的区别&#xff0c;特别对于Mac新用户来说&#xff0c;如何无残留的卸载删…