Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple tone(), analogWrite(), and Servo #4640

Merged
merged 13 commits into from
Jun 8, 2018

Conversation

earlephilhower
Copy link
Collaborator

@earlephilhower earlephilhower commented Apr 13, 2018

-edit 5/14 to remove WIP -

Remove and rewrite all the parts of the core/libraries using TIMER1
and consolidate into a single, shared waveform generation interrupt
structure. Tone, analogWrite(), Servo all now just call into this
shared resource to perform their tasks.

This setup enables multiple tones, analogWrites, servos, and
to be concurrent with reasonable accuracy. It uses both TIMER1
and the internal ESP cycle counter to handle timing of waveform edges.
TIMER1 is used in non-reload mode and only edges cause interrupts. The
interrupt is started and stopped as required, minimizing overhead when
these features are not being used.

A generic "startWaveform(pin, high-US, low-US, runtime-US)" and
"stopWaveform(pin)" allow for further types of interfaces.

Using a simple USB DSO shows that with only a single channel running,
a "tone(1000)" generates a waveform of 999.8Hz (500.1us per high/low).
Running additional generators or steppers will potentially increase the
jitter when multiple edges need to be generated in close proximity (.5-1us).

Remove and rewrite all the parts of the core/libraries using TIMER1
and consolidate into a single, shared waveform generation interrupt
structure.  Tone, analogWrite(), Servo all now just call into this
shared resource to perform their tasks.

This setup enables multiple tones, analogWrites, servos, and stepper
motors to be controlled with reasonable accuracy.  It uses both TIMER1
and the internal ESP cycle counter to handle timing of waveform edges.
TIMER1 is used in non-reload mode and only edges cause interrupts.  The
interrupt is started and stopped as required, minimizing overhead when
these features are not being used.

Adds in control of DRV8825 or A4988 interfaces stepper motors to the
timer infrastructure.  The interrupt handles acceleration and velocity
of stepper pulses including jerk (dA/dt) calculations, but it is up
to the user application to do path planning and generate S-curve
accelerations.  All stepper DIR pins are connected together, and then
the STEP from each stepper interface board is routed to an individual
pin.  For motions which involve multiple steppers, there is an option
to wait before beginning the next stepper motion until all active
steppers have completed their moves (i.e. moving to an X, Y where there
may be 1-2us delay between the X arrival and Y arrival).

A generic "startWaveform(pin, high-US, low-US, runtime-US)" and
"stopWaveform(pin)" allow for further types of interfaces.

Using a simple USB DSO shows that with only a single channel running,
a "tone(1000)" generates a waveform of 999.8Hz (500.1us per high/low).
Running additional generators or steppers will potentially increase the
jitter.

The implementation also is limited in the time of the shortest pulse
it can generate (mostly an issue for analogWrite at values <1% or
>99% of the scale).  Presently measurements of ~2-3us are seen on
the shortest of pulses.  There is no optimization to allow generating
two edges on a single interrupt, which leads to this minimum pulse
width.

The current implementation was written focusing on readability and
flexibility, not squeezing every cycle out of interrupt loops. There
are probably many places where the code could be optimized (after it's
fully validated!) to reduce CPU load and timing errors.
@AlfonsMittelmeyer
Copy link

@earlepillhower

you wrote:

At a minimum, it may give some ideas to
@AlfonsMittelmeyer and his
assembly-level work.

---snip--- Remove and rewrite all the parts of the core/libraries using TIMER1 and consolidate into a single, shared waveform
generation interrupt structure. Tone, analogWrite(), Servo all now
just call into this shared resource to perform their tasks. ...
...
The implementation also is limited in the time of the shortest pulse
it can generate (mostly an issue for analogWrite at values <1% or
99% of the scale). Presently measurements of ~2-3us are seen for shortest of pulses. There is no optimization to allow generating two
edges on a single interrupt, which leads to this minimum pulse
width.

It seams, you didn't quite catch my idea. It wasn't not only to have more timers, but especially high accuracy timers. Two timers are meant for Arduino, which has tone and pwm. Two further timers are meant to be used by implementations like yours. One timer may be declared to be a high accuracy timer with no jitter. The minimum time between the end if the timer isr callback to the begin of the next isr callback again is 20 CPU ticks (for 80 MHz). This would mean, that PWM pulses also for analogWrite(1) with a correct duration of 78 ticks should be possible. This is possible, because short pulses are performed by a loop within the timer1 isr. Such short timer steps in a loop may be used for PWM pulses, for unregularely fast following or overlapping timers, but not for square waves with higher frequencies as maybe 50 kHz, because not leaving the loop would cause a watchdog reset. In case, that there shall not be something else like normal code context, wifi or communication also short square waves maybe could be possible by watch dog retriggering. And the timimng of the high prio timer is exact, maybe sometimes one tick error. Of course other timers jitter, when overlapping happens. There is no automatic TIM_LOOP now, because this would cost time, which I wanted to spare. I deleted the features DIV1, DIV16, DIV256 and TIM_LOOP, which I first had implemented. Now there is only DIV1 but there is also a replacement for TIM_LOOP.

For using my timers there isn't much to change. The isr callback has to be of type uint32_t instead of void. And ticks are triggered by "return ticks;" for additive after jitter, by "return ticks | 0x8FFFFFFF;" for not additive after jitter - replacement for TIM_LOOP, or "return 0xFFFFFFFF;" for stopping the timer. Of course also a function for arming or stopping a timer from outside the isr exists, and for initializing the timers and for attaching a callback function.

My idea, only four timers, but with very high accuracy and exactness and allowing very small steps and one timer jitterless, so that more timers or implementations like yours may be based on one of these timers without destroying tone or pwm and letting one timer remaining for use by the user.

@AlfonsMittelmeyer
Copy link

What I want to have, is a correct working analogWrite already for the value one. If you think, you could replace timer1, then I tell you my requirements for PWM.

GPIO costs 20 ticks, 10 ticks for D0 and ten ticks for the other digital pins. I calculate for code about 38 ticks. This means, that about 20 ticks remain for requesting the next ISR. For saving time I thought to return this 20 ticks. Subtracting return from ISR and call the ISR, until it's beginning, there will be left about 13 ticks from receiving the required ticks until calling the ISR. Can you do this?

@AlfonsMittelmeyer
Copy link

AlfonsMittelmeyer commented Apr 14, 2018

And because I wouldn't be pleased very much by a jitter of 1 tick, I would rate a jitter of more than 200 ticks as completely inadmissable.

@devyte
Copy link
Collaborator

devyte commented Apr 14, 2018

I think we're talking about different things here. This PR allows very fllexible usage, including use of all of pwm, servo, tone, and steppers at the same time. However, at first glance it looks like a top-down design, which means it doesn't solve the low level issues.
@AlfonsMittelmeyer 's work is a bottom-up design at timer level, that optimizes first with assembler to solve the low level issues, but at top level doesn't provide the features or flexibility provided here.
I think @earlephilhower comment about providing ideas has to do with how the low level code (shared high accuracy timers) should be used in a full implementation, I.e.: what it should be capable of.
What I would like to see is a final work that combines the best of both worlds.

