首頁 > 評(píng)測 > 第三篇-嵌入式系統(tǒng)音頻基本實(shí)踐-播放聲音之二

第三篇-嵌入式系統(tǒng)音頻基本實(shí)踐-播放聲音之二

  • 作者:zhanzr
  • 來源:21ic
  • [導(dǎo)讀]
  • Everyboard Can Sing

21ic打算攜手資(tu)深(ding)直男癌晚期工程師zhanzr21,來給大家講一講嵌入式系統(tǒng)與音頻處理的故事。

關(guān)于zhanzr21

曾經(jīng)混跡于兩岸三地,摸爬滾打在前端后端,搞過學(xué)術(shù)上過班,F(xiàn)在創(chuàng)業(yè)中,歡迎各種撩

點(diǎn)擊鏈接加入群【嵌入式音頻信號(hào)處理】:https://jq.qq.com/?_wv=1027&k=45wk8Ks

嵌入式音頻專用資料代碼分享:https://pan.baidu.com/s/1dFh5pWd

本期活動(dòng)地址:bbs.21ic.com/icview-1713672-1-1.html

TIM截圖20170428101100.jpg

1.說明

這一期繼續(xù)上次的話題,還是播放.因?yàn)樯洗尾シ艦榱苏f明原理,使用了非常原始的軟件結(jié)構(gòu),即使用定時(shí)器定時(shí)來更新DAC輸出.雖然說明了音頻播放的本質(zhì),但此種方法在實(shí)踐中很少被用到.原因相信善于思考的讀者早就猜到了.那就是對(duì)CPU的資源消耗很嚴(yán)重. 舉上次的8K采樣率為例子, CPU需要每125 us更新一次Sample. 這對(duì)于跑幾十幾百M(fèi)Hz的處理器來說不算什么. 但是通常嵌入式系統(tǒng)使用的20MHz左右的主頻率,假使中斷+更新操作100個(gè)指令周期完成(更新的數(shù)據(jù)源一般來自外接存儲(chǔ)器,已經(jīng)很保守估計(jì)). 那么此操作所占用的CPU資源:

Acpu = 100*0.05 / 125 = 4%

如果這個(gè)還算能接受的話, 那么加多一個(gè)通道,就是增加一倍. 采樣率增多為16KHz則又是一倍. 就算這個(gè)占用率可以接受, 這還只是原始音頻數(shù)據(jù)播放, 而實(shí)踐中經(jīng)常會(huì)使用某種編解碼算法以減輕存儲(chǔ)與傳輸?shù)膸拤毫? 即使使用更快的處理器可以負(fù)擔(dān)得起這種浪費(fèi),從能耗的角度來看也不傾向于使用這種結(jié)構(gòu).

話說回來, 嵌入式系統(tǒng)的特點(diǎn)就是沒有特定的規(guī)則, 如果簡單的方法能實(shí)現(xiàn)設(shè)計(jì)目的, 也不能說絕對(duì)否定這種方法. 本系列文章的目的就是發(fā)揚(yáng)探索精神, 將各種方法都來試驗(yàn)一把,品味其中優(yōu)劣, 學(xué)習(xí)諸種原理. 以下介紹幾種應(yīng)用了其他技術(shù)的播放實(shí)驗(yàn).

2.實(shí)踐之一(DAC+DMA)

2.1 雙緩沖播放

解決上文所敘的CPU資源占用問題簡單直接的方法就是DMA傳輸. 當(dāng)然DMA也不是每個(gè)處理器都有. 這里只是實(shí)驗(yàn)一下子有DMA的處理器如何將DMA利用起來.事實(shí)上幾乎所有專門處理音頻的處理器(DSP,或者音頻ASIC)都利用DMA來傳輸音頻數(shù)據(jù).

還是接上回的實(shí)驗(yàn), 直接使用上次所敘的DAC輸出的硬件結(jié)構(gòu),接耳機(jī),接音箱,接放大板都可以.

f722_nucleo_dac_earphone1.jpg

將軟件改成這樣的結(jié)構(gòu):

3.jpg

 

這個(gè)實(shí)驗(yàn)看起來簡單,其實(shí)內(nèi)容有點(diǎn)多.最主要的是Buffer管理. 下面簡單講講這個(gè)buffer的管理過程.

