../主页

mp3.audio

大家好,我是积分猫。这篇文章用于说明我前两天发的mp3中,音乐播放部分技术实现思路。如果大家比较感兴趣怎么自己也来做一个,或者感兴趣想通过这个项目学一些东西,我会将这个做成一个系列教程。希望大家能够喜欢,如果遇见不懂得随时提问,我看到就会解答。

代码已经在Github开源:maxwell-ghost/rpi-pico-mp3: 积分猫用树莓派pico做的mp3 mp3最终成品展示:bilibili

0 写在前面

0.1 项目参考及教程

#c 动机 本项目的初始动机

本项目的初始动机来源于我某次看见Github上的一个项目,是教你如何裸机进行单片机开发的,看到后感觉很不错,于是star之后准备后续自己也整一个。再之后就是开春开始着手准备,拖拖拉拉知道现在才搞得有点模样。大家可以去瞧瞧这个项目,是很好的一个教程,点这里

bare_metal_github

#d 裸机开发

这里的裸机开发是指不借助芯片厂商提供的SDK(Software Development Kit),仅使用GNU的C编译器(GCC)和Make工具来进行开发,是的,你没有听错,我当时也很震惊。

#e 使用Keil开发 裸机开发

接触过51单片机的应该对这个开发工具很熟悉,我大学的时候课程设计用过这个开发环境。当时只需要定义一个IO(Input & Output)口变量,然后调用工具提供的接口(Interface)就可以进行上层开发,不需要关注具体的底层细节。

0.2 要求

#c 建议 前置知识需求

要看懂本项目最起码需要C语言过关,并且懂一些计算机底层知识和基本数字电路知识。同时需要会一点Linux操作系统使用(Make 和 GCC)。不过这些在有ChatGPT之后简单了许多,可以自己先看看这篇文章,如果喜欢,何愁不会。

1 项目组织结构

这部分是关于项目是如何被组织起来,以及各个文件及目录的作用。如果感觉吃力可以跳过,后续如果做系列教程,我单独写一篇文章来说明。

1.0 树莓派Pico

#d 正常启动模式

树莓派Pico有两种启动模式,按住树莓派的按键(下图BOOTSEL)上电(Power on,就是插电),会进入U盘模式,此时计算机上会出现Pico化身的U盘。如果不按按键上电,则会进入正常启动模式。此时编写好的程序会运行。

rpi_pico_board

#d U盘启动模式

树莓派Pico的U盘模式用于烧录编译(Compile)好的固件(Firmware),将工程文件中./build/firmware.uf2 文件拷贝到Pico中,此时firmware.uf2被拷贝到树莓派Pico的Flash(类比计算机的硬盘)上,Pico就会自动加载镜像并进入正常启动模式。

#t 计算机开机 正常启动模式

类比计算机,树莓派的正常启动类似于计算机的启动。而U盘模式类似于计算机安装系统的模式。

1.1 启动文件关系描述

  • 本项目通过startup.c文件中的_reset函数启动,_reset函数调用main函数
  • main函数位于main.c文件中,main.c通过引入头文件来管理各种功能,也就是通常C语言那个main,但是也不完全等同。

#c 流程 树莓派Pico启动

首先,树莓派Pico进入_reset函数,完成系列操作之后,调用main函数来完成mp3的所有功能。

#d _reset函数功能

具体而言,当硬件初始化完成进入Flash Second Stage(也就是退出U盘模式,运行我们写的代码开始运行的时候),这个时候内置的rom(Read Only Memory,出厂烧录的程序,不可更改,类比计算机的BIOS)代码已经读了256Byte的代码到SRAM(Static Radom Access Memory,类比计算机的内存)中,_reset函数首先初始化XIP(eXecute In Place)硬件(用以访问Flash中烧录的代码,因此仅初始化读的功能)。然后初始化内存,具体而言,就是将flash中的代码复制到SRAM中,从text段开始复制到data段,然后将bss段(未初始化的变量)全部置零。