How about this for a plan: we review this PR first, then once merged we add in @AlfonsMittelmeyer 's work to optimize and reduce jitter?

@AlfonsMittelmeyer
Copy link

AlfonsMittelmeyer commented Apr 14, 2018

@devyte you wrote:

@AlfonsMittelmeyer 's work is a bottom-up design at timer level, that optimizes first with assembler to solve the low level issues, but at top level doesn't provide the features or flexibility provided here.

I had also first the idea for offering multiple tones and waveforms. And it wasn't only an idea, because I soon had presented dimming (PWM) combined with multichord tone for Oh Susanna. But then I saw, that there are problems with pwm about accuracy, exactness and jitter. My second thought then was, that we first would need a stabile fundament, which offers these requirements and upon we could build then also, what earlepillhower did. This overbuilding I had thought to do after. And of course in the same manner, as we can make of one hardware timer more shared timers, we may also make of one shared timer a lot of further shared timers. I had thought also, that this wouldn't need first a call to the callback, which offers further callbacks, but that we may also make a shortcut, which leaps the second level of abstraction and so behaves as further timers of the second level would have been added to the first level. What I do, is a stabile fundament for a combination of timers with critical requirements and of further multiple timers, with less critical requirements, which may be added for a lot of waveforms.

My current implementation is not offering this lot of multiple timers yet, but only the fundament for this by offering very accurate and exact timers, which further may be divided into further timers.

The closest time between interrupts is about 3us, so rework the waveform
generator to support phases down to 1us, while limiting the time in the
IRQ handler to avoid WDT or WiFi errors.

Waveforms now are based off of absolute cycle, not delta-cycles, and
the GPIOs are converted from a bit to a mask (to save clocks in the IRQ)
costing about 40 more bytes in global storage and reducing the minimum
period down from 3.5us to 1.20us at 160MHz.

Steppers still based off of delta-times.
@earlephilhower
Copy link
Collaborator Author

Cool, @AlfonsMittelmeyer . I think we're working on orthogonal things, like @devyte was saying. And I'm not sure that unlimited tone, pulse, servo, and steppers are needed by the general populace.

Optimizing the original code, I've been able to get the general, straight C code to generate down to 1.2us measured pulses (analogWriteRange(1000); analogWrite D1, 1)l) on GPIOs0-15 (GPIO16 is slow as all thanks to a read-modify-write requirement no matter what you do...although it may be possible to actually write a value directly into it and ignore the non-0th bit....but w/o a real register map it's hard to say for certain) while ensuring it doesn't take too long in the IRQ and minimizes the # of interrupts. At higher phases (10us+) it's a pretty constant .8us offset which I haven't looked at touching since it's a very small %age of full-scale and things stay monotonically increasing anyway which makes closing physical feedback loops doable.

Also, you may want to look at the SDK. It seems like it already has a NMI based PWM generator at 14 bits of precision implemented. See chapter 12 of the ESP8266-Technical-Reference.pdf doc. If you need a very high precision PWM for something, that may be the way of least resistance...

@earlephilhower
Copy link
Collaborator Author

earlephilhower commented Apr 17, 2018

I just did some measurements on the existing analogWrite and my version using a DSO and running at 160MHz. All measurements were at 1KHz, 1000 scale (so that 1==1us in analogWrite and avoid any issues of rounding error).

The one already in there is pretty darn good, with the exception at very small ranges and having a constant offset of ~1.5us from requested length of each phase (i.e. 3.0us add'l per full cycle, split between the high and low periods).

The redesigned waveform generator is a little better and ~2x more accurate at 1-3us thanks to the busy-looping, but otherwise shrinks the delta in half to average of 1.5us total period offset.

But, frankly, either version is monotonically increasing and seems quite stable over the range so should be usable in any sort of feedback loop.

I also noticed that analogWrite does not actually load the TIMER1 so it takes 0.1s (TIMER1 full 7fffff count) for the 1st analogWrite to take effect in GIT head. After that, timer's already running so it's OK. This patch reloads when starting the timer for the first time so it doesn't have the same quirk, but again in GIT it's only on the 1st write from no PWM running.

waveform.zip

Remove unneeded curTimeHigh/curTimeLow from the waveform structure,
saving 88 bytes (11 pins x 8bytes) of heap.  Also reduces inner loop
in the IRQ by an couple stores.

Also add measured offset to the waveform generation request, enabling
a 1/1000 reqest to produce a 1.083us high pulse, and a 999/1000 to
give a 1.083 low period, and the cycle time to be exactly 1.00ms over
the range.

IRQ doesn't need to be disabled on waveform changes as the code order
between the irq and start/stop is safe.

Allow GPIO16 as a stepper out pin
@earlephilhower
Copy link
Collaborator Author

earlephilhower commented Apr 22, 2018

I ran some tests with this PR and an analogWrite@1000 range, 1khz while doing HTTP page fetching in a tight loop (~1.5 requests/second) and dumping it to Serial, and recorded the waveforms with the logic analyzer and post-processed to get the mean and stddev over a 10 second period @ 24MHz sampling.

With just the use of the TIMER1 standard IRQ and the ESP cycle counter, I'm getting what seems to be only ~8us worst case add'l delays in the waveform, but the average and standard deviation are very nice with the code at 1, 10, and 100us pulses:

earle@server:~/Arduino/hardware/esp8266com/esp8266/cores/esp8266$ ~/jitter < ~/Desktop/web1.csv 
High Phase:  min=1.083333, mean=1.112677, max=1.125000, stddev=0.019016
Period:  min=999.833333, mean=1000.295549, max=1003.291667, stddev=0.092125
earle@server:~/Arduino/hardware/esp8266com/esp8266/cores/esp8266$ ~/jitter < ~/Desktop/web10.csv 
High Phase:  min=9.625000, mean=10.058978, max=18.041667, stddev=0.108871
Period:  min=999.750000, mean=1000.226190, max=1008.166667, stddev=0.142370
earle@server:~/Arduino/hardware/esp8266com/esp8266/cores/esp8266$ ~/jitter < ~/Desktop/web100.csv 
High Phase:  min=99.833333, mean=100.082767, max=102.750000, stddev=0.092273
Period:  min=999.875000, mean=1000.239494, max=1009.625000, stddev=0.151456

So, I'd say that even w/o NMI and using the ESP cycle counter as the real waveform base (and TIMER1 only as a "hint") you can get some pretty nice numbers.

Remove the stepper code due to its size.  If it's included here it will
take a large amount of IRAM in *all* sketches, even those which do not
use it.

Add in a traditional callback option, which will allow Stepper to be put
in as a library, only being linked when necessary.
@earlephilhower earlephilhower changed the title WIP - Support multiple tone(), analogWrite(), Servo, and add stepper control WIP - Support multiple tone(), analogWrite(), and Servo Apr 25, 2018
@earlephilhower
Copy link
Collaborator Author

Our IRAM is tighter than even the AVR's PROGMEM. Ouch! Removed the stepper bit, will use a callback interface instead so that it won't be linked in unless you actually use it, and place into a separate library.

@d-a-v d-a-v mentioned this pull request May 11, 2018
Copy link
Collaborator

@d-a-v d-a-v left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only have tried PWM and it's working better. A pulsing LED like in #4321 stays smooth even with busy loop and also seems to fix its wdt.
The clarity of the code, its genericity and the sub-microsecond standard deviation measurements are convincing enough.
Thank you for this goldsmith's work.

@earlephilhower
Copy link
Collaborator Author

Thanks, but I haven't had time recently to get back to this or the stepper work with path planning, unfortunately.

Let me clean up the comments (which still reference the stepper bits) and remove the WIP and see if other folks can give it a go, too.

@earlephilhower earlephilhower changed the title WIP - Support multiple tone(), analogWrite(), and Servo Support multiple tone(), analogWrite(), and Servo May 15, 2018
@earlephilhower
Copy link
Collaborator Author

I've been pulled away from my eggbot/3d printer work, but wanted to move this from WIP to the "give it a go" stage. The comments have been cleaned up to remove the references to stepper motors. Moved them out to free IRAM as most users will not need this (and the linker will put it in already-tight IRAM if it's included).

