其实我们说的低功耗,P=I*U,当U即电压一定的时候,当电流很低的时候就是指的功耗很低。在STM8系列的芯片中,STM8L主打低功耗。
一、 首先要了解一下,MCU在运行模式耗电的基本情况。
图1 STM8L051xx MCU运行模式下总电流的消耗
图2 STM8L 片上外设电流消耗
由上两图可以发现:
1.1 代码执行置为影响运行功耗,代码从RAM中执行时的电源电流在其他条件参数一致的情况下比代码从Flash中执行师的电源电流要小,说明RAM存取速度快,执行效率高。
1.2 时钟源及频率影响运行功耗。在相同频率振荡信号情况下,HSE石英晶体振荡器方式比HSE外部信号输入方式消耗的电流更多。若对不同时钟源所消耗的功耗进行排序,相同频率振荡信号情况下是HSE时钟源比HSI时钟源消耗的电流高,HSI时钟源又比LSI时钟源消耗的电流高。
1.3 片上外设影响运行功耗。一般编程人员往往只需要启用单片机的部分片上资源,而非全部启用,即便是全部启用了,也存在分时运行的情况。对比发现,AD转换是“耗电大户”。
默认情况下在系统上电复位后,MCU处于运行模式。在这种情况下,CPU由fCPU提供运行时钟并执行程序代码,系统时钟fMASTER分别为各个处于激活状态的片上外设提供时钟,MCU功耗可以达到最大。但是,如果对于某些系统需求,CPU并不需要一直保持运行模式,这时就可以用STM8单片机电源管理技术提供的几种低功耗运行模式,参考数据手册可知,
Low power features
– 5 low power modes: Wait, Low power run(5.1 µA), Low power wait (3 µA), Active-halt with RTC (1.3 µA),Halt (350 nA)
由上可知,共有五种模式:等待模式,低功耗运行模式,低功耗等待模式,活跃停机模式和停机模式。停机模式时功耗最低的模式,若启用该模式后时钟振荡器和CPU外设均关闭,功耗较低的是活跃停机,其次是等待模式。
二、 Wait等待模式
在运行模式下执行STM8单片机专用中断指令“WFI”后,即可进入等待模式。
void main()
{
.....(此处省略具体程序语言)
asm("WFI");//或者写asm("WFE");
.....(此处省略具体程序语言)
}
若单片机运行状态已切换至等待模式,此时CPU停止运行,但片上外设和中断控制器仍保持运行,因此功耗会有所降低。
在等待模式下,所有寄存器与RAM中的内容保持不变,之前所定义的时钟配置也保持不变。当一个内部中断,RTC或外部中断(包含通信外设中断)请求产生时,CPU从等待模式唤醒并恢复工作。
启用等待模式究竟对功耗参数有多大的影响呢?可参考数据手册:
三、Halt 停机模式
停机模式是STM8单片机低功耗模式中电流消耗最小的一种,在该模式下主时钟会被关闭,即由fMASTER提供时钟的CPU及所有片上外设均被关闭。因此,所有外设均没有时钟供给,MCU的数字部分不消耗能量。可以通过STM8单片机专用中断指令“Halt”进入停机模式,可写成:
void main()
{
.....(此处省略具体程序语言)
asm("HALT");
.....(此处省略具体程序语言)
}
在停机模式下,所有寄存器与RAM中的内容保持不变,默认情况下时钟配置也保持不变。外部中断可将MCU从停机模式唤醒。“外部中断”是指配置为中断输入的GPIO端口或具有触发外设中断能力的相关端口。
在这种模式下,为了节省功耗注电压调节器被关闭,仅低电压调节器和掉电复位处于工作状态。如果用户需要快速唤醒单片机电源,迅速回归到运行模式,可以对时钟源进行配置。由于片内高速RC振荡时钟源HSI的启动速度比HSE快,因此为了减少MCU的唤醒时间,可以在进入停机模式前选择HSI作为fMASTER的时钟源。如果觉得操作比较麻烦,也可以在进入停机模式前设置内部时钟寄存器(CLK_ICKR)中的“FHWU”位选择HSI时钟作为fMASTER的时钟源,这样一来就不需要再进行时钟源切换了。
需要特别注意的是在默认情况下,MCU进入停机模式后,Flash的唤醒时间较长(几个微妙)。如果用户需要从停机模式快速唤醒,可将Flash控制寄存器1(FLASH_CR1)中的“HALT”位置1,当MCU进入停机模式时,就可以确保Flash处于等待状态,唤醒时间可以降至几个纳秒,但功耗将增至几微安。
由上可知,停机模式的功耗是最低的。停机模式只能用外部中断才能唤醒。
四、Active-Halt活跃停机模式
活跃停机模式与停机模式类似,但是停机模式只能通过外部中断唤醒,假如我们需要单片机每一分钟唤醒一次完成相关的功能后进入停机模式,那么停机模式就不可行了。但是活跃停机可以做到。主要是使用自动唤醒“RTC”功能,RTC单元会在一定的延时后产生一个内部唤醒事件,延迟事件是用户可编程的。此处的RTC单元本身是一个计数器,其功能可以理解为单片机“睡觉”状态下的“定时唤醒闹钟”。
在活跃停机模式下,主振荡器,CPU和几乎所有的外设都被停止。如果RTC单元被使能,则只有LSI和HSE时钟源仍处于运行状态,用于驱动RTC功能。
如果用户需要配置单片机运行状态为活跃停机模式,需首先使能RTC功能,然后执行STM8单片机专用中断指令“HALT”即可。当MCU进入活跃停机模式时,主电压调节器可自动关闭。在唤醒时主电压调节器重新被打开,这需要一个比较长的唤醒时间。
在活跃停机模式下,快速唤醒是很重要的。若需要实现快速唤醒,可以在进入活跃停机模式前选择HSI时钟作为fMASTER的时钟源。如果觉得操作比较麻烦,也可以在进入活跃停机模式前设置内部时钟寄存器(CLK_ICKR)中的”FHWU”位选择HSI时钟作为fMASTER的时钟源,这样一来就不需要在进行时钟源切换了。这样操作可以提高CPU的执行效率,是MCU处于运行状态与低功耗模式之间的时间最短,从而减少整体平均功耗。
五、 项目解析
基于上面的解析之后,可知STM8L051xxx芯片功耗的大小为:Wait, Low power run(5.1 µA)等待模式> Low power wait (3 µA)低功耗等待模式> Active-halt with RTC (1.3µA)活跃停机模式>Halt (350 nA)。具体项目不能功耗最低就选哪个模式。这得根据具体的项目来选择。详情请见下表:
以下用几个特别简单的案例来表示在不同的项目中应选择不同的低功耗模式,在下面的案例中均使用5V电池供电,使用芯片均为STM8L051xx:
5.1 项目只需要实现功能如当按键按下时,唤醒单片机执行相应的功能程序后再次进入低功耗模式。
分析:可以设置按键为输入上拉外部中断,下降沿触发,我们可以在平时都让单片机处于停机模式,当按键按下时,外部中断唤醒单片机。此时可用停机模式。
5.2 若项目需要实现5.1项目的功能,还需要自身每分钟亮灯一次,每小时采样一次AD。
分析:如果使用停机模式,其中按键是使用外部中断可从停机模式中唤醒。但是每分钟亮一次则无法通过外部中断唤醒,需要单片机定时唤醒,由上图可知,在停机模式下,除了时钟源关闭外,外设均是关闭的,所以无法使用定时器计时。但是活跃停机模式下,RTC却是可以启用的,且RTC中断可以唤醒单片机,因此可以设置RTC每次进入中断的时间。使能LSI 38k为RTC提供时钟源。因此可以选择活跃停机模式。
5.3 若项目除了实现5.2的功能,还需实现当接收到串口数据时,能够唤醒单片机执行相应的功能。
分析:此项目除了外部中断,内部中断,还有外设(USART)可以唤醒时钟,这个一般有三种解决方法:
5.3.1 因为外设中断需要唤醒单片机,在停机模式和活跃停机模式下,外设是关闭的,不能从停机模式中唤醒单片机,但是在等待模式下,外设可以唤醒单片机,因而可以直接使用等待模式。
5.3.2 在IO口还富裕的情况下,单独使用一个IO口作为外部输入中断,当需要接收数据时,先触发外部中断,使单片机进入运行模式,再使用串口收发数据。因此可选用活跃停机模式。
5.3.3 也可以不增加IO口,在进入停机模式之前先把串口接收IO口先设为外部中断输入端。当需要接收数据时,通过外部中断唤醒后,在把串口配置为串口接收中断,即可以接收数据。接收完后又恢复为外部中断输入端。使用活跃停机模式即可。
六、 活跃停机模式基本设置
如果一个项目简化情况如下:
芯片:STM8L051F3
电源:3.3v电池供电
实现功能:1.每分钟醒来亮灯一次。2.当按键按下时,灯长亮。
分析:查看STM8L051xx的datasheet文档可知,MCU正常工作电压在1.8v~3.3v, 为了使电池寿命更长,就需要使用低功耗来实现此项目. 如果选用停机模式,按键可用外部中断可从停机模式中唤醒。但是每分钟亮一次则无法通过外部中断唤醒,需使用RTC中断因而饿哦们选择活跃停机模式完成此项目。
在一般的预想中,使用活跃停机模式,以为只要在运行状态下使用指令asm(“HALT”);即可,这样确实是可以的。但是功耗还是会很大,多大100uA,与要求相距甚远。这是什么原因?
这是因为进入停机(halt)/活跃停机(halt)/等待(WFI/WFE)模式后,只是MCU中最耗电的内核部分被关了,但是所有寄存器与RAM中的内容保持不变,之前所定义的时钟配置(主时钟状态寄存器CLK_CMSR中的配置状态)也保持不变。因而这些都在消耗电量。所以我们在进入停机模式前,必须先进行以下的一系列配置。
6.1 关闭外设时钟
外设开启时需要先开时钟再设置寄存器,关闭时需先清除寄存器再关闭时钟。因为操纵一个外设的寄存器肯定是需要时钟的,如果先把时钟给关了,则无法操作寄存器,如果只是关了时钟,不清寄存器的相应位,那么外设还处于“静态耗电”的状态。所以需要在开始时关闭外设的时钟,比如定时器,串口,ADC等外设时钟。
void Haltmode_PeriphClkClose(void)//Close the periphic clock
{
//Disable the ADC function,close the ADC system clock
ADC1->CR1=ADC_CR1_RESET_VALUE;
ADC1->CR2=ADC_CR2_RESET_VALUE;
ADC1->CR3=ADC_CR3_RESET_VALUE;
ADC1->SR=~ADC_SR_RESET_VALUE;
ADC1->DRH=ADC_DRH_RESET_VALUE;
ADC1->DRL=ADC_DRL_RESET_VALUE;
ADC1->SQR[0]=ADC_SQR1_RESET_VALUE;
ADC1->SQR[1]=ADC_SQR2_RESET_VALUE;
CLK->PCKENR2&=~0x01;//disable the ADC function
}
6.2 关闭相应的GPIO口
没有用到的引脚:推挽输出高电平/低电平
不用的外部晶振引脚:上拉输入
外部有上拉电阻:推挽输出高电平/低电平
和外部芯片相连的引脚:推挽输出低电平
6.3 RTC初始化配置
步骤:
6.3.1 CLK打开RTC时钟
6.3.2 选择LSI 38KHZ时钟为RTC的时钟源
6.3.3 对RTC时钟进行分频 Clock=38k/1=38khz
6.3.4 开启电源快速唤醒功能
6.3.5 使能电源低功耗模式
6.3.6 选择唤醒时钟的分频 Clock=38k/16=2375hz
6.3.7 使能自动唤醒中断功能
6.3.8.设置唤醒的时间 Counter*(fLSI/Prec1/Prec2)=2375*(1/(38000/1/16))=1s
6.3.9 设置RTC软件的优先级为3,在其中寄存器操作为ITC->ISPR2|=0x03;这个的操作过程为 因为软件的优先级,主要是由ITC_SPRx的I1和I0位决定,
I1 I0
1 0 Level 0(main)
0 1 Level 1
0 0 Level 2
1 1 Level 3(=software priority disabled)
那么我们在哪设置它的优先级呢?根据向量的地址,得到I1_x/I0_x,看datasheet页的“Interrupt vector mapping", 查看它的IRQ No.如RTC的IRQ No.为4,则为I1_4/I0_4,在ITC->SPRx寄存器中找到VECT4SPR[1:0]在ITC_SPR2的低三位。
如果设置优先级为3的话,则为ITC->ISPR2|=0x03;如果需要设置为优先级1的话,则为ITC->ISPR2|=0x01;设置为优先级为0的话,则为ITC->ISPR2|=0x02;
6.3.10 开启总中断
void Haltmode_RTCsystemClock(void)//RTC configue
{
//SYSCLK to RTC peripheral enabled
CLK->PCKENR2|=0x04;
//LSI clock used as RTC clock source
CLK->CRTCR|=0x04;
//Clock RTC prescaler is RTC clock source/1=38khz
CLK->CRTCR&=~CLK_CRTCR_RTCDIV;//Clock RTC prescaler is RTC clock source/1=38khz
//Power mange fast wake up
PWR->CSR2|=PWR_CSR2_FWU;
// Enable the PWR ultra lower power
PWR->CSR2|=PWR_CSR2_ULP;
/*******Write the protected register************/
//unlock the protected register
RTC->WPR=0xCA;
RTC->WPR=0x53;
//RTC_CR1_WUCKSEL can be write only when WUTE bit is set to 0 in RTC_CR2 and WUTWF is set to 1 in RTC_ISR1.
RTC->CR2 &=~RTC_CR2_WUTE;
RTC->ISR1|=RTC_ISR1_WUTWF;
//wakeup RTCCKL/16 clock is selected, RTC clock/16=2375hz
RTC->CR1&=~RTC_CR1_WUCKSEL;
//Set wakeup counter is 2375,wakeup time equal to 1S
RTC->WUTRH=0x09;//Set wakeup Counter is 2375=0x0947, Wakeup time equal to 2375*(1/(38000/16))=1S ,This register can be written only when WUTE bit is set to 0 in RTC_CR2, and WUTWF to 1in RTC_ISR1.
RTC->WUTRL=0x47;//This register can be written only when WUTE bit is set to 0 in RTC_CR2, and WUTWF to 1 in RTC_ISR1.
//WakeUp function enable
RTC->CR2|=(uint8_t)RTC_CR2_WUTE;
//Wakeup timer interrupt enable
RTC->CR2|=RTC_CR2_WUTIE;
//Lock the protected register
RTC->WPR=0xFF;
/**************************************/
//RTC software priority as level 3
ITC->ISPR2|=0x03;
//open the all interrupt
asm("rim");
}
6.4 RTC中断服务函数
对应RTC中断的中断向量地址如何寻找,主要是要查看datasheet页的“Interrupt vector mapping",查看它的IRQ No.则它的中断向量地址为(IRQ No。+2),如RTC的IRQ No.为4,则向量地址为4+2=6,在此要特别注意GPIO口的外部中断,STM8L是PB,PD全部的IO口有专门的中断向量地址EXTIB,EXTID,但是其他的IO口没有专门的中断向量地址,则根据不同的引脚,有EXTI0-EXTI7,假如PC1,它的中断向量地址则为EXTI1.因为这些没有区分具体的IO口,在确定具体为那个引脚时,需要使用查询法。即查询相应的引脚状态即可。
#pragma vector=6
__interrupt void RTC_IRQHandler(void)
{
static uint32_t RTC_Counter1Second=0;
static uint32_t RTC_Counter1Day=0;
RTC_Counter1Second++;
if(RTC_Counter1Second>=TIME_ONEMINUTE)//用来计时,作为每分钟亮灯的处理
{
flag_LED=1;
RTC_Counter1Second=0;
}
//Clr RTC_IT pending clear bit
RTC->ISR2 &=~RTC_ISR2_WUTF;
}
6.5 活跃停机模式开启
开启活跃停机模式只需要使用指令asm(“HALT”);即可.
void Haltmode_Cmd(void)
{
asm("HALT");
}
至此,活跃停机的设置是基本完成了,虽然以为这样设置好了就OK了,但是实际上配置上要全面的理解它却不是这样就可以了。我们要整体的把握它的整体构架才能灵活的使用。
由上图可知执行HALT指令后的基本情况。下面我们针对上面的案例来表示一下程序的流程:
由上流程图即可达到该目的,根据上面的流程图得到下面的程序:
ActiveHalt mode.c
#include "ActiveHalt mode.h"
/*****************************************************************************************
name: Wait mode.c
introduce: 低功耗驱动
*****************************************************************************************/
/******************************************************************************************
name:void Haltmode_PeriphClkClose(void)
use:关闭外设时钟
********************************************************************************************/
void Haltmode_PeriphClkClose(void)//Close the periphic clock
{
/*根据具体的项目编写*/
}
/******************************************************************************************
name:void Haltmode_GPIODefault(void)
use:GPIO初始化,关闭GPIO口
********************************************************************************************/
void Haltmode_GPIODefault(void)//GPIO configue
{
/*根据具体的项目编写*/
}
/******************************************************************************************
name:void Haltmode_RTCsystemClock(void)
use:RTC初始化配置
********************************************************************************************/
void Haltmode_RTCsystemClock(void)//RTC configue
{
//SYSCLK to RTC peripheral enabled
CLK->PCKENR2|=0x04;
//LSI clock used as RTC clock source
CLK->CRTCR|=0x04;
//Clock RTC prescaler is RTC clock source/1=38khz
CLK->CRTCR&=~CLK_CRTCR_RTCDIV;//Clock RTC prescaler is RTC clock source/1=38khz
//Power mange fase wake up
PWR->CSR2|=PWR_CSR2_FWU;
// Enable the PWR ultra lower power
PWR->CSR2|=PWR_CSR2_ULP;
/***************Write the protected register*************/
//unlock the protected register
RTC->WPR=0xCA;
RTC->WPR=0x53;
//RTC_CR1_WUCKSEL can be write only when WUTE bit is set to 0 in RTC_CR2 and WUTWF is set to 1 in RTC_ISR1.
RTC->CR2 &=~RTC_CR2_WUTE;
RTC->ISR1|=RTC_ISR1_WUTWF;
//wakeup RTCCKL/16 clock is selected, RTC clock/16=2375hz
RTC->CR1&=~RTC_CR1_WUCKSEL;
//Set wakeup counter is 2375,wakeup time equal to 1S
RTC->WUTRH=0x09;//Set wakeup Counter is 2375=0x0947, Wakeup time equal to 2375*(1/(38000/16))=1S ,This register can be written only when WUTE bit is set to 0 in RTC_CR2, and WUTWF to 1in RTC_ISR1.
RTC->WUTRL=0x47;//This register can be written only when WUTE bit is set to 0 in RTC_CR2, and WUTWF to 1 in RTC_ISR1.
//WakeUp function enable
RTC->CR2|=(uint8_t)RTC_CR2_WUTE;
//Wakeup timer interrupt enable
RTC->CR2|=RTC_CR2_WUTIE;
//Lock the protected register
RTC->WPR=0xFF;
/*************************************************/
//RTC software priority as level 3
ITC->ISPR2|=0x03;
//open the all interrupt
asm("rim");
}
/******************************************************************************************
name:__interrupt void RTC_IRQHandler(void)
use:RTC中断服务函数
*********************************************************************************************/
#pragma vector=6
__interrupt void RTC_IRQHandler(void)
{
static uint32_t RTC_Counter1Second=0;
RTC_Counter1Second++;
if(RTC_Counter1Second>=TIME_ONEMINUTE)//用来计时,作为每分钟亮灯的处理
{
flag_LED=1;
RTC_Counter1Second=0;
}
//Clr RTC_IT pending clear bit
RTC->ISR2 &=~RTC_ISR2_WUTF;
}
/******************************************************************************************
name:void Haltmode_Cmd(void)
use:进入活跃停机模式
********************************************************************************************/
void Haltmode_Cmd(void)
{
asm("HALT");
}
ActiveHalt mode.h
#include "stm8l15x.h"
#define TIME_ONEMINUTE (60)
extern uint8_t flag_LED;
void Haltmode_PeriphClkClose(void); //Close the periphic clock
void Haltmode_GPIODefault(void); //GPIO configue
void Haltmode_RTCsystemClock(void); //RTC configue
void Haltmode_Cmd(void); //Halt mode enable
Key.c
#include "Key.h"
/******************************************************************************************
name:void KEY_GPIOInit(void)
use:KEY IO口初始化为输入上拉有中断
********************************************************************************************/
void KEY_GPIOInit(void) //PC1 KEY GPIO_Mode_In_PU_No_IT
{
/*配置相应的按键为*/
}
/******************************************************************************************
name:void delay(uint16_t Count)
use:编写delay函数,一个循环表示5ms
********************************************************************************************/
void delay(uint16_t Count)
{
uint8_t Count1,Count2;
while(Count--)
{
for(Count1=0;Count1<50;Count1++)
for(Count2=0;Count2<20;Count2++);
}
}
/******************************************************************************************
name:#pragma vector=0x0B
__interrupt void EXTI1_GPIOC_IRQHandler(void)
use:EXTI 外部中断服务函数
********************************************************************************************/
#pragma vector=0x0B
__interrupt void EXTI1_GPIOC_IRQHandler(void)
{
flag_ALARM=1;
EXTI->SR1|=0x02;//根据对应的程序清除响应的位
}
Key.h
#include "stm8l15x.h"
extern uint8_t flag_ALARM;
void KEY_GPIOInit(void);
void delay(uint16_t Count);
LED.c
#include "LED.h"
/******************************************************************************************
name:void LED_GPIOInit(void)
use:LED IO口初始化为推挽输出低速
********************************************************************************************/
void LED_GPIOInit(void)//PB1 LED GPIO_Mode_Out_PP_Low_Slow
{
/*配置相应的LED IO口为推挽输出低速*/
}
/******************************************************************************************
name:void LED_GPIODeInit(void)
use:LED IO口关闭初始化推挽输出低电平
********************************************************************************************/
void LED_GPIODeInit(void)//PB1 LED GPIO_Mode_Out_PP_Low_Slow
{
/*配置相应的LED IO口为推挽输出低电平*/
}
/******************************************************************************************
name:void LEDON(void)
use:LED 灯亮
********************************************************************************************/
void LEDON(void)
{
/*配置相应的LED IO ODR位置1*/
}
/******************************************************************************************
name:void LEDOFF(void)
use :LED 灯灭
********************************************************************************************/
void LEDOFF(void)
{
/*配置相应的LED IO ODR位置0*/
}
/******************************************************************************************
name:void LED_NormallyON(void)
use:当RTC每一分钟唤醒灯闪亮一次,之后继续进行停机模式
********************************************************************************************/
void LED_Normally1MinuteON(void)
{
LED_GPIOInit();
LEDON();
delay(20);
LEDOFF();
LED_GPIODeInit();
}
LED.h
#include "stm8l15x.h"
#include "Key.h"
extern uint8_t flag_ALARM;
/*********************内部函数声明************************/
void LED_GPIOInit(void);
void LED_GPIODeInit(void);
void LEDON(void);
void LEDOFF(void);
void LED_Normally1MinuteON(void);
main.c
#include "main.h"
uint8_t flag_LED=0;
uint8_t flag_ALARM=0;
/******************************************************************************************
name:void HSISystemcolck_init(void)
use:配置HSI为2MHZ
********************************************************************************************/
void HSISystemcolck_init(void)//HSI 16MHZ/8=2MHZ as system clock source
{
CLK->SWR = (uint8_t)0x01;
CLK->CKDIVR = (uint8_t)0x03;
while (CLK->SCSR!=0x01){}
}
/******************************************************************************************
name:void SYSCONFIG_INIT(void)
use:初始化外设
********************************************************************************************/
void SYSCONFIG_INIT(void)
{
HSISystemcolck_init();//配置主时钟源及停机模式唤醒后的时钟源
Haltmode_PeriphClkClose();//关闭外设时钟及寄存器中的值
Haltmode_GPIODefault();//初始化全部的GPIO
KEY_GPIOInit();//开启按键的输入上拉外部中断
Haltmode_RTCsystemClock();//配置RTC及开启总中断
}
/******************************************************************************************
name:void main(void)
use:主函数
********************************************************************************************/
void main(void)
{
asm("sim");
SYSCONFIG_INIT();//初始化外设和相应的中断
Haltmode_Cmd();
while(1)
{
if(flag_ALARM)
{
LED_GPIOInit();
LEDON();
flag_ALARM=0;
}
if(flag_LED)
{
LED_Normally1MinuteON();
flag_LED=0;
}
if(!flag_ALARM && !flag_LED)
{
Haltmode_Cmd();
}
}
}
main.h
#include "stm8l15x.h"
#include "Key.h"
#include "ActiveHalt mode.h"
#include "LED.h"
注:本文有参考龙顺宇老师的《深入浅出STM8单片机入门、进阶与应用实例》的理论部分和网上的一些资料,特此感谢!