HAL库STM32常用外设教程(二)—— GPIO输入\输出

2023-12-05

HAL库STM32常用外设教程(二)—— GPIO输入\输出



前言

所用工具:
1、STM32F407ZGT6
2、STM32CubeMx软件
3、keil5
内容简述:
通过本篇内容您将学到
GPIO的8个工作模式

1、GPIO功能概述
2、GPIO的HAL库驱动
3、GPIO使用示例
(1)CubeMx配置
(2)GPIO驱动程序


一、GPIO功能概述

STM32F407ZG有8个16引脚的GPIO端口,从PA到PH,还有一个12引脚的PI端口,这些IO端口都连接在APB1总线上,最高时钟频率168MHz,GPIO引脚能承受5V电压,作为GPIO引脚使用时,我们可以输入或输出数字信号。
一个端子的16个GPIO引脚的功能可以单独设置,每个引脚的输入\输出数据可以单独读取或输出,一个GPIO引脚的内部结构如下图显示,内部有一个 双向保护二极管 (电路电压较大时可以保护电路),有可配置的是否使用的上拉和下拉电阻。每个GPIO都可配置为多个工作模式。
在这里插入图片描述
图1-1
GPIO的八个工作模式:
(1)浮空输入(input floating),作为GPIO的输入引脚,不使用上拉或下拉电阻。
(2)输入上拉(input pull-up),作为GPIO的输入引脚,使用内部上拉电阻,引脚外部无输入时读取的引脚输入电平为高电平。
(3)输入下拉(input pull-down),作为GPIO的输入引脚,使用内部下拉电阻,引脚外部无输入时读取的引脚输入电平为低电平。
(4)模拟(analog)输入,作为GPIO模拟引脚,用于ADC输入引脚或DAC输出引脚。
(5)开漏输出(output open-drain),如果不使用上拉或下拉电阻,开漏输出1时引脚为高阻态(相当于开路、不会输出任何电平),输出0时引脚为低电平,它们不能提供高电平,只能提供低电平或悬空状态,需要外接上拉电阻,才能实现输出高电平。
(6)推挽输出(output push-pull),如果不使用上拉或下拉电阻,推挽输出1时引脚为高电平,输出0时引脚为低电平。若需要增强引脚输出驱动能力,就可以使用上拉,例如,需要GPIO引脚输出高电平点LED时。
(7)推挽 复用输出(alternate function push-pull),当GPIO为复用IO时的推挽输出模式,一般用于外设功能,如I2C的SCL、SDA。
(8)开漏 复用输出(alternate function push-pull),当GPIO为复用IO时的开漏输出模式,一般用于外设功能,如TX1、MOSI、MISO.SCK.SS。
注:
①所有未进行任何配置的GPIO引脚,在系统复位后处于输入浮空模式。
②推挽输出与开漏输出的区别
推完输出是一种主动驱动型输出。当输出为高电平时,推完输出器件会提供正电压来驱动负载;而当输出为低电平时,它会提供零电压或接近零电压来驱动负载。这种输出方式可以提供较高的驱动能力和较快的切换速度,适合直接连接到负载或需要驱动高电平信号的应用。
开漏输出是一种被动驱动型输出。在高电平状态下,开漏输出器件不提供电压,相当于断开了输出端与电源之间的连接;而在低电平状态下,它会将输出端拉低,与地(GND)连接。为了使输出得到正确的电平,通常需要使用上拉电阻将输出端连接到正电源。这种输出方式可以实现多个设备的共享以及电平适配,适合需要进行电平转换、总线通信或多路输出的应用。
总体而言,推完输出可以提供两个电平的输出,具有较高的驱动能力和切换速度,而开漏输出只提供低电平输出,需要使用上拉电阻实现正电平。选择哪种输出方式取决于具体应用需求和连接环境。

二、GPIO的HAl库驱动

GPIO引脚的操作主要包括初始化、读取引脚输入和设置引脚输出,相关的HAL库1驱动程序定义在文件stm32f4xx_hal_gpio.h中,主要操作函数如下表所示。
表2-1 GPIO操作相关函数

