More interrupts
The first is the evil tempter. The timer interrupt is the place that can get the system in the most trouble most times without even giving it a second thought. The reason is timer interrupts are the most likely to become interrupt bound and start interrupting themselves. This usually happens for periodic interrupts where the loop is small and the number of tasks is large. It just makes such a convenient place to have a process run, after all, it is a real time system and having a process run on a timer just makes sense. The way to avoid the trap is to work with signal flags and counters. Signal flags are for high speed response, so the main routine knows when a tick occurs, and a counter is used to keep track of longer events like number of mS to wait for something. Timer interrupts are were coders gain the most experience in managing interrupts. Mostly because they are the simplest to implement.
Knowing the pitfalls of timer interrupts provides the programmer with necessary tools for their use. Debugging is often required when working with interrupts, but it is also a luxury that is often not possible. How do you trap a breakpoint and monitor an update for an operation that needs to occur every 3mS. By the time the screen updates, and you are able to restart several seconds have elapsed and you are several hundred events behind. For timer operations, I often use an oscilloscope and set a pin status high or low on the event. If you set the bit high at the start of the interrupt task and turn it back off at the end, it provides a means of determining the time spent in the interrupt by measuring the pulse width.
This is not to say that you should not call any functions from within a timer interrupt, on the contrary, as stated in the temptation section, it is an ideal location for performing time critical tasks. The things to remember are the rules related to interrupt tasks; avoid loops, avoid nesting calls, determine if the work can just be alerted and not performed, and get out quick. An example will be shown in the analog to digital code, where the readings are performed periodically, so they will be current.
The code:
/************************************************************************************************ timers.c History: [JAC] Original Creation when the earth was cooling Copyright(c) 2016 Chaney Firmware ************************************************************************************************/ #include "types.h" #define TICS_PER_mS 2000L #define mS_PER_SEC 1000L #define SEC_PER_MIN 60L #define TICS_PER_MIN (TICS_PER_mS*mS_PER_SEC*SEC_PER_MIN) #define TIMSK0_INIT (1<<TOIE0)|(1<<OCIE0A) #define TIMSK1_INIT (1<<TOIE1) #define TIMSK2_INIT (1<<TOIE2) #define TCCR0A_INIT 0 #define TCCR0B_INIT (3<<CS00) #define TCCR1A_INIT 0 #define TCCR1B_INIT (2<<CS10) #define TCCR1C_INIT 0 #define TCCR2A_INIT 0 #define TCCR2B_INIT (3<<CS00) #define mS_UPDATE 250 void rtUpdateA2D(void); static UWord msTimeOut; void setMsTimeOut(UWord n) { msTimeOut = n; } UWord getMsTimeOut(void) { return msTimeOut; } static UWord ov0Tic; static UByte ov1Tic; static UByte ov2Tic; ULong getTmr0(void) { ULong rVal; cli(); rVal = (ULong)TCNT0 + ((ULong)ov0Tic << 8); sei(); return (rVal & 0x0fffffff); } ULong getTmr1(void) { ULong rVal; cli(); rVal = (ULong)TCNT1 + ((ULong)ov1Tic << 16); sei(); return (rVal & 0x0fffffff); } ULong getTmr2(void) { ULong rVal; cli(); rVal = (ULong)TCNT2 + ((ULong)ov2Tic << 8); sei(); return (rVal & 0x0fffffff); } void initTimers(void) { TIMSK0 = TIMSK0_INIT; TIMSK1 = TIMSK1_INIT; TIMSK2 = TIMSK2_INIT; TCCR0A = TCCR0A_INIT; TCCR0B = TCCR0B_INIT; TCCR1A = TCCR1A_INIT; TCCR1B = TCCR1B_INIT; TCCR1C = TCCR1C_INIT; TCCR2A = TCCR2A_INIT; TCCR2B = TCCR2B_INIT; OCR0A = mS_UPDATE; } ISR(TIMER0_COMPA_vect) { OCR0A += mS_UPDATE; /* refresh for 1mS Timer Tic */ if (msTimeOut > 0) { --msTimeOut; } rtUpdateA2D(); } ISR(TIMER0_COMPB_vect) { } ISR(TIMER1_CAPT_vect) { } ISR(TIMER1_COMPA_vect) { } ISR(TIMER1_COMPB_vect) { } ISR(TIMER2_COMPA_vect) { } ISR(TIMER2_COMPB_vect) { } ISR(TIMER0_OVF_vect) { ov0Tic++; } ISR(TIMER1_OVF_vect) { ov1Tic++; } ISR(TIMER2_OVF_vect) { ov2Tic++; } /* end of file */
The code that is implemented provides overflow counters so the timers are extended and the timer zero is configured to provide a periodic 1mS interrupt with a call to start the A/D conversions, which takes us to the next module of code for analog to digital conversion.
Analog to DIgital
Analog to digital conversion (A/D) is performed as a triggered event that needs to settle before the reading is taken. The hardware provides a mechanism for reading the status of the conversion, and a conversion complete flag is available in a register as part of the hardware. Many times there is also an external signal that can be tied to an interrupt so the completion of conversion can be managed on an interrupt basis. The A/D section of the 328P has eight lines that are multiplexed into a single A/D converter, and each conversion is done separately as a three stage process; stage one sets the multiplexer to the proper channel using a value in a register, stage two triggers the A/D to perform start the conversion, and stage three when the conversion is complete the value in the A/D register is read as a count. The particular register is a 10 bit converted value between 0 and 1023 representing the voltage on the input pin between 0 and whatever the reference voltage is.
The next step of the process is where I diverge from most of my Electrical Engineer friends. My view is the computer’s view, which is “don’t understand voltage”. A sensor is connected to the input line the sensor senses an environmental condition; like temperature, or pressure, and reacts to the condition. This reaction is measured on the input pin which is converted to a count value. The common method is to convert the count to a voltage value based on the reference voltage, then convert that voltage to its temperature, or pressure, or whatever value. I diverge from this method because the conversion to voltage is only important to people, the computer doesn’t care about the voltage, because it converts an environmental condition to a bit count. Since that is the case, a single conversion to the proper value is more economical than a conversion to voltage then a conversion to value. There is also a benefit of improved accuracy since error can be introduced at each stage of the operation. Conversions of this type will be discussed at the proper time when the proper tools are introduced. For now the conversion is only going to be from reading the input and obtaining the converted value.
Noise!!!
Any time there is an electrical signal on a wire, there is going to be noise. Any amount of insulation and shielding only reduce the affect, and it is never entirely eliminated (and many companies and experts will be happy to sell you the newest technology at a highly inflated price, to prove me wrong). Because some noise is always going to be induced on the line, a means of filtering out the noise is needed in order to provide a steady reading. How much filtering is needed depends on how noisy the environment is, and how fast you are taking readings. The method used in the example for filtering is done as a running average. The average is for a fixed number of readings, and the result is the rounded, divided sum of N readings. As an equation:
average=(sum+(N2 ))N
"sum" is updated on each reading by subtracting 1/N of the SUM then adding the new reading. This works for any value of N, but because this is a computer, and binary shifting is simple and fast, making N a power of 2 is an advantage. Also remember the size of the container and the size of the reading affect the selection. As an example, a 12 bit A/D using a 16 bit storage should not use 32 for N because 32 is a shift of 5 and 5 + 12 is would need 17 bits.
So, back to the process, now that all the components are in place. There are three stages to the process, and an output from the result. In the example device, the A/D channel is 10 bits, and there are eight channels. It is possible that not all eight channels are being used, so a means of labeling the active lines is needed. The initiate sequence will set the first multiplex channel, trigger the start of conversion, and turn on the interrupt. The interrupt process reads the A/D register and adds it to the SUM (as an array one for each channel). If the last conversion is completed, the interrupt is turned off, otherwise the multiplex is set to the next channel, and trigger the start of conversion.
Here is the code…
/************************************************************************************************ a2d.c History: [JAC] Original Creation when the earth was cooling Copyright(c) 2016 Chaney Firmware ************************************************************************************************/ #include "types.h" /************************************************************************************************ A2D all operate the same each channel is sampled in turn, and each sampling cluster is triggered in the mainline process. The A2D values are kept as a running average of 8, each update 1/8 of of the current value is removed then the new reading is added. ************************************************************************************************/ #define A2D_CHAN 8 typedef void (*callBack)(void); /* Right aligned Free running /64 prescaler */ #define ADCSRA_INIT (7<<ADPS0) #define ADMUX_INIT 7 #define AD_FILTER 8 #define AD_ROUNDER (AD_FILTER/2) static const UByte msk[] = { 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 }; static UByte actFlag = 0xff; static UByte on_Flag = 0xff; static UByte a2dChan = 0; static UByte a2dSemi4 = 0; static SWord a2dVal[A2D_CHAN]; bool isA2dAct(UByte n) { return (n < A2D_CHAN ? ((actFlag & msk[n]) == 0) : false); } // negative logic for blank EEPROM bool isA2dOn(UByte n) { return (n < A2D_CHAN ? ((on_Flag & msk[n]) == 0) : false); } // negative logic for blank EEPROM SWord getA2D(UByte n) { return (isA2dAct(n) ? (a2dVal[n] + AD_ROUNDER) / AD_FILTER : 0); } //SWord getSns(UByte n) { return (isA2dAct(n) ? isA2dOn(n) ? getTableVal(tA2dConv,getA2D(n),n) : getTableVal(tA2dDflt,0,n) : -1)); } void initA2D(void) { a2dSemi4 = 0; // pseudo semiphore, to only run the conversions when ready ADCSRA = ADCSRA_INIT; } void refreshA2D(void) { if (a2dSemi4 == 0) { // check the update flag a2dSemi4 = 1; a2dChan = 0; // clear to refresh? while ((a2dChan < A2D_CHAN) && !isA2dAct(a2dChan)) { a2dChan++; } if (a2dChan < A2D_CHAN) { // should not be necessary (carry over from old process) ADMUX = a2dChan; // set A/D channel for read ADCSRA |= (1<<ADEN); // toggle the start ADCSRA |= (1<<ADSC); // start conversion ADCSRA |= (1<<ADIE); // turn on the interrupt } else { a2dSemi4 = 0; // (carry over from before) } } } void rtUpdateA2D(void) { actFlag = 0xff; on_Flag = 0xff; if (a2dSemi4 == 0) { refreshA2D(); } } ISR(ADC_vect) { SWord tmp; ADCSRA &= ~(1<<ADEN); // disable A/D while changing MUX tmp = ADC; // and get the current value a2dVal[a2dChan] += tmp - (a2dVal[a2dChan] / AD_FILTER); a2dChan++; while ((a2dChan < A2D_CHAN) && !isA2dAct(a2dChan)) { a2dChan++; } if (a2dChan < A2D_CHAN) { // only the right number of channels ADMUX = a2dChan; // set A/D channel for read ADCSRA |= (1<<ADEN); // toggle the start ADCSRA |= (1<<ADSC); // start the next conversion } else { a2dSemi4 = 0; // all done so clear the flag ADCSRA &= ~(1<<ADIE); // last one so kill the interrupt } } /* end of file */
That is all the devices in our system, at least for this processor. Next time a few more tools, for math and calibrations.