首頁 > 評測 > 第三篇-嵌入式系統(tǒng)音頻基本實踐-播放聲音之二
第三篇-嵌入式系統(tǒng)音頻基本實踐-播放聲音之二
- [導讀]
- Everyboard Can Sing
21ic打算攜手資(tu)深(ding)直男癌晚期工程師zhanzr21,來給大家講一講嵌入式系統(tǒng)與音頻處理的故事。
關于zhanzr21:
曾經(jīng)混跡于兩岸三地,摸爬滾打在前端后端,搞過學術上過班,F(xiàn)在創(chuàng)業(yè)中,歡迎各種撩
點擊鏈接加入群【嵌入式音頻信號處理】:https://jq.qq.com/?_wv=1027&k=45wk8Ks
嵌入式音頻專用資料代碼分享:https://pan.baidu.com/s/1dFh5pWd
本期活動地址:bbs.21ic.com/icview-1713672-1-1.html
1.說明
這一期繼續(xù)上次的話題,還是播放.因為上次播放為了說明原理,使用了非常原始的軟件結構,即使用定時器定時來更新DAC輸出.雖然說明了音頻播放的本質,但此種方法在實踐中很少被用到.原因相信善于思考的讀者早就猜到了.那就是對CPU的資源消耗很嚴重. 舉上次的8K采樣率為例子, CPU需要每125 us更新一次Sample. 這對于跑幾十幾百MHz的處理器來說不算什么. 但是通常嵌入式系統(tǒng)使用的20MHz左右的主頻率,假使中斷+更新操作100個指令周期完成(更新的數(shù)據(jù)源一般來自外接存儲器,已經(jīng)很保守估計). 那么此操作所占用的CPU資源:
Acpu = 100*0.05 / 125 = 4%
如果這個還算能接受的話, 那么加多一個通道,就是增加一倍. 采樣率增多為16KHz則又是一倍. 就算這個占用率可以接受, 這還只是原始音頻數(shù)據(jù)播放, 而實踐中經(jīng)常會使用某種編解碼算法以減輕存儲與傳輸?shù)膸拤毫? 即使使用更快的處理器可以負擔得起這種浪費,從能耗的角度來看也不傾向于使用這種結構.
話說回來, 嵌入式系統(tǒng)的特點就是沒有特定的規(guī)則, 如果簡單的方法能實現(xiàn)設計目的, 也不能說絕對否定這種方法. 本系列文章的目的就是發(fā)揚探索精神, 將各種方法都來試驗一把,品味其中優(yōu)劣, 學習諸種原理. 以下介紹幾種應用了其他技術的播放實驗.
2.實踐之一(DAC+DMA)
2.1 雙緩沖播放
解決上文所敘的CPU資源占用問題簡單直接的方法就是DMA傳輸. 當然DMA也不是每個處理器都有. 這里只是實驗一下子有DMA的處理器如何將DMA利用起來.事實上幾乎所有專門處理音頻的處理器(DSP,或者音頻ASIC)都利用DMA來傳輸音頻數(shù)據(jù).
還是接上回的實驗, 直接使用上次所敘的DAC輸出的硬件結構,接耳機,接音箱,接放大板都可以.

將軟件改成這樣的結構:

這個實驗看起來簡單,其實內容有點多.最主要的是Buffer管理. 下面簡單講講這個buffer的管理過程.
假設Buffer大小為BUF_SIZE,那么第一次需要從資源處填充BUF_SIZE的內容到這個Buffer.開始DMA,這里注意這兩個回調函數(shù):
void HAL_DACEx_ConvCpltCallbackCh2(DAC_HandleTypeDef* hdac)
{
UpdatePointer = PLAY_BUFF_SIZE/2;
}
void HAL_DACEx_ConvHalfCpltCallbackCh2(DAC_HandleTypeDef* hdac)
{
UpdatePointer = 0;
}
分別是DMA傳輸完成與DMA傳輸完一半的回調函數(shù).需要用戶實現(xiàn),如果用戶不實現(xiàn),將使用默認的HAL自帶的回調函數(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
*/
}
看到前面那個__weak關鍵字沒有,有__weak修飾表明這函數(shù)可以被重載,或者覆蓋,或者隱藏. 這里也不知道該用什么術語,對C++或者其他面向對象語言有了解的同學應該一下子就能明白, 這里不節(jié)外生枝以后有時間再展開來說.
首先初始化buffer的時候,將buffer填充了第一次要播放的內容.

