How to program piezo buzzer without the tone() library?

Hi everyone,

As I mentioned in a couple of other topics - I am working on a "Simon says" handheld game.
For those who are not familiar - its a kind of game where the computer blinks different lights in a particular sequence and the player then needs to repeat it by pressing the buttons in the correct order.

Anyway, I though it would be really cool to add some sound to the game, for example game could make different sounds for different colours, so it would be easier to memorize the sequence and repeat it.
Or also play game over / victory music.

My project does have a limitation though - I want to transfer this game to an attiny45 chip which has much less memory and RAM available compared to the 328p chips. So unfortunately I can not use the tone() library, a simple project that plays 3 notes with it weights at least 2.5K and that is what my whole game currently weighs.

I decided to write my own implementation to keep the size of the whole project small.

Unfortunately I don't know much about how piezo speakers work, I hope someone could put me on the right track.
My main concerns at this point:

  • I don't think its a good idea to use PWM for this because from what I understand - changing frequency quickly and to specific rates can be quite a complex task and may require a lot of code - please correct me if I am wrong on this one.

  • Because of that I think it is best to just manually set the pin on/off inside the loop, changing the time between switches to change the frequency. Do you think this is a good idea?

Thanks in advance!

If the piezo buzzer is not one that produces its own tone when a voltage is applied across it then you need to turn the voltage on and off at the rate required to produce the required tone(s). PWM is not really the right tool for this job. Have you tried outputting a tone using the BlinkWithoutDelay principle ? Turn on the pin, wait a bit (without using delay()), turn off the pin, wait a bit and so on ?

Hi,
Why transfer it to a smaller chip?
Tom.... :slight_smile:

UKHeliBob:
If the piezo buzzer is not one that produces its own tone when a voltage is applied across it then you need to turn the voltage on and off at the rate required to produce the required tone(s). PWM is not really the right tool for this job. Have you tried outputting a tone using the BlinkWithoutDelay principle ? Turn on the pin, wait a bit (without using delay()), turn off the pin, wait a bit and so on ?

Yep, that's pretty much my current plan. Just wanted to confirm that I am not doing something stupid that already has better solutions.

TomGeorge:
Hi,
Why transfer it to a smaller chip?
Tom.... :slight_smile:

Well its gonna be a handheld, so I was trying to pick something smaller and to be honest I think a 328 chip is an overkill for this project.

YemSalat:

  • Because of that I think it is best to just manually set the pin on/off inside the loop, changing the time between switches to change the frequency. Do you think this is a good idea?

That's exactly what the tone() function is doing: Switching a pin HIGH and LOW all the time, so on the output pin a 'square wave' is generated.

But while the tone() function is using interrupts for doing so, and allows "playing a tone in the background" while other code in your program gets executed, you could create a blocking function that runs in the foreground, generates the tone, and after the tone is done, the next code is executed. Pretty easy.

jurs:
That's exactly what the tone() function is doing: Switching a pin HIGH and LOW all the time, so on the output pin a 'square wave' is generated.

But while the tone() function is using interrupts for doing so, and allows "playing a tone in the background" while other code in your program gets executed, you could create a blocking function that runs in the foreground, generates the tone, and after the tone is done, the next code is executed. Pretty easy.

Thanks!

Since I already keep track of current milliseconds - I think I can just check whether it divides without remainder by the required frequency. E.g.:

if (currentMilliseconds % requiredFrequency == 0) {
    // SET PIN HIGH/LOW
}

Haven't tested this yet though, just a thought.

YemSalat:
Since I already keep track of current milliseconds - I think I can just check whether it divides without remainder by the required frequency. E.g.:

No chance. If you want to generate tones in different frequencies with an accurate timing, you will need to be accurate down to a few microseconds, so you will need the micros() function to create the timing.

jurs:
No chance. If you want to generate tones in different frequencies with an accurate timing, you will need to be accurate down to a few microseconds, so you will need the micros() function to create the timing.

Thanks for the advice! I guess I’ll have to write something that runs independentlyfrom the main loop (I hope I’ll have enough memory!)