#d 初始化Flash _reset函数功能

初始化XIP用以读flash。

	XIP_SSI->SSIENR = 0;
	XIP_SSI->BAUDR = 2;
	XIP_SSI->CTRLR0 = (0U << 21) | (3U << 8) | ((32 - 1) << 16); // Read
	XIP_SSI->CTRLR1 = (0 << 0);
	XIP_SSI->SPI_CTRLR0 = (3U << 24) | (6U << 2) | (2U << 8) | (0 << 0);
	XIP_SSI->SSIENR = BIT(0); // Enable

#d 初始化SRAM _reset函数功能

初始化内存,将flash中的代码和初始化的数据读到SRAM中。

	// Initialise memory
	extern long _sbss, _ebss, _edata, _stext, _sflash;
	for (long *src = &_sflash, *dst = &_stext; dst < &_edata;)
		*dst++ = *src++;
	for (long *src = &_sbss; src < &_ebss;)
		*src++ = 0;

#d 初始化中断服务 _reset函数功能

然后中断向量表(指向中断服务程序)加载到对应的寄存器中,注意这里SCB->VTOR前低八位无效,原因是ARM的要求中断向量表的地址和256对齐。这一点由链接脚本(./link.ld)中,boot部分为256Byte且vector段紧随其后保证。

	extern void (*vectors[])(void);
	SCB->VTOR = (uint32_t)vectors; // Point SCB to where our IRQ table is

#c 说明 中断概念理解

理解上面的 初始化中断服务 需要理解中断的概念,以及C语言中函数指针的以及函数指针数组,以及计算机是如何调用函数,这些知识,如果这个成为一个系列,我单独写一篇文章来讲这里。

#c 说明 无意义的返回

然后调用main函数,此函数不要返回,因为没有操作系统,返回值没有意义。

	extern void main(void);
	main(); // Call main()
	for (;;) (void)0; // Loop forever if main() returns

#d main函数的返回意义

如果你学过C语言,那么一定好奇过,main函数的返回值为什么是0。原因在于,在操作系统中,执行一个程序,如果成功执行,那么会返回给操作系统一个0作为成功的标志,如果执行失败,则会返回其他值,不同的值编码着不同的错误结果。因此,程序如果执行到return 0;,那么代表着,程序执行成功。

int main() {
    return 0;
}

1.2 应用文件关系描述

#d 总体关系描述

main.c和其他文件工程组成了链接时候的.text \\ 表示代码 .rodata \\ 表示只读的数据 .data \\ 初始化的数据 .bss \\未初始化的数据段,通过链接器将这些部分和启动代码部分组装起来,具体见./link.ld

#c 方法 C语言多文件管理

C语言通过头文件引入各个函数的声明和结构的定义,此时关于其他文件的函数调用仅仅只有调用指令,没有具体实现,通过编译具体头文件对应的.c文件,此时函数有了实现,程序变得完整。具体到这个项目,main函数依赖hal.h, syscall.c, oled.h, pinmap.h, rpi_time.h, key.h, audio.h,其中hal.h和pinmap.holed.h, rpi_time.h, key.h, audio.h依赖。

#c 说明 文件

  • 硬件相关:hal.h hal.c
  • 管脚定义相关:pinmap.h
  • 显示相关:oled.h
  • 时间相关: rpi_time.h
  • 声音相关: audio.h

2 Audio

关于演奏的基本原理,大家可以看我写的这篇博客, 用Matlab演奏《错位时空》。具体不再赘述。

#d 演奏基本原理

通过Pico上的PWM(Pulse Width Modulation)硬件产生不同的频率的方波,对应不同的音高的音符,以及通过控制延时来控制音符的时间长短,这样就可以完美演奏一条音轨的音乐。声音的大小可以通过控制PWM波形的占空比来调整。

#d 占空比

占空比用于表示方波的一个周期的 \(f(t)=1\) 和 \(f(t) = 0\) 所占时间的比值。

