STM32F1之OV7725摄像头·SCCB总线代码编写附带源码详解

STM32F1之OV7725摄像头-CSDN博客

STM32F1之OV7725摄像头·像素数据输出时序、FIFO 读写时序以及摄像头的驱动原理详解-CSDN博客

目录

1.  硬件设计

1.1  SCCB 控制相关

1.2  VGA 时序相关

1.3  FIFO 相关

1.4  XCLK 信号

2.  代码设计

2.1  SCCB总线软件实现

2.1.1  宏定义

2.1.2  SCCB管脚配置

2.1.3  延时函数

2.1.4  SCCB起始信号

2.1.5  SCCB终止信号

2.1.6  SCCB应答信号

2.1.7  SCCB非应答信号

2.1.8  SCCB等待应答信号

2.1.9  发送数据

2.1.10  SCCB总线返回的数据

2.1.11  SCCB写一个字节数据

2.1.12  SCCB读一串数据


1.  硬件设计

        摄像头与 STM32 连接关系中主要分为 SCCB 控制、VGA 时序控制、FIFO 数据读取部分,介绍如下:

1.1  SCCB 控制相关

        摄像头中的 SIO_C 和 SIO_D 引脚直接连接到 STM32 普通的 GPIO,它们不具有硬件I2C 的功能,所以在后面的代码中采用模拟 I2C 时序,实际上直接使用硬件 I2C 是完全可以实现 SCCB 协议的,本设计采用模拟 I2C 是芯片资源分配妥协的结果。

1.2  VGA 时序相关

        检测 VGA 时序的 HREF、VSYNC 引脚,它们与 STM32 连接的 GPIO 均设置为输入模式,其中 HREF 在本实验中并没有使用,它已经通过摄像头内部的与非门控制了 FIFO 的写使能;VSYNC 与 STM32 连接的 GPIO 引脚会在程序中配置成中断模式,STM32 利用该中断信号获知新的图像是否采集完成,从而控制 FIFO 是否写使能。

1.3  FIFO 相关

        与 FIFO 控制相关的 RCLK、RRST、WRST、WEN 及 OE 与 STM32 连接的引脚均直接配置成推挽输出,STM32 根据图像的采集情况利用这些引脚控制 FIFO;读取 FIFO 数据内容使用的数据引脚 DO[0:7]均连接到 STM32 同一个 GPIO 端口连续的高 8 位引脚 PB[8:15],这些引脚使用时均配置成输入,程序设计中直接读取 GPIO 端口的高 8 位状态直接获取一个字节的 FIFO 内容,建议在连接这部分数据信号时,参考本设计采用同一个 GPIO 端口连续的 8 位(高 8 位或低 8 位均可),否则会导致读取数据的程序非常复杂。

1.4  XCLK 信号

        本设计中 STM32 的摄像头接口还预留了 PA8 引脚用于与摄像头的 XCLK 连接,STM32的 PA8 可以对外输出时钟信号,所以在使用不带晶振的摄像头时,可以通过该引脚给摄像头提供时钟,秉火摄像头内部已自带晶振,在程序中没有使用 PA8 引脚。

2.  代码设计

2.1  SCCB总线软件实现

2.1.1  宏定义

