Skip to content

Commit 65d7294

Browse files
Merge pull request #1 from ElectricRCAircraftGuy/adding_in_timer1_counter
Adding in "timer1_counter" capability for 0.5us timestamps
2 parents 7dfa43b + c98b4ea commit 65d7294

File tree

5 files changed

+365
-12
lines changed

5 files changed

+365
-12
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,4 @@ Temporary Items
4444
eRCaGuy_PPM_Writer - time (work) log.ods
4545
.~lock.eRCaGuy_PPM_Writer - time (work) log.ods#
4646
eRCaGuy_PPM_Writer - What to work on next - Gabriel.odt
47+
.~lock.eRCaGuy_PPM_Writer - What to work on next - Gabriel.odt#
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
PPM_Writer_demo2.ino
3+
By Gabriel Staples
4+
http://www.ElectricRCAircraftGuy.com
5+
My contact info is available by clicking the "Contact Me" tab at the top of my website.
6+
Written: 26 March 2016
7+
Updated: 26 March 2016
8+
9+
LICENSE: GNU GPLV3 or later (refer to .h file and attached license for details)
10+
11+
-outputs PPM signal on Arduino pin 9
12+
*/
13+
14+
#include <eRCaGuy_PPM_Writer.h>
15+
16+
const byte CH1 = 0,
17+
CH2 = 1,
18+
CH3 = 2,
19+
CH4 = 3,
20+
CH5 = 4,
21+
CH6 = 5,
22+
CH7 = 6,
23+
CH8 = 7;
24+
25+
26+
void setup()
27+
{
28+
Serial.begin(115200);
29+
Serial.println(F("\n\nbegin"));
30+
31+
//manually set a few channels
32+
PPMWriter.setChannelVal(CH1,900*2); //set channel 1 (index 0) in the PPM train to 900us
33+
PPMWriter.setChannelVal(CH2,1000*2); //set channel 2 (index 2) in the PPM train to 1000us
34+
PPMWriter.setChannelVal(CH3,1100*2);
35+
PPMWriter.setChannelVal(CH4,1200*2);
36+
37+
PPMWriter.setPPMPeriod(20000*2UL); //currently you cannot set above 65535 since Timer1 is a 16-bit timer/counter, and I'm not taking into account rollovers past this value, *yet*; NB: the UL is *mandatory* to force the constant/literal multiplication here to follow unsigned long rules *instead of* signed int rules, which I believe are default otherwise here in this compiler's C++ handling; take out the UL and you'll see weird results/errors for any values where the result of the multiplication is > 32767, which is the max value storable in a signed int.
38+
Serial.print(F("PPMPeriod(0.5us) = ")); Serial.println(PPMWriter.getPPMPeriod());
39+
Serial.print(F("PPMFreq(Hz) = ")); Serial.println(PPMWriter.getPPMFrequency());
40+
41+
PPMWriter.begin(); //start the PPM train; default will be 1500us for all channels, 22ms for each PPM frame (I'm copying the Spektrum DX8 signal)
42+
43+
44+
PPMWriter.overflowInterruptOff();
45+
PPMWriter.overflowInterruptOn();
46+
PPMWriter.attachOverflowInterrupt(blinkLED);
47+
// PPMWriter.detachOverflowInterrupt();
48+
}
49+
50+
void loop()
51+
{
52+
static unsigned long loopCount = 0;
53+
54+
static byte ch_i = 0;
55+
if (PPMWriter.readChannelFlag(ch_i)==true)
56+
{
57+
unsigned long t_now = micros(); //us
58+
unsigned long t_now2 = PPMWriter.getMicros(); //us
59+
unsigned long count_now = PPMWriter.getCount(); //0.5us units
60+
static unsigned long t_now_old = t_now; //us
61+
static unsigned long t_now2_old = t_now2_old; //us
62+
static unsigned long count_now_old = count_now; //0.5us
63+
64+
//print data
65+
Serial.print(F("ch_i = ")); Serial.print(ch_i); Serial.print(F(", frameNum = "));
66+
Serial.print(PPMWriter.getFrameNumber());
67+
Serial.print(F(", t_now(us) = ")); Serial.print(t_now); Serial.print(F(", t_now2(us) = ")); Serial.print(t_now2);
68+
Serial.print(F(", count_now(0.5us) = ")); Serial.print(count_now);
69+
//deltas:
70+
Serial.print(F(", delta times = ")); Serial.print(t_now - t_now_old); Serial.print(", ");
71+
Serial.print(t_now2 - t_now2_old); Serial.print(", "); Serial.println(count_now - count_now_old);
72+
73+
//updates
74+
t_now_old = t_now; //us
75+
t_now2_old = t_now2; //us
76+
count_now_old = count_now; //0.5us
77+
78+
// ch_i++;
79+
// if (ch_i >= PPMWriter.getNumChannels())
80+
// ch_i = 0; //reset
81+
}
82+
83+
}
84+
85+
void blinkLED()
86+
{
87+
pinMode(13,OUTPUT);
88+
static bool led_state = LOW;
89+
led_state = !led_state; //toggle
90+
digitalWrite(13,led_state);
91+
}
92+
93+
94+

