tone(), noTone()

Post your cool example code here.
enif
Posts: 31
Joined: Wed Jul 29, 2015 4:49 pm
Location: Switzerland
Contact:

tone(), noTone()

Postby enif » Fri Aug 14, 2015 1:26 pm

When trying to port my little "car blink sound booster" program to the Maple Mini, I realized that there are no tone() and noTone() functions available yet in the STM32-Arduino library - or at least, I could not find them...

Thus, here is my first attempt to implement Arduino compatible versions of the tone() and noTone() functions. Be warned that this is my first attempt dealing with the STM32 timers, so the code might still be far from optimal. However, it seems to work in my program, and it also tested well with the Arduino toneMelody.ino example:

Code: Select all

///////////////////////////////////////////////////////////////////////
//
// tone(pin,frequency[,duration]) generate a tone on a given pin
//
// noTone(pin)                    switch of the tone on the pin
//
///////////////////////////////////////////////////////////////////////

#include "Arduino.h"
#include <HardwareTimer.h>

#ifndef TONE_TIMER
#define TONE_TIMER 2
#endif

HardwareTimer tone_timer(TONE_TIMER);

bool tone_state = true;             // last pin state for toggling
short tone_pin = -1;                // pin for outputting sound
short tone_freq = 444;              // tone frequency (0=pause)
unsigned tone_micros = 500000/444;  // tone have wave time in usec
int tone_counts = 0;                // tone duration in units of half waves

// timer hander for tone with no duration specified,
// will keep going until noTone() is called
void tone_handler_1(void) {
    tone_state = !tone_state;
    digitalWrite(tone_pin,tone_state);
}

// timer hander for tone with a specified duration,
// will stop automatically when duration time is up.
void tone_handler_2(void) {   // check duration
    if(tone_freq>0){
       tone_state = !tone_state;
       digitalWrite(tone_pin,tone_state);
    }
    if(!--tone_counts){
       tone_timer.pause();
       pinMode(tone_pin, INPUT);
    }
}

//  play a tone on given pin with given frequency and optional duration in msec
void tone(uint8_t pin, unsigned short freq, unsigned duration = 0) {
   tone_pin = pin;
   tone_freq = freq;
   tone_micros = 500000/(freq>0?freq:1000);
   tone_counts = 0;

   tone_timer.pause();

   if(freq >= 0){
      if(duration > 0)tone_counts = ((long)duration)*1000/tone_micros;
      pinMode(tone_pin, OUTPUT);

      // set timer to half period in microseconds
      tone_timer.setPeriod(tone_micros);

      // Set up an interrupt on channel 1
      tone_timer.setChannel1Mode(TIMER_OUTPUT_COMPARE);
      tone_timer.setCompare(TIMER_CH1, 1);  // Interrupt 1 count after each update
      tone_timer.attachCompare1Interrupt(tone_counts?tone_handler_2:tone_handler_1);

      // Refresh the tone timer
      tone_timer.refresh();

      // Start the timer counting
      tone_timer.resume();
   } else
      pinMode(tone_pin, INPUT);
}

// disable tone on specified pin, if any
void noTone(uint8_t pin){
    tone(pin,-1);
}

User avatar
RogerClark
Posts: 5303
Joined: Mon Apr 27, 2015 10:36 am
Location: Melbourne, Australia
Contact:

Re: tone(), noTone()

Postby RogerClark » Fri Aug 14, 2015 8:57 pm

@enif

Thanks

After we know its stable, I will integrate it into the core.

victor_pv
Posts: 887
Joined: Mon Apr 27, 2015 12:12 pm

Re: tone(), noTone()

Postby victor_pv » Sat Aug 15, 2015 2:10 am

Enif, half the pins in the maple mini can be set as PWM output to one of the timers. A few can not, though, and would rather need a software implementation to get a tone on them. But for the ones that can be set as output of a timer channel, it could be easier to get them to generate the frequency directly that way.

Look at the leaflabs hardware timer documentation for that.

