Pages

Friday 8 November 2013

Arduino PWM Audio DAC

This post is an addendum to the previous one. To make our lives (as a software developers) even more easier it is possible to recreate a real audio without even having to built the R-2R ladder. Atmega328 comes with a PWM capable timers and they're good enough to produce audio quality analog signal.

PWM fundamentals


I'm not going to go into details about PWM digital to analog conversion, on this blog I focus on my code and do not intend to cover the theoretical background very thoroughly. Please take some time and familiarize yourself with the PWM theory explained on those sites:

  1. Open Music - PWM DAC
  2. Open Music - Dual PWM
  3. Open Music - Distortion Analysis
  4. Open Music - PWM Tutorial

In general every PWM DAC can be described by a PWM frequency - fpwm (the frequency of the squarewave generated - which is constant) and the PWM bit resolution - bitres (which describes the granularity with which we can control the duty cycle).

All of three Arduino's Timers are capable of generating PWM signal. I'll focus of course on Timer1 since it's the only 16 bit timer. As mentioned above we have two properties to configure - the PWM frequency - fpwm and the bit resolution. The signal theory says that the fpwm should be at least two times higher than the highest frequency in our signal. How to calculate it and what is the highest possible we can achieve ? To answer that some details from the datasheet are needed and in fact an understanding of how Atmega's Timer really works.

Timer in PWM mode


Using either Fast (FPWM) or Phase & Frequency Correct PWM (PFPWM) the timer counts up from 0 to value in either OCR1A or ICR1 register (I'll use ICR1 to define the frequency, since obviously the value in OCR1A will control the comparator and in result the duty cycle). This means that ICR1 defines the TOP value for the timer (and thus implicitly defines how often the timer will overflow). The PWM frequency will be defined as:

Fast PWM:

fpwm = fclk / [ N * (1 + ICR1) ]

Phase and Frequency Correct:

fpwm = fclk / [ 2 * N * (1 + ICR1) ]

where N is prescaler.

I want the fpwm to be as high as possible so, obviously the prescaler will be configured to 1. It may not be visible at first glance but the fpwm has a major implications on the bit resolution as well. The value in OCR1A will control the duty cycle - it can't be any bigger than the value defined in ICR1 - which defines the frequency. if it will be higher than ICR1 than the timer simply will never reach it and the result will be a flat line - 100% duty cycle. It's more clear if you look at the picture

Principles of PWM with Atmega's Timers.

It's clear now that the demand for highest possible fpwm as well as highest possible bitrate is self contradicting. A golden merit must be established. In general it's worth to sacrifice some of the bit resolution just to increase the fpwm. I'll talk about real values in just a moment.

... Need more bandwidth


Indeed, just as previously the main bottleneck is the Serial Port. I did some tests pushing it to it's limits and it seems that the highest possible baud rate I can get is 230400. I don't want to change the firmware in Arduino's Atmega16U2 chip responsible for USB <-> serial communication, so I guess I'll have to live with this constrain and try to squeeze as much of the serial port as possible anyway.

230400 is not that bad. It's 23kB per second. What I can play with it ? There are a three options really (not mentioning the compressed formats - but that's a completely different story):

  • 16 kHz, 8 bit = 16 kB/s
  • 8 kHz, 16 bit = 16 kB/s
  • 22 kHz, 8 bit = 22 kB/s 18 kHz, 8bit = 18 kB/s
22 kHz sounds pretty good. This means that the highest frequency that can be recreated is around 11 kHz. As it will be shown later the bit resolution doesn't have that much of a spectacular influence on the quality, The bit resolution defines the so called "noise floor" which in simple words means that the lesser the bit resolution the noisier the signal will be and it will be impossible to recreate some very quiet - low amplitude sounds since they will drown in the digital noise itself. In this case, having the sampling rate as highest as possible is crucial.

22 kB/s doesn't leave much time margin though. It's almost feeding the DAC directly from the USART without much of a buffering - this may be a source of glitches and noise as well, not mentioning the glitches that will happen if the transmission is unsuccessful. The MCU will be pretty busy. In fact the USART is too slow to provide the data on time with this sampling rate. The highest I could get is 18 kHz unfortunately. 18 kHz is an absolute limit for the USART and even at that speed occasionally some glitches may happed

Data delivered to slow. A short moment of silence.

Anyway it's pretty good though, 9 kHz of audible bandwidth should sound good.

PWM frequency and bit resolution


Going back to the PWM frequency. I'll use the Phase & Frequency correct PWM mode. Our requirement is to play an audio with highest frequency up to 9 kHz. The fpwm must be higher than 18 kHz. We'll base our calculations on the bit resolution though, since we're not going to play anything above 8 bits (per PWM channel). Assuming that OCR1A must have an 8 bit resolution so ICR1 must be configured to 256. Taking those values into consideration:

fpwm = 16 MHz / [ 2 * 1 * (1 + 255) ] = 31,25 kHz

which is 3,4 higher than the highest frequency of the signal - that'll do.

16 bit playback


One of the options that I have is to play 16bits 8 kHz audio. Now, how to do this when just a paragraph before I configured the PWM resolution to 8 bits ? It's actually pretty easy. Atmega has two comparators per timer OC1A and OC1B. Each of them has an independent register OCR1A and OCR1B which value is constantly compared with TCNT1 value. The PWM duty cycle of the waveform on OC1A and OC1B pins is controlled by configuring OCR1A and OCR1B to appropriate values. In fact we can sum the signal from those two 8 bit outputs using resistors to form one combined output 16 bit output. The trick is that the resistor for the least signifficant byte has 256 smaller value than the one for the most signifficant byte - by natural way the levels of those two PWM outputs are shifted.

Playback


The playback loop is slightly different than the one for R-2R ladder. Now instead of pushing the data to the ports, it is "pushed" to the OCR1A timer registers:


you noticed the MODE macro. The software; during compilation, can be configured into any of the mentioned mode

  • #define MODE_8K_8B 0
  • #define MODE_8K_16B 1
  • #define MODE_16K_8B 2
  • #define MODE_18K_8B 3

By default MODE is configured to

#define MODE MODE_16K_8B

Dependently on the mode selected (16bit or 8 bit) the sample value is copied only to OCR1AL or to both OCR1AL and OCR1BL

The Circuit


PWM requires a couple of components forming a low pass filter in order to get rid of the PWM carrier frequency. The PWM frequency is 31,25 kHz, but we don't need anything really above 9 kHz, so the filter values should be:

f = 1/ [ 2 * 3,14 * R*C] = 1/[2 * 3.14 * 3k9 * 4n7] = 8,69 kHz 

The filter itself:

PWM filters for both PWM channels


PWM filters on the breadboard.

Software


As mentioned the software is more or less identical as for the R-2R ladder. It has only been enhanced to support conversion into couple of different formats instead of one (8kHz, 8 bit) and the baudrate has been increased to 230400 bps. As previously everything is done in binary manner to squeeze out everything that the USART can provide. The PWM configuration is being done by the libpca functions, there's no need to go into details about that, all the settings are standard, nothing extra out of ordinary. The timer is by defauilt configured into Phase & Frequency correct PWM mode, but it's possible to play with this setting in the code to compare the quality.

#if MODE == MODE_8K_16B
/* tpwm_fpwm_init(E_TIMER1, E_PWM_DOUBLE); */
tpwm_pwm_init(E_TIMER1, E_PWM_DOUBLE);
#else
/* tpwm_fpwm_init(E_TIMER1, E_PWM_SINGLE); */
tpwm_pwm_init(E_TIMER1, E_PWM_SINGLE);
#endif

How to use it


On the PC side, as previously one should use the player.sh script. The syntax has been changed slightly. The first argument is the mode, the rest are the audio files

player.sh <16b | 16k | 8k | 18k> <audiofile(s)>

The options are self explaining I think, In order to use the 16 kHz mode we should invoke the script like this:

./player.sh 16k myfile.mp3

One must remember to adjust the Arduino's serial port name in the wpwm.pl script accordingly if needed.

On the Arduino side, we must select the mode which we want to try by adjusting the MODE macro in main.c file (as mentioned previously). After compiling and flashing the firmware, one should be able to successfully use the player script in order to hear some music. 

I connect my Arduino directly to the MIC-IN of my laptop in order to record the sound - I don't use any amplifiers whatsoever.

The software as usual is available either as a snapshot from here (containing libpca snapshot and the project itself) or it can be clones from my github repositories:

git clone git@github.com:dagon666/avr_Libpca pca
git clone git@github.com:dagon666/avr_ArduinoProjects projects

cd projects/dac

Building & flashing:
make
make install

Let's hear some music finally


Just as expected the difference between 8 and 16 bits is almost unrecognizable. Only at the begining when the dynamic is quite low, the noise level for 16 bits is signifficantly lower. The most spectacular change comes along with the sampling rate - the higher the better.





Throughout the clip some glitches are noticable from time to time - they're the result of corrupted frames (CRC does not match) or simply the fact that the data was not delivered on time (18 kHz mode). In 18 kHz the USART is really operating on it's limit (as well as the CPU - which must service the RX interrupt), I mostly wanted to demonstrate that it's possible to go that high, but the audio is far more stable and less glitchy in 16 kHz mode.

No comments:

Post a Comment