Correct timing by timer + overflow count - Page 1
Forum section: electronics / general

author: igendel
author_rank: Frequent Contributor
timestamp: September 08, 2015, 09:45:07 am
content: Hi all,This is more of an MCUprogrammingquestion, that was mentioned in Jack Ganssle's latest newsletter. He presented the naive way of measuring time beyond a timer's bit-size limit: whenever the timer overflows, in the overflow interrupt, you increase a separate variable. Then, when you need to know the time, you take that variable and the current timer value, and do the math.Jack said this usually works but is guaranteed to fail eventually. I assume that's because between the time you access the variable and the time you access the timer value, an overflow may occur and the overall result will be false.This issue was actually presented as sort of an advertisement to one of Jack's seminars, so I hope I don't harm his business by asking this here- how does one overcome the problem?It seems to me that the only really safe way is to halt the timer itself right before the reading. However, this pause will interfere with the accuracy of the timing, unless I do exact cycle-counting on the process (or activate a separate timer just for that) and add the missing time to the timer afterwards. This doesn't feel very elegant.So, is there a better way?Thanks!

author: Jeroen3
author_rank: Super Contributor
timestamp: September 08, 2015, 10:07:56 am
content: You'll need a capture time (on that stops by hardware) otherwise your extra bit hack will have interrupt jitter.But, an RTOS might make the jitter predictable. Which gives an known error you can put on your spec sheet.https://en.wikipedia.org/wiki/Interrupt_latency

author: Psi
author_rank: Super Contributor
timestamp: September 08, 2015, 10:12:52 am
content: Use a stm32 and enable hardware master/slave over all of the timersThat's like 6x 16bit timers and one 32bit timerA 128 bit counter at 48mhz overflows after 7,089,215,977,519,551,322,153,637,654,828 seconds

author: Chris C
author_rank: Frequent Contributor
timestamp: September 08, 2015, 10:13:06 am
content: 1) Disable the timer interrupt (preferably all interrupts, so we can execute the following in a predictably timely fashion).2) Get a copy of the timer value and the overflow count.3) Check the interrupt flag. If set, then we know we have to add one to our copy of the overflow count, since the interrupt hasn't yet been able to do it. The timer value is where uncertainly creeps in, as we don't know if the value overflowed prior to, or after, previously copying its value - and if it was at 999 of 1,000, adding another 1,000 via the overflow count would give us an incorrect result. But we know for certain it's overflowed NOW! So copy the timer value again, problem solved.4) Re-enable interrupts.5) Use the copy of value and overflow count to calculate our final time.I figured this out on my own, the first time I encountered this problem. There may be other ways to do it. Jack has no monopoly on the solution.

author: igendel
author_rank: Frequent Contributor
timestamp: September 08, 2015, 10:33:37 am
content: Quote from: Chris C on September 08, 2015, 10:13:06 amYes! That last step, of re-reading the timer value if the interrupt flag is set, is what I missed in my attempt to solve this earlier. Thanks!

author: Chris C
author_rank: Frequent Contributor
timestamp: September 08, 2015, 10:34:51 am
content: Here's an alternate solution. It doesn't require disabling interrupts. But does require a long enough timer interval, and short enough interrupts, that the following can ALWAYS run to completion in half the interval. If you cannot guarantee that, it's fallible, which is why I usually use the prior approach.1) Get a copy of the timer value (V1) and the overflow count (OC).2) Get another, separate copy of the timer value (V2). If V2 < V1, then resolve ambiguity by setting V1=V2, and getting OC again.3) Compute time based on V1 and OC.

author: hamster_nz
author_rank: Super Contributor
timestamp: September 08, 2015, 10:38:37 am
content: Old-timer timer trick from 8-bit days for accessing long timers through small registers was to read the least most significant word(s) then least significant word, then read the most significant word again. If the most significant word changes you've sampled it during a roll-over, so repeat reading the least significant and most significant words again. The most significant word should be stable. Worst case is you'll stall for a few a handful of instructions.If you do have to reread the registers you can roll take a few counts off the value to make allowance for the extra instruction cycles it took.

