GD32F330C8T6单片机外设应用教程(三):编码开关、M62429控制与PWM定时器程序实现【详解】

文章目录

  • 前言
  • 一、旋转编码开关控制时序
  • 1、对开关状态进行采集——得到四个组合状态
  • 2、判断一个完整旋转步进过程的标准——pulse0[]或者pulse1[]累加超过4次
  • 3、在其他暂稳态里,用变量pulse0[]和pulse1[]记录开关变化行踪
  • 4、实现多路旋转开关的方法——单个变量变成数组,理论上可以采集更多旋转开关
  • 二、定时器1实现PWM信号输出
  • 1、占空比设置函数的3个入参的含义
  • 2、TIMER1初始化和调整占空比
  • 三、M62429的衰减代码和控制时序
  • 1、对音量衰减值的处理
  • 2、将指令正确输送到M62429
  • 总结

  • 前言

    1、本章节介绍旋转编码开关的程序实现过程,能采集三路甚至更多路旋转编码开关的触发信号;
    2、通过LED渐变,介绍定时器x的PWM信号输出功能:改变分频值,改变输出信号的占空比,在本项目中,LED渐变表示旋转编码开关的旋转步数增减程度;
    3、将M62429的控制功能封装成函数,该函数包括音频衰减值的数组(步进1dB),和控制时序代码;入参包括音频衰减数组的序号,(0-84),和声道选择序号;


    旋转编码开关演示

    视频展示了三个旋转开关分别控制M62429芯片音量衰减的过程,电路板左下角是音频信号输入,右下角是输出;LED渐暗同步表示音量变小,变亮表示音量变大

    一、旋转编码开关控制时序

    本程序多年前从贵站获得,在众多控制时序代码中,笔者认为该代码效果最好,不仅能够准确旋转方向,而且不会产生误动作。但是代码较长,经过实践和分析,笔者将分为三个部分分别讲解:

    1、对开关状态进行采集——得到四个组合状态

    旋转编码开关动作一次,A,B两个端分别产生一个方波,由于其特殊结构设计,这两个方波信号正好相差90°,这样就能经过四个组合状态:

    A=1,B=1 状态1
    A=1,B=0 状态4
    A=0,B=1 状态2
    A=0,B=0 状态3

    uint8_t EncoderProcess( uint8_t SWA , uint8_t SWB, uint8_t SWNum  )
    {
        if(SWA)                 //buttonA代表编码开关旋转是产生的信号A
        {
            if(SWB)              //buttonB为信号B
            {
                input_status[SWNum] = 1;     //A=1;B=1  ;input_status用1,2,3,4代表两信号的状态
            }
            else if(!SWB)
            {
                input_status[SWNum] = 4;     //A=1;B=0
            }
        }
        else if(!SWA)
        {
            if(SWB)
            {
                input_status[SWNum] = 2;      //A=0;B=1
            }
            else if(!SWB)
            {
                input_status[SWNum] = 3; 	    //A=0;B=0
            }
        }
    

    每一次执行(执行间隔时间1ms即可)此函数,第一件事就是更新input_status这个变量,看看当前旋转开关在哪个状态,注意的是: A=1,B=1是稳定状态,其他三个状态都是暂时状态。

    2、判断一个完整旋转步进过程的标准——pulse0[]或者pulse1[]累加超过4次

     //如果状态为 1 因为开关旋转后 电平会停在 1 状态,在此执行所需要的东西
            if(scanf_status[SWNum] == 1)	  //若scanf_status为1,说明旋转开关正向旋转了一个步进(20个步进是一圈)
            {
                if(pulse0[SWNum] >= 4)
                {
                    pulse0[SWNum] = 0;						
    			if (counter[SWNum] >= 84 )//限制在84个步进,与使用的编码开关型号有关
    			   {counter[SWNum] = 84;}
    			else counter[SWNum] ++;
    		    return counter[SWNum] ;//确定正转,返回1;
                }
                if(pulse1[SWNum] >= 4)
                {
                 pulse1[SWNum] = 0;
    			 if (counter[SWNum] < 2)
    			  {counter[SWNum] = 0;}
    			 else counter[SWNum] --;
                 return counter[SWNum];//确定反转,返回2
                }
                //在状态1 的 前提下,判断是正旋转还是反旋转
                if (input_status[SWNum] == 2)        //正旋转
                {
                    scanf_status[SWNum] = 2;
                    pulse0[SWNum]++;
                    pulse1[SWNum] = 0;
                }
                else if (input_status[SWNum] == 4)        //反旋转
                {
                    scanf_status[SWNum] = 4;
                    pulse1[SWNum]++;
                    pulse0[SWNum] = 0;
                }
            }
    

    scanf_status里记录第二次执行函数时开关的状态,因为只有开关动起来才能叫旋转开关,所以捕捉第二次动作才可能判断旋转的方向。

    这里的逻辑是:如果第二次采集到开关时发现是稳定状态,也就是 A=1,B=1,那就再观察它是否还经历了 A=1,B=0、 A=0,B=1、 A=0,B=0,在如果经历了这些状态,分别表示方向的变量pulse0或者pulse1其中一个会累计到4;

    一旦pulse0或者pulse1累计到4,说明旋转开关真正地完成了一次动作,计数变量counter可以加减数并且返回了,在这里counter可以自由定义变化幅度,加1还是加2随意;也可以定义循环加还是加到一定程度就不变了,这与项目需要而自由发挥。

    3、在其他暂稳态里,用变量pulse0[]和pulse1[]记录开关变化行踪

     //在状态1 的 前提下,判断是正旋转还是反旋转
    
                if (input_status[SWNum] == 2)        //正旋转
                {
                    scanf_status[SWNum] = 2;
                    pulse0[SWNum]++;
                    pulse1[SWNum] = 0;
                }
                else if (input_status[SWNum] == 4)        //反旋转
                {
                    scanf_status[SWNum] = 4;
                    pulse1[SWNum]++;
                    pulse0[SWNum] = 0;
                }
            }
    
            //在状态2 的 前提下,判断是正旋转还是反旋转
    
            if (scanf_status[SWNum] == 2)        //正旋转
            {
                if (input_status[SWNum] == 3)        //正旋转
                {
                    scanf_status[SWNum] = 3;
                    pulse0[SWNum]++;
                    pulse1[SWNum] = 0;
                }
                if (input_status[SWNum] == 1)        //反旋转
                {
                    scanf_status[SWNum] = 1;
                    pulse1[SWNum]++;
                    pulse0[SWNum] = 0;
                }
            }
            if (scanf_status[SWNum] == 3)        //在状态 3 的 前提下,判断是正旋转还是反旋转
            {
                if (input_status[SWNum] == 4)        //正旋转
                {
                    scanf_status[SWNum] = 4;
                    pulse0[SWNum]++;
                    pulse1[SWNum] = 0;
                }
                if (input_status[SWNum] == 2)        //反旋转
                {
                    scanf_status[SWNum] = 2;
                    pulse1[SWNum]++;
                    pulse0[SWNum] = 0;
                }
            }
            if (scanf_status[SWNum] == 4)         //在状态 4 的 前提下,判断是正旋转还是反旋转
            {
                if (input_status[SWNum] == 1)        //正旋转
                {
                    scanf_status[SWNum] = 1;
                    pulse0[SWNum]++;
                    pulse1[SWNum] = 0;
                }
                if (input_status[SWNum] == 3)        //反旋转
                {
                    scanf_status[SWNum] = 3;
                    pulse1[SWNum]++;
                    pulse0[SWNum] = 0;
                }
            }
    

    这部分是其他三种状态的记录过程,在状态1但是pulse未达到4,检查下一次的状态是怎么样的:在状态1下,发现下次状态是2,那这就是朝正方向转的趋势,pulse0累加而pulse1归零;如果发现是状态4,那就是朝反方向转的态势,pulse1累加而pulse0归零。

    在状态2和状态3下用相同的逻辑原理做判断和累加。

    4、实现多路旋转开关的方法——单个变量变成数组,理论上可以采集更多旋转开关

    笔者得到源程序时,所有变量都是8位有符号变量,因为本项目使用了3只旋转编码开关,并且没有使用总线形式。

    要想高效使用这个代码有点长的功能函数,就要做点修改——将记录变化的变量都变成8位数组,数组元素是3个,对应旋转编码开关的数量。函数声明如下:

    uint8_t EncoderProcess( uint8_t SWA , uint8_t SWB ,uint8_t SWNum);
    

    理论上可以记录256个开关的变化。如本章开头演示的那样,三个开关可以分别控制三个LED

    二、定时器1实现PWM信号输出

    通过本项目,特别熟悉了一回通用定时器的PWM输出功能,这样做一方面试图熟练掌握相关库函数,另一方面,通过用LED的亮暗变化表示编码开关的步进程度。

    1、占空比设置函数的3个入参的含义

    请看代码:

      SW1Sta =	EncoderProcess( SW1_A , SW1_B , SW1);//SW1Sta最小0,最大20
      if (SW1Sta != SW1StaLast) //查询是否动了旋钮,动了就执行M62429_WriteData,否则推出保持
        {
           SW1StaLast = SW1Sta;
           M62429_WriteData( 1 ,  SW1Sta );
           timer_channel_output_pulse_value_config(TIMER1, TIMER_CH_2, SW1Sta * 400);
         }
    

    在if判断里的第三个函数:

    timer_channel_output_pulse_value_config(TIMER1, TIMER_CH_2, SW1Sta * 400);
    

    这是执行PWM输出的函数,一旦执行,TIMER1的通道2TIMER_CH_2,也就是单片机管脚的PA2就会输出既定频率和占空比的信号;

    如截图可知,该信号频率1kHz,占空比为84%,这两个参数如何得来的呢?这是初始化阶段定义好的。

    2、TIMER1初始化和调整占空比

    这里介绍定时器1的初始化函数关键部分:

        /* 基本定时器1初始化函数
         * 参数:  psr:时钟预分频系数,预分频值=psr+1
                   arr:自动重装载值,计数次数=arr+1
         * 返回值:无	*/
    
        void timer1_init(uint32_t psr, uint32_t arr)
        {
    
            gpio_mode_set (GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2);
            gpio_output_options_set (GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2);
            gpio_af_set (GPIOA, GPIO_AF_2, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2);
    
            timer_oc_parameter_struct timer_ocintpara;
            timer_parameter_struct timer_initpara;
            rcu_periph_clock_enable(RCU_TIMER1);
            timer_deinit(TIMER1);
    
            timer_initpara.prescaler         = psr;
            timer_initpara.alignedmode       = TIMER_COUNTER_EDGE;
            timer_initpara.counterdirection  = TIMER_COUNTER_UP;
            timer_initpara.period            = arr;
            timer_initpara.clockdivision     = TIMER_CKDIV_DIV1;
            timer_initpara.repetitioncounter = 0;
            timer_init(TIMER1, &timer_initpara);
    
            timer_ocintpara.ocpolarity   = TIMER_OC_POLARITY_HIGH;
            timer_ocintpara.outputstate  = TIMER_CCX_ENABLE;
            timer_ocintpara.ocnpolarity  = TIMER_OCN_POLARITY_HIGH;
            timer_ocintpara.outputnstate = TIMER_CCXN_DISABLE;
            timer_ocintpara.ocidlestate  = TIMER_OC_IDLE_STATE_LOW;
            timer_ocintpara.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW;
    
            timer_channel_output_config(TIMER1, TIMER_CH_0, &timer_ocintpara); //PA0
            timer_channel_output_config(TIMER1, TIMER_CH_1, &timer_ocintpara); //PA1
            timer_channel_output_config(TIMER1, TIMER_CH_2, &timer_ocintpara); //PA2
            timer_channel_output_mode_config(TIMER1, TIMER_CH_0, TIMER_OC_MODE_PWM0);
            timer_channel_output_mode_config(TIMER1, TIMER_CH_1, TIMER_OC_MODE_PWM0);
            timer_channel_output_mode_config(TIMER1, TIMER_CH_2, TIMER_OC_MODE_PWM0);
            timer_channel_output_shadow_config(TIMER1, TIMER_CH_0, TIMER_OC_SHADOW_DISABLE);
            timer_channel_output_shadow_config(TIMER1, TIMER_CH_1, TIMER_OC_SHADOW_DISABLE);
            timer_channel_output_shadow_config(TIMER1, TIMER_CH_2, TIMER_OC_SHADOW_DISABLE);
            timer_primary_output_config(TIMER1, ENABLE);
            timer_auto_reload_shadow_enable(TIMER1);
    
            timer_enable(TIMER1);
        }
    

    调用函数:

     timer1_init(1-1 , 8000-1);
    

    1、端口配置

    PWM信号是端口资源,必须先将端口状态配置成复用功能
    PA0, PA1,PA2这三个端口可以复用为定时器1的通道2

     gpio_mode_set (GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2);
     gpio_output_options_set (GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2);
     gpio_af_set (GPIOA, GPIO_AF_2, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2);
    

    2、自动重新装载arr和预分频数psr

        timer_initpara.prescaler         = psr;
        timer_initpara.alignedmode       = TIMER_COUNTER_EDGE;
        timer_initpara.counterdirection  = TIMER_COUNTER_UP;
        timer_initpara.period            = arr;
        timer_initpara.clockdivision     = TIMER_CKDIV_DIV1;
        timer_initpara.repetitioncounter = 0;
        timer_init(TIMER1, &timer_initpara);
    

    第四行arr是自动重新装载值,在声明函数时将其作为入参。
    该参数决定了定时器的计数周期。

             计数周期 = (计数周期系数 - 1) /   系统时钟周期
    

    PWM信号频率为1kHz是这样算的:注意公式中第五行时钟分频的参数为TIMER_CKDIV_DIV1,也就是系统时钟不分频,公式中的系统时钟周期是这个设置在 systick_config();函数里已经定义过,取的是的外部8MHz。

    /* set the system clock frequency and declare the system clock configuration function */
    #ifdef __SYSTEM_CLOCK_8M_HXTAL
    uint32_t SystemCoreClock = __SYSTEM_CLOCK_8M_HXTAL;
    static void system_clock_8m_hxtal(void);
    
    
        计数周期 = ( 计数周期系数 - 1 ) /   系统时钟周期 = ( 8000 - 1)/ 8M = 1ms
    

    预分频数psr,STM32系统有复杂的时钟系统,在这里是对系统时钟的再次分频,这里取0,实际上是不再分频,也就是分频数为1

    arr取7999,也就是计数到8000,代入公式,算出计数周期是1ms,也可以说计数频率是1kHz

    3、调整占空比

    timer_channel_output_pulse_value_config(TIMER1, TIMER_CH_2, SW1Sta * 400);
    

    执行这个函数就可以改变占空比,在这里SW1Sta * 400在函数定义时叫做pulse,它作为入参,和arr关系如下:

                         占空比(%)= pulse /  arr
    

    笔者的SW1Sta 是旋转开关的步进数,范围是0-20,乘上400,就能做到:每个改变一个步进得到5%的占空比的变化。

    三、M62429的衰减代码和控制时序

    1、对音量衰减值的处理

    笔者根据数据手册的介绍,将音量衰减值编成了表,如下

    |
    为方便读者取用,现将衰减值列在下面:

    		unsigned int  VloumeData[85] = 
    			{ 0x15F,0x157,0x15B,0x153,0x5F,
    				0x57,0x5B,0x53,0x19F,0x197,
    				0x19B,0x193,0x9F,0x97,0x9B,
    				0x93,0x11F,0x117,0x11B,0x113,
    				0x1F,0x17,0x1B,0x13,0x1EF,0x1E7,
    				0x1EB,0x1E3,0xEF,0xE7,0xEB,0xE3,
    				0x16F,0x167,0x16B,0x163,0x6F,0x67,
    				0x6B,0x63,0x1AF,0x1A7,0x1AB,0x1A3,
    				0xAF,0xA7,0xAB,0xA3,0x12F,0x127,0x12B,
    				0x123,0x2F,0x27,0x2B,0x23,0x1CF,0x1C7,
    				0x1CB,0x1C3,0xCF,0xC7,0xCB,0xC3,0x14F,
    				0x147,0x14B,0x143,0x4F,0x47,0x4B,0x43,
    				0x18F,0x187,0x18B,0x183,0x8F,0x87,0x8B,
    				0x83,0x10F,0x107,0x10B,0x103,0xF
    			};
    

    笔者是这样处理的:
    1、芯片的音量衰减分为两部分,一部分是以4dB为步进,从0dB直到-∞,此部分占据D2-D6,另一部分是-1dB -2dB -3dB,如果使用它们,就可以补充在4dB步进之间,以此来提高音量调整精度,这部分数据占据D7和D8两位。

    例如,若想得到-7dB的衰减值,D2-D8就是001010011,0x53


    在表格中,笔者按照1dB的步进,将D2到D8按照音量衰减顺序列出。另外,D9和D10是常为1,这两位是用来触发指令的确认位,M62429芯片没有读取返回,只有指令写入,指令写入是靠时钟信号的上升沿确认的,指令写入完毕,最后触发是靠时钟信号的下降沿确认的,具体如何实现下面会介绍。D2-D8、包括D9和D10。一起放在数组 **VloumeData[85]**里。这里笔者埋下了一个小bug,你是否能发现呢?答案后面会有揭晓。

    2、D0和D1位,用来控制单声道还是双声道的,一般来说没有特别要求,都是两个声道一起控制,但是也保留了单独控制的需求,所以将这部分单独存放在数组中。

    unsigned int  channelData[3] =    {1, 3, 0};
    

    2、将指令正确输送到M62429

    1、函数声明为:

    void M62429_WriteData(uint8_t channel , uint8_t data);
    

    入参有两个:channel 声道数组元素位;data衰减值数组元素位置。

    2、将声道数组channelData[x]和音量数组VloumeData[y],合并为一个int型的变量tempdata ,这里要说明的是,tempdata 必须定义成无符号整型unsigned int,才能执行 tempdata_1 = tempdata_1 << 21指令。

        tempdata =  channelData[channel];
        tempdata = tempdata << 30; //将声道选择D0,D1放在最高两位
        tempdata = tempdata & 0xC0000000;//将低30位清掉
        tempdata_1 = VloumeData[data];//Vloume包括了D2到D10,共9位
        tempdata_1 = tempdata_1 << 21;
        tempdata_1 = tempdata_1 & 0x3FFFFFFF;//将高2位清掉
        tempdata = tempdata | tempdata_1;//得到11位有用的数据:D0-D10
    

    3、将 tempdata数据传输到芯片去

    先将数据从最高位开始,“架”在数据线上,准备“发射”;

          if(tempdata & 0x80000000)  //判断数据,把数据架在输出数据位上
            {
                M62429_Data_Set;
            }
            else
            {
                M62429_Data_Reset;
            }
    

    然后,置位时钟线,芯片得到上升沿就将一个数据位传输给了芯片;

    接着,数据线复位,然后时钟线再复位,这步很重要,因为只有信号上升沿来到的时候,数据才能被写进芯片去。

       for(i = 0 ; i < 10 ; i++)
        {
          //  
            delay_us(5);
            if(tempdata & 0x80000000)  //判断数据,把数据架在输出数据位上
            {
                M62429_Data_Set;
            }
            else
            {
                M62429_Data_Reset;
            }
            delay_us(5);
            M62429_Clk_Set;//时钟上升沿到来,数据写进芯片
            delay_us(5);
            M62429_Data_Reset;//将信号线拉低
            delay_us(5);
            M62429_Clk_Reset ;//时钟再复位
            delay_us(5);
            tempdata = tempdata << 1;   //把新数据架在输出数据线上
        }
    

    如果你细心,会发现这里有个值得注意的地方:

    VloumeData[85]里的元素都是9位,D2-D10,在这里再加上D0和D1,合并在tempdata ,应该是传输11位数据给芯片,对不对?可是for循环里,传输数据只传送了前10位:

       for(i = 0 ; i < 10 ; i++)
    

    那么第11位D10去哪里了?它在for循环后面:

        M62429_Data_Set;
        delay_us(5);
        M62429_Clk_Set;
        delay_us(5);
        M62429_Clk_Reset ;
        delay_us(5);
    

    在这里,数据线拉高,然后给一个时钟的变化——一高一低——芯片得到下降沿信号后,就认为得到了“触发”,从此将前面10位数据串行集中处理。

    在前10位的操作中,如果时钟复位的时候,数据线为高,那样芯片就会得到“触发”信号,触发动作是在D10为出现时才应该有的操作。

    4、void M62429_WriteData(uint8_t channel , uint8_t data)完整代码在此:

    //VloumeData是D2到D10 ,数组元素低位是0db,最后位是∞
    
    
    unsigned int  VloumeData[85] = 
    			{ 0x15F,0x157,0x15B,0x153,0x5F,
    				0x57,0x5B,0x53,0x19F,0x197,
    				0x19B,0x193,0x9F,0x97,0x9B,
    				0x93,0x11F,0x117,0x11B,0x113,
    				0x1F,0x17,0x1B,0x13,0x1EF,0x1E7,
    				0x1EB,0x1E3,0xEF,0xE7,0xEB,0xE3,
    				0x16F,0x167,0x16B,0x163,0x6F,0x67,
    				0x6B,0x63,0x1AF,0x1A7,0x1AB,0x1A3,
    				0xAF,0xA7,0xAB,0xA3,0x12F,0x127,0x12B,
    				0x123,0x2F,0x27,0x2B,0x23,0x1CF,0x1C7,
    				0x1CB,0x1C3,0xCF,0xC7,0xCB,0xC3,0x14F,
    				0x147,0x14B,0x143,0x4F,0x47,0x4B,0x43,
    				0x18F,0x187,0x18B,0x183,0x8F,0x87,0x8B,
    				0x83,0x10F,0x107,0x10B,0x103,0xF
    			};
    unsigned int  channelData[3] =    {1, 3, 0};
    
    /* 向芯片M62429写入数据
     * 参数:衰减值Vloume,共25个选择;声道选择channel—0:左声道;1:右声道;2:两声道一起控制
     * 返回值:无;
    */
    uint8_t i;
    unsigned int tempdata , tempdata_1;
    
    void M62429_WriteData(uint8_t channel , uint8_t data)
    
    {
    
        tempdata =  channelData[channel];
        tempdata = tempdata << 30; //将声道选择D0,D1放在最高两位
        tempdata = tempdata & 0xC0000000;//将低30位清掉
    
        tempdata_1 = VloumeData[k];//Vloume包括了D2到D10,共9位
    
        tempdata_1 = tempdata_1 << 21;
        tempdata_1 = tempdata_1 & 0x3FFFFFFF;//将高2位清掉
        tempdata = tempdata | tempdata_1;//得到11位有用的数据:D0-D10
        M62429_Data_Set ;
        M62429_Clk_Reset ;	
        for(i = 0 ; i < 10 ; i++)
        {
          //  
            delay_us(5);
            if(tempdata & 0x80000000)  //判断数据,把数据架在输出数据位上
            {
                M62429_Data_Set;
            }
            else
            {
                M62429_Data_Reset;
            }
            delay_us(5);
            M62429_Clk_Set;
            delay_us(5);
            M62429_Data_Reset;
            delay_us(5);
            M62429_Clk_Reset ;
            delay_us(5);
            tempdata = tempdata << 1;   //把新数据架在输出数据线上
        }
        M62429_Data_Set;
        delay_us(5);
        M62429_Clk_Set;
        delay_us(5);
        M62429_Clk_Reset ;
        delay_us(5);
    
    }
    
    
    
    

    总结

    1、旋转编码开关代码较长,流程复杂,但是比较好用,特别是相关变量改成数组后,扩展了旋转开关的使用数量;
    2、PWM信号在这里只是用于LED的亮灭,实际用途非常广泛;
    3、M62429最为关键的一个是对音量衰减代码的理解;另一个是对时序的理解。

    作者:电子设计笔记

    物联沃分享整理
    物联沃-IOTWORD物联网 » GD32F330C8T6单片机外设应用教程(三):编码开关、M62429控制与PWM定时器程序实现【详解】

    发表回复