eRCaGuy_PPM_Writer.cpp

Lines changed: 125 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ Pin Mapping: https://www.arduino.cc/en/Hacking/PinMapping168
5858
#include "eRCaGuy_PPM_Writer.h"
5959
#include <util/atomic.h> //http://www.nongnu.org/avr-libc/user-manual/group__util__atomic.html
6060

61+
//NOTE/TODO: ALLOW ONE TO CHOOSE WHICH TIMER YOU'D LIKE TO USE FOR THIS LIBRARY IN THE "eRCaGuy_TimerCounterTimers.h" file
62+
#include "eRCaGuy_TimerCounterTimers.h"
63+
6164
//macros
6265
#define readPinA() (PINB & _BV(1)) //Arduino pin 9
6366
#define togglePinA() (TCCR1C = _BV(FOC1A)) //see datasheet pg. 135 & 132; force OC1A pin (Arduino D9) to toggle; datasheet pg. 132: setting one or both of the COM1A1:0 bits to 1 overrides the normal port functionality of the I/O pin it is connected to, so this is how you must toggle the pin; digitalWrite(9,HIGH/LOW) on pin 9 will NOT work anymore on this pin; note, however, that *reading* the pin port directly, or calling digitalRead(9) DOES still work!
@@ -77,19 +80,33 @@ eRCaGuy_PPM_Writer PPMWriter; //preinstantiation of object
7780
//========================================================================================================
7881
//ISRs
7982
//========================================================================================================
83+
84+
//--------------------------------------------------------------------------------------------------------
8085
//Timer 1 Compare Match A interrupt
86+
//--------------------------------------------------------------------------------------------------------
8187
ISR(TIMER1_COMPA_vect)
8288
{
8389
// writePin2HIGH; //FOR MEASURING THE ISR PROCESSING TIME. ANSWER: <= ~6us per ISR interrupt
8490
PPMWriter.compareMatchISR();
8591
// writePin2LOW;
8692
}
8793

88-
//Here is where the magic happens (ie: the actual writing of the PPM signal)
89-
void eRCaGuy_PPM_Writer::compareMatchISR()
94+
//--------------------------------------------------------------------------------------------------------
95+
//Timer 1 Overflow Interrupt
96+
//--------------------------------------------------------------------------------------------------------
97+
ISR(TIMER1_OVF_vect) //Timer1's counter has overflowed
98+
{
99+
PPMWriter.overflowISR();
100+
}
101+
102+
//--------------------------------------------------------------------------------------------------------
103+
//compareMatchISR
104+
//-Here is where the magic happens (ie: the actual writing of the PPM signal)
105+
//--------------------------------------------------------------------------------------------------------
106+
inline void eRCaGuy_PPM_Writer::compareMatchISR()
90107
{
91108
//local variables
92-
unsigned int incrementVal; //units of 0.5us
109+
long incrementVal; //units of 0.5us; make long too allow for (and later be able to handle) the rare case of negative values, which would occur if someone tries to set the PPM period too short
93110

94111
if (_currentState==FIRST_EDGE)
95112
{
@@ -134,6 +151,18 @@ void eRCaGuy_PPM_Writer::compareMatchISR()
134151
_timeSinceFrameStart += incrementVal; //0.5us; this will be the time elapsed at the start of the NEXT Compare Match A interrupt
135152
}
136153

154+
//--------------------------------------------------------------------------------------------------------
155+
//overflowISR
156+
//-this is used to generate a 0.5us timestamp capability, to get better resolution than what micros() can provide
157+
//--(micros only has a 4us resolution)
158+
//--------------------------------------------------------------------------------------------------------
159+
inline void eRCaGuy_PPM_Writer::overflowISR()
160+
{
161+
_overflowCount++;
162+
if (_userOverflowFuncOn)
163+
_p_userOverflowFunction(); //call the user-attached function
164+
}
165+
137166
//========================================================================================================
138167
//eRCaGuy_PPM_Writer CLASS METHODS
139168
//========================================================================================================
@@ -157,6 +186,8 @@ eRCaGuy_PPM_Writer::eRCaGuy_PPM_Writer()
157186
_channelSpace = DEFAULT_CHANNEL_SPACE; //0.5us
158187
_frameNumber = 0;
159188
_PPMPolarity = PPM_WRITER_NORMAL;
189+
_overflowCount = 0; //for 0.5us timestamps
190+
_userOverflowFuncOn = false;
160191
}
161192

162193
//--------------------------------------------------------------------------------------------------------
@@ -184,12 +215,100 @@ void eRCaGuy_PPM_Writer::begin()
184215

185216
OCR1A = TCNT1 + 100; //set to interrupt (to begin PPM signal generation) 50us (100 counts) from now
186217

187-
//enable Output Compare A interrupt (TIMSK1, datasheet pg. 136)
188-
TIMSK1 = _BV(OCIE1A);
218+
TIMSK1 = _BV(OCIE1A); //enable Output Compare A interrupt (TIMSK1, datasheet pg. 136)
219+
overflowInterruptOn();
189220

190221
ensurePPMPolarity();
191222
}
192223

224+
//--------------------------------------------------------------------------------------------------------
225+
//getCount
226+
//-get the total 32-bit (unsigned long) count on the timer, ***in units of COUNTS, not microseconds***
227+
//--ex: with prescaler = 8, you have 0.5us/count for a 16Mhz Arduino, so you divide counts by 2 to get us
228+
//-Note that the time returned WILL update even in Interrupt Service Routines (ISRs), so if you call this function in an ISR, and you want the time to be as close as possible to a certain event that occurred which called the ISR you are in, make sure to call getCount() first thing when you enter the ISR.
229+
//-Also, note that calling getCount() is faster than calling getMicros() and is therefore the preferable way to measure a time interval.
230+
//--For example: call getCount() at the beginning of some event, then at the end. Take the difference and divide it by 2 (assuming 16Mhz Arduino, prescaler is 8) to get the time interval in microseconds.
231+
//--------------------------------------------------------------------------------------------------------
232+
unsigned long eRCaGuy_PPM_Writer::getCount()
233+
{
234+
unsigned long totalCount; //units of COUNTS
235+
uint8_t SREG_old = SREG; //back up the AVR Status Register; see example in datasheet on pg. 14, as well as Nick Gammon's "Interrupts" article - http://www.gammon.com.au/forum/?id=11488
236+
noInterrupts(); //prepare for critical section of code
237+
{
238+
unsigned int TCNTn_save = TCNTn; //grab the counter value from timer
239+
bool overflowFlag = bitRead(TIFRn,0); //grab the timer overflow flag value; see datasheet pg. 160, for ex.
240+
if (overflowFlag) //if the overflow flag is set
241+
{
242+
TCNTn_save = TCNTn; //update variable just saved since the overflow flag could have just tripped between previously saving the TCNTn value and reading bit 0 of TIFRn. If this is the case, TCNTn might have just changed from 255 to 0 (for an 8-bit timer), and so we need to grab the new value of TCNTn to prevent an error of up to 127.5us in any time obtained using this counter. (Note: 255 counts / 2 counts/us = 127.5us).
243+
//Note: this line of code DID in fact fix the error just described, in which I periodically saw an error of ~127.5us in some values read in by some PWM read code I wrote.
244+
_overflowCount++; //force the overflow count to increment
245+
TIFRn |= _BV(0); //reset the Timer overflow flag since we just manually incremented above; ex: see datasheet pg. 160; this prevents execution of the timer's overflow ISR
246+
}
247+
totalCount = _overflowCount*TC_RESOLUTION + TCNTn_save;
248+
}
249+
SREG = SREG_old; //restore interrupt-enable state
250+
return totalCount;
251+
}
252+
253+
//--------------------------------------------------------------------------------------------------------
254+
//getMicros
255+
//-returns the 32-bit timer time, with full resolution, as a float.
256+
//--Ex: for a 16Mhz Arduino, with prescaler of 8, the resolution is 0.5us per count; with prescaler of 1, the resolution is 0.0625us per count. Both of these are better than the default Arduino micros() resolution of 4us
257+
//-this function is slower than calling getCount() and therefore is not the preferred way of getting time. It is better to get the time by calling getCount() then dividing the value by the appropriate divisor to convert to us.
258+
//--------------------------------------------------------------------------------------------------------
259+
float eRCaGuy_PPM_Writer::getMicros()
260+
{
261+
return (float)getCount()/2; //returns a time stamp in us
262+
}
263+
264+
//--------------------------------------------------------------------------------------------------------
265+
//overflowInterruptOff
266+
//-turns off the timer's overflow interrupt so that you no longer interrupt your code (every 128us for an 8-bit timer with prescaler of 8) in order to increment your overflow counter.
267+
//-This may be desirable when you are no longer needing timestamps and want your main code or other interrupts to run more jitter-free, but you don't want to call end() in order to change all of the timer's settings back to default.
268+
//-Assuming 16Mhz Arduino, 8-bit timer, and prescaler of 8 (ie: 0.5us/count), turning off the overflow interrupt will give you savings of approximately 4~5us every 128us. This is a CPU processing time savings of ~4%.
269+
//--Source: Nick Gammon; "Interrupts" article; "How long does it take to execute an ISR?" section, found here: http://www.gammon.com.au/forum/?id=11488
270+
//-Note: If you disable the timer overflow interrupt but still call getCount() or getMicros() at least every 128us (or whatever your interrupt period is given your clock speed, timer size [8 or 16-bit], and prescaler), you will notice no difference in the counter, since calling getCount() or getMicros() also checks the interrupt flag and increments the overflow counter automatically. You have to wait > 128us between calls before you see any missed overflow counts.
271+
//--------------------------------------------------------------------------------------------------------
272+
void eRCaGuy_PPM_Writer::overflowInterruptOff()
273+
{
274+
TIMSKn &= ~_BV(0);
275+
}
276+
277+
//--------------------------------------------------------------------------------------------------------
278+
//overflowInterruptOn
279+
//-turns the timer's overflow interrupt back on, so that the overflow counter will start to increment again
280+
//-see "overflowInterruptOff" for details
281+
//--------------------------------------------------------------------------------------------------------
282+
void eRCaGuy_PPM_Writer::overflowInterruptOn()
283+
{
284+
TIMSKn |= _BV(0);
285+
}
286+
287+
//--------------------------------------------------------------------------------------------------------
288+
//attachOverflowInterrupt(myFunction)
289+
//-this allows you to attach your own custom function that you want to be called every overflow interrupt. Very useful for recurring events, for example, but you'll have to write some more smarts into your function if you want specific timing.
290+
//-this will also be very useful for my upcoming SoftwarePWM library, for example, which will allow software PWMing (analogWrite) on EVERY Arduino pin!
291+
//--------------------------------------------------------------------------------------------------------
292+
void eRCaGuy_PPM_Writer::attachOverflowInterrupt(void (*myFunc)())
293+
{
294+
//ensure atomic access
295+
uint8_t SREGbak = SREG;
296+
noInterrupts();
297+
_userOverflowFuncOn = true;
298+
_p_userOverflowFunction = myFunc; //attach function
299+
SREG = SREGbak; //restore interrupt state
300+
}
301+
302+
//--------------------------------------------------------------------------------------------------------
303+
//detachOverflowInterrupt
304+
//-detach (stop) your attached function
305+
//--ie: prevent it from being called
306+
//--------------------------------------------------------------------------------------------------------
307+
void eRCaGuy_PPM_Writer::detachOverflowInterrupt()
308+
{
309+
_userOverflowFuncOn = false; //this is already atomic since it is a single byte; no atomic guards necessary
310+
}
311+
193312
//--------------------------------------------------------------------------------------------------------
194313
//ensurePPMPolarity()
195314
//-private method
@@ -278,6 +397,7 @@ void eRCaGuy_PPM_Writer::setChannelVal(byte channel_i, unsigned int val)
278397
//setPPMPeriod
279398
//-control the PPM output frequency by setting the desired PPM train frame period directly
280399
//-units are 0.5us
400+
//-currently, the longest value you can set is 2^16 - 1, or 65535, since the longest timer/counter is only 16-bits, and I'm not taking into account roll-overs beyond the length of the timer/counter, *yet*. ***********TODO: do take them into account so I can start using 8-bit timer/counters for this library too.***************
281401
//-ex: setPPMPeriod(20000*2) sets a pd of 20000us (40000 0.5us counts), which results in a PPM freq. of 1/20ms=50Hz
282402
//-WARNING: if your channel values are all at their max values, the local variable "_minFrameSpace" acts as a safety feature to ensure the frame space after the last channel is longer than the longest channel. This is necessary because that is how a device *reading* a PPM signal finds the first frame: it knows that the *longest* frame is the frame space, and that the first channel comes after that. In the event that you set the PPM period too short, or have too many channels, with all of them maxed out, the PPM writer will force the frame space after the last channel to be at least as long as what is set in the local variable "_minFrameSpace." In this event, the PPM period will be lengthened for that frame, and the desired PPM frequency will not be reached.
283403
//--ex: assume 9 channels, max channel value of 2100us, PPM period set to 20ms; if all channels are simultaneously maxed out it takes 2100x9 = 18.9ms just to write the channels. To keep the 20ms PPM period, the frameSpace would have to be 20-18.9 = 1.1ms. However, THIS WILL BREAK THE SIGNAL AND PREVENT THE DEVICE READING THE PPM SIGNAL FROM DETERMINING WHERE THE START OF THE PPM FRAME IS. Therefore, the _minFrameSpace (default 3ms last I set it) will be used instead of 1.1ms. This makes the PPM frame 18.9ms + 3ms = 21.9ms (45.7Hz) instead of the desired 20ms (50Hz). Depending on your settings, since this PPM writer is totally user-customizable, you may get even more drastic results. Ex: if you were trying to fit 12 full-length channels in a 20ms PPM frame....you do the math.

0 commit comments

Comments
 (0)