author: igendel
author_rank: Frequent Contributor
timestamp: September 08, 2015, 10:47:35 am
content: Quote from: Psi on September 08, 2015, 10:12:52 amQuite safe for everyday use, I guess, but if I want my system to outlast the universe, there may still be an issueI actually got a cheap STM32 board lately and a programmer for it, but proper documentation for beginners seems hard to find. I guess I'll have to do some serious datasheet reading.ThanksChris Candhmaster_nzfor the other suggestion. It sounds reasonable tooEdit: Oh, and thanksJeroen3, however I currently don't work with RTOSs.

author: ralphd
author_rank: Frequent Contributor
timestamp: September 08, 2015, 12:11:16 pm
content: This is pretty basic. Just disable interrupts while accessing the counter in RAM. The timer still keeps running without any skew getting added.I posted some example code to do this on an AVR last year.http://nerdralph.blogspot.ca/2014/07/writing-avr-interrupt-service-routines.html

author: igendel
author_rank: Frequent Contributor
timestamp: September 08, 2015, 12:42:18 pm
content: Quote from: ralphd on September 08, 2015, 12:11:16 pmNo, if I understand you correctly, that doesn't solve the problem Ganssle was talking about.Let's say the timer is 8-bit. To get the time, I read it, disable interrupts, read the counter in RAM etc.Now suppose I caught the timer when it was 255, one nanosecond before it overflowed. There is a possibility, I think, that the overflow interrupt will trip before my "main" code gets the chance to disable it. So it runs, the counter is +1'ed, timer now 0, and only then I get to disable the interrupt and read the (updated) counter. So I get the new RAM value but the old timer value, and that's a bug.Same if I read the RAM counter first and the timer later.

author: Brutte
author_rank: Frequent Contributor
timestamp: September 08, 2015, 02:29:13 pm
content: I have asked a more generic question onavrfreakssome time ago.By contrast, I was not interested in extending one timer register but rather whole timer.I have found that implementations can be categorized in terms of "permissible delays" and range from "ridiculous" up to "timer period".All (not buggy) extensions require runtime resolution of events which is PITA but doable. An example for resolution of only two events for AVR8 is available on the linked page.QuoteThat is neither pretty nor basic.

author: Chris C
author_rank: Frequent Contributor
timestamp: September 09, 2015, 12:38:29 am
content: Quote from: Brutte on September 08, 2015, 02:29:13 pmVirtual timers could accurately be called the heart of my custom PIC programming environment.It takes a single real timer, and expands it to up to 16 virtual timers; with up to 32-bit resolution, regardless of what the real timer is. It's bug-free, lean, can defer its own maintenance tasks in order to not delay an imminent event, and emulates interrupt chaining even if the MCU lacks that feature. Glitchless PWM is built-in. So is MIBAM, which can make every pin on an MCU capable of LED dimming, regardless of the number of pins, for about 5% CPU utilization.Other functions can be hard coded in, or provided via a virtual interrupt (callback). If any function will run too long, it can use what I call a "bridge" to pass data down to a lower priority interrupt; which does the real processing, and no longer interferes with other virtual timers.It can be used to poll pins to implement virtual external interrupts, frequency counters, duty cycle measurement, quadrature decoding, and so on. That obviously requires a trade-off between poll rate and CPU utilization, but it's still handy. I can partially port my environment to a new MCU, and add native hardware support for these functions when it actually becomes necessary, or at my leisure. And after that, I can still use it to extend what hardware peripherals are there, using hardware for the most demanding tasks, and software for the least.And it can produce "0-tick error" when generating a single, larger timer. Though I'm not sure how often that would actually be useful. I feel that would usually be better addressed by starting with at least a 16-bit MCU in the first place.It seems your search for prior art was limited to a single forum, and probably also too narrow in looking specifically for a timer extension. You'll have better luck by Googling for finished and tested interrupt-driven software PWM implementations. Which seem to be more common. And if you think about it, they are nothing but virtual timers being used for a single purpose.Still, it seems relatively few people attempt anything of this nature. Or get it right on the first try. So by all means, publish your project!

author: ralphd
author_rank: Frequent Contributor
timestamp: September 09, 2015, 04:41:32 am
content: Quote from: igendel on September 08, 2015, 12:42:18 pmYou obviously didn't look at my code and I'm guessing you didn't even read the whole blog post that explains the code.The getseconds() function is only a few lines of C.Now if you need a microsecond-resolution timer things get a bit more complicated, but you never said that is a requirement.