#e 占空比为1:2 占空比

下图中,$[\ 0, T\ ]$ 代表一个周期,IOVDD表示1(这里是3.3V)。

pwm_wave

2.1 管脚分配与硬件相关

#d 蜂鸣器

这里我们使用无源蜂鸣器来进行演奏,由于声音是震动产生的,我们使用特定频率的方波就可以让无源蜂鸣器产生震动,从而产生声音。

#c 说明 管脚分配

所有的管脚均可以配置为PWM输出,这里使用GP06号管脚。 pwm_channel_pins.png

#d PWM初始化

首先要启用PWM硬件电路,通过调用enable_subsystem函数(这里的subsystem包括各种外设等,具体见RP2040硬件手册2.14)完成,然后设置参数。CC(Counter Compared)设置PWM的占空比,这里设置为占空比为0:0xffff(0xffff=65535),相当于一直输出0(初始化后要先调节占空比)。TOP设置计数器最大值,这里设置为0xffff。DIV用于调整频率,CSR用于使能PWM。

static inline void pwm_init(struct pwm* pwm_slice) {
	if (pwm_slice == PWMCH3) {
		enable_subsystem(BIT(14));
	} else {
		printf("Not support now!\n");
	}

	pwm_slice->CSR = 0; // Disable pwm
	pwm_slice->CTR = 0; // Direct to pwm counter
	pwm_slice->CC = 0x00000000; // compared value
	pwm_slice->TOP = 0x0000ffff; // Max conut
	pwm_slice->DIV = (8UL << 4); // 8倍分频, 使用这个来控制频率
	pwm_slice->CSR = (1UL << 0); // 启用PWM
}

#c 说明 PWMCH3

可以看到,这里pwm_slice是一个结构体指针,指向对应的寄存器(Register)的地址,通过操作寄存器来操作PWM硬件电路。这里和PWMCH3(需要查看数据手册来确定值,我们要初始化通道三,这里是0x4005003c)做比较,初始化PWM硬件。

struct pwm {
	volatile uint32_t CSR;
	volatile uint32_t DIV;
	volatile uint32_t CTR;
	volatile uint32_t CC;
	volatile uint32_t TOP;
};
#define PWMCH3 ((struct pwm *) 0x4005003c)

#t 调节旋钮 寄存器

这里的寄存器可以类比成调节旋钮,通过调节这些旋钮,就可以完成对硬件电路的操作。

#d PWM频率设置

PWM频率计算公式如下,这里需要根据f_pwm(对应需要输出的频率,也就是音符频率下)算出DIV_INTDIV_FRAC从而控制设置频率,具体见下面。DIV_INT对应DIV寄存器11:4位,DIV_FRAC对应3:0。

f_pwm = f_sys / period
period = (TOP + 1)x(CSR_PH_CORRECT+1)x(DIV_INT + DIV_FRAC/16)

推出 -> (DIV_INT + DIV_FRAC/16) = f_sys / (65536 x f_pwm)

TOP 设置为0xffff=65535
CSR_PH_CORRECT 设置为0x0, 设置为1后将会从零到满再到零,0代表从零到满再从零到满
DIV_INT 和 DIV_FRAC 设置频率
f_sys = 133MHz

freq_formula

2.2 音符频率计算与使用

#d 频率转换

下面的频率对应对应从C0音的16.35Hz到B6音的1975.53Hz。每一行位12个音,代表12平均律,也就是C0和C1的频率差为两倍,C0到C1中间总共12个音,分别为