假設(shè)Buffer大小為BUF_SIZE,那么第一次需要從資源處填充BUF_SIZE的內(nèi)容到這個(gè)Buffer.開始DMA,這里注意這兩個(gè)回調(diào)函數(shù):

void HAL_DACEx_ConvCpltCallbackCh2(DAC_HandleTypeDef* hdac)

{

UpdatePointer = PLAY_BUFF_SIZE/2;

}

void HAL_DACEx_ConvHalfCpltCallbackCh2(DAC_HandleTypeDef* hdac)

{

UpdatePointer = 0;

}

分別是DMA傳輸完成與DMA傳輸完一半的回調(diào)函數(shù).需要用戶實(shí)現(xiàn),如果用戶不實(shí)現(xiàn),將使用默認(rèn)的HAL自帶的回調(diào)函數(shù):

__weak void HAL_DACEx_ConvCpltCallbackCh2(DAC_HandleTypeDef* hdac)

{

/* Prevent unused argument(s) compilation warning */

UNUSED(hdac);

/* NOTE : This function Should not be modified, when the callback is needed,

the HAL_DACEx_ConvCpltCallbackCh2 could be implemented in the user file

*/

}

__weak void HAL_DACEx_ConvHalfCpltCallbackCh2(DAC_HandleTypeDef* hdac)

{

/* Prevent unused argument(s) compilation warning */

UNUSED(hdac);

/* NOTE : This function Should not be modified, when the callback is needed,

the HAL_DACEx_ConvHalfCpltCallbackCh2 could be implemented in the user file

*/

}

看到前面那個(gè)__weak關(guān)鍵字沒有,有__weak修飾表明這函數(shù)可以被重載,或者覆蓋,或者隱藏. 這里也不知道該用什么術(shù)語,對(duì)C++或者其他面向?qū)ο笳Z言有了解的同學(xué)應(yīng)該一下子就能明白, 這里不節(jié)外生枝以后有時(shí)間再展開來說.

首先初始化buffer的時(shí)候,將buffer填充了第一次要播放的內(nèi)容.

4.jpg

在T1時(shí)刻開始播放(就是DMA傳輸), 這時(shí)候主循環(huán)可以處理其他事務(wù),只是定期檢查一下子上面所說的Flag即可.如果BUF_SIZE設(shè)定的足夠大,檢查Flag的期限可以很長.

T2時(shí)刻發(fā)生了傳輸完成一半的事件,因此半傳輸回調(diào)函數(shù)被調(diào)用.通過檢查Flag主循環(huán)知道了之后取數(shù)據(jù)填充在Buffer的前一半也就是綠色的那一半.

過一陣子在T3時(shí)刻又發(fā)生了傳輸完成事件,全傳輸回調(diào)函數(shù)被調(diào)用. 通過檢查Flag主循環(huán)知道之后取數(shù)據(jù)填充在Buffer的后一半也就是紅色的那一半.

如此循環(huán)下去播放每個(gè)Buffer期間軟件只需要干預(yù)兩次(比如把BUF_SIZE設(shè)定為可以裝播放0.5秒的數(shù)據(jù),則軟件干預(yù)的間隔為250ms,如果沒有很多其他任務(wù),CPU完全可以在此期間Sleep,當(dāng)然只有CPU能Sleep,其他外設(shè)還得干活.).

這種更新數(shù)據(jù)的方式稱之為雙緩沖方式.不僅僅是音頻應(yīng)用,很多類似的場合都能用上.

那么說DMA傳輸數(shù)據(jù)到DAC,采樣率怎樣控制呢? 答案是將DAC設(shè)為定時(shí)器更新觸發(fā).再將定時(shí)器的重裝頻率設(shè)置為想要的采樣率即可.STM32系列的處理器中定時(shí)器6與定時(shí)器7是專為此功能設(shè)計(jì)的(這兩個(gè)定時(shí)器當(dāng)然也能當(dāng)通用的定時(shí)器來用).

2.2 wav格式

這一期的實(shí)驗(yàn)燒寫文件還是與上一集一樣子.只是這次我直接燒寫wav文件而不是原始的raw文件.所以這里簡單的介紹一下子wave格式以理解實(shí)驗(yàn)代碼.wave格式以后還要詳細(xì)一點(diǎn)介紹.