@AlfonsMittelmeyer
Copy link

Hi earle, did you implement multichannel tone? And does your solution work well with a lot of tone channels?

I would be interested, whether your solution performs well. I have written a test program which uses 7 pins for tone and one pin for PWM. For PWM I have chosen 2 kHz and a range of 40000. What is the resolution of your implementation? Are 1/80 µs possible for 80 MHz CPU clock?

Here is my test program:

/*
    ESP8266 example multichannel tone and PWM
    PWM at pin D8, tone at pins D0 .. D3 and D5 .. D7
    Hardware: NodeMCU
    2018 by Alfons Mittelmeyer

    Programm starts, when pin D1 is connected with ground (over resistor)
*/
#include <Ticker.h>  //Ticker Library

#define TONEMAXCHANNELS 7


//=======================================================================
//                               tonechannels
//=======================================================================

uint8_t tonepins[TONEMAXCHANNELS];

void channelTone(byte id, float frequency, unsigned int duration) {
tone(tonepins[id],frequency,duration);
}

void tonechannels(byte pin1 = 0,byte pin2 = 0,byte pin3 = 0,byte pin4 = 0, byte pin5 = 0, byte pin6 = 0, byte pin7 = 0);


void tonechannels(byte pin1,byte pin2,byte pin3,byte pin4, byte pin5, byte pin6, byte pin7) {
  tonepins[0] = pin1;
  tonepins[1] = pin2;
  tonepins[2] = pin3;
  tonepins[3] = pin4;
  tonepins[4] = pin5;
  tonepins[5] = pin6;
  tonepins[6] = pin7;
}
//=======================================================================
//                             Sound Machine
//=======================================================================

#define SOUND_CLOCK_CYCLE 4000

#define  d2 73.4162
#define  G2 97.9989
#define  A2 110.0
#define  H2 123.471 
#define  C3 130.813
#define  Cis3 138.591
#define  d3 146.832
#define  Dis3 155.563
#define  E3 164.814
#define  F3 174.614
#define  Fis3 184.997
#define  G3 195.998
#define  Gis3 207.652
#define  A3 220.0
#define  B3 233.082
#define  H3 246.942
#define  C4 261.626
#define  Cis4 277.183
#define  d4 293.665
#define  Dis4 311.127
#define  E4 329.628
#define  F4 349.228
#define  Fis4 369.994
#define  G4 391.995
#define  Gis4 415.305
#define  A4 440.0
#define  B4 466.164
#define  H4 493.883
#define  C5 523.251
#define  Cis5 554.365
#define  d5 587.330
#define  Dis5 622.254
#define  E5 659.255
#define  F5 698.456
#define  Fis5 739.989
#define  G5 783.991
#define PAUSE 0
#define SOUND_CLOCK -1
#define SOUND_END -2

float * sound_voices[TONEMAXCHANNELS];
Ticker sound_tickers[TONEMAXCHANNELS];
int sound_indices[TONEMAXCHANNELS];
int sound_voicecount = 0;
  

void play_voice(int channel) {
  float note;
  float nextnote;
  unsigned int duration;

  bool isClock = false;
  float * data = sound_voices[channel];
  note = data[sound_indices[channel]++];
  if(note == SOUND_CLOCK) {
    note = data[sound_indices[channel]++];
    if(!channel)
      isClock = true;    
  }
  if(note == SOUND_END) {
    sound_tickers[0].once_ms(2000,sound_start);
  }
  else {
    duration = data[sound_indices[channel]++] * SOUND_CLOCK_CYCLE;
    nextnote = data[sound_indices[channel]];
    if(!channel || nextnote >= 0)
      sound_tickers[channel].once_ms(duration,play_voice,channel);
    if(note > 0) {
      channelTone(channel,note,duration-20);
    }
    if(isClock) {
      for(int i = 1; i < sound_voicecount; ++i) {
        sound_tickers[i].once_ms(i,play_voice,i); // ticker dependent on i for avoiding accumulation
        //play_voice(i); 
      }
    }
  }
}    

  
void sound_start() {
  for(int i = 0; i < sound_voicecount; ++i) {
    sound_indices[i] = 0;
    sound_tickers[i].once_ms(1+i,play_voice,i); // ticker dependent on i for avoiding accumulation
    //play_voice(i);      
  }
}

void sound_voice(int id, float * voicedata) {
  sound_voicecount = max(sound_voicecount,id+1);
  sound_voices[id] = voicedata;
}

float voice1[] = {

  G4,0.375,A4,0.125,SOUND_CLOCK,H4,0.25,A4,0.25,C5,0.25,H4,0.25,SOUND_CLOCK,A4,0.125,Fis4,0.125,G4,0.25,E5,0.25,d5,0.25,SOUND_CLOCK,C5,0.25,H4,0.25,A4,0.25,H4,0.125,G4,0.125,SOUND_CLOCK,d5,0.5,
  G4,0.375,A4,0.125,SOUND_CLOCK,H4,0.25,A4,0.25,C5,0.25,H4,0.25,SOUND_CLOCK,A4,0.125,Fis4,0.125,G4,0.25,E5,0.25,d5,0.25,SOUND_CLOCK,C5,0.25,H4,0.25,A4,0.25,H4,0.125,G4,0.125,SOUND_CLOCK,d5,0.5,
  A4,0.25,H4,0.25,SOUND_CLOCK,A4,0.125,Fis4,0.125,d4,0.25,C5,0.25,H4,0.25,SOUND_CLOCK,A4,0.125,Fis4,0.125,d4,0.25,d5,0.25,C5,0.25,SOUND_CLOCK,H4,0.375,H4,0.125,Cis5,0.25,Cis5,0.125,d5,0.125,SOUND_CLOCK,d5,0.5,
  G5,0.375,Fis5,0.125,SOUND_CLOCK,E5,0.25,d5,0.25,E5,0.375,d5,0.125,SOUND_CLOCK,C5,0.25,H4,0.25,A4,0.375,H4,0.0625,C5,0.0625,SOUND_CLOCK,d5,0.125,E5,0.125,C5,0.125,A4,0.125,G4,0.25,A4,0.125,G4,0.125,SOUND_CLOCK,G4,0.5,
  G5,0.375,Fis5,0.125,SOUND_CLOCK,E5,0.25,d5,0.25,E5,0.375,d5,0.125,SOUND_CLOCK,C5,0.25,H4,0.25,A4,0.375,H4,0.0625,C5,0.0625,SOUND_CLOCK,d5,0.125,E5,0.125,C5,0.125,A4,0.125,G4,0.25,A4,0.125,G4,0.125,SOUND_CLOCK,G4,0.5,
  SOUND_END
  
};