#ifndef __SCCB_H
#define __SCCB_H#include "stm32f10x.h"/************************** OV7725 连接引脚定义********************************/
#define      OV7725_SIO_C_SCK_APBxClock_FUN              RCC_APB2PeriphClockCmd
#define      OV7725_SIO_C_GPIO_CLK                       RCC_APB2Periph_GPIOC
#define      OV7725_SIO_C_GPIO_PORT                      GPIOC
#define      OV7725_SIO_C_GPIO_PIN                       GPIO_Pin_6#define      OV7725_SIO_D_SCK_APBxClock_FUN              RCC_APB2PeriphClockCmd
#define      OV7725_SIO_D_GPIO_CLK                       RCC_APB2Periph_GPIOC
#define      OV7725_SIO_D_GPIO_PORT                      GPIOC
#define      OV7725_SIO_D_GPIO_PIN                       GPIO_Pin_7#define SCL_H         GPIO_SetBits(OV7725_SIO_C_GPIO_PORT , OV7725_SIO_C_GPIO_PIN) 
#define SCL_L         GPIO_ResetBits(OV7725_SIO_C_GPIO_PORT , OV7725_SIO_C_GPIO_PIN) #define SDA_H         GPIO_SetBits(OV7725_SIO_D_GPIO_PORT , OV7725_SIO_D_GPIO_PIN) 
#define SDA_L         GPIO_ResetBits(OV7725_SIO_D_GPIO_PORT , OV7725_SIO_D_GPIO_PIN) #define SCL_read      GPIO_ReadInputDataBit(OV7725_SIO_C_GPIO_PORT , OV7725_SIO_C_GPIO_PIN) 
#define SDA_read      GPIO_ReadInputDataBit(OV7725_SIO_D_GPIO_PORT , OV7725_SIO_D_GPIO_PIN) #define ADDR_OV7725   0x42void SCCB_GPIO_Config(void);
int SCCB_WriteByte( u16 WriteAddress , u8 SendByte);
int SCCB_ReadByte(u8* pBuffer,   u16 length,   u8 ReadAddress);#endif 

2.1.2  SCCB管脚配置

        前面我们说过,SCCB的引脚配置类似于IIC的引脚配置,这里我们对SCCB的引脚进行初始化,使用的类似软件IIC的协议,可以不用规定必须是IIC的引脚,进行配置为开漏输出模式:

void SCCB_GPIO_Config(void)
{GPIO_InitTypeDef  GPIO_InitStructure; 	/* SCL(PC6)、SDA(PC7)管脚配置 */RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE );GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;  GPIO_Init(GPIOC, &GPIO_InitStructure);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE );GPIO_InitStructure.GPIO_Pin =GPIO_Pin_7;GPIO_Init(GPIOC, &GPIO_InitStructure);	
}

        按照宏定义进行转换:

void SCCB_GPIO_Config(void)
{GPIO_InitTypeDef  GPIO_InitStructure; /* SCL(PC6)、SDA(PC7)管脚配置 */OV7725_SIO_C_SCK_APBxClock_FUN ( OV7725_SIO_C_GPIO_CLK, ENABLE );GPIO_InitStructure.GPIO_Pin =  OV7725_SIO_C_GPIO_PIN ;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;  GPIO_Init(OV7725_SIO_C_GPIO_PORT, &GPIO_InitStructure);OV7725_SIO_D_SCK_APBxClock_FUN ( OV7725_SIO_D_GPIO_CLK, ENABLE );GPIO_InitStructure.GPIO_Pin =  OV7725_SIO_D_GPIO_PIN ;GPIO_Init(OV7725_SIO_D_GPIO_PORT, &GPIO_InitStructure);}

2.1.3  延时函数

        一个简单的循环等于0时跳出循环,起到延时的作用:

static void SCCB_delay(void)
{	uint16_t i = 400; while(i) { i--; } 
}

2.1.4  SCCB起始信号

        类比IIC协议,起始条件下,SCL高电平期间,SDA从高电平切换到低电平。

static int SCCB_Start(void)
{GPIO_SetBits(GPIOC, GPIO_Pin_7);SCCB_delay();GPIO_SetBits(GPIOC, GPIO_Pin_6);SCCB_delay();GPIO_ResetBits(GPIOC, GPIO_Pin_7);SCCB_delay();GPIO_ResetBits(GPIOC, GPIO_Pin_6);SCCB_delay();}

       为了提高程序的健壮性,可以加入:GPIO_ReadInputDataBit 函数,其作用是读取指定GPIO端口的指定引脚的输入状态,并返回该引脚的输入值(0 或 1) ,进行检测SDA线是否忙碌是否正常。

static int SCCB_Start(void)
{GPIO_SetBits(GPIOC, GPIO_Pin_7);GPIO_SetBits(GPIOC, GPIO_Pin_6);SCCB_delay();if(!GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_7))return DISABLE;	/* SDA线为低电平则总线忙,退出 */GPIO_ResetBits(GPIOC, GPIO_Pin_7);SCCB_delay();if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_7)) return DISABLE;	/* SDA线为高电平则总线出错,退出 */GPIO_ResetBits(GPIOC, GPIO_Pin_7);SCCB_delay();return ENABLE;
}

        按照宏定义进行转换:

static int SCCB_Start(void)
{SDA_H;SCL_H;SCCB_delay();if(!SDA_read)return DISABLE;	/* SDA线为低电平则总线忙,退出 */SDA_L;SCCB_delay();if(SDA_read) return DISABLE;	/* SDA线为高电平则总线出错,退出 */SDA_L;SCCB_delay();return ENABLE;
}

2.1.5  SCCB终止信号

        终止条件下,SCL高电平期间,SDA从低电平切换到高电平。

static void SCCB_Stop(void)
{GPIO_ResetBits(GPIOC, GPIO_Pin_6);SCCB_delay();GPIO_ResetBits(GPIOC, GPIO_Pin_7);SCCB_delay();GPIO_SetBits(GPIOC, GPIO_Pin_6);SCCB_delay();GPIO_SetBits(GPIOC, GPIO_Pin_7);SCCB_delay();
}

        按照宏定义进行转换:

static void SCCB_Stop(void)
{SCL_L;SCCB_delay();SDA_L;SCCB_delay();SCL_H;SCCB_delay();SDA_H;SCCB_delay();
}

2.1.6  SCCB应答信号

        与 I2C 时序类似,在 SCCB 时序也使用自由位(Don’t care bit )和非应答(NA)信号来保证正常通讯。自由位和非应答信号位于 SCCB 每个传输阶段中的第九位。

        在写数据的第一个传输阶段中,第 9 位为自由位,在一般的正常通讯中,第 9 位时,主机的 SDA 线输出高电平,而从机把 SDA 线拉低作为响应,只是传输的内容分别为目的寄存器地址和要写入的数据。

        应答信号的发送是 SCCB 协议中的一个重要步骤,用于确认数据传输的成功。

static void SCCB_Ack(void)
{GPIO_ResetBits(GPIOC, GPIO_Pin_6);SCCB_delay();GPIO_ResetBits(GPIOC, GPIO_Pin_7);SCCB_delay();GPIO_SetBits(GPIOC, GPIO_Pin_6);SCCB_delay();GPIO_ResetBits(GPIOC, GPIO_Pin_6);SCCB_delay();
}

        按照宏定义进行转换:

static void SCCB_Ack(void)
{	SCL_L;SCCB_delay();SDA_L;SCCB_delay();SCL_H;SCCB_delay();SCL_L;SCCB_delay();
}

2.1.7  SCCB非应答信号

        非应答信号的发送也是 SCCB 协议中的一部分,用于处理数据传输失败或其他错误情况。在某些情况下,如果从设备无法正确接收数据,主设备可能会发送非应答信号并采取相应的错误处理措施。

static void SCCB_NoAck(void)
{GPIO_ResetBits(GPIOC, GPIO_Pin_6);SCCB_delay();GPIO_SetBits(GPIOC, GPIO_Pin_7);SCCB_delay();GPIO_SetBits(GPIOC, GPIO_Pin_6);SCCB_delay();GPIO_ResetBits(GPIOC, GPIO_Pin_6);SCCB_delay();
}

        按照宏定义进行转换:

static void SCCB_NoAck(void)
{	SCL_L;SCCB_delay();SDA_H;SCCB_delay();SCL_H;SCCB_delay();SCL_L;SCCB_delay();
}

2.1.8  SCCB等待应答信号

        让主机把 SDA 线设为高电平,延时一段时间后再检测 SDA 线的电平,若为低则返回 ENABLE 表示接收到从机的应答,反之返回 DISABLE。(L代表低,H代表高,以下直接使用SCL和SDA的宏定义表示)

static int SCCB_WaitAck(void) 	
{SCL_L;SCCB_delay();SDA_H;			SCCB_delay();SCL_H;SCCB_delay();if(SDA_read){SCL_L;return DISABLE;}SCL_L;return ENABLE;
}

