Garbled delayMicroseconds() on Uno

I've been developing some codes that drive a 433MHz transmitter, and I ran into an issue with the Arduino Uno R3 with regard to either "delayMicroseconds()" or "writeDigital()" or the two working in combination.

I generate a pulse train by using writeDigital() to turn on, then off, the voltage for on-off keying (OOK) transmissions by a 433MHz transmitter. For certain timing patterns, the pulse timings generated by the Uno are incorrect.

The problem occurs when I want to hold the output voltage HIGH for more than about 500 microseconds. When I attempt that, the HIGH is held for only about 524us maximum, and the LOW is extended by 100's of microseconds.

I generated some code (below) to isolate the problem from the larger program. The code runs on both the Uno and the Pico2, and the Pico2 does not exhibit this problem. So I think the issue is with my misunderstanding of either the Uno architecture, the Arduino IDE's compiled code, or the library code for either "delayMicroseconds()", "writeDigital()", or some interaction between the two that I don't understand.

The tables below show the results of running the code on both the Uno and the Pico2. The code times the actual loop durations with "micros()" and reports them. rtl_433 monitors the RTL-SDR dongle and records the signals received. The tables show the internally recorded times for the Uno and for the Pico2, and then the results for both as seen by rtl_433. The original code (emulating a Lacrosse TX141 remote sensor) worked fine on the Pico2 (rtl_433 recognized the device with no issues), but transmissions from the Uno weren't recognized at all.

Variations of 10-20 microseconds are acceptable, so my concern is not that a pulse that should be 400us is actually 412us. My concern is that a pulse of 500 high/500 low works fine (512/504 is OK), but that a pulse of 700/700 gets changed into 528/892 !.

I'm running
Arudino IDE Version: 2.3.6,
Date: 2025-04-09T11:22:51.016Z
CLI Version: 1.2.0
on Mac Mini M2, Sequoia 15.5.

Have I stumbled into a known issue? Is there a workaround?
Or is there a flaw in my code or my thinking?


Times as reported by Arduino Uno

15:17:46.240 -> For delay = 100us, actual time was 105.96us
15:17:47.260 -> For delay = 200us, actual time was 206.40us
15:17:48.284 -> For delay = 300us, actual time was 307.16us
15:17:49.340 -> For delay = 400us, actual time was 407.76us
15:17:50.363 -> For delay = 500us, actual time was 508.28us
15:17:51.448 -> For delay = 600us, actual time was 608.84us
15:17:52.505 -> For delay = 700us, actual time was 709.52us
15:17:53.589 -> For delay = 800us, actual time was 810.24us
15:17:54.675 -> For delay = 900us, actual time was 910.76us


Times as reported by Raspberry Pi Pico2

15:54:00.558 -> For delay = 100us, actual time was 100.49us
15:54:01.576 -> For delay = 200us, actual time was 200.49us
15:54:02.630 -> For delay = 300us, actual time was 300.48us
15:54:03.649 -> For delay = 400us, actual time was 400.48us
15:54:04.705 -> For delay = 500us, actual time was 500.48us
15:54:05.761 -> For delay = 600us, actual time was 600.49us
15:54:06.849 -> For delay = 700us, actual time was 700.50us
15:54:07.937 -> For delay = 800us, actual time was 800.48us
15:54:09.026 -> For delay = 900us, actual time was 900.52us


Times as reported by RTL_433 ("Recv'd")
Note Uno results at markers :51, :52, and :53

                        ARDUINO UNO		RPi Pico2
	    Time            Recv'd	  Recv'd	Recv'd	  Recv'd
Time	Req'd by        High      Low		High      Low
Marker	Program         Pulse     Pulse		Pulse     Pulse
:46	    100us		    120us	  92us		112us	   84us
:47	    200us		    212us	  200us		200us	  196us
:48	    300us		    316us	  300us		300us	  300us
:49	    400us		    416us	  400us		400us	  400us
:50	    500us		    512us	  504us		496us	  500us
:51	    600us		    524us	  692us		[garbled]
:52	    700us		    528us	  892us		696us	  700us
:53	    800us		    528us	 1092us		[garbled]
:54     900us           [garbled]		    900us	  896us

[garbled] means the message wasn't received because of
other simultaneous broadcasts in the neighborhood.

The code:

# Arduino Uno R3 or RPi Pico2