float voice2[] = {

  d4,0.375,d4,0.125,SOUND_CLOCK,G4,0.25,Fis4,0.25,A4,0.25,G4,0.25,SOUND_CLOCK,D4,0.5,C5,0.25,H4,0.25,SOUND_CLOCK,Fis4,0.25,G4,0.25,E4,0.5,SOUND_CLOCK,A4,0.5,
  d4,0.375,d4,0.125,SOUND_CLOCK,G4,0.25,Fis4,0.25,A4,0.25,G4,0.25,SOUND_CLOCK,D4,0.5,C5,0.25,H4,0.25,SOUND_CLOCK,Fis4,0.25,G4,0.25,E4,0.5,SOUND_CLOCK,A4,0.5,
  PAUSE,0.5,SOUND_CLOCK,PAUSE,0.5,Fis4,0.25,G4,0.25,SOUND_CLOCK,PAUSE,0.5,H4,0.25,A4,0.25,SOUND_CLOCK,G4,0.375,G4,0.125,G4,0.25,G4,0.125,PAUSE,0.125,SOUND_CLOCK,Fis4,0.5,
  d5,0.375,PAUSE,0.125,SOUND_CLOCK,C5,0.25,H4,0.25,C5,0.375,G4,0.125,SOUND_CLOCK,Fis4,0.25,G4,0.25,Fis4,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.25,E4,0.25,d4,0.25,Fis4,0.25,SOUND_CLOCK,d4,0.5,
  d5,0.375,PAUSE,0.125,SOUND_CLOCK,C5,0.25,H4,0.25,C5,0.375,G4,0.125,SOUND_CLOCK,Fis4,0.25,G4,0.25,Fis4,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.25,E4,0.25,d4,0.25,Fis4,0.25,SOUND_CLOCK,d4,0.5,
  SOUND_END
 
};


float voice3[] = {

  G4,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,D4,0.5,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,Fis4,0.5,
  G4,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,D4,0.5,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,Fis4,0.5,
  PAUSE,0.5,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,0.5,
  G4,0.375,PAUSE,0.125,SOUND_CLOCK,G4,0.25,G4,0.25,G4,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,d4,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,G3,0.5,
  G4,0.375,PAUSE,0.125,SOUND_CLOCK,G4,0.25,G4,0.25,G4,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,d4,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,G3,0.5,
  SOUND_END
};


float voice4[] = {
  G3,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,A3,0.25,H3,0.25,SOUND_CLOCK,C4,0.25,H3,0.25,PAUSE,0.5,SOUND_CLOCK,A3,0.25,H3,0.25,C3,0.25,Cis3,0.25,SOUND_CLOCK,d4,0.5,
  G3,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,A3,0.25,H3,0.25,SOUND_CLOCK,C4,0.25,H3,0.25,PAUSE,0.5,SOUND_CLOCK,A3,0.25,H3,0.25,C3,0.25,Cis3,0.25,SOUND_CLOCK,d4,0.5,
  Fis3,0.25,G3,0.25,SOUND_CLOCK,Fis3,0.5,A3,0.25,G3,0.25,SOUND_CLOCK,Fis3,0.5,d3,0.25,E3,0.125,Fis3,0.125,SOUND_CLOCK,G3,0.5,E3,0.25,E3,0.25,SOUND_CLOCK,d3,0.5,
  G3,0.5,SOUND_CLOCK,G3,0.25,G3,0.25,C4,0.375,H3,0.125,SOUND_CLOCK,A3,0.25,G3,0.25,A3,0.5,SOUND_CLOCK,G3,0.25,A3,0.25,H3,0.25,C4,0.25,SOUND_CLOCK,G3,0.5,
  G3,0.5,SOUND_CLOCK,G3,0.25,G3,0.25,C4,0.375,H3,0.125,SOUND_CLOCK,A3,0.25,G3,0.25,A3,0.5,SOUND_CLOCK,G3,0.25,A3,0.25,H3,0.25,C4,0.25,SOUND_CLOCK,G3,0.5,
  SOUND_END
};

float voice5[] = {
d3,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,Fis3,0.25,G3,0.25,SOUND_CLOCK,d3,0.25,G3,0.25,PAUSE,0.5,SOUND_CLOCK,d3,0.25,G3,0.25,PAUSE,0.5,SOUND_CLOCK,d3,0.5,
d3,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,Fis3,0.25,G3,0.25,SOUND_CLOCK,d3,0.25,G3,0.25,PAUSE,0.5,SOUND_CLOCK,d3,0.25,G3,0.25,PAUSE,0.5,SOUND_CLOCK,d3,0.5,
d3,0.5,SOUND_CLOCK,d3,0.5,d3,0.25,d3,0.25,SOUND_CLOCK,d3,0.5,H2,0.25,C3,0.125,d3,0.125,SOUND_CLOCK,E3,0.5,A2,0.25,A2,0.25,SOUND_CLOCK,d2,0.5,
d3,0.5,SOUND_CLOCK,E3,0.25,G2,0.25,G3,0.375,G3,0.125,SOUND_CLOCK,d3,0.25,G3,0.25,Fis3,0.5,SOUND_CLOCK,H2,0.25,C3,0.25,D3,0.5,SOUND_CLOCK,G2,0.5,
d3,0.5,SOUND_CLOCK,E3,0.25,G2,0.25,G3,0.375,G3,0.125,SOUND_CLOCK,d3,0.25,G3,0.25,Fis3,0.5,SOUND_CLOCK,H2,0.25,C3,0.25,D3,0.5,SOUND_CLOCK,G2,0.5,
SOUND_END
};

float voice6[] = {
H2,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,d2,0.5,
H2,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,d2,0.5,
PAUSE,0.5,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,0.5,
H2,0.5,SOUND_CLOCK,C3,0.25,PAUSE,0.25,E3,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,d3,0.5,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,0.5,
H2,0.5,SOUND_CLOCK,C3,0.25,PAUSE,0.25,E3,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,d3,0.5,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,0.5,
SOUND_END
};


float voice7[] = {
G2,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,0.5,
G2,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,0.5,
PAUSE,0.5,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,0.5,
PAUSE,0.5,SOUND_CLOCK,PAUSE,0.5,C3,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,C3,0.5,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,0.5,
PAUSE,0.5,SOUND_CLOCK,PAUSE,0.5,C3,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,C3,0.5,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,0.5,
SOUND_END
};


//=======================================================================
//                               Setup
//=======================================================================++++++++++++++++++++++++++++++++++++++++--

