1

I need to control a 24 kHz ultrasonic transducer and decided to use an arduino uno I have lying around.

Using tone() or the atmega's hardware pwm is not very suitable because I need two 50/50 square waves with a phase difference of pi (which is equal to two XOR'ed pins) to switch the output polarity of the h-bridge synchronously. As the uC dosen't have anything else to do I wrote the following sketch where I just wait for an interrupt and toggle pins 2&3:

bool do_task=false;
void timer_init () {
  TCCR0A |= (1 << WGM01);
  OCR0A = 83;
  TIMSK0 |= (1 << OCIE0A);
  TCCR0B |= (1 << CS01);
}
ISR (TIMER0_COMPA_vect) {
  do_task=true;
}

bool state=false;
void toggle() {
  state=!state;
  digitalWrite(2,state);
  digitalWrite(3,!state);

}

void setup() {
  pinMode(2,OUTPUT);
  pinMode(3,OUTPUT);
  timer_init();
}

void loop() {
  if(do_task) {
    do_task=false;
    toggle();
  }
}

I expected the output to have a frequency of ~24 kHz, but it is 500 Hz instead.

From my understanding:

  • 16 MHz CPU frequency
  • timer0 prescaler is 8 (TCCR0B |= (1 << CS01))
  • output compare value (OCR0A) is 83

would result in a frequency of 16 MHz / 8 / 83 = 24096,4 kHz -but as I mentioned- it's 500 Hz and doesn't seem to depend on the prescaler.

Sim Son
  • 1,878
  • 13
  • 21

2 Answers2

3

You wrote:

Using [...] the atmega's hardware pwm is not very suitable because I need two 50/50 square waves with a phase difference of pi

I believe hardware PWM is the best solution to your problem. The PWM outputs can be set to either “inverting” or “non-inverting” mode. Set the two channels of a single timer to the same duty cycle, with one in non-inverting mode and the other one in inverting mode, and you have your complementary outputs.

Example using Timer 1:

const float SIGNAL_FREQUENCY = 24e3;
const uint16_t HALF_PERIOD = round(F_CPU / SIGNAL_FREQUENCY / 2);
const uint16_t PERIOD = 2 * HALF_PERIOD;

int main()
{
    /* Set the PWM pins as outputs. */
    DDRB |= _BV(PB1);  // digital  9 = PB1 = OC1A
    DDRB |= _BV(PB2);  // digital 10 = PB2 = OC1B

    /* Configure Timer 1. */
    ICR1   = PERIOD   - 1;
    OCR1A  = PERIOD/2 - 1;
    OCR1B  = PERIOD/2 - 1;
    TCCR1A = _BV(COM1A1)  // non-inverting PWM on OC1A
           | _BV(COM1B0)  // inverting PWM on OC1B
           | _BV(COM1B1)  // ditto
           | _BV(WGM11);  // mode 14: fast PWM, TOP = ICR1
    TCCR1B = _BV(WGM12)  // ditto
           | _BV(WGM13)  // ditto
           | _BV(CS10);  // clock at F_CPU, start timer
}
Edgar Bonet
  • 45,094
  • 4
  • 42
  • 81
2

what exactly is it that makes a simple function like digitalWrite() so damn slow??! Why isn't it just a wrapper function for direct port manipulation?

Well, it can take a variable as an argument. And the contents of the variable have to be looked up in a table to see which port and which bit.

If you use the DigitalWriteFast library then that turns your writes into direct port manipulation providing you use constants.

So you can use:

digitalWriteFast(2,HIGH);  // or LOW

But not:

digitalWriteFast(myPort,someState);  // assuming they are not constants

In fact you can send variables, but it degrades into the same slow calls that digitalWrite uses.


I now decided not to use the arduino core

The core is absolutely fine. However you need to understand what it is doing. To be nice to newbies things like digitalWrite can take variables as arguments, so this sort of thing is possible:

for (int i = 0; i < 13; i++)
  digitalWrite (i, HIGH);

As I hinted in a comment, you could do it all in the ISR:

ISR (TIMER0_COMPA_vect) {
  static bool state;
  if (state)
    {
    digitalWriteFast(2,HIGH);
    digitalWriteFast(3,LOW);
    state = false; 
    }
  else
    {
    digitalWriteFast(2,LOW);
    digitalWriteFast(3,HIGH);
    state = true; 
    }
}  // end of TIMER0_COMPA_vect

Initially, I planned to read the frequency from a potentiometer after each interrupt, wouldn't this be easily possible in 8×83=664 cycles?

Analog reads (reads from the ADC) take around 104µs using the default ADC prescaler. That is, 1664 clock cycles. So, no, you can't read in that time. That is a hardware limitation. You can reduce the prescaler without losing much accuracy.

See my page about the ADC. Making the prescaler 16 rather than 128 will significantly increase speed with minimal accuracy loss.

To do that on the Uno:

ADCSRA &= ~(bit (ADPS0) | bit (ADPS1) | bit (ADPS2)); // clear prescaler bits
ADCSRA |= bit (ADPS2);                               //  16
Nick Gammon
  • 38,901
  • 13
  • 69
  • 125