$$ f_{C0},\ f_{C0} \cdot 2^\frac{1}{12}, \ f_{C0} \cdot 2^\frac{2}{12} , \ ... \ f_{C0} \cdot 2^\frac{11}{12} $$
note_freq = [ 16.35, 17.32, 18.35, 19.45, 20.60, 21.83, 23.12, 24.50, 25.96, 27.50, 29.14, 30.87, # C0 C#0 D0 D#0 E0 F0 F#0 G0 G#0 A0 A#0 B0
  32.70, 34.65, 36.71, 38.89, 41.20, 43.65, 46.25, 49.00, 51.91, 55.00, 58.27, 61.74, # C1
  65.41, 69.30, 73.42, 77.78, 82.41, 87.31, 92.50, 98.00, 103.83, 110.00, 116.54, 123.47, #C2
  130.81, 138.59, 146.83, 155.56, 164.81, 174.61, 185.00, 196.00, 207.65, 220.00, 233.08, 246.94, #C3
  261.63, 277.18, 293.66, 311.13, 329.63, 349.23, 369.99, 392.00, 415.30, 440.00, 466.16, 493.88, #C4
  523.25, 554.37, 587.33, 622.25, 659.25, 698.46, 739.99, 783.99, 830.61, 880.00, 932.33, 987.77, #C5
  1046.50, 1108.73, 1174.66, 1244.51, 1318.51, 1396.91, 1479.98, 1567.98, 1661.22, 1760.00, 1864.66, 1975.53 #C6 
]

我们需要为这些频率找到【通过DIV_INT和DIV_FRAC调节所能达到最相近的设置】,这里使用Python来转换。

#d DIV_INT于DIV_FRAC计算

首先对2.1的频率计算公式进行变形,带入(f_sys=133MHz)和(f_pwm=上面的Python列表),就可以得到(DIV_INT + DIV_FRAC/16)的值,然后取整数部分就是DIV_INT,小数部分可以通过枚举所有 1/16~15/16的值,取和目标小数部分差的绝对值最小来得出DIV_FRAC的值。

# 得到最接近的小数部分
def get_near(frac_float):
	error = [] #误差表
	for i in range(16):
		error.append(abs(frac_float - i/16))
	
	min_index = 0
	min = 520
	# 找出最小的值,对应的索引值为DIV_FRAC
	for i, e in enumerate(error):
		if e < min:
			min = e
			min_index = i
	return min_index

#-> (DIV_INT + DIV_FRAC/16) = f_sys / (65536 x f_pwm)
request_float = [133000000 / (65536*item) for item in note_freq]

# 计算整数和小数部分
int_part = []
frac_part = []
for item in request_float:
	int_part.append(int(item))
	frac_part.append(get_near(item - int(item)))

#d 让C语言更易读取

最终结果如下为两张表,最终需要转换为pwm_slice->DIV 寄存器的格式,整数对应11:4位,小数对应3:0位。寄存器为32位,其他位未使用设为0。这里的Python代码将整数左移4位到11:4,与没有左移小数部分相与来得到对应的值,然后转换为16进制,补零到4位,然后和"0x"字符串相加,以"0x0000"字符串列表形式存储。

div_register

int16_allnote_list = []
for i, f in zip(int_part, frac_part):
	temp = i << 4 | f
	int16_allnote_list.append("0x" + hex(temp)[2:].zfill(4))

如下,为了让C语言更容易读取。

0x07c2, 0x0753, 0x06ea, 0x0685, 0x0628, 0x05cf, 0x057c, 0x052d, 0x04e3, 0x049d, 0x045a, 0x041c, 
0x03e1, 0x03a9, 0x0375, 0x0343, 0x0314, 0x02e8, 0x02be, 0x0297, 0x0272, 0x024e, 0x022d, 0x020e, 
0x01f0, 0x01d5, 0x01ba, 0x01a1, 0x018a, 0x0174, 0x015f, 0x014b, 0x0139, 0x0127, 0x0117, 0x0107, 
0x00f8, 0x00ea, 0x00dd, 0x00d1, 0x00c5, 0x00ba, 0x00af, 0x00a6, 0x009c, 0x0094, 0x008b, 0x0083, 
0x007c, 0x0075, 0x006f, 0x0068, 0x0063, 0x005d, 0x0058, 0x0053, 0x004e, 0x004a, 0x0046, 0x0042, 
0x003e, 0x003b, 0x0037, 0x0034, 0x0031, 0x002e, 0x002c, 0x0029, 0x0027, 0x0025, 0x0023, 0x0021, 
0x001f, 0x001d, 0x001c, 0x001a, 0x0019, 0x0017, 0x0016, 0x0015, 0x0014, 0x0012, 0x0011, 0x0010, 

