3.5mm耳机接口是PC上最常见的音频接口,因为它工艺简单、价格低廉在涉及到声音输入输出的地方都能看到它的身影。但随着时代的发展,这样的接口正在逐步退出历史的舞台。为了继续让手上的3.5mm耳机发挥余热,笔者用手上的Arduino开发板制作了一个USB对3.5mm音频接口的转接器。 这次出场的是 Arduino Pro Micro,严格的说这个型号并不是官方正式推出的 Arduino 产品,只是Arduino的兼容品,但它使用和Arduino Leonardo相同的主控芯片ATmega32u4,还可以使用Leonardo相同的Bootloader,因此可以直接被视为Leonardo的缩小版,两者在编程方面完全相同,只有存在物理尺寸上的差别。
在动手之前,先解说一下基本原理:Arduino Pro Micro 的主控是ATmega32u4 芯片,它内置 了USB Device功能(这也是为什么 ArduinoLeonardo 能够方便的将自己模拟为USBKeyboard、Mouse或者游戏手柄)。配合程序它能够将自己声明为一个USBAudio的输出设备(通常情况下,谈论到的输出和输入是针对PC上的CPU来说,因此这里是输出设备),插入Windows后,OS会把它当作一个声卡,这样音频流就会被发送到它的ATmega32u4芯片上。代码上来说,设备插入之后设置芯片内部定时器,按照48K频率触发,每次触发之后,在中断服务函数中将收到的音频数据转化为模拟方式输出即可实现数字到模拟的转换。额外的问题是主控ATmega32u4没有 DAC ,但是可以通过 PWM 的方式模拟出来同样的功能。比如:占空比 50% 的PWM信号可以被认为是5V*50%=2.5V的直流信号。对此有兴趣的可以去查看之前我写的用Arduino直接播放存储在Flash 中的声音的文章。下面的图中PWM的信号频率没有变化,但是占空比发生了变化,红色的是对应的等效信号,可以看到不同的占空比能够等效于不同的直流信号。
为了实现目标,使用 Lufa 库。这是一个开源的AVR 系列单片机的开源库,提供了AVR 系列单片机的 USB 框架,用户可以直接在这个框架内实现AVR单片机USB的各种功能。例如:平时使用到的ArduinoUno 上面的USB转串口芯片ATmega16u2上面运行的Firmware就是在这个架构上实现的。 在 Lufa 的库中有一个 USB Output 的Demo(Demo\Device\CalssDriver\AudioOutput\),这次的代码就是基于这个程序修改而来,主要的修改如下: 1. 1.因为选择的 Arduino Pro Micro没有对应的LED,移除项目代码中所有LED 的相关内容; 2. 2.Makefile 中将 MCU 修改为 Atmega32u4; 3. 3.修改 EVENT_USB_Device_Connect 函数中的PWM设定相关代码,原本的代码中,使用OC3A和 OC3B 作为输出Pin,但是对于 32U4来说,只有 OC3APin,后者不存在……为此,修改代码更换为OC1A/PB5 (D9)和OC1B/PB6 (D10)。
因为Lufa USB框架的存在,代码并不复杂,关键函数只有2个: 1.处理设备插入的Event,主要完成的工作是设置一个定时器,按照CurrentAudioSampleFrequency(48K)的频率触发。另外就是设定D9、D10为 PWM 输出Pin。左右声道的声音就是分别从这两个Pin输出。 [mw_shl_code=c,true]/** Event handler for the library USB Connection event. */
void EVENT_USB_Device_Connect(void)
{
/* Sample reload timer initialization */
TIMSK0 = (1 << OCIE0A);
OCR0A = ((F_CPU / 8 / CurrentAudioSampleFrequency) - 1);
TCCR0A = (1 << WGM01); // CTC mode
TCCR0B = (1 << CS01); // Fcpu/8 speed
#if defined(AUDIO_OUT_MONO)
/* Set speaker as output */
DDRB |= (1 << 6);
#elif defined(AUDIO_OUT_STEREO)
/* Set speakers as outputs */
DDRB |= ((1 << 6) | (1 << 5));
#elif defined(AUDIO_OUT_PORTC)
/* Set PORTC as outputs */
DDRB |= 0xFF;
#endif
#if (defined(AUDIO_OUT_MONO) || defined(AUDIO_OUT_STEREO))
/* PWM speaker timer initialization */
TCCR1A = ((1 << WGM10) | (1 << COM1A1) | (1 << COM1A0)
| (1 << COM1B1) | (1 << COM1B0)); // Set on match, clear on TOP
TCCR1B = ((1 << WGM12) | (1 << CS10)); // Fast 8-Bit PWM, F_CPU speed
#endif
}[/mw_shl_code]
2.定时器的触发函数。每次触发的时候,音频数据通过PWM模拟出来的DAC发送出去。架构上来说,能够支持单声道(MONO)、双声道(STEREO,即例子中使用的模式)还有将一个完整的PORTC,8个Pin作为一个字节的输出。 [mw_shl_code=c,true]/** ISR to handle the reloading of the PWM timer with the next sample. */
ISR(TIMER0_COMPA_vect, ISR_BLOCK)
{
uint8_t PrevEndpoint = Endpoint_GetCurrentEndpoint();
/* Check that the USB bus is ready for the next sample to read */
if (Audio_Device_IsSampleReceived(&Speaker_Audio_Interface))
{
/* Retrieve the signed 16-bit left and right audio samples, convert to 8-bit */
int8_t LeftSample_8Bit = (Audio_Device_ReadSample16(&Speaker_Audio_Interface) >> 8);
int8_t RightSample_8Bit = (Audio_Device_ReadSample16(&Speaker_Audio_Interface) >> 8);
/* Mix the two channels together to produce a mono, 8-bit sample */
int8_t MixedSample_8Bit = (((int16_t)LeftSample_8Bit + (int16_t)RightSample_8Bit) >> 1);
#if defined(AUDIO_OUT_MONO)
/* Load the sample into the PWM timer channel */
OCR1A = (MixedSample_8Bit ^ (1 << 7));
#elif defined(AUDIO_OUT_STEREO)
/* Load the dual 8-bit samples into the PWM timer channels */
OCR1A = (LeftSample_8Bit ^ (1 << 7));
OCR1B = (RightSample_8Bit ^ (1 << 7));
#elif defined(AUDIO_OUT_PORTC)
/* Load the 8-bit mixed sample into PORTC */
PORTC = MixedSample_8Bit;
#endif
Endpoint_SelectEndpoint(PrevEndpoint);
}[/mw_shl_code]
Lufa 库对应的编译器是:WinAVR,这是一款开源的编译器。安装完成后,进入代码所在目录使用 make 命令即可完整编译:
接下来的问题就是如何上传到 Arduino Pro Micro中。和普通的ArduinoUno 不同,ArduinoLeonardo这样的板卡是通过用1200波特率打开对应串口的方式来实现自动Reset进入Bootloader,之后再进行串口通讯完成烧写。为此使用下面的批处理完成自动的上传工作(使用之前,要确保Arduino Leonardo驱动已经安装,而不是使用默认的系统驱动,后者会导致下面的批处理无法确定Arduino使用的端口): [mw_shl_code=bash,true]for /f "tokens=1* delims==" %%I in ('wmic path win32_pnpentity get caption /format:list ^| find "Arduino"') do (
call :resetCOM "%%~J"
)
:continue
:: wmic /format:list strips trailing spaces (at least for path win32_pnpentity)
for /f "tokens=1* delims==" %%I in ('wmic path win32_pnpentity get caption /format:list ^| find "Arduino Leonardo bootloader"') do (
call :setCOM "%%~J"
)
:: end main batch
goto :EOF
:resetCOM <WMIC_output_line>
:: sets _COM#=line
setlocal
set "str=%~1"
set "num=%str:*(COM=%"
set "num=%num =%"
set port=COM%num%
echo %port%
mode %port%: BAUD=1200 parity=N data=8 stop=1
timeout /t 1
goto :continue
:setCOM <WMIC_output_line>
:: sets _COM#=line
setlocal
set "str=%~1"
set "num=%str:*(COM=%"
set "num=%num =%"
set port=COM%num%
echo %port%
goto :flash
:flash
d:\arduino-1.8.4\hardware\tools\avr\bin\avrdude -v -CC: \arduino-1.8.4\hardware\tools\avr\etc\avrdude.conf -patmega32u4 -cavr109 -P%port% -b57600 -D -V -Uflash:w:./ AudioOutput.hex:i[/mw_shl_code]
使用上面的批处理即可完成上传动作:
主机端就会出现一个USB 音频设备,播放音乐即可从3.5寸接口听到声音。
工作的视频可以在 https://zhuanlan.zhihu.com/p/48489777 看到
|