author: Jeroen3
author_rank: Super Contributor
timestamp: September 09, 2015, 05:34:57 am
content: Quote from: Chris C on September 09, 2015, 12:38:29 amVirtual timers are often included in operating systems, few people have time to roll their own.

author: igendel
author_rank: Frequent Contributor
timestamp: September 09, 2015, 06:37:28 am
content: Quote from: ralphd on September 09, 2015, 04:41:32 amIt might be my own daftness, but even now afterre-reading the post you linked to, I don't see how it relates to the very specific issue I raised in the original post.

author: Brutte
author_rank: Frequent Contributor
timestamp: September 09, 2015, 10:34:47 am
content: Quote from: Chris C on September 09, 2015, 12:38:29 amThat was not the subject of the Muse.QuoteNor software PWM.As stated before:Quotethe software PWM falls into "ridiculous" category because to obtain a "0-tick error" you would have to pinpoint toggling code in exact instruction clock.QuoteI have already abandoned that sinking ship.

author: ralphd
author_rank: Frequent Contributor
timestamp: September 09, 2015, 12:05:05 pm
content: Quote from: igendel on September 09, 2015, 06:37:28 amMy solution provides a glitch-free timer. Isn't that what you want?If you want to do it the more complicated way, reading the timer register and combining it with the overflow count, it's a bit more code, but still not rocket science. After cli(), if the overflow interrupt flag is set and the timer register value is < 128 then add 1 to the overflow value.

author: igendel
author_rank: Frequent Contributor
timestamp: September 09, 2015, 03:36:25 pm
content: Quote from: ralphd on September 09, 2015, 12:05:05 pmAs a by-product, perhaps. My original question was about the technique of counting time specifically using overflow counting + timer value.Quote from: ralphd on September 09, 2015, 12:05:05 pmYes, in the vein of some answers given above. It all boils down to checking, in retrospect, whether an overflow occurred while I was "taking time"; the exact details of the implementation seem to be a matter of taste and resource constraints. Thanks!

author: hamster_nz
author_rank: Super Contributor
timestamp: September 09, 2015, 11:40:04 pm
content: Quote from: ralphd on September 09, 2015, 12:05:05 pmI didn't read your code, but based on the description "This is pretty basic. Just disable interrupts while accessing the counter in RAM. The timer still keeps running without any skew getting added" are you sure that it is glitch free?Right column is RAM_Counter:Timer Counter for a 16 bit counter, left column is the instructions being run....0000:FFFF disable interrupts0000:FFFF read timer. Gives xxxx:FFFFF for timer value (the upper half comes from RAM)--- timer rolls over here ---0000:0000 read RAM - GIves 0000:FFFF for timer value0000:0000 check timer's IRQ flag = true0000:0000 enable interrupts-- IRQ happens --0001:0000 IRQ flag is set, so inc reading from RAM (0000:FFFF => 00001:FFFF)Or how about:0000:FFFF disable interrupts0000:0000 read timer. Gives xxxx:FFFF for timer value (the upper half comes from RAM)0000:0000 read RAM - GIves 0000:FFFF for timer value--- timer rolls over here ---0000:0000 check timer's IRQ flag = true0000:0000 enable interrupts-- IRQ happens --0001:0000 IRQ flag is set, so inc reading from RAM (0000:FFFF => 00001:FFFF)I guess you are now about to suggest that if an IRQ was flagged you look at the value from the h/w timer, and decide if the IRQ was raised before of after the timer was read (based on the high bit).Why not just keep it simple...read RAM value into reg1read H/W timer into reg2read RAM value into reg3if reg3 != reg1clear reg2 to zerouse reg3:reg2 as timer valueYou don't even need to disable interrupts...

author: ralphd
author_rank: Frequent Contributor
timestamp: September 10, 2015, 02:52:54 am
content: Quote from: igendel on September 09, 2015, 03:36:25 pmI can only think of one way to do it, and it is straightforward enough that I doubt there's a better way: (code tested only in my head)Code:__uint24 get_ticks(){uint16_t ovfl;uint8_t timerval;cli();ovfl = _overflows;timerval = TCNT0;if ( (TIFR0 & (1<<TOV0)) && (timerval < 128) ) ovfl++;sei();return ( (ovfl << 8) | timerval );}

