一文搞懂如何通过SPI+PWM模拟I2S

2023-12-11 13:28
文章标签 模拟 一文 spi 搞懂 pwm i2s

本文主要是介绍一文搞懂如何通过SPI+PWM模拟I2S,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

        I2S是一种数字音频接口标准,全称为Inter-IC Sound。它是一种串行接口,主要用于音频传输。但是由于有一些MCU可能没有I2S外设,然后你还需要用I2S进行语音播放,这个时候你就要用到本文的内容了。


一、什么是I2S?

        I2S是一种数字音频接口标准,全称为Inter-IC Sound。它是一种串行接口,主要用于连接数字音频处理器(例如MCU等)与数字音频器件(例如音频解码器、放大器等)之间的数据传输。I2S接口支持高质量音频传输,采用标准化的数据格式,可以在不同的音频设备之间实现互操作性。I2S接口在多种音频应用中广泛应用,例如智能音频系统、数码音乐播放器、智能家居音箱等。

        I2S一般有四种不同音频标准,包括 I2S Philips 标准、MSB 和 LSB 对齐标准,以及 PCM 标准。下面开始简单介绍下这四种协议:

(1)I2S Philips 标准

        I2S Philips 标准使用 WS 信号来指示当前正在发送的数据所属的通道。该信号从当前通道数据的第一个位(MSB) 之前的一个时钟开始有效,发送方在时钟信号 (CK) 的下降沿改变数据,接收方在上升沿读取数据。 WS 信号也在 CK 的下降沿变化。值得注意的是下图红框中,真正的数据是WS拉低后延迟一个时钟才开始发送的。

(2)MSB 对齐标准

        MSB 对齐标准同时生成 WS 信号和第一个数据位(即 MSBit)

(3)LSB 对齐标准

        LSB 对齐标准与 MSB 对齐标准类似(对于 16 位和 32 位全精度帧格式,没有任何不同)

(4)PCM 标准

        对于 PCM 标准,无需使用通道信息。有两种 PCM 模式(短帧和长帧),对于长帧同步,在主模式下会将 WS 信号持续 13 个周期。对于短帧同步, WS 同步信号的持续时间仅为一个周期。

        看完了上面的四种协议,对于SPI比较熟悉的人估计已经知道了,I2S的MSB和LSB协议和SPI非常相像,甚至STM32的部分MCU把SPI和I2S做成了一个外设,通过寄存器来选择功能,接下来讲解如何通过SPI+PWM的方式模拟I2S。


二、通过SPI+PWM模拟I2S

        首先我们再来看下I2S MSB协议,可以看到协议有三个波形,分别为CLK、WS、DATA,数据的发送方要在时钟信号 (CLK) 的下降沿改变数据,接收方在上升沿读取数据。 WS 信号也在 CLK 的下降沿变化。

        下图是SPI的MISO和CLK的波形,发送方的数据也是在CLK下降沿改变,接受方在数据的上升沿读取数据,这种方式和I2S的CLK和DATA的信号一模一样,因此我们就有了I2S的CLK引脚和DATA引脚的波形。

        那么还差一个WS的信号,这个WS的频率是根据CLK的频率计算得来的,也就是

                                            Freq(WS) = Freq(CLK) / 32(I2S的位数X2)

        所以,只要WS的信号和CLK的信号能够一起产生,且WS的频率是CLK频率的三十二分之一(假定I2S位数为16,而一个WS周期包含了左右声道两个16bit数据),这样就可以了。

        因此,我们需要两个条件,一个是CLK和WS的信号一起产生且CLK和DATA的信号也要一起产生;而SPI主机模式下,CLK和DATA在SPI使能之后的一段时间内才能有信号,这个信号的延时是不定的,这个跟MCU的外设设计以及编译器的优化程度有关。因为我们没有办法让CLK和WS一起产生。

        那么如何让CLK、WS、DATA一起产生呢?那就是使用SPI的从机发送模式,SPI的从机发送模式会在收到CLK之后发送自己的数据,且是同步产生的。

        有的MCU在SPI从机模式下要求设置必须CS引脚,如果是这样的话,那就随便定义一个CS引脚,然后把CS引脚拉低就好了

        那么,我们就只需要在MCU的外部产生两个PWM,一个作为I2S的CLK信号且连接到SPI的SPI_CLK上,另一个作为I2S的WS信号,然后SPI的SPI_DATA作为I2S_DATA信号进行输出。且让两个PWM同时输出,这样就实现了CLK、WS、DATA同时产生的目的。如下图:


三、实机测试

接下来我在亮牛半导体的LN882H WIFI芯片上测试下SPI+PWM模拟I2S。