簡單的說wav文件就是一種容器格式RIFF的實(shí)例化.RIFF文件可以裝各種數(shù)據(jù),只是為人們熟知的就是wav文件了.我們這個(gè)實(shí)驗(yàn)中從文件頭跳過44個(gè)字節(jié)就是原始音頻數(shù)據(jù)了.

WAV文件 = WAV頭 + 原始音頻

這里給個(gè)簡單的wav頭的結(jié)構(gòu):

struct zWavHdr

{

FOURCC RiffHdr; //"RIFF"

uint32_t ChunkSize; //file size - 8

FOURCC WavHdr;//"WAVE"

FOURCC FmtHdr; //"fmt "

uint32_t HdrLen; //16, length of above

uint16_t DataType; //1->PCM

uint16_t ChanNo; //1 Channel

uint32_t SampleRate;//8000 Hz

uint32_t SamplePerSec; //8000 sample per second

uint16_t BytePerSample;//Bytes per sample

uint16_t BitsPerSample;//Bits per sample

FOURCC dataHdr;//"data"

uint32_t RawSize;//data size from this point

};

記得某大公司某年的筆試題目給出類似這樣的一個(gè)結(jié)構(gòu), 對(duì)應(yīng)試者進(jìn)行提問. 所以有志于以后進(jìn)大公司的同學(xué)們也可以將此結(jié)構(gòu)體當(dāng)做一個(gè)練習(xí),理解一下子這樣設(shè)計(jì)結(jié)構(gòu)體的思路以后興許用得上. 當(dāng)然我這里給出的只是我自己進(jìn)行簡單應(yīng)用的代碼, 命名注釋什么沒怎么細(xì)究規(guī)范. 真正感興趣的同學(xué)請(qǐng)自行找該公司官方發(fā)布的wav頭實(shí)現(xiàn)代碼進(jìn)行學(xué)習(xí). 不那么關(guān)心的同學(xué)可以今后看本連載了解, 因?yàn)橐院筮會(huì)講到這個(gè)wav文件的.

3.實(shí)踐之二(PWM+LPF)

上次寫了文章后, 有人就說還有好多處理器沒DAC呢,怎么辦.

別擔(dān)心,這里介紹一種所有處理器都能用的播放音頻的方式.就是PWM+LPF的方式.這里L(fēng)PF指的是低通濾波器.

5.jpg

事實(shí)上不只是PWM,PDM也能實(shí)現(xiàn).就算沒有PWM,PDM這樣的外設(shè),自己計(jì)算指令周期從而通過IO口拉高拉低也能實(shí)現(xiàn)這功能.

關(guān)于濾波器,最簡單的一階RC濾波器就行.根據(jù)實(shí)驗(yàn)結(jié)果,不要濾波器直接接耳機(jī)或者8歐姆的揚(yáng)聲器也可以.此種情況是因?yàn)镻WM到揚(yáng)聲器的傳輸路徑上的分布電容電阻與揚(yáng)聲器本身的分布參數(shù)構(gòu)成了濾波器.但是不是每個(gè)揚(yáng)聲器都能構(gòu)成合適的濾波器.這種直接連接方式一來對(duì)IO口負(fù)載過大, 多少有點(diǎn)風(fēng)險(xiǎn)(比DAC直接驅(qū)動(dòng)耳機(jī)的風(fēng)險(xiǎn)大,因?yàn)镻WM引腳輸出全高的瞬間對(duì)外輸出全部加在外部負(fù)載上, 不像DAC很少有滿幅度輸出的情況), 二來不利于說明原理, 所以這里還是不跳過LPF這個(gè)環(huán)節(jié).

現(xiàn)在回到PWM+LPF這種典型方式.這種方式的原理其實(shí)就是用PWM+LPF做了一個(gè)偽DAC而已.關(guān)鍵在于濾波器的設(shè)計(jì),將20KHz以上的頻率分量濾去.那么每個(gè)周期的占空比就和輸出的能量成線性比例關(guān)系了.

為了做這個(gè)實(shí)驗(yàn),作者專門做了個(gè)濾波器板子.有兩種濾波器實(shí)現(xiàn),分別是有源一階與無源二階RC兩種類型(其中無源的后面還是加了一級(jí)跟隨以便于驅(qū)動(dòng)耳機(jī)與揚(yáng)聲器).

電路圖如下:

6.jpg

上圖是有源的.

7.jpg

上圖是無源的.

這兩個(gè)濾波器經(jīng)過實(shí)驗(yàn),都可以用.下面一種效果稍為好一點(diǎn).可能跟我使用的運(yùn)放有點(diǎn)關(guān)系.因?yàn)檎檬诌厸]有Rail-to-Rail的運(yùn)放隨手拿了個(gè)運(yùn)放焊接上去了.另外上述各種參數(shù)都需要在實(shí)際使用中進(jìn)行調(diào)整,標(biāo)注的都是我計(jì)算出來的值.購買器件也沒有買高精度的,因?yàn)槎紝儆谀M范疇的這里不多講.

這是LPF板子的實(shí)物圖:

 

8.jpg

 

lpf2.jpg

資源文件還是用之前實(shí)驗(yàn)用剩下的8KHz,8bit的樣本. 這些都在本章的共享文件夾中可以找到.

為求多樣化,新鮮感, 后面的實(shí)驗(yàn)盡量用不同的板子來做. 這一節(jié)的實(shí)驗(yàn)使用Arduino Uno板子來實(shí)現(xiàn). 因?yàn)锳Tmega328的Flash大小為32KB, 所以在資源中截取3秒,也就是24KB的數(shù)據(jù)出來.

9.jpg

bin文件到C數(shù)組有很多種方法和現(xiàn)成工具,作者這里圖個(gè)快捷,使用HexEdit的一個(gè)菜單復(fù)制出來.

10.jpg

重要代碼如下:(完全代碼在共享文件夾中找)

#include "resource.h"

#define speakerPin 11

#define SAMPLE_RATE 8000

uint32_t sample_idx = 0;

bool update_flag = true;

// the setup function runs once when you press reset or power the board

void setup() {

pinMode(2, OUTPUT);

pinMode(speakerPin, OUTPUT);

// 這里要修改PWM引腳的頻率,因?yàn)锳rduino的默認(rèn)PWM輸出使用了64分頻,導(dǎo)致要使用的IO口最多只能輸出幾百Hz的PWM出來.詳情請(qǐng)參閱完整代碼

// 讓定時(shí)器1每125us中斷一次,這里上節(jié)講過原理

}

ISR(TIMER1_COMPA_vect) {

sample_idx ++;

//翻轉(zhuǎn)IO口以測試實(shí)際的更新率

}

void loop() {

if (update_flag)

{

if (sample_idx == RSC_SIZE)

{

//循環(huán)

}

//設(shè)定輸出

update_flag = false;

}

}

代碼跟上一篇文章的定時(shí)器+DAC的例子基本一樣子,只是將DAC換成了PWM+LPF.下圖是實(shí)際的工作圖.

11.jpg

實(shí)驗(yàn)證明將信號(hào)轉(zhuǎn)接至音箱效果更好, 這是因?yàn)樾∫粝鋬?nèi)部也LPF的原因.

4.實(shí)踐之三(I2S輸出,以CS4344為例)

4.1 I2S信號(hào)連接

這個(gè)實(shí)驗(yàn)講述的應(yīng)該屬于最主流的音頻播放方式了. 就是通過I2S接口將數(shù)據(jù)傳輸給外部DAC. 使用外部音頻DAC的動(dòng)機(jī)大致有兩條:

1.如之前的文章所述,就音頻角度來講,外部音頻DAC的性能一般情況下都比內(nèi)部DAC要強(qiáng).ST與其類似處理器的DAC是被設(shè)計(jì)用來做測量控制類的應(yīng)用的.F4,F7這一類的中高端片子所帶的DAC也就只有12bit解析度. 而只要1美元或者之下的價(jià)格就能采購到16bit雙通道的I2S接口音頻DAC, 還自帶耳放. 比較過兩種DAC性能的讀者應(yīng)該有體會(huì).

2.通過I2S或者類似的接口將數(shù)據(jù)以數(shù)字形式發(fā)送給外部音頻DAC, 使得布線簡化.可以將對(duì)干擾敏感的音頻部分隔離開來. 包括對(duì)尺寸敏感的手機(jī)等移動(dòng)設(shè)備也有時(shí)采用外部音頻DAC. 這點(diǎn)還能成為宣傳的噱頭.比如至少有兩個(gè)國產(chǎn)手機(jī)都曾使用外部音頻DAC作為HIFI手機(jī)宣傳的賣點(diǎn).