2.1.9  发送数据

        首先,我们先创建一个用于接收数据的参数SendByt,它接受一个 8 位的字节数据作为输入参数,使用一个循环逐位发送这个字节的数据。循环从最高位开始,依次发送每一位,直到最低位。在每一次循环中,首先将 SCL(时钟线)拉低,然后通过 SCCB_delay 函数产生一定延迟,接着,根据 SendByte 的当前最高位,控制 SDA(数据线)为高电平或低电平,以此来发送数据的当前位,然后将 SendByte 左移一位,准备发送下一位数据。通过循环8次来完成一位数据的发送。

static void SCCB_SendByte(uint8_t SendByte) 
{uint8_t i=8;while(i--){SCL_L;SCCB_delay();if(SendByte&0x80)SDA_H;  else SDA_L;   SendByte<<=1;SCCB_delay();SCL_H;SCCB_delay();}SCL_L;
}

2.1.10  SCCB总线返回的数据

        首先声明了一个变量 i 并初始化为 8,以及再声明了一个变量 ReceiveByte 并初始化为 0,用于存储接收到的字节数据,然后,将 SDA(数据线)拉高,准备接收数据,通过一个循环逐位接收一个字节的数据。循环从最高位开始,依次接收每一位,直到最低位,读取 SDA 线上的数据,并根据其值决定是否将 ReceiveByte 的当前最低位设置为 1,将 ReceiveByte 左移一位,准备接收下一位数据。

static int SCCB_ReceiveByte(void)  
{ uint8_t i=8;uint8_t ReceiveByte=0;SDA_H;				while(i--){ReceiveByte<<=1;      SCL_L;SCCB_delay();SCL_H;SCCB_delay();	if(SDA_read){ReceiveByte|=0x01;}}SCL_L;return ReceiveByte;
}

2.1.11  SCCB写一个字节数据

        首先,明确两个宏,其实OV7725的设备地址:

#define ADDR_OV7725 0x42
#define DEV_ADR ADDR_OV7725

        为了确保SCCB总线通信正常启动,我们可以先进行判断SCCB是否发送起始信号,若是发送继续,若是失败则返回DISABLE。

    if(!SCCB_Start()){return DISABLE;}

        然后,通过 SCCB_SendByte 函数发送设备地址 DEV_ADR,用于指定要通信的设备,此时指定要通信的设备会发送应答,通过判断本机时候接收到应答,来确定两个设备间是否能进行正常通信,若是不能则终止信号,并返回DISABLE。

    SCCB_SendByte( DEV_ADR );                   if( !SCCB_WaitAck() ){SCCB_Stop(); return DISABLE;}

        若是能则将要写入的寄存器地址的低八位(WriteAddress 的低八位)发送给设备,以确定写入数据的目标寄存器,再次等待指定要通信的设备的应答,然后发送要写入的数据 SendByte 给设备,再次等待设备的应答,,停止 SCCB 通信,并返回 ENABLE 表示写入操作成功。

    SCCB_SendByte((uint8_t)(WriteAddress & 0x00FF));  SCCB_SendByte(SendByte);SCCB_WaitAck();   SCCB_Stop(); return ENABLE;

完整代码:

#define ADDR_OV7725 0x42
#define DEV_ADR ADDR_OV7725int SCCB_WriteByte( uint16_t WriteAddress , uint8_t SendByte )
{		if(!SCCB_Start()){return DISABLE;}SCCB_SendByte( DEV_ADR );                    /* Æ÷¼þµØÖ· */if( !SCCB_WaitAck() ){SCCB_Stop(); return DISABLE;}SCCB_SendByte((uint8_t)(WriteAddress & 0x00FF));   /* ÉèÖÃµÍÆðʼµØÖ· */      SCCB_WaitAck();	SCCB_SendByte(SendByte);SCCB_WaitAck();   SCCB_Stop(); return ENABLE;
}

2.1.12  SCCB读一串数据