首先定义I2S的引脚:

	/* I2S引脚接法:	CK 		-> 	A8和B5WS 		-> 	A2DATA 	-> 	B8*/hal_gpio_pin_afio_select(GPIOB_BASE,GPIO_PIN_5,SPI0_CLK);           //SPI的CLKhal_gpio_pin_afio_select(GPIOB_BASE,GPIO_PIN_8,SPI0_MISO);          //SPI的MISOhal_gpio_pin_afio_select(GPIOA_BASE,GPIO_PIN_2,ADV_TIMER_PWM0);     //PWM通道0hal_gpio_pin_afio_select(GPIOA_BASE,GPIO_PIN_8,ADV_TIMER_PWM2);     //PWM通道2hal_gpio_pin_afio_en(GPIOB_BASE,GPIO_PIN_5,HAL_ENABLE);hal_gpio_pin_afio_en(GPIOB_BASE,GPIO_PIN_8,HAL_ENABLE);hal_gpio_pin_afio_en(GPIOA_BASE,GPIO_PIN_2,HAL_ENABLE);hal_gpio_pin_afio_en(GPIOA_BASE,GPIO_PIN_8,HAL_ENABLE);

然后初始化SPI从机模式:

    /* 2. 配置SPI */spi_init_type_def spi_init;memset(&spi_init,0,sizeof(spi_init));spi_init.spi_baud_rate_prescaler = SPI_BAUDRATEPRESCALER_2;       //设置波特率(从机此操作无影响)spi_init.spi_mode      = SPI_MODE_SLAVE;                          //设置从模式spi_init.spi_data_size = SPI_DATASIZE_16B;                        //设置数据16bit宽度spi_init.spi_first_bit = SPI_FIRST_BIT_MSB;                       //设置帧格式为MSBspi_init.spi_cpol      = SPI_CPOL_LOW;                            //设置时钟极性0spi_init.spi_cpha      = SPI_CPHA_1EDGE;                          //设置时钟相位0hal_spi_init(SPI0_BASE,&spi_init);                                //初始化SPIhal_spi_en(SPI0_BASE,HAL_DISABLE);                                //SPI使能hal_spi_ssoe_en(SPI0_BASE,HAL_DISABLE);                           //关闭CS OUTPUT/* 初始化SPI的发送DMA */dma_init_t_def dma_init;memset(&dma_init,0,sizeof(dma_init));dma_init.dma_mem_addr = (uint32_t)song_temp;    //设置DMA内存地址(设置音频数据地址)dma_init.dma_data_num = 0;                      //设置DMA传输次数dma_init.dma_dir = DMA_READ_FORM_MEM;           //设置DMA方向dma_init.dma_mem_inc_en = DMA_MEM_INC_EN;       //设置DMA内存是否增长dma_init.dma_p_addr = SPI0_DATA_REG;            //设置DMA外设地址dma_init.dma_mem_size = DMA_MEM_SIZE_16_BIT;dma_init.dma_p_size   = DMA_P_SIZE_16_BIT;hal_dma_init(DMA_CH_4,&dma_init);               //初始化DMAhal_dma_en(DMA_CH_4,HAL_DISABLE);               //DMA使能

再设置两个独立通道的PWM,PWM0为8K,PWM1为8*16*2K,由于LN882H APB总线为40M,因此无法输出很准的8*16*2K,只能输出一个近似值256410Hz(目标是256000Hz),为了让WS和CLK匹配,只能让实际输出的CLK除以32,这就是WS的实际频率了。(这样做的结果就是实际8K采样的音频在播放的时候可能会有一点差异)。

    adv_tim_init_t_def adv_tim_init;memset(&adv_tim_init,0,sizeof(adv_tim_init));adv_tim_init.adv_tim_clk_div     = 0;                           //设置时钟分频,0为不分频adv_tim_init.adv_tim_load_value  = 4992 - 2;                    //设置PWM频率为8Kadv_tim_init.adv_tim_cmp_a_value = 4992/2 ;                     //设置通道a比较值,占空比为 50%adv_tim_init.adv_tim_cmp_b_value = 4992/2 ;                     //设置通道b比较值,占空比为 50%adv_tim_init.adv_tim_cnt_mode    = ADV_TIMER_CNT_MODE_INC;      //向上计数模式adv_tim_init.adv_tim_cha_en      = ADV_TIMER_CHA_EN;            //使能通道ahal_adv_tim_init(ADV_TIMER_0_BASE,&adv_tim_init);               //初始化TIMER0hal_adv_tim_a_en(ADV_TIMER_0_BASE,HAL_DISABLE);                 //先失能TIMER0memset(&adv_tim_init,0,sizeof(adv_tim_init));adv_tim_init.adv_tim_clk_div     = 0;                           //设置时钟分频,0为不分频adv_tim_init.adv_tim_load_value  = 156 - 2;                     //设置PWM频率为8*16*2Kadv_tim_init.adv_tim_cmp_a_value = 156/2 ;                      //设置通道a比较值,占空比为 50%adv_tim_init.adv_tim_cmp_b_value = 156/2 ;                      //设置通道b比较值,占空比为 50%adv_tim_init.adv_tim_cnt_mode    = ADV_TIMER_CNT_MODE_INC;      //向上计数模式adv_tim_init.adv_tim_cha_en      = ADV_TIMER_CHA_EN;            //使能通道ahal_adv_tim_init(ADV_TIMER_1_BASE,&adv_tim_init);               //初始化ADV_TIMER1hal_adv_tim_a_en(ADV_TIMER_1_BASE,HAL_DISABLE);                 //先失能TIMER1