#define TX  3

void setup(void) {

     Serial.begin(9600);
     pinMode(TX, OUTPUT);
     delay(1000);
     digitalWrite(TX, LOW);
     delay(1000);
     Serial.println("Testing delayMicroseconds");
     };

void loop(void) {

     unsigned int times[] = {100,200,300,400,500,600,700,800,900};
     int i, j;
     unsigned long before, after;
     int pulses = 50;
     
     for (i=0; i<9; i++) {
         after = 0;
	     for (j=0; j<pulses; j++) {
            before = micros();
	        digitalWrite(TX, HIGH);
	        delayMicroseconds(times[i]);
	        digitalWrite(TX, LOW);
	        delayMicroseconds(times[i]);
            after += (micros() - before);
	     };
	 Serial.print("For delay = ");
	 Serial.print(times[i]);
	 Serial.print("us, actual time was ");
	 Serial.print(after/(2.0*pulses));
	 Serial.println("us");
	 delay(1000);
	 };
    delay(30000);
};
  • Please give us a timing diagram of what you want to produce.
  • Avoid delayMicroseconds() in your sketches. (only use for short timing adjustments)
  • Consider using a non blocking TIMER to generate a sequence.

For debugging purposes, move your pulse output to another digital, like D2. See if that cures the issue.

I strongly suspect that Serial is trying to control that pin while you're setting it.

Remember, serial output runs on an interrupt process, which you've awakened by Serial.begin().

Edit. Never mind, missed the define. Carry on!

Try disabling interrupts around your pulse sequence?

I don't see that behavior. A simple example follows:

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

void loop() {
   digitalWrite(3, LOW);
   delayMicroseconds(750);
   digitalWrite(3, HIGH);
   delayMicroseconds(750);
}

and here's what I see as the output:

~750us high, ~750us low. As I'd expect.

@westfw has a point, but maybe comment out all serial references, see if the barebones code behaves?

@van_der_decken how about changing the pin number to the OP's define style. If it confused me...

Here's the actual TX141 signal I'm trying to reproduce.
https://triq.org/pdv/#AAB011070402C80088015C006C03A8022027148455+AAB039070102C80088015C006C03A80220271495A295959595A2A2959595959595A29595A295A2959595A295A29595A29595A2A2A295A2A29595908455+AAB011070302C80088015C006C03A8022027148455+AAB039070102C80088015C006C03A80220271495A295959595A2A2959595959595A29595A295A2959595A295A29595A29595A2A2A295A2A29595908455+AAB011070302C80088015C006C03A8022027148455+AAB039070102C80088015C006C03A80220271495A295959595A2A2959595959595A29595A295A2959595A295A29595A29595A2A2A295A2A29595908455+AAB011070302C80088015C006C03A8022027148455+AAB039070102C80088015C006C03A80220271495A295959595A2A2959595959595A29595A295A2959595A295A29595A29595A2A2A295A2A29595908455+AAB011070302C80088015C006C03A8022027148455+AAB039070102C80088015C006C03A80220271495A295959595A2A2959595959595A29595A2B5A2959595A295A29595A29595A2A2A295A2A29595908455+AAB011070302C80088015C006C03A8022027148455+AAB039070102C80088015C006C03A80220271495A295959595A2A2959595959595A29595A295A2959595A295A29595A29595A2A2A295A2A29595908455+AAB011070302C80088015C006C03A8022027148455+AAB039070102C80088015C006C03A80220271495A295959595A2A2959595959595A29595A295A2959595A295A29595A29595A2A2A295A2A29595908455+AAB011070302C80088015C006C03A8022027148455+AAB039070102C80088015C006C03A80220271495A295959595A2A2959595959595A29595A295A2959595A295A29595A29595A2A2A295A2A29595908455+AAB011070302C80088015C006C03A8022027148455+AAB039070102C80088015C006C03A80220271495A295959595A2A2959595959595A29595A295A2959595A295A29595A29595A2A2A295A2A29595908455+AAB011070302C80088015C006C03A8022027148455+AAB039070102C80088015C006C03A80220271495A295959595A2A2959595959595A29595A295A2959595A295A29595A29595A2A2A295A2A29595908455+AAB011070302C80088015C006C03A8022027148455+AAB039070102C80088015C006C03A80220271495A295959595A2A2959595959595A29595A295A2959595A295A29595A29595A2A2A295A2A29595908455+AAB011070302C80088015C006C03A8022027148455+AAB039070102C80088015C006C03A80220271495A295959595A2A2959595959595A29595A295A2959595A295A29595A29595A2A2A295A2A29595908455+AAB011070102C80088015C006C03A8022027148655