#c 用法 使用频率值

通过用C语言创建uint16_t类型数组,然后索引使用值赋值给DIV寄存器来使用。

2.3 更高层级的抽象

这部分是关于播放音乐的,总体思路就是读一个音符,调整PWM频率,延时对应时长,然后读下一个音符重复同样的操作。

2.3.1 音乐概念

#d 八度

音乐中的高一个八度相当于上述的数组的索引整体加12,低一个八度相反。也就是频率变为之前的两倍或者1/2。C0高八度就是C1音,C2低八度就是C1音。

#d 调式

简谱中的数字为8个,0,1,2,3,4,5,6,7,其中1到7对应七个音,0对应休止符,也就是什么都不干。这七个音通过从12平均律的12个当中选取7个,于是就形成了不同的调式(modus)。不同的调式的音乐听起来有不同的感觉。目前做了下面这些调式。

enum e_modus {
	F_MAJOR = 0,
	F_SHARP_MAJOR,
	A_MAJOR,
	B_FLAT_MAJOR,
};

#d 起的调子高低

我也不知道该如何正式称呼这个。代表一首歌曲中数字1对应的音的频率是多高。平时唱歌时如果调子起高了,遇到音调更高的音我们就唱不上去。因为蜂鸣器在低音(低频率)效果不好,因此需要控制这个尽量高来保证比较好的结果。

#d 连音与乐谱

指音符之间过度平滑,没有停顿。给人读的音乐脚本。我们这里使用简谱。

2.3.2 程序对应

如果看这块吃力,建议去看我之前的一篇博客, 用Matlab演奏《错位时空》

#e 乐谱 连音与乐谱

以下是faded的简谱对应,虽然很抽象。由我来说明一下,一行代表一小节,简谱的一行用三行空格隔开。里面每三个int8_t对应一个音符,或者一个控制字符,音符的第一个int8_t为 0-7,控制字符大于7。第二个int8_t对应的是高几个八度,第三个表示时间长短。

int8_t music_script_faded[] = {
	1,ndot,nbar, 1,ndot,nbar, 1,ndot,nbar, 3,ndot,nbar, sep,0,0,

	6,ndot,nbar, 6,ndot,nbar, 6,ndot,nbar, 5,ndot,nbar, sep,0,0,

	3,ndot,nbar, 3,ndot,nbar, 3,ndot,nbar, 3,ndot,nbar, sep,0,0,

	7,ddot,nbar, 7,ddot,nbar, 7,ddot,nbar, 6,ddot,nbar, sep,0,0,



	0,ndot,bar, 1,ndot,bar, 1,ndot,bar, 6,ddot,bar,

	1,ndot,bar, conn,0,0, 6,ddot,bar, 1,ndot,bar, 2,ndot,bar, sep,0,0, // You were

	3,ndot,nbar, 1,ndot,bar, 1,ndot,bar, 5,ddot,bar, 3,ndot,nbar, 3,ndot,bar, sep,0,0,

	0,ndot,nbar, 0,ndot,nbar, 0,ndot,bar, 1,ndot,bar, conn,0,0, 1,ndot,nbar, sep,0,0,
};

可以看到控制音符如下,用来实现连音和变调功能。

enum note_ctrl {
	sep = 8, // 每小节的分隔符
	con = 9, // 用来代替每小节的分隔符,如果出现连音
	conn = 10, // 用于小节内的连音,connect note
	modus_change = 11, // 用于改变调, 紧接着的用于表示新的调子
};

#c 说明 简谱对应