使能DMA,开始播放音频数据

//关闭DMA,重新配置音频传输数量
hal_spi_en(SPI0_BASE,HAL_DISABLE); 
hal_adv_tim_a_en(ADV_TIMER_1_BASE,HAL_DISABLE);
hal_adv_tim_a_en(ADV_TIMER_0_BASE,HAL_DISABLE);
hal_dma_en(DMA_CH_4,HAL_DISABLE);
hal_dma_set_data_num(DMA_CH_4,30000);//开启DMA,开始播放音频
hal_dma_en(DMA_CH_4,HAL_ENABLE);
hal_adv_tim_a_en(ADV_TIMER_1_BASE,HAL_ENABLE);
hal_adv_tim_a_en(ADV_TIMER_0_BASE,HAL_ENABLE);
hal_spi_en(SPI0_BASE,HAL_ENABLE); //等待播放完成,然后关闭DMA,并延时
while(hal_dma_get_data_num(DMA_CH_4) != 0);
hal_adv_tim_a_en(ADV_TIMER_0_BASE,HAL_DISABLE);
hal_adv_tim_a_en(ADV_TIMER_1_BASE,HAL_DISABLE);
OS_MsDelay(1000); 

结果:

这篇关于一文搞懂如何通过SPI+PWM模拟I2S的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/480842

相关文章

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

usaco 1.2 Transformations(模拟)

我的做法就是一个一个情况枚举出来 注意计算公式: ( 变换后的矩阵记为C) 顺时针旋转90°:C[i] [j]=A[n-j-1] [i] (旋转180°和270° 可以多转几个九十度来推) 对称:C[i] [n-j-1]=A[i] [j] 代码有点长 。。。 /*ID: who jayLANG: C++TASK: transform*/#include<

【STM32】SPI通信-软件与硬件读写SPI

SPI通信-软件与硬件读写SPI 软件SPI一、SPI通信协议1、SPI通信2、硬件电路3、移位示意图4、SPI时序基本单元(1)开始通信和结束通信(2)模式0---用的最多(3)模式1(4)模式2(5)模式3 5、SPI时序(1)写使能(2)指定地址写(3)指定地址读 二、W25Q64模块介绍1、W25Q64简介2、硬件电路3、W25Q64框图4、Flash操作注意事项软件SPI读写W2

hdu4431麻将模拟

给13张牌。问增加哪些牌可以胡牌。 胡牌有以下几种情况: 1、一个对子 + 4组 3个相同的牌或者顺子。 2、7个不同的对子。 3、13幺 贪心的思想: 对于某张牌>=3个,先减去3个相同,再组合顺子。 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOExcepti

【每日一题】LeetCode 2181.合并零之间的节点(链表、模拟)

【每日一题】LeetCode 2181.合并零之间的节点(链表、模拟) 题目描述 给定一个链表,链表中的每个节点代表一个整数。链表中的整数由 0 分隔开,表示不同的区间。链表的开始和结束节点的值都为 0。任务是将每两个相邻的 0 之间的所有节点合并成一个节点,新节点的值为原区间内所有节点值的和。合并后,需要移除所有的 0,并返回修改后的链表头节点。 思路分析 初始化:创建一个虚拟头节点

每日一题|牛客竞赛|四舍五入|字符串+贪心+模拟

每日一题|四舍五入 四舍五入 心有猛虎,细嗅蔷薇。你好朋友,这里是锅巴的C\C++学习笔记,常言道,不积跬步无以至千里,希望有朝一日我们积累的滴水可以击穿顽石。 四舍五入 题目: 牛牛发明了一种新的四舍五入应用于整数,对个位四舍五入,规则如下 12345->12350 12399->12400 输入描述: 输入一个整数n(0<=n<=109 ) 输出描述: 输出一个整数

【算法专场】模拟(下)

目录 前言 38. 外观数列 算法分析 算法思路 算法代码 1419. 数青蛙 算法分析 算法思路 算法代码  2671. 频率跟踪器 算法分析 算法思路 算法代码 前言 在前面我们已经讲解了什么是模拟算法,这篇主要是讲解在leetcode上遇到的一些模拟题目~ 38. 外观数列 算法分析 这道题其实就是要将连续且相同的字符替换成字符重复的次数+

模拟实现vector中的常见接口

insert void insert(iterator pos, const T& x){if (_finish == _endofstorage){int n = pos - _start;size_t newcapacity = capacity() == 0 ? 2 : capacity() * 2;reserve(newcapacity);pos = _start + n;//防止迭代

PHP实现二叉树遍历(非递归方式,栈模拟实现)

二叉树定义是这样的:一棵非空的二叉树由根结点及左、右子树这三个基本部分组成,根据节点的访问位置不同有三种遍历方式: ① NLR:前序遍历(PreorderTraversal亦称(先序遍历)) ——访问结点的操作发生在遍历其左右子树之前。 ② LNR:中序遍历(InorderTraversal) ——访问结点的操作发生在遍历其左右子树之中(间)。 ③ LRN:后序遍历(PostorderT