int SCCB_ReadByte(uint8_t* pBuffer, uint16_t length, uint8_t ReadAddress)
{	if(!SCCB_Start()){return DISABLE;}SCCB_SendByte( DEV_ADR );         /* Æ÷¼þµØÖ· */if( !SCCB_WaitAck() ){SCCB_Stop(); return DISABLE;}SCCB_SendByte( ReadAddress );     /* ÉèÖÃµÍÆðʼµØÖ· */      SCCB_WaitAck();	SCCB_Stop(); if(!SCCB_Start()){return DISABLE;}SCCB_SendByte( DEV_ADR + 1 );     /* Æ÷¼þµØÖ· */ if(!SCCB_WaitAck()){SCCB_Stop(); return DISABLE;}while(length){*pBuffer = SCCB_ReceiveByte();if(length == 1){SCCB_NoAck();}else{SCCB_Ack(); }pBuffer++;length--;}SCCB_Stop();return ENABLE;
}

OV7725摄像头_时光の尘的博客-CSDN博客

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

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

相关文章

AI图书推荐:ChatGPT解码—人工智能增强生活指南

《ChatGPT解码—人工智能增强生活指南》&#xff08;ChatGPT Decoded. A Beginners Guide to AI-Enhanced Living &#xff09;是一本由 大卫维恩斯&#xff08;David Wiens &#xff09;所著的书籍&#xff0c;旨在帮助读者了解并有效利用GPT-4语言模型这一强大工具来提升日常…

Date、SimpleDateFormat、Calendar(JDK7以前的时间类)