If you want to find which Timer device and timer channel corresponds to a certain pin, you can use pin_map, such as:

For the timer device:
timer_dev * mytimer = PIN_MAP[Pin].timer_device;

And for the channel number:
uint8 mychannel = PIN_MAP[Pin].timer_channel;

You can then manipulate the registers directly, or you can create a HardwareTimer object corresponding to that timer_device, with something like:
uint8 timer_num = (mytimer - TIMER1) + 1;
HardwareTimer My_timer_object(timer_num);

So you set the pin as PWM output, and then manipulate the timer and timer_channel settings (or the hardware timer object) corresponding to that timer to adjust the frequency you want.
I believe the leaflabs documentation may have some example of generating a certain frequency on a pin.

For any ping for which PIN_MAP returns NULL for the timer_device or timer_channel, means that there is no hardware timer associated with it.
You can rely on the use to check that, or perhaps check for it in your code.

enif
Posts: 31
Joined: Wed Jul 29, 2015 4:49 pm
Location: Switzerland
Contact:

Re: tone(), noTone()

Postby enif » Sat Aug 15, 2015 6:42 am

Thanks a lot, Victor. I was vaguely aware of the relation between the timers and the PWM pins, but it helps to have the actual coding how to access the corresponding timers and channels from the program.

My implementation is indeed just a quick+dirty approach to at least get my Nano sketch working on the Maple Mini. But I thought that it might still be useful to post it under "code snipplets", as it might also help others until an official version of tone()/noTone() will become available...