函数名 功能描述
HAL_GPIO_Init() GPIO引脚初始化
HAL_GPIO_DeInit() GPIO引脚反初始化,恢复为复位后的状态
HAL_GPIO_WritePin() 使引脚输出0或1
HAL_GPIO_ReadPin() 读取引脚的输入电平
HAL_GPIO_TogglePin() 翻转引脚的输入
HAL_GPIO_LockPin() 锁定引脚配置,而不是锁定引脚的输入或输出状态

三、GPIO使用示例

1.示例功能

开发板有1个LED、4个按键和一个有源蜂鸣器,它们都是通过GPIO引脚控制,电路图如下:图上标识了连接的MCU引脚。
(1)LED电路,是由外接+3.3V 电源驱动的。当GPIO引脚输出为0时,LED点亮,输出为1时,LED熄灭。因此,与LED连接的引脚PF9和PF10要设置为推挽输出。
(2)对于KeyUp键,它的外端接的是+3.3V。在按键按下时,输入PA0引脚的是高电平,所以引脚PAO应该设置为输入下拉。在按键未按下时,输入是0。
(3)另外3个连接在PE2、PE3、PE4上的按键,外端接地。按键按下时,输入低电平,所
以使用输入上拉。
(4)蜂鸣器的控制端接PF8,应设置为推挽输出。当PF8输出为1时,蜂鸣器响,输出为
0时,蜂鸣器不响。
注:此处通过三极管状态判断,该三极管是NPN型三极管,该三极管导通条件为b、e之间的PN结一定要正偏(即b端的电压要高于e端),所以b端输出为1时三极管导通,蜂鸣器一段接24V,一端接地,才能导通。

在这里插入图片描述
图3-1
在这里插入图片描述 图3-3
在这里插入图片描述
图3-4

示例流程如下:
按下KEY0 键时,使LED0的输出翻转。
按下KEY1 键时,蜂鸣器输出翻转。
按下KEY2 键时,使LED1的输出翻转。
按下KEYUp 键时,使LED1和LED2的输出都翻转。

根据按键、LED和峰鸣器的电路,整理出MCU连接的GPIO引脚的输入/输出配置,如表2-2所示,根据表6-2的配置在CubeMX里进行设置。

表3-1 与按键、LED、蜂鸣器连接的MCU引脚的配置

用户标签 引脚名称 引脚功能 GPIO模式 上拉或下拉
LED0 PF9 GPIO_Output 推挽输出
LED1 PF10 GPIO_Output 推挽输出
KEY_UP PA0 GPIO_Intput 输入 下拉
KEY0 PE2 GPIO_Intput 输入 上拉
KEY1 PE3 GPIO_Intput 输入 上拉
KEY2 PE4 GPIO_Intput 输入 上拉
Buzzer PF8 GPIO_Output 推挽输出

在CubeMx里,我们选择STM32F407ZG新建一个项目,做如下一些初始设置。
(1)在SYS组件中,设置Debug接口为Serial Wrie。
(2)在RCC组件,设置HSE为Crystal/Ceramic Resonator.
(3)在时钟树上,设置HSE频率为8MHz(开发板实际晶振的频率)。主锁相环选择HSE作为时钟源,设置HCLK频率为168MHz,由软件自动配置时钟树。
(4)根据表 进行GPIO引脚设置。
(5)设置保存项目,导出工程。

在这里插入图片描述 图3-5 新建CubeMx工程

在这里插入图片描述
图3-6 在GPIO引脚视图上设置引脚功能,在GPIO组件配置界面对引脚进行更多配置

在这里插入图片描述 图3-7 文件保存设置

在这里插入图片描述 图3-8 文件保存设置

在这里插入图片描述 图3-9 导出工程

四、代码讲解

在CubeMX生成的工程后 自己又创建一个独立的文件夹(slave_module),并重新定义了一个头文件和源文件(keyled.c和keyled.h)自定义头文件和源文件放在这个文件夹中。这样,当重新生成工程时,CubeMX只会更新与配置相关的文件,而不会触及到自定义文件。
注:别忘了添加头文件的路径
在这里插入图片描述 图4-1 添加独立文件

main.h