Date: demo1: package Date;import java.util.Date;public class demo1 {public static void main(String[] args) {//1.创建对象表示一个时间Date d new Date();System.out.println(d);//2.形参是long类型&#xff0c;加LDate d1 new Date(0L);System.out.println(d1);//3…

Vectorbt回测框架

https://zhuanlan.zhihu.com/p/689057801

springmvc Web上下文初始化

Web上下文初始化 web上下文与SerlvetContext的生命周期应该是相同的&#xff0c;springmvc中的web上下文初始化是由ContextLoaderListener来启动的 web上下文初始化流程 在web.xml中配置ContextLoaderListener <listener> <listener-class>org.springframework.…

ncnn 优化量化

问题&#xff1a;发现推理时间过长&#xff0c;需要优化 当前正在做人脸检测部署&#xff0c;发现检测速度有点吓人&#xff0c;以下监测的时间 gpu&#xff1a; cpu&#xff1a; gpu推理大概整体时间200多毫秒&#xff0c;cpu推理时间300多毫秒&#xff0c;这里暂时没去考虑…

「TypeScript系列」TypeScript 对象及对象的使用场景

文章目录 一、TypeScript 对象1. 对象字面量2. 类实例化3. 使用接口定义对象形状4. 使用类型别名定义对象类型5. 使用工厂函数创建对象 二、TypeScript 对象属性及方法1. 对象属性2. 对象方法3. 访问器和修改器&#xff08;Getters 和 Setters&#xff09; 三、TypeScript 对象…

Oracle实践|内置函数之字符串函数

&#x1f4eb; 作者简介&#xff1a;「六月暴雪飞梨花」&#xff0c;专注于研究Java&#xff0c;就职于科技型公司后端工程师 &#x1f3c6; 近期荣誉&#xff1a;华为云云享专家、阿里云专家博主、腾讯云优秀创作者、ACDU成员 &#x1f525; 三连支持&#xff1a;欢迎 ❤️关注…

Jenkins - Pipeline try catch

Jenkins - Pipeline try catch 引言try catch 引言 Jenkins pipeline 脚本&#xff0c;有时因某些异常而中断执行&#xff0c;导致整个 pipeline job 都失败。为了整个 Job 能继续运行&#xff0c;我们需要处理某些异常。 try catch 当在 Jenkins Pipeline 中使用 try-catch…

基于redis的分布式锁解决token续期冲突的问题

场景&#xff1a;用户登录状态存储到redis&#xff0c;2小时后过期。在过期前的30分钟如果用户进行操作&#xff0c;则对登录状态进行续期&#xff0c;续期后仍有2小时时限&#xff0c;并更换新的token。在微服务模式下&#xff0c;如果两个服务同时请求续期&#xff0c;则会返…

C++模板——函数模板和类模板

目录 泛型编程 函数模板 函数模板概念 函数模板的定义和语法 函数模板的工作原理 函数模板的实例化 隐式实例化 显示实例化 函数模板的匹配原则 类模板 类模板的定义格式 类模板的实例化 泛型编程 什么是泛型编程&#xff1f; 泛型编程&#xff08;Generic Pr…

【代码随想录37期】Day18 找树左下角的值、路径总和、从中序与后序遍历序列构造二叉树

找树左下角的值 class Solution { public:int findBottomLeftValue(TreeNode *root) {TreeNode *node;queue<TreeNode *> q;q.push(root);while (!q.empty()) {node q.front(); q.pop();if (node->right) q.push(node->right);if (node->left) q.push(node-&…

机械臂学习笔记

目录 python 像素坐标系转空间坐标系 基于yolov7得并联机械臂实时抓取 KINOVA Gen3 lite机械臂上 UR5机械臂仿真平台 勤牛智能 Mirobot六自由度机械臂 Python SDK 调用示例 6自由度 c的 彭志辉 开源的&#xff1a; 搜索&#xff1a;机械臂 language:Python python 像…

【Linux-并发与竞争】

Linux-并发与竞争 ■ 原子操作■ 原子操作简介■ 原子整形操作 API 函数■ 原子位操作 API 函数■ 示例一&#xff1a;原子操作实验&#xff0c;使用原子变量来实现对实现设备的互斥访问 ■ 自旋锁■ 自旋锁 API 函数■ 死锁■ 最好的解决死锁方法就是获取锁之前关闭本地中断&a…

LeetCode 124 —— 二叉树中的最大路径和

阅读目录 1. 题目2. 解题思路3. 代码实现 1. 题目 2. 解题思路 二叉树的问题首先我们要想想是否能用递归来解决&#xff0c;本题也不例外&#xff0c;而递归的关键是找到子问题。 我们首先来看看一棵最简单的树&#xff0c;也就是示例 1。这样的一棵树总共有六条路径&#xf…

docker如何拉取nginx最新镜像并运行

要拉取Docker Hub上的最新Nginx镜像&#xff0c;您可以使用以下命令&#xff1a; docker pull nginx 这个命令会从Docker Hub下载最新版本的Nginx镜像。如果您想要拉取特定版本的Nginx镜像&#xff0c;可以指定版本号&#xff0c;例如&#xff1a; docker pull nginx:1.18.0 拉…

JQuery从入门到精通2万字面试题

目录 解释jQuery库中的$()函数是什么? 如何使用jQuery选择页面上的所有 元素?

详细分析tcping的基本知识以及用法

目录 前言1. 安装配置2. 基本知识3. Demo 前言 针对ping的基本知识推荐阅读&#xff1a;详细分析ping的基本知识以及常见网络故障的诊断&#xff08;图文解析&#xff09; 1. 安装配置 针对Window的下载如下&#xff1a; 安装路径&#xff1a;tcping官网 下载tcping.exe&a…

《微服务王国的守护者:Spring Cloud Dubbo的奇幻冒险》

5. 经典问题与解决方案 5.3 服务追踪与链路监控 在微服务架构的广袤宇宙中&#xff0c;服务间的调用关系错综复杂&#xff0c;如同一张庞大的星系网络。当一个请求穿越这个星系&#xff0c;经过多个服务节点时&#xff0c;如何追踪它的路径&#xff0c;如何监控整个链路的健康…

AIGC行业现在适合进入吗?

AIGC行业现在适合进入吗 人工智能生成内容&#xff08;AIGC&#xff0c;Artificial Intelligence Generated Content&#xff09;行业近年来迅速崛起&#xff0c;尤其在自然语言处理、图像生成和内容创作等方面取得了显著的进展。要判断当前是否适合进入AIGC行业&#xff0c;需…

C++ 实现深度优先搜索(DFS)的简单示例代码

C 实现深度优先搜索&#xff08;DFS&#xff09;的简单示例代码 #include <iostream> #include <vector> #include <stack>/**C 实现深度优先搜索&#xff08;DFS&#xff09;的简单示例代码。这段代码演示了如何在一个无向图中使用 DFS 进行遍历。 首先&am…