本節學習目的 1)分析Linux中的OSS音效卡系統 2)移植wm9876音效卡 3)使用madplay應用程式播放mp3 1.聲音三要素 採樣頻率 音頻採樣率是指錄音設備在一秒鐘內對聲音信號的採樣次數, 常用的採樣率有: 8KHz - 電話所用採樣率, 對於人的說話已經足夠清除 22.05KHz - ...
本節學習目的
- 1)分析Linux中的OSS音效卡系統
- 2)移植wm9876音效卡
- 3)使用madplay應用程式播放mp3
1.聲音三要素
採樣頻率
音頻採樣率是指錄音設備在一秒鐘內對聲音信號的採樣次數, 常用的採樣率有:
- 8KHz - 電話所用採樣率, 對於人的說話已經足夠清除
- 22.05KHz - 無線電廣播所用採樣率
- 32KHz - miniDV 數位視頻、DAT所用採樣率
- 44.1KHz - 音頻 CD, 也常用於 MPEG-1 音頻(VCD, SVCD, MP3)所用採樣率
- 48KHz - miniDV、數字電視、DVD、DAT、電影和專業音頻所用的數字聲音所用採樣率
- 50KHz - 商用數字錄音機所用採樣率
- 96K - BD-ROM(藍光碟)音軌、和 HD-DVD (高清晰度 DVD)音軌等所用採樣率
而2440開發板的採樣頻率IISRCK最高可以達到96KHz,滿足了很多常用的採樣場合,如下圖所示:
量化位數
指每個採樣點里傳輸的數字信號次數,如下圖所示, 其中藍線表示模擬信號,紅線表示數字信號,量化位越高,數字信號就越可能接近原始信號,音質越好
一般的量化位數為:
- 8位: 分成 256 次;
- 16位: 分為65536次, 已到 CD 標準;
- 32位: 分為 4294967296次,很少用到
2440的開發板只支持8位,16位,如下圖所示:
其中LRCK就是採樣頻率,當LRCK為低時,表示傳輸的採樣數據是左聲道,當LRCK為高時,表示傳輸的採樣數據是右聲道,每個採樣點,SD(serial data)都可以傳輸8位,或16位數字信號(從低位到高位傳輸)
聲道數
常有單聲道和立體聲之分,(有的也處理成兩個喇叭輸出同一個聲道的聲音),而立體聲更能感受到空間效果,但數據量翻倍
所以,聲音的每秒數據量(位元組/s)= (採樣頻率 × 量化位數 × 聲道數) / 8;
2. WM9876音效卡硬體分析
音效卡是負責錄音、播音、調節音量和聲音合成等的一種多媒體板卡
本節使用的音效卡是2440板上自帶的WM9876音效卡
當我們播放聲音時 ,將數字信號傳入I2SDO腳,音效卡便通過解碼,產生模擬信號到喇叭/耳機
錄音時,音效卡便獲取麥克風的模擬信號,編碼出數字信號到I2SDI引腳上
WM8976介面分為兩種:I2S介面(提供音頻接收和發送)、控制介面(控制音量大小,使能各個輸出通道等)
IIS介面相關的引腳如下
- CDCLK : 為編解碼晶元提供系統同步時鐘 (系統時鐘)
- I2SSCLK: IIS控制器提供的串列時鐘信號 (位數據傳輸時鐘)
- I2SLRCK: 採樣頻率信號,當為低電平時是採樣的是左聲道信號,為高電平是右聲道信號
- I2SDI : ADC數據輸入
- I2SDO :DAC數據輸出
控制介面相關的引腳如下
- CSB/GPIO1: 3線 控制數據使能引腳
- SCLK: 3線/2線 時鐘引腳
- SDIN: 3線/2線 數據輸入輸出引腳
- MODE: 3線/2線 控制選擇,當MODE為高,表示為3線控制,MODE位低,表示2線控制,如下圖所示:
其它引腳如下:
- R/LOUT1:音頻左/右輸出通道1,外接耳機插孔
- R/LOUT2:音頻左/右輸出通道2,未接
- OUT3:單聲道輸出通道3,未接
- OUT4:單聲道輸出通道4,未接
- LIP/LIN:音頻輸入通道,外接麥克風
那麼3線和2線的控制引腳又有什麼區別?
3線控制:
如下圖所示,3線控制,每周期都要傳輸16位數據(7位寄存器地址+9位寄存器數據),傳輸完成後,給CSB一個上升沿便完成一次數據的傳輸
2線控制:
如下圖所示,2線控制就是I2C通信方式
本節的WM8976的MODE腳接的高電平,所以是3線控制
3.接下來便來分析linux內核的音效卡系統
在linux音效卡中存在兩種音效卡系統,一種是OSS(開放聲音系統),一種是ALSA(先
進Linux聲音架構)。本節系統以OSS(Open Sound System)為例 ,
內核以linux-2.6.22.6版本為例,位於:linux-2.6.22.6\sound\Sound_core.c
3.1首先進入入口函數
如下圖所示:
入口函數里,只註冊了一個主設備號為(SOUND_MAJOR)14的“sound”字元設備和class類,這裡為什麼沒有創建設備節點?
是因為, 當註冊音效卡系統的驅動後,才會有設備節點,此時這裡的代碼是沒有驅動的,後面會分析到
3.2 再來看看“sound”字元設備的file_perations:
這裡只有個.open,為什麼沒有.read,.write函數?
顯然在.open函數里做了某些處理,我們進入soundcore_open()來看看
3.2 soundcore_open()代碼如下:
int soundcore_open(struct inode *inode, struct file *file) { int chain; int unit = iminor(inode); //獲取次設備號,通過次設備號來找音效卡驅動 struct sound_unit *s; const struct file_operations *new_fops = NULL; //定義一個新的file_operations chain=unit&0x0F; if(chain==4 || chain==5) /* dsp/audio/dsp16 */ { unit&=0xF0; unit|=3; chain=3; } spin_lock(&sound_loader_lock); s = __look_for_unit(chain, unit); //裡面通過chains[chain]數組裡找到sound_unit結構體 //一個sound_unit對應一個音效卡驅動 if (s) new_fops = fops_get(s->unit_fops); //通過sound_unit,獲取對應的file_operations ... ... if (new_fops) { //當找到file_operations int err = 0; const struct file_operations *old_fops = file->f_op;//設上次的file_operations等於當前的 file->f_op = new_fops; //設置系統的file_operations等於s-> unit_fops spin_unlock(&sound_loader_lock); if(file->f_op->open) err = file->f_op->open(inode,file); if (err) { fops_put(file->f_op); file->f_op = fops_get(old_fops); } fops_put(old_fops); return err; } spin_unlock(&sound_loader_lock); return -ENODEV; }
通過上面的代碼和註釋分析到,系統音效卡之所以只有一個open(),裡面是通過次設備號來調用__look_for_unit()函數,找到chains[chain]數組裡的驅動音效卡sound_unit結構體,然後來替換系統音效卡的file_operations,實現偷天換日的效果。
__look_for_unit()函數如下圖所示:
其中chains[]數組定義如下所示:
其中, chains[0]存放的Mixers,實現調節音量,高音等,就是我們VM8976的控制介面
chains[3]存放的DSP,用來實現音頻輸入輸出,就是我們VM8976的I2S介面
顯然VM8976的驅動有2個,需要將2個file_operations放入chains[0]和chains[3]數組裡,供給系統的open()來調用
3.3 我們以DSP為例,搜索chains[3]來看看
如上圖所示,顯然register_sound_dsp()函數就是被我們音效卡驅動調用的,用來註冊dsp設備節點,繼續進入sound_insert_unit()函數看看
3.4 sound_insert_unit()函數如下
static int sound_insert_unit(struct sound_unit **list, const struct file_operations *fops, int index, int low, int top, const char *name, umode_t mode, struct device *dev) { struct sound_unit *s = kmalloc(sizeof(*s), GFP_KERNEL); //分配個新的sound_unit int r; if (!s) return -ENOMEM; spin_lock(&sound_loader_lock); // __sound_insert_unit()里主要實現:將分配的新的s插入到chains[3]里,然後並放入fops操作結構體 r = __sound_insert_unit(s, list, fops, index, low, top); spin_unlock(&sound_loader_lock); if (r < 0) goto fail; else if (r < SOUND_STEP) sprintf(s->name, "sound/%s", name); //s->name="sound/dsp" else sprintf(s->name, "sound/%s%d", name, r / SOUND_STEP); device_create(sound_class, dev, MKDEV(SOUND_MAJOR, s->unit_minor),s->name+6);
//s->name+6="dsp",也就是在/dev下創建"dsp"的設備節點 return r; fail: kfree(s); return r; }
所以,register_sound_dsp()函數用來創建/dev/dsp 設備節點,同時將dsp相關的file_operations放入chains[3]裡面
3.5 同樣, Mixers的驅動流程也是這樣,它的函數是register_sound_mixer(),如下圖所示
也是創建/dev/mixer設備節點, 同時將dsp相關的file_operations放入chains[0]裡面
3.6 接下來,我們便搜索register_sound_dsp()函數,看看被哪些音效卡驅動調用
如下圖所示,找到一個支持s3c24xx板卡的音效卡驅動uda1341
uda1341音效卡和WM8976音效卡非常相似,音頻都是I2S介面,就只有控制部分不一樣
uda1341音效卡的硬體,如下圖所示:
它的控制引腳只有3個:
L3MODE:模式引腳,為高表示傳輸的是數據,為低表示傳輸的是寄存器地址
L3CLOCK:時鐘引腳
L3DATA: 數據輸入/輸出引腳
控制介面的時序如下所示:
和WM8976的控制時序完全不一樣,WM8976控制時序如下所示:
所以接下來,便修改S3c2410-uda1341.c的控制部分,來移植為wm8976驅動
4.移植wm8976驅動
首先進入uda1341的probe函數
static int s3c2410iis_probe(struct device *dev) { struct platform_device *pdev = to_platform_device(dev); struct resource *res; unsigned long flags; printk ("s3c2410iis_probe...\n"); /*獲取資源*/ res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (res == NULL) { printk(KERN_INFO PFX "failed to get memory region resouce\n"); return -ENOENT; } iis_base = (void *)S3C24XX_VA_IIS ; if (iis_base == 0) { printk(KERN_INFO PFX "failed to ioremap() region\n"); return -EINVAL; } /*獲取I2S時鐘,並使能*/ iis_clock = clk_get(dev, "iis"); if (iis_clock == NULL) { printk(KERN_INFO PFX "failed to find clock source\n"); return -ENOENT; } clk_enable(iis_clock); /*進入臨界區, 禁止中斷,並保存中斷狀態*/ local_irq_save(flags); /*設置管腳功能*/ /* GPB 4: L3CLOCK, OUTPUT */ s3c2410_gpio_cfgpin(S3C2410_GPB4, S3C2410_GPB4_OUTP); s3c2410_gpio_pullup(S3C2410_GPB4,1); /* GPB 3: L3DATA, OUTPUT */ s3c2410_gpio_cfgpin(S3C2410_GPB3,S3C2410_GPB3_OUTP); /* GPB 2: L3MODE, OUTPUT */ s3c2410_gpio_cfgpin(S3C2410_GPB2,S3C2410_GPB2_OUTP); s3c2410_gpio_pullup(S3C2410_GPB2,1); /* GPE 3: I2SSDI */ s3c2410_gpio_cfgpin(S3C2410_GPE3,S3C2410_GPE3_I2SSDI); s3c2410_gpio_pullup(S3C2410_GPE3,0); /* GPE 0: I2SLRCK */ s3c2410_gpio_cfgpin(S3C2410_GPE0,S3C2410_GPE0_I2SLRCK); s3c2410_gpio_pullup(S3C2410_GPE0,0); /* GPE 1: I2SSCLK */ s3c2410_gpio_cfgpin(S3C2410_GPE1,S3C2410_GPE1_I2SSCLK); s3c2410_gpio_pullup(S3C2410_GPE1,0); /* GPE 2: CDCLK */ s3c2410_gpio_cfgpin(S3C2410_GPE2,S3C2410_GPE2_CDCLK); s3c2410_gpio_pullup(S3C2410_GPE2,0); /* GPE 4: I2SSDO */ s3c2410_gpio_cfgpin(S3C2410_GPE4,S3C2410_GPE4_I2SSDO); s3c2410_gpio_pullup(S3C2410_GPE4,0); /*退出臨界區,使能中斷,並恢復之前保存的flags中斷狀態*/ local_irq_restore(flags); /*設置2440的I2S寄存器*/ init_s3c2410_iis_bus(); /*初始化uda1341音效卡的控制部分*/ init_uda1341(); /*設置DMA輸出通道,用來接收聲音*/ output_stream.dma_ch = DMACH_I2S_OUT; if (audio_init_dma(&output_stream, "UDA1341 out")) { audio_clear_dma(&output_stream,&s3c2410iis_dma_out); printk( KERN_WARNING AUDIO_NAME_VERBOSE": unable to get DMA channels\n" ); return -EBUSY; } /*設置DMA輸入通道,用來接收聲音*/ input_stream.dma_ch = DMACH_I2S_IN; if (audio_init_dma(&input_stream, "UDA1341 in")) { audio_clear_dma(&input_stream,&s3c2410iis_dma_in); printk( KERN_WARNING AUDIO_NAME_VERBOSE": unable to get DMA channels\n" ); return -EBUSY; } /*創建/dev/dsp,/dev/mixer兩個設備節點,
並將smdk2410_audio_fops和smdk2410_mixer_fops 兩個file_operations放入chains[0]和chains[3]里,供給內核的音效卡系統調用*/ audio_dev_dsp = register_sound_dsp(&smdk2410_audio_fops, -1); audio_dev_mixer = register_sound_mixer(&smdk2410_mixer_fops, -1); printk(AUDIO_NAME_VERBOSE " initialized\n"); return 0; }
從上面的代碼來看, uda1341的管腳和wm8976的管腳連接都是一樣的,只有init_uda1341()不一樣,裡面是初始化uda1341的控制引腳介面,所以需要屏蔽,然後自己來寫個init_wm8976()函數
4.1寫init_wm8976()函數之前需要先寫一個寄存器操作函數
參考wm8976晶元手冊時序圖:
所以,代碼如下:
static void wm8976_write_reg(unsigned char reg, unsigned int data) { int i; unsigned long flags; //對於wm8976來說,數據的高七位表示寄存器地址,低9位表示寄存器的值 unsigned short val = (reg << 9) | (data & 0x1ff);
/*wm8976引腳csb,dat,clk分別對應2440晶元的GPB2,3,4引腳*/ s3c2410_gpio_setpin(S3C2410_GPB2,1); s3c2410_gpio_setpin(S3C2410_GPB3,1); s3c2410_gpio_setpin(S3C2410_GPB4,1); /*退出臨界區,使能中斷,並恢復之前保存的flags中斷狀態*/ local_irq_save(flags);
/*把val值寫入wm8976,共16位,從高到低傳輸*/ for (i = 0; i < 16; i++){ if (val & (1<<15)) { s3c2410_gpio_setpin(S3C2410_GPB4,0); s3c2410_gpio_setpin(S3C2410_GPB3,1); udelay(1); s3c2410_gpio_setpin(S3C2410_GPB4,1); } else { s3c2410_gpio_setpin(S3C2410_GPB4,0); s3c2410_gpio_setpin(S3C2410_GPB3,0); udelay(1); s3c2410_gpio_setpin(S3C2410_GPB4,1); } val = val << 1; } //傳輸完成,需要讓csb信號產生低脈衝,寫入wm8976 s3c2410_gpio_setpin(S3C2410_GPB2,0); udelay(1); //引腳恢復到高電平狀態 s3c2410_gpio_setpin(S3C2410_GPB2,1); s3c2410_gpio_setpin(S3C2410_GPB3,1); s3c2410_gpio_setpin(S3C2410_GPB4,1); local_irq_restore(flags); }
4.2.參考wm8976g.pdf第87頁,來初始化wm8976,使能輸出聲道1,2,混響器等
static void init_wm8976(void) { uda1341_volume = 57; // wm8976的音量預設值,後面會講到 uda1341_boost = 0; /* software reset */ wm8976_write_reg(0, 0); /* BIT[6-5]:使能音頻的輸出左右通道2 * BIT[3]: 使能mixer混音器的輸出右通道 * BIT[2]: 使能mixer混音器的輸出右通道 * BIT[1]: 使能DAC傳輸的右通道 * BIT[0]: 使能DAC傳輸的左通道 */ wm8976_write_reg(0x3, 0x6f); /* BIT[4]: 使能輸出麥克風電壓 */ wm8976_write_reg(0x1, 0x1f); wm8976_write_reg(0x2, 0x185);//ROUT1EN LOUT1EN, inpu PGA enable ,ADC enable wm8976_write_reg(0x6, 0x0);//SYSCLK=MCLK wm8976_write_reg(0x4, 0x10); // [4:3]=10:使用I2S介面傳輸 wm8976_write_reg(0x2B,0x10);//BTL OUTPUT wm8976_write_reg(0x9, 0x50);//Jack detect enable wm8976_write_reg(0xD, 0x21);//Jack detect wm8976_write_reg(0x7, 0x01);//Jack detect }
wm8976初始化修改完成後,還需要修改音量控制等函數,之前就分析了uda1341的probe函數,裡面會註冊dsp、mixer設備節點:
/dev/dsp
用來播發和錄音,由於uda1341和wm8976都用了I2S介面,所以dsp的file_operations不需要修改,
/dev/mixer
用來控制音量,調低音,高音等,由於wm8976的控制介面不一樣,所以需要修改mixer的file_operations->ioctl函數
4.3 mixer的file_operations->ioctl函數如下所示:
static int smdk2410_mixer_ioctl(struct inode *inode, struct file *file,unsigned int cmd, unsigned long arg) { int ret; long val = 0; switch (cmd) { case SOUND_MIXER_INFO: //CASE : 獲取音效卡的描述信息 { mixer_info info; strncpy(info.id, "UDA1341", sizeof(info.id)); strncpy(info.name,"Philips UDA1341", sizeof(info.name)); info.modify_counter = audio_mix_modcnt; return copy_to_user((void *)arg, &info, sizeof(info)); //上傳用戶層 } ... ... case SOUND_MIXER_WRITE_VOLUME: //CASE: 寫音量,音量值為0~99 ret = get_user(val, (long *) arg); //讀用戶層的數據,並放在val里 if (ret) return ret; uda1341_volume = 63 - (((val & 0xff) + 1) * 63) / 100; //轉換為寄存器音量值 uda1341_l3_address(UDA1341_REG_DATA0); //寫入音量的寄存器地址 uda1341_l3_data(uda1341_volume); //寫入轉換後的寄存器值數據 break; case SOUND_MIXER_READ_VOLUME: //CASE: 讀音量,音量值為0~100 val = ((63 - uda1341_volume) * 100) / 63; //將寄存器音量值轉換為原始數據 val |= val << 8; return put_user(val, (long *) arg); //上傳音量值 case SOUND_MIXER_READ_IGAIN: //CASE: 讀(in gain)混音輸入增益 val = ((31- mixer_igain) * 100) / 31; return put_user(val, (int *) arg);
case SOUND_MIXER_WRITE_IGAIN: //CASE: 寫(in gain)混音輸入增益 ret = get_user(val, (int *) arg); if (ret) return ret; mixer_igain = 31 - (val * 31 / 100); /* use mixer gain channel 1*/ uda1341_l3_address(UDA1341_REG_DATA0); uda1341_l3_data(EXTADDR(EXT0)); uda1341_l3_data(EXTDATA(EXT0_CH1_GAIN(mixer_igain))); break; default: DPRINTK("mixer ioctl %u unknown\n", cmd); return -ENOSYS; } return 0; }
從上面的代碼來看,顯然接下還要修改以下幾個與控制介面相關的case:
- case SOUND_MIXER_WRITE_VOLUME: //寫音量
- case SOUND_MIXER_READ_VOLUME: //讀音量
- case SOUND_MIXER_READ_IGAIN: //讀(in gain)混音輸入增益
- case SOUND_MIXER_WRITE_IGAIN: //寫(in gain)混音輸入增益
4.4修改“case SOUND_MIXER_WRITE_VOLUME:”和“case SOUND_MIXER_READ_VOLUME:”
如下圖所示(參考wm8976手冊的P86頁):
其中52,53對應的輸出左右通道1的音量,54,55對應的輸出左右通道2的音量
而我們耳機位於輸出左右通道1,如下圖所示,所以我們需要設置52,53的寄存器
接下來,便來看看寄存器,如何讀寫音量
我們以53通道1寄存器為例:
如上圖所示:
- bit8: 為1,表示每次寫入音量值,即立刻更新音量
- bit7: 位1,表示通道1的左右聲道都靜音
- bit6: 位1,表示通道1的右聲道靜音
- bit5~0: 表示音量大小,預設值為57(111001),最大值為63
所以修改的內容如下所示:
case SOUND_MIXER_WRITE_VOLUME: //音量0~100 ret = get_user(val, (long *) arg); //讀取應用數據,存到val里 if (ret) return ret; uda1341_volume = (((val & 0xff)) * 63) / 100; //最大值為63,最小值為0 wm8976_write_reg(52, (1<<8)| uda1341_volume); wm8976_write_reg(53, (1<<8)| uda1341_volume); break; case SOUND_MIXER_READ_VOLUME: val = (uda1341_volume * 100) / 63; //最大值為99 return put_user(val, (long *) arg);
4.5修改“case SOUND_MIXER_READ_IGAIN:”和“case SOUND_MIXER_WRITE_IGAIN:”
參考wm8976手冊的P86頁,如下圖所示:
其中50,51對應的就是左右混音控制寄存器
我們以50左聲道混音寄存器為例:
如上圖所示:
bit8~6: 混音輸入增益,預設值為0,最大值為7
所以修改的內容如下所示:
1)首先修改混音輸入增益的初始預設值為0,如下圖所示
2)修改“case SOUND_MIXER_READ_IGAIN:”和“case SOUND_MIXER_WRITE_IGAIN:”
case SOUND_MIXER_READ_IGAIN: //混音輸入:0~100 val = (mixer_igain* 100) / 7; return put_user(val, (int *) arg); case SOUND_MIXER_WRITE_IGAIN: ret = get_user(val, (int *) arg); if (ret) return ret; mixer_igain = val * 7 / 100; /* use mixer gain channel 1*/ wm8976_write_reg(50, mixer_igain<<6); wm8976_write_reg(51, mixer_igain<<6); break;
5.配置,修改內核文件
5.1 make menuconfig 配置內核
-> Device Drivers
-> Sound
-> Advanced Linux Sound Architecture // 相容OSS
-> Advanced Linux Sound Architecture
-> System on Chip audio support
<*> I2S of the Samsung S3C24XX chips //*:將/linux-2.6.22.6/sound/soc/s3c24xx下的makefile指定的文件加入內核里
5.2 將修改好的s3c-wm8976.c放入/linux-2.6.22.6/sound/soc/s3c24xx目錄下
5.3修改該目錄下的makefile
obj-y += s3c2410-uda1341.o
改為:
obj-y += s3c-wm8976.o
5.4 make uImage,如下圖所示,可以看到內核已經被編譯
最後下載並啟動內核,如下圖所示,可以看到該兩個設備節點
6.測試與運行
6.1使用wav測試音效卡
wav是屬於一個未經壓縮的音頻文件,所以可以直接調用給我們音效卡播放
播放:
cat Windows.wav > /dev/dsp
錄音(還需要修改下驅動才行):
cat /dev/dsp > sound.bin
//然後對著麥克風說話
ctrl+c //退出
cat sound.bin > /dev/dsp // 就可以聽到錄下的聲音
6.2使用madplay應用程式測試音效卡
Madplay是一個根據MAD演算法寫的MP3播放器,而MP3屬於高壓縮比(11:1)的文件,所以需要madplay解碼後才能給我們音效卡播放,使用之前,需要先來移植madplay
步驟如下:
1)首先下載並解壓3個文件
- libid3tag-0.15.1b.tar.gz //mp3的解碼庫
- libmad-0.15.1b.tar.gz //madplay的文件庫
- madplay-0.15.2b.tar.gz //madplay播放器的源碼
2)先創建安裝目錄mkdir tmp
3)接下來先安裝2個庫(./configure使用參考: http://www.cnblogs.com/lifexy/p/7866453.html