YemSalat:
Thanks for the advice! I guess I’ll have to write something that runs independentlyfrom the main loop

I don’t know what you mean by “runs independently from the main loop”.

If you would write a function that could create a tone interrupt driven in the background while the rest of the program is executing at the same time, you will probable need the same flash size for the program as the inventor of the tone() function needs.

If you want to write a blocking function that blocks the rest of the code from execution while the tone plays, it is as easy as that:

void myTone(byte pin, uint16_t frequency, uint16_t duration)
{ // input parameters: Arduino pin number, frequency in Hz, duration in milliseconds
  unsigned long startTime=millis();
  unsigned long halfPeriod= 1000000L/frequency/2;
  pinMode(pin,OUTPUT);
  while (millis()-startTime< duration)
  {
    digitalWrite(pin,HIGH);
    delayMicroseconds(halfPeriod);
    digitalWrite(pin,LOW);
    delayMicroseconds(halfPeriod);
  }
  pinMode(pin,INPUT);
}

void setup() {
}

void loop() {
  myTone(8,1000, 2000); // tone on pin-8 with 1000 Hz for 2000 milliseconds
  delay(5000);
}

Only the duration is timed using ‘millis()’, but the frequency generation is controlled very accurate up to a few microseconds by using ‘delayMicroseconds()’.

If you need to save even more flash memory, it would be a good idea not to use the Arduino comfort functions like “pinMode()”, “digitalWrite()”, “millis()” or “delayMicroseconds()”, but to create a program from low-level functions and direct register programming only.

1 Like

jurs:
I don’t know what you mean by “runs independently from the main loop”.

Sorry, my bad, I don’t mean the loop() function, I was talking about my internal game timing system within the loop.

jurs:
If you would write a function that could create a tone interrupt driven in the background while the rest of the program is executing at the same time, you will probable need the same flash size for the program as the inventor of the tone() function needs.

Nah, I’d stay out of there for now, I am already not too sure if its gonna fit on the chip and not overflow the memory (256B!) and I also got more functionality planned xP

jurs:
If you want to write a blocking function that blocks the rest of the code from execution while the tone plays, it is as easy as that:

void myTone(byte pin, uint16_t frequency, uint16_t duration)

{ // input parameters: Arduino pin number, frequency in Hz, duration in milliseconds
  unsigned long startTime=millis();
  unsigned long halfPeriod= 1000000L/frequency/2;
  pinMode(pin,OUTPUT);
  while (millis()-startTime< duration)
  {
    digitalWrite(pin,HIGH);
    delayMicroseconds(halfPeriod);
    digitalWrite(pin,LOW);
    delayMicroseconds(halfPeriod);
  }
  pinMode(pin,INPUT);
}

void setup() {
}

void loop() {
  myTone(8,1000, 2000); // tone on pin-8 with 1000 Hz for 2000 milliseconds
  delay(5000);
}




Only the duration is timed using 'millis()', but the frequency generation is controlled very accurate up to a few microseconds by using 'delayMicroseconds()'.

Thanks, that’s very useful!
Wouldn’t it be better to increment a variable every step for example or check micros() instead of millis() in order to get below milliseconds, this way it would be able to stay ‘async’
Or would that not be acurate enough?

If you need to save even more flash memory, it would be a good idea not to use the Arduino comfort functions like “pinMode()”, “digitalWrite()”, “millis()” or “delayMicroseconds()”, but to create a program from low-level functions and direct register programming only.

I know, but I am not ready to dive that deep yet :slight_smile:

Does it have to be ATtiny45? ATtiny85 has twice the memory of each kind.
Or is this a case of "this is what I already have"?

YemSalat:
Wouldn't it be better to increment a variable every step for example or check micros() instead of millis() in order to get below milliseconds, this way it would be able to stay 'async'

There are always different ways to do something.
The code I posted was just one suggestion how to play a tone without using the Arduino tone() function.

YemSalat:
I know, but I am not ready to dive that deep yet :slight_smile:

If you want to run a program on a controller with just 256 bytes of SRAM, I'd better not rely too much on the functions of the Arduino core library. The functions of the Arduino core library are invented with the intention to be "platform compatible". So (in many cases) the same code can compile and execute with a 8-bit Atmega328, a 8-bit Atmega2560 or a 32-bit SAM3X8E (Arduino DUE) without any changes. The Arduino core library functions are not invented to save RAM or execution time. Quite the opposite is true: When using Arduino core library functions like "pinMode" or "digitalWrite" or many others, you will waste a lot of SRAM memory and a lot of execution time for being 'compatible' with the code to different types of microcontrollers.

oqibidipo:
Does it have to be ATtiny45? ATtiny85 has twice the memory of each kind.
Or is this a case of "this is what I already have"?

Yes, pretty much, the whole project is just a collection of spare parts that I had.
I could of course order an 85, it is not that expensive, but part of this project is also the "challenge" of fitting everything into a smaller space.

jurs:
If you want to run a program on a controller with just 256 bytes of SRAM, I'd better not rely too much on the functions of the Arduino core library. The functions of the Arduino core library are invented with the intention to be "platform compatible". So (in many cases) the same code can compile and execute with a 8-bit Atmega328, a 8-bit Atmega2560 or a 32-bit SAM3X8E (Arduino DUE) without any changes. The Arduino core library functions are not invented to save RAM or execution time. Quite the opposite is true: When using Arduino core library functions like "pinMode" or "digitalWrite" or many others, you will waste a lot of SRAM memory and a lot of execution time for being 'compatible' with the code to different types of microcontrollers.

Hm, thats great food for thought, thanks!

In case I do want to try it on my own - would I need to look up specific commands/etc. for my particular chip, or are there just general ways of doing things for, say, all AVRs?

YemSalat:
In case I do want to try it on my own - would I need to look up specific commands/etc. for my particular chip, or are there just general ways of doing things for, say, all AVRs?

Generally, all Attiny and Atmega controllers have similar hardware like:

  • flash memory to store program code
  • SRAM memory to deal with variables
  • EEPROM memory as a non-volatile storage area during power-off
  • timers to handle timing, automatic counting and PWM
  • hardware and pinchange interrupts to handle asynchronous input
  • port registers for input and output handling
    and in many cases additional hardware for interaction with other standard hardware like
  • serial input/output
  • I2C input/output
  • SPI input/output

Which hardware is available on which controller and what is the name for it is always documented in the "data sheet" of the controller such like the Attiny45 datasheet.

Fortunately, you will not need much of that datasheet for just setting some pins high and low. But programming on a lower level of compatibility with Port-/Pinregisters and direct register access is a bit different from just using Arduinos comfort functions like "digitalWrite()". But low-level programming can save you a lot of RAM and flash usage compared to a program using the Arduino core library functions.

So learning to make a program for a much smaller microcontroller than an Atmega328 is mostly learning to avoid Arduino functions and using low-level functions instead. Otherwise you will run out of RAM and/or out of flash size before your program is ready.

But I think you already noticed that when trying to include the Arduino 'tone()' function into your program.

jurs:
When using Arduino core library functions like "pinMode" or "digitalWrite" or many others, you will waste a lot of SRAM memory...

The functions pinMode and digitalWrite only use a very small amount of RAM on the stack, temporarily.

Now this is a bit confusing :slight_smile:

Also, when I am looking for avr tutorials (like this one), most of them just do: #include <avr/io.h>
Isn’t that pretty much the same as using the Arduino libraries?

No, io.h comes from Atmel, and contains only real basics. That's where the #defines for all the register names and bits come from.

Can't you just program timer 1 directly to generate a square wave for the tone(s) you want? That seems like the leanest approach to me.

DrAzzy:
No, io.h comes from Atmel, and contains only real basics. That's where the #defines for all the register names and bits come from.

Ah, ok cool :slight_smile:

jboyton:
Can't you just program timer 1 directly to generate a square wave for the tone(s) you want? That seems like the leanest approach to me.

I would love to but I am really new to this stuff, so trying to a bit at a time.

Right now I can't make sense of how I would interact with a timer from the main code.

Maybe it's time to learn: Gammon Forum : Electronics : Microprocessors : Timers and counters