I am trying to get a routine working to control aircraft lights on a model aircraft. I have it working 99% but have one thing that has me stumped.
The circuit takes the output from one channel of an R/C receiver which is a pulse of between about 550 and 2400 mS. The code measures the pulse width and does several different things based on the pulse length. The ISR measures the time between the rising and falling edge of the pulse.
The problem is every once in a while it loses almost exactly 1000 uS and records an incorrect pulse length. For example if the pulse is 850 uS it records it as 1850 uS. This happens quite randomly every few seconds. It almost acts like the device just goes to sleep for that time.
At first I though I was getting hung up in a Delay() loop so changed them out to use a millis() based function but it made no difference.
Hoping someone can have a look and let me know where I am going wrong.
// RC input settings
#define PIN_SERVO 4 // RC channel input pin number - this needs to match whatever interrupt is used
#define SERVO_LOW 1000 // RC channel low threshold
#define SERVO_MID 1400 // RC channel midpoint
#define SERVO_HIGH 1800 // RC channel high threshold
#define SERVO_DEAD_BAND 25 // Servo signal dead-band size, eliminates flicker
#define SERVO_REVERSED false // Whether or not the servo channel is reversed
// Strobe settings
#define STB_PIN_LIGHT 5 // Strobe light output pin number
#define STB_BLINK_INTERVAL 1500000 // Blink interval for strobe light in microseconds
// Anti-collision beacon settings
#define ACB_PIN_LIGHT 7 // Anti-collision beacon output pin number
#define ACB_FADE_MIN 0 // Minimum fade level for beacon (0-255)
#define ACB_FADE_MAX 75 // Maximum fade level for beacon (0-255)
#define ACB_FADE_INTERVAL 8000 // Fade step interval, in microseconds (lower numbers = faster fade)
// Var declarations
volatile unsigned long servoPulseStartTime;
volatile int servoPulseWidth = 0;
// strobe via servo, acb is on whenever we have power!
boolean curStrobeLight = false;
boolean switchStrobeLight = false;
unsigned long lastFadeTime = 0;
unsigned long lastStrobeTime = 0;
int currentFade = ACB_FADE_MIN;
int fadeDirection = 1;
// the setup function runs once when you press reset or power the board
void setup() {
// Set up interrupt handler.
attachInterrupt(digitalPinToInterrupt(PIN_SERVO), measureServoSignal, CHANGE);
//Initialize input pin.
pinMode(PIN_SERVO, INPUT_PULLUP);
// initialize light pins as an output.
pinMode(STB_PIN_LIGHT, OUTPUT);
pinMode(ACB_PIN_LIGHT, OUTPUT);
}
// the loop function runs over and over again forever
void loop() {
unsigned long currentTime = micros();
checkServo();
if (servoPulseWidth > 800) { //
Serial.print(servoPulseWidth); // uncomment to monitor input pulse width
Serial.println(" "); //
}
// Check if it's time to fade the anti-collision lights.
if ((currentTime - lastFadeTime) > ACB_FADE_INTERVAL) {
doFade();
lastFadeTime = currentTime;
}
// Check if it's time to flash the strobes
setStrobeLight(switchStrobeLight);
if (switchStrobeLight) {
if ((currentTime - lastStrobeTime) > STB_BLINK_INTERVAL && switchStrobeLight) {
doStrobe();
lastStrobeTime = currentTime;
}
}
}
// Check servo signal, and decide whether to turn things on or off
void checkServo() {
// Strobe Lights
// Modify threshold to prevent flicker
int strobeThreshold = SERVO_MID;
if (!curStrobeLight) {
// Strobe are not on; adjust threshold up
strobeThreshold += SERVO_DEAD_BAND;
} else {
// Strobe are on, adjust threshold down
strobeThreshold -= SERVO_DEAD_BAND;
}
// Set output condition
if (servoPulseWidth >= strobeThreshold) { // strobe on
switchStrobeLight = true;
} else {
switchStrobeLight = false;
}
}
// Turn on or off strobe lights
void setStrobeLight(boolean state) {
curStrobeLight = state;
}
// Fade anti-collision LEDs
void doFade() {
currentFade += fadeDirection;
if (currentFade == ACB_FADE_MAX || currentFade == ACB_FADE_MIN) {
// If we hit the fade limit, flash the beacon, and flip the fade direction
if (fadeDirection == 1) analogWrite(ACB_PIN_LIGHT, 255); // Rotating ACB
Pause(millis(), 100);
fadeDirection *= -1;
}
analogWrite(ACB_PIN_LIGHT, currentFade);
}
// Strobe double-blink
void doStrobe() {
digitalWrite(STB_PIN_LIGHT, HIGH);
//delay(75);
Pause(millis(), 75);
digitalWrite(STB_PIN_LIGHT, LOW);
//delay(50);
Pause(millis(), 50);
digitalWrite(STB_PIN_LIGHT, HIGH);
//delay(50);
Pause(millis(), 50);
digitalWrite(STB_PIN_LIGHT, LOW);
}
// Measure servo PWM signal
void measureServoSignal() {
int pinState = digitalRead(PIN_SERVO);
if (pinState == HIGH) {
// Beginning of PWM pulse, mark time Serial.println(pinState);
servoPulseStartTime = micros();
} else {
// End of PWM pulse, calculate pulse duration in uS
servoPulseWidth = (long)(micros() - servoPulseStartTime);
// If servo channel is reversed, use the inverse
if (SERVO_REVERSED) {
servoPulseWidth = (1000 - (servoPulseWidth - 1000)) + 1000;
}
}
}
void Pause(int startMillis, int Duration) {
while (millis() - startMillis < Duration);
}
That is a known "issue", at least with AVR-based Arduinos. The millisecond counter does not increment 1000 times per second.
The counter is on occasion incremented by 2, purposely skipping an intermediate value, to make overall event timing more accurate. Apparently the same approach was taken for the SAMD21.
For continuous, accurate timing of short term events, use a hardware timer in input capture mode.
No, it wasn't... On SAMD platforms, the millisecond interrupt occurs exactly every millisecond.
In any case, they're using micros()
micros() returns a result based on millis()+numerOfMicroSeconds since last millis() increment. micros() has some fancy logic to try to make it safe in the face of millisecond interrupts, but It's possible that there is a bug there.
// Interrupt-compatible version of micros
// Theory: repeatedly take readings of SysTick counter, millis counter and SysTick interrupt pending flag.
// When it appears that millis counter and pending is stable and SysTick hasn't rolled over, use these
// values to calculate micros. If there is a pending SysTick, add one to the millis counter in the calculation.
unsigned long micros( void )
{
uint32_t ticks, ticks2;
uint32_t pend, pend2;
uint32_t count, count2;
ticks2 = SysTick->VAL;
pend2 = !!(SCB->ICSR & SCB_ICSR_PENDSTSET_Msk) ;
count2 = _ulTickCount ;
do
{
ticks=ticks2;
pend=pend2;
count=count2;
ticks2 = SysTick->VAL;
pend2 = !!(SCB->ICSR & SCB_ICSR_PENDSTSET_Msk) ;
count2 = _ulTickCount ;
} while ((pend != pend2) || (count != count2) || (ticks < ticks2));
return ((count+pend) * 1000) + (((SysTick->LOAD - ticks)*(1048576/(VARIANT_MCK/1000000)))>>20) ;
// this is an optimization to turn a runtime division into two compile-time divisions and
// a runtime multiplication and shift, saving a few cycles
}
So I ported the code back to a Pro Micro and it works as it should; no timing issues whatsoever. The only changes I made were a couple of pin assignments to support the interrupts and a PWM output and to go back to using delay() instead of my Pause function which I gather is blocking the same as delay() would anyway.
My reason for wanting to go with the XIAO was the size factor. The Pro Micro is double the size of the XIAO.
As @westfw mentions, I think that there might be a bug in the Arduino Zero's micros() code.
It was a long time ago, but I seem to remember that unlike other Arduino boards (including the ARM based Due), the Zero's micros() function wasn't interrupt safe.
At the time I ended up creating a micros2() function using the SAMD21's TC4 timer. This was based on the AVR micros() code, but runs to 1us accuracy (rather than the AVR's 4us):
// Implementation of alternative micros2() function
volatile uint32_t timer2Counter; // Overflow counter
void setup()
{
SerialUSB.begin(115200);
while(!SerialUSB);
GCLK->GENDIV.reg = GCLK_GENDIV_DIV(3) | // Divide the 48MHz clock source by divisor 3: 48MHz/3=16MHz
GCLK_GENDIV_ID(4); // Select Generic Clock (GCLK) 4
GCLK->GENCTRL.reg = GCLK_GENCTRL_IDC | // Set the duty cycle to 50/50 HIGH/LOW
GCLK_GENCTRL_GENEN | // Enable GCLK4
GCLK_GENCTRL_SRC_DFLL48M | // Set the 48MHz clock source
GCLK_GENCTRL_ID(4); // Select GCLK4
while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization
GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN | // Enable the generic clock
GCLK_CLKCTRL_GEN_GCLK4 | // Select GCLK4
GCLK_CLKCTRL_ID_TC4_TC5; // Direct GCLK4 to timers TC4 and TC5
NVIC_SetPriority(TC4_IRQn, 0); // Set the Nested Vector Interrupt Controller (NVIC) priority for TC3 to 0 (highest)
NVIC_EnableIRQ(TC4_IRQn); // Connect TC3 to Nested Vector Interrupt Controller (NVIC)
TC4->COUNT8.INTENSET.reg = TC_INTENSET_OVF; // Enable the TC4 overflow interrupt
TC4->COUNT8.PER.reg = 0xFF; // Set the PER register to 255 (maximum
while (TC4->COUNT8.STATUS.bit.SYNCBUSY); // Wait for synchronization
TC4->COUNT8.CTRLA.reg = TC_CTRLA_PRESCALER_DIV16 | // Set prescaler to 16, 16MHz/16 = 1MHz
TC_CTRLA_PRESCSYNC_PRESC | // Set the reset/reload to trigger on prescaler clock
TC_CTRLA_MODE_COUNT8; // Set the counter to 8-bit mode
TC4->COUNT8.READREQ.reg = TC_READREQ_RCONT | // Enable a continuous read request
TC_READREQ_ADDR(0x10); // Offset of the 8 bit COUNT register
while (TC4->COUNT8.STATUS.bit.SYNCBUSY); // Wait for (read) synchronization
TC4->COUNT8.CTRLA.bit.ENABLE = 1; // Enable TC4
while (TC4->COUNT8.STATUS.bit.SYNCBUSY); // Wait for synchronization
}
void loop()
{
SerialUSB.println(micros());
SerialUSB.println(micros2());
SerialUSB.println(micros() - micros2());
SerialUSB.println();
delay(1000);
}
// Micros2 using timer TC4 - based on AVR micros() code
uint32_t micros2()
{
uint32_t m;
uint8_t t;
noInterrupts(); // Disable interrupts
m = timer2Counter; // Get the number of overflows
t = TC4->COUNT8.COUNT.reg; // Get the current TC4 count value
if (TC4->COUNT8.INTFLAG.bit.OVF && (t < 255)) // Check if the timer has just overflowed (and we've missed it)
{
m++; // Then in this rare case increment the overflow counter
}
interrupts(); // Enable interrupts
return ((m << 8) + t); // Return the number of 1us counts that have occured since the timer started
}
// This ISR is called every 128us
void TC4_Handler() // ISR timer 2 overflow callback function
{
if (TC4->COUNT8.INTFLAG.bit.OVF)
{
timer2Counter++; // Increment the overflow counter
}
TC4->COUNT8.INTFLAG.bit.OVF = 1;
}
So I have been beating my head against a wall for week now trying to figure a workaround for this. Nothing I have tried has worked reliably. I have tried validating the pulse length but as the falling edge interrupt can happen at any time, my validation routine doesn’t always catch it. I have also tried using the pulseIn() function but as it is a blocking function, it messes up some other timing in my program. I also tried using @MartinL's micros2() code but couldn't get that to work either.
I have found that yes, there is (or was) a bug in the micros() function and has been for some time although for most it seems to have been fixed. The consensus is, it is or was in the wiring.c file. Why my code works fine on a Pro Micro but not the SAMD21 is puzzling though.
I am more of a hack at this time and getting down to that level is way beyond my skillset at the moment.
I am using the latest IDE (2.3.2) on a Macbook. I did find another simpler code example that duplicates what I am trying to do but alas, it does the same thing.
#define BUTTON_PIN 4
volatile unsigned long pulseInTimeBegin = micros();
volatile unsigned long pulseInTimeEnd = micros();
volatile bool newPulseDurationAvailable = false;
void buttonPinInterrupt() {
if (digitalRead(BUTTON_PIN) == HIGH) {
// start measuring
pulseInTimeBegin = micros();
} else {
// stop measuring
pulseInTimeEnd = micros();
newPulseDurationAvailable = true;
}
}
void setup() {
Serial.begin(9600);
pinMode(BUTTON_PIN, INPUT);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN),
buttonPinInterrupt,
CHANGE);
}
void loop() {
if (newPulseDurationAvailable) {
newPulseDurationAvailable = false;
unsigned long pulseDuration = pulseInTimeEnd - pulseInTimeBegin;
if (pulseDuration > 1000) Serial.println(pulseDuration);
}
// do your other stuff here
}
I really would like to get this to work on the XIAO due to its smaller size.
OK, so I took another stab at using @MartinL's micros2() code and I have made some progress. I have managed to get my stripped down version working although I did run into something odd. With the micros2() code I couldn't use pin6 (A6/D6/TX/PB08) on the Seeeduino for the ACB. If I did, I found the timing was out by 3X and I couldn't get anything to work on pin 6. I assume that is because the micros2() code uses the TC4 timer which interferes with the PWM functionality on that port. I moved the output to pin 5 and it started to work. Next is to add the rest of the functions back in and clean things up.
Question: can micros2() use one of the other timers?
You need to go 'hardware' as much as possible ( in contrast with the 'software' solution you are using )
Tha samd21 you are using has powerfull timers that can do this for you.
Timers has 'pulse-witdth capture' that is what you need ( datasheet par 30 and 31 ).
You can also use 'input capture' feature to capture each front...
Hardware counters don't skip counts ( generally ; - )
To use T5 you would just need to search and replace TC4 for TC5 in the code.
T3 would additionally require the generic clock (in this instance GCLK4) to be routed to it with the register's bitfield set to GCLK_CLKCTRL_ID_TCC2_TC3:
GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN | // Enable the generic clock
GCLK_CLKCTRL_GEN_GCLK4 | // Select GCLK4
GCLK_CLKCTRL_ID_TCC2_TC3; // Direct GCLK4 to timers TCC2 and TC3
As @davidefa mentions, an alternative approach would be to use one of the SAMD21's timers in pulse width and period capture mode. This example code receives the input on D6 then routes it via the External Interrupt Controller (EIC) over the event system, (an on-chip 12-channel peripheral-to-peripheral highway) to timer TCC0 in capture mode. The code outputs the period and pulse width in microseconds:
// Setup TCC0 to capture pulse-width and period on D6
volatile boolean periodComplete;
volatile uint32_t isrPeriod;
volatile uint32_t isrPulsewidth;
uint32_t period;
uint32_t pulsewidth;
void setup()
{
SerialUSB.begin(115200); // Configure the native USB port
while(!SerialUSB); // Wait for the console to be ready
PM->APBCMASK.reg |= PM_APBCMASK_EVSYS; // Switch on the event system peripheral
GCLK->GENDIV.reg = GCLK_GENDIV_DIV(3) | // Divide the 48MHz system clock by 3 = 16MHz
GCLK_GENDIV_ID(4); // Set division on Generic Clock Generator (GCLK) 5
GCLK->GENCTRL.reg = GCLK_GENCTRL_IDC | // Set the duty cycle to 50/50 HIGH/LOW
GCLK_GENCTRL_GENEN | // Enable GCLK 4
GCLK_GENCTRL_SRC_DFLL48M | // Set the clock source to 48MHz
GCLK_GENCTRL_ID(4); // Set clock source on GCLK 4
while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization
GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN | // Route GCLK4 to TCC0 and TCC1
GCLK_CLKCTRL_GEN_GCLK4 |
GCLK_CLKCTRL_ID_TCC0_TCC1;
PORT->Group[PORTA].PINCFG[20].bit.PMUXEN = 1; // Enable the port multiplexer on digital pin D6
PORT->Group[PORTA].PMUX[20 >> 1].reg |= PORT_PMUX_PMUXO_A; // Set-up the pin as an EIC (interrupt) peripheral on D6
EIC->EVCTRL.reg |= EIC_EVCTRL_EXTINTEO4; // Enable event output on external interrupt 4
EIC->CONFIG[0].reg |= EIC_CONFIG_SENSE4_HIGH; // Set event detecting a HIGH level
EIC->INTENCLR.reg = EIC_INTENCLR_EXTINT4; // Clear the interrupt flag on channel 4
EIC->CTRL.reg |= EIC_CTRL_ENABLE; // Enable EIC peripheral
while (EIC->STATUS.bit.SYNCBUSY); // Wait for synchronization
EVSYS->USER.reg = EVSYS_USER_CHANNEL(1) | // Attach the event user (receiver) to channel 0 (n + 1)
EVSYS_USER_USER(EVSYS_ID_USER_TCC0_EV_1); // Set the event user (receiver) as timer TCC0, event 1
EVSYS->CHANNEL.reg = EVSYS_CHANNEL_EDGSEL_NO_EVT_OUTPUT | // No event edge detection
EVSYS_CHANNEL_PATH_ASYNCHRONOUS | // Set event path as asynchronous
EVSYS_CHANNEL_EVGEN(EVSYS_ID_GEN_EIC_EXTINT_4) | // Set event generator (sender) as external interrupt 4
EVSYS_CHANNEL_CHANNEL(0); // Attach the generator (sender) to channel 0
TCC0->EVCTRL.reg |= TCC_EVCTRL_MCEI1 | // Enable the match or capture channel 1 event input
TCC_EVCTRL_MCEI0 | //.Enable the match or capture channel 0 event input
TCC_EVCTRL_TCEI1 | // Enable the TCC event 1 input
/*TCC_EVCTRL_TCINV1 |*/ // Invert the event 1 input
TCC_EVCTRL_EVACT1_PPW; // Set up the timer for capture: CC0 period, CC1 pulsewidth
NVIC_SetPriority(TCC0_IRQn, 0); // Set the Nested Vector Interrupt Controller (NVIC) priority for TCC0 to 0 (highest)
NVIC_EnableIRQ(TCC0_IRQn); // Connect the TCC0 timer to the Nested Vector Interrupt Controller (NVIC)
TCC0->INTENSET.reg = TCC_INTENSET_MC1 | // Enable compare channel 1 (CC1) interrupts
TCC_INTENSET_MC0; // Enable compare channel 0 (CC0) interrupts
TCC0->CTRLA.reg = TCC_CTRLA_CPTEN1 | // Enable capture on CC1
TCC_CTRLA_CPTEN0 | // Enable capture on CC0
TCC_CTRLA_PRESCSYNC_PRESC | // Reload timer on the next prescaler clock
TCC_CTRLA_PRESCALER_DIV16; // Set prescaler to 16, 16MHz/16 = 1MHz
TCC0->CTRLA.bit.ENABLE = 1; // Enable TCC0
while (TCC0->SYNCBUSY.bit.ENABLE); // Wait for synchronization
}
void loop()
{
if (periodComplete) // Check if the period is complete
{
noInterrupts(); // Read the new period and pulse-width
period = isrPeriod;
pulsewidth = isrPulsewidth;
interrupts();
SerialUSB.print(F("PW: ")); // Output the results
SerialUSB.print(pulsewidth);
SerialUSB.print(F(" "));
SerialUSB.print(F("P:" ));
SerialUSB.println(period);
periodComplete = false; // Start a new period
}
}
void TCC0_Handler() // Interrupt Service Routine (ISR) for timer TCC0
{
// Check for match counter 0 (MC0) interrupt
if (TCC0->INTFLAG.bit.MC0)
{
isrPeriod = TCC0->CC[0].reg; // Copy the period
periodComplete = true; // Indicate that the period is complete
}
// Check for match counter 1 (MC1) interrupt
if (TCC0->INTFLAG.bit.MC1)
{
isrPulsewidth = TCC0->CC[1].reg; // Copy the pulse-width
}
}
Using this method it's possible to route a signal from almost any of the SAMD21's pins to any one of its TCC or TC timers, since in this instance the pin isn't bound to the timer.
Thanks Martin. It is a bit clearer but your code is still way over my head. Correct me if I am wrong though. Your example uses PA20 on the SAMD21 which maps to D6? On the module I am using, the XIAO SAMD21, PA20 does not appear to be used. D6 maps to PB08. Are you thinking of the Zero perhaps?
If that is the case, what would I need to change to move it to D6/PB08/pin7 on the XIAO?
Thanks
Schem attached. Seeeduino-XIAO-v1.0-SCH-191112.pdf (37.6 KB)
My apologies, as you mention I mistakenly used the Arduino Zero pin mapping.
The Xiao SAMD21 input on PB08 just requires the following lines to be swapped out:
// Enable the port multiplexer on digital pin D6
PORT->Group[PORTB].PINCFG[8].bit.PMUXEN = 1;
// Set-up the pin as an EIC (interrupt) peripheral on D6
PORT->Group[PORTB].PMUX[8 >> 1].reg |= PORT_PMUX_PMUXE_A;
EIC->EVCTRL.reg |= EIC_EVCTRL_EXTINTEO8; // Enable event output on external interrupt 4
EIC->CONFIG[1].reg |= EIC_CONFIG_SENSE0_HIGH; // Set event detecting a HIGH level
EIC->INTENCLR.reg = EIC_INTENCLR_EXTINT8; // Clear the interrupt flag on channel 4
EIC->CTRL.reg |= EIC_CTRL_ENABLE; // Enable EIC peripheral
while (EIC->STATUS.bit.SYNCBUSY);
Plus this change for the event system:
EVSYS->CHANNEL.reg = EVSYS_CHANNEL_EDGSEL_NO_EVT_OUTPUT | // No event edge detection
EVSYS_CHANNEL_PATH_ASYNCHRONOUS | // Set event path as asynchronous
EVSYS_CHANNEL_EVGEN(EVSYS_ID_GEN_EIC_EXTINT_8) | // Set event generator (sender) as external interrupt 8
EVSYS_CHANNEL_CHANNEL(0); // Attach the generator (sender) to channel 0
The port pin PB08 uses EIC interrupt channel 8. The EIC's CONFIG register is divided into two blocks of 8, CONFIG[0] for channels 0 to 7 and CONFIG[1] for 8 to 15. However the SENSEx values are repeated from 0 to 7, hence SENSE0 for channel 8.
Thanks @MartinL . That seems to have done it. I have added your routine to my program and it has been running for a couple of hours now with no false triggers. I will let it run overnight and see what happens.