代码如下:
该处的宏定义优势主要有三个
(1) 通过使用条件编译,你可以选择是否启用或禁用某些特定的功能。例如,如果没有定义KEY2_Pin,这部分按键检测的代码就不会被编译进去(在keyled.c里面从#ifdef KEY2_Pin到 #endif之间的程序),从而减小了程序的体积,如果定义了 ,则启用了相关的按键检测功能。
(2)可维护性: 使用宏定义可以提高代码的可维护性。如果你想禁用或启用按键检测,只需在程序的其他地方定义或注释掉 KEY2_Pin 宏,而不必修改检测按键的实际代码。这使得修改程序行为变得更加灵活和容易。
(3)平台移植性: 如果你的代码需要在不同的平台或不同的项目中使用,通过定义或取消定义宏,可以使相同的代码适应不同的硬件配置或项目需求,提高了代码的可移植性。

在这里插入图片描述 图4-2 引脚宏定义

#define KEY2_Pin GPIO_PIN_2
#define KEY2_GPIO_Port GPIOE
#define KEY1_Pin GPIO_PIN_3
#define KEY1_GPIO_Port GPIOE
#define KEY0_Pin GPIO_PIN_4
#define KEY0_GPIO_Port GPIOE
#define Buzzer_Pin GPIO_PIN_8
#define Buzzer_GPIO_Port GPIOF
#define LED0_Pin GPIO_PIN_9
#define LED0_GPIO_Port GPIOF
#define LED1_Pin GPIO_PIN_10
#define LED1_GPIO_Port GPIOF
#define WK_UP_Pin GPIO_PIN_0
#define WK_UP_GPIO_Port GPIOA

main.c
①添加头文件
在这里插入图片描述 图4-3 添加头文件

#include	"keyled.h"

②在while循环里面不断检测哪一个按键被按下
在这里插入图片描述 图4-4 添加main.c代码

		 KEYS curKey=ScanPressedKey(KEY_WAIT_ALWAYS);

	  switch(curKey)
	  {
	  case KEY0:
		  LED0_Toggle();
		  break;

	  case KEY2:
		  LED1_Toggle();
		  break;

	  case KEY_UP:
		  LED0_Toggle();
		  LED1_Toggle();
		  break;

	  case KEY1:
		  Buzzer_Toggle();
	  }

整个keyled.h

代码如下:

#ifndef KEYLED_H_
#define KEYLED_H_

#include	"main.h"   //在main.h中定义了Keys和LEDs的Labels宏

//表示4个按键的枚举类型
typedef enum {
	KEY_NONE=0,		//没有按键被按下
	KEY0,		//KeyLeft
	KEY2,		//KeyRight
	KEY_UP,			//KeyUp
	KEY1		//KeyDown
}KEYS;

#define		KEY_WAIT_ALWAYS		0	//作为函数ScanKeys()的一种参数,表示一直等待按键输入
//轮询方式扫描按键,timeout=KEY_WAIT_ALWAYS表示一直扫描,否是等待时间timeout, 延时单位ms
KEYS  ScanPressedKey(uint32_t timeout);


#ifdef	LED0_Pin		//LED1的控制
	#define	 LED0_Toggle()	HAL_GPIO_TogglePin(LED0_GPIO_Port, LED0_Pin)    //输出翻转

	#define	 LED0_ON()		HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET)  //输出0,亮

	#define	 LED0_OFF()		HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET)  //输出1,灭
#endif


#ifdef	LED1_Pin	//LED2的控制
	#define	 LED1_Toggle()	HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin)    //输出翻转

	#define	 LED1_ON()		HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET)  //输出0,亮

	#define	 LED1_OFF()		HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET)  //输出1,灭
#endif


#ifdef	Buzzer_Pin		//蜂鸣器的控制
	#define	 Buzzer_Toggle()	HAL_GPIO_TogglePin(Buzzer_GPIO_Port, Buzzer_Pin)  //输出翻转

	#define	 Buzzer_ON()		HAL_GPIO_WritePin(Buzzer_GPIO_Port, Buzzer_Pin, GPIO_PIN_RESET)  //输出0,蜂鸣器响

	#define	 Buzzer_OFF()		HAL_GPIO_WritePin(Buzzer_GPIO_Port, Buzzer_Pin, GPIO_PIN_SET)  //输出1蜂鸣器不响
#endif


#endif /* KEYLED_H_ */


整个keyled.c

代码如下:

#include	"keyled.h"

//轮询方式扫描4个按键,返回按键值
//timeout单位ms,若timeout=0表示一直扫描,直到有键按下
KEYS ScanPressedKey(uint32_t timeout)
{
	KEYS  key=KEY_NONE;
	uint32_t  tickstart = HAL_GetTick();  //当前计数值
	const uint32_t  btnDelay=20;	//按键按下阶段的抖动,延时再采样时间
	GPIO_PinState keyState;

	while(1)
	{
#ifdef	KEY0_Pin		 //如果定义了KeyLeft,就可以检测KeyLeft
		keyState=HAL_GPIO_ReadPin(KEY0_GPIO_Port, KEY0_Pin); //PE4=KeyLeft,低输入有效
		if (keyState==GPIO_PIN_RESET)
		{
			HAL_Delay(btnDelay);  //前抖动期
			keyState=HAL_GPIO_ReadPin(KEY0_GPIO_Port, KEY0_Pin); //再采样
			if (keyState ==GPIO_PIN_RESET)
				return	KEY0;
		}
#endif

#ifdef	KEY2_Pin 	//如果定义了KeyRight,就可以检测KeyRight
		keyState=HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin); //PE2=KeyRight,低输入有效
		if (keyState==GPIO_PIN_RESET)
		{
			HAL_Delay(btnDelay); //前抖动期
			keyState=HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin);//再采样
			if (keyState ==GPIO_PIN_RESET)
				return	KEY2;
		}
#endif

#ifdef	KEY1_Pin		//如果定义了KeyDown,就可以检测KeyDown
		keyState=HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin); //PE3=KeyDown,低输入有效
		if (keyState==GPIO_PIN_RESET)
		{
			HAL_Delay(btnDelay); //前抖动期
			keyState=HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin);//再采样
			if (keyState ==GPIO_PIN_RESET)
				return	KEY1;
		}