author: nctnico
author_rank: Super Contributor
timestamp: September 10, 2015, 04:19:26 am
content: Quote from: igendel on September 08, 2015, 12:42:18 pmInteresting problem... The timer interrupt adds extra difficulties due to parallel process synchronisation. It is not a particular timer problem but a process synchronisation problem. If you don't use a timer interrupt but just poll the counter from a main loop you can create a global time counter in a very simple way. Every time you read a value from the counter which is less than the previous value an overflow has occured. If you need the instant time just call the routine which determines the time. The only requirement is that the counter is polled often enough so that an overflow do not happen unnoticed.

author: igendel
author_rank: Frequent Contributor
timestamp: September 10, 2015, 11:24:30 am
content: Quote from: ralphd on September 10, 2015, 02:52:54 amWhy do you check whether timerval < 128?

author: mikerj
author_rank: Super Contributor
timestamp: September 10, 2015, 11:48:08 am
content: Quote from: igendel on September 10, 2015, 11:24:30 amThink about what would happen if the overflow occurred after TCNT0 had been read.

author: igendel
author_rank: Frequent Contributor
timestamp: September 10, 2015, 12:00:49 pm
content: Quote from: mikerj on September 10, 2015, 11:48:08 amActually that's another bug in the code above. If the OF flag is raised, it should re-read TCNT0 to get its updated value, not just increase the overflow count. This has nothing to do with the number 128. [edit: perhaps no need to re-read TCNT0, but the 128 check is still redundant]

author: ralphd
author_rank: Frequent Contributor
timestamp: September 11, 2015, 12:39:14 am
content: Quote from: igendel on September 10, 2015, 12:00:49 pmSounds like it is rocket science to you.

author: igendel
author_rank: Frequent Contributor
timestamp: September 11, 2015, 01:02:34 am
content: Quote from: ralphd on September 11, 2015, 12:39:14 amKind as you are, that still doesn't answer the question - what's the point of comparing to 128.Under what circumstances, in the code you wrote, will the OF flag be set AND timerval will be>=128?[Added: to prevent further insults, please be aware that this is just "part 1" of the question]

author: igendel
author_rank: Frequent Contributor
timestamp: September 11, 2015, 01:28:39 am
content: Ok, sorry for the constant editing, I think I finally figured it out.Still, a code like this, without that comparison, is clearer and will probably give more accurate results:Code:cli();ovfl = _overflows;timerval = TCNT0;if (TIFR0 & (1<<TOV0)) {ovfl++;timerval = TCNT0;}sei();return ( (ovfl << 8) | timerval ); // return result is quite accurate, no time wasted on comparison

author: ralphd
author_rank: Frequent Contributor
timestamp: September 11, 2015, 02:33:25 am
content: Quote from: igendel on September 11, 2015, 01:28:39 amThat way is worse because it adds jitter when the overflow flag is set.And although avoiding the comparison to 128 is a wash (maybe 1 extra cycle) vs reloading TCNT0 on the 8-bit AVR, on other MCUs the IO read lantency can be slower than the comparison.p.s. I think I went pretty easy on you after you falsely accused me of posting buggy code. Suggesting you are not as smart as me isn't an insult if it is true. :-)

author: Chris C
author_rank: Frequent Contributor
timestamp: September 11, 2015, 03:35:19 am
content: Quote from: ralphd on September 11, 2015, 02:33:25 amThen again, on some MCUs the "disable all interrupts" facility does not disable those at the very highest priority. That priority is reserved for hard real-time tasks, like NTSC generation for instance. And if that priority is used, not only is jitter reduction in get_ticks() pointless; but you may run into a situation where the timer interrupt flag is set and TCNT0>=128, yet hasn't rolled over a full second time.Introducing hardware-specific and unusual scenarios does complicate things a bit, and I don't blame  for questioning why you did it that way.

author: igendel
author_rank: Frequent Contributor
timestamp: September 11, 2015, 08:44:16 am
content: Quote from: ralphd on September 11, 2015, 02:33:25 amYeah, sorry about that. I guess I was inclined to doubt you, since your very first response in this thread was irrelevant to the question, and then you falsely accusedmeof not reading carefully.

author: ralphd
author_rank: Frequent Contributor
timestamp: September 11, 2015, 11:56:43 am
content: Quote from: igendel on September 11, 2015, 08:44:16 amIf you are serious, then I'll be adding you to my ignore preferences. You ended your initial post with, "So, is there a better way? "My first response was a better way to do correct timing. You respond saying my solution doesn't solve the problem, and then when you realize it avoids the problem altogether while providing a glitch-free timer, you say it was irrelevant to the question. Kinda reminds me of the ex-wife, "I don't care what I said, that's not what I meant!" :-)And since you admitted you had to re-read my blog post and code to understand how it really works, then you really didn't read it carefully the first time.

author: igendel
author_rank: Frequent Contributor
timestamp: September 11, 2015, 01:25:39 pm
content: Quote from: ralphd on September 11, 2015, 11:56:43 amA better way => to overcome the specific overflow-while-reading issue, that I detailed so clearly throughout the post.Please do add me to your ignore list. It appears both of us will benefit from it.

author: C
author_rank: Super Contributor
timestamp: September 11, 2015, 03:47:54 pm
content: Hi guysI may be wrong, but I see some problems with the general idea.Firstremember that C and other high level languages can reorder the machine code they can produce.A = A + 1could have additional instructions inserted between the machine code steps needed to accomplish this. The compiler is adding additional time for change to happen which expands the time window where the wrong value could be read.SecondIf the A from above needs more than one instruction to read/write the value, the time window expands again.just having to use two machine code instructions for a read modify write adds a time window for a change to happen.Turning off interrupts can cause harm to other interrupt based tasks.So when the change is made in interrupt code, the main code will see the change happen at same time. But if the read of A takes more than one machine code instruction, you still have a problem.A different way to handle this would be to put a serial number on the A value.read the serial number, read A, read the serial number.If first serial number = second serial number then A should be good.The serial number is changed in interrupt code and is one machine code instruction in size for read/write.Now the value of A could be many machine code instructions in size for a read.Here you are dealing with a hardware timer and ram value soloop: serial_number1 = serial_numberread hardware timerread ram valueserial number2 = serial_numberif serial number1 <> serial number2 then loopThink on possibilities,Any time you have interrupt code changing a value that takes more than one instruction to read you have a problem.You have a choice of having a specific fix or a general fix.C

author: nctnico
author_rank: Super Contributor
timestamp: September 11, 2015, 06:31:39 pm
content: This is why you have critical sections and or mutexes. Asynchronous parallel processes are a common occurence for software engineers.

author: westfw
author_rank: Super Contributor
timestamp: September 12, 2015, 07:29:55 am
content: QuoteThe opportunities for that should be somewhat limited given the number of "volatile" values in use. The compiler can't re-order accesses to volatile values, right?I had this exact problem with a SAMD10 implementation of a microsecond counter recently. Obscured by rather lengthy synchronization issues and weird reset behavior, too. (http://www.avrfreaks.net/forum/samd10-rtc)It IS that "re-read the counter" that really seems to get you. I ended up with this code, that seems to work, and includes what I found to be useful debugging hooks:Code:uint32_t micros(){uint32_t thismicros;CpuCriticalVar();CpuEnterCritical();#if DEBUGrtctick_microscalled++;#endifthismicros = RTC->MODE0.COUNT.reg;if (RTC->MODE0.INTFLAG.bit.CMP0) {/** Check for a ms interrupt pending but not yet* serviced, and correct for it.*/thismicros = RTC->MODE0.COUNT.reg; //re-readthismicros += 1000;}thismicros += 1000*millisecondcount;#if DEBUGif (thismicros < rtctick_lastmicros) {// We shouldn't get here. But allow it to BKPTrtctick_backwards++;rtctick_trace.current = thismicros;rtctick_trace.last = rtctick_lastmicros;rtctick_tracei = (rtctick_tracei+1) & 7;}rtctick_lastmicros = thismicros;#endifCpuExitCritical();return thismicros;}

author: mikerj
author_rank: Super Contributor
timestamp: September 12, 2015, 08:07:45 am
content: Quote from: westfw on September 12, 2015, 07:29:55 amYou'd think not, but I had this exact problem in avr-gcc. I had to use a memory barrier to force it to work as intended.

author: donotdespisethesnake
author_rank: Super Contributor
timestamp: September 12, 2015, 08:30:45 am
content: Quote from: westfw on September 12, 2015, 07:29:55 amvolatile used to be a cheap way to get synchronization, but in fact the C spec is hazy on the subject (like many other things). At least volatile accesses can be reordered within a sequence point, and volatile can be be re-ordered with respect to non-volatile accesses. Modern gcc exploits more of this behavior, so traditional assumptions about volatile should not be relied on.The general rule is that "a single-threaded application should see no difference in behavior" which is great for desktop apps, not great for embedded. Memory barriers can help, but you may still need proper synchronization primitives to get correct behavior.I've struggled against gcc, and so apparently have Linux kernel team. In one case, to get the code and timing I wanted I gave up with C and used assembler. Otherwise keeping it in C I have to accept some compromise, and generally avoid "tricky" code which requires a specific sequence to work correctly. Even if code works during test, it can easily break depending on optimization when code is added or changed.It's bad enough when I found bugs in the hardware timers, and then C compiler adds other weirdness on top. "Keep it simple" is a good approach.Some interesting links :http://blog.regehr.org/archives/28https://software.intel.com/en-us/blogs/2007/11/30/volatile-almost-useless-for-multi-threaded-programminghttps://lwn.net/Articles/233479/

author: Jeroen3
author_rank: Super Contributor
timestamp: September 12, 2015, 01:03:51 pm
content: Volatile insists that each operation with the variables uses the variable from memory. Instead of a cached one in the registers.Example:Code:int d; // d is global, otherwise it won't get a memory locationint main(void) {volatile int vd;vd++;vd++;vd++;__DMB();d++;d++;d++;}Will have 3 read-modify-write cycles. As compared to a non volatile one where it will only have 1 read, 3 modifies, and 1 write.That makes optimizing much more complicated. But there might still be things to do. For example, I used a NOP instead of an DMB first. But it was inserted between the last two vd++'s. Not wat I typed in C.Code:34: vd++;0x080002A6 9800 LDR r0,[sp,#0x00]0x080002A8 1C40 ADDS r0,r0,#10x080002AA 9000 STR r0,[sp,#0x00]35: vd++;0x080002AC 9800 LDR r0,[sp,#0x00]0x080002AE 1C40 ADDS r0,r0,#10x080002B0 9000 STR r0,[sp,#0x00]36: vd++;37:0x080002B2 9800 LDR r0,[sp,#0x00]0x080002B4 1C40 ADDS r0,r0,#10x080002B6 9000 STR r0,[sp,#0x00]38: __DMB();39:0x080002B8 F3BF8F5F DMB.W40: d++;41: d++;0x080002BC 4903 LDR r1,[pc,#12] ; @0x080002CC0x080002BE 6808 LDR r0,[r1,#0x00]42: d++;0x080002C0 1CC0 ADDS r0,r0,#30x080002C2 6008 STR r0,[r1,#0x00]43: }0x080002C4 2000 MOVS r0,#0x00(yes, its arm)Registers are volatile defined in the header. Or at least they should be!

author: C
author_rank: Super Contributor
timestamp: September 12, 2015, 04:24:25 pm
content: Thank you Jeroen3 your code shows exactly part of what I was trying to get at.Yourvd++;shows part what I was trying to say. This is not a new problem.Back in the days of S-100 & VME buses where you could have many processors on the buss, it was easy to get caught.You have three instructions for eachvd++So if you had a second processor accessing this same value, you still have a problem.The timing could end up like thisProcessor codeCode:1 34: vd++;2 34: vd++;1 0x080002A6 9800 LDR r0,[sp,#0x00]2 0x080002A6 9800 LDR r0,[sp,#0x00]1 0x080002A8 1C40 ADDS r0,r0,#12 0x080002A8 1C40 ADDS r0,r0,#11 0x080002AA 9000 STR r0,[sp,#0x00]2 0x080002AA 9000 STR r0,[sp,#0x00]Now instead of VD increasing by 2 it increased by 1 and the second was hidden with no errors or notice.With three instructions you do not have a single buss cycle so a second processor could access the value.The 68000 had an instruction "TAS" Test and SET instruction. The memory buss was held so the memory read & memory Write were back to back and indivisible.Above I used a second processor, but DMA could do this also.The hardware could be the second processor.If Processor #2 is interrupt code you have just bunched the three for #2 together where one of the #2's instructions are now.The C language is bad news in a lot of ways, But the C programmer is just as bad.C lost the "Keep it Simple" a very long time ago.C