The 0's and 1's timings are fine: it's the initial 712/936 pulse that is getting garbled, so the rtl_433 recognizer can't match the pattern.

I understand the unsigned int restriction on delayMicroseconds() and I'm well below the limit.

Some of my pulse trains are up to 300 HIGH/LOW sequences, so that would be a pretty long string of concurrent tasks for TIMER. I could probably use arduino-timer to do that, but it's not clear that it would provide more precise timing that delayMicroseconds() for this application, if it worked at all for that many pulses.

The signal-consumer tolerates the kind of variation you see from interrupts. This kind of distortion of timings isn't due to interrupts, I think, but rather to something either much simpler (for example, I've made a stupid mistake) or much deeper (a subtle interaction among libraries).

van_der_decken,

THANKS! I don't have a scope, so that's exactly what I needed to see.

I'll try the simple experiment of not using Serial and see if that works.

I also did a "do 3 seconds of each interval from 100 to 900uS in 100uS steps" sketch and it looked fine as well. No bobbles.

Well, I tried your code (with a loop so that I get a burst that rtl_433 can recognize), but the problem is still there. I'm getting 556/952 as the widths rather than 750/750.

That transmitter is wire-wrapped into the board, so I'll have to rewire it to move it to a different pin. I'll give that a try tomorrow.

But thanks for your test. If changing the pin doesn't fix it, I'll try a different Uno.

Thanks!

Since you were using pin 3, too, it doesn't look like that's the problem, but it's worth checking.

Super, super, SWAG here: once upon a time, I somehow blew one of the output FETs on a pin.

It was on a Pico actually, not that it matters at all.

Anyhow, when I looked at the output with the scope, I got a nice crisp vertical edge when the output was set HIGH, and a slowly decaying curve when the output was set LOW.

It would have completely trashed the duty cycle of any signal I'd put out that pin, though the frequency would have been unaffected.

Probably not what's going on in your case, but something to think about.

  • Here is a non-blocking (similar version) that you might find interesting.
//
//================================================^================================================
//                                  B a s i c   R a w   S k e t c h
//================================================^================================================
//
//  URL
//
//
//
//  Version    YY/MM/DD    Comments
//  =======    ========    ========================================================================
//  1.00       25/05/28    Running code
//
//
//
//
//  Notes:
//
//
//
//



//================================================
#define LEDon              HIGH   //PIN---[220R]---A[LED]K---GND
#define LEDoff             LOW

#define ENABLED            true
#define DISABLED           false

#define EXECUTEcode        true   //the code in this State is executed
#define BYPASScode         false  //the code in this State is not executed

#define RELAYon            LOW
#define RELAYoff           HIGH


//                              G P I O s   A n d   V a r i a b l e s
//================================================^================================================
//

//Analogs
//================================================
//

//INPUTS
//================================================
//

//OUTPUTS
//================================================
//
const byte heartbeatLED         = 13;
const byte tx                   = 3;

//VARIABLES
//================================================
//
bool waveformTIMERflag          = ENABLED;

unsigned long heartbeatTime;
unsigned long waveTime;
unsigned long waveInterval;

const byte pulsesMax            = 50;
//const byte pulsesMax            = 1;  //for testing
byte pulses                     = 0;
byte outputLevel                = HIGH;

//================================================
struct mySequence
{
  unsigned long interval;
};

#define delayFactor 15

mySequence waveSequence[]       = {100ul - delayFactor, 200ul - delayFactor, 300ul - delayFactor,
                                   400ul - delayFactor, 500ul - delayFactor, 600ul - delayFactor,
                                   700ul - delayFactor, 800ul - delayFactor, 900ul - delayFactor
                                  };
                                  
const byte sequenceSize         = sizeof(waveSequence) / sizeof(waveSequence[0]);
int arrayIndex                  = 0;