在T1時刻開始播放(就是DMA傳輸), 這時候主循環(huán)可以處理其他事務,只是定期檢查一下子上面所說的Flag即可.如果BUF_SIZE設定的足夠大,檢查Flag的期限可以很長.
T2時刻發(fā)生了傳輸完成一半的事件,因此半傳輸回調函數(shù)被調用.通過檢查Flag主循環(huán)知道了之后取數(shù)據(jù)填充在Buffer的前一半也就是綠色的那一半.
過一陣子在T3時刻又發(fā)生了傳輸完成事件,全傳輸回調函數(shù)被調用. 通過檢查Flag主循環(huán)知道之后取數(shù)據(jù)填充在Buffer的后一半也就是紅色的那一半.
如此循環(huán)下去播放每個Buffer期間軟件只需要干預兩次(比如把BUF_SIZE設定為可以裝播放0.5秒的數(shù)據(jù),則軟件干預的間隔為250ms,如果沒有很多其他任務,CPU完全可以在此期間Sleep,當然只有CPU能Sleep,其他外設還得干活.).
這種更新數(shù)據(jù)的方式稱之為雙緩沖方式.不僅僅是音頻應用,很多類似的場合都能用上.
那么說DMA傳輸數(shù)據(jù)到DAC,采樣率怎樣控制呢? 答案是將DAC設為定時器更新觸發(fā).再將定時器的重裝頻率設置為想要的采樣率即可.STM32系列的處理器中定時器6與定時器7是專為此功能設計的(這兩個定時器當然也能當通用的定時器來用).
2.2 wav格式
這一期的實驗燒寫文件還是與上一集一樣子.只是這次我直接燒寫wav文件而不是原始的raw文件.所以這里簡單的介紹一下子wave格式以理解實驗代碼.wave格式以后還要詳細一點介紹.
簡單的說wav文件就是一種容器格式RIFF的實例化.RIFF文件可以裝各種數(shù)據(jù),只是為人們熟知的就是wav文件了.我們這個實驗中從文件頭跳過44個字節(jié)就是原始音頻數(shù)據(jù)了.
WAV文件 = WAV頭 + 原始音頻
這里給個簡單的wav頭的結構:
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
};
記得某大公司某年的筆試題目給出類似這樣的一個結構, 對應試者進行提問. 所以有志于以后進大公司的同學們也可以將此結構體當做一個練習,理解一下子這樣設計結構體的思路以后興許用得上. 當然我這里給出的只是我自己進行簡單應用的代碼, 命名注釋什么沒怎么細究規(guī)范. 真正感興趣的同學請自行找該公司官方發(fā)布的wav頭實現(xiàn)代碼進行學習. 不那么關心的同學可以今后看本連載了解, 因為以后還會講到這個wav文件的.
3.實踐之二(PWM+LPF)
上次寫了文章后, 有人就說還有好多處理器沒DAC呢,怎么辦.
別擔心,這里介紹一種所有處理器都能用的播放音頻的方式.就是PWM+LPF的方式.這里LPF指的是低通濾波器.

事實上不只是PWM,PDM也能實現(xiàn).就算沒有PWM,PDM這樣的外設,自己計算指令周期從而通過IO口拉高拉低也能實現(xiàn)這功能.
關于濾波器,最簡單的一階RC濾波器就行.根據(jù)實驗結果,不要濾波器直接接耳機或者8歐姆的揚聲器也可以.此種情況是因為PWM到揚聲器的傳輸路徑上的分布電容電阻與揚聲器本身的分布參數(shù)構成了濾波器.但是不是每個揚聲器都能構成合適的濾波器.這種直接連接方式一來對IO口負載過大, 多少有點風險(比DAC直接驅動耳機的風險大,因為PWM引腳輸出全高的瞬間對外輸出全部加在外部負載上, 不像DAC很少有滿幅度輸出的情況), 二來不利于說明原理, 所以這里還是不跳過LPF這個環(huán)節(jié).
現(xiàn)在回到PWM+LPF這種典型方式.這種方式的原理其實就是用PWM+LPF做了一個偽DAC而已.關鍵在于濾波器的設計,將20KHz以上的頻率分量濾去.那么每個周期的占空比就和輸出的能量成線性比例關系了.
為了做這個實驗,作者專門做了個濾波器板子.有兩種濾波器實現(xiàn),分別是有源一階與無源二階RC兩種類型(其中無源的后面還是加了一級跟隨以便于驅動耳機與揚聲器).
電路圖如下:

上圖是有源的.

上圖是無源的.
這兩個濾波器經(jīng)過實驗,都可以用.下面一種效果稍為好一點.可能跟我使用的運放有點關系.因為正好手邊沒有Rail-to-Rail的運放隨手拿了個運放焊接上去了.另外上述各種參數(shù)都需要在實際使用中進行調整,標注的都是我計算出來的值.購買器件也沒有買高精度的,因為都屬于模擬范疇的這里不多講.
這是LPF板子的實物圖:

資源文件還是用之前實驗用剩下的8KHz,8bit的樣本. 這些都在本章的共享文件夾中可以找到.
為求多樣化,新鮮感, 后面的實驗盡量用不同的板子來做. 這一節(jié)的實驗使用Arduino Uno板子來實現(xiàn). 因為ATmega328的Flash大小為32KB, 所以在資源中截取3秒,也就是24KB的數(shù)據(jù)出來.

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

