Hi!
I've been fiddling with the idea of real time 1-bit sound generation with my Arduino Micro.
What I'd like to achieve is having sound play continuously in "background", while other parts of the program can modify the sound parameters in real time.
Specifically, I want to connect a PS/2 keyboard to the Arduino in the end, but I'm not there yet, just doing some testing for now.
So I used Timer1 interrupt to generate sound, I set it up for 1000Hz, always monitoring the output squarewave on my oscilloscope to see if the frequency was ok.
All good, then I added another part of the program to flash an led, still all good, then I added another part doing an analogRead(), and the audio started getting flaky/stuttering.
Here is the code I used:
/*** DEFINES */
#define SND A5
#define LED 13
#define POT_A A4
#define PRESCALER 1 /* 1 8 64 256 1024 */
#define SAMPLERATE 20000
#define TIMER_INTERVAL (uint16_t)((1000.f/(float)SAMPLERATE) / (1000.f/((float)F_CPU/(float)PRESCALER))) /* in 1/(F_CPU/PRESCALER) units */
// defines for setting and clearing register bits
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit)) // ClearBIt -> 0
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit)) // SetBIt -> 1
#endif
/* DEFINES end. */
/*** INCLUDES */
#include <avr/sleep.h>
/* INCLUDES end. */
/*** GLOBALS */
volatile uint8_t TimerFlag = 0; // set to 1 on timer wakeup
uint8_t ledStatus = 0;
uint32_t ledAcc = 0;
uint8_t sndStatus = 0;
uint32_t sndAcc = 0;
#define FIXFREQ 1000.0
uint32_t potAcc = 0;
uint16_t pota16;
/* GLOBALS end. */
void setup(){
pinMode(LED, OUTPUT);
pinMode(SND, OUTPUT);
pinMode(POT_A, INPUT);
TCNT1 = 0; // clear timer counter
TCCR1A = 0b11000000; // select the compare match A to set flag on match
TCCR1B = 0b00001000; /* CTC1 is set, so the timer will reset on compare match */
OCR1A = TIMER_INTERVAL; // set output compare value
TIMSK1 = 0b00000010; // enable timer compare match A interrupt
#if PRESCALER == 1
TCCR1B |= 0b00000001;
#elif PRESCALER == 8
TCCR1B |= 0b00000010;
#elif PRESCALER == 64
TCCR1B |= 0b00000011;
#elif PRESCALER == 256
TCCR1B |= 0b00000100;
#elif PRESCALER == 1024
TCCR1B |= 0b00000101;
#endif
// set ADC prescale to 128, slows ADC samplerate, lightweight
sbi(ADCSRA,ADPS2) ;
sbi(ADCSRA,ADPS1) ;
sbi(ADCSRA,ADPS0) ;
sei(); // enable interrupts
}
void loop(){
do{
} while (TimerFlag == 0);
TimerFlag = 0; // reset the timer flag
sndAcc++;
if(sndAcc <= (uint32_t)(((float)SAMPLERATE / FIXFREQ) / 2.f) ){
digitalWrite(SND, 1);
}
else{
digitalWrite(SND, 0);
}
if(sndAcc >= (uint32_t)((float)SAMPLERATE / FIXFREQ) ){
sndAcc = 0;
}
if(ledAcc == (uint32_t)((float)SAMPLERATE / 2.f) ){ /* flipping led's state 2times per second */
ledAcc = 0;
ledStatus = !ledStatus;
digitalWrite(LED, ledStatus);
}
else{
ledAcc++;
}
if(potAcc == (uint32_t)((float)SAMPLERATE / 30.f) ){ /* reading pot value 30 times per second */
potAcc = 0;
pota16 = analogRead(POT_A);
}
else{
potAcc++;
}
}
ISR (TIMER1_COMPA_vect){
TimerFlag = 1; // set flag to signal timer interrupt occurred
}
I'm aware that's one of many ways to do it, using timer's interrupt looked like the best to me.
My question is: is there something I can do / do better to generate sound that will be unaffected by other processes, or am I already running out of horsepower?
ok I ditched the timer flag and put my code directly inside the ISR. But actually, nothing changed.
My code looks like this now:
/*** DEFINES */
#define SND A5
#define LED 13
#define POT_A A4
#define PRESCALER 1 /* 1 8 64 256 1024 */
#define SAMPLERATE 20000
#define TIMER_INTERVAL (uint16_t)((1000.f/(float)SAMPLERATE) / (1000.f/((float)F_CPU/(float)PRESCALER))) /* in 1/(F_CPU/PRESCALER) units */
// defines for setting and clearing register bits
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit)) // ClearBIt -> 0
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit)) // SetBIt -> 1
#endif
/* DEFINES end. */
/*** GLOBALS */
uint8_t ledStatus = 0;
uint32_t ledAcc = 0;
uint8_t sndStatus = 0;
uint32_t sndAcc = 0;
#define FIXFREQ 1000.0
uint32_t potAcc = 0;
uint16_t pota16;
/* GLOBALS end. */
void setup(){
pinMode(LED, OUTPUT);
pinMode(SND, OUTPUT);
pinMode(POT_A, INPUT);
TCNT1 = 0; // clear timer counter
TCCR1A = 0b11000000; // select the compare match A to set flag on match
TCCR1B = 0b00001000; /* CTC1 is set, so the timer will reset on compare match */
OCR1A = TIMER_INTERVAL; // set output compare value
TIMSK1 = 0b00000010; // enable timer compare match A interrupt
#if PRESCALER == 1
TCCR1B |= 0b00000001;
#elif PRESCALER == 8
TCCR1B |= 0b00000010;
#elif PRESCALER == 64
TCCR1B |= 0b00000011;
#elif PRESCALER == 256
TCCR1B |= 0b00000100;
#elif PRESCALER == 1024
TCCR1B |= 0b00000101;
#endif
// set ADC prescale to 128, slows ADC samplerate, lightweight
sbi(ADCSRA,ADPS2) ;
sbi(ADCSRA,ADPS1) ;
sbi(ADCSRA,ADPS0) ;
sei(); // enable interrupts
}
void loop(){
}
ISR (TIMER1_COMPA_vect){
sndAcc++;
if(sndAcc <= (uint32_t)(((float)SAMPLERATE / FIXFREQ) / 2.f) ){
digitalWrite(SND, 1);
}
else{
digitalWrite(SND, 0);
}
if(sndAcc >= (uint32_t)((float)SAMPLERATE / FIXFREQ) ){
sndAcc = 0;
}
if(ledAcc == (uint32_t)((float)SAMPLERATE / 2.f) ){ /* flipping led's state 2times per second */
ledAcc = 0;
ledStatus = !ledStatus;
digitalWrite(LED, ledStatus);
}
else{
ledAcc++;
}
if(potAcc == (uint32_t)((float)SAMPLERATE / 30.f) ){ /* reading pot value 30 times per second */
potAcc = 0;
pota16 = analogRead(POT_A);
}
else{
potAcc++;
}
}
The "problem" is the analogRead(), if I get rid of that one, the wave is clean and the frequency stable. I guess my real question is "if even an analogRead() is enough to cause trouble, is it possible to do anything significant at 20kHz?(that's the samplerate I choose here)"
Can someone answer this question? Have you tried something similar?
Yes because that is 0.1mS of blocking code. To minimise this then set the A/D converter in the free running mode and read it when required.
That sample rate is pushing it for a 16MHz processor. I have got an analogue input rate of about 56KHz when doing nothing else but save the samples in memory, but I was not trying to do anything else as well.
A simple sample in / sample out to D/A can run on that sort of processor at a about 16 Ksps ( samples per second ).
That sample rate is pushing it for a 16MHz processor.
uhm, yeah, in fact the only thing that made things better is lowering the samplerate to 10kHz. Still hearing some clicks when the ADC is sampling, tho
Yes because that is 0.1mS of blocking code. To minimise this then set the A/D converter in the free running mode and read it when required.
I also set it in free running mode, no change with the clicks. As far as I understood, even in free running mode, if I decide to sample every X, there will be some blocking code every X. I had a look at the source code for the analogRead() function, there is a very explicit empty while loop waiting for the AD conversion to complete. Is there a different workaround for that?
Anyways, I was using the analogRead() just as a test, might not even want to include it in my project. Now I'll try to add my PS/2 keyboard code and see if it holds up :))
Well code in the ISR will be blocking by definition because the global interrupt flag is set when an interrupt happens. However inside the ISR that deals with a D/A Conversion complete interrupt, you can lower the global flag, and so any time timeout interrupts will still get through while you are doing stuff with your sample you just read.
Can you post the code of when you put the A/D into free running mode and it made no difference?
yeah sure, here the code with the freerunning mode:
/*** DEFINES */
#define SND 3 /* PORTD0 */
#define LED 13
#define POT_A A4 /* ADC1 */
#define PRESCALER 1 /* 1 8 64 256 1024 */
#define SAMPLERATE 20000
#define TIMER_INTERVAL (uint16_t)((1000.f/(float)SAMPLERATE) / (1000.f/((float)F_CPU/(float)PRESCALER))) /* in 1/(F_CPU/PRESCALER) units */
// defines for setting and clearing register bits
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit)) // ClearBIt -> 0
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit)) // SetBIt -> 1
#endif
/* DEFINES end. */
/*** GLOBALS */
uint8_t ledStatus = 0;
uint32_t ledAcc = 0;
uint8_t sndStatus = 0;
uint32_t sndAcc = 0;
#define FIXFREQ 1000.0
uint32_t potAcc = 0;
uint16_t pota16;
/* GLOBALS end. */
void setup(){
pinMode(LED, OUTPUT);
pinMode(SND, OUTPUT);
pinMode(POT_A, INPUT);
TCNT1 = 0; // clear timer counter
TCCR1A = 0b11000000; // select the compare match A to set flag on match
TCCR1B = 0b00001000; /* CTC1 is set, so the timer will reset on compare match */
OCR1A = TIMER_INTERVAL; // set output compare value
TIMSK1 = 0b00000010; // enable timer compare match A interrupt
#if PRESCALER == 1
TCCR1B |= 0b00000001;
#elif PRESCALER == 8
TCCR1B |= 0b00000010;
#elif PRESCALER == 64
TCCR1B |= 0b00000011;
#elif PRESCALER == 256
TCCR1B |= 0b00000100;
#elif PRESCALER == 1024
TCCR1B |= 0b00000101;
#endif
// // set ADC prescale to 128, slows ADC samplerate, lightweight
// sbi(ADCSRA,ADPS2) ;
// sbi(ADCSRA,ADPS1) ;
// sbi(ADCSRA,ADPS0) ;
// free running adc
cbi(ADCSRB, ADTS3);
cbi(ADCSRB, ADTS2);
cbi(ADCSRB, ADTS1);
cbi(ADCSRB, ADTS0);
sbi(ADCSRA, ADSC); // kickstart conversion for the ADC
sei(); // enable interrupts
}
void loop(){
}
ISR (TIMER1_COMPA_vect){
sndAcc++;
if(sndAcc <= (uint32_t)(((float)SAMPLERATE / FIXFREQ) / 2.f) ){
PORTD |= 0b00000001; // digitalWrite(SND, 1);
}
else{
PORTD &= 0b11111110; // digitalWrite(SND, 0);
}
if(sndAcc >= (uint32_t)((float)SAMPLERATE / FIXFREQ) ){
sndAcc = 0;
}
if(ledAcc == (uint32_t)((float)SAMPLERATE / 2.f) ){ /* flipping led's state 2times per second */
ledAcc = 0;
ledStatus = !ledStatus;
digitalWrite(LED, ledStatus);
}
else{
ledAcc++;
}
if(potAcc == (uint32_t)((float)SAMPLERATE / 30.f) ){ /* reading pot value 30 times per second */
potAcc = 0;
pota16 = analogRead(POT_A);
}
else{
potAcc++;
}
}
I'm also using direct port addressing to flip the sound pin. Didn't seem to make a difference either, the bulk of the delay is in the analogRead()
inside the ISR that deals with a D/A Conversion complete interrupt, you can lower the global flag , and so any time timeout interrupts will still get through while you are doing stuff with your sample you just read.
not sure I understood this one, I tried doing it like that, but I get the feeling that's not what you meant:
if(potAcc == (uint32_t)((float)SAMPLERATE / 30.f) ){ /* reading pot value 30 times per second */
potAcc = 0;
cli(); /* disable global interrupt flag */
pota16 = analogRead(POT_A);
}
else{
potAcc++;
}
Thanks for posting that code, I can see where you are going wrong.
The whole point about using the A/D in free running mode is that you can use the time normally spent waiting for a conversion to finish to do something else. What you have done here is to still use an analogRead() to get a sample. That function starts off a conversion and waits until it has finished, so it is not taking any advantage of you earlier putting it into free running mode.
When you are in free running mode you can use it in one of two ways.
Check that the EOC end of conversion flag has been set and then read the two results registers.
Enable the EOC flag to generate an interrupt and have the ISR for that vector read the two results registers.
Correct it is not what I mean.
If you look at the data sheet for the Arduino Micro's processor (ATmega 32u4) Download the complete data sheet from here Section 9.1 contains a list of the interrupt vectors
Vector number 30 is the one for "ADC Conversion Complete" This is the one where you put the address of the ISR to run when the free running A/D has been set and you want an ISR to just read the A/D registers. It is in this ISR that you can enable the global interrupts again if you want so that other sources of interrupt can get a look in and interrupt your interrupt routine. You do not do this in "normal" code.
oh wow! thanks a lot! now it works perfectly even at 20kHz, and I learned a lot in the process.
this is the working code:
/*** DEFINES */
#define SND 3 /* PORTD0 */
#define LED 13
#define POT_A A4 /* ADC1 */
#define PRESCALER 1 /* 1 8 64 256 1024 */
#define SAMPLERATE 20000
#define TIMER_INTERVAL (uint16_t)((1000.f/(float)SAMPLERATE) / (1000.f/((float)F_CPU/(float)PRESCALER))) /* in 1/(F_CPU/PRESCALER) units */
// defines for setting and clearing register bits
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit)) // ClearBIt -> 0
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit)) // SetBIt -> 1
#endif
/* DEFINES end. */
/*** GLOBALS */
uint8_t ledStatus = 0;
uint32_t ledAcc = 0;
uint8_t sndStatus = 0;
uint32_t sndAcc = 0;
#define FIXFREQ 1000.0
uint32_t potAcc = 0;
uint16_t pota16;
/* GLOBALS end. */
void setup(){
pinMode(LED, OUTPUT);
pinMode(SND, OUTPUT);
pinMode(POT_A, INPUT);
TCNT1 = 0; // clear timer counter
TCCR1A = 0b11000000; // select the compare match A to set flag on match
TCCR1B = 0b00001000; /* CTC1 is set, so the timer will reset on compare match */
OCR1A = TIMER_INTERVAL; // set output compare value
TIMSK1 = 0b00000010; // enable timer compare match A interrupt
#if PRESCALER == 1
TCCR1B |= 0b00000001;
#elif PRESCALER == 8
TCCR1B |= 0b00000010;
#elif PRESCALER == 64
TCCR1B |= 0b00000011;
#elif PRESCALER == 256
TCCR1B |= 0b00000100;
#elif PRESCALER == 1024
TCCR1B |= 0b00000101;
#endif
// // set ADC prescale to 128, slows ADC samplerate, lightweight
// sbi(ADCSRA,ADPS2) ;
// sbi(ADCSRA,ADPS1) ;
// sbi(ADCSRA,ADPS0) ;
// free running adc
cbi(ADCSRB, ADTS3);
cbi(ADCSRB, ADTS2);
cbi(ADCSRB, ADTS1);
cbi(ADCSRB, ADTS0);
sbi(ADCSRA, ADEN); /* ADC enable */
sbi(ADCSRA, ADIE); /* ADC completed interrupt enable */
sbi(ADCSRA, ADSC); // kickstart conversion for the ADC
sei(); // enable interrupts
// Serial.begin(9600);
}
void loop(){
}
ISR(ADC_vect){
// sei(); /* enable global interrupt flag */
uint8_t low = ADCL;
uint8_t high = ADCH;
pota16 = (high << 8) | low;
}
ISR(TIMER1_COMPA_vect){
sndAcc++;
if(sndAcc <= (uint32_t)(((float)SAMPLERATE / FIXFREQ) / 2.f) ){
PORTD |= 0b00000001; // digitalWrite(SND, 1);
}
else{
PORTD &= 0b11111110; // digitalWrite(SND, 0);
}
if(sndAcc >= (uint32_t)((float)SAMPLERATE / FIXFREQ) ){
sndAcc = 0;
}
if(ledAcc == (uint32_t)((float)SAMPLERATE / 2.f) ){ /* flipping led's state 2times per second */
ledAcc = 0;
ledStatus = !ledStatus;
digitalWrite(LED, ledStatus);
// Serial.println(pota16);
}
else{
ledAcc++;
}
if(potAcc == (uint32_t)((float)SAMPLERATE / 30.f) ){ /* reading pot value 30 times per second */
potAcc = 0;
ADMUX = 0b11000001; /* Internal 2.56V Voltage Reference, MUX set to read from ADC1(pin A4) */
sbi(ADCSRA, ADSC); /* start conversion */
}
else{
potAcc++;
}
}
I also added a Serial.print() to check if I was actually reading changes from the potentiometer and yeah the serialprint itself does cause clicks, so I just commented it out
Adding sei() inside the ADComplete ISR doesn't seem to make a difference, I think that's because when the TIMER1_COMPA interrupt code finished running it automatically resets the global interrupt flag, and being faster than the ADC ISR, it resumes normal execution before it.
Things might change if I add code to the TIMER1_COMPA ISR, I guess, but I'm not really sure this assumption is correct overall.
The thing is that these two interrupts are running asynchronously. So you have to consider what will happen when they go off at exactly the same time, or very close together and which is the more important for you. So if the ADC one goes off just a fraction before the timer then that ISR will be running when the timer interrupt goes off. Clearing the interrupt in the ADC ISR will allow the timer to interrupt the ADC ISR. So an ISR is interrupted by another ISR which has a higher priority.
Some processors allow you to select the order of priority of ISRs but the ATmega is not one of these, so you have to resort to other tricks like this.
When the timer ISR returns, it will return to the ADC ISR which will then be vulnerable to being interrupted again. But when this returns everything is back to normal.
You won't notice this every time or even at all if the delay is very small, but in real time computing like this you have to consider all possibilities of what could happen and when.
yeah sure, it makes sense! I can decide which interrupt has the highest priority by making it the only one that doesn't reset the global flag until it's done.