//                                           s e t u p ( )
//================================================^================================================
//
void setup()
{
  Serial.begin(115200);

  digitalWrite(heartbeatLED, LEDoff);
  pinMode(heartbeatLED, OUTPUT);

  digitalWrite(tx, LOW);
  pinMode(tx, OUTPUT);

  //========================
  //initializing
  arrayIndex = 0;
  pulses = 0;
  waveInterval = waveSequence[arrayIndex].interval;

  waveTime = micros();

} //END of   setup()


//                                            l o o p ( )
//================================================^================================================
//
void loop()
{
  //========================================================================  T I M E R  heartbeatLED
  //Is it time to toggle the heartbeat LED ?
  if (millis() - heartbeatTime >= 500ul)
  {
    //restart thei s TIMER
    heartbeatTime = millis();

    //toggle the heartbeat LED
    digitalWrite(heartbeatLED, digitalRead(heartbeatLED) == HIGH ? LOW : HIGH);
  }

  //========================================================================  T I M E R  waveForm
  //Is it time to service the waveForm sequence ?
  if (waveformTIMERflag == ENABLED && micros() - waveTime >= waveInterval)
  {
    serviceWaveform();

    //restart this TIMER
    waveTime = micros();
  }


  //================================================
  //       Other non blocking code goes here
  //================================================


} //END of   loop()


//                                s e r v i c e W a v e f o r m ( )
//================================================^================================================
//
void serviceWaveform()
{
  //========================
  //have we finished with this time period ?
  if (pulses >= pulsesMax)
  {
    //next pulse sequence
    pulses = 0;

    arrayIndex++;

    //next interval
    waveInterval = waveSequence[arrayIndex].interval;

    //are we finished with all the time elements ?
    if (arrayIndex >= sequenceSize)
    {
      digitalWrite(tx, LOW);

      waveformTIMERflag = DISABLED;
      Serial.println("Finished with all pulses");

      return;
    }
  }

  digitalWrite(tx, outputLevel);

  //next pin level
  outputLevel = !outputLevel;

  //========================
  //have we made one HIGH/LOW period ?
  if (outputLevel == HIGH)
  {
    pulses++;
  }

} //END of   serviceWaveform()


//================================================^================================================

Thanks, Larry! I ran it on my stand-alone transmitter testbed (no other devices on the Uno) and got the following results:

Pulse width distribution:
 [ 0] count:   50,  width:  116 us [104;152]	(  29 S)
 [ 1] count:   50,  width:  212 us [204;220]	(  53 S)
 [ 2] count:   50,  width:  312 us [308;324]	(  78 S)
 [ 3] count:   99,  width:  460 us [408;516]	( 115 S)
 [ 4] count:  151,  width:  708 us [516;816]	( 177 S)
 [ 5] count:   50,  width:  908 us [908;916]	( 227 S)

I modified my version to send the packets back-to-back, as you did, and got the following:

Pulse width distribution:
 [ 0] count:   50,  width:  120 us [116;152]	(  30 S)
 [ 1] count:   50,  width:  216 us [208;224]	(  54 S)
 [ 2] count:   50,  width:  316 us [312;328]	(  79 S)
 [ 3] count:   99,  width:  464 us [412;524]	( 116 S)
 [ 4] count:  151,  width:  716 us [520;824]	( 179 S)
 [ 5] count:   50,  width:  920 us [912;924]	( 230 S)

So, indeed, it looks like the non-blocking approach is generally 1 clock tick (4µs) better than the delayMicroseconds() approach. There is an issue with both of them in that the packets aren't separated by the rtl_433 program, but that wouldn't be an issue in real practice.

When I get a bit more time, I'll go back and retrofit non-blocking into my code base.

Meanwhile, I have more information about my original problem, which I'll post shortly. No solution, just some evidence and more questions.

Thanks, again, for taking time to demonstrate the non-blocking approach.

David

van_der_decken:

OOOHHH. That one would have been impossible to track down without the scope!

Fortunately (or not!), I seem to have found the source of the problem, though I don't understand why or how, so I don't know how to fix it yet. Posting to follow with more information.

But thanks for scaring me with that horror story! :wink: I saw myself having to explain to my wife why I needed to buy an oscilloscope! :sweat_smile:

A little DSO138 or DSOshell won't break the bank. And while they're not anywhere near as capable as a "real" scope, they are still amazingly useful for the range of frequencies you are looking at.

van_der_decken, Larry, et al,

The problem seems to be interference from I2C circuits.

I modified van_der_decken's code to generate 50-pulse wave trains, then delay. No Serial or other activity, just the digitalWrite() and the timing.

