This thread has been locked.

If you have a related question, please click the "Ask a related question" button in the top right corner. The newly created question will be automatically linked to this question.

SPWM波频率改变实现:按键、滚轮?

通过Timer0定时中断,以10k频率响应定时中断。但是想改变10k的响应频率。决定用按键和滚轮。

1.按键。

从初始化Timer0程序里找到“// 8K Hz响应  

                     ROM_TimerLoadSet (TIMER0_BASE, TIMER_A,    

                                    ROM_SysCtlClockGet() / SAMPLE_FREQUENCY);”;

用CCS手电筒搜索到“ SAMPLE_FREQUENCY”赋值所在位置:

                     #define TIVA_MAIN_FREQUENCY       80000000        //80MHz tiva主频

                     #define SAMPLE_FREQUENCY          10000         //10KHz SPWM波的频率

决定改变: 

                     int M;                                                             //按键控制M在1到16之间循环

                     #define SAMPLE_FREQUENCY          M * 5000         //5MKHz SPWM波的频率

  • if (Key1 == 0)
    {
           delayNms(10);
           if (key1 == 0)
           {
                  while(key1 == 1);
                  M = (M++) & 0x0f;
           }
    }
    
    if (Key2 == 0)
    {
           delayNms(10);
           if (key2 == 0)
           {
                  while(key2 == 1);
                  M = (M--) & 0x00;
           }
    }

    只会C,而且。。。。。按键该怎样去定义也不是很懂。。。想请教下有没有人会的啊?

    还有如果我用滚轮去实现,现实吗?

    滚轮的程序我找了下:

    /*
     * main.c
     */
    
    
    /*
     * main.c
     */
    
    #include <stdint.h>
    #include <stdbool.h>
    #include "driverlib/adc.h"
    #include "driverlib/gpio.h"
    #include "driverlib/interrupt.h"
    #include "driverlib/sysctl.h"
    #include "inc/hw_memmap.h"
    #include "inc/hw_ints.h"
    #include "LCDDriver.h"
    #include "inc/hw_gpio.h"
    enum
    {
    	LINE_ONE,
    	LINE_TWO,
    	LINE_THREE,
    	LINE_FOUR,
    
    	LINE_NUM
    }line_define;
    
    //#define CHAR_WIDTH			8
    #define WORD_WIDTH			16
    
    #ifndef TARGET_IS_BLIZZARD_RA1
    #define TARGET_IS_BLIZZARD_RA1
    #endif
    
    /***************************************************************
     * @brief	LCD模块按键响应中断 PC7 对应的为按键S1
     * @param	null
     * @return  null
    ***************************************************************/
    extern uint8_t VCA_BUTTON_UP_DOWM;
    char cur_Col = 0;
    //中断处理子函数
    void Int_GPIO_D_Handler(void)
    {
    	unsigned long ulStatus;
    	// 读取中断状态
    	ulStatus = GPIOIntStatus(GPIO_PORTD_BASE, true);
    	// 清除中断状态
    	GPIOIntClear(GPIO_PORTD_BASE, ulStatus);
    	// 如果KEY的中断状态有效
    	if (ulStatus & GPIO_PIN_7)
    	{
    		// 延时约10ms,消除按键抖动
    		SysCtlDelay(10 * (SysCtlClockGet() / 3000));
    		// 等待KEY抬起
    		while (GPIOPinRead(GPIO_PORTD_BASE, GPIO_PIN_7) == 0x00);
    		// 延时约10ms,消除松键抖动
    		SysCtlDelay(10 * (SysCtlClockGet()  / 3000));
    		//TODO 逻辑处理
    		if(cur_Col < (NUM_CHAR - 1))
    			cur_Col++;
    	}
    
    	if (ulStatus & GPIO_PIN_6)// 如果KEY的中断状态有效
    	{
    		// 延时约10ms,消除按键抖动
    		SysCtlDelay(10 * (SysCtlClockGet() / 3000));
    		// 等待KEY抬起
    		while (GPIOPinRead(GPIO_PORTD_BASE, GPIO_PIN_6) == 0x00);
    		// 延时约10ms,消除松键抖动
    		SysCtlDelay(10 * (SysCtlClockGet()  / 3000));
    		//TODO 逻辑处理
    		if(cur_Col > 0)
    			cur_Col--;
    	}
    
    }
    /***************************************************************
     * @brief  	初始化ADC获取滚轮电压值
     * 		————————|
    //		TIVA	    |
    //		M4	PE0|<--ADC		模数转换信号源
    //		________|
    ***************************************************************/
    #define ADC_BASE		ADC0_BASE			// 使用ADC0
    #define SequenceNum 	3				// 使用序列3
    void Init_ADCWheel(){
    	// 使能ADC0外设
    	SysCtlPeripheralEnable(SYSCTL_PERIPH_ADC0);
    	// 使能外设端口E
    	SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOE);
    	// 选择PE0为外部模拟数字转换功能
    	GPIOPinTypeADC(GPIO_PORTE_BASE, GPIO_PIN_0);
    	// 使能采样序列号为触发处理获取模式
    	ADCSequenceConfigure(ADC_BASE, SequenceNum, ADC_TRIGGER_PROCESSOR, 0);
    	// ADC配置
    
    	ADCSequenceStepConfigure(ADC_BASE, SequenceNum, 0, ADC_CTL_CH3 | ADC_CTL_IE |ADC_CTL_END);
    	// 使能ADC配置
    	ADCSequenceEnable(ADC_BASE, SequenceNum);
    	// 清除ADC中断标志位
    	ADCIntClear(ADC_BASE, SequenceNum);
    }
    
    
    /***************************************************************
     * @brief   获取特定ADC的模数转化采样值。
     * @param   ui32Base            ADC采样基地址
     * @param   ui32Peripheral   ADC启动的外设端口
     * @return                        ADC采样值
    ***************************************************************/
    unsigned long ADC_ValueGet(uint32_t ui32Base, uint32_t ui32SequenceNum)
    {
        unsigned long value = 0;
    // 保存ADC采样值
        uint32_t ADCValue[1];
        // 触发获取端口采样
        ADCProcessorTrigger(ui32Base, ui32SequenceNum);
        //等待采样结束
        while(!ADCIntStatus(ui32Base, ui32SequenceNum, false))
        {
        }
        // 清除ADC采样中断标志
        ADCIntClear(ui32Base, ui32SequenceNum);
        // 读取ADC采样值
        ADCSequenceDataGet(ui32Base, ui32SequenceNum, ADCValue);
        value = ADCValue[0];
        return value;
    }
    
    /************************************************************
     * @brief  	对端口C、D进行按键中断初始化
     * @param	none
     * @param	none
     ***********************************************************/
    void Init_Int_Key() {
    
    	HWREG(GPIO_PORTD_BASE+GPIO_O_LOCK)  |= GPIO_LOCK_KEY;
    
    	HWREG(GPIO_PORTD_BASE+GPIO_O_CR)   |= (1<<7);
    
    	HWREG(GPIO_PORTD_BASE+GPIO_O_DEN)  &=(~(1<<7));
    
    	 HWREG(GPIO_PORTD_BASE+GPIO_O_PDR)  &= (~(1<<7));
    
    	HWREG(GPIO_PORTD_BASE+GPIO_O_PUR)  &= (~(1<<7));
    
    	HWREG(GPIO_PORTD_BASE+GPIO_O_AFSEL) &=(~(1<<7));
    
    	// Make PD6/7 an output.
    	SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOD);
    
    	GPIODirModeSet(GPIO_PORTD_BASE, GPIO_PIN_6 | GPIO_PIN_7, GPIO_DIR_MODE_IN);
    
    	GPIOPadConfigSet(GPIO_PORTD_BASE, GPIO_PIN_6 | GPIO_PIN_7, GPIO_STRENGTH_2MA,
    			GPIO_PIN_TYPE_STD_WPU);
    
    	GPIOIntTypeSet(GPIO_PORTD_BASE, GPIO_PIN_6 | GPIO_PIN_7, GPIO_LOW_LEVEL);
    
    	GPIOIntEnable(GPIO_PORTD_BASE, GPIO_PIN_6 | GPIO_PIN_7);
    
    	IntEnable(INT_GPIOD_TM4C123);
    
    	IntMasterEnable();
    
    	GPIOIntClear(GPIO_PORTD_BASE, GPIO_PIN_6 | GPIO_PIN_7);
    }
    
    unsigned char director = 0;				// 当前的移动方向
    
    int main(void) {
    	
    	uint32_t pui32ADC0Value[1];			// 保存ADC采样值
    
    	unsigned char old_Raw, old_Col;		// 保留上一次的位置
    
    	unsigned char cur_Raw; 	// 记录光标当前的横坐标,纵坐标
    
    	cur_Raw = 0;
    	cur_Col = 0;
    
    	// 设置系统主频
    	SysCtlClockSet(SYSCTL_SYSDIV_1 | SYSCTL_USE_PLL | SYSCTL_OSC_MAIN
    							| SYSCTL_XTAL_16MHZ);
    
    	Init_ADCWheel();
    
    
    
    	LCD_Enable();						// 使能LCD
    	LCD_Init();							// 初始化LCD
    	LCD_ScreenClr();					// 清屏
    
    	Init_Int_Key();
    
    	while(1)
    	{
    		ADCProcessorTrigger(ADC_BASE, SequenceNum);
    
    		// Wait for conversion to be completed.
    		while(!ADCIntStatus(ADC_BASE, SequenceNum, false))
    		{
    		}
    
    		// Clear the ADC interrupt flag.
    		ADCIntClear(ADC_BASE, SequenceNum);
    
    		// Read ADC Value.
    		ADCSequenceDataGet(ADC_BASE, SequenceNum, pui32ADC0Value);
    
    		unsigned long sample = pui32ADC0Value[0];
    
    		cur_Raw = (LCD_MAX_COL * sample) / 4096;
    
    		// 如果光标发生变化则刷新光标位置
    		if(old_Col != cur_Col || old_Raw != cur_Raw)
    		{
    			LCD_Draw_Char_8x8(' ', old_Col, old_Raw, LCD_DRAW_NORMAL);
    			LCD_Draw_Char_8x8(' ', cur_Col, cur_Raw, LCD_DRAW_HIGHLIGHT);
    		}
    
    		// 保存光标及采样值用于下次比较
    		old_Raw = cur_Raw;
    		old_Col = cur_Col;
    
    		// 系统延时
    		SysCtlDelay(SysCtlClockGet() / 12);
    	}
    }
    
    

    但是不知道该怎么应用进去!!!!是否应该折算出滚轮所在电压值,然后对应M????

  • 看了艾研例程里产生spwm的程序,打算更改Timer0的响应频率,找到
                “             // 8K Hz响应
                              ROM_TimerLoadSet (TIMER0_BASE, TIMER_A,
                                        ROM_SysCtlClockGet() / SAMPLE_FREQUENCY);”
    试着更改SAMPLE_FREQUENCY(原定义为10000),将它与按键S1,S2结合,结果失败了.
    /***************************************************************
     * @brief	LCD模块按键响应中断 PC7 对应的为按键S1
     * @param	null
     * @return  null
    ***************************************************************/
    extern uint8_t VCA_BUTTON_UP_DOWM;
    #define MAX_a      					16
    
    //中断处理子函数
    void Int_GPIO_D_Handler(void)
    {
    	unsigned long ulStatus;
    	// 读取中断状态
    	ulStatus = GPIOIntStatus(GPIO_PORTD_BASE, true);
    	// 清除中断状态
    	GPIOIntClear(GPIO_PORTD_BASE, ulStatus);
    	// 如果KEY的中断状态有效
    	if (ulStatus & GPIO_PIN_7)
    	{
    		// 延时约10ms,消除按键抖动
    		SysCtlDelay(10 * (SysCtlClockGet() / 3000));
    		// 等待KEY抬起
    		while (GPIOPinRead(GPIO_PORTD_BASE, GPIO_PIN_7) == 0x00);
    		// 延时约10ms,消除松键抖动
    		SysCtlDelay(10 * (SysCtlClockGet()  / 3000));
    		//TODO 逻辑处理
    		if(num2 < (MAX_a - 1))
    			num2++;
    	}
    
    	if (ulStatus & GPIO_PIN_6)// 如果KEY的中断状态有效
    	{
    		// 延时约10ms,消除按键抖动
    		SysCtlDelay(10 * (SysCtlClockGet() / 3000));
    		// 等待KEY抬起
    		while (GPIOPinRead(GPIO_PORTD_BASE, GPIO_PIN_6) == 0x00);
    		// 延时约10ms,消除松键抖动
    		SysCtlDelay(10 * (SysCtlClockGet()  / 3000));
    		//TODO 逻辑处理
    		if(num2 > 0)
    			num2--;
    	}
    
    }
    //float Change_Num(int num1, int num2){
    
    //	int num1 = 1;
    //	int num2 = 2;
    //	num1 = num2;
    
    //	return num1;
    
    //}
    

     之前的:

  • 学会了怎么弄按键和滚轮、、、但是改变不了SAMPLE_FREQUENCY........输出的波形频率不变。。。。

     

    话说其实没有很明白Timer0的时钟频率怎么设定的,80M/10000=8k,可是实际测出的为10k左右。。。。。是芯片内部有什么地方导致设定的1000减少了么?还是函数问题?

     

  • TM0的时钟频率不就是主时钟除以TM0的分频值么,你看看库函数,应该有一个函数专门设置这个值的

  • 是的,找到了程序中设定的

    #define TIVA_MAIN_FREQUENCY       80000000        //80MHz tiva主频

    #define SAMPLE_FREQUENCY          10000         //10KHz SPWM波的频率

    #define CARRIER_PERIOD_TIME       TIVA_MAIN_FREQUENCY / SAMPLE_FREQUENCY // SPWM波的周期

    我想改变SAMPLE_FREQUENCY ,使它能随按键变化,而不是每次都手动改动程序。让他SAMPLE_FREQUENCY   =    N(已经实现了N随按键和滚轮变化)* SPWM_FREQUENCY。

    可是没能成功,N在变(显示出来的)可是出来的PWM频率没变。

    新手求教~~~~         

  • 那就说明,PWM的频率不是通过这个改变的。正确的做法是,你找到操作PWM频率寄存器的函数(在那里修改了),或者应该是在中断中改变的。应该是在这个函数中设置PWM输出宽度的

  • 真的很谢谢~~

     

    不过我有想到过这个,也特意看了下,

                   // 设置PWM生成器和生成周期  

                   PWMGenPeriodSet(PWM0_BASE, PWM_GEN_3, CARRIER_PERIOD_TIME);

                  // s设置占空比

                  PWMPulseWidthSet(PWM0_BASE, PWM_OUT_6 | PWM_OUT_7, CARRIER_PERIOD_TIME / 2);

    而上面的楼层也提到了CARRIER_PERIOD_TIME = 主频 / SAMPLE_REQUENCY.

    在改动的时候仅仅是将SAMPLE_REQUENCY改动了下,使它不为定值,那么按理说应该会影响到最终生成的PWM波啊。。。。

     

    毕竟在我做改动之前,也是只需要更改SAMPLE_REQUENCY的数值(手动更改程序.h中定义的数值)而已,其他的是能传递过去的。

    以前学的C和51,对这个刚入门, 是否只改动在main.c中定义 SAMPLE_REQUENCY,并使CARRIER_PERIOD_TIME = 主频 / SAMPLE_REQUENCY是不够的,还得在其他地方修改?

     

    请问我可以加你的Q聊吗?

    QQ:1025173868

  • 私人qq还是算了吧,有问题在论坛提问就好。看了看你的程序,想法是没错的,但是你的框架搭的有漏洞,外部中断中居然有延时,你需要做的是

    1.找准PWM宽度的设置,到底是如何改变的,确保你找的公式正确

    2.按键的驱动写好,最好用状态机,或者延时检测完毕,再给一个修改标志

    3.写好按键和PWM改变的配合流程。

  • 回看了程序,改动后不知为何显示白屏了。

    将Timer0在中断中的0A改为了0B,然后又加了上图所示定义,现在做到的仅仅是更改SAMPLE_FREQUENCY,SPWM_FREQUENCY(打算使SAMPLE_FREQUENCY= N * SPWM_FREQUENCY),CARRIER_PERIOD_TIME,同时按比例改变,那么Timer0能出来所想要的频率,

    可是一旦写函数,新的SAMPLE_FREQUENCY却没能返回到程序里的SAMPLE_FREQUENCY中去;

    求教,到底应该怎样写才能使它能正确地赋值过去、、、、、

    PS:有些地方理解地还不是很到位,多多包涵~~~

    1.因为不止一个地方用到了SAMPLE_FREQUENCY,所以想的是不动SAMPLE_FREQUENCY所在的地方,只是加一个函数,使其能返回一个新的SAMPLE_FREQUENCY值,这些新的值再赋值给原本用到SAMPLE_FREQUENCY的地方;

    2.改变了下按键控制的方式,显示、N值随按键/滚轮变化、PWM频率随定义里的值改变而改变这些都已经做好,所以只剩能正确赋值!

    (1)

    // 8K Hz响应   

    ROM_TimerLoadSet (TIMER0_BASE, TIMER_B,     

                              ROM_SysCtlClockGet() / SAMPLE_FREQUENCY);

    (2)

    // 设置PWM生成器和生成周期

     PWMGenPeriodSet(PWM0_BASE, PWM_GEN_3, CARRIER_PERIOD_TIME);

     // s设置占空比  

    PWMPulseWidthSet(PWM0_BASE, PWM_OUT_6 | PWM_OUT_7, CARRIER_PERIOD_TIME / 2);

    (3)

    ROM_PWMGenPeriodSet (PWM0_BASE, PWM_GEN_3, CARRIER_PERIOD_TIME);  

    ROM_PWMPulseWidthSet (PWM0_BASE, PWM_OUT_7, SIN_TABLE[SinTable_Index]);

  • 用野蛮的方法使SAMPLE_FREQUENCY随按键的改变而改变了,但是!!!!这样的野蛮方法导致整个.c文件中含有SAMPLE_FREQUENCY只认最前面定义的初始值,而不是变化后的SAMPLE_FREQUENCY!

     

    果然还是得用函数令SAMPLE = N * SPWM,CARRIER_PERIOD_TIME=主频/SAMPLE,然后将新的SAMPLE给到所有涉及到这两个变量的地方。

     

    但是我试了下,仿照程序中原有的程序去做,没能做到新的SAMPLE赋值给原有的3个地方。

     

    又看看了,尝试在Init_Timer0_SPWM那儿加个中断(貌似Gu之前就是这个意思。。。?可惜因为不会写中断,总是偏离方向、、、)

  • 1.按键用状态机,进定时器中断检测

    2.主循环检测按键,根据按键状态修改PWM的设置。

     

  • 再次看了看你的程序,你有个概念没搞清,你的PWM的信号输出,到底是通过定时器产生的,还是通过PWM引脚产生的,你把这个理清楚,就好了。

  • 确实没太懂程序,四月末刚接触到CCS、TM4C与频相模块,只是在这段时间知道了些大概。

    作为程序渣,直接粗暴地去把真正影响Timer0输出频率的地方找到了------三段地方直接其他不动改其中一段数值: PWMGenPeriodSet(PWM0_BASE, PWM_GEN_3, CARRIER_PERIOD_TIME);-------------------void Int_Timer0A_Handler(void),就在第一个中断点处、、、

    在我改他的时候输出频率变了=_=、

     

    按键程序借鉴的艾研CH3中光标移动的程序,其实也就是频相跟踪main.c文件里LCD初始化处预留的:

    1.void Int_GPIO_C_Handler(void) //中断处理子函数

    2.void Int_GPIO_D_Handler(void)(把这里改成按键控制N了,用S2和S3控制)

    刚开始看的时候其实就很纳闷,为什么实验貌似没有用到按键之类的东西,还留着这两段和(对端口C、D进行按键中断初始化:void Init_Int_Key()),也许是忘记删掉,或者原版本是可以用按键操作什么的?

    话说我问过技术支持,那实习生告诉我这实验里没用到按键,可以忽略它。。。

     

    不知道这个是不是你说的状态机。。。。?说实话。。不太懂。。

    如果是,那么你提出的1应该就是解决了。。。。所以关键就是怎样赋值过去,也就是你说的第二条。

    弱弱地问一句,中断产生的顺序,也就是这一个中断点所在的前后。。。。是不是就是这一段中断程序在所有中断程序的前后位置。。。?

    看了程序和软件流程图估计是这样,把影响Timer0频率的那句改为N倍变化,然后试着将void Int_GPIO_D_Handler(void)放到void Int_Timer0A_Handler(void)前面,结果失败了。。。。。。

  • 作为程序渣,直接粗暴地去把真正影响Timer0输出频率的地方找到了------三段地方直接其他不动改其中一段数值: PWMGenPeriodSet(PWM0_BASE, PWM_GEN_3, CARRIER_PERIOD_TIME);-------------------void Int_Timer0A_Handler(void),就在第一个中断点处、、、

    在我改他的时候输出频率变了=_=、

    输出频率比变了?按照你设置的变化的么?are you sure?可是,据我理解,这里用到的是PWM中断,如下

    void
    PWM0IntHandler(void)
    {
        //
        // Clear the PWM0 LOAD interrupt flag.  This flag gets set when the PWM
        // counter gets reloaded.
        //
        PWMGenIntClear(PWM0_BASE, PWM_GEN_0, PWM_INT_CNT_LOAD);
    
        //
        // If the duty cycle is less or equal to 75% then add 0.1% to the duty
        // cycle.  Else, reset the duty cycle to 0.1% cycles.  Note that 64 is
        // 0.01% of the period (64000 cycles).
        //
        if((PWMPulseWidthGet(PWM0_BASE, PWM_OUT_0) + 64) <=
           ((PWMGenPeriodGet(PWM0_BASE, PWM_GEN_0) * 3) / 4))
        {
            PWMPulseWidthSet(PWM0_BASE, PWM_OUT_0,
                             PWMPulseWidthGet(PWM0_BASE, PWM_OUT_0) + 64);
        }
        else
        {
            PWMPulseWidthSet(PWM0_BASE, PWM_OUT_0, 64);
        }
    }
    
  • 那个。。。。艾研CH10频相跟踪是实验里没有这个啊。。。。。

     

    我问的Timer0频率调节是频相跟踪实验里的。。。。。。

    T _T。。。。。。。。

     

    看着这个感觉很熟悉。。。貌似是一个PWM占空比自动从0.1%~100%循环变化的程序里有用到?

     

    。。。。。。。。以下是我说道的改变了CARRIER_PERIOD_TIME就改变了PWM输出频率的地方:

    void Int_Timer0A_Handler(void)
    {
    //	start0 = ROM_SysTickValueGet();
    
    	ROM_TimerIntClear (TIMER0_BASE, TIMER_TIMA_TIMEOUT);
    
    	ROM_TimerEnable (TIMER0_BASE, TIMER_A);
    
    	add_phase_step = Phase_step_N * Sample_Index + Delay_phase;   //微调相位指针
    
    	Sample_Index++;
    
    	SinTable_Index = (int) add_phase_step & 0x7FF;
    
    	//ROM_PWMGenPeriodSet (PWM0_BASE, PWM_GEN_3, CARRIER_PERIOD_TIME);
    	//ROM_PWMGenPeriodSet (PWM0_BASE, PWM_GEN_3, 40000);
    	ROM_PWMGenPeriodSet (PWM0_BASE, PWM_GEN_3, 80000000 / SAMPLE_FREQUENCY);
    	ROM_PWMPulseWidthSet (PWM0_BASE, PWM_OUT_7, SIN_TABLE[SinTable_Index]);
    
    }

    果然帖子里没有说清楚么。。。。

     

    占空比的问题已经解决了、、、觉得直流电机貌似就是个滚轮改变占空比的程序,所以就直接参照它做好了。。。。

     

    一开始是不知道频相跟踪实验里怎样影响到生成的SPWM波的频率,只是老师给了个任务让做这个,随着多看看程序,把问题简化到了改变Timer0的响应频率,也就是SAMPLE_FREQUENCY,在社区的另外一个帖子我也这样问了、、、、、

  • 很谢谢你一直耐心的回复,已经成功地两种方法实现改变了~~ 如果时间够,我打算尝试下TI高校培训用的菜单模式,把两种方法做成lab1,2,再将滚轮改变PWM占空比的实验和频相放主菜单~ 希望层主不要嫌弃,如果看懂了那程序,能一起探讨下~~~~ 我知道跳得太快,因为并不是自己在玩板子,时间很紧………………
  • 恭喜恭喜。其实,我也没能帮你什么忙。。。哈哈。

  • 请问层主方便发一下源码吗?我觉得您说的这个功能和相关代码非常有趣,想好好学习一下。如果不方便在这发布的话可以麻烦您发到我的邮箱(2674871752@qq.com.)吗?非常感谢!