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.

6
  • Thw calls to digitalWrite() are taking to long. It`s a rather big function. Use direct portmanipulation
    – chrisl
    Commented Jul 21, 2019 at 6:46
  • you should delete the previous timer settings. Arduino core initializes the timers for PWM and you do |=
    – Juraj
    Commented Jul 21, 2019 at 7:34
  • Why don't you do the toggling in the ISR? The time it takes the loop function to find the flag has changed won't be trivial.
    – Nick Gammon
    Commented Jul 21, 2019 at 7:36
  • @chrisl in the end I decided not to use the arduino core, things behave so weired... 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?
    – Sim Son
    Commented Jul 21, 2019 at 12:47
  • @NickGammon I started to reduce the code and it just remained there. How much cycles does such a boolean comparison actually take? Initially, I planned to read the frequency from a potentiometer after each interrupt, wouldn't this be easily possible in 8×83=664 cycles?
    – Sim Son
    Commented Jul 21, 2019 at 12:52

2 Answers 2

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
2
  • I'm not that new to uC programming, I just tend to use programing registers directly rather than using the arduino environment. I thought this little program might be faster to implement using the arduino core without having to look up things in the datasheet, but now I did so anyway. Concerning the adc: if it is in free-running mode, the hardware is doing the conversion in parallel. So if I only check the status bit for a completed conversion, this shouldn't be a problem and the actual time needed to handle the analog value would decrease a lot. Isn't that true?
    – Sim Son
    Commented Jul 23, 2019 at 16:13
  • I only need the analog value every 100ms and I'm not doing very much processing with it...
    – Sim Son
    Commented Jul 23, 2019 at 16:15
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
}
2
  • Thanks for your answer! Yes, I know the inverting mode, but I will soon have to be able to also change the frequency in a little range. I'm now doing direct port manipulation and it works fine so far. But a question: what is this _BV() you are using?
    – Sim Son
    Commented Jul 23, 2019 at 16:18
  • @SimSon: _BV(n) is a macro for (1<<(n)), from avr-libc. I guess it stands for “byte value”. I like to use it when writing AVR-idiomatic code. Commented Jul 23, 2019 at 17:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.