1
\$\begingroup\$

I'm using an Atmel ATtiny85 microcontroller to implement a PID controller for a temperature regulation application.

I have configured Timer1 to be a simple PWM generator using the following code:

void Init_Timer1()
{
  //Timer1 generates the PWM signal for the heater resistors.

  TCCR1 |= (1 << CS13) | (1 << CS12) | (1 << CS11) | (0 << CS10); //divide system clock by 8192

  OCR1A = 0; //The ISR handling this compare match interrupt will turn ON the heater resistors.
  OCR1B = 128; //The duty cycle of the PWM, 0 - 255, where 255 is 100%.

  TIMSK |= (1 << OCIE1A) | (1 << OCIE1B); //Turn on both MatchA and MatchB compare interrupts.
}

ISR(TIM1_COMPA_vect)
{
  PORTB |= HEATER_PB3; //HEATER ON
}

ISR(TIM1_COMPB_vect)
{
  PORTB &= ~HEATER_PB3; //HEATER OFF
}

To summarise, I've enabled the two compare match A and B interrupts. When the timer count equals 0, the heater turns on, when the timer count equals OCR1B, it turns off again.

This works very well for the most part, I can vary the duty cycle by changing the value in the OCR1B register. But very occasionally there will be a spurious, abnormal triggering of one of the ISRs. It can be either direction - keeping the heater turned on slightly too long, or turning it on for a very short time (microseconds). Here is a picture of a brief "Heater ON" pulse shown by the red trace. Notice how it disrupts the regular pattern.

Screenshot of logic analyser readings, showing a very brief positive-going glitch

The white trace is RS-232 data output, green is ADC activity. There is no pattern that suggests that either RS-232 or the ADC is coincident with the glitch.

I'm still developing the code and I'm not actually varying the duty-cycle at runtime. For now it's hard-coded at 128 (50%). This glitch is very rare indeed, perhaps 1 occurrence per 10,000 normal operations.

I'm not using any of the _delay_us() family of delay functions, because I know that this can interfere with timer operations. Any delays that I'm using are implemented as simple "busy loops".

I'm at a bit of a loss as to how to troubleshoot this glitch. The power supply is rock-solid at 5V (measured at the microcontroller's VCC: pin 8) and shows no sign of perturbation by the heater (a 10 Ω, 3 W resistor) turning on or off.

How can I find the cause of this issue?

\$\endgroup\$
4
  • \$\begingroup\$ Post the whole code. Likely something you did not show yet because you did not think it's important, but everything is. Do you write to PORTB in any other place in the code? Also another question is, the timers support hardware PWM output, why use timer interrupts to control the pins in software? Is the hardware PWM on another pin? \$\endgroup\$
    – Justme
    Mar 11, 2023 at 17:42
  • \$\begingroup\$ @Justme, I understand your point. But the question is about how to (generally) troubleshoot glitchy ISR issues. My particular codebase is simply an example. I'm looking for a methodology, rather than a "please fix my code" solution. Sure the code is likely to blame, but how do I diagnose it? \$\endgroup\$
    – Wossname
    Mar 11, 2023 at 19:06
  • 2
    \$\begingroup\$ Well, you just have to know how the code runs on the MCU. Find any read-modify-write on PORTB which is non-atomic so the timer interrupts momentarily change the pin and then the original read-modify-write operation continues and overwrites what just was written in the interrupt. Actually, does that already qualify as an answer? \$\endgroup\$
    – Justme
    Mar 11, 2023 at 20:51
  • \$\begingroup\$ Could be an answer, yes. \$\endgroup\$
    – Wossname
    Mar 12, 2023 at 16:40

3 Answers 3

3
\$\begingroup\$

The only explanation is that there is some other piece of code which writes to PORTB. So the problem is not in the interrupt code.

If the other code does a read-modify-write operation to any unrelated pin of PORTB, it has to be atomic.

Otherwise that code may read the port, then the interrupt fires and modifies the port, and after interrupt the operation continues, and writes back the old value read before interrupt modified it.

\$\endgroup\$
2
\$\begingroup\$

Typically there are three circumstances where glitches occur, and they are all concurrency errors.

1. A non-atomic operation.

Typically a read-modify-write operation must be done on this platform by fetching memory into CPU register(s), performing the arithmetic/logic, and writing back. There are several cycles during which the operation has begun but the memory hasn't been written back yet.

This could apply to a data structure used by the interrupt, or the hardware itself (something else reading/writing it that shouldn't).

This underlying mechanism may manifest in the following cases.

2. Multiple code paths are modifying the same registers.

So don't do that. Make one code path/module responsible for the resource. Route updates through an internal data structure (buffered events) or code path (function calls to update internal queue, etc.). Extensively test (or better yet, prove) the behavior of the interface -- that random calls to the functions, or updates of the data, don't cause unspecified corruption (or crashing).

3. The interrupt is the only code path modifying the register, but it is badly delayed.

This happens when an atomic section in main(), or another interrupt, delays execution of the interrupt in question. Solution: remove delays as much as possible; make use of interrupt priority; shorten/remove atomic sections (e.g. consider using a buffer-swapping scheme where only the semaphore need be updated atomically -- which can be a single byte modification on this platform, no ATOMIC section required).

If you're using Arduino, there may be overhead (such as the above) that you didn't realize are enabled. (I'm not very familiar with the Arduino ecosystem, I don't know how in-depth these sorts of automatic features go.) These might be adding unexpected delays, or reading/writing registers or data you are also using.

I'm not intimately familiar with ATtiny family, but other AVRs, either: interrupt priority is fixed by hardware order (check the manual), in which case you can potentially use a different combination of equivalent peripherals to get a more suitable ordering; or interrupt priority is set voluntarily (ATmega, interrupts are disabled during the interrupt, re-enable them to make the currently executing interrupt low priority; warning, the same interrupt can re-interrupt itself so make sure it is either re-entrant or completes before the next interrupt arrives); or the priorities can be changed in a few ways (AVR-Dx), or a few priority levels are available (XMEGA).

You can also have all interrupts received into an event queue and then dealt with in the main() loop, but it doesn't sound like these are complex enough (say, networking stacks) to warrant such an architecture.


Finally, remember there is nothing the CPU is doing that you cannot inspect. You can read the ASM output by adding the -S flag to the compiler command (GCC), or extracting it from ELF output using avr-objdump -h -S (redirect the output into a *.lss file). The AVR instruction set is pretty easy to read as ISAs go, but it is verbose (RISC-y) so can be easy to lose track of what data you're following, or what code you're even looking at. Compiling at low or no optimization will keep source interleaved for an easier time (but may also hide bugs* that only manifest on higher optimization levels, or vice versa).

*Which are not actually bugs, but use of undefined behavior (UB). Which is often helpful to make use of, on embedded platforms like this, but which also gives the compiler a certain amount of license to do whatever it wants with the (UB) expressions/statements. Use it carefully.

Debugging isn't always possible, or easy (depending on MCU and available tools), but the upside to real-time MCUs like this is, you can put single-cycle port writes at the top and bottom of interrupts to see when they are executing. A brief syntax is required to make GCC emit an IO instruction: PORTx.OUT |= PINn_bm; and PORTx.OUT &= ~PINn_bm;. (At least as of more recent GCCs; it may be unreliable on very old versions, I don't know.)

Note that this will always appear within the ISR header/footer (PUSH / POP stuff) so isn't quite the totality of ISR execution. That just means, for some (dozens of?) cycles before the pin goes high, and after it goes low, the CPU is still in the ISR as such.

\$\endgroup\$
0
\$\begingroup\$

In the image, the only time there is UART communication at the same time as a rising edge is that one time of the glitch.

I'm guessing there is a UART related interrupt causing the glitch.

\$\endgroup\$
1
  • \$\begingroup\$ As I said in the question, this is a rare event and there is no correlation between the UART and the glitch. This screenshot is not able to convey microsecond-level timings, but it does show the glitch in the context of a "normal" pattern over the course of a few seconds. \$\endgroup\$
    – Wossname
    Mar 11, 2023 at 16:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.