#endif

#ifdef	WK_UP_Pin		//如果定义了KeyUp,就可以检测KeyUp
		keyState=HAL_GPIO_ReadPin(WK_UP_GPIO_Port, WK_UP_Pin); //PA0=KeyUp,高输入有效
		if (keyState==GPIO_PIN_SET)
		{
			HAL_Delay(btnDelay); //10ms 抖动期
			keyState=HAL_GPIO_ReadPin(WK_UP_GPIO_Port, WK_UP_Pin);//再采样
			if (keyState ==GPIO_PIN_SET)
				return	KEY_UP;
		}
#endif

		if (timeout != KEY_WAIT_ALWAYS)  //没有按键按下时,会计算超时,timeout时退出
		{
			if ((HAL_GetTick() - tickstart) > timeout)
				break;
		}
	}

	return	key;
}

程序中定义了表示按键的枚举类型KEYS,函数ScanPressedKey(uint32_t timeout)用于检测案件输入,参数timeout是等待时间,如果timeout为KEY_WAIT_ALWAYS,就表示无限等待时间,即timeout=KEY_WAIT_ALWAYS,只有当按键按下时才能跳出ScanPressedKey函数。


五、总结

用上述的方法轮询的去查按键按下的状态不是处理按键不是一个好方法,之所以采用这种方法,是因为方便学习者能够更加深入的了解GPIO的用法。需要注意的是,这种方式的坏处是当按键按下时,其状态可能被触发很多次,例如,原本LED0处于熄灭状态,当按下KEY0时,LED0还是处于熄灭状态,之所以产生这种情况,并不是因为该程序没有起作用,可能是在极短的时间内LED0亮后接着熄灭了,可以在翻转电平的地方通过打印具体看到底执行了几次。
一般来说,还可以通过外部触发中断进行按键检测,在中断回调函数里面开启一个10ms的定时器,在定时器中断服务例程里面进行消抖,然后到中断外部去做进一步的处理时较好的一种方式。

