Crude Timers

Making Timer Interrupt as short as possible

  • 1
  • 2
  • Page 1 of 2
Merlin
Administrator
Avatar
Gender:
Age: 24
Posts: 1370
Registered: 03 / 2005
Subject:

Crude Timers

 · 
Posted: 18.09.2018 - 14:18  ·  #1
I was hit with a problem recently. I had a very time critical interrupt. but I also needed several timers. I found that the time spent in the timer interrupt with SysTimers produced too much jitter in my interrupt.

So I designed this routine, which might be useful to others.

It is quite specific; At 16Mhz it produces a 'tick' of 1.024 ms. That was good enough for me, although it could be made more accurate.. Hence I called it 'crude' timers. You will need to modify CrudeTimersInit if your crystal is not 16MHz.

The 'timers' are defined as an enum, so you could in theory have up to 255. That is one downside. - all timers have to be defined in one place.

Usage can be very similar to SysTimers, i.e. SetCrudeTimer, IsCrudeTimerZero in place of SetSysTimer and IsSysTimerZero, but also you can register a call-back for each timer if you wish, but if you do that and don't is IsCrudeTimerZero, you will need to call CrudeTimerIdleAction regularly.

Anyway here it is.

Code
Unit CrudeTimers;
(*******************************************************************************
 *                                                                             *
 * The idea behind this is to spend as little time in the timer interrupt as   *
 * possible, for applications where other interrupts are critical, but timers  *
 * are still needed.                                                           *
 *                                                                             *
 * The timers themselves are defined  as an enum of type  TCrudeTimer.         *
 *                                                                             *
 * An optional callback of type procedure may be exectuted when the timer      *
 * reaches zero - for efficiency of code writing.                              *
 *                                                                             *
 * However it is not a requirement. IsCrudeTimerZero can be used instead       *
 *                                                                             *
 *******************************************************************************)


interface
// global part

{ $W+}                  // enable/disable warnings for this unit

uses
  ;

{--------------------------------------------------------------}
{ Const Declarations }
const

{$IDATA}
{--------------------------------------------------------------}
{ Type Declarations }
type
  TCrudeTimer = (TimerFred, TimerBill, TimerEtc); // Can be defined externally
  TOnTimerZero = procedure;

{--------------------------------------------------------------}
{ Var Declarations }
var

{--------------------------------------------------------------}
{ functions }
// can use like normal timers
procedure CrudeTimersInit;
procedure CrudeTimersIdleAction;

procedure SetCrudeTimer( pTimer : TCrudeTimer; pDelay : word ); // delay in ms
function IsCrudeTimerZero( pTimer : TCrudeTimer ) : boolean;

// Advanced options

procedure RegisterCallback( pTimer : TCrudeTimer; pCallback : TOnTimerZero; pRepeat : word );
          // repeat of 0 means no repeat. Otherwise = repeat in ms
          // If repeat is 0, a call to SetCrudeTimer is required. Otherwise not.
          // If no calls to IsCrudeTimerZero are made, CrudeTimerIdleACtion must be called regularly
          
procedure UnRegisterCallback( pTimer : TCrudeTimer );

implementation
// local part

{--------------------------------------------------------------}
{ Type Declarations }
type

{--------------------------------------------------------------}
{ Const Declarations }
const

{--------------------------------------------------------------}
{ Var Declarations }
{$DATA}
var
  fPulse : byte;
{$IDATA}
var
  fTimers : array[ TCrudeTimer ] of word;
  fTimerCallBack : array[ TCrudeTimer ] of TOnTimerZero;
  fTimersRepeat: array[ TCrudeTimer ] of word;
  
{--------------------------------------------------------------}
{ functions }
procedure RegisterCallback( pTimer : TCrudeTimer; pCallback : TOnTimerZero; pRepeat : word );
          // repeat of 0 means no repeat. Otherwise = repeat in ms
          // If repeat is 0, a call to SetCrudeTimer is required. Otherwise not.
          // If no calls to IsCrudeTimerZero are made, CrudeTimerIdleACtion must be called regularly
begin
  fTimerCallBack[ pTimer ] := pCallback;
  fTimersRepeat[ pTimer ] := pRepeat;
  if pRepeat <> 0 then
    fTimers[ pTimer ] := pRepeat;
  endif;
end;

procedure UnRegisterCallback( pTimer : TCrudeTimer );
begin
  fTimerCallBack[ pTimer ] := nil;
end;

procedure SetCrudeTimer( pTimer : TCrudeTimer; pDelay : word ); // delay in ms
begin
  fTimers[ pTimer ] := pDelay;
end;

function IsCrudeTimerZero( pTimer : TCrudeTimer ) : boolean;
begin
  CrudeTimersIdleAction;
  return( fTimers[ pTimer ] = 0);
  // Note - this is valid because fTimers is NOT updated in an interrupt!
end;

interrupt TIMER0;
begin
  asm;
    INC CrudeTimers.fPulse;
  endasm;
end;

procedure CrudeTimersInit;
begin
  // 16 MHz crystal
  asm;
                        LDI       _ACCA, 000h
                        OUT       tcnt0, _ACCA
                        LDI       _ACCA, 3
                        OUT       tccr0, _ACCA         ; internal clock / 64
                        LDI       _ACCA, 001h
                        STS       timsk0, _ACCA
  endasm;
end;

procedure CrudeTimersIdleAction;
var
  i : TCrudeTimer;
begin

    asm;
    CTAStart:
       TST CrudeTimers.fPulse
       BRNE CTAContinue
       JMP CTADone
    CTAContinue:
       DEC CrudeTimers.fPulse
    endasm;
    
    for i := MIN( TCrudeTimer ) to MAX(TCrudeTimer) do
      if fTimers[ i ] > 0 then
        dec( fTimers[ i ] );
        if fTimers[ i ] = 0 then
          if fTimerCallBack[ i ] <> nil then
            fTimers[ i ] := fTimersRepeat[ i ];
            fTimerCallBack[ i ]();
          endif;
        endif;
      endif;
    endfor;
    
    asm;
       JMP CTAStart
    CTADone:
    endasm;

end;

initialization
// at StartUp
  CrudeTimersInit;

// finalization          // optional
// at System_ShutDown
end CrudeTimers.


I hope someone finds it useful.
Avra
Schreiberling
Avatar
Gender:
Location: Belgrade, Serbia
Age: 53
Homepage: rs.linkedin.com/in…
Posts: 653
Registered: 07 / 2002
Subject:

Re: Crude Timers

 · 
Posted: 18.09.2018 - 22:48  ·  #2
Thank you very much!

Simple demo project would be very appreciated.
Merlin
Administrator
Avatar
Gender:
Age: 24
Posts: 1370
Registered: 03 / 2005
Subject:

Re: Crude Timers

 · 
Posted: 19.09.2018 - 09:16  ·  #3
Hi Avra.

I'll try and create one over the next week or so.
Avra
Schreiberling
Avatar
Gender:
Location: Belgrade, Serbia
Age: 53
Homepage: rs.linkedin.com/in…
Posts: 653
Registered: 07 / 2002
Subject:

Re: Crude Timers

 · 
Posted: 20.09.2018 - 10:07  ·  #4
Thanks a lot !
Merlin
Administrator
Avatar
Gender:
Age: 24
Posts: 1370
Registered: 03 / 2005
Subject:

Re: Crude Timers

 · 
Posted: 20.09.2018 - 11:27  ·  #5
Here are two examples, both of which run in the sim. One using the SysTimer style approach and the other using callbacks. You will need to create your own projects, but that is easily done.

Although each method is shown separately for clarity, you can mix and match.

Just a warning - this is a kind of 'how-to' demonstration, for my processor and my crystal. It contains assembly code that may (will) need to be tweaked for internal names if you want to use another processor or crystal

Someone may be able to improve on that...
You must be logged in or your permissions are to low to see this Attachment(s).
Thomas.AC
Benutzer
Avatar
Gender: n/a
Age: 43
Posts: 308
Registered: 07 / 2013
Subject:

Re: Crude Timers

 · 
Posted: 20.09.2018 - 21:56  ·  #6
Thanks a lot for sharing your code. Your code was good style and engage me to give feedback.

I think there are two mistakes.

1.) Read modify access to the variable fpulse in procedure CrudeTimersIdleAction() could miss (skip) a pulse and leading to some inaccuracy, if timer interrupt occurs exactly between the read and write access. This is probably not a big issue for your application.

2.) Reloading a crude timer with SetCrudeTimer() is very inaccurate. Imagine, the current value of fpulse is 40 (ms) at time of calling SetCrudeTimer(TimerFred, 30). Then, calling the procedure CrudeTimersIdleAction(). This would set the timer value of Fred to zero immediately, because there are already 40 (ms) queued.

And a suggestion / improvement, if interrupt latency is really a concern:
A timer interrupt is not required. Polling the hardware timer value (frequently) should be sufficient to implement your timer.
Merlin
Administrator
Avatar
Gender:
Age: 24
Posts: 1370
Registered: 03 / 2005
Subject:

Re: Crude Timers

 · 
Posted: 21.09.2018 - 10:39  ·  #7
Hi Thomas.

Thanks very much for your feed back.

To comment on your concerns

1.) I think you are wrong here. The reason why I use assembler, and INC and DEC is for precisely this reason. Also fPulse is a counter, not a boolean, for the same reason. The worst that can happen is that the action is delayed until the next poll, just as if the interrupt had occurred one machine cycle later.

2.) I think you misunderstand how it works. fPulse does not climb to a limit - it is simply a counter of how many times the interrupt has fired since the last poll by CrudeTimersIdleAction. As such, SetCrudeTimers does not touch fPulse at all, and the action you describe will not occur. Try it in the sim if you don't believe me. If the current value of a timer is 40ms and is replaced by 30, that is exactly what will happen. It will fire in 30 ms, not immediately. It is exactly the same behaviour as for SysTimer.

Your suggestion is interesting but requires much more frequent polling. In my case though it was not the only interrupt, just by far the largest, and I think that I can live with the 6 clock cycles it consumes ;-).
Thomas.AC
Benutzer
Avatar
Gender: n/a
Age: 43
Posts: 308
Registered: 07 / 2013
Subject:

Re: Crude Timers

 · 
Posted: 21.09.2018 - 18:51  ·  #8
Hi Merlin.

Quote

1.) I think you are wrong here. The reason why I use assembler, and INC and DEC is for precisely this reason. Also fPulse is a counter, not a boolean, for the same reason. The worst that can happen is that the action is delayed until the next poll, just as if the interrupt had occurred one machine cycle later.


Sorry. I overlooked that fPulse is located in $DATA area and you are using assembly 'INC' and 'DEC'. Is this possible with xmega, too?

Quote

2.) I think you misunderstand how it works. fPulse does not climb to a limit - it is simply a counter of how many times the interrupt has fired since the last poll by CrudeTimersIdleAction. As such, SetCrudeTimers does not touch fPulse at all, and the action you describe will not occur. Try it in the sim if you don't believe me. If the current value of a timer is 40ms and is replaced by 30, that is exactly what will happen. It will fire in 30 ms, not immediately. It is exactly the same behaviour as for SysTimer.


Please see my attached files.

Quote

Your suggestion is interesting but requires much more frequent polling. In my case though it was not the only interrupt, just by far the largest, and I think that I can live with the 6 clock cycles it consumes ;-).


Yes. A 8-Bit Timer with prescaler of 1024 needs polling time shorter than 16ms, quite fast. Someone may using a 16-Bit timer ...
You must be logged in or your permissions are to low to see this Attachment(s).
  • 1
  • 2
  • Page 1 of 2
Selected quotes for multi-quoting:   0

Registered users in this topic

Currently no registered users in this section

The statistic shows who was online during the last 5 minutes. Updated every 90 seconds.
MySQL Queries: 15 · Cache Hits: 14   129   143 · Page-Gen-Time: 0.026083s · Memory Usage: 2 MB · GZIP: on · Viewport: SMXL-HiDPI