1、PIC32参考资源
PIC32系列参考手册 中文版 链接地址:PIC32系列参考手册 第17章 10位AD转换器
2、10位ADC转换器简介
PIC32MX10位模数(Analog-to-Digital,A/D)转换器(或 ADC)具有以下特性;
• 逐次逼近寄存器 (Successive Approximation Register, SAR)转换
• 最多 16 个模拟输入引脚
• 外部参考电压输入引脚
• 一个单极性差分采样 / 保持放大器 (Sample-and-Hold Amplifier, SHA)
• 自动通道扫描模式
• 可选择的转换触发源
• 16 字的转换结果缓冲区
• 可选择的缓冲区填充模式
• 8 个转换结果格式选项
• 可在 CPU SLEEP(休眠)和 IDLE(空闲)模式下工作
10 位 ADC 最多有 16 个模拟输入引脚,指定为 AN0-AN15。此外,还有两个模拟输入引脚用于连接外部参考电压。
10 位 ADC 与 16 字结果缓冲区连接。在从结果缓冲区读取每个 10 位转换结果时,转换结果被转换为 8 种 32 位输出格式之一。
3、控制寄存器
ADC 模块包含以下特殊功能寄存器:
AD1CON1、AD1CON2和AD1CON3控制寄存器用于控制 ADC 模块的操作。
AD1CHS寄存器用于选择要连接到SHA 的输入引脚。ADC输入通道选择寄存器
AD1PCFG 寄存器用于将模拟输入引脚配置为模拟输入或数字 I/O。
AD1CSSL 寄存器用于选择要顺序扫描的输入。
中断控制的相关位
• “IFS1:中断标志状态寄存器 1”中的中断请求标志状态位 (AD1IF)
• “IEC1:中断允许控制寄存器 1”中的中断允许控制位 (AD1IE)
• “IPC6:中断优先级控制寄存器 6”中的中断优先级控制位(AD1IP<2:0>)和(AD1IS<1:0>)
AD1CON1:ADC控制寄存器1
bit 15 ON:ADC 工作模式位
1 = A/D 转换器模块正在工作
0 = A/D 转换器关闭
注: 使用 1:1 PBCLK 分频比时,在清零模块 ON 位的指令之后,用户的软件不应立即在 SYSCLK周期中读 / 写外设的 SFR。
bit 14 FRZ :调试异常模式冻结位
1 = 在 CPU 进入调试异常模式时冻结工作
0 = 在 CPU 进入调试异常模式时继续工作
注: FRZ 仅在调试异常模式下可写。它在正常模式下读为 0。
bit 13 SIDL: : IDLE (空闲)模式停止位
1 = 当器件进入 IDLE (空闲)模式时,模块停止工作
0 = 在 IDLE (空闲)模式下模块继续工作
bit 12-11 保留:写入 0 ;忽略读操作
bit 10-8 FORM<2:0> :数据输出格式位
011 = 有符号小数, 16 位 (DOUT = 0000 0000 0000 0000 sddd dddd dd00 0000)
010 = 小数, 16 位 (DOUT = 0000 0000 0000 0000 dddd dddd dd00 0000)
001 = 有符号整数, 16 位 (DOUT = 0000 0000 0000 0000 ssss sssd dddd dddd)
000 = 整数, 16 位 (DOUT = 0000 0000 0000 0000 0000 00dd dddd dddd)
111 = 有符号小数, 32 位 (DOUT = sddd dddd dd00 0000 0000 0000 0000)
110 = 小数, 32 位 (DOUT = dddd dddd dd00 0000 0000 0000 0000 0000)
101 = 有符号整数, 32 位 (DOUT = ssss ssss ssss ssss ssss sssd dddd dddd)
100 = 整数, 32 位 (DOUT = 0000 0000 0000 0000 0000 00dd dddd dddd)
bit 7-5 SSRC<2:0> :转换触发源选择位
111 = 由内部计数器结束采样并启动转换(自动转换)
110 = 保留 101 = 保留 100 = 保留 011 = 保留
010 = 由 Timer3 周期匹配结束采样并启动转换
001 = 由 INT0 引脚的有效跳变结束采样并启动转换
000 = 由清零 SAMP 位结束采样并启动转换
bit 4 CLRASAM :停止转换序列位 (在产生第一个 A/D 转换器中断时)
1 = 在产生第一个 ADC 中断时停止转换。当产生 ADC 中断时,硬件会清零 ASAM 位。
0 = 正常工作,缓冲区内容会被下一个转换序列覆盖
bit 3 保留:写入 0 ;忽略读操作
bit 2 ASAM:ADC 采样自动启动位
1 = 最后一次转换结束后立即开始采样; SAMP 位自动置 1
0 = SAMP 位置 1 时开始采样
bit 1 SAMP:ADC 采样使能位
1 = ADC SHA 正在采样
0 = ADC SHA 保持
当 ASAM = 0 时,向该位写入 1 将启动采样。
当 SSRC = 000 时,向该位写入 0 将结束采样并启动转换。
bit 0 DONE :A/D 转换状态位
1 = A/D 转换已完成
0 = A/D 转换尚未完成或尚未启动
清零该位不会影响进行中的任何操作。在自动模式下, DONE 位不是持久性的。在下一次采样开始时,硬件会将它清零。
AD1CON2 :ADC控制寄存器2
bit 15-13 VCFG<2:0> :参考电压配置位
000 AVDD AVSS
001 外部VREF+引脚 AVSS
010 AVDD外部 VREF-引脚
011 外部VREF+引脚 外部VREF-引脚
1xx AVDD AVSS
bit 12 OFFCAL:输入失调校准模式选择位
1 = 使能失调校准模式 SHA 的 V INH 和 V INL 连接到 V R –
0 = 禁止失调校准模式 SHA 的输入由 AD1CHS 或 AD1CSSL 控制
bit 11 保留:写入 0 ;忽略读操作
bit 10 CSCNA :MUX A 输入多路开关设置的 CH0+ SHA 输入的扫描输入选择位
1 = 扫描输入
0 = 不扫描输入
bit 9-8 保留:写入 0 ;忽略读操作
bit 7 BUFS: : 缓冲区填充状态位
仅当 BUFM = 1 时有效 (ADRES 分隔为 2 个 8 字缓冲区)。
1 = ADC 当前正在填充缓冲区 0x8-0xF,用户应访问 0x0-0x7 中的数据
0 = ADC 当前正在填充缓冲区 0x0-0x7,用户应访问 0x8-0xF 中的数据
bit 6 保留:写入0;忽略读操作
bit 5-2 SMPI<3:0> :每次中断的采样 / 转换序列数选择位
1111 = 每完成 16 个采样 / 转换序列时产生中断
1110 = 每完成 15 个采样 / 转换序列时产生中断
…..
0001 = 每完成 2 个采样 / 转换序列时产生中断
0000 = 每完成 1 个采样 / 转换序列时产生中断
bit 1 BUFM: : ADC 结果缓冲区模式选择位
1 = 缓冲区配置为两个 8 字缓冲区 ADC1BUF(7…0) 和 ADC1BUF(15…8)
0 = 缓冲区配置为一个 16 字缓冲区 ADC1BUF(15…0)
bit 0 ALTS: : 交替输入采样模式选择位
1 = 对于第一次采样,使用 MUX A 输入多路开关设置,然后对于所有后续采样,在 MUX B 和 MUX A输入多路开关设置之间交替
0 = 总是使用 MUX A 输入多路开关设置
AD1CON3:ADC控制寄存器3
bit 15 ADRC:ADC 转换时钟源位
1 = ADC 内部 RC 时钟
0 = 时钟由外设总线时钟 (PBclock)产生
bit 14-13 保留:写入 0 ;忽略读操作
bit 12-8 SAMC<4:0> :自动采样时间位
11111 = 31 T AD
·····
00001 = 1 T AD
00000 = 0 T AD (不允许)
bit 7-0 ADCS<7:0> :ADC 转换时钟选择位
11111111 = T PB • 2 • (ADCS<7:0> + 1) = 512 • T PB = T AD
······
00000001 = T PB • 2 • (ADCS<7:0> + 1) = 4 • T PB = T AD
00000000 = T PB • 2 • (ADCS<7:0> + 1) = 2 • T PB = T AD
AD1CHS:ADC输入通道选择寄存器
bit 31 CH0NB: : MUX B 的反相输入选择位
1 = 通道 0 的反相输入为AN1
0 = 通道 0 的反相输入为VR-
bit 27-24 CH0SB<3:0> :MUX B 的同相输入选择位
1111 = 通道 0 的同相输入为 AN15
1110 = 通道 0 的同相输入为 AN14
1101 = 通道 0 的同相输入为 AN13
…
0001 = 通道 0 的同相输入为 AN1
0000 = 通道 0 的同相输入为 AN0
bit 23 CH0NA :MUX A 多路开关设置的反相输入选择位
1 = 通道 0 的反相输入为 AN1
0 = 通道 0 的反相输入为 VR-
bit 19-16 CH0SA<3:0> :MUX A 多路开关设置的同相输入选择位
1111 = 通道 0 的同相输入为 AN15
1110 = 通道 0 的同相输入为 AN14
1101 = 通道 0 的同相输入为 AN13
…
0001 = 通道 0 的同相输入为 AN1
0000 = 通道 0 的同相输入为 AN0
AD1PCFG :ADC端口配置寄存器
bit15-0 PCFG<15:0> :模拟输入引脚配置控制位
1 = 模拟输入引脚处于数字模式,使能端口读输入,该模拟输入的 ADC 输入多路开关输入连接到 AVss
0 = 模拟输入引脚处于模拟模式,数字端口读操作将不考虑引脚电压和 ADC 采样引脚电压而返回 1
AD1CSSL :ADC 输入扫描选择寄存器
bit 15-0 CSSL<15:0> :ADC 输入引脚扫描选择位
1 = 选择对 ANx 进行输入扫描
0 = 输入扫描时跳过 ANx
IFS1:中断标志状态寄存器1
bit 1 AD1IF :模数转换器 1 中断请求标志位
1 = 产生了中断请求
0 = 未产生中断请求
IEC1:中断允许控制寄存器1
bit 1 AD1IE :模数转换器 1 中断允许位
1 = 允许中断
0 = 禁止中断
IPC6 :中断优先级控制寄存器 6
bit 28 – 26 AD1IP<2:0> :模数转换器 1 中断优先级位
111 = 中断优先级为 7
110 = 中断优先级为 6
101 = 中断优先级为 5
100 = 中断优先级为 4
011 = 中断优先级为 3
010 = 中断优先级为 2
001 = 中断优先级为 1
000 = 禁止中断
bit 25-24 AD1IS<1:0> :模数转换器 1 中断子优先级位
11 = 中断子优先级为 3
10 = 中断子优先级为 2
01 = 中断子优先级为 1
00 = 中断子优先级为 0
4、ADC工作原理及配置
模拟采样包含两个步骤:采集和转换。采集时间和 A/D 转换时间之和就是总采样时间。
要配置 ADC 模块,请执行以下步骤
A-1. 在AD1PCFG<15:0>中配置模拟端口引脚。
B-1. 在 AD1CHS<32:0> 中选择 ADC MUX 的模拟输入。
C-1. 使用 FORM<2:0> (AD1CON1<10:8>)选择 ADC 结果格式。
C-2. 使用 SSRC<2:0> (AD1CON1<7:5>)选择采样时钟源。
D-1. 使用 VCFG<2:0>(AD1CON2<15:13>)选择参考电压源。
D-2. 使用 CSCNA (AD1CON2<10>)选择扫描模式。
D-3. 如果要使用中断,则设置每个中断的转换数SMP<3:0>(AD1CON2<5:2>)。
D-4. 使用 BUFM(AD1CON2<1>)设置缓冲区填充模式。
D-5. 在 ALTS AD1CON2<0> 中选择要与 ADC 连接的 MUX。
E-1. 使用 ADRC (AD1CON3<15>)选择 ADC 时钟源。
E-2. 如果要使用自动转换,则使用 SAMC<4:0> (AD1CON3<12:8>)选择采样时间。
E-3. 使用 ADCS<7:0>(AD1CON3<7:0>)选择 ADC 时钟预分频比。
F. 使用 AD1CON1<15> 开启 ADC 模块。
配置 ADC 中断 (如果需要)。
A-1. 清零 AD1IF 位 (IFS1<1>)。
A-2. 如果要使用中断,则选择 ADC 中断优先级 AD1IP<2:0> (IPC<28:26>)和子优先级AD1IS<1:0> (IPC<24:24>)。
通过启动采样来启动转换序列。
结果格式
32位数据模式
16位数据模式
A/D 转换器存在一个最高的转换速率。模拟模块时钟 T AD 用于控制转换时序。A/D 转换需要 12 个时钟周期 (12 T AD )。
ADC 转换时钟周期:TAD = 2 • (TPB (ADCS + 1)
ADC其他功能
中止采样
处于手动采样模式时,清零 SAMP (AD1CON1<1>)位会终止采样,但如果 SSRC(AD1CON1<7:5>) = 000,则也可能会启动转换。
在自动采样模式下,清零 ASAM (AD1CON1<2>)位不会终止正在进行的采集 / 转换序列;但是,在当前采样转换完成之后,将不会自动继续采样。
DONE 位操作 DONE(AD1CON1<0>)位会在转换序列完成时置 1。
在手动模式下,DONE 位是持久性的。它会一直保持置 1,直到软件将它清零为止。可以通过查询 DONE 位来确定转换何时完成。
在所有自动采样模式 (ASAM = 1)下, DONE 位不是持久性的。它会在转换序列结束时置 1,并在下一次采集开始被硬件清零。当 ADC 工作于自动模式时,建议不要查询 DONE 位。在转换序列完成之后, AD1IF (IFS1<1>)标志会被锁存,因而可以进行查询。
休眠和空闲模式下的操作
SLEEP (休眠)和 IDLE (空闲)模式有助于将转换噪声降至最小,因为 CPU、总线和其他外设的数字活动被减到最少。
不使用 RC ADC 时钟时的 CPU 休眠模式:当器件进入 SLEEP (休眠)模式时,模块的所有时钟源被关闭并保持为逻辑 0。
使用 RC ADC 时钟时的 CPU 休眠模式:如果将内部 ADC RC 振荡器设置为 ADC 时钟源(ADRC = 1),ADC 模块就可以在 SLEEP(休眠)模式下工作。
CPU 空闲模式下的 ADC 操作:对于 A/D 转换器,ADC SIDL 位(AD1CON1<13>)用于选择模块在 IDLE(空闲)模式下是停止还是继续工作。
各种复位的影响
MCLR复位、上电复位、看门狗定时器复位:所有ADC控制寄存器(AD1CON1、AD1CON2、AD1CON3、AD1CHS、AD1PCFG 和 AD1CSSL)都会复位为值 0x00000000。这会禁止 ADC,并将模拟输入引脚设置为模拟输入模式。
ADC引脚
AN0 RB0 Pin25
AN1 RB1 Pin24
AN2 RB2 Pin23
AN3 RB3 Pin22
AN4 RB4 Pin21
AN5 RB5 Pin20
AN6 RB6 Pin26
AN7 RB7 Pin27
AN8 RB8 Pin32
AN9 RB9 Pin33
AN10 RB10 Pin34
AN11 RB11 Pin35
AN12 RB12 Pin41
AN13 RB13 Pin42
AN14 RB14 Pin43
AN15 RB15 Pin44
5、使用Harmony工具生成ADC工程代码
1、在Available Components中将ADC组件添加到右侧的Project Graph中;
2、ADC组件初始化配置如下
3、根据需要进行配置
4、生成的代码与原始代码存在差异,需进行确认;
5、代码生成后需要的操作;
1、系统初始化完成后添加ADC处理回调函数;
2、添加应用层ADC操作函数;
6、编译运行将代码烧录到开发板中;点击编译按钮,编译提示BUILD SUCCESSFUL,点击烧录,提示Programming/Verify complete,通过串口观察发送ADC数据。
ADC配置说明
Interrupt Mode:中断模式选择,勾选后会生成中断相关配置代码,未勾选则不生成,通过查询方式读取;
ADC Conversion Clock Source bit:ADC转换器时钟源选择,可选择为Clock derived from Peripheral Bus Clock(PBCLK)或Clock derived from FRC,当选择PBCLK时钟时可设置采样时间
ADC Clock(TAD)(nano sec)为ADC采样时间
Voltage Reference Configuration bits为参考电压选择,可选为如下四种:
Stop in Idle Mode bit为空闲模式配置,可选为继续运行或停止;
Alternate Input Sample Mode Select bit为交替输入采样模式选择,可选为总是使用 MUX A 输入多路开关设置;对于第一次采样,使用 MUX A 输入多路开关设置,然后对于所有后续采样,在 MUX B 和 MUX A输入多路开关设置之间交替
Enable Input Scan on Mux A:为ADC输入引脚扫描选择可选Select AN0 for Input Scan~Select AN15 for Input Scan;
Negative Input Select bit for Sample A Mutiplexer Setting:MUXA多路开关设置的反相输入选择位,可选为Channel 0 negative input is VREFL通道 0 的反相输入为VR- 或者Channel 0 negative input is AN1 通道 0 的反相输入为 AN1
Positive Input Select bits for Sample A Mutiplexer Setting:MUXA多路开关设置的同相输入选择位,可选为Channel 0 positive input is AN0~Channel 0 positive input is AN15 通道0的同相输入为AN0~ AN15
Enable Auto Sample:使能自动采样;勾选后下侧还有Stop Auto Sample After First Conversion Sequence停止转换序列,勾选后在产生第一个ADC中断时停止转换;
Conversion Trigger Source Select bits:转换触发源选择位,可选由清零SAMP位结束采样并启动转换、由INT0引脚的有效跳变结束采样并启动转换、由Timer3周期匹配结束采样并启动转换、由内部计数器结束采样并启动转换、
Data Output Format bits:ADC输出数据格式,
ADC Result Buffer Mode Select bit:ADC 结果缓冲区模式选择位,可选为Buffer configured as one 16-word buffer ADC1BUFF-ADC1BUF0缓冲区配置为一个 16 字缓冲区 ADC1BUF(15…0),Buffer configured as two 8-word buffers ADC1BUF7-ADC1BUF0 / ADC1BUFF-ADC1BUF8缓冲区配置为两个 8 字缓冲区 ADC1BUF(7…0) 和 ADC1BUF(15…8)
Sample/Convert Sequences Per Interrupt Selection bits:每次中断的采样/转换序列数选择位,可选每完成1个采样/转换序列时产生中断~可选每完成16个采样/转换序列时产生中断
6、实际代码分析
plib_adc.h
typedef enum { ADC_MUX_A, ADC_MUX_B } ADC_MUX; typedef enum { ADC_RESULT_BUFFER_0 = 0, ADC_RESULT_BUFFER_1, ADC_RESULT_BUFFER_2, ADC_RESULT_BUFFER_3, ADC_RESULT_BUFFER_4, ADC_RESULT_BUFFER_5, ADC_RESULT_BUFFER_6, ADC_RESULT_BUFFER_7, ADC_RESULT_BUFFER_8, ADC_RESULT_BUFFER_9, ADC_RESULT_BUFFER_10, ADC_RESULT_BUFFER_11, ADC_RESULT_BUFFER_12, ADC_RESULT_BUFFER_13, ADC_RESULT_BUFFER_14, ADC_RESULT_BUFFER_15 }ADC_RESULT_BUFFER; typedef enum { ADC_INPUT_POSITIVE_AN15 = 15, ADC_INPUT_POSITIVE_AN14 = 14, ADC_INPUT_POSITIVE_AN13 = 13, ADC_INPUT_POSITIVE_AN12 = 12, ADC_INPUT_POSITIVE_AN11 = 11, ADC_INPUT_POSITIVE_AN10 = 10, ADC_INPUT_POSITIVE_AN9 = 9, ADC_INPUT_POSITIVE_AN8 = 8, ADC_INPUT_POSITIVE_AN7 = 7, ADC_INPUT_POSITIVE_AN6 = 6, ADC_INPUT_POSITIVE_AN5 = 5, ADC_INPUT_POSITIVE_AN4 = 4, ADC_INPUT_POSITIVE_AN3 = 3, ADC_INPUT_POSITIVE_AN2 = 2, ADC_INPUT_POSITIVE_AN1 = 1, ADC_INPUT_POSITIVE_AN0 = 0, }ADC_INPUT_POSITIVE; typedef enum { ADC_INPUT_NEGATIVE_AN1 = 1, ADC_INPUT_NEGATIVE_VREFL = 0, }ADC_INPUT_NEGATIVE; typedef enum { ADC_INPUT_SCAN_AN0 = 0x1, ADC_INPUT_SCAN_AN1 = 0x2, ADC_INPUT_SCAN_AN2 = 0x4, ADC_INPUT_SCAN_AN3 = 0x8, ADC_INPUT_SCAN_AN4 = 0x10, ADC_INPUT_SCAN_AN5 = 0x20, ADC_INPUT_SCAN_AN6 = 0x40, ADC_INPUT_SCAN_AN7 = 0x80, ADC_INPUT_SCAN_AN8 = 0x100, ADC_INPUT_SCAN_AN9 = 0x200, ADC_INPUT_SCAN_AN10 = 0x400, ADC_INPUT_SCAN_AN11 = 0x800, ADC_INPUT_SCAN_AN12 = 0x1000, ADC_INPUT_SCAN_AN13 = 0x2000, ADC_INPUT_SCAN_AN14 = 0x4000, ADC_INPUT_SCAN_AN15 = 0x8000, }ADC_INPUTS_SCAN;
plib_adc.c
ADC_CALLBACK_OBJECT ADC_CallbackObj; void ADC_Initialize(void) { AD1CON1CLR = _AD1CON1_ON_MASK; //关闭转换器 AD1CON1 = 0x4; //ASAM ADC采样自动启动位 AD1CON3 = 0x1f02; //SAMC自动采样时间 11111 31TAD; ADCS转换时钟选择位 0000010 AD1CHS = 0x10000; //ADC输入通道选择 0001 通道1 AN1 /* Clear interrupt flag */ IFS1CLR = _IFS1_AD1IF_MASK; //模数转换器1中断请求标志位 /* Interrupt Enable */ IEC1SET = _IEC1_AD1IE_MASK; //模数转换器1中断允许位 /* Turn ON ADC */ AD1CON1SET = _AD1CON1_ON_MASK; //ADC工作模式位,启动 } void ADC_Enable(void) { AD1CON1SET = _AD1CON1_ON_MASK; //ADC工作模式位,启动 } void ADC_Disable(void) { AD1CON1CLR = _AD1CON1_ON_MASK; //关闭转换器 } void ADC_SamplingStart(void) { AD1CON1CLR = _AD1CON1_DONE_MASK; //清除转换状态位 AD1CON1SET = _AD1CON1_SAMP_MASK; //采样使能位 } void ADC_ConversionStart(void) { AD1CON1CLR = _AD1CON1_SAMP_MASK; //采样使能位 } void ADC_InputSelect(ADC_MUX muxType, ADC_INPUT_POSITIVE positiveInput, ADC_INPUT_NEGATIVE negativeInput) { if (muxType == ADC_MUX_B) { AD1CHSbits.CH0SB = positiveInput; //MUX B的同相输入选择位 AD1CHSbits.CH0NB = negativeInput; //MUX B的反向输入选择位 } else { AD1CHSbits.CH0SA = positiveInput; //MUX A的同相输入选择位 AD1CHSbits.CH0NA = negativeInput; //MUX A的反向输入选择位 } } void ADC_InputScanSelect(ADC_INPUTS_SCAN scanInputs) { AD1CSSL = scanInputs; //输入引脚扫描选择位 } /*Check if conversion result is available */ bool ADC_ResultIsReady(void) { return AD1CON1bits.DONE; //AD转换状态位 } /* Read the conversion result */ uint32_t ADC_ResultGet(ADC_RESULT_BUFFER bufferNumber) { return (*((&ADC1BUF0) + (bufferNumber << 2))); //缓冲区地址 } void ADC_CallbackRegister(ADC_CALLBACK callback, uintptr_t context) { ADC_CallbackObj.callback_fn = callback; ADC_CallbackObj.context = context; } void ADC_InterruptHandler(void) { IFS1CLR = _IFS1_AD1IF_MASK; if (ADC_CallbackObj.callback_fn != NULL) //清除中断标志位 { ADC_CallbackObj.callback_fn(ADC_CallbackObj.context); } }
interrupts.c
void __ISR(_ADC_VECTOR, ipl1SOFT) ADC_Handler (void) { ADC_InterruptHandler(); }
7、实验验证
编译运行将代码烧录到开发板中;点击编译按钮,编译提示BUILD SUCCESSFUL,点击烧录,提示Programming/Verify complete,通过串口观察发送ADC数据。
————————————————
版权声明:本文为CSDN博主「Huangtop」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Huangtop/article/details/119786791