Found something…
I get tons of informational emails every day, and sometimes it is a chore to slog through all of it. Most of the time I give it a once over then store the email away in the archive and don’t give it another thought. But lately, I did hit on an interesting link in my “Instructables” that relates to this system Getting Started With the ATMega328P If I decided that I wanted to work from scratch instead of using an Arduino, I could go with this set up. The problem of communication port would need to be solved. Luckily, there is a solution USB to serial converters like this one PL2303HX USB to TTL Converter Adapter Module make it easy to plug into the proto board and have the communications work. Or get the Arduino and skip the build process.
A world of exceptions
The system to this point has all been operating in a steady state condition. Steady state is a wonderful imaginary place, where everything operates exactly as defined, and all operating information is correct and updated… Like I said, “imaginary”.
Steady state has a mountain of presumptions, known state information, known data, and known short term variance. The advantage of steady state is to give a base platform for solving system design. Design for steady state, then add controls for exceptions, and the two major exceptions for any system are the start-up and the shut-down, or starting and stopping. Starting, can be two types; initialization, and activation. Initialization is the state where all items are beginning from the nothing state. This can be power up, or factory settings, or component reset. In all cases of initialization, the information is known, but might not be in the proper setting for beginning of system application. So the initialization process configures the system to a known initialized state. This is where the init routines get called for devices, components, and registers. Activation, simply, is the process that moves the system from initialized state to steady state.
For the system here, I provided code for determining speed of rotation, but that is half the information needed. It is also necessary to determine angle of rotation. The known information is the number of teeth (events) in a rotation, and that the events are equally spaced in the rotation. With these two pieces of information, it is possible to know the precise angle based on speed and elapsed time from the previous event, but the known angle is only accurate relative to the previous event. The angle of the event is still unknown. So as a task for activation, the system needs to determine which tooth belongs to the previous event. Strategy is everything here. Which strategy to use brings to mind the old statement; if there are “N” engineers in a room there will always be a minimum of “N + 1” practical solutions to the task. Remembering to discount a significant number that will never work, or work poorly. The system presented here is intentionally simplified. The number of teeth on the crank wheel is going to be four, and there is a single event on the cam wheel that turns at half the speed of the crank wheel. Meaning, for each cam event, there will be eight crank events.
One of the crank events, will have a special designation of the zero point reference, which will be designated as zero degrees, and often called TDC. In an engine application, this is the top dead center (highest point of the ignition cycle) of cylinder #1. There is also a constant for the system which is called "cam to TDC". This is a tooth count from the cam event to the TDC event. Since the system needs a means to synchronize, to determine which tooth belongs to the most recent event, and this can’t be done as part of the initialization process (because the engine might not be turning yet), it will be part of the activation.
The way to perform the synchronization is to identify the cam event, and when it is detected, the previous tooth (last tooth event, first tooth after cam), is defined as, double the tooth count, less the cam to TDC value. So it is necessary for the cam event to be detected, before synchronization can occur. There should be a better way, because, with a single cam event, the worst case is two full rotations of the crank before synchronization, and there is. Like I said, N+1 solutions. But for the simple configuration of this system, the delay is necessary, and in actuality, the average synchronization statistically is in a single rotation, because the cam can be right away, as easily as two turns away, at start.
The system now has a location procedure to determine which tooth an event occurred. In this system there are eight teeth per cam rotation, and the angle is some fraction of the difference between two teeth. If teeth are numbered from zero, then the cam angle can be represented by a decimal number from 0 to 8. The way to determine the exact value, requires a conversion between time and angle, which is done using the value of lastDiff, or the time in system tics between teeth, and lastTime, or the event time of the previous tooth. The equation for this is:
currentAngle=lastTooth+((currentTime−lastTime)lastDiƒ ƒ )
This results in a decimal number between 0 and 8, but is also a floating point number, which I said should be discouraged in a real time system. I also mentioned that computers don’t care about units. People think in terms of degrees, and the angle of cam rotation is often noted as cardinal degrees from 0 to 720 and the conversion for it is:
camAngle=(720×currentAngle)camToothCount
Where cam tooth count is 2x crank tooth count. This again results in a decimal number, good for people, not necessarily “good for computers”. Here is where I determined a good substitution for degrees. The enlightenment came when I thought, radians measure the same angle, but just uses a different angle root, so why not use a root that is appropriate for a computer. By using a 16 bit signed integer there would be 32768 points, which is relatively close to degrees with two decimal places in a fixed point operation. Also, by masking the high order bit, adding and subtracting would result in the correct value, because integer wrap around would put the value in the right location in the circumference (win, win). Additionally, because the cam uses two revolutions or 2x degrees in a circle, an unsigned 16 bit number will result in the correct location in the correct revolution.
Keeping track of where the rotation is becomes very simple. By calculating the value of degPerTooth as:
degPerTooth=32768crankToothCount
Then, once the system is synchronized, a value of lastAngle can be a running number where at each tooth event the degPerTooth value is added. Rollover is not a problem, because it lines up with the cam rotation.
OOPS, stumbled on another exception
What happens if the number of teeth doesn’t divide exactly into 32768 (a power of 2), because the system uses integer math and not floating point. The solution in our system is to use a trick for line drawing in graphics. Where each instance of update introduces a small amount of error. When the error is significant, a correction is performed to move the error to the other side of the correct value. So, if the number is 2/3, an error counter starts at 3 and each iteration, 2 is subtracted. When the value turns zero or negative, 3 is added and the secondary item is incremented, and the process continues.
As an example using the system, the example is a 6 tooth wheel. 6 does not go into 32768 evenly, there is a remainder of 2. Which makes each tooth is at 5461 1/3, from the previous tooth, the increment value is the whole part, the two components are 6 (the number of teeth) for the base and 2 (the remainder) for the decrement, and when the error is detected the angle is incremented by 1 to correct. To step through the operation, error value is 6, first tooth value is 0, and angle is 0. The event occurs, the angle is incremented to 5461, and the error value becomes 4 (positive). The next event occurs, the angle is incremented to 10922, and error value becomes 2 (still positive). Next event, the angle is incremented to 16383 and error becomes 0, so correct, angle becomes 16384 and add back the base to the error which becomes 6. Next event, angle is now 21845 and error is 4. Next event, angle is now 27306 and error is 2. Sixth event, gives angle as 32767 and error becomes 0. Correcting, the angle becomes 32768 and error becomes 6, and the first rotation of the cam is complete
Greatest error of angle is 1/3 of a digit, because the explained method is one sided. Remember 1/3 of a digit is 0.0037degrees in cardinal units, because 360 degrees = 32768. Following the process the first error is 1/3 low, the second is 1/3 high, and at the midpoint, it is back to the same.
What kind of a noise annoys an oyster?
A noisy noise. In embedded systems of whatever type, noise is a fact of life. The A/D code from before provides a noise filter by performing an averaging of the input values, so fluctuations from noise (short duration) are dampened out by time. The problem here is the interrupt mechanism for our cam and crank are also susceptible to noise. What results is an exception case where an unexpected event occurs. A way to detect and ignore a noise event is by tracking the next level of integration (yes, integration). The first value is the event time (lastTime), the first integration is the change in time (lastDiff), so the next integration is the change in change in time, or the acceleration. In a steady state, the value of acceleration is zero. Physical limitations prevent a number too large (positive or negative) from occurring. A noise event will cause a greatly shortened (>50%) change in time. This would result in a generally moderate acceleration value. Because of this, an acceleration filter can be used to eliminate a significant amount the effect of noise on the interrupt line. There is also a “coast” phenomenon from inertia. If one or two tooth events are not managed, it is acceptable to presume things are operating at steady state, and teeth 2 and 3 after the event will follow at their proper time. During that time the lastDiff value can be restored if necessary (only takes two teeth to calculate diff). Truthfully, it is best to just ignore the event, and leave the old values for lastDiff and lastTime. The genuine event will occur at the proper time, and everything will proceed like nothing happened.
Once again back to talking about motors
Earlier I gave an explanation about advance and dwell. Advance and dwell are predictive events, meaning they occur before the referencing event (the top dead center of the particular cylinder). A predictive system is based on scheduling an event for a specific time. The system here, has an event time (lastTime) and a predicted next event time (lastTime+lastDiff). Using the table method from before, the system also is able to provide the base advance angle, and dwell is a time value required to saturate the coil, saved as calibration values.
Each cylinder has a top dead center, which is an angle in the cam rotation. These values will be stored as calibration values as well. Done this way, the cylinder is decoupled from the crank teeth, because the angle is continuously calculated. The result is, the number of teeth can be anything, as long as it follows the rule of; active edge is evenly spaced, so the angle can be determined at each event. The value for advance is subtracted from the cylinder top dead center value to yield the angle when the spark should occur. The dwell needs to be factored in as well, but the dwell is time based, not angle based, so a conversion is needed from time to degrees. This value changes based on the speed of the motor. The system holds lastDiff, which is ticks per tooth. The value of degrees per tooth is also available through calculation (can be done at initialization, degrees per rev / crank tooth count), so a running value of tics per degree or degrees per tic is a calculation away.
How big is your number?
When working in embedded systems, and using integer math instead of floating point, it is always a good idea to keep track of the size of the numbers in equations. Keeping in mind maximum values. Basic rules; if adding two numbers, the result can be 1 bit greater than the container size (overflow), if multiplying two numbers the result can be as much as double the container size (8 bit number * 8 bit number => 16 bit number), etc. Since the system uses 16 bit numbers in general, intermediate results, generally, are stored as long integer (32 bit). It is good practice to keep track of operations as well, particularly balancing multiply and divide operations, like, ((16 bit * 16 bit / 16 bit) * 16 bit) / 16 bit, so the result will always fit in the container.
Back to converting time to angle. In this case degrees per tic or tics per degree can result in a small number. One case if the motor is running fast, the other if the motor is running slowly. Small number results usually means accuracy is lost. To prevent this, if the numbers are maintained and used only when needed in two operations, the resulting value will retain its accuracy. The conversion done in two steps becomes:
dwellAngle=(dwell×degPerTooth)lastDiƒ ƒ
The value can now be used to provide scheduling for the events in terms of angles:
sparkEvent=cylinderTopDeadCenter−advance
startDwell=sparkEvent−dwellAngle
The values are calculated for each cylinder, and scheduled based on current angle of the system.
Time to regroup.
Seems that the system is performing a lot of operations, and it would be easy to think the processor is going to bend under the stress. However, what has happened, is exactly what should happen when developing a system. Lots of work and consideration has been done away from the system, not in the system. Now that the heavy lifting has been done offline, the pieces that are necessary will be added in to the system. First thing is an angle locator (lastAngle) that gets updated at each tooth event. Next a variable for the update angle (degPerTooth) value, and two variables for the error and correction. Then, a flag for cam event detection, a flag for synchronization, and a variable for filtering noise on the interrupt line. Otherwise it is just comparisons and summations for overhead, and the variables are either calibration or are calculated at initialization. Which leaves deciding when to perform the calculations. Embedded thinking says as close to when the values change is best, but it is actually, any time between when the value changes and when it is needed, trying to avoid doing all the work right when the value is needed.
The value of degPerTooth needs to be updated when the tooth event is detected, so that happens in the tooth interrupt. Since it is just a summation, the processing cost is low. The calculation of the angle increment has two parts, an integer divide to get the tooth angle, and an integer mod to get the remainder, for the error correction. Luckily this doesn’t (shouldn’t) change during operation, so it can be performed at initialization. The detect/correct operation is an additional piece to add to the degPerTooth update, and needs to be in the interrupt as well. Again luckily this only requires a summation, and an “if” with a possible second summation, still low cost.
Getting back to the motor part…
There are two variables for each cylinder, one for angle of spark, the other for angle for start of dwell. These values are dependent on air pressure, and engine speed, dwell is additionally the time to charge the coil which is dependent on energy level (oh dear, another exception) for now just have it as a constant. The other two values don’t change rapidly, as described in the acceleration part, so the updates can be at a slower rate as well. It is a loop process, which can be an expensive process, depending on what is inside the loop, and the number of iterations. This time it is pretty lightweight, because the number of cylinders is limited, and the operations are just summation. This can actually be done in the 1mS update, which is an interrupt, but a slow one, just need to guard against starvation.
This leaves us with an update to code…
- The code in the crank interrupt gets an addition.
- The initialization has new variables to calculate.
- A new rtUpdate gets a loop added to calculate spark and dwell.
Updated code
/******************************************************************************** * Copyright(c) 2016 Chaney Firmware * * History: * original creation [JAC] When the earth was cooling ********************************************************************************/ #include "types.h" #include "tables.h" ULong getTmr1(void); SWord getRpmIdx(void); SWord getMapIdx(void); SWord getRpm(void); void setEngSync(bool t); bool isEngCamDetected(void); void setEngCamDetected(bool t); SWord getEngCylCnt(void); SWord getEngBaseAdv(void); SWord getEngDwell(void); SWord getEngCylTdc(UByte n); SWord getEngCamToTdc(void); SWord getEngToothCount(void); #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 EICRA_INIT (1<<ISC00)|(1<<ISC10) // Trigger INT0 and INT1 on either edge #define EIMSK_INIT (1<<INT0)|(1<<INT1) // Set INT0 and INT1 active #define MAX_CYLINDER 16 inline bool isEngCrkLow(void) { return ((PIND & (1<<PD2)) != 0); } inline bool isEngCamLow(void) { return ((PIND & (1<<PD3)) != 0); } bool isEngCrkRising(void) { return ((getTableVal(tEngFlgs,0,0) & bit0) != 0); } bool isEngCamRising(void) { return ((getTableVal(tEngFlgs,0,0) & bit1) != 0); } static UWord angSpark[MAX_CYLINDER]; static UWord angDwell[MAX_CYLINDER]; static UWord crkAngle = 0; static UWord degPerTooth; // integer portion of degrees per tooth static SWord crkError; // integer adjustment error static SWord crkAdjust; // integer adjustment static SWord toothCount; // crank wheel tooth count static SWord camToTdc; // angle of first tooth after cam to TDC static SWord rpm; SWord getRpm(void) { return rpm; } static ULong crkTime = 0; static ULong crkDiff = 0; static ULong preRpm = 0; void rtUpdateCrkCam(void) { SWord i; SWord ct = getEngCylCnt(); // good to have a cylinder count UWord adv = (UWord)getEngBaseAdv(); // from the table UWord dwl = (UWord)getEngDwell(); // constant for now UWord dwlAng; rpm = crkDiff != 0 ? (SWord)(preRpm / crkDiff) : 0; dwlAng = crkDiff != 0 ? (UWord)((ULong)dwl * (ULong)degPerTooth / crkDiff) : 0; for (i = 0; i < ct; i++ ) { if (i < MAX_CYLINDER) { // just in case... angSpark[i] = getEngCylTdc(i) - adv; angDwell[i] = angSpark[i] - dwlAng; } } } void initCrkCam(void) { EICRA = EICRA_INIT; EIMSK = EIMSK_INIT; toothCount = (UWord)getEngToothCount(); degPerTooth = (UWord)32768 / toothCount; crkAdjust = (UWord)32768 % toothCount; crkError = toothCount; camToTdc = (UWord)(((SLong)(-32768) * (SLong)getEngCamToTdc() / (SLong)toothCount) & 65535); preRpm = TICS_PER_MIN / (ULong)toothCount; setEngSync(false); } ISR(INT0_vect) { ULong tmpTime; ULong tmpDiff; SLong crkFilt; tmpTime = getTmr1(); tmpDiff = tmpTime + (crkTime > tmpTime ? 0x10000000 : 0) - crkTime; crkFilt = tmpDiff - crkDiff; // change in, change in time = acceleration if (isEngCrkRising() ^ isEngCrkLow()) { // logic to handle active rising or active falling edge crkAngle += degPerTooth; // tooth event, so increment angle crkError -= crkAdjust; // error adjustment for partial angle error if (crkError <= 0) { // error adjustment is the mod so if the count is divisible this never runs crkAngle++; // error too low so adjust angle by one crkError += toothCount; // correct the error } if (isEngCamDetected()) { crkAngle = camToTdc; // first tooth after cam. setEngCamDetected(false); // this was already available. setEngSync(true); // safe to spark } crkDiff = tmpDiff; crkTime = tmpTime; } } ISR(INT1_vect) { if (isEngCamRising() ^ isEngCamLow()) { setEngCamDetected(true); } } /* end of file */
All code provided comes with a 100% guarantee, I guarantee it will either work, or won't
But seriously, if there are any problems (particularly if they don't cost anything), I will do what I can to help resolve it (them).
Top Comments