参考书籍:《STM32Cube高效开发教程(基础篇)》王维波

“生活不可能像你想象得那么好,但也不会像你想象得那么糟。” ————《傲慢与偏见》

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

HAL库STM32常用外设教程(二)—— GPIO输入\输出 的相关文章

  • Tomcat 配合虚拟线程,一种新的编程体验

    Java 21 在今年早些时候的 9 月 19 日就正式发布 并开始正式引入虚拟线程 但是作为 Java 开发生态中老大哥 Spring 并没有立即跟进 而是在等待了两个月后的 11 月 29 日 伴随着 Spring Boot 3 2 版
  • leetcode:468. 验证IP地址

    验证IP地址 中等 249 相关企业 给定一个字符串 queryIP 如果是有效的 IPv4 地址 返回 IPv4 如果是有效的 IPv6 地址 返回 IPv6 如果不是上述类型的 IP 地址 返回 Neither 有效的IPv4地址 是
  • 《许犁庭与柔性世界》第三十章 出云,乘雷与君影

    再次回到校园 伴随着老师舒缓的节奏 徜徉在大大小小 深浅不一的草垛间 头顶碧空如洗 脚底金蕊盖霜 四周稻色黄莽 远处绿黛青芒 境与心会 便欲驭风跨蜿虹 1 若不是老师一把拉住 许犁庭估计会一头撞到前方那澹烟笼罩的巨树上 呵呵 第一次进入伊拉