I am sure that generating the tone directly with the timer/channel that is associated with the given pin would be more efficient. However, I am afraid that the problem would remain, that even so, using tone() would still disturb the PWM output on the other 3 pins sharing the same timer. So my worry is less getting the most efficient code, but rather being able to minimize the interference it causes with other uses of the timers. So my idea was at least to have some possibility use an alternate timer (with #define TONE_TIMER), in case the default timer is disturbing PWM pins needed by the program.

How would one implement tone() on a given timer/channel without compromising the PWM on the other pins associated with that timer?

victor_pv
Posts: 887
Joined: Mon Apr 27, 2015 12:12 pm

Re: tone(), noTone()

Postby victor_pv » Sat Aug 15, 2015 1:01 pm

enif wrote:Thanks a lot, Victor. I was vaguely aware of the relation between the timers and the PWM pins, but it helps to have the actual coding how to access the corresponding timers and channels from the program.

My implementation is indeed just a quick+dirty approach to at least get my Nano sketch working on the Maple Mini. But I thought that it might still be useful to post it under "code snipplets", as it might also help others until an official version of tone()/noTone() will become available...

I am sure that generating the tone directly with the timer/channel that is associated with the given pin would be more efficient. However, I am afraid that the problem would remain, that even so, using tone() would still disturb the PWM output on the other 3 pins sharing the same timer. So my worry is less getting the most efficient code, but rather being able to minimize the interference it causes with other uses of the timers. So my idea was at least to have some possibility use an alternate timer (with #define TONE_TIMER), in case the default timer is disturbing PWM pins needed by the program.

How would one implement tone() on a given timer/channel without compromising the PWM on the other pins associated with that timer?


If the other pins associated with the same timer are being used for PWM, you can't change the frequency without affecting them, as the reload value is one for the timer :(
Each output has its own CC register, that will control how long the output is low or high, but not the frequency. In that case your software implementation would be the best. You may want to change the digitalWrite for something more efficient in the ISR, as digitalWrite is very inefficient and at high frequencies the ISR may take too much time away from the main code.
This is how the display libraries gets the port and mask corresponding to a pin to avoid using digitalWrite:
csport = portOutputRegister(digitalPinToPort(_cs));
cspinmask = digitalPinToBitMask(_cs);

csport ->PIO_CODR |= cspinmask; // this one sets it low.

I dont remember off the top of my head all the registers that control a port but I think there are 4, one to set the mode, one to set (1 sets it hgh), one to reset (1 sets it low), and one that would be direct out (1 high, 0 low).

User avatar
RogerClark
Posts: 5303
Joined: Mon Apr 27, 2015 10:36 am
Location: Melbourne, Australia
Contact:

Re: tone(), noTone()

Postby RogerClark » Sat Aug 15, 2015 9:10 pm

AFIK

Ports have 4 registers

The control reg, to set the pin mode
The ODR, Output data register (lower 16 bits only)
The IDR, Input data register ( again only the lower 16 bits)
The BSSR, which can set and reset individual bits ( its split into the upper and lower 16 bits. I cant remember off the top of my head, which 16 bits is the Set pattern, and which 16 bits is the reset (low) pattern. But using the BSSR is the fastest way to set or reset individual bits, as it doesnt require the code to readback the existing bit pattern before you OR in ( or AND in) a new pattern. Hence its about 3 times faster.
So I'd recommend if you are doing an ISR based tone generator, just have one variable with the BSSR pattern to set the bit, and another var which stores the reset pattern, and set those up when the tone is started, and use them in your ISR to set or reset the bit in question.

victor_pv
Posts: 887
Joined: Mon Apr 27, 2015 12:12 pm

Re: tone(), noTone()

Postby victor_pv » Sun Aug 16, 2015 2:16 am

enif wrote:... So my worry is less getting the most efficient code, but rather being able to minimize the interference it causes with other uses of the timers. So my idea was at least to have some possibility use an alternate timer (with #define TONE_TIMER), in case the default timer is disturbing PWM pins needed by the program.

How would one implement tone() on a given timer/channel without compromising the PWM on the other pins associated with that timer?


Given that using any timer will affect the PWM output in 4 pins, I am not sure the software implementation using a timer to call an interrupt is much better than using timer OC function. Using the OC you would disrupt only 3 pins, as 1 of them would be the one purposely controlled, while using a timer to time the output of a different pin, would disrupt 4 other pins.
There is a systick timer that doesn't affect any pin, but I am dont remember if that's used for the run milliseconds count. It may be.
Search in the code where the systick is used, you may be able to set it to a much higher frequency, and then divide that by a number to obtain the desired frequency. It may already be like that...

enif
Posts: 31
Joined: Wed Jul 29, 2015 4:49 pm
Location: Switzerland
Contact:

Re: tone(), noTone()

Postby enif » Sun Aug 16, 2015 7:15 pm

Thanks for your feedback and input, Victor and Roger!

I have done some more thinking about this, and ended up with a different implementation which only uses one channel of one timer and does not affect the PWM operation of the pins sharing the same timer. The idea is to keep the timer itself unchanged, i.e. not changing its period, but leaving it free running on the 16 bit count register (which is what PWM uses). The frequency is generated by modifying the channel's compare register on the fly inside the interrupt handler, kind of "hopping" from half wave to halve wave. It becomes a bit more complicated when the frequency is less than 550Hz (=36000000/65536), since then more than one interrupt must pass before the output is toggled, so the period of one half wave is divided in junks of less than 65536/36000000 sec.

Using the info given by Victor, the current code will use the corresponding PWM timer/channel for PWM pins, and a default timer/channel for the other pins. This default is now set to Timer4/Channel4, since that one is not used by any PWM pin. However,
I don't know at all, whether that channel is already used for something else (Do you know??). If it isn't, we could just as well use Timer4/Channel4 for all the pins... In any case, I have also added a function setToneTimerChannel(timer,channel) to optionally force using a specific timer/channel combination.

I have also implemented the BSRR register write, as suggested by Roger. If macro USE_BSRR is defined, BSRR is used, otherwise uses digitalWrite() - which might be useful for testing and benchmarking.


Code: Select all

///////////////////////////////////////////////////////////////////////
//
// tone(pin,frequency[,duration])     generate a tone on a given pin
//
// noTone(pin)                        switch off the tone on the pin
//
// setToneTimerChannel(timer,channel) force use of given timer/channel
//
///////////////////////////////////////////////////////////////////////

#include "Arduino.h"
#include <HardwareTimer.h>

// define default timer and channel
#ifndef TONE_TIMER
#define TONE_TIMER 4
#endif
#ifndef TONE_CHANNEL
#define TONE_CHANNEL 4
#endif

#define PinTimer(pin) (PIN_MAP[pin].timer_device->clk_id-RCC_TIMER1+1)
#define PinChannel(pin) (PIN_MAP[pin].timer_channel)

// if USE_PIN_TIMER is set, the PWM timer/channel is used for PWM pins
#define USE_PIN_TIMER

// if USE_BSRR is set the tone pin will be written via the fast BSRR register
// instead of using the slow digitalWrite() function in the interrupt handler
#define USE_BSRR

// construct static timer array (
HardwareTimer TTimer1(1), TTimer2(2), TTimer3(3), TTimer4(4);
#ifdef STM32_HIGH_DENSITY
HardwareTimer TTimer5(5), TTimer6(6), TTimer7(7), TTimer8(8);
#endif
HardwareTimer *TTimer[4] {
   &TTimer1,&TTimer2,&TTimer3,&TTimer4
#ifdef STM32_HIGH_DENSITY
  ,&TTimer5,&TTimer6,&TTimer7,&TTimer8
#endif
   };

uint8_t tone_force_channel = 0;                 // forced timer channel
uint8_t tone_force_ntimer = 0;                  // forced timer

HardwareTimer *tone_timer = TTimer[TONE_TIMER]; // timer used to generate frequency
uint8_t tone_channel = TONE_CHANNEL;            // timer channel used to generate frequency
uint8_t tone_ntimer = TONE_TIMER;               // timer used to generate frequency

bool tone_state = true;             // last pin state for toggling
short tone_pin = -1;                // pin for outputting sound
short tone_freq = 444;              // tone frequency (0=pause)
uint32_t tone_nhw = 0;              // tone duration in number of half waves
uint16_t tone_tcount = 0;           // time between handler calls in 1/36 usec
uint16_t tone_ncount = 0;           // handler call between toggling
uint16_t tone_n = 0;                // remaining handler calls before toggling
uint32_t tone_next = 0;             // counter value of next interrupt

#ifdef USE_BSRR
volatile uint32_t *tone_bsrr;       // BSRR set register (lower 16 bits)
uint32_t tone_smask=0;              // BSRR set bitmask
uint32_t tone_rmask=0;              // BSRR reset bitmask
#endif
 

////////////////////////////////////////////////////////////////////////////////
// timer hander for tone with no duration specified,
// will keep going until noTone() is called
void tone_handler_1(void) {
   tone_next += tone_tcount;            // comparator value for next interrupt
   tone_timer->setCompare(tone_channel, tone_next); // and install it
   if(--tone_n == 0){
      tone_state = !tone_state;         // toggle tone output

#ifdef USE_BSRR
      if(tone_state)
         *tone_bsrr = tone_smask;
      else
         *tone_bsrr = tone_rmask;
#else
      digitalWrite(tone_pin,tone_state);// and output it
#endif

      tone_n = tone_ncount;             // reset interrupt counter
   }
}

////////////////////////////////////////////////////////////////////////////////
// timer hander for tone with a specified duration,
// will stop automatically when duration time is up.
void tone_handler_2(void) {
   tone_next += tone_tcount;
   tone_timer->setCompare(tone_channel, tone_next);
   if(--tone_n == 0){
      if(tone_freq>0){ // toggle pin
         tone_state = !tone_state;
#ifdef USE_BSRR
         if(tone_state)
            *tone_bsrr = tone_smask;
         else
            *tone_bsrr = tone_rmask;
#else
         digitalWrite(tone_pin,tone_state);// and output it
#endif
       }
       tone_n = tone_ncount;
       if(!--tone_nhw){ // check if tone duration has finished
          tone_timer->pause();       // disable timer
          pinMode(tone_pin, INPUT); // disable tone pin
       }
    }
}

////////////////////////////////////////////////////////////////////////////////
//  play a tone on given pin with given frequency and optional duration in msec
void tone(uint8_t pin, short freq, unsigned duration = 0) {
   tone_pin = pin;

#ifdef USE_PIN_TIMER
   // if the pin has a PWM timer/channel, use it (unless the timer/channel are forced)
   if(PinChannel(tone_pin) && !tone_force_channel){
      tone_channel = PinChannel(tone_pin);
      tone_ntimer = PinTimer(tone_pin);
   } else
#endif
   {
      // set timer and channel to default resp values forced with setToneTimerChannel
      tone_ntimer = tone_force_channel?tone_force_ntimer:TONE_TIMER;
      tone_channel = tone_force_channel?tone_force_channel:TONE_CHANNEL;
   }

   tone_timer = TTimer[tone_ntimer-1];
   tone_freq = freq;
   tone_nhw = 0;
   tone_next = 0;

   tone_timer->pause();

   if(freq > 0 || duration >0 ){
      uint32_t count = 18000000/freq;       // timer counts per half wave
      tone_ncount = tone_n = (count>>16)+1; // number of 16-bit count chunk
      tone_tcount = count/tone_ncount;      // size of count chunk
      if(duration > 0) // number of half waves to be generated
         tone_nhw = ((duration*(freq>0?freq:100))/1000)<<1;
      else  // no duration specified, continuous sound until noTone() called
         tone_nhw = 0;

      pinMode(tone_pin, PWM);    // configure output pin
      pinMode(tone_pin, OUTPUT); // configure output pin

#ifdef USE_BSRR
      // Set up BSRR register values for fast ISR
      tone_bsrr = &((PIN_MAP[tone_pin].gpio_device)->regs->BSRR);
      tone_smask = (BIT(PIN_MAP[tone_pin].gpio_bit));
      tone_rmask = tone_smask<<16;
#endif

      // Set up an interrupt on given timer and channel
      tone_next = tone_tcount; // prepare channel compare register
      tone_timer->setMode(tone_channel,TIMER_OUTPUT_COMPARE);
      tone_timer->setCompare(tone_channel,tone_next);
      // attach corresponding handler routine
      tone_timer->attachInterrupt(tone_channel,tone_nhw?tone_handler_2:tone_handler_1);

      // Refresh the tone timer
      tone_timer->refresh();

      // Start the timer counting
      tone_timer->resume();
     
   } else {

      // detach handler routine
      tone_timer->detachInterrupt(tone_channel);
      // disactive pin by configuring it as input
      pinMode(tone_pin, INPUT);

   }
}

////////////////////////////////////////////////////////////////////////////////
// disable tone on specified pin, if any
void noTone(uint8_t pin){
    tone(pin,-1);  // it's all handled in tone()
}

////////////////////////////////////////////////////////////////////////////////
// set timer and channel to some different value
// must be called before calling tone() or after noTone() was called
void setToneTimerChannel(uint8_t ntimer, uint8_t channel){
   tone_force_ntimer = ntimer;
   tone_force_channel = channel;
}

victor_pv
Posts: 887
Joined: Mon Apr 27, 2015 12:12 pm

Re: tone(), noTone()

Postby victor_pv » Sun Aug 16, 2015 8:25 pm

That channel goes to pb9, which is used for discovery in the maple mini. So that is safe enough in the mini, as that pin should never be used for PWM in that board, but on other boards it may be used.
You may want to put a warning comment or something, for people using a different maple, or one of the many other stm32 boards available, but is nice that you already found a way to affect only 1 timer channel :)

User avatar
ekawahyu
Posts: 87
Joined: Wed Apr 13, 2016 6:17 am

Re: tone(), noTone()

Postby ekawahyu » Mon Jul 11, 2016 4:00 pm

@enif: What is the status of this work? Is it available somewhere on github and stable?


Return to “Code snipplets”

Who is online

Users browsing this forum: No registered users and 1 guest