这个和简谱对应,ddot对应down dot(音符下面的点),是低一个八度;udot对应up dot(音符上面的点),是高一个八度;ndot表示no dot,表示不高不低。 nbar对应下面没有横线,bar表示一个横线,音符持续时间变为1/2,也就是原来的4分音符变成8分音符;注意bar2是变为1/4而不是1/3。

// 表示八度,点
enum eig {
	ddot2 = -2,
	ddot = -1,
	ndot = 0,
	udot = 1,
	udot2 = 2
};
// 表示持续时间,横线
enum dur {
	nbar = 1,
	bar = 2,
	bar2 = 4,
	bar3 = 8
};

#e 调式

通过二维数组,每个元素由7个音,通过调号索引包含7个音的元素,再通过 7个音索引 索引base_freq_indexbase_freq_index为0起点(决定起的调子的高低),最终完成不同歌使用不同的调式。

enum e_modus {
	F_MAJOR = 0,
	F_SHARP_MAJOR,
	A_MAJOR,
	B_FLAT_MAJOR,
};

// -12 -11 -10 -9 -8 -7 -6 -5 -4 -3 -2 -1     0 1  2 3  4 5 6  7 8  9 10 11
// C   C#  D   D# E  F  F# G  G# A  A# B |||| C C# D D# E F F# G G# A A# B
uint16_t all_notes[] = {
	0x07c2, 0x0753, 0x06ea, 0x0685, 0x0628, 0x05cf, 0x057c, 0x052d, 0x04e3, 0x049d, 0x045a, 0x041c, //C0
	0x03e1, 0x03a9, 0x0375, 0x0343, 0x0314, 0x02e8, 0x02be, 0x0297, 0x0272, 0x024e, 0x022d, 0x020e, //C1
	0x01f0, 0x01d5, 0x01ba, 0x01a1, 0x018a, 0x0174, 0x015f, 0x014b, 0x0139, 0x0127, 0x0117, 0x0107, //C2
	0x00f8, 0x00ea, 0x00dd, 0x00d1, 0x00c5, 0x00ba, 0x00af, 0x00a6, 0x009c, 0x0094, 0x008b, 0x0083, //C3 
	0x007c, 0x0075, 0x006f, 0x0068, 0x0063, 0x005d, 0x0058, 0x0053, 0x004e, 0x004a, 0x0046, 0x0042, //C4 
	0x003e, 0x003b, 0x0037, 0x0034, 0x0031, 0x002e, 0x002c, 0x0029, 0x0027, 0x0025, 0x0023, 0x0021, //C5
	0x001f, 0x001d, 0x001c, 0x001a, 0x0019, 0x0017, 0x0016, 0x0015, 0x0014, 0x0012, 0x0011, 0x0010, //C6
};
int modus[][7] = {
	{ -7, -5, -3, -2, 0, 2, 4 },
	{ -6, -4, -2, -1, 1, 3, 5 },
	{ -3, -1, 1, 2, 4, 6, 8 },
	{ -2, 0, 2, 3, 5, 7, 9 }
};

#e 其他概念

例如下面的乐谱,可以看到由一个小节间的连音和2个音符连音,以及正常的音符和休止符。

6,ndot,nbar, 6,ndot,nbar, 5,ndot,bar, 2,ndot,bar, con,0,0,

2,ndot,bar, 1,ndot,bar, conn,0,0, 1,ndot,nbar, conn,0,0, 1,ndot,nbar, sep,0,0,

0,ndot,nbar, 0,ndot,nbar, 0,ndot,nbar, sep,0,0,

下面为演奏音符函数的实现,可以看到休止符是通过禁用PWM一定的时长,来完成;正常音符通过设置PWM频率(pwm_dset_freq函数)完成(all_notes为上2.2 读取Python程序生成的16进制数 的数组)。而连音通过检测下一个音符是否是连音控制音符,从而正常演奏间隔20ms(NOTE_SEP_TIME)还是禁用PWM 20ms来实现演奏间隔。起的调子高低通过 base_freq_index来设置音符的起始索引位置来实现。八度通过加数组索引 + 八度数*12来实现。

/*
i: 音符索引
*/
void play_note(uint32_t i, int one_beat_ms, int8_t* music, uint8_t base_freq_index, enum e_modus modus_type) {
	switch (music[i*3]) {
		// 音符间连接
		case conn: return; break;
		// 小节连接
		case con: return; break;
		case sep: break;
		case 0: // 休止符
			pwm_disable(PWMCH3);
			wait_ms((uint32_t)one_beat_ms - NOTE_SEP_TIME);
			break;
		default: // 1 2 3 4 5 6 7
			pwm_dset_freq(PWMCH3, all_notes\
			[ base_freq_index + (modus[modus_type][music[i*3] - 1]) + 12*music[i*3+1] ]);
			wait_ms((uint32_t) ( (one_beat_ms / music[i*3+2]) - NOTE_SEP_TIME ) ); //减去间隔
			break;
		}
		// 这里用来延时演奏间隔 如果遇见点或者横线
		if (music[(i+1)*3] == con || music[(i+1)*3] == conn) {
			wait_ms(NOTE_SEP_TIME);
		} else {
			// 正常音符的演奏间隔
			pwm_disable(PWMCH3);
			wait_ms(NOTE_SEP_TIME);
			pwm_enable(PWMCH3);
		}
}

2.4 音乐的播放和界面返回

#d 音乐播放与返回

通过一个for循环,循环音符数量次,每次循环都播放一个音符,来完成音乐播放,代码如下。通过每次循环捕捉按键按下,如果按下则结束循环,进入返回流程代码。

pwm_enable(PWMCH3);
for (uint32_t i = 0; i < note_num; i++) {
	// 返回
	if (key.pressed && (key_process(&key) == 0)) {
		if (key.key_pin == KEY_R || key.key_pin == KEY_Y) {
			key_reset(&key);
			key_enable(true);  
			break;
		}
		key_reset(&key);
		key_enable(true);  
	}
	// 变调
	if (music[i*3] == modus_change) {
		modus_type = music[(i*3)+1];
		continue;
	}
	play_note(i, one_beat_ms, music, base_freq_index, modus_type);
}

pwm_disable(PWMCH3);

管脚定义与分配

#c 定义 树莓派Pico管脚

![[rpi_pin.png]]

#c 定义 mp3管脚定义

// 第一列表示序号,无物理意义
// 第二行表示代码中使用的管脚号
|01 (GP00) -> OLED_RX 接受数据的管脚(用于SPI0)
|02 (GP01) -> OLED_CSn 片选(SPI0)
|03 (GND ) 
|04 (GP02) -> OLED_SCK (SPI0)                 
|05 (GP03) -> OLED_TX 发送数据 (SPI0)              
|06 (GP04) -> OLED_DC 数据:命令(用于普通GPIO口)             
|07 (GP05) -> OLED_RES reset(GPIO)   
|08 (GND )                         
|09 (GP06) -> PWM 播放声音 // 连接无源蜂鸣器,蜂鸣器另一端接地
|10 (GP07)
|11 (GP08)
|12 (GP09)
|13 (GND )
|14 (GP10)
|15 (GP11)
|16 (GP12)
|17 (GP13)
|18 (GND )
|19 (GP14)
|20 (GP15) -> LED 闪烁(GPIO)


// 下面逆时针旋转和上图对应
|21 (GP16) -> UART_TX (UART0)
|22 (GP17) -> UART_RX (UART0)
|23 (GND )
|24 (GP18)
|25 (GP19)
|26 (GP20)
|27 (GP21)
|28 (GND )
|29 (GP22)
|30 (RUN )
|31 (GP26)
|32 (GP27)
|33 (GND )
|34 (GP28)
|35
|36
|37
|38 (GND )
|39
|40