随机推荐

  • 如何更好地平衡工作和生活?从实际出发

    在当今快节奏的生活中 平衡工作和生活的重要性越来越受到人们的关注 工作和生活是相辅相成的 只有通过良好的平衡 我们才能在工作和生活中获得最佳的状态和体验 然而 如何更好地平衡工作和生活呢 下面将介绍一些实用的方法 一 制定合理的时间表 制定
  • 某音订单接口在电商行业中的重要性及实践应用

    一 引言 随着移动互联网的快速发展 短视频平台抖音已经成为人们日常生活中不可或缺的一部分 越来越多的商家开始利用抖音平台推广和销售商品 从而实现商业变现 在这个过程中 抖音订单接口起到了至关重要的作用 本文将详细探讨抖音订单接口在电商行业中
  • 利用FileZilla下载PHOENIX高分辨率光谱

    下载PHOENIX光谱 在诸如CCF SED拟合中经常需要下载模板光谱 因此有一些大牛提供了相关的光谱库供大家使用 例如 Gottingen Spectral Libary by PHOENIX 论文 A new extensive lib
  • Node.js爬虫实战:搜狗图片爬取

    说在前面 当我们在网上寻找图片时 经常会遇到需要批量下载搜索结果中的图片的情况 而搜狗作为中国颇具影响力的搜索引擎之一 其图片搜索功能提供了丰富多样的图片资源 在这种情况下 我们希望能够通过编程的方式 批量下载搜狗图片搜索结果中的图片 以便
  • 数字化转型的引擎:企业与消费者同步变革

    数字化转型的引擎 企业与消费者同步变革 随着科技的飞速发展 数字化转型已成为企业生存和发展的必然选择 在这个过程中 企业和消费者都面临着巨大的变革和挑战 本文将探讨数字化转型的推动力 以及企业和消费者如何应对这些变革 一 数字化转型的推动力
  • VLAN实验

    题目要求 1 PC1和PC3所在接口为Access接口 2 PC2 4 5 6处于同一网段 其中PC2可以访问PC4 5 6 PC4可以访问PC5 但不能访问PC6 PC5不能访问PC6 3 PC1 3与PC2 4 5 6不在同一网段 4
  • 编译tzdata提示内存越界 malloc(): memory corruption /bin/sh: line 1: 32317 Aborted

    编译tzdata时 遇到如下所示的错误提示 awk v outfile main zi f ziguard awk africa antarctica asia australasia europe northamerica southam
  • 图片转表格软件有哪些?进来马上给你揭晓

    在整理资料的时候 你或多或少都会遇到一些图片格式的表格 需要你手动输入文字信息进行整理 然而 如果数据多的话 很容易出错 并且很浪费时间和精力 为了解决这个问题 你可以借助一些工具 来帮助你更高效地处理这种情况 这些工具通常可以将图片中的表
  • Ubuntu20.04使用SVN(Rabbitvcs)

    原文 https blog csdn net u014552102 article details 129914787 1 安装Rabbitvcs sudo apt get install rabbitvcs nautilus sudo r
  • WebGL笔记:js中矩阵库的使用

    矩阵库 手写矩阵 其实很麻烦 可以将其模块化 市面上已经有许多开源的矩阵库 比如 WebGL 编程指南 里的 cuon matrix js three js 的 Matrix3 和 Matrix4 对象 three js的 Matrix4
  • WebGL笔记:矩阵缩放的数学原理和实现

    矩阵缩放的数学原理 和平移一样 以同样的原理 也可以理解缩放矩阵 让向量OA基于原点进行缩放 x方向上缩放 sx y方向上缩放 sy z方向上缩放 sz 最终得到向量OB 矩阵缩放的应用 比如我要让顶点在x轴向缩放2 y轴向缩放3 轴向缩放
  • VBA字典与数组第八讲:数组及数组公式结果的制约性和集合性

    VBA数组与字典方案 教程 10144533 是我推出的第三套教程 目前已经是第二版修订了 这套教程定位于中级 字典是VBA的精华 我要求学员必学 7 1 3 9教程和手册掌握后 可以解决大多数工作中遇到的实际问题 这套字典教程共两册 一共
  • linux下gdb的使用以及dump转存文件的生成使用

    1 gdb Linux 调试器 gdb的使用 Linux之gdb的使用 gdb调试工具 如何在多线程 多进程以及正在运行的程序下调试 2 dump文件 c linux dump定位错误 Linux下更改 coredump文件生成路径 lin
  • Node.js爬虫实战:百度图片爬取

    说在前面 网络爬虫是一种自动化工具 能够模拟人类在互联网上浏览和提取信息的行为 它的应用范围广泛 包括数据采集 信息监控 搜索引擎优化等方面 而在数据抓取和处理中 获取图片资源往往是一个常见的需求 本文将介绍如何使用Node js和相关库构
  • 抖音商品详情接口在电商行业中的重要性及实时数据获取实现

    一 引言 抖音作为当下最热门的短视频平台之一 拥有庞大的用户群体和活跃度 为电商行业带来了巨大的商业机会 抖音商品详情接口作为连接抖音平台和电商系统的关键纽带 具有重要的作用 本文将深入探讨抖音商品详情接口在电商行业中的重要性 并介绍如何通
  • 提取音频哪个软件好?揭秘市面上的热门选择

    就像许多人都喜欢在视频中加入动听的音乐来增强情感氛围一样 你有没有想过如果能够将那些打动你的音乐片段单独提取出来 作为自己的手机铃声 那该有多好呢 是的 某个视频中听到了一段难以忘怀的音乐 你可能会迫不及待地想将其设置为自己的专属手机铃声
  • 阿里云服务器有WordPress还可以再安装宝塔面板吗?

    不推荐 宝塔面板要求必须是纯净的操作系统环境安装宝塔 否则可能会有问题的 所以最好是先安装宝塔面板 再去安装wordpress 原文地址 阿里云服务器 WordPress 还可以再安装宝塔面板吗 轻量云Cloud WordPress一款广泛
  • C++简易计数器

    Created by Carlgood Note This program is written in version DEV C 5 11 include
  • 腾讯云用centos还是ubuntu系统好?

    腾讯云服务器提供了多种操作系统选择 包括 CentOS Ubuntu Windows Server 等 用户可以根据自己的需求和习惯选择适合的操作系统 通常比较推荐安装centos 7 x版本的系统 但在 CentOS 和 Ubuntu 之
  • HAL库STM32常用外设教程(二)—— GPIO输入\输出

    HAL库STM32常用外设教程 二 GPIO输入 输出 文章目录 HAL库STM32常用外设教程 二 GPIO输入 输出 前言 一 GPIO功能概述 二 GPIO的HAl库驱动 三 GPIO使用示例 1 示例功能 四 代码讲解 五 总结