H_On个人小站

个人站

这都被你发现了【惊
奖励你一朵小红花~


电子琴 - PWM 是由频率和占空比组成的

目录

前言

其实上一章就想制作电子琴的,但是上一章用到的定时器实现的 PWM 输出只能控制电压,也就是说,它只能控制 “占空比”。

占空比 指的是一个周期内有效电平的比率,比如一个 LED 正极接 VCC,负极接 GPIO 引脚,那么就是输出低电平时 LED 会发光。这时如果持续输出低电平,也就是说一个周期的 100% 都是 “有效电平”。如果一个周期内 50% 是低电平,50% 是高电平,那么亮度就会是最亮的一半。LED 灯就是这样不断闪烁着发光的。由于频率太高人类肉眼几乎看不到闪烁,又因为每个周期的发光时长不同,因此宏观上会有明暗差异。

经过不断的摸索和练习,最终掌握了直接手动控制引脚输出高低电平的次数和时间间隔来实现对输出电信号的频率和占空比的调整。

频率

当我们想要发出某个 频率 的声音时,只需要记住一点,频率其实就是 1s 内的周期个数。例如中音 DO 的频率是 523,也就是说想要发出 DO 的音,只需要让蜂鸣器在 1s 内响 523 次就可以了。1s 内响的次数越多,宏观上频率越高,音调越高。

所以我们需要做的就是:计算周期时长:1s = 1Mus,1Mus/频率 = 一个周期的时长,然后将占空比设为 50% 即可。假设我们想让中音 DO 响上 1s,只需这样:

uint32_t waitt;
uint16_t frq = 523;
uint16_t e;
// 占空比:一个周期里有多少是 “有效的”
// 这里 1s = 1Mus,半个周期就是 500Kus,除以频率就是半个周期的时长
waitt = 500000/((uint32_t)frq);
// 频率:1s 内的周期个数
for (e = 0; e < (uint16_t)(frq); e++) {
	GPIO_ResetBits(GPIOA, GPIO_Pin_6);
	delay_us(waitt);
	GPIO_SetBits(GPIOA, GPIO_Pin_6);
	delay_us(waitt);
}

如果想响上 0.25s,就减少 for 循环的次数,即周期个数

// 右移 2 位相当于除以 4
for (e = 0; e < (uint16_t)((frq)>>2); e++) {...}

实现代码

初始化输出引脚

// 设置一个普通的推挽输出引脚即可
void beep_init(void) {
	GPIO_InitTypeDef GPIO_InitStructure;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	GPIO_SetBits(GPIOA, GPIO_Pin_6);
}

定义音阶

由于太长了,这部分放到文章最后展示 -> 音阶定义

编写歌曲

请看 这里。由于我使用数组存储,指针访问,因此需要设置一个与众不同的数据来标志结束,这里我使用的是 5000。关于这部分的判断可以在下面的代码中看到。

演奏歌曲

// 传入数组名,实际上就是数组的 “首地址”
// 取值然后指针后移,直到当前值为结束标志的 5000,即停止演奏
void play_song(uint16_t* pscs) {
	while (*pscs != 5000) {
		play_one_key(*pscs);
		pscs++;
	}
}

演奏单音

// 传入频率
// 由于我使用的是低电平触发的蜂鸣器,因此前半个周期作为有效周期,且拉低电平,不过宏观上有效的部分在前还是在后应该差别不大
void play_one_key(uint16_t frq) {
	uint32_t waitt;
	uint16_t e;
	if (frq) {
		waitt = 500000/((uint32_t)frq);
		for (e = 0; e < (uint16_t)((frq)>>2); e++) {
			GPIO_ResetBits(GPIOA, GPIO_Pin_6);
			delay_us(waitt);
			GPIO_SetBits(GPIOA, GPIO_Pin_6);
			delay_us(waitt);
		}
	} else delay_ms(1000);
}

主函数

// 这里 play_song 直接传入上面设定的音乐数组的变量名即可
int main(void) {
	delay_init();
	led_init();
	beep_init();
	while (1) {
		play_song(Tones);
		// 这里设置了每演奏一次后休息 1s
		delay_ms(1000);
	}
}

定义代码

音阶

// 低一
#define D1_DO 262
#define D1_RE 294
#define D1_MI 330
#define D1_FA 349
#define D1_SOL 392
#define D1_LA 440
#define D1_XI 494
#define D1_SDO 277
#define D1_SRE 311
#define D1_SFA 370
#define D1_SSOL 415
#define D1_SLA 464
#define D1_DRE 277
#define D1_DMI 311
#define D1_DSOL 370
#define D1_DLA 415
#define D1_DXI 464
// 中音
#define DO 523
#define RE 587
#define MI 659
#define FA 698
#define SOL 784
#define LA 880
#define XI 988
#define SDO 554
#define SRE 622
#define SFA 740
#define SSOL 831
#define SLA 932
#define DRE 554
#define DMI 622
#define DSOL 740
#define DLA 831
#define DXI 932
// 高一
#define S1_DO 1046
#define S1_RE 1175
#define S1_MI 1318
#define S1_FA 1397
#define S1_SOL 1568
#define S1_LA 1760
#define S1_XI 1976
#define S1_SDO 1109
#define S1_SRE 1245
#define S1_SFA 1480
#define S1_SSOL 1661
#define S1_SLA 1865
#define S1_DRE 1109
#define S1_DMI 1245
#define S1_DSOL 1480
#define S1_DLA 1661
#define S1_DXI 1865

示例音乐

// 音阶测试
uint16_t Tones[] = {
	DO, RE, MI, FA, SOL, LA, XI, S1_DO,
	5000
};
// 两只老虎
uint16_t TwoTigers[] = {
	DO, DO, RE, RE, MI, MI, DO, DO,
	DO, DO, RE, RE, MI, MI, DO, DO,
	MI, MI, FA, FA, SOL, SOL, 0, 0,
	MI, MI, FA, FA, SOL, SOL, 0, 0,
	SOL, LA, SOL, FA, MI, MI, DO, DO,
	SOL, LA, SOL, FA, MI, MI, DO, DO,
	RE, RE, D1_SOL, D1_SOL, DO, DO, 0, 0,
	RE, RE, D1_SOL, D1_SOL, DO, DO, 0, 0,
	5000
};
// 送别
uint16_t Leave[] = {
	SOL, SOL, MI, SOL, S1_DO, S1_DO, S1_DO, S1_DO,
	LA, LA, S1_DO, LA, SOL, SOL, SOL, SOL,
	SOL, SOL, DO, RE, MI, MI, RE, DO,
	RE, RE, RE, RE, RE, RE, 0, 0,
	SOL, SOL, MI, SOL, S1_DO, S1_DO, S1_DO, XI,
	LA, LA, S1_DO, S1_DO, SOL, SOL, 0, 0,
	SOL, SOL, RE, MI, FA, FA, FA, D1_XI,
	DO, DO, DO, DO, DO, DO, 0, 0,
	LA, LA, S1_DO, S1_DO, S1_DO, S1_DO, S1_DO, 0,
	XI, XI, LA, XI, S1_DO, S1_DO, S1_DO, 0,
	LA, XI, S1_DO, LA, LA, SOL, MI, DO,
	RE, RE, RE, RE, RE, RE, 0, 0,
	SOL, SOL, MI, SOL, S1_DO, S1_DO, S1_DO, XI,
	LA, LA, S1_DO, S1_DO, SOL, SOL, 0, 0,
	SOL, SOL, RE, MI, FA, FA, FA, D1_XI,
	DO, DO, DO, DO, DO, DO, 0, 0,
	5000
};