void setup()
{
  Serial.begin(115200);
    while(!Serial);

  pinMode(D1,INPUT_PULLUP); // starting with unconnected head phones, because the NodeCMU doesn't boot with too much pins connected  with ground (inspite connected resistors)
  while(digitalRead(D1)) // waiting for connection of headphones or speaker
    delay(10);

  delay(2000); // then 2 seconds pause

  tonechannels(D0,D1,D2,D3,D4,D6,D7);
  
  sound_voice(0,voice1);  // registerung melody for tone channel 0
  sound_voice(1,voice2);  // registerung melody for tone channel 1
  sound_voice(2,voice3);  // registerung melody for tone channel 2
  sound_voice(3,voice4);  // registerung melody for tone channel 3
  sound_voice(4,voice5);  // registerung melody for tone channel 4
  sound_voice(5,voice6);  // registerung melody for tone channel 5
  sound_voice(6,voice7);  // registerung melody for tone channel 6

  sound_start();          // start playing music

  analogWriteFreq(2000);
  analogWriteRange(40000);

}

//=======================================================================
//                MAIN LOOP
//=======================================================================

void loop()
{
  for(float i = 1; i < 40000; i *= 1.05 ) {
    analogWrite(D8,i);
    delay(10);
  }

  for(float i = 40000; i >= 1; i /= 1.05 ) {
    analogWrite(D8,i);
    delay(10);
  }

  digitalWrite(D8,LOW);
  delay(1000);
}

@earlephilhower
Copy link
Collaborator Author

Howdy @AlfonsMittelmeyer,

Hi earle, did you implement multichannel tone? And does your solution work well with a lot of tone channels?

Yes, it runs multichannel anything. The architecture is each pin can have a waveform with definable high and low periods, forever or for a limited time. So it'll do multiple tone + PWM + servo up to one per GPIO pin. Because it is tickless (interrupts on need, not every X us) the jitter will vary depending on how the edges line up (and interrupts are not NMI, so if WiFi is busy it'll have to wait to get control until the WiFi handler relinquishes the CPU). That's what the outliers were in my jitter analysis came from on Apr 22.

I would be interested, whether your solution performs well. I have written a test program which uses 7 pins for tone and one pin for PWM. For PWM I have chosen 2 kHz and a range of 40000. What is the resolution of your implementation? Are 1/80 µs possible for 80 MHz CPU clock?

The internal representation uses CPU cycles, not timer cycles, so theoretically you can specify down to 1/80Mhz. However, I can't guarantee skew or jitter at that level since it's a single core with other things to do, so the "set waveform (pin, hiper, lowper)" only allows microseconds. It does try and compensate for this core jitter by setting the timer to fire a little before it needs to work with dynamic adjustment.