感興趣的讀者可以瀏覽本人拍的視頻,對(duì)比兩種DAC的性能:

http://v.youku.com/v_show/id_XMjcwNDUyMDA1Mg==.html

首先看看實(shí)驗(yàn)圖片:

12.jpg

本節(jié)實(shí)驗(yàn)是用一塊本人自制的GD32F105RCT6的開發(fā)板子做演示. GD32的片子與ST的同后綴基本兼容, 除了某些細(xì)節(jié)比如HSE啟動(dòng)等等. 相關(guān)改動(dòng)詳情可以參閱共享文件夾中的代碼.

由于篇幅問題,此處也不對(duì)此板子進(jìn)行過多闡述.只是介紹一下I2S部分的信號(hào)接口, 其余部分將在今后的文章中介紹.

13.jpg

這四根腳連接到外部I2S的DAC即可.

14.jpg

四根線定義分別如下:

MCK:主時(shí)鐘 = Fsample * SampleDepth * ChannelNum * 某個(gè)系數(shù),此系數(shù)跟當(dāng)前采樣率相關(guān).

CK: Bit時(shí)鐘 = Fsample * SampleDepth * ChannelNum

SD: 數(shù)據(jù)線,相當(dāng)I2C中的SDA

WS: 左右時(shí)鐘 = Fsample

至于軟件流程則跟本篇文章第一個(gè)實(shí)驗(yàn)的流程一樣, 只是把DAC換成了I2S了.

關(guān)于I2S信號(hào)接口問題,以后還會(huì)講到.

4.2 下載資源到Flash中

這一節(jié)的實(shí)驗(yàn)通過插件將音頻數(shù)據(jù)下載到Flash中去. 在共享文件夾中找到"F105_AudioT2_25Q128.FLM"這個(gè)文件, 放在你安裝的Keil的目錄:

15.jpg

將資源文件定位于0xC0000000開始的虛擬位置,

#ifndef __SPI_AUDIO_INFO_H__

#define __SPI_AUDIO_INFO_H__

#include

#define E_FLASH_START_ADD 0xC0000000

//16bit 2ch 8K

#define AUDIO_SEC_1 0x00000000

#define SEC_1_SIZE 128000

//16bit 2ch 8K

#define AUDIO_SEC_2 (AUDIO_SEC_1+SEC_1_SIZE)

#define SEC_2_SIZE 485032

//8bit 1ch 8K

#define AUDIO_SEC_3 (AUDIO_SEC_2+SEC_2_SIZE)

#define SEC_3_SIZE 125548

#endif

...

#include "spi_audio_info.h"

static

const uint8_t raw_audio_16bit2ch8k_2[] __attribute__((at(E_FLASH_START_ADD + AUDIO_SEC_2))) = {

0x1F, 0x00, 0x1F, 0xFB, 0x54, 0xFF, 0xEA, 0xF9, 0x92, 0x00, 0xA7, 0xFB, 0x55, 0x00, 0x38, 0xFB,

....

};

選擇Flash類型:

16.jpg

點(diǎn)下載就可以將音頻數(shù)據(jù)下載到版上的Flash芯片中. 當(dāng)然這個(gè)Flash插件是為此F105開發(fā)板定制的. 以后會(huì)介紹如何寫這個(gè)插件.

更好的方法是通過USB與文件系統(tǒng)來下載音頻數(shù)據(jù). 或者下次做個(gè)能插TF卡的實(shí)驗(yàn)板子.

5.總結(jié)與后記

此篇補(bǔ)充了一些常用的播放音頻的方式與實(shí)驗(yàn).因?yàn)槠? 有些問題簡化帶過了. 期望今后講到具體情況時(shí)候再加以鋪陳展開. 讀者如果有疑問或者建議也可在文章后面留言. 暫時(shí)打住,下期見!

  • 本文系21ic原創(chuàng),未經(jīng)許可禁止轉(zhuǎn)載!

網(wǎng)友評(píng)論

  • 聯(lián)系人:巧克力娃娃
  • 郵箱:board@21ic.com
  • 我要投稿
  • 歡迎入駐,開放投稿

熱門標(biāo)簽
項(xiàng)目外包 more+