I'm trying to synthesize sound on the Arduboy, which is a handheld gaming device with an AVR ATMega32u4 microcontroller and a speaker attached between its pins C6 and C7.
My plan is to use timer 4 to generate a high-frequency PWM signal on C7, and then use timer 3 to change timer 4's duty cycle. For a "hello world"-level program, I'm trying to read 3906 8-bit samples per second from PROGMEM.
First of all, to make sure my sample file is really in the format I think it is, I have used SoX to play it on a computer:
$ play -e unsigned-integer -r 3906 -b 8 sample2.raw
Here's the relevant parts of my code:
pub fn setup() {
without_interrupts(|| {
PLLFRQ::unset(PLLFRQ::PLLTM1);
PLLFRQ::set(PLLFRQ::PLLTM0);
TCCR4A::write(TCCR4A::COM4A1 | TCCR4A::PWM4A); // Set output C7 to high between 0x00 and OCR4A
TCCR4B::write(TCCR4B::CS40); // Enable with clock divider of 1
TCCR4C::write(0x00);
TCCR4D::write(0x00);
TC4H::write(0x00);
OCR4C::write(0xff); // One full period = 256 cycles
OCR4A::write(0x00); // Duty cycle = OCR4A / 256
TCCR3B::write(TCCR3B::CS32 | TCCR3B::CS30); // Divide by 1024
OCR3A::write(3u16); // 4 cycles per period => 3906 samples per second
TCCR3A::write(0);
TCCR3B::set(TCCR3B::WGM30); // count up to OCR3A
TIMSK3::set(TIMSK3::OCIE3A); // Interrupt on OCR3A match
// Speaker
port::C6::set_output();
port::C6::set_low();
port::C7::set_output();
});
}
progmem_file_bytes!{
static progmem SAMPLE = "sample2.raw"
}
// TIMER3_COMPA
#[no_mangle]
pub unsafe extern "avr-interrupt" fn __vector_32() {
static mut PTR: usize = 0;
// This runs at 3906 Hz, so at each tick we just replace the duty cycle of the PWM
let sample: u8 = SAMPLE.load_at(PTR);
OCR4A::write(sample);
PTR += 1;
if PTR == SAMPLE.len() {
PTR = 0;
}
}
The basic problem is that it just doesn't work: instead of hearing the audio sample, I just hear garbled noise from the speaker.
Note that it is not "fully wrong", there is some semblance of the intended operation. For example, I can hear that the noise has a repeating structure with the right length. If I set the duty cycle sample to 0 when PTR < SAMPLE.len() / 2, then I can clearly hear that there is no sound for half of my sample length. So I think timer 3 and its interrupt handler are certainly working as intended.
So this leaves me thinking either I am configuring timer 4 incorrectly, or I am misunderstanding the role of OCR4A and how the duty cycle needs to be set, or I could just have a fundamentally wrong understanding of how PWM-based audio synthesis is supposed to work.
Irrespective of the code you have written above, it seems your design suffers from several flaws:
a) speakers require alternating current to flow through it; it is achieved by varying the tension between a positive and a negative voltage. In your design, the output of your PWM is likely to be between 0 and 5V. If your PWM has a duty cycle of 50%, it turns into an average offset voltage of 2.5V constantly applied to your speaker. This could be fatal to it (the saving grace potentially being flaw 3 below, i.e. not enough current). A common way to fix this is to cut off DC current by connecting the speaker through a capacitor, to filter out any DC current. The value of the capacitor must be adjusted such as it doesn't filter out the signal you wish to use on low frequencies.
b) you shouldn't connect a speaker directly to your microcontroller PINs, for several reasons:
Your microcontroller is likely the expensive device of your board, and should be preserved from devices malfunctions (like shortcuts) that could lead to destroy it.
Your microcontroller can drive max 40mA per io PIN, as described in datasheet, 29.1 Absolute Maximum Ratings . A typical speaker has a resistance varying between 8 Ohms and 32 Ohms, and if we consider a duty cycle of 50%, you are passing in average 20mA across it. To drive your HP efficiently, you would need much more current to flow through. That's why speakers are generally driven by an amplifier stage.
Speakers are devices with coils inside. As such, they store magnetic energy that they yield back to the circuit when the voltage drops, injecting reverse currents. These reverse currents can easily destroy transistor gates if not properly handled. In your case, luckily, the amount of inverse current is low, since you don't drive the HP enough. But you should avoid that situation at all times.
c) Using a PWM to produce sound seems awkward. Since they produce square waves , these are plenty of harmonics that will add a lot of parasitic noises to the sound. But more importantly, PWM output does not allow you to vary the intensity (voltage) of the output, which is normally needed to reproduce sound properly.
To produce sound, you need an digital to analogue converter (DAC), then the proper circuitry to condition that signal and amplify it for a speaker. Unfortunately, the ATMega32u4 doesn't seem to have that feature. You should pick another microcontroller.
For questions on electronic designs, I suggest you to search on https://electronics.stackexchange.com.
Related
I'm trying to understand how to implement audio playback from scratch on attiny85. The goal is to play a short sound (cat meows, so i want it to remain recognizable) from an array representing strength of audio signal sampled at fixed interval.
As far as i understand, signal strength is linearly mapped to voltage of analogue audio signal. As far as I know, audio cards are Digital to Analogue Converters, but attiny85 probably doesn't have that.
I'm curious if I can use pwm to play the sound back. Since pwm changes average voltage by changing duty cycle of alternating high and low phases of signal, it most likely would result in the drop of audio quality. Wav sampling rates can differ between 1 HZ and 4.3 GHz according to google. Attiny85 has internal clock with frequency up to 8MHz (which I hope is same for it's pwm generator).
Considering reconfiguring the timer and pwm settings as well as looping in the array, what is the maximum sampling rate of audio i can reliably play? And should i even try to do it with pwm, or there are better options?
Given a system clock of 8 MHz, you can use PWM to generate mono (single-channel) audio.
Consider a PWM period of 1000 clocks, giving you about 10 bit resolution. The sample rate will be 8000 Hz then, which gives you some kind of lo-fi audio.
If you reduce your signal resolution to 8 bits, you'll get 8 MHz / 28 = 31.25 kHz sample rate. This gets near hi-fi.
Synchronize your sample output with the PWM generator, and use an appropriate analogue filter.
Many years ago I built a digital door bell with a sample rate of 8 kHz and 8 bit samples. It played nice sounds in the quality of telephones. The microcontroller was a 8051 derivative and it used an R-2R ladder as DAC.
A simpel sinus can be generated by using a 50% PWM signal and varying the frequency. Given some filtering effect through the speaker, it would mimik a single tone audio signal.
Making more advanced tones (needed for natural sound) quickly gets more complicated and the duty cycle of the signal can also be used to trick the human ear into hearing harmonics. Check out the arduino function tone() for some inspiration.
Be carefull when connecting a small speaker to the Arduino, preferably a transistor/buffer/small amplifyer should be place between the Arduino and the speaker.
My goal is to record audio using an electret microphone hooked into the analog pin of an esp8266 (12E) and then be able to play this audio on another device. My circuit is:
In order to check the output of the microphone I connected the circuit to the oscilloscope and got this:
In the "gif" above you can see the waves made by my voice when talking to microphone.
here is my code on esp8266:
void loop() {
sensorValue = analogRead(sensorPin);
Serial.print(sensorValue);
Serial.print(" ");
}
I would like to play the audio on the "Audacity" software in order to have an understanding of the result. Therefore, I copied the numbers from the serial monitor and paste it into the python code that maps the data to (-1,1) interval:
def mapPoint(value, currentMin, currentMax, targetMin, targetMax):
currentInterval = currentMax - currentMin
targetInterval = targetMax - targetMin
valueScaled = float(value - currentMin) / float(currentInterval)
return round(targetMin + (valueScaled * targetInterval),5)
class mapper():
def __init__(self,raws):
self.raws=raws.split(" ")
self.raws=[float(i) for i in self.raws]
def mapAll(self):
self.mappeds=[mapPoint(i,min(self.raws),max(self.raws),-1,1) for i in self.raws ]
self.strmappeds=str(self.mappeds).replace(",","").replace("]","").replace("[","")
return self.strmappeds
Which takes the string of numbers, map them on the target interval (-1 ,+1) and return a space (" ") separated string of data ready to import into Audacity software. (Tools>Sample Data Import and then select the text file including the data). The result of importing data from almost 5 seconds voice:
which is about half a second and when I play I hear unintelligible noise. I also tried lower frequencies but there was only noise there, too.
The suspected causes for the problem are:
1- Esp8266 has not the capability to read the analog pin fast enough to return meaningful data (which is probably not the case since it's clock speed is around 100MHz).
2- The way software is gathering the data and outputs it is not the most optimized way (In the loop, Serial.print, etc.)
3- The microphone circuit output is too noisy. (which might be, but as observed from the oscilloscope test, my voice has to make a difference in the output audio. Which was not audible from the audacity)
4- The way I mapped and prepared the data for the Audacity.
Is there something else I could try?
Are there similar projects out there? (which to my surprise I couldn't find anything which was done transparently!)
What can be the right way to do this? (since it can be a very useful and economic method for recording, transmitting and analyzing audio.)
There are many issues with your project:
You do not set a bias voltage on A0. The ADC can only measure voltages between Ground and VCC. When removing the microphone from the circuit, the voltage at A0 should be close to VCC/2. This is usually achieved by adding a voltage divider between VCC and GND made of 2 resistors, and connected directly to A0. Between the cap and A0.
Also, your circuit looks weird... Is the 47uF cap connected directly to the 3.3V ? If that's the case, you should connect it to pin 2 of the microphone instead. This would also indicate that right now your ADC is only recording noise (no bias voltage will do that).
You do not pace you input, meaning that you do not have a constant sampling rate. That is a very important issue. I suggest you set yourself a realistic target that is well within the limits of the ADC, and the limits of your serial port. The transfer rate in bytes/sec of a serial port is usually equal to baud-rate / 8. For 9600 bauds, that's only about 1200 bytes/sec, which means that once converted to text, you max transfer rate drops to about 400 samples per second. This issue needs to be addressed and the max calculated before you begin, as the max attainable overall sample rate is the maximum of the sample rate from the ADC and the transfer rate of the serial port.
The way to grab samples depends a lot on your needs and what you are trying to do with this project, your audio bandwidth, resolution and audio quality requirements for the application and the amount of work you can put into it. Reading from a loop as you are doing now may work with a fast enough serial port, but the quality will always be poor.
The way that is usually done is with a timer interrupt starting the ADC measurement and an ADC interrupt grabbing the result and storing it in a small FIFO, while the main loop transfers from this ADC fifo to the serial port, along the other tasks assigned to the chip. This cannot be done directly with the Arduino libraries, as you need to control the ADC directly to do that.
Here a short checklist of things to do:
Get the full ESP8266 datasheet from Expressif. Look up the actual specs of the ADC, mainly: the sample rates and resolutions available with your oscillator, and also its electrical constraints, at least its input voltage range and input impedance.
Once you know these numbers, set yourself some target, the math needed for successful project need input numbers. What is your application? Do you want to record audio or just detect a nondescript noise? What are the minimum requirements needed for things to work?
Look up in the Arduino documentartion how to set up a timer interrupt and an ADC interrupt.
Look up in the datasheet which registers you'll need to access to configure and run the ADC.
Fix the voltage bias issue on the ADC input. Nothing can work before that's done, and you do not want to destroy your processor.
Make sure the input AC voltage (the 'swing' voltage) is large enough to give you the results you want. It is not unusual to have to amplify a mic signal (with an opamp or a transistor), just for impedance matching.
Then you can start writing code.
This may sound awfully complex for such a small task, but that's what the average day of an embedded programmer looks like.
[EDIT] Your circuit would work a lot better if you simply replaced the 47uF DC blocking capacitor by a series resistor. Its value should be in the 2.2k to 7.6k range, to keep the circuit impedance within the 10k Ohms or so needed for the ADC. This would insure that the input voltage to A0 is within the operating limits of the ADC (GND-3.3V on the NodeMCU board, 0-1V with bare chip).
The signal may still be too weak for your application, though. What is the amplitude of the signal on your scope? How many bits of resolution does that range cover once converted by the ADC? Example, for a .1V peak to peak signal (SIG = 0.1), an ADC range of 0-3.3V (RNG = 3.3) and 10 bits of resolution (RES = 1024), you'll have
binary-range = RES * (SIG / RNG)
= 1024 * (0.1 / 3.3)
= 1024 * .03
= 31.03
A range of 31, which means around Log2(31) (~= 5) useful bits of resolution, is that enough for your application ?
As an aside note: The ADC will give you positive values, with a DC offset, You will probably need to filter the digital output with a DC blocking filter before playback. https://manual.audacityteam.org/man/dc_offset.html
I've got an Adafruit Bluefruit NRF52 hooked up to the Adafruit BNO055 9-axis orientation sensor, gathering 3 axis of absolute orientation plus 3 axis of acceleration (6 floats in total) and sending over Bluetooth through bleuart. I need the bleuart to update every 7.5 milliseconds with a new line of values, but when I run it, it doesn't print more than about 20 lines new lines of values every second. Essentially I need values to update as quickly as possible, as I am measuring very high speed, high fidelity movement.
At the start of each line I also have three digit number, which represents the calibration status of each sensor on the IMU. Each printed line looks something like:
303 68.69 4.19 -2.19 -0.12 0.14 -0.40
I am currently streaming to my iphone with the latest iOs version, which in theory can handle 7.5ms intervals.
I've read that a solution may be to buffer the values and send over in a larger chunk at larger connection intervals, but am unsure on how to do this.
My relevant Arduino code is below:
Bluefruit.setConnIntervalMS(7.5, 20);
void loop()
{
imu::Vector<3> accel =
bno.getVector(Adafruit_BNO055::VECTOR_LINEARACCEL);
/* Get a new sensor event */
sensors_event_t event;
bno.getEvent(&event);
/* Display the floating point data */
bleuart.print(event.orientation.x);
bleuart.print("\t");
bleuart.print(event.orientation.y);
bleuart.print("\t");
bleuart.print(event.orientation.z);
bleuart.print("\t");
/* Display the floating point data for Linear Acceleration */
bleuart.print(accel.x());
bleuart.print("\t");
bleuart.print(accel.y());
bleuart.print("\t");
bleuart.print(accel.z());
bleuart.print("\n");
}
iOS doesn't actually support a 7.5ms connection interval. Check the connection parameters section (11.6) in the Apple developer guidelines. Just because you are specifying a CI that low doesn't mean that you'll actually get it. In this scenario the nRF52 is the slave and only requests an interval that low from the master (your phone). The master, if it so wishes, can completely disregard the request you make.
You'd be better off, as you've already eluded to, buffering your data and sending it via a custom characteristic. Figure out how many bytes you need and maybe you can pack a couple of readings into a single BLE write. If you're really struggling with throughput then you'll need a custom service with multiple characteristics. I recently worked on a project that streams 8 channels of data (~125Hz/16-bit) over BLE with three characteristics and this is bordering on the maximum throughput you can achieve.
As an aside: judging data throughput by the amount of lines printed per second is a big no no. Print functions typically have huge overheads and will drastically affect your measured throughput in a negative way.
Let me know if I can help further.
I had a assignment for college where we needed to play a precompiled wav as integer array through the PWM and DAC. Now, I wanted more of a challenge, so I went out of my way and created a audio dac over usb using the micro controller in question: The STM32F051. It basically listens to my soundcard output using a wasapi loopback recorder, changes the resolution from 16 to 12 bit (since the dac on the stm32 only has a 12 bit resolution) and sends it over using usart using 10x sample rate as baud rate (in my case 960000). All done in C#.
On the microcontroller I simply use a interrupt for usart and push the received data to the dac.
It works pretty well, much better than PWM, and at a decent sample frequency of 48kHz.
But... here it comes.. When there is some (mostly) high pitch symphonic melody it starts to sound "wobbly".
Here a video where you can hear it: https://youtu.be/xD3uTP9etuA?t=88
I read up on the internet a bit about DIY dac's and someone somewhere (don't remember where) mentioned that MCU's in general have interrupt jitter. So may basic question is: Is interrupt jitter actually causing this? If so, are there ways to limit the jitter happening?
Or is this something entirely different?
I am thinking of trying to compact the pcm data send over serial (as said before, resolution of 12 bits, but are sent in packet of 2 8bits forming 16bits, hence twice the samplerate as the baud rate, so my plan is trying to shift 12 bits to the MSB and adding four bits of the next 12 bit value to the current 16 bit variable, hence only needing 12 transfers instead of 16 per 8 samples. Might read upon more efficient ways of compacting data for transport.), put the samples in a buffer and then use another timer that triggers at 48kHz for sending the samples to the dac. Would this concept work? Or would I just waste time?
For code, here is the project: https://github.com/EldinZenderink/SoundOverSerial
I need to tune PI(D) gains in a system which has a quite large delay. It's a common temperature controller, but the temperature probe is far away from the heater. Some further info:
the response of the probe is delayed about 10 seconds from any change on the heater
the temperature is sampled # 1 Hz, with a resolution of 0.01 °C
the heater is controller in PWM with a period of 1 Hz, with a 10-bit PWM
the goal is to maintain the oscillation below ±0.05 °C
Currently I'm using the controller as PI. I can't avoid oscillations. The higher the gain, the smaller and faster the oscillations. Still too high (about ±0.15 °C).
Reducing the P and I gains leads to very long and deep oscillations.
I think this is due to the delay.
The settling time is not a problem, it may take all the time it needs.
I'm puzzling over how get the system to work. Let's think to use only I. When the probe reaches the target value and the I output starts to decrease, the temperature will rise for some other time. I cannot use the derivative term because the variations are too slow and the dError is very close to zero (if I set the dGain to a huge value there is too much noise).
Any idea?
Try P-only. How fast are the proportional-only oscillations? If you can't tune Kp small enough to get no oscillations, then your heater is overpowered for your system.
If the dead time of the of the system is on the order of 10s, the time constant (T_i) for the Integral term should be 3.3 times the dead time, using a Ziegler Nichols open-loop PI rule ( https://controls.engin.umich.edu/wiki/index.php/PIDTuningClassical#Ziegler-Nichols_Open-Loop_Tuning_Method_or_Process_Reaction_Method: ) , and then Integral term should be Ki = Kp/T_i. So with deadtime = 10s, then Ki should be Kp/33 or slower.
If you are getting integral-only oscillations, then the integral is winding up and down quicker than the process responds, and it should be even smaller.
Also -- think of the units of the different terms. It might not be the delay causing your problems so much as the resolution of the measurement and control systems. If you're driving a (for example) 100W heater with a 1/1024 resolution PWM, you've got 0.1W resolution per PWM count that you are trying to adjust based on 0.01C temperature differences. At less than Kp = 100 PWMcount/degree (or 10W/degree) you don't have enough resolution in the PWM to make changes in response to a 0.01C error. At a Kp=10PWM/C you might need a 0.10C change to result in an actual change in the PWM power. Can you use a higher resolution PWM?
Thinking of it the other way, if you want to operate a system over a range of 30C at 0.01C, I'd think you would want at least a 15bit PWM to have 10 times the resolution in the controlled system. With only 10 bits of PWM you only get about 1C of total range with control at 10x the resolution of the measurements.
Normally for large delays you have two options: Lower the gains of the system or, if you have a model of the plant you are controlling, use a Smith Predictior.
I would start by modelling your system (using open-loop steps in the input) to quantify the delay and the time constant of your plant, then check if the sampling of the temperature and the PWM rate are OK.
Notice that if your PWM frequency is too small in comparison to the plant dynamics, you will have sustained oscillations because of the slow PWM. You can check it using just an constant input to your PWM (with no controllers, open loop).
EDIT: Didn't see that the problem was already solved, but I'll leave this here for reference.