Whether or not it performs "well" really depends on the requirements. For me, it lets me use all the pwm, servo, tone (and steppers, but I'm trying to do better than constant jerk) that I need and gives me the numbers I measured on Apr 22 while doing WiFi while using little CPU.

I don't have your physical setup, so would only be able to run your sample code and look at my USB "DSO", but since it's playing different notes at different times, I wouldn't be able to really give a good measurement. It does compile just fine and should run, but I can't test it. You can apply this pull request to your own GIT copy from master if you want to give it a go:
https://help.github.com/articles/checking-out-pull-requests-locally/

@AlfonsMittelmeyer
Copy link

Howdy @earlephilhower

I don't have your physical setup, so would only be able to run your sample code and look at my USB "DSO", but since it's playing different notes at different times, I wouldn't be able to really give a good measurement. It does compile just fine and should run, but I can't test it. You can apply this pull request to your own GIT copy from master if you want to give it a go:
https://help.github.com/articles/checking-out-pull-requests-locally/

The physical setup is very simple. I have a NodeMCU and connected the tone pins by resistors (470 Ohm) and then with a satellite box of my hifi-system.

But I noticed, that this code example cannot be used for stability tests. Using a lot of tickers may cause a reset after minutes or hours. I have to do a sequential conversion of the tone data, so that the tone data may be processed without tickers.

Maybe I will give your implementation a go tomorrow or the day after tomorrow. Tomorrow or the day after tomorrow I would like to present my own solution of the tone and pwm incompatibiliy problem.

Individual pwm ranges and frequencies and duration per pin and waveform integration I haven't implemented yet, but will do it later. Now for tone I use gaps in the Interrupt between PWM, because a little jitter for tone, if it's intended for speakers, doesn't matter. The advantage is less load for the interrupt and a very nice PWM. About FRC1 or NMI interupt: it may be configured.

Allow tone to take a double/float as wekk as an integer frequency.
Set the pin to an output on a tone() call to match original code.
@earlephilhower
Copy link
Collaborator Author

I've just pushed in a simple method that can take a float/double as well as an int. On the AVR Arduino you wouldn't want to run anything with floating point, which is horribly slow and uses lots of the small program flash. ESP8266 is also very slow, but it's not so bad and the code space isn't nearly so tight.

Also fixed that pin direction, thanks.

I'm looking at the scope and seeing definite (uneven) pauses between notes, so you probably don't want to use Ticker to send notes or you'll get stuttering between notes like someone just learning to play an instrument. Probably related to the stuff you said about its jitter.

[begin plug] If you really want to send polyphonic music out, check my audio library https://github.com/earlephilhower/ESP8266Audio which has everything from MP3 to MIDI and can even use the I2S to generate analog outputs to a speaker w/o a DAC. [end plug]

@AlfonsMittelmeyer
Copy link

AlfonsMittelmeyer commented May 25, 2018

I've just pushed in a simple method that can take a float/double as well as an int. On the AVR Arduino you wouldn't want to run anything with floating point, which is horribly slow and uses lots of the small program flash. ESP8266 is also very slow, but it's not so bad and the code space isn't nearly so tight.

Of course I don't use float within the ISR. The ISR only uses ticks. So float as parameter for tone doesn't matter for the speed. I use also float frequency for PWM. If pin D0 isn't used, jitter free PWM steps >= 33 ticks (for 80 MHz) are possible (2.4 kHz and range 1024).

The problem which I have with these tickers, is, that for each tone I enable a timer and disable it, when the duration ends. This enabling and disabling and arming costs too much time, because each time a search for a free timer has to be done. And for 7 tone channels it's too much time. So I should use only one timer for tone (and for advanced PWM per pin), which I can divide in further timers for pins. A search then needn't to be done. This will then be similar, as you do it.

But the normal PWM I also want to have. Because it starts with all pins at one time, the load during the ISR is only about half as much as for waves like tone and it may be adjusted to be completely exact also, when combining the wave forms. Because there is a setting and clearing of pins in each time slot, when combining tone and PWM waves, clearing will be about 10 ticks later than setting, which would result in not exact PWM time spans. So I want to shift the first time slot (setting pins for PWM) about 10 ticks into the future for complete exact PWM, also when the wave forms are combined.

@AlfonsMittelmeyer
Copy link

I'm looking at the scope and seeing definite (uneven) pauses between notes, so you probably don't want to use Ticker to send notes or you'll get stuttering between notes like someone just learning to play an instrument. Probably related to the stuff you said about its jitter.

Now it works correct. Instead of tickers I use delays. And there were bugs in the music data. The note D4 was wrong, because this is a pin. It had to be d4. And I had declared pin D4 as tone pin. It should have been D5 instead. D4 we need for flashing, so it's better not to use it.

Now this works fine:

/*
    ESP8266 example multichannel tone and PWM
    PWM at pin D8, tone at pins D0 .. D3 and D5 .. D7
    Hardware: NodeMCU
    2018 by Alfons Mittelmeyer

    Programm starts, when pin D1 is connected with ground (over resistor)
*/
#include <Ticker.h>  //Ticker Library
#include <list>

#define TONEMAXCHANNELS 7


//=======================================================================
//                               tonechannels
//=======================================================================

uint8_t tonepins[TONEMAXCHANNELS];

typedef struct {
  uint32_t ms = 0;
  uint16_t index = 0;
} times_t;

times_t sound_times[TONEMAXCHANNELS];


void tonechannels(byte pin1 = 0,byte pin2 = 0,byte pin3 = 0,byte pin4 = 0, byte pin5 = 0, byte pin6 = 0, byte pin7 = 0);


void tonechannels(byte pin1,byte pin2,byte pin3,byte pin4, byte pin5, byte pin6, byte pin7) {
  tonepins[0] = pin1;
  tonepins[1] = pin2;
  tonepins[2] = pin3;
  tonepins[3] = pin4;
  tonepins[4] = pin5;
  tonepins[5] = pin6;
  tonepins[6] = pin7;
}
//=======================================================================
//                             Sound Machine
//=======================================================================

#define SOUND_CLOCK_CYCLE 4000

// well temperament
/*
#define  d2 73.4162
#define  G2 97.9989
#define  A2 110.0
#define  H2 123.471 
#define  C3 130.813
#define  Cis3 138.591
#define  d3 146.832
#define  Dis3 155.563
#define  E3 164.814
#define  F3 174.614
#define  Fis3 184.997
#define  G3 195.998
#define  Gis3 207.652
#define  A3 220.0
#define  B3 233.082
#define  H3 246.942
#define  C4 261.626
#define  Cis4 277.183
#define  d4 293.665
#define  Dis4 311.127
#define  E4 329.628
#define  F4 349.228
#define  Fis4 369.994
#define  G4 391.995
#define  Gis4 415.305
#define  A4 440.0
#define  B4 466.164
#define  H4 493.883
#define  C5 523.251
#define  Cis5 554.365
#define  d5 587.330
#define  Dis5 622.254
#define  E5 659.255
#define  F5 698.456
#define  Fis5 739.989
#define  G5 783.991
*/


// just intonation for G-Dur
#define  C2 67.5
#define  Cis2 70.3125
#define  d2 75
#define  E2 84.375
#define  Fis2 93.75
#define  G2 100
#define  A2 112.5
#define  H2 125
#define  C3 133.333
#define  Cis3 140.625
#define  d3 150
#define  E3 166.667
#define  Fis3 187.5
#define  G3 200
#define  A3 225
#define  H3 250
#define  C4 266.667
#define  Cis4 281.25
#define  d4 300
#define  E4 333.333
#define  Fis4 375
#define  G4 400
#define  A4 450
#define  H4 500
#define  C5 533.333
#define  Cis5 562.5
#define  d5 600
#define  E5 666.667
#define  Fis5 750
#define  G5 800
#define  A5 900
#define  H5 1000

#define PAUSE 0
#define SOUND_CLOCK -1
#define SOUND_END -2


float * sound_voices[TONEMAXCHANNELS];
int sound_indices[TONEMAXCHANNELS];
int sound_voicecount = 0;
 
void sound_voice(int id, float * voicedata) {
  sound_voicecount = max(sound_voicecount,id+1);
  sound_voices[id] = voicedata;
}


typedef struct {
  int8_t pin;
  float frequency;
  int ms;
} tone_t;

std::list <tone_t> tone_list;



void sound_prepare(void) {

  tone_t note;

  uint32_t ms = 0;
  uint32_t mintime;
  uint8_t minindex;
  uint8_t sound_index;
  while(true) {
    minindex = 0;
    mintime = sound_times[0].ms;
    for(int i=1;i<sound_voicecount;++i) {
      if(sound_times[i].ms < mintime) {
        mintime = sound_times[i].ms;
        minindex = i;
      }
    }
    if(mintime != ms) {
      note.pin = -1;
      note.frequency = PAUSE;
      note.ms = mintime - ms;
      ms = mintime;
      tone_list.push_back(note);
    }
    sound_index = sound_times[minindex].index;
    if(sound_voices[minindex][sound_index] == SOUND_CLOCK)
      ++sound_index;
     if(sound_voices[minindex][sound_index] == SOUND_END)
      break;
    note.frequency = sound_voices[minindex][sound_index++];
    note.ms = SOUND_CLOCK_CYCLE * sound_voices[minindex][sound_index++];
    if(note.frequency > 0) {
      note.pin = tonepins[minindex];
      tone_list.push_back(note);
    }
    sound_times[minindex].ms += note.ms;
    sound_times[minindex].index = sound_index;
  }
}

void sound_play() {
  std::list <tone_t> :: iterator it;
  for(it = tone_list.begin(); it != tone_list.end(); ++it) {
    if(it->frequency>0)
      tone(it->pin,it->frequency,it->ms-10);
    else
     delay(it->ms);
  }
}


float voice1[] = {

  G4,0.375,A4,0.125,SOUND_CLOCK,H4,0.25,A4,0.25,C5,0.25,H4,0.25,SOUND_CLOCK,A4,0.125,Fis4,0.125,G4,0.25,E5,0.25,d5,0.25,SOUND_CLOCK,C5,0.25,H4,0.25,A4,0.25,H4,0.125,G4,0.125,SOUND_CLOCK,d5,0.5,
  G4,0.375,A4,0.125,SOUND_CLOCK,H4,0.25,A4,0.25,C5,0.25,H4,0.25,SOUND_CLOCK,A4,0.125,Fis4,0.125,G4,0.25,E5,0.25,d5,0.25,SOUND_CLOCK,C5,0.25,H4,0.25,A4,0.25,H4,0.125,G4,0.125,SOUND_CLOCK,d5,0.5,
  A4,0.25,H4,0.25,SOUND_CLOCK,A4,0.125,Fis4,0.125,d4,0.25,C5,0.25,H4,0.25,SOUND_CLOCK,A4,0.125,Fis4,0.125,d4,0.25,d5,0.25,C5,0.25,SOUND_CLOCK,H4,0.375,H4,0.125,Cis5,0.25,Cis5,0.125,d5,0.125,SOUND_CLOCK,d5,0.5,
  G5,0.375,Fis5,0.125,SOUND_CLOCK,E5,0.25,d5,0.25,E5,0.375,d5,0.125,SOUND_CLOCK,C5,0.25,H4,0.25,A4,0.375,H4,0.0625,C5,0.0625,SOUND_CLOCK,d5,0.125,E5,0.125,C5,0.125,A4,0.125,G4,0.25,A4,0.125,G4,0.125,SOUND_CLOCK,G4,0.5,
  G5,0.375,Fis5,0.125,SOUND_CLOCK,E5,0.25,d5,0.25,E5,0.375,d5,0.125,SOUND_CLOCK,C5,0.25,H4,0.25,A4,0.375,H4,0.0625,C5,0.0625,SOUND_CLOCK,d5,0.125,E5,0.125,C5,0.125,A4,0.125,G4,0.25,A4,0.125,G4,0.125,SOUND_CLOCK,G4,0.5,
  SOUND_END
  
};

float voice2[] = {

  d4,0.375,d4,0.125,SOUND_CLOCK,G4,0.25,Fis4,0.25,A4,0.25,G4,0.25,SOUND_CLOCK,d4,0.5,C5,0.25,H4,0.25,SOUND_CLOCK,Fis4,0.25,G4,0.25,E4,0.5,SOUND_CLOCK,A4,0.5,
  d4,0.375,d4,0.125,SOUND_CLOCK,G4,0.25,Fis4,0.25,A4,0.25,G4,0.25,SOUND_CLOCK,d4,0.5,C5,0.25,H4,0.25,SOUND_CLOCK,Fis4,0.25,G4,0.25,E4,0.5,SOUND_CLOCK,A4,0.5,
  PAUSE,0.5,SOUND_CLOCK,PAUSE,0.5,Fis4,0.25,G4,0.25,SOUND_CLOCK,PAUSE,0.5,H4,0.25,A4,0.25,SOUND_CLOCK,G4,0.375,G4,0.125,G4,0.25,G4,0.125,PAUSE,0.125,SOUND_CLOCK,Fis4,0.5,
  d5,0.375,PAUSE,0.125,SOUND_CLOCK,C5,0.25,H4,0.25,C5,0.375,G4,0.125,SOUND_CLOCK,Fis4,0.25,G4,0.25,Fis4,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.25,E4,0.25,d4,0.25,Fis4,0.25,SOUND_CLOCK,d4,0.5,
  d5,0.375,PAUSE,0.125,SOUND_CLOCK,C5,0.25,H4,0.25,C5,0.375,G4,0.125,SOUND_CLOCK,Fis4,0.25,G4,0.25,Fis4,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.25,E4,0.25,d4,0.25,Fis4,0.25,SOUND_CLOCK,d4,0.5,
  SOUND_END
 
};


float voice3[] = {

  G4,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,d4,0.5,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,Fis4,0.5,
  G4,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,d4,0.5,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,Fis4,0.5,
  PAUSE,0.5,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,0.5,
  G4,0.375,PAUSE,0.125,SOUND_CLOCK,G4,0.25,G4,0.25,G4,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,d4,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,G3,0.5,
  G4,0.375,PAUSE,0.125,SOUND_CLOCK,G4,0.25,G4,0.25,G4,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,d4,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,G3,0.5,
  SOUND_END
};


float voice4[] = {
  G3,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,A3,0.25,H3,0.25,SOUND_CLOCK,C4,0.25,H3,0.25,PAUSE,0.5,SOUND_CLOCK,A3,0.25,H3,0.25,C3,0.25,Cis3,0.25,SOUND_CLOCK,d4,0.5,
  G3,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,A3,0.25,H3,0.25,SOUND_CLOCK,C4,0.25,H3,0.25,PAUSE,0.5,SOUND_CLOCK,A3,0.25,H3,0.25,C3,0.25,Cis3,0.25,SOUND_CLOCK,d4,0.5,
  Fis3,0.25,G3,0.25,SOUND_CLOCK,Fis3,0.5,A3,0.25,G3,0.25,SOUND_CLOCK,Fis3,0.5,d3,0.25,E3,0.125,Fis3,0.125,SOUND_CLOCK,G3,0.5,E3,0.25,E3,0.25,SOUND_CLOCK,d3,0.5,
  G3,0.5,SOUND_CLOCK,G3,0.25,G3,0.25,C4,0.375,H3,0.125,SOUND_CLOCK,A3,0.25,G3,0.25,A3,0.5,SOUND_CLOCK,G3,0.25,A3,0.25,H3,0.25,C4,0.25,SOUND_CLOCK,G3,0.5,
  G3,0.5,SOUND_CLOCK,G3,0.25,G3,0.25,C4,0.375,H3,0.125,SOUND_CLOCK,A3,0.25,G3,0.25,A3,0.5,SOUND_CLOCK,G3,0.25,A3,0.25,H3,0.25,C4,0.25,SOUND_CLOCK,G3,0.5,
  SOUND_END
};

float voice5[] = {
d3,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,Fis3,0.25,G3,0.25,SOUND_CLOCK,d3,0.25,G3,0.25,PAUSE,0.5,SOUND_CLOCK,d3,0.25,G3,0.25,PAUSE,0.5,SOUND_CLOCK,d3,0.5,
d3,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,Fis3,0.25,G3,0.25,SOUND_CLOCK,d3,0.25,G3,0.25,PAUSE,0.5,SOUND_CLOCK,d3,0.25,G3,0.25,PAUSE,0.5,SOUND_CLOCK,d3,0.5,
d3,0.5,SOUND_CLOCK,d3,0.5,d3,0.25,d3,0.25,SOUND_CLOCK,d3,0.5,H2,0.25,C3,0.125,d3,0.125,SOUND_CLOCK,E3,0.5,A2,0.25,A2,0.25,SOUND_CLOCK,d2,0.5,
d3,0.5,SOUND_CLOCK,E3,0.25,G2,0.25,G3,0.375,G3,0.125,SOUND_CLOCK,d3,0.25,G3,0.25,Fis3,0.5,SOUND_CLOCK,H2,0.25,C3,0.25,D3,0.5,SOUND_CLOCK,G2,0.5,
d3,0.5,SOUND_CLOCK,E3,0.25,G2,0.25,G3,0.375,G3,0.125,SOUND_CLOCK,d3,0.25,G3,0.25,Fis3,0.5,SOUND_CLOCK,H2,0.25,C3,0.25,D3,0.5,SOUND_CLOCK,G2,0.5,
SOUND_END
};

float voice6[] = {
H2,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,d2,0.5,
H2,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,d2,0.5,
PAUSE,0.5,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,0.5,
H2,0.5,SOUND_CLOCK,C3,0.25,PAUSE,0.25,E3,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,d3,0.5,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,0.5,
H2,0.5,SOUND_CLOCK,C3,0.25,PAUSE,0.25,E3,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,d3,0.5,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,0.5,
SOUND_END
};


float voice7[] = {
G2,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,0.5,
G2,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,0.5,
PAUSE,0.5,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,0.5,
PAUSE,0.5,SOUND_CLOCK,PAUSE,0.5,C3,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,C3,0.5,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,0.5,
PAUSE,0.5,SOUND_CLOCK,PAUSE,0.5,C3,0.375,PAUSE,0.125,SOUND_CLOCK,PAUSE,0.5,C3,0.5,SOUND_CLOCK,PAUSE,1,SOUND_CLOCK,PAUSE,0.5,
SOUND_END
};


Ticker pwm_ticker;
volatile float pwm_value = 1;

void do_pwm1(void) {
   analogWrite(D8,pwm_value);
   pwm_value *= 1.05;
   if(pwm_value > 1023) {
    pwm_value = 1024;
    pwm_ticker.attach_ms(10,do_pwm2);
   }
}

void do_pwm2(void) {
  if(pwm_value < 1) {
    digitalWrite(D8,LOW);
    pwm_ticker.once_ms(1000,do_pwm3);
  }
  else {
    analogWrite(D8,pwm_value);
    pwm_value /= 1.05;
  }
}

void do_pwm3(void) {
  pwm_value = 1;
  pwm_ticker.attach_ms(10,do_pwm1);
}


void start_pwm(void) {
  pwm_value = 1;
  pwm_ticker.attach_ms(10,do_pwm1);
}

void test_ticker(void) {

  
}

//=======================================================================
//                               Setup
//=======================================================================++++++++++++++++++++++++++++++++++++++++--

void setup()
{
  Serial.begin(115200);
    while(!Serial);

  pinMode(D1,INPUT_PULLUP); // starting with unconnected head phones, because the NodeCMU doesn't boot with too much pins connected  with ground (inspite connected resistors)

  tonechannels(D0,D1,D2,D3,D5,D6,D7);
  
  sound_voice(0,voice1);  // registerung melody for tone channel 0
  sound_voice(1,voice2);  // registerung melody for tone channel 1
  sound_voice(2,voice3);  // registerung melody for tone channel 2
  sound_voice(3,voice4);  // registerung melody for tone channel 3
  sound_voice(4,voice5);  // registerung melody for tone channel 4
  sound_voice(5,voice6);  // registerung melody for tone channel 5
  sound_voice(6,voice7);  // registerung melody for tone channel 6
  
  sound_prepare();

  while(digitalRead(D1)) // waiting for connection of headphones or speaker
    delay(10);

  delay(2000);
  

  start_pwm();
}

//=======================================================================
//                MAIN LOOP
//=======================================================================

void loop()
{
  sound_play();
  delay(2000);
}

@earlephilhower
Copy link
Collaborator Author

Just confirmed this fixes #4349 as well.

@cjcharles0
Copy link

So is this fix now in Milestone 2.4.2?
Thanks @earlephilhower and @devyte (and others) - looking forward to the PWM improvements!

@X-Stas-EEE
Copy link

X-Stas-EEE commented Jul 19, 2018

Guys, I've observed rare resets of PWM to zero or to maximum. I use AnalogWrite in my weather station to set adaptive backlight of the screen. AnalogWrite was used for several times in a second (probably, it is a reason of spontaneous resets). I've optimized the algorithm and now AnalogWrite is used only when the lighting is changed. It's been a while since the changes were made, but so far everything is working fine.

@earlephilhower
Copy link
Collaborator Author

earlephilhower commented Jul 19, 2018

@X-Stas-EEE, can you open a new bug for this? I'd like to get more details and, if possible, some way to reproduce it without having a whole weather station attached. Also, "resets to 0 or 1" means the analogWriteten pin is stuck high or low (how long), not a system reset, yes? About how frequent was it (1/30 mins, 1/hour, 1/day, etc.), how often you were doing analogWrite (3/sec, 300/sec, etc.?), if they were the same or different values you were writing, if there was anything going on at the time (i.e. serving a web page, reassociating with a AP, etc.). And did it fix itself after (#) of analogWrite loops, or was it dead forever at that point?

If it's reproducible relatively easily with your original code, I also can see about making a PR with the PWM a non-maskable interrupt to see if that helps things.

On my own I've not visually seen anything like it on constantly analogWriting the same value to the built in LED. Same good result with a USB logic analyzer with a trigger window (i.e. if pulse high > 10us, capture) for 15-30 minutes. That said, the clone, $10 analyzer I've got can only run for 15-30 minutes before suffering a USB error (sometimes only seconds), so I can't get longer runs to do an automated check like that.

@X-Stas-EEE
Copy link

X-Stas-EEE commented Jul 19, 2018

@earlephilhower

"resets to 0 or 1" means the analogWriteten pin is stuck high or low (how long), not a system reset, yes?

Yes, backlight is off or very bright in such cases. If I cover the photoresistor with my finger (which means analogWrite(0)) and then take it away, the adaptive backlight is back to life. Just to be on the safe side I've checked if the AnalogRead (from the photoresistor) works properly in the described cases. Proper values have been written to serial console even when the backlight was stuck. Thus, the problem is clearly in AnalogWrite. From time to time my station plays a primitive music using tone(). Perhaps this can influence.
I was doing AnalogWrite for 10 times per second, It is quite hard to say if they were the same or different values - the external lighting is not constant, but in my sketch AnalogRead has 1024 levels and AnalogWrite has 255 levels. The error rate may vary from once per 2-3 days to couple times an hour.
I'll try to simplify the reproduction sequence by writing a simple sketch with tone() and analogWrite(). Once I do that and catch the bug, I'll report the issue.

@X-Stas-EEE
Copy link

X-Stas-EEE commented Jul 20, 2018

I succeed in replication with the following sketch:

#define LED_PIN D1 // GPIO5
#define BUZ_PIN D2 // GPIO4
int bright;
bool increase;
unsigned long cur_millis = millis();
unsigned long prev_millis = cur_millis;

void setup() {
  pinMode(LED_PIN, OUTPUT);
  pinMode(BUZ_PIN, OUTPUT);
  bright = 0;
  increase = true;
}

void loop() {
  cur_millis = millis();
  if (abs(cur_millis - prev_millis) > 100) {
    analogWrite(LED_PIN, bright);
    if (increase) {
      bright++;
    } else {
      bright--;
    }
    if (bright >= 105) {
      increase = false;
    } else if (bright <= 10) {
      increase = true;
    }
    prev_millis = cur_millis;
    tone(BUZ_PIN, (double)bright+200, 85);
  } 
}

I ran it in evening and left for the night (with the unplugged buzzer, of course). In the morning the led stuck at one brightness level. I suppose the bug can be reproduced without tone(). Will create new issue soon.

@earlephilhower
Copy link
Collaborator Author

@X-Stas-EEE Thanks! Open the bug when you can. I'll see if I can get it running locally over the weekend and see if it's an obvious goof.

One other question: Are you running at 80mhz or 160mhz? There are slightly different code paths involved, and I do most of my work by default at 160mhz (so SSL connections don't take forever...)

@X-Stas-EEE
Copy link

X-Stas-EEE commented Jul 20, 2018

@earlephilhower
Ah, I'm running at 80mhz.

@X-Stas-EEE
Copy link

X-Stas-EEE commented Jul 20, 2018

@earlephilhower
I've reported the issue: #4944
Not sure if I did it correctly. This is my first report.
Should it be assigned to you?

return b;
}

static inline ICACHE_RAM_ATTR uint32_t min_s32(int32_t a, int32_t b) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens here for negative values?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants