本文目的是主要介绍通过STM32F103C8T6去理解OLED屏显和汉字点阵编码原理,并使用STM32F103的SPI或IIC接口去实现显示学号和姓名,显示AHT20的温湿度,并实现滑动显示长字符的实例。
文章目录
前言
※ 温湿度传感器AHT20的数据采集实现,可参考博主的博客进行学习:https://blog.csdn.net/qq_52199251/article/details/127625858
※ 本文主要讲解采用STM32F103C8T6去理解OLED屏显和汉字点阵编码原理,并使用STM32F103的SPI或IIC接口去实现显示学号和姓名,显示AHT20的温湿度,并实现滑动显示长字符的实例。
等待那一刻来临时,山川、花海均为你而贺
(一)需求分析
※ 采用STM32F103C8T6去理解OLED屏显和汉字点阵编码原理,并使用STM32F103的SPI或IIC接口去实现显示学号和姓名,显示AHT20的温湿度,并实现滑动显示长字符的实例。
所需工具:
1、芯片: STM32F103C8T6
2、IDE: MDK-Keil软件
3、温湿度传感器AHT20
4、OLED屏
(二)SPI详解
1.SPI接口简介
🎈 SPI接口是一种同步串行总线(Serial Peripheral Interface)多用于Flash存储器(如NOR Flash&Nand Flash),ADC、LCD控制器等外围器件的通讯接口。大大增强了处理器的外设扩展能力。
SPI接口缩写
SSEL:slave select,常常也被写作CS(chip select)或SS(slave select)
SCK:serial clock,常常也写作SCLK或SCL
MISO:master input slave output,常常被简写为SO(slave output,也有说是serial output)
MOSI:master output slave input,常常被简写为SI(slave input,也有说是serial input)
🎈 在SPI总线上,当一个主机和多个从机进行通讯时,通过CS来选择和那个设备进行通讯,可以将CS理解为enable信号,低电平有效。当多个从机存在时,这就要求从机的MISO口具有三态特性,使得该接口在器件未被选通时表现为高阻抗。当前多数SPI设备在不做通讯时,默认的状态通常就是高阻抗状态。
🎈 如下图是某个SPI Nand Flash中的Timing图。
🎈 SPI接口通常少有被用作一主多从的状态。常常被用作一对一的SPI通讯,常常的连接方式如下方式。
2.SPI四线
①、 CS/SS, Slave Select/Chip Select,这个是片选信号线,用于选择需要进行通信的从设备。I2C 主机是通过发送从机设备地址来选择需要进行通信的从机设备的, SPI 主机不需要发送从机设备,直接将相应的从机设备片选信号拉低即可。
②、 SCK, Serial Clock,串行时钟,和 I2C 的 SCL 一样,为 SPI 通信提供时钟。
③、 MOSI/SDO, Master Out Slave In/Serial Data Output,简称主出从入信号线,这根数据线只能用于主机向从机发送数据,也就是主机输出,从机输入。
④、 MISO/SDI, Master In Slave Out/Serial Data Input,简称主入从出信号线,这根数据线只能用户从机向主机发送数据,也就是主机输入,从机输出。
🎈 **SPI 通信都是由主机发起的,主机需要提供通信的时钟信号。**主机通过 SPI 线连接多个从设备的结构如下图所示:
3.SPI四种工作模式
🎈 SPI 有四种工作模式,通过串行时钟极性(CPOL)和相位(CPHA)的搭配来得到四种工作模式:
①、 CPOL=0,串行时钟空闲状态为低电平。
②、CPOL=1,串行时钟空闲状态为高电平,此时可以通过配置时钟相位(CPHA)来选择具体的传输协议。
③、CPHA=0,串行时钟的第一个跳变沿(上升沿或下降沿)采集数据。
④、 CPHA=1,串行时钟的第二个跳变沿(上升沿或下降沿)采集数据。
这四种工作模式如下图所示:
4.SPI时序图
🎈 以 CPOL=0, CPHA=0 这个工作模式为例, SPI 进行全双工通信的时序如下图所示:
🎈 从上图可以看出, SPI 的时序图很简单,不像 I2C 那样还要分为读时序和写时序,因为 SPI 是全双工的,所以读写时序可以一起完成。
图中CS 片选信号先拉低,选中要通信的从设备,然后通过 MOSI 和 MISO 这两根数据线进行收发数据, MOSI 数据线发出了0XD2 这个数据给从设备,同时从设备也通过 MISO 线给主设备返回了 0X66 这个数据。这个就是 SPI 时序图。
5.SPI特性
🎈 SPI总线包括4条逻辑线,定义如下:
MISO:
Master input slave output
主机输入,从机输出(数据来自从机);
MOSI:Master output slave input
主机输出,从机输入(数据来自主机);
SCLK :Serial Clock
串行时钟信号,由主机产生发送给从机;
SS:Slave Select
片选信号,由主机发送,以控制与哪个从机通信,通常是低电平有效信号。
🎈 其他制造商可能会遵循其他命名规则,但是最终他们指的相同的含义。以下是一些常用术语;
MISO也可以是SIMO,DOUT,DO,SDO或SO(在主机端);
MOSI也可以是SOMI,DIN,DI,SDI或SI(在主机端);
NSS也可以是CE,CS或SSEL;
SCLK也可以是SCK;
🎈 本文将按照以下命名进行讲解[MISO, MOSI, SCK,NSS]
下图显示了单个主机和单个从机之间的典型SPI连接。
5.1 SPI时钟频率
🎈SPI总线上的主机必须在通信开始时候配置并生成相应的时钟信号。在每个SPI时钟周期内,都会发生全双工数据传输。
🎈主机在MOSI线上发送一位数据,从机读取它,而从机在MISO线上发送一位数据,主机读取它。
🎈 就算只进行单向的数据传输,也要保持这样的顺序。这就意味着无论接收任何数据,必须实际发送一些东西!在这种情况下,我们称其为虚拟数据;
🎈 从理论上讲,只要实际可行,时钟速率就可以是您想要的任何速率,当然这个速率受限于每个系统能提供多大的系统时钟频率,以及最大的SPI传输速率
。
5.2 时钟极性(CKP/Clock Polarity)
🎈 除了配置串行时钟速率(频率)外,SPI主设备还需要配置时钟极性。
🎈 根据硬件制造商的命名规则不同,时钟极性通常写为CKP或CPOL。时钟极性和相位共同决定读取数据的方式,比如信号上升沿读取数据还是信号下降沿读取数据;
🎈 CKP可以配置为1或0。
这意味着您可以根据需要将时钟的默认状态(IDLE)设置为高或低。极性反转可以通过简单的逻辑逆变器实现。您必须参考设备的数据手册才能正确设置CKP和CKE。
CKP = 0:时钟空闲IDLE为低电平 0;
CKP = 1:时钟空闲IDLE为高电平1;
5.3 时钟相位 (CKE /Clock Phase (Edge))
🎈 除配置串行时钟速率和极性外,SPI主设备还应配置时钟相位(或边沿)。根据硬件制造商的不同,时钟相位通常写为CKE或CPHA;
🎈 顾名思义,时钟相位/边沿,也就是采集数据时是在时钟信号的具体相位或者边沿;
CKE = 0:在时钟信号SCK的第一个跳变沿采样;
CKE = 1:在时钟信号SCK的第二个跳变沿采样;
5.4 时钟配置总结
🎈 综上几种情况,下图总结了所有时钟配置组合,并突出显示了实际采样数据的时刻;
其中黑色线为采样数据的时刻;
蓝色线为SCK时钟信号;
具体如下图所示;
6.模式编号
🎈 SPI的时钟极性和相位的配置通常称为 SPI模式,所有可能的模式都遵循以下约定;具体如下表所示;
🎈 除此之外,我们还应该仔细检查微控制器数据手册中包含的模式表,以确保一切正常。
7.多从机模式
🎈 前面说到SPI总线必须有一个主机,可以有多个从机,那么具体连接到SPI总线的方法有以下两种:
7.1 多NSS
🎈 通常,每个从机都需要一条单独的SS线。
🎈 如果要和特定的从机进行通讯,可以将相应的NSS信号线拉低,并保持其他NSS信号线的状态为高电平;如果同时将两个NSS信号线拉低,则可能会出现乱码,因为从机可能都试图在同一条MISO线上传输数据,最终导致接收数据乱码。
具体连接方式如下图所示;
7.2 菊花链
🎈 在数字通信世界中,在设备信号(总线信号或中断信号)以串行的方式从一 个设备依次传到下一个设备,不断循环直到数据到达目标设备的方式被称为菊花链。
菊花链的最大缺点是因为是信号串行传输,所以一旦数据链路中的某设备发生故障的时候,它下面优先级较低的设备就不可能得到服务了;
另一方面,距离主机越远的从机,获得服务的优先级越低,所以需要安排好从机的优先级,并且设置总线检测器,如果某个从机超时,则对该从机进行短路,防止单个从机损坏造成整个链路崩溃的情况;
具体的连接如下图所示;
所以最终的数据流向图可以表示为:
SCK为时钟信号,8clks表示8个边沿信号;
其中D为数据,X为无效数据;
🎈 所以不难发现,菊花链模式充分使用了SPI其移位寄存器的功能,整个链充当通信移位寄存器,每个从机在下一个时钟周期将输入数据复制到输出。
8.SPI优缺点
8.1 SPI通讯优势
🎈 使SPI作为串行通信接口脱颖而出的原因很多;
全双工串行通信;
高速数据传输速率。
简单的软件配置;
极其灵活的数据传输,不限于8位,它可以是任意大小的字;
非常简单的硬件结构。从站不需要唯一地址(与I2C不同)。从机使用主机时钟,不需要精密时钟振荡器/晶振(与UART不同)。不需要收发器(与CAN不同)。
8.2 SPI的缺点
没有硬件从机应答信号(主机可能在不知情的情况下无处发送);
通常仅支持一个主设备;
需要更多的引脚(与I2C不同);
没有定义硬件级别的错误检查协议;
与RS-232和CAN总线相比,只能支持非常短的距离;
(三)OLED屏显简介
1.OLED介绍
🎈 OLED即有机发光二极管(Organic Light-Emitting Diode),又称为有机电激光显示(Organic Electroluminesence Display, OELD)。
OLED 由于同时具备自发光,不需背光源、对比度高、厚度薄、视角广、反应速度快、可用于挠曲性面板、使用温度范围广、构造及制程较简单等优异之特性,被认为是下一代的平面显示器新兴应用技术。
🎈 LCD 都需要背光,而 OLED 不需要,因为它是自发光的。
这样同样的显示,OLED 效果要来得好一些。🎈 以目前的技术,OLED 的尺寸还难以大型化,但是分辨率确可以做到很高。
LCD 都需要背光,而 OLED 不需要,因为它是自发光的。这样同样的显示,OLED 效果要来得好一些。以目前的技术,OLED 的尺寸还难以大型化,但是分辨率确可以做到很高。
我们使用的是 ALINETEK 的 OLED 显示模块,该模块有以下特点:
1)模块有单色和双色两种可选,单色为纯蓝色,而双色则为黄蓝双色。
2)尺寸小,显示尺寸为 0.96 寸,而模块的尺寸仅为 27mmx26mm 大小。
3)高分辨率,该模块的分辨率为128x64。
4)多种接口方式,该模块提供了总共 5 种接口包括:6800、8080 两种并行接口方式、3线或 4 线的穿行 SPI 接口方式、IIC 接口方式(只需要 2 根线就可以控制 OLED 了)。
5)不需要高压,直接接 3.3V 就可以工作了。
注意该模块不和 5.0V 接口兼容,所以在使用的时候一定要小心,勿直接接到 5V 的系统上去,否则可能烧坏模块。
注意:该模块不和 5.0V 接口兼容,所以在使用的时候一定要小心,勿直接接到 5V 的系统上去,否则可能烧坏模块。
2.OLED发光原理
🎈 相比LCD,OLED除了开关管T1之外,还多了控制管T2。
寻址信号
Gate
,加载到SW_TFT(T1)的栅极,控制它的导通/开关管T1;
数据信号Source
,通过T1,加载到DRV_TFT(T2)的栅极,控制流过OLED的电流,实现不同灰阶度/控制管T2;
储存电容Cst
:Source信号将保持在T2栅极持续到下一次寻址。
3.OLED屏显特点
🎈 OLED
与LED\LCD
对比
1、相较于LED或LCD的晶体层,OLED的有机塑料层更薄、更轻而且更富于柔韧性。
2、OLED的发光层比较轻,因此它的基层可使用富于柔韧性的材料,而不会使用刚性材料。OLED基层为塑料材质,而LED和LCD则使用玻璃基层。
3、OLED比LED更亮,OLED有机层要比LED中与之对应的无机晶体层薄很多,因而OLED的导电层和发射层可以采用多层结构。此外,LED和LCD需要用玻璃作为支撑物,而玻璃会吸收一部分光线。OLED则无需使用玻璃。
4、OLED并不需要采用LCD中的逆光系统。LCD工作时会选择性地阻挡某些逆光区域,从而让图像显现出来,而OLED则是靠自身发光。因为OLED不需逆光系统,所以它们的耗电量小于LCD(LCD所耗电量中的大部分用于逆光系统)。这一点对于靠电池供电的设备(例如移动电话)来说,尤其重要。
5、OLED制造起来更加容易,还可制成较大的尺寸。OLED为塑胶材质,因此可以将其制作成大面积薄片状。而想要使用如此之多的晶体并把它们铺平,则要困难得多。
6、OLED的视野范围很广,可达170度左右。而LCD工作时要阻挡光线,因而在某些角度上存在天然的观测障碍。OLED自身能够发光,所以视域范围也要宽很多。
4.OLED屏显优势
A、0.96寸OLED屏,支持黑白、黑蓝或者黄蓝双色显示
B、128x64分辨率,显示效果清晰,对比度高
C、超大可视角度:大于160°(显示屏中可视角度最大的一种屏幕)
D、宽电压供电(3V~5V),兼容3.3V和5V逻辑电平,无需电平转换芯片
E、默认为4线制SPI总线,可以选择3线制SPI总线或者IIC总线
F、超低功耗:正常显示仅为0.06W(远低于TFT显示屏)
G、提供丰富的STM32、C51、Arduino、Raspberry Pi以及MSP430平台示例程序
H、提供底层驱动技术支持
🎈 接口定义:
① 本模块支持IIC、3线制SPI以及4线制SPI接口总线模式切换(如图2红框内所示),具体说明如下:
a、使用4.7K电阻只焊接R3、R4,则选择4线制SPI总线接口(默认);
b、使用4.7K电阻只焊接R2、R3,则选择3线制SPI总线接口;
c、使用4.7K电阻只焊接R1、R4、R6、R7、R8,则选择IIC总线接口;
② 接口总线模式切换后,需要选择相应配套的软件和相应的接线引脚(如图1所示),模块才能正常运行。相应的接线引脚说明如下:
a、选择4线制SPI总线接口,所有的引脚都需要使用;
b、选择3线制SPI总线接口,只有DC引脚不需要使用(可以不接),其他引脚都需要使用;
c、选择IIC总线接口,只需要使用GND、VCC、D0、D1这四个引脚,同时将RES接高电平(可以接VCC),DC和CS接电源地;
🎈 引脚连线如下:
模块引脚 | 对应STM32开发板接线 |
---|---|
GND | GND |
VCC | 3.3V/5V |
D0 | PB13 |
D1 | PB15 |
RES | PB12 |
DC | PB10 |
CS | PB11 |
更多介绍请移步:http://www.lcdwiki.com/zh/0.96inch_SPI_OLED_Module
(四)“初展锋芒”——STM32+OLED显示姓名学号
1.字模提取器预置
🎈 网站上搜索字模提取器 V2.2即可下载文件包:
2.提取器环境配置
🎈 配置如下:
注意:如果需要将名字倒着输出,可以选择字节倒序。
🎈 选择C51即可生成点阵。
3.代码编写
🎈 首先对自己的名字进行取模:
🎈 内容显示 TEST_MainPage
函数->test.c
文件
void TEST_MainPage(void)
{
// GUI_ShowString(28,0,"abc",16,1);//英文姓名
GUI_ShowCHinese(28,20,16,"彭旺仔",1);//中文姓名
GUI_ShowString(4,48,"632007030***",16,1);//数字详细
delay_ms(1500);
delay_ms(1500);
}
🎈 找到工程项目中oledfont.h
文件下的cfont16数组:
🎈 主函数代码如下:
#include "delay.h"
#include "sys.h"
#include "oled.h"
#include "gui.h"
#include "test.h"
int main(void)
{
delay_init();
NVIC_Configuration();
OLED_Init();
OLED_Clear(0);
while(1)
{
TEST_MainPage();
}
}
4.电路搭建
🎈 发现没有报错
🎈 电路搭建如下:
5.效果实现
🎈 烧录实现
实现如下:
(五)“小试牛刀”——STM32+OLED显示温湿度
1.字幕提取生成点阵
🎈 在文字输入区输入温湿度显示
,并ctrl+enter
,得到显示图:
2.代码编写
🎈 找到项目中oledfont.h
,增添所需文字点阵:
"温",0x00,0x00,0x23,0xF8,0x12,0x08,0x12,0x08,0x83,0xF8,0x42,0x08,0x42,0x08,0x13,0xF8,
0x10,0x00,0x27,0xFC,0xE4,0xA4,0x24,0xA4,0x24,0xA4,0x24,0xA4,0x2F,0xFE,0x00,0x00,/*"温",0*/ "湿",0x01,0x00,0x00,0x80,0x3F,0xFE,0x22,0x20,0x22,0x20,0x3F,0xFC,0x22,0x20,0x22,0x20,
0x23,0xE0,0x20,0x00,0x2F,0xF0,0x24,0x10,0x42,0x20,0x41,0xC0,0x86,0x30,0x38,0x0E,/*"湿",0*/
"度",0x00,0x00,0x27,0xF8,0x14,0x08,0x14,0x08,0x87,0xF8,0x44,0x08,0x44,0x08,0x17,0xF8, 0x11,0x20,0x21,0x20,0xE9,0x24,0x25,0x28,0x23,0x30,0x21,0x20,0x2F,0xFE,0x00,0x00,/*"度",0*/
"显",0x00,0x00,0x1F,0xF0,0x10,0x10,0x10,0x10,0x1F,0xF0,0x10,0x10,0x10,0x10,0x1F,0xF0,
0x04,0x40,0x44,0x44,0x24,0x44,0x14,0x48,0x14,0x50,0x04,0x40,0xFF,0xFE,0x00,0x00,/*"显",0*/ "示",0x00,0x00,0x3F,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0xFE,0x01,0x00,
0x01,0x00,0x11,0x10,0x11,0x08,0x21,0x04,0x41,0x02,0x81,0x02,0x05,0x00,0x02,0x00,/*"示",0*/
🎈 在bsp_i2c.c
中重新写入函数read_AHT20:
void read_AHT20(void)
{
uint8_t i;
for(i=0; i<6; i++)
{
readByte[i]=0;
}
//-------------
I2C_Start();
I2C_WriteByte(0x71);
ack_status = Receive_ACK();
readByte[0]= I2C_ReadByte();
Send_ACK();
readByte[1]= I2C_ReadByte();
Send_ACK();
readByte[2]= I2C_ReadByte();
Send_ACK();
readByte[3]= I2C_ReadByte();
Send_ACK();
readByte[4]= I2C_ReadByte();
Send_ACK();
readByte[5]= I2C_ReadByte();
SendNot_Ack();
//Send_ACK();
I2C_Stop();
//--------------
if( (readByte[0] & 0x68) == 0x08 )
{
H1 = readByte[1];
H1 = (H1<<8) | readByte[2];
H1 = (H1<<8) | readByte[3];
H1 = H1>>4;
H1 = (H1*1000)/1024/1024;
T1 = readByte[3];
T1 = T1 & 0x0000000F;
T1 = (T1<<8) | readByte[4];
T1 = (T1<<8) | readByte[5];
T1 = (T1*2000)/1024/1024 - 500;
AHT20_OutData[0] = (H1>>8) & 0x000000FF;
AHT20_OutData[1] = H1 & 0x000000FF;
AHT20_OutData[2] = (T1>>8) & 0x000000FF;
AHT20_OutData[3] = T1 & 0x000000FF;
}
else
{
AHT20_OutData[0] = 0xFF;
AHT20_OutData[1] = 0xFF;
AHT20_OutData[2] = 0xFF;
AHT20_OutData[3] = 0xFF;
printf("lyy");
}
/*通过串口显示采集得到的温湿度
printf("\r\n");
printf("温度:%d%d.%d",T1/100,(T1/10)%10,T1%10);
printf("湿度:%d%d.%d",H1/100,(H1/10)%10,H1%10);
printf("\r\n");*/
t=T1/10;
t1=T1%10;
a=(float)(t+t1*0.1);
h=H1/10;
h1=H1%10;
b=(float)(h+h1*0.1);
sprintf(strTemp,"%.1f",a); //调用Sprintf函数把DHT11的温度数据格式化到字符串数组变量strTemp中
sprintf(strHumi,"%.1f",b); //调用Sprintf函数把DHT11的湿度数据格式化到字符串数组变量strHumi中
GUI_ShowCHinese(16,00,16,"温湿度显示",1);
GUI_ShowCHinese(16,20,16,"温度",1);
GUI_ShowString(53,20,strTemp,16,1);
GUI_ShowCHinese(16,38,16,"湿度",1);
GUI_ShowString(53,38,strHumi,16,1);
delay_ms(1500);
delay_ms(1500);
}
🎈 主函数main.c
文件:
3.电路搭建
🎈 烧录无报错。
🎈 电路搭建如下:
4.效果实现
🎈 效果实现如下:
湿度显示1
🎈 可以发现,当手握AHT20时候,温度与湿度都会有所上升。
湿度显示2
(六)“大刀阔斧”——STM32+OLED显示滚屏字幕
1.诗词点阵生成
🎈 在文字输入区输入梅花香自苦寒来
,并ctrl+enter
,得到显示图:
2.代码编写
🎈 添加文字字模到oledfont.h
文件中:
🎈 在test.c
中对函数Test_MainPage
进行修改:
void TEST_MainPage(void)
{
GUI_ShowCHinese(28,10,16,"梅花香自苦寒来",1);
delay_ms(1500);
}
🎈 修改主函数,添加相应的OLED滚动代码:
🎈 删除while
内的函数Test_MainPage:
#include "delay.h"
#include "sys.h"
#include "oled.h"
#include "gui.h"
#include "test.h"
int main(void)
{
delay_init(); //延时函数初始化
NVIC_Configuration(); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
OLED_Init(); //初始化OLED
OLED_Clear(0); //清屏(全黑)
OLED_WR_Byte(0x2E,OLED_CMD); //关闭滚动
OLED_WR_Byte(0x27,OLED_CMD); //水平向左或者右滚动 26/27
OLED_WR_Byte(0x00,OLED_CMD); //虚拟字节
OLED_WR_Byte(0x00,OLED_CMD); //起始页 0
OLED_WR_Byte(0x07,OLED_CMD); //滚动时间间隔
OLED_WR_Byte(0x07,OLED_CMD); //终止页 7
OLED_WR_Byte(0x00,OLED_CMD); //虚拟字节
OLED_WR_Byte(0xFF,OLED_CMD); //虚拟字节
TEST_MainPage();
OLED_WR_Byte(0x2F,OLED_CMD); //开启滚动
}
3.电路搭建
🎈 烧录无报错。
🎈 电路搭建如下(类似于实验一)
4.效果实现如下:
🎈 宝剑锋从磨砺出,梅花香自苦寒来
滚屏
(七)总结
🎈 本文介绍了通过STM32F103C8T6去理解OLED屏显和汉字点阵编码原理,并使用STM32F103的SPI或IIC接口去实现显示学号和姓名,显示AHT20的温湿度,并实现滑动显示长字符的实例。
🎈 本次实操还是挺复杂的,姓名与学号的显示挺顺利的,滚动字幕的实现也不是太难,查阅资料与细心操作,也是一次实现,但对于显示AHT20的温度和湿度,这里首先是实现了显示,但是温度和湿度一直没有采集到,直到后面反复调试代码,才发现是电压需要给5V,同时接触不良导致了无法采集数据,最后反复测试与检查电路,成功实现了,还是成就满满的,嵌入式开发感觉就是这个过程很难,但最终成功的时候,哪一种喜悦真的是发自内心的。
加油吧,少年!!!
寄语:一路走来
热泪盈眶
不知从何处说起
但满是成长,还望温柔以待
(八)参考文献
[1]https://blog.csdn.net/zangqihu/article/details/122626009
[2]https://blog.csdn.net/as480133937/article/details/105764119
[3]https://blog.csdn.net/qq_46467126/article/details/121439142
[4]https://blog.csdn.net/m0_66322708/article/details/124148443