重要代碼如下:(完全代碼在共享文件夾中找)
#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引腳的頻率,因為Arduino的默認PWM輸出使用了64分頻,導致要使用的IO口最多只能輸出幾百Hz的PWM出來.詳情請參閱完整代碼
// 讓定時器1每125us中斷一次,這里上節(jié)講過原理
}
ISR(TIMER1_COMPA_vect) {
sample_idx ++;
//翻轉IO口以測試實際的更新率
}
void loop() {
if (update_flag)
{
if (sample_idx == RSC_SIZE)
{
//循環(huán)
}
//設定輸出
update_flag = false;
}
}
代碼跟上一篇文章的定時器+DAC的例子基本一樣子,只是將DAC換成了PWM+LPF.下圖是實際的工作圖.

實驗證明將信號轉接至音箱效果更好, 這是因為小音箱內部也LPF的原因.
4.實踐之三(I2S輸出,以CS4344為例)
4.1 I2S信號連接
這個實驗講述的應該屬于最主流的音頻播放方式了. 就是通過I2S接口將數(shù)據(jù)傳輸給外部DAC. 使用外部音頻DAC的動機大致有兩條:
1.如之前的文章所述,就音頻角度來講,外部音頻DAC的性能一般情況下都比內部DAC要強.ST與其類似處理器的DAC是被設計用來做測量控制類的應用的.F4,F7這一類的中高端片子所帶的DAC也就只有12bit解析度. 而只要1美元或者之下的價格就能采購到16bit雙通道的I2S接口音頻DAC, 還自帶耳放. 比較過兩種DAC性能的讀者應該有體會.
2.通過I2S或者類似的接口將數(shù)據(jù)以數(shù)字形式發(fā)送給外部音頻DAC, 使得布線簡化.可以將對干擾敏感的音頻部分隔離開來. 包括對尺寸敏感的手機等移動設備也有時采用外部音頻DAC. 這點還能成為宣傳的噱頭.比如至少有兩個國產手機都曾使用外部音頻DAC作為HIFI手機宣傳的賣點.
感興趣的讀者可以瀏覽本人拍的視頻,對比兩種DAC的性能:
http://v.youku.com/v_show/id_XMjcwNDUyMDA1Mg==.html
首先看看實驗圖片:

本節(jié)實驗是用一塊本人自制的GD32F105RCT6的開發(fā)板子做演示. GD32的片子與ST的同后綴基本兼容, 除了某些細節(jié)比如HSE啟動等等. 相關改動詳情可以參閱共享文件夾中的代碼.
由于篇幅問題,此處也不對此板子進行過多闡述.只是介紹一下I2S部分的信號接口, 其余部分將在今后的文章中介紹.
這四根腳連接到外部I2S的DAC即可.

四根線定義分別如下:
MCK:主時鐘 = Fsample * SampleDepth * ChannelNum * 某個系數(shù),此系數(shù)跟當前采樣率相關.
CK: Bit時鐘 = Fsample * SampleDepth * ChannelNum
SD: 數(shù)據(jù)線,相當I2C中的SDA
WS: 左右時鐘 = Fsample
至于軟件流程則跟本篇文章第一個實驗的流程一樣, 只是把DAC換成了I2S了.
關于I2S信號接口問題,以后還會講到.
4.2 下載資源到Flash中
這一節(jié)的實驗通過插件將音頻數(shù)據(jù)下載到Flash中去. 在共享文件夾中找到"F105_AudioT2_25Q128.FLM"這個文件, 放在你安裝的Keil的目錄:

將資源文件定位于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類型:

點下載就可以將音頻數(shù)據(jù)下載到版上的Flash芯片中. 當然這個Flash插件是為此F105開發(fā)板定制的. 以后會介紹如何寫這個插件.
更好的方法是通過USB與文件系統(tǒng)來下載音頻數(shù)據(jù). 或者下次做個能插TF卡的實驗板子.
5.總結與后記
此篇補充了一些常用的播放音頻的方式與實驗.因為篇幅原因, 有些問題簡化帶過了. 期望今后講到具體情況時候再加以鋪陳展開. 讀者如果有疑問或者建議也可在文章后面留言. 暫時打住,下期見!
-
- 本文系21ic原創(chuàng),未經(jīng)許可禁止轉載!
網(wǎng)友評論
- 聯(lián)系人:巧克力娃娃
- 郵箱:board@21ic.com
- 我要投稿
-
歡迎入駐,開放投稿
-
人均百萬?英偉達中國員工收入曝光! 2024-08-29
-
《黑神話:悟空》玩家硬盤升級攻略:提升游戲體驗,暢享3A大作 2024-08-29
-
數(shù)睿數(shù)據(jù)參加《系統(tǒng)與軟件工程 低代碼開發(fā)平臺通用技術要求》國家標準編制 2024-08-29
- NRF52810藍牙數(shù)字耳機找人定制
預算:¥30005天前
- 125KW模塊式PCS軟硬件外包開發(fā)
預算:¥1100000015小時前
- 12V汽車啟動電源項目BMS設計
預算:¥50000023小時前
- 數(shù)據(jù)可視化軟件 開發(fā)
預算:¥5000023小時前
- PLC項目調試修改
預算:¥100001天前
- 起動電機控制器開發(fā)
預算:¥1100001天前