Into the great unknown... I have never made a "blog" before, so I hope everything works out well, and I also hope people are patient with my aged and slothful ways. I plan to present the development of an engine controller using low cost and readily available components. Also, as a side effect of many diversions, a generalized controller can be extracted for use in other projects that require speed and simple interface, using the tools developed here. So on to the first four parts...
Part 1:
Best place to start is at the beginning. The problem is, which beginning. There is a beginning to the platform, and a beginning to the process of engine management. I guess the platform is something that can come later, so, I will begin with the process of engine management.
Engines (internal combustion type), have some basic principles. They need air, fuel, and spark to make them work. Engine diagnostics tell us, if you take any one of the three away, the motor doesn't run. We will start with the spark generation, and how to get it "just right". Now, to make my train of thought a little more difficult to follow, a little bit about problem solving. Dijkstra tells us, if a problem seems too big, solve the parts that you can solve, then work on filling in the parts that are left. For a process problem, quite often, the "steady state" is the main part of the process, and the management of the exceptions is where things get troubled. So for our engine management problem, we will start from the steady state condition (no changes due to acceleration, positive or negative).
Kind of a long winded introduction to get us to a point of where I want to start the discussion, which is about timing. I am going to work with four cycle motors in this series, so people interested in two cycle, or diesel, will have to wait until another series. Four cycle, means the crankshaft turns twice causing the piston to rise and fall twice, per each sequence (called a cam cycle). There is a compression sequence, and an exhaust sequence. The compression sequence is where the spark needs to happen. But when in that cycle should we make the spark happen? Still more background. For the energy event during the compression, we have an air:fuel mix that is put under pressure, and ignited by a spark. How to generate the spark in just a moment, first when exactly do we need to make the spark happen. The way the ignition occurs is interesting because it is not immediate. If you were to take a very high speed film of the ignition event, you would see that when the spark ignites the air:fuel mix, the burn extends from the ignition point in a plume. Because of the properties of the fuel, this rate is fairly constant. Keeping in mind different fuel types have a different burn rate (alcohol, very fast, diesel, very slow). The ideal timing for ignition, is so the plume reaches the top of top of the piston, just as it reaches its state of top dead center (TDC) (highest point in the cycle). To make this happen, the ignition needs to happen a little before TDC, this is referred to as the advance. The advance changes as the motor changes speed, the faster the motor is turning the earlier the advance needs to be. Luckily for us, we are dealing with the steady state first, so adjustments to advance will come later.
Our next piece of the timing puzzle is about generating the spark, this is done in many different ways, but we will be starting with the conventional method first, the ignition coil. Coils work on an interesting electrical principle. By applying a direct current to the coil, for a brief period, it behaves like a resistor until it reaches its saturation point (can't absorb more current), then it behaves like a shorted wire (no resistance). When the current is removed from the coil quickly, an interesting phenomenon occurs, called field collapse. This drives a voltage at a high current spike through the circuit, which generates the spark at the plug. I over simplified because I don't want to get too hung up in coil design and efficiencies, again this leans toward exceptions. What I did want to cover was there was a period of time before the ignition, when we need to charge the coil. This time is called the dwell, and dwell is dependent on the type of coil, but some general cases can be used for now and we will just call it our dwell.
Now we have what we need for our basic system, at least for one cylinder. Oh, first diversion from general case engine model, the cylinder object. If we write all the code for a single cylinder, then make N number of instances, we can have any number of cylinders we want in our motor. Information we have about our cylinder is the angle of its TDC, the dwell time for the coil it is attached to (multiple coils are an exception to be discussed). So we need to determine some elements; current angle in cam cycle, and engine speed, which will help determine the value of the advance. Sounds like we need some inputs, most engines with electronic ignition systems have a couple of signals that are helpful, called cam and crank. These signals are generated from the camshaft, and crankshaft, and have one, or a series of triggering elements mounted to produce a signal to identify position. Here is where I am going to diverge again. The thing we are interested in, is rotational speed, and current position. The cam and crank are a closed system, so the process is continually repeating, and once we have identified were we are in a cam cycle, as long as the speed remains consistent, we should know where we are at any time, and additional inputs, provide additional information.
Let’s look at a common type of cam and crank configuration 4x crank and single pulse cam. The good thing for this part is the engine manufacturers go through a very large effort to make sure the active edge of the crank and cam signals are extremely precise. As a result each active edge of the crank is exactly 90 degrees from the previous signal. Also, the cam signal only occurs one time in a cam cycle. This gives us our prime reference. How this works is, a value of TDC0 is set, based on the operation of the motor, which is the 0 degree reference point and synchronized with one of the crank signals (called teeth), and the cam signal is given as number of crank signals from cam to TDC.
So we have input signals, and can begin doing some determination. Starting our timer when we get a crank event, and stopping the timer when the next event occurs, we now know how fast the motor is running, because the time dT, to move 90 degrees is 1/4 the time for a full rotation, and if we convert the units of dT to one minute, then multiply by 4 to provide RPM. If we know which crank signal just passed, we also know our position, this is possible if we have had a cam signal, in which case we count the number of teeth for cam to TDC. Some clever calculation will let us know where we are, as soon as the cam signal is detected, because cam to TDC means the crank signal that just passed when the cam signal is detected is 2x number of teeth - cam to TDC number. Now that we have our information, we can get to work, since it is ridiculous to think that a motor doesn't change velocity we will need to provide continuous update to the dT value, so the stopwatch idea is not practical. Instead, we will use a free running timer, and get a trigger time when the crank event is recorded. Then, take previous recorded timer value, and current recorded timer value, and the difference is our dT that gives us our rotational speed. Hopefully, you can begin to see now, that the number of events in a crank, provided they are equally spaced, is only significant to determine accuracy of rotational speed, and more quickly determine change.
The result also provides a means of predicting the precise moment when the next event will occur, as well as other, future events. This is important, because from our earlier discussion, the spark event needs to occur before the TDC event (which is usually synchronized with a crank event), and the start of dwell needs to occur before the spark event. Also, remembering, starting the dwell too soon, can cause potential damage due to heat buildup from a shorted system, so the dwell cannot be started too soon. I will finish off this first entry by establishing some formulas to use...
t0 is the previous recorded event time (time of the previous crank event), in an arbitrary unit of "ticks"
t1 is the current recorded event time (time of this crank event), again in the arbitrary unit of "ticks"
dT is the delta time providing rotational speed (t0 - t1) in ticks
teeth is the number of crank events in a single rotation
TicsPerMin is the number of tick units in a single minute
Part 2:
Last time, after a long discussion, we figured out how to calculate RPM. This time, hopefully, without a lot of discussion, we will get some detail about our ignition events. I also hinted before, that this will be done on a per-cylinder basis, and engines with multiple cylinders, would have multiple instances of the same operation.
I teach through metaphor as a means to explain a complicated idea using familiar imagery. The ignition management process is kind of like a carousel ride, with the riders trying to grab the gold ring. For a majority of the ride, the rider watches the target, and makes the plan. Then a short period of time before, adjust seating, and extend an arm. Then at the exact moment, clutch the ring and win the prize. Looking at the problem from the viewpoint of a single cylinder, the process is similar for ignition. The majority of the time is used for configuration and determining when we need to perform the spark. Then, a short time before, we start dwell, then at just the right moment, the spark is ignited.
With this in mind we begin with the first group of equations. Each cylinder has, what is called a top dead center (TDC) point in its cycle. This is, independent of whether it is a two or four cycle operation, the position in the cycle of full compression, before the power stroke of the cycle following ignition. Moving earlier in the cycle is referred to as advance, while moving later in the cycle is referred to as retard. Advance and retard are angle based measurements and managed in degrees. Several factors go into the calculation, but for now the result is all that is important so the resultant total advance will be used for a working operator. The expression to add timing, means adding more angle to total advance. Conversely, taking away timing, or retarding, means subtracting angle from total advance.
Two factors go into the calculation of the total advance, first is rotational speed of the engine (measured in rpm), the second is the burn rate of the fuel (nothing happens instantly). Many factors affect both of these values, and need to be monitored over the course of operation. The desired effect is to have the maximization of the expansion of ignited gasses occur at the instant past TDC (for measurement this can be TDC) to produce the maximum amount of energy. This yields our first calculation.
sparkEventcyl = TDCcyl – totalAdvance;
Which seems pretty simple, but TDC is a point in the cycle for each cylinder and there is one TDC for each, so this calculation needs to be repeated for each cylinder. This brings the discussion to “timing” which is the mechanical derivative of the angle to time conversion. Even though there is a TDC for each cylinder, in discussion when the term TDC is used it references TDC of cylinder 1 or the first cylinder in the firing sequence. (Again mechanical versus mathematical definitions so cylinder numbers are one based and not zero based)
The spark event now depends on the spark generation, a dwell must be started some amount of time prior to the spark event. Again, several factors go into the calculation but for simplification, the resultant total dwell will be used for now. Total dwell is measured in time, which cannot be directly combined with the spark event and a conversion needs to occur. The two calculations for converting between time and angle are:
eventAngle = DEGS_PER_REV * eventTime / ROTATIONAL_SPEED;
or
eventTime = ROTATIONAL_SPEED * eventAngle / DEGS_PER_REV;
For discussion the calculations will be maintained in angle here, and the conversion will be from total dwell to dwell angle. This yields the second equation for start of dwell.
dwellStartcyl = sparkEventcyl – dwellAngle;
Again this equation is simple, but needs to be repeated for each cylinder. After the preceding operation we have obtained two events for each cylinder, the start of dwell, and the spark event. Both events need to be scheduled to occur based on the operation of the engine, and require a scheduling mechanism to link the event to the action.
This is a good point to start collecting constants and variable information. From the previous discussion there was a t0 and t1 used for the event time of the previous tooth event and the current tooth event. With that was dT for the delta between the two events, these items are candidates for variable data, where t1 becomes the new t0 at the next event. To keep track of information, the system will use a section called calibration values. Calibration values, are values that can be different for a specific motor's operation, but are not likely to change substantially during operation. Candidates for calibration values are; teeth per revolution, cam to TDC1, cylinder count, and cylinder TDC (for each). Last set of items are constants. These are physical constants that do no change like mass of dry air at sea level (handy for fueling), or number of degrees in a rotation, etc.
The basic discussion is complete, and the process is now ready to start coming together. The determination of angular velocity has already been determined, but it is necessary to locate the angular position in the rotation as well. The value of cam to TDC is used to locate the correct angle. From the previous discussion, it was determined, the cam event provides the angle of the crank event that preceded using the formula d0 = 2 * {degrees_per_rotation} - ((cam to TDC + 1) * degrees_per_tooth). For the question that is raised, "why use a term degrees per rotation in place of some accepted value?" Please accept that numbering systems are being kept as flexible as possible early on in the process and units might need to be adjusted.
Finally for this section, as soon as the value of current angle (meaning the most recent crank event) it can be determined the time until start of dwell and ignition event (spark). This is done using the formulas
rotation_distance = sparkEventcyl - d0 {where d0 is the angle of the most recent event}
if rotation_distance is a negative number, then add (2 * degrees_per_rotation). Then converting from angle to time
time_to_event = ROTATIONAL_SPEED * rotation_distance / degrees_per_rotation
Similar equations are performed to determine start of dwell.
Promised information; a discussion about degrees in a circle. In mathematics, the units for angles is calculated in degrees, radians, and grads. Degrees are used because they date back centuries. Radians are used, mostly in mathematics because they represent a pi based ratio. Grads are used because they provide a decimal percentage of distance around the circle. Because this is a computer system, and integer operations are more portable to other processors, the system here will use a 16 bit value for angle where 360 degrees is equal to 32768. This was chosen, because two rotations are needed per cam cycle, and it also provides a signed angle capability. I leave it to you, to perform a number of operations to prove out the logic. For discussion of theory, the named constant will be used. Once the discussion changes to applied operation, the actual value will be used.
Part 3:
Time for time.
I discussed cam and crank, and how they give the system location and speed information. I discussed advance and dwell, and why they are necessary for making the engine run properly. Now the next step in the process is to provide the time based operation in a scalable manner, so a simple processor will be able to manage without being overburdened. The work to this point has been focused on a single cylinder, but most engines (at least for motor vehicles) contain more than one cylinder. Also our system only understands steady state, (no variation), so the timing remains static. There has also been an absence of feedback for the system. No user interface, no monitoring, and no inputs. The only input the system has are the cam and crank signals.
Inputs for engine management are usually from sensors. The more expensive the system, the more sensors are available. To start off slowly, the two primary sensors that get used the most, are manifold air pressure (MAP), and throttle position sensor (TPS). It can be argued that if you have one, you can do without the other. This argument will become apparent as we move on. The next sensors that are of value are engine coolant temperature (ECT), and intake air temperature (IAT). These provide a means of adjusting operations based on current temperature and cooling actions. The next level of sensors are used to provide added information for operation in less than standard conditions; the barometric sensor (BARO), and battery voltage level (Vbatt).
Getting the information from the sensors is done through the use of an A/D converter. Many of the new processors have them built in, so they need to be serviced. How an A/D works is a voltage is applied to the input line, and a trigger is activated that charges a capacitor. Then the voltage is removed and a very high speed counter runs to see how long it takes for the voltage level to drop below a particular threshold. Over a particular range, this behaves in a linear manner, and so, an accurate reading of the voltage is possible by converting the number of counts. The more counter bits, the more accurate, and the more expensive. Also the more time it takes for the value to be obtained. The A/D circuit is usually switched through a multiplexor, where a single A/D can manage (or take the values of) many input signals. This is done by setting the MUX value to the correct channel, then performing the A/D process.
A/D converters often have signaling to let the process know the conversion is complete and the value is ready to use. This signal is often a status bit in a register, but also often is provided as an interrupt signal. Both mechanisms have their advantages, and disadvantages. Whatever the disadvantages, I prefer using the interrupt method, and will cover how to do that in a while.
The other component, also often included in the processor, are timer / counters. Most timer components offer modes of operation; continuous, countdown, compare, and capture. The other activity provide by timer sections is the ability to generate interrupts, either periodically, or at a scheduled time. The other resource a timer provides is a link to real time. A computer process does not particularly care how long an operation will take, so it takes as long as it needs. The real world is based on events which happen at predictable times. As a result, a timer can provide a stimulus to the processor to make sure it is performing the right task at the right time. This is done through the use of interrupt messaging, where the interrupt process sets a flag, and the mainline routine monitors the flag for when it is activated.
I have already discussed how cam and crank signals operate through the use of an interrupt and we use the timer to record the moment of the interrupt. The importance of interrupts is evident at this point, so familiarity with their operation is extremely important in order to provide proper management of resources.
Time to start putting together some real applications, by putting some values to the systems discussed. Starting with a static, steady state, operating motor, running at 750RPM (a generally good idle speed), and with a 4 cylinder configuration, a 2x (two crank events per rotation) timing wheel, and a single pulse cam sensor set to just before TDC1 (cam to TDC = 0). The timer is set to free running at a rate of 1MHz (or 1uS per tic). If everything is operating properly, the system will be triggering a crank interrupt every 0.04 seconds. Or 40000 tics of our counter. Next step is deciding what needs to happen when. The interrupt from the crank will trigger a process that captures the value of the free running timer and stores it in a variable curTime (for current time). Next calculate the value of curDiff (for current difference), for this the value of the preTime (for previous time) is subtracted from curTime. Before exiting the interrupt routine, the value of preTime is replaced by curTime. As I stated before, at first the system will deal with ideal situations, then address exceptions. Here is the first exception, what happens when the counter rolls over a maximum value, and the preTime is greater than the curTIme value. The simple way is to check before the operation and add in the maxTime (for maximum time value). If curTime < preTime, then curDiff = (maxTime + curTime) – preTime. Or a little cleaner curDiff = maxTime – (preTime – curTime). For this system, I am also keeping to integer operations to prevent introducing conversion error, and because integer operations are run in registers, attempts will be made to prevent overflow conditions.
This takes the discussion to a delicate area of how accurate and how big are the numbers that are being used. In the example, 40000 was needed to hold the value of curDiff, when the motor is running at 750RPM (idle speed). Working out some constants real quickly and adding our tooth count as TEETH;
TICS_PER_mS = 1000
mS_PER_SEC = 1000
SEC_PER_MIN = 60
TICS_PER_MIN = (SEC_PER_MIN*mS_PER_SEC*TICS_PER_MIN)
RPM = TICS_PER_MIN / (curDiff * TEETH)
By doing the inverse of the formula, we can see as RPM decreases, the value of curDiff increases, so limits are now able to be established. If the free running timer is a 16 bit value, the lower RPM limit is reached when curDiff = 65535. For the 2x crank, that becomes, 60000000 / (65535 * 2), or slightly less than 458 RPM. The problem with this value is, capture of run to start usually happens at a slower rate (200-300 RPM). Using a larger integer provides two improvements, first, the 60000000 value doesn’t need special handling, and second with 32 bits the system can read extremely low crank speed. The system now knows how fast, and only needed a simple subtraction in the interrupt. But it is also necessary to know position.
Each crank event provides positional information, however, the position is still obscure without the proper reference. Remembering our system operates in steady state, the position is known, because it is one tooth further in the revolution from the previous event. When does the resolution of which tooth we are on get determined? This operation happens continuously, on every occurrence of a cam event, and the current tooth is always measured as number of teeth after cam event with the first tooth after cam equaling zero. The resulting positional information is now 0 degrees is at the tooth where CAM_TO_TDC – TOOTH_AFTER_CAM is equal to 0. This provides our additional information for our crank interrupt. On each crank event the value of toothAfterCam is incremented. If the cam event is detected, the value of toothAfterCam is set to 0. Complex cam event detection is reserved for a future installment for now accept the system provides a single pulse for cam, and the interrupt routine only sets a simple event flag. The flag is cleared in the crank operation when the counter is set to zero.
Now on to determining when we need our events to occur. For each cylinder, there are two significant events, first is start of dwell, the second is the ignition of the spark. The calculations are the same for each instance, spEv[n] (for spark event time) is at its TDC[n] angle, less advTotal (for the total calculated advance), and dwEv[n] (for start of dwell) is the angle spEv[n], less dwlAngle (for the total dwell angle).
The events are scheduled based on current value of tooth by calculating the angle at the tooth events. If the event needs to happen between teeth 2 and 3, then the event is added to the queue when tooth 1 is detected. Likewise, if an event is supposed to happen between teeth 30 and 31, then the event is added to the queue when tooth 29 is detected. The other element to keep in mind, from a resource standpoint, there will not be a situation where more than one spark will be occurring at the same time, and by extension, if all the dwells are equal, no two dwells will start at the same time. The result is a simple scheduler using only two timer compares, one for dwell the other for spark.
Lots covered, more to come. Should start on exceptions next time.
Part 4:
Talked a lot about how motors worked, and gave some of the mechanisms for making a motor work, but it still seems like the motor isn’t going to actually run. There seems to be quite a bit missing still. The truth in fact is very much of what is needed to have the motor run has been presented. What is missing are the exception conditions. Startup and shutoff, and acceleration (both up and down, second being deceleration).
What is missing are the exception conditions. Startup and shutoff, and acceleration (both up and down, second being deceleration). Exceptions are detected through the use of measurements. Measurements of pressure, and temperature provide the best operational information. Collection of sensor information is a discipline that is portable to mostly all embedded operations, and techniques are many and varied. The method presented here is done for speed with a good level of accuracy, which are two of the requirements for engine management. First item discussed was method of detecting the sensor information which is done with an Analog to Digital (A/D) converter. The A/D converts an analog voltage to a digital count proportional to the voltage level. The value of counts is easily converted:
CountA2Dmax =VVreƒ
Where MAX_VAL is the max count of the A/D. (i.e. 10 bit = 1024, 12 bit = 4096), and REF_VOLT is the reference voltage provided in the circuit. Depending on who is makes the circuit, and what was available, the value can be 1v, 3.3v, 5v, or 12v. Need to check the circuit for this information. Sensors then provide a value of conversion, which is done as either a series of steps in a table, where a particular voltage represents a particular reading, or a conversion formula (usually linear y=mx+b). If the count represents a voltage, and the voltage represents a sensor value, why can’t the count represent a sensor value, without an intermediate conversion? Sounds like a great way to reduce the work on the processor and speed up operation. True if the conversion does not prove to be too complex, meaning, the single conversion is significantly more involved, than the two conversions.
The method presented here eliminates any complex runtime calculations, because they are done before operation. It also takes advantage of a feature used in integration. Calculus goes through explanation of how to obtain accuracy through successive approximation, and use of limits as values approach either zero or infinity. The premise of the operation used here is, a curve of undetermined value has points defined as y=f(x), where for each x there is a single value f(x) that lies on the line. The method used in calculus is the change in x relates to a change in y and as the value of delta X (difference in x) decreases, the approximation of the area under the curve is made more accurate. Also the approximation can be taken as a height of X0 or X1 or as the area of the trapezoid using the line between (X0,Y0) and (X1,Y1). First giving a low estimate, second giving a high estimate, then the last providing a generally accurate reading. The other feature is by reducing delta X the accuracy increases, so the more points used, the more accurate the result.
For speed of operation, it is often better to maintain calculations in integer form as well, but some values need to have fractional parts to preserve accuracy, so an agreed fixed point is used in those cases. The system described here uses 16 bit integer base for all numbers and calculations, which provides a satisfactory level of accuracy, as well as maintaining an integer operation for equations the system used is:
Angles are in binary radians where 360 degrees = 32768
Ratios are all *1024 100% = 1024
Temperature is maintained in degrees K * 100 unsigned (27315 = 0 degrees C)
Pressure is maintained in absolute kPa (1000 N/m^2) * 100
Voltage is maintained in mV (V * 1000)
Limits of accuracy for equations are imposed based on 16 bit values
angles (error +/- 0.0109 degrees)
ratios < 6399.9% for unsigned or 3199.9% for signed (error +/- 0.098%)
temperature < 655.35 degrees K or 382.20 degrees C (error +/- 0.01 degree C)
pressure < 655.35 kPa or 6.47 atm (error +/- 10Pa)
voltage +/- 32.767v (usually 5v reference)
The conversion for sensors is now managed as a single array with values equally spaced for values of A/D counts. Here is an example for a sensor converted from a 10 bit A/D using 8 cells for the table, and a non-linear conversion.
X | 0 | 128 | 256 | 384 | 512 | 640 | 768 | 896 |
Y | 2.1 | 2.2 | 2.8 | 3.6 | 4.5 | 5.1 | 5.7 | 6.1 |
For each value of the A/D a corresponding value exists in the table with the exception of the high end. A linear extension is performed to create a “phantom” cell for the last position. The table then is interpolated between any two cells to obtain the resulting value. So an A/D reading of 400 means a value between 384 and 512 taken as a percentage. The equation becomes a simple ratio interpolation:
(Yb−Ya)(Xb−Xa) =(Yb−Val)(Xb−Count) => Val=Yb−(Xb−Count)(Yb−Ya)(Xb−Xa) => Val=4.5−(112·(4.5−3.6))128
Giving a final answer 3.71 after substitution. If more accuracy is needed (closer following the curve) more points can be added. Remembering, you cannot exceed the number of points equivalent to the A/D count. Which brings up the issue of significant digits. The temptations of floating point operations is the lure of perceived precision. Precision is limited by the measuring instrument, so it is necessary to maintain that item in the background. As such, all measurements need to have the error included as part of the answer. Many times, by including the error, the number of significant digits becomes very evident. Providing a solution 3.9999999 +/- 0.001 should be evident the last four or five digits are insignificant and should be ignored. Too many times I have seen systems where the conversion from a 5v 10 bit A/D is maintained in a double precision floating point, then wondering why equates don’t seem to work because the compare is outside the significance.
I need to provide one more formulaic method, to perform the same functionality with two variables. Often with engine management, two variables act on the same value, the most common case being timing advance, which is affected by engine speed, and manifold pressure. How much air is getting in, and how fast it is moving. For this, using similar logic of incremental elements, there is a formula for interpolation in two dimensions (planar). Using an array, of values with x and y components, for any value dx and dy, the result will lie at a point bounded by the four corners. This is an extension of the two points in the linear form from before. The four corners have coordinates (Xa, Ya), (Xb, Ya), (Xa, Yb), and (Xb, Yb). The formula for determining the interpolated value is:
[Z0(Xb−dX)(Yb−dY)+Z1(dX−Xa)(Yb−dY)+Z2(Xb−dX)(dY−Ya)+Z3(dX−Xa)(dY−Ya)](Xb−Xa)(Yb−Ya)
Where the Z values are the cell values in each cell, and the values dx and dy are the values like “Count” in the previous linear example. The equation is available in most math table books. This table form will be used extensively, so it is good to get comfortable with the method of the equation, and brings about our first table. As stated before, two inputs for engine speed (RPM) and Manifold Air Pressure (MAP) are used as the axis lines RPM => X and MAP => Y the cells will hold a value named base advance and is retrieved whenever it is needed for calculating spark advance providing a base value for the equation.
The other item to address in this installment is getting the motor started. The steady state provides for when the motor is already running, but does not take care of getting the motor to that state. Again an exception, in this case, the crank, capture, accelerate to run, sequence. Looking at the system, there are the two event captures for cam and crank. Starting from an unknown position, it is not wise to schedule a spark as damage can occur, so determining position is the critical operation. In the simple system being used, there is a single cam pulse, and a single pulse at each cylinder TDC. The system has a retained value for cam to TDC. Engines have a device, called a starter. The starter cranks the engine at a generally constant rate until the motor is able to run on its own. Calculations from before provide a means for determining how fast the engine is turning during crank. In order to command a spark (at the proper time), identification of cam angle is required. This requires waiting until the cam signal is detected. Once the cam signal is detected, the very next cylinder spark event can be scheduled using the cam to TDC equation provided earlier. The values for the scheduling are provided based on current engine speed, and angle. Pseudo code for the operations are as follows:
// Global variables
camToTdc = 0;
toothAfterTdc = 0;
cylTdc[] = {0, 16384, 32768, 49152};
lastTime = 0;
lastDiff = 0;
camDetected = false;
sparkOk = false;
// Event managers with local variables
CrankEvent() {
currentTime = getEventTime(); // grab the current event time from the timer (measured in tics)
currentDiff = currentTime – lastTime; // calculate the delta to get speed
lastTime = currentTime; // current becomes previous, for next event
lastDiff = currentDiff; // record how fast we’re going
toothAfterCam++; // increment the tooth counter
if (camDetected == true) { // did we have a cam event???
toothAfterCam = camToTdc; // now we know for sure where we are, so…
sparkOk = true; // we can activate our spark
camDetected = false; // clear the flag, so we can track it again
}
}
CamEvent() {
camDetected = true; // we have a cam signal, so set the flag.
}
The toothAfterCam value holds the most recent tooth, and the value lastTime holds the timer value of when that tooth was detected. So taking lastTime + lastDiff will give a pretty good approximation of the time when the next event will occur. Using 2x lastDiff will give the tooth following, 3x the next, etc. etc. etc. Using the angle to time conversion from before, it is now possible to schedule when the start of dwell, and the spark event will need to occur. By repeating the process for each cylinder and each event, we get varoom!
Real code next time.
Part 5:
How is a person supposed to get anything done with all the interruptions…
Time to start putting together a real device. So with that in mind, the first task is determining what resources are needed, then select devices to suit the needs. First what is needed; there needs to be timers to convert real time events into coordinated operations, there needs to be input signals that trigger interrupts (for cam and crank signals), and there needs to be analog to digital converters for MAP and Temperature inputs. It is always best to look around and see if there is a device that provides a working platform already available.
Here is where the embedded world maintains their warring factions, in the world of processor selection many camps, and many noble attempts. All have their merits, and all have their shortcomings. None are perfect, so it is always a battle of compromise. I am a cheap and easy kind of guy, and have had a good amount of experience with Atmel devices, so our system will start out using an Arduino Mega2560, with the 16MHz Mega2560. It also has a serial port USB for communications, and access to all the lines necessary. I have found that the Arduino Sketch system is good for instructional purposes, and for many applications provides a satisfactory solution. However, I tend to bump into timing issues with interrupts, and A/D management, so I ditched the Sketch system and downloaded the AVR Studio and purchased a programmer to go directly to the processor. Doing this means rebuilding a bunch of library mechanisms, in order to have a working platform. The list of library functions to develop will be covered at some other time, because I could generate hours of discussion relating to BIOS and Operating System development. Instead I will just provide solutions, and leave discussion for a later bunch of papers.
First thing I always start with is some basic communication. The Nano has a USB serial line (also provides power) and it is tied to the tx and rx lines of the 2560. So the first coding will be to create a serial interface. The processor runs at 16MHz, so driving a serial port at 9600 or 19200 bps would slow things way down, so, the input and output will be managed through the use of buffers and interrupts. Lots of folks have done a buffered input, but for some reason get hung up attempting to make a buffered output handler. Truth of the matter is, they behave exactly the same with a single difference; the output process turns the interrupt on and off. Each has two component processes, one for interrupt, and the other for transfer (a get and put for each buffer). Both have an instance of a circular buffer, the tx buffer needs to have a hold, when the buffer is full, and needs to turn off the interrupt when the buffer is empty (turn back on for the first output). The rx buffer needs to watch for overflow, and provide status for an empty buffer condition. I will cover messages, a bit later.
/******************************************************************************** * Copyright(c) 2016 Chaney Firmware * * History: * * original creation [JAC] When the earth was cooling * ********************************************************************************/ #include "types.h" #define UBAUD_57_6 34 #define UBAUD_38_4 51 #define UBAUD_19_2 103 #define UBAUD__9_6 207 #define UCSRA_INIT (1<<U2X0) // Timebase for UART 2x clock #define UCSRB_INIT (1<<TXEN0)|(1<<RXEN0)|(1<<RXCIE0) // Enable transmit and receive lines and receive interrupt #define UCSRC_INIT (3<<UCSZ00)|(0<<UPM00)|(0<<UMSEL00) // n-8-1 #define COMMSPEED UBAUD_57_6 // 57600 // #define COMMSPEED UBAUD__9_6 // 9600 #define BUFFSIZE 32 static UByte rx0Buf[BUFFSIZE]; static UByte tx0Buf[BUFFSIZE]; static volatile UByte rx0Hed, rx0Tal, rx0Siz; static volatile UByte tx0Hed, tx0Tal, tx0Siz; static UByte i0On; bool isCommNotEmpty(void) { return (rx0Siz != 0); } void initUart(void) { UCSR0A = UCSRA_INIT; UCSR0B = UCSRB_INIT; UCSR0C = UCSRC_INIT; UBRR0 = COMMSPEED; rx0Hed = 0; rx0Tal = 0; rx0Siz = 0; tx0Hed = 0; tx0Tal = 0; tx0Siz = 0; i0On = 0; } //=========================== // *** WARNING *** // Do not use these functions // within an interrupt void putComm(UByte c) { if (i0On == 0) { i0On = 1; UDR0 = c; UCSR0B |= (1<<TXCIE0); } else { while (tx0Siz >= BUFFSIZE); cli(); tx0Buf[tx0Tal] = c; tx0Tal = (tx0Tal + 1) < BUFFSIZE ? tx0Tal + 1 : 0; tx0Siz++; sei(); } } UByte getComm(void) { UByte c = 0; if (rx0Siz > 0) { cli(); c = rx0Buf[rx0Hed]; rx0Hed = (rx0Hed + 1) < BUFFSIZE ? rx0Hed + 1 : 0; --rx0Siz; sei(); } return c; } void putStr(char* s) { while (*s != 0) { putComm((UByte)(*s++)); } } //=========================== ISR(USART0_TX_vect) { /* USART, Tx Complete */ UByte c; if (tx0Siz > 0) { c = tx0Buf[tx0Hed]; tx0Hed = (tx0Hed + 1) < BUFFSIZE ? tx0Hed + 1 : 0; --tx0Siz; UDR0 = c; } else { i0On = 0; UCSR0B &= ~(1<<TXCIE0); } } ISR(USART0_RX_vect) { /* USART, Rx Complete */ UByte c = UDR0; if (rx0Siz < BUFFSIZE) { rx0Buf[rx0Tal] = c; rx0Tal = (rx0Tal + 1) < BUFFSIZE ? rx0Tal + 1 : 0; rx0Siz++; } else { /* Dropped bytes go here */ } } /* end of file */
The code demonstrates a few things, first are some conventions that I find useful, mostly the use of UByte, UWord, and ULong. These are unsigned char, word, and longword (8, 16, and 32 bits). The names are all the same length, so the listing looks nicer. I also put the open brace ({) on the line with the function name instead of the line under. That’s just my preference. Last the source provided in these articles, is stuff I developed over a lot of time, so if you use it and get really rich, remember to say thank you.
Next device are the timers. The 328 has three timers; two eight bit, and one sixteen bit. All three have overflow interrupts (to provide extension). All three have compare mode, and the 16 bit has a capture mode. The interrupt management for these is pretty straight forward, configure running speed, set the value of the compare register, and perform the simple operation in the interrupt. The system needs two compare operations; one to turn on the coil (dwell), a second to turn the coil off (spark). Then the free running timer will be used to provide current time for cam and crank events.
/******************************************************************************** * Copyright(c) 2016 Chaney Firmware * * History: * * original creation [JAC] When the earth was cooling * ********************************************************************************/ #include "types.h" #define TICS_PER_mS 2000L /* system specific */ #define mS_PER_SEC 1000L #define TICS_PER_SEC (TICS_PER_mS*mS_PER_SEC) #define SEC_PER_MIN 60L #define MIN_PER_HR 60L #define HR_PER_DAY 24L #define HR_PER_QDAY (HR_PER_DAY/4L) /* hours per quarter day */ #define QDAY_PER_YR 1461L /* quarter days per year */ #define mS_PER_MIN (mS_PER_SEC*SEC_PER_MIN) #define TIMSK0_INIT (1<<TOIE0)|(1<<OCIE0A) #define TIMSK1_INIT (1<<TOIE1) #define TIMSK2_INIT (1<<TOIE2) #define TIMSK3_INIT (1<<TOIE3) #define TIMSK4_INIT (1<<TOIE4) #define TIMSK5_INIT (1<<TOIE5) #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 TCCR3A_INIT 0 #define TCCR3B_INIT (3<<CS00) #define TCCR4A_INIT 0 #define TCCR4B_INIT (3<<CS00) #define TCCR5A_INIT 0 #define TCCR5B_INIT (3<<CS00) #define mS_UPDATE 250 #define OCR0A_INIT mS_UPDATE void rtUpdateA2D(void); void rtUpdateDlog(void); static UWord mSofMin; // Running time mS of minnutes static UWord minitRun; // Running time SWord getMsRunning(void) { return mSofMin; } SWord minutesRunning(void) { return minitRun; } static UWord msTimeOut; void setMsTimeOut(UWord n) { msTimeOut = n; } UWord getMsTimeOut(void) { return msTimeOut; } static ULong ov0Tic; static ULong ov1Tic; static ULong ov2Tic; static ULong ov3Tic; static ULong ov4Tic; static ULong ov5Tic; ULong getTmr0(void) { ULong rVal; cli(); rVal = (ULong)TCNT0 + ov0Tic; sei(); return (rVal & 0x0fffffff); } ULong getTmr1(void) { ULong rVal; cli(); rVal = (ULong)TCNT1 + ov1Tic; sei(); return (rVal & 0x0fffffff); } ULong getTmr2(void) { ULong rVal; cli(); rVal = (ULong)TCNT2 + ov2Tic; sei(); return (rVal & 0x0fffffff); } ULong getTmr3(void) { ULong rVal; cli(); rVal = (ULong)TCNT3 + ov3Tic; sei(); return (rVal & 0x0fffffff); } ULong getTmr4(void) { ULong rVal; cli(); rVal = (ULong)TCNT4 + ov4Tic; sei(); return (rVal & 0x0fffffff); } ULong getTmr5(void) { ULong rVal; cli(); rVal = (ULong)TCNT5 + ov5Tic; sei(); return (rVal & 0x0fffffff); } void initTimers(void) { TIMSK0 = TIMSK0_INIT; TIMSK1 = TIMSK1_INIT; TIMSK2 = TIMSK2_INIT; TIMSK3 = TIMSK3_INIT; TIMSK4 = TIMSK4_INIT; TIMSK5 = TIMSK5_INIT; TCCR0A = TCCR0A_INIT; TCCR0B = TCCR0B_INIT; TCCR1A = TCCR1A_INIT; TCCR1B = TCCR1B_INIT; TCCR1C = TCCR1C_INIT; TCCR2A = TCCR2A_INIT; TCCR2B = TCCR2B_INIT; TCCR3A = TCCR3A_INIT; TCCR3B = TCCR3B_INIT; TCCR4A = TCCR4A_INIT; TCCR4B = TCCR4B_INIT; TCCR5A = TCCR5A_INIT; TCCR5B = TCCR5B_INIT; OCR0A = OCR0A_INIT; msTimeOut = 0; mSofMin = 0; minitRun = 0; } ISR(TIMER0_COMPA_vect) { OCR0A += mS_UPDATE; /* refresh for 1mS Timer Tic */ if (msTimeOut > 0) { --msTimeOut; } rtUpdateA2D(); } ISR(TIMER0_COMPB_vect) { } ISR(TIMER0_OVF_vect) { ov0Tic += (1<< 8); } ISR(TIMER1_OVF_vect) { ov1Tic += (1<<16); } ISR(TIMER2_OVF_vect) { ov2Tic += (1<< 8); } ISR(TIMER3_OVF_vect) { ov3Tic += (1<<16); } ISR(TIMER4_OVF_vect) { ov4Tic += (1<<16); } ISR(TIMER5_OVF_vect) { ov5Tic += (1<<16); } /* end of file */
Third are the A/Ds. Up to eight lines, in a matrixed operation. Plenty for this operation. A/D management is another instance where I prefer to use the interrupt, because waiting for the A/D to settle is wasted time, so I operate the A/D using an interrupt process. The sequence is easy enough; periodically, initialize the A/D, multiplex the first channel, turn on the interrupt, then start the conversion. In the interrupt, get the converted value and store in a buffer, multiplex the next channel, and start the conversion, or if all the channels have been read, shut off the interrupt.
/******************************************************************************** * Copyright(c) 2016 Chaney Firmware * * History: * * original creation [JAC] When the earth was cooling * ********************************************************************************/ #include "types.h" #include "calibration.h" /************************************************************************************************** A2D all operate the same. Each channel is sampled in turn, and each sampling cluster is triggered periodically. 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. **************************************************************************************************/ /* Right aligned Free running /64 prescaler */ #define ADCSRA_INIT (7<<ADPS0) #define ADMUX_INIT (7) #define A2D_CHAN (16) #define AD_FILTER (8) #define AD_ROUNDER (AD_FILTER/2) static UByte a2dChan = 0; static UByte a2dSemi4 = 0; static UByte n_A2D = 0; static UWord aFlags; static UWord oFlags; static SWord a2dVal[A2D_CHAN]; bool isA2dAct(UByte n) { return (n < A2D_CHAN ? ((aFlags & (1<<n)) == 0) : false); } bool isA2dOn(UByte n) { return (n < A2D_CHAN ? ((oFlags & (1<<n)) == 0) : false); } SWord getA2D(UByte n) { return (isA2dAct(n) ? (a2dVal[n] + AD_ROUNDER) / AD_FILTER : 0); } void initA2D(void) { a2dSemi4 = 0; // pseudo semiphore, to only run the conversions when ready a2dChan = 0; aFlags = getA2DFlags(0); oFlags = getA2DFlags(1); ADCSRA = ADCSRA_INIT; } void rtUpdateA2D(void) { // gets called on the 1mS tick aFlags = getA2DFlags(0); oFlags = getA2DFlags(1); if ((n_A2D++ & 1) == 0) { // each half runs every 2 mS 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) { // first active A/D or past end 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) } } } else { /* * updates with A/D data values * check limits and set warnings */ } } 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 */
Last are the interrupt lines, two lines are available (how convenient, cam and crank), the operation of each has been described previously. Because the system is supposed to be as generic as possible, and depending on the crank wheel, and cam sequence, the active edge could be either rising or falling, a secondary check is made in the interrupt with a flag maintained as a calibration value (like cam to TDC, or cylinder TDCs). One flag for each camFalling, and crankFalling. Then using an exclusive or with the state of the input line active edge = crankFalling ^ crankState. Setting the interrupt on either rising or falling, then provides a means to obtain delay for width of tooth as well as tooth event time. There is lots more stuff here than needs to be, and it will be covered, in a future installment. Remember the intention is to handle exceptions.
/******************************************************************************** * Copyright(c) 2016 Chaney Firmware * * History: * * original creation [JAC] When the earth was cooling * ********************************************************************************/ #include "types.h" #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 inline bool isEngCrkLow(void) { return ((PIND & (1<<PD2)) != 0); } inline bool isEngCamLow(void) { return ((PIND & (1<<PD3)) != 0); } static SWord crkNope; // Wait number of tooth events static SWord crkTeeth; // Tooth count for crank static ULong crkTime; // Crank event time static ULong crkDiff; // Crank to Crank time static ULong crkTimeout; // inactivity timeout static ULong camTime; // Cam event time static ULong camDiff; // Cam to Cam time static ULong base; static UWord toothAfterCam; // synchronization value static UWord toothLast; // last tooth detected static UWord toothPerCam; // total number of teeth per cam event static ULong preRpm; // partial math calculated constant void rtUpdateCrkCam(void) { // periodic real time updates (called from a timer event) if (crkTimeout > 0) { // mS timeout for detecting engine stop --crkTimeout; base = crkDiff * (ULong)crkTeeth / PART1; // engine still running "base" is calculation base for RPM } else { // detected engine stop, so shut off spark and reset counters spkLev = SPK_NSYNC; crkNope = 4; toothLast = 0; } } void refreshCrkCam(void) { // work that gets performed as part of initialization crkTeeth = getCrkToothCount(); toothAfterCam = getCamToTdc(); preRpm = TICS_PER_MIN / (ULong)crkTeeth; toothPerCam = crkTeeth + crkTeeth; } void initCrkCam(void) { EICRA = EICRA_INIT; EIMSK = EIMSK_INIT; spkLev = SPK_NSYNC; crkNope = 4; toothLast = 0; refreshCrkCam(); } SWord getRpm(void) { return (crkDiff != 0 ? (SWord)(preRpm / (ULong)crkDiff) : 0); } /* Crank interrupt Interrupt on both rising and falling transition. Active edge is determined if true falling and low state or rising and high state. Since values are relative to previous event, a not ready counter is implemented (crkNope) until a diff can be calculated. Two tasks for the crank interrupt. Record event time (crkTime), and calculate time since last event (crkDiff). Tooth counter maintains last recorded tooth, based on cam to TDC. Cam interrupt single event CAM. */ ISR(INT0_vect) { ULong tmpDiff; ULong tmpTime; tmpTime = getTmr1(); tmpDiff = tmpTime + (crkTime > tmpTime ? 0x10000000 : 0) - crkTime; if (isEngCrkRising() ^ isEngCrkLow()) { // logic to handle active rising or active falling edge crkTout = CRANK_DELAY; // there was an event, so reset the timeout if (crkNope > 0) { --crkNope; crkDiff = tmpDiff; crkTime = tmpTime; } // enough events to capture speed else { // everything is good to start our operation if (isEngCamDetected()) { setEngCamDetected(false); // cam detected so reset the flag toothLast = toothAfterCam; // set tooth to correct index spkLev = SPK_SYNC; // we are in sync so spark can happen } else { toothLast++; } if (toothLast > toothPerCam) { toothLast =- toothPerCam; } crkDiff = tmpDiff; crkTime = tmpTime; } } } ISR(INT1_vect) { if (isEngCamRising() ^ isEngCamLow()) { setEngCamDetected(true); } } /* end of file */
The caution that must be monitored is with all these interrupts, there is a risk that the system becomes interrupt bound is more possible. The same principles that are used in other system are used here as well, but with more attention to detail. Keep the process in the interrupt as brief as possible, and don’t do things in the interrupt that can be done elsewhere. If something is not tightly bound to the event, then remove it from the interrupt routine.
I have provided source code for all this, in order that generations of examiners will be able to point and laugh at what is attempted here, and others will be able to see what shouldn’t be attempted. My code always comes with a 100% guarantee. There is a 100% assurance that it either works, or doesn't.
I said I would dabble with communications as well, so here goes. Output messages only need to be understood, so the burden is on the listener, the task of the sender is to use the messages in the agreed format. Incoming messages are usually some form of command, and need to have some sort of parsing performed so the listening device understands what is being requested. This brings up one of my favorite comments about system designers; if you count up the number of designers in a room and multiply that number by two, and that is the minimum number of perfect solutions you will be sorting through for the final design. In this case, just me, so you get two. First solution uses a continuous output with embedded ANSI escape sequences for screen positioning, creating a display of operating parameters that will interface with a standard serial interface, kind of like the old batch stations of the ‘80s. The problem with this solution is communication in the other direction for sending commands to the controller. Second solution is a master slave operation where the user sends commands, and the system responds with requested data. This operation is a bit cleaner because, there is no burden on the system when requests are not being handled. The second solution requires a protocol for the messages, and an agreed format for responses. Advantages to the second system is only the data being requested is returned, and validity checking (checksum) can be performed. For the last piece, I was interested in using LabView to develop the user interface, so the protocol should be able to manage those exchanges as well. Sounds like a topic for a full section on its own, sounds like a plan.
Part 6:
Some people say I don’t communicate well…
To establish a communication system, the first thing that is needed is a description of the types of messages that need to be passed, and the frequency of message update. Answering the questions; what data needs to be transferred, and how fresh does the data need to be? The system presented here uses a call and response method. The host sends a request to the service, which responds with either the data or an error message of some sort. For simplification of work effort, and because test tools are not always readily available, all the command structure will be able to be sent from a simple serial communication port (standard ASCII). Because this means the commands will most likely be typed, the formatting should be terse, but also make sense to a somewhat casual observer.
Next thing to determine is what data needs to be transferred. The system contains two forms of information, first form is calibration data. This is the content of the tables that will be used to convert A/D, provide angle of advance, configuration constants, and conversion factors. Tables all take the form of an NxN array, with size available by request, which means the system also needs to provide table structure information. The second form of data are monitors. Monitors are the resulting value of processes in the device, like RPM, total advance, battery level, etc. Since monitor information is needed frequently, and repeated often, the command must provide for configuring the request, bundling several monitors as a single response. Because no system is ever absolutely correct, or complete in the first attempt, expandability is also a requirement.
So, for now, two command sets; table, and monitor, each with a command to configure, and fetch. To keep things simple and small, the commands have an abbreviated form. MONI for monitors, and TABL for tables. CONF for configure, and FETC for fetch. Because configure has two forms, one for setting and one for getting, if a command has an intended response (getting), then the command will end with a question mark (?). And because fetch is inherently intends a response, it will be followed by a question mark. From a sentence sort of structure, as a command, it should read as command followed by system, so it is CONF TABL and not TABL CONF. Parameters are needed for configuration, so a format for parameters will also be required. It is also necessary to distinguish between command and parameters. If a space is used, then no space can be used for the command, so a colon will be used as a separator for elements of the command. Simple carriage return will work, as an alternative (for making command strings) the use of a semi colon will also signify the end of a command. Parsers need to know about case sensitivity, and many of the parameters require them to be case sensitive, however, the commands do not, so commands are case insensitive, but the parameter list is case sensitive.
Parameter lists are determined by the system being accessed. With the logical view of tables, they can be updated as a single cell (X, Y), a row of cells (X0-Xn, Y), a column of cells (X, Y0-Yn), and a block of cells (Xa-Xb, Ya-Yb). Simple method is to use comma separation and parenthetical grouping. To create an example, table 4 is an 8x8 array of integers, we wish to update the 3rd row with new data. Remembering zero offset, the command becomes:
CONF:TABL 4,R,2,(3,4,5,6,7,8,9,10);
Which translates to configure table 4, update row 2 (zero offset), with the data 3,4,5,6,7,8,9, and 10.
For a cell update, the parameters change to C for cell instead of R for row, (2,3) for row and column instead of 2 for row, and a single number for the content data. For a block, the parameters change to B for block, ((x,y)(w,h)) for upper left starting cell and width, height of the block, data list would be ((1,2,3),(4,5,6),(7,8,9)) for a 3x3 block.
Monitors are usually named, or enumerated lists, remembering, the more complex the listing, the more complicated the task of parsing becomes. For a simple enumeration type of list, the configuration becomes:
CONF:MONI 3,8,4,5,2,10;
Requesting 6 monitors, enumerated.
To check on the current configuration, the commands CONF:MONI? and CONF:TABL? will return the current relevant information. The FETC command retrieves the formatted response. The FETC command does not make much sense with TABL, because the table configuration will be returned with CONF:TABL? And there is no active value change for the table data. To retrieve the configuration of table 3, the command sequence would be CONF:TABL 3;CONF:TABL? To set the table system to table 3, then retrieve the table configuration. If table 3 were a 2x2 array, the response would look something like this:
3,0,(2,2),((1,2),(3,4));
Which translates to table 3 of type 0 (how the data is used by the system), a 2x2 table, containing 1,2,3, and 4 for data elements.
Retrieving the monitors is done with a FETC:MONI? command. The return will contain the enumerated list of monitors, and the data from the monitors. This is different from the CONF:MONI? which would only return the enumerated list and not the values. The enumerated list is included in the fetch command, so interface devices do not have to keep track of which monitors are being returned, or which order they are being returned. The enumeration matches up with the data element and the response becomes:
6,(3,8,4,5,2,10),(100,352,16002,1200,500,788);
Using an enumerated list means a preexisting understanding between the interface and the system is required, and if a monitor is changed, from MAP=2 to MAP=3 the interface will need to be made aware.
Additional communication features would be to control formatting, units, return structure, burst mode (large blocks of data), error messages, etc. But for simplicity, the system here will start off with just these commands.
The system now has configuration tables and monitors for generated data. Previously, I described the use of integer only data, which provides for simplification of the monitors, because all return values will be 16 bit integers in the defined units. A user interface will need to perform any conversions, to relieve the system from performing excessive calculations to manage floating point, US/Metric, and absolute pressure to relative pressure. The communication protocol is now simplified to listen for incoming messages, then parse the message when a carriage return or semi-colon is received, and format and send the response.
Parsing messages is a topic that takes up entire chapters in books on compiler design. I won’t go into any long winded dissertation of merits of one system over another, but I will present a somewhat unique design for serial parsing, that uses a state process for parsing each character, instead of waiting for the whole message before performing the parse. The advantage is, idle time is spread out over the receiving of the message, so there is less impact to operation, and there is no need for an input buffer. Code first, then how it works.
/************************************************************************************************* * Serial Parser - a creation by Jack Chaney, created long ago when the earth was cooling. * Instead of waiting for the entire string to be entered, before parsing the instructions, * a state tree is used to parse the string on a per character basis as the string comes in * Optionally, at each node (recognized character) it is possible to route the incomming data * to a procedure. If the procedure returns 0 the search continues along the "found" stream. * If the procedure returns a 1 the search restarts at the head of the tree. **************************************************************************************************/ struct pN { UByte c; Struct pN *next; Struct pN *found; UByte (*callback)(UByte c); }; void parse(UByte c) { UByte p; static SByte b = 0; static struct pN *t = null; if (isSysEchoOn()) { putComm(c); } // Send the character back if echo is on if (t == null) { t = head; b = 0; } // Last pass was an end of branch, so reset to top of tree p = ('a' <= c) && (c <= 'z') ? c & ~bit5 : c; // (optional) upper case instructions if (b == 0) { // Clear flag = no more processing & last character found while ((t != null) && (t->c != p)) { t =t->next; } // Search the list for a match if (t != null) { // not end of branch, so we have found a match if (t->callBack == null) { t = t->.found; } // if no process then point to next level search else { b = 1; } // if existing process, set flag so characters are sent there } } else { // b is non zero, send incomming charater to linked process b = t->callback(c); // b returns: 0=complete, 1=reset, other=continue to process t = b == 0 ? t->found : b == 1 ? null : t; // 0, point to next level search. 1, point to tree top } }
The parser operates off the binary tree principle, with a node, and a pass or fail link. A record is used to keep track of each state information, and a pointer is used to track the location in the state table (tree). At each character input, a comparison is performed. If found and there is a function to run, the next time the character is passed to a callback routine. If the callback routine returns zero it is done, if it returns something else, the callback routine gets called again until the return value is zero. Special case of returning a 1 will reset the search to the head of the tree.
Nifty toy… call back routine is a number parser that works in the same manner, for input of parameter values.
Part 7:
This discussion seems to be getting off track a bit. I started out to develop a simple engine controller and got all hung up on putting together tools. The problem is the tools are always necessary to make things work and to keep them working. The tools I have presented so far, do provide a good platform for other work as well, The A/D and UART code is totally portable for any application using 8bit Atmel processors, and with a small amount of adjustment, is portable to other systems as well. Time to survey what has been developed so far in the system. For basic tools, there is communication, control for A/D channels, basic timers, and a core engine monitor providing speed (lastDiff), angle (lastTooth), and the event time of the last tooth (lastTime). As mentioned earlier, the system operates primarily in steady state, with exception handling.
Returning to steady state operation, the equations for location and delay from before are needed to convert the time to angle and angle to time. This is important because most engine technicians think of advance in terms of angle, and dwell comes from physical properties of the coil and is time based. An important property of the timer is, it continues to run in spite of an interrupt that has been generated, or the current value having been read. This way the timer capture and compare operations will work properly, independent of other processes. This will be very useful to the operation for triggering specific events (dwell and spark). Again, returning to the single cylinder model, the cylinder has a TDC value measured in an angle between 0 and the angle reading of two revolutions (720 degrees, 4pi radians, etc.). For discussion purposes, I will use degrees for descriptions. The advance is maintained as an angle, at idle this is in the area of 20 degrees, and for general purposes, a coil takes about 3-5mS to charge. The cylinder TDC is 90 degrees, the engine speed is 750 RPM, the crank is a four tooth wheel, synchronization has occurred (steady state), and we just triggered tooth 6. That should be enough for the calculations.
- Tooth 6 of 8 means the angle was 540 degrees.
- Engine speed is 750 RPM meaning lastDiff was calculated to be 20000 tics
- TDC is 90 so there is 180 degrees before the cylinder TDC (wrap around at 720).
- Advance of 20 degrees puts spark event at 70 degrees, or 160 degrees from last trigger.
- 4mS for dwell converts to18 degrees, or 142 degrees from last trigger.
- Timer compare for start of dwell is lastTime + 35556 tics
- Timer compare for spark is lastTime + 31556 tics
Not all that complicated. What was done is the values of 137.5 and 160 degrees were converted to the number of tics at the current engine speed it would take to cover that distance.
Dwell142 =2000090 and Spark160 =2000090
The numbers work out right, after the equation too, because 35556 – 31556 yields 4000 tics at 1MHz (our fictional clock rate) is 4mS which is the dwell value. The solution for single cylinder is equivalent for all cylinders. The difference in the process, is the TDC for the particular cylinder, and for the continuous operation, the process has to be corrected at each tooth event. As an example if the event was tooth 7 instead, then there would only be 90 degrees to TDC and 70 degrees to spark, giving 15556 tics remaining to spark (notice it is 20000 less which is the value of lastDiff).
If the system has several spark and dwell events in a cam rotation, some form of scheduling will be required. Luckily, a good amount of the process is duplicated for each cylinder, and at each tooth event, so it can be done in a loop process. A good rule for embedded applications is to look for similarities and determine operations that are repeated, so they can be run a single time, instead of many times. In the example, the operation of lastDiff / degreesPerTooth is used repeatedly, making it a proper candidate to pull aside and perform a single time. The value of lastDiff is updated on each crank event and degreesPerTooth is a constant based on the number of teeth on the crank wheel. Remembering the crank event is an interrupt, and the rule of interrupt processing is to keep it as short as possible, and a division operation is a time expensive command. If the value of degreesPerTooth is done previously, and lastDiff is calculated already to determine speed, then a single divide is not too expensive and provides a valuable use variable. Earlier the system maintained a lastTooth counter, which is a good generalization for multiple systems with varying number of crank teeth. However, our equation needs to know number of degrees until spark and dwell, which are based on the value of cylinderTDC – lastToothAngle. Instead of updating tooth on each event, and because degreesPerTooth is a known value, updating angleOfLastTooth is a better value to maintain. So the new crank interrupt process performs the operations:
- Retrieve current timer value
- Calculate time from previous event
- Replace old time with new time
- Calculate intermediate angle:distance conversion angleToTime (lastDiff/degreesPerTooth)
- Update angleOfLastTooth
With this information, the scheduler is able to perform the process to obtain times for spark and start of dwell with, (cylinderTDC – advance) – angleOfLastTooth giving the angleOfSpark, then angleOfSpark – dwell giving the angleOfDwell. The values are calculated for angleOfSpark * angleToTime giving time to spark event, and angleOfDwell * angleToTime giving time to start of dwell. Repeating the equations for each cylinder in a multi cylinder operation, only requires a change in the cylinderTDC for each cylinder. Sharp eyes will notice that there are two multiply instructions required for each cylinder, which could be time expensive, if done for many cylinders at each change, so scheduling should organized so multiply instructions are performed, only when necessary.
The calculation for angleOfSpark and angleOfDwell only requires simple summation, so can be performed on each cylinder without significant expense of time. The known value of angleOfLastTooth is updated at each event, giving a comparison for the operation. Comparison is done on a range, rather than on a single value, creating a window of activity. If the angleOfSpark or the angleOfDwell is determined to be in the window, the secondary multiply operation is performed and the time is added to the event queue.
Like any good math discussion, formulas are presented then parts are backed out to trace another path of logic. With that in mind the values of angleOfSpark and angleOfDwell contain the element angleOfLastTooth which will be removed, and angleOfLastTooth is used as a reference to make a window. Since last tooth already happened, it is too late to schedule an event there, so the window should start some angle beyond that point. It should also cover events for the next tooth event for an equivalent distance past the next event. Half is a good number and provides a good general case for the window:
windowStartAngle=angleOƒ LastTooth+12 degreesPerTooth
and
windowEndAngle=windowStartAngle+degressPerTooth
This provides a sampling window for each event angle. Returning to what needs to happen, when… The values of dwell and advance don’t change significantly from crank event to crank event, so they can be updated at a slower regular rate. Since the values of advance and dwell are applied to all the cylinderTDCs this can be done in a loop in the slower process. The value angleOfLastTooth is updated at each crank event, so the window is also updated at each crank event. If the values of angleOfDwell, and angleOfSpark are updated regularly at the slower rate, and maintained in an array, the values of the angles can be compared to the window range to determine if action should be performed. The process now has two components; a slow operation, and a fast operation. The fast operation has the 5 elements from before, then adds a window check. The slow operation calculates dwell and advance, then the values for angleOfDwell and angleOfSpark of each cylinder.
Here is the spark operation...
/******************************************************************************** * Copyright(c) 2016 Chaney Firmware * * History: * * original creation [JAC] When the earth was cooling * ********************************************************************************/ #include "types.h" #define PART1 ??? // first half of equation (partial) #define PART2 ??? // second half of equation (partial) static UWord advTotal; static UWord dwlTotal; static UWord halfTooth; static UWord cyl; void crkSlowEvent(void) { UWord c; UWord tmp; advTotal = getEngAdvBase(); // base advance for engine tmp = getEngDwellBase(); // base dwell (time) need to convert to degrees dwlTotal = tmp * PART2 / base; // need to make sure base is not 0 cyl = getEngCylCount(); // how many cylinders are we using halfTooth = degPerTooth / 2; for (c = 0; c < cyl; c++) { spark[c] = getEngCylTdc(c) - advTotal; dwell[c] = spark[c] - dwlTotal; } } void crkFastEvent(void) { UWord winMin, winMax, c; winMin = toothAngleLast + halfTooth; // window start angle winMax = winMin + degPerTooth; // window end angle for (c = 0; c < cyl; c++) { if ((winMin <= spark[c]) && (spark[c] < winMax)) { fTime = spark[c] - toothAngleLast; scheduleSpark(fTIme); } if ((winMin <= dwell[c]) && (dwell[c] < winMax)) { fTime = dwell[c] - toothAngleLast; scheduleDwell(fTime); } } } /* end of file */
Here is the updated cam/crank...
/******************************************************************************** * Copyright(c) 2016 Chaney Firmware * * History: * * original creation [JAC] When the earth was cooling * ********************************************************************************/ #include "types.h" // PART1 and PART2 are values which when multiplied give number of degrees in a revolution #define PART1 ??? // first half of equation (partial) #define PART2 ??? // second half of equation (partial) void crkFastEvent(void); void crkSlowEvent(void); #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 inline bool isEngCrkLow(void) { return ((PIND & (1<<PD2)) != 0); } inline bool isEngCamLow(void) { return ((PIND & (1<<PD3)) != 0); } static SWord crkNope; // Wait number of tooth events static SWord crkTeeth; // Tooth count for crank static ULong crkTime; // Crank event time static ULong crkDiff; // Crank to Crank time static ULong crkTimeout; // inactivity timeout static ULong camTime; // Cam event time static ULong camDiff; // Cam to Cam time static ULong base; static UWord toothAfterCam; // synchronization value static UWord toothLast; // last tooth detected static UWord toothPerCam; // total number of teeth per cam event static UWord degPerTooth; // degrees from tooth to tooth static UWord angleAfterCam; // degrees of toothAfterCam static ULong preRpm; // partial math calculated constant void rtUpdateCrkCam(void) { // periodic real time updates (called from a timer event) if (crkTimeout > 0) { // mS timeout for detecting engine stop --crkTimeout; base = crkDiff * (ULong)crkTeeth / PART1; // engine still running "base" is calculation base for RPM } else { // detected engine stop, so shut off spark and reset counters spkLev = SPK_NSYNC; crkNope = 4; toothLast = 0; } crkSlowEvent(); // spark operations for "slow" operations } void refreshCrkCam(void) { // work that gets performed as part of initialization crkTeeth = getCrkToothCount(); toothAfterCam = getCamToTdc(); preRpm = TICS_PER_MIN / (ULong)crkTeeth; toothPerCam = crkTeeth + crkTeeth; degPerTooth = DEG_PER_REV / crkTeeth; angleAfterCam = toothAfterCam * degPerTooth; } void initCrkCam(void) { EICRA = EICRA_INIT; EIMSK = EIMSK_INIT; spkLev = SPK_NSYNC; crkNope = 4; toothLast = 0; refreshCrkCam(); } SWord getRpm(void) { return (crkDiff != 0 ? (SWord)(preRpm / (ULong)crkDiff) : 0); } /* Crank interrupt Interrupt on both rising and falling transition. Active edge is determined if true falling and low state or rising and high state. Since values are relative to previous event, a not ready counter is implemented (crkNope) until a diff can be calculated. Two tasks for the crank interrupt. Record event time (crkTime), and calculate time since last event (crkDiff). Tooth counter maintains last recorded tooth, based on cam to TDC. */ ISR(INT0_vect) { ULong tmpDiff; ULong tmpTime; tmpTime = getTmr1(); tmpDiff = tmpTime + (crkTime > tmpTime ? 0x10000000 : 0) - crkTime; if (isEngCrkRising() ^ isEngCrkLow()) { // logic to handle active rising or active falling edge crkTout = CRANK_DELAY; // there was an event, so reset the timeout if (crkNope > 0) { --crkNope; crkDiff = tmpDiff; crkTime = tmpTime; } // enough events to capture speed else { // everything is good to start our operation if (isEngCamDetected()) { setEngCamDetected(false); // cam detected so reset the flag toothLast = toothAfterCam; // set tooth to correct index spkLev = SPK_SYNC; // we are in sync so spark can happen } else { toothAngleLast += degrPerTooth; } if (toothLast > toothPerCam) { toothLast =- toothPerCam; } crkDiff = tmpDiff; crkTime = tmpTime; crkFastEvent(); // spark operations for "fast" operations } } } /* Cam interrupt Interrupt on both rising and falling transition. Active edge, like crank, is determined if true falling and low state or rising and high state. */ ISR(INT1_vect) { if (isEngCamRising() ^ isEngCamLow()) { setEngCamDetected(true); } } /* end of file */
This is a good place to stop for now. The system should have everything except a queueing system scheduler for turning the coils on and off to make the spark, which I should cover in short order. This thing is looking an awful lot like an operating system, maybe there is a reason for that. Notice the code will change as exceptions are addressed. (Using this code directly will require debugging and is meant for example purposes). Feel free to post questions.
Not going the way that I wanted, will be starting over with an outline and a better structure to the information. If the desire is to try pressing on with the information provided to this point, the trick to the scheduling is to use two timers, one to turn the coil on, the second to turn the coil off. No matter how fast the engine is running, there will not be a state where two coils turn on at the same time (though, they may be on at the same time), and no state when two coils will turn off at the same time. So using two queues, one for on and one for off, then set the timer compare for the time to turn the coil on, and off. Use a 2d table for base advance based on engine speed and MAP with values of around 20 degrees, and you should be able to make the motor run. I will be back soon.