void setup() {
   pinMode(3,OUTPUT);
   digitalWrite(3, LOW);
   delay(1000);
}

void loop() {
   for (int i=0; i<50; i++) {
   digitalWrite(3, HIGH);
   delayMicroseconds(750);
   digitalWrite(3, LOW);
   delayMicroseconds(750);
};
   delay(5000);
}

I ran the code on my original setup and again got the 548µs HIGH/964µs LOW pulses (should have been 750/750).

I replaced that Arduino with another that I have, and I got 540/972 for the pulses -- slightly different, but still wildly incorrect.

So the problem wasn't caused by that particular Arduino Uno.

My prototype board circuit is wired as follows (I actually have a wire-wrapped Arduino prototype board rather than a breadboard block):


So, it looks like this:

The repository for all this is at GitHub - hdtodd/WP_433.

Rather than change the wirewrap on the board in order to use pin 4 rather than pin 3, I moved the transmitter to a separate breadboard and connected it, and just it, to a Uno, and I continued to use pin 3.

I re-ran my test code and I got a very repeatable 768µs/744µs for pulse HIGH/LOW timings.

So the problem isn't the use of pin 3.

So my guess is that the I2C circuits connected to the Uno on the prototype board are somehow interfering with the timings, even though the test codes don't activate the devices or communicate with them at all.

The other devices on the board (light sensor, DS18B20, transmitter) are all passive. Only the DHT20 and MPL3115 I2C devices might be causing the problem.

But that leaves me with some questions to which some of you might have answers:

  1. The problem doesn't occur on the Pico2. I presume that the different architecture and the much faster clock rate accounts for that. But does that suggest that there's something in the Uno architecture that I'm not accounting for that causes the problem?
  2. Is it really possible for the I2C devices to be causing problems with the test code if they haven't been activated? I can see how that might happen with my weather probe code, but not if the code hasn't activated the I2C system as is the case with our testing programs.
  3. Why does this only happen when the leading pulse is >500µsec? And why is it so perfectly repeatable if it's due to some interrupt event? I'd have expected more randomness.
  4. A disruption every 500µs might be caused by a 20KHz communication signal, but I2C's slowest communication rate seems to be 100K bits/sec. I can't see how to make the math work out to prove that I2C is the culprit, but it sure looks like it to me.
  5. I tried breaking longer pulses into two delayMicroseconds() calls and got the same problem. That, again, seems to suggest that it's something other than my transmission loop code or the library routines that's causing the problem.

I really need to use the I2C sensors, but I can think of only a couple of solutions:

  1. Try Larry's non-blocking code for waveform generation. It's not clear to me that, if it's really an I2C interference issue, moving to that code will avoid the problem. Thoughts?
  2. Use a CC1101 transmitter rather than bit-banging on the 433MHz transmitter. But looking at some of the library code for that, it looks like they use delayMicroseconds(), too, so I'd have the same problem.
  3. Drop any plan to support the Uno with this and focus on the Pico2, which doesn't exhibit this problem.

Any other suggestions? I'd really like to solve this in software with the hardware I have, but I don't see a way to do it.

Thanks for any insights you can offer.

David

Wow! I hadn't seen these before. Well worth the money for the time it would save with just basic diagnostics. Thanks for the pointer!

I'll wager a pan of freshly baked blueberry muffins (virtually, in this case) that the answer is quite simple. Or, as the saying goes, "when you hear hoofbeats, think horses, not zebras".

I'll bet that if you measured the output of pin3 on your original shield, you'd see the proper pulse widths. Which would indicate that delayMicroseconds is working perfectly. I would bet several pans of muffins on this.

And I'll bet that the output of the RF transmitter is being affected by the proximity of all that digital circuitry (the Uno, mostly) which, unintentionally, is also transmitting RF. And interfering with your transmission.

A voice of an instructor from long ago is echoing in my head: "all electronics is analog, and every wire is an antenna."

It's going to have nothing to do with inactive I2C devices. It's going to have nothing to do with an obscure bug in delayMicroseconds that has evaded notice from millions of users for almost 2 decades. It's just going to be the Uno being a good healthy source of RF at short range. Which, in this case, is bad. And explains why moving the (likely very poorly shielded) transmitter to a separate breadboard fixed the problem: it was further away from the interference.