GD32F330C8T6单片机外设应用教程(三):编码开关、M62429控制与PWM定时器程序实现【详解】
文章目录
前言
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最为关键的一个是对音量衰减代码的理解;另一个是对时序的理解。
作者:电子设计笔记