The Nano is all ready to do some work. First it needs to have some additional pieces added. A simple divider for sensor input, and content for the associated table the arrow is the input to the A/D pin. The larger circle is the sensor input, the smaller is the reference voltage (typically 5v), and the resistors are based on the sensor but typically 10k ohm for the current limiter and 100k ohm for the divider.
A first sensor
The source code provided previously had three tables to manage the A/D channels. The first was a flag field for active state and in use (on/off) switch for each channel. The second table was to provide a default sensor value, in case the sensor was marked active, but currently off. The third table were linear arrays for each channel. To program the values for a simple Engine Coolant Temperature sensor takes three steps. First, program the array with the values.
CONF:TABL 2,1,0,(39815,35315,31315,29315,28315,27315,25815,24315)
This programs table 2, by row(1), row 0, with the string of 8 values. The values are in units of degrees K * 100, and extracted from a table for a common engine coolant temperature sensor found on line. Second, program a good default value for the sensor, in case it is set to off.
CONF:TABL 1,0,(0,0),31315
This programs table 2, by cell(0), column 0, row 0, with the value for 40 degrees C. Provided the sensor circuit is in place, and the sensor is attached, it can now be activated and switched on. Bits are left to right, and in two groups of 8. This requires converting to a decimal number. Also, if a sensor set is already in use, it is a bad idea to deactivate it by accident, so first get the value of the flags.
CONF:TABL 0
FETC:TABL?
The returned value is the current content of the flags. Presuming a fresh start this value is -1 because all the bits are on. The switches for sensors is implemented using negative logic (so things default to inactive and off). The bits are left to right for the sensors (just the way I did it), with on off in the high byte, and active state in the low byte. The sensor is 0 so the value should turn off bits 15 and 7.
CONF:TABL 0,0,(0,0),32639
Provided everything is connected and working, the device should now be able to provide an accurate temperature reading from the sensor. To do this, it is necessary to read the monitor. The source provided has all eight channels as readable values as raw A/D and converted value. The commands to output the A/D and converted sensor value takes two steps. Configure and fetch.
CONF:MONI 0,8
FETC:MONI?
The values returned are the count of monitors (number of monitors), the list of enumerated monitors, and the list of monitor values (signed). Something like:
2,(0,8),(370,-3234)
Which translates to two monitors, numbered 0 and 8, monitor 0=370, and monitor 8=-3234, but because our monitor is unsigned, the value is really 29534, which translates to 22.19C or 71.95F… room temperature.
Another sensor
Repeating the process to add a sensor for pressure. A standard MAP sensor (1 bar) again, the information was extracted from a table for a common MAP sensor found on line, table 2, by row(1), row 1 (for the next sensor), and the string of values.
CONF:TABL 2,1,1,1000,2400,3400,4800,5800,7200,8100,9600
A good default value for pressure is standard, sea level, barometric pressure 101.33 kPa (used as a filler).
CONF:TABL 1,0,(0,1),10133
Notice the table incremented y not x because they are 8 1x1 arrays. Last, turn on the sensor, again go through the steps, get the value first.
CONF:TABL 0
FETC:TABL?
This will return the value 32639 that was set before, so turning off bits 14 and 6 makes the new value 16191.
CONF:TABL 0,0,(0,0),16191
…A good exercise
Some more inputs are going to be needed for the next section. These are purely interrupts, and are controlled via the interrupt input lines. Externally, they need to have some line conditioning to handle the input from hall-effect sensors, and to trim the voltage to work properly with the circuit in the Nano, which is relatively unprotected. This series of articles is not focused on hardware, so only simplistic circuits are provided to convey intent, and should not be used in a final product without proper review. The left is input from the hall-effect sensor, and the right is the output to the interrupt line (int0 or int1).
The two inputs are from an electrical system, with two periodic signals, of defined structure, which will provide speed and positional data for a rotating object. For both signals, rising and falling edges could be significant, so it will be necessary to define either as the active edge, and be able to record both rising and falling edge events. For both signals, the active edge is maintained, to high degree of tolerance, to occur at regular intervals in the rotation, so time measurement between any two active edge events of the same sensor, should provide an accurate reading of rotational speed. I think that generalizes a cam and crank sensor well enough.
OK, it’s a motor.
I wanted the system to be able to work for any engine configuration that was thrown at it, so after searching available information, and having extended discussions with experts in the field I was able to construct a method for determining position information and angular velocity for a significant number of engine configurations. I learned, while trying to manage the scope of the problem, working from a “steady” state, then managing the exceptions is the best solution. The model of the operation is a pair of turning shafts; one called cam, the other called crank. The shafts turn in unison with the crank shaft turning exactly twice as fast as the cam shaft. Signals on both cam and crank are named “teeth” and the number of teeth is designated N, where Nx is the number of teeth in a single rotation (4x meaning 4 teeth per rotation). It is also possible to have missing teeth in the rotation, in which case, the total number of positions is stated followed by the number of missing teeth (N-m for N teeth with m missing, 36-1, read as thirty six minus one). This means the system will need to be able to manage any number of teeth, as well as gaps for missing teeth. Through research, I also found it was necessary to manage the case of filled gaps between teeth. The reason for all the variation is related to the starting sequence. By having a “coded” sequence, it is possible to detect position of rotation in the least distance. Knowing position is necessary to provide spark at the proper time as quickly as possible and because everybody has a “best” idea, there are many different strategies for coding cam and crank signals. Because this system is intended for “any” strategy, all the forms need to be possible.
Luckily, some basic ground rules are followed by all manufacturers, primarily, the active edge of the crank happens at regular intervals evenly spaced based on the total number of teeth. For example, a 4x crank wheel, will have active edges at each point 90 degrees apart from each other, and when the wheel is turning, each active edge event, will occur at ¼ of the rotation speed. At 300 RPM, the active edge events will occur every 50 mS. This is valuable information, because, by detecting the active transition, and measuring the time until the next transition, the rate of speed can be determined. The process becomes, simply; when the transition is detected, read the value of a clock, and subtract the value of the previous clock value, the difference is the time between events. Before exiting the routine, store the new clock value in the “previous” clock value location. This process repeats for each tooth event, and will provide continuous speed information. To obtain positional information, it is possible to extrapolate based on current time to give elapsed time since the last event, and using rotational speed, be able to determine current angle, since the last event. This gets back to the coding of signals, to identify where in the cycle the last event occurred (which tooth was it). But that question is not a steady state question, so determination of which tooth will be reserved for later.
Interrupt routines
Basic rule of interrupt routines is to not do too much. Just get in and get out. With that in mind, the process will maintain two variables, one called lastTime, and the other called lastDiff. The Time variable is the value of the timer on the previous interrupt and the value gets updated on each call to the interrupt. The Diff variable is the elapsed time since the previous call to the interrupt, and is updated as the lastTime subtracted from the current time. The interrupt process becomes read the timer into tmpTime, set lastDiff equal to lastTime minus tmpTime, update lastTime with the value of tmpTime, and get out. The variables lastTime and lastDiff are accessible to calculate speed and position. The timers were set up from before and have a sufficiently high resolution. Which leaves determining which edge is the active transition. This is an exclusive or condition of the state and the transition required. If the desired state is fallingEdge=true and the state of the pin is false, or fallingEdge=false and the state of the pin is true, then the transition was an active edge.
First pass at code
Still need some unknowns to get filled in. The teeth are evenly spaced around the wheel, but it is necessary to know how many teeth are on the wheel, so for a place holder our system has getTeethCount(), to get a value from calibration (a 1x1 table). The system also needs to construct some constant values for calculations. The timer has a counting rate, which can be expressed as a value of counts per millisecond. So make a #define TICS_PER_mS to account for this system constant. The calculation for rotational speed is in units of revolutions per minute, so conversion of mS to minutes is also needed, and can be constructed logically using milliseconds per second, and seconds per minute. By multiplying all three the system now has counts per minute. Balancing out the equations; lastDiff is the counts between teeth, and the value from getTeethCount is the number of teeth per revolution. Therefore, lastDiff * toothCount is the number of counts per revolution and by dividing counts per minute by counts per rotation yields revolutions per minute (RPM) which is the rotational speed. I like to perform as much math outside the operation as possible, so the value of TICS_PER_MIN can be divided by toothCount, before running the continuous operation.
Previous code
The code provided in the first group of installments turns an Arduino Nano or Uno into a significant platform (more than the “educational” tool). Do not, in any way, get the impression, that I am not highly appreciative of the Arduino platforms and what they provide as an entry mechanism to embedded projects. However, like BASIC, for computer programming, it has its place, but should be viewed as a beginning platform, not an end point. “Crutches are for the disabled” (he says, trying not to be too offensive). To become serious about embedded development, it is necessary to move beyond the “platform” or development “kit”, and read the documentation for the processor to see what is possible. Perhaps it is possible to do something with the processor you are using that is not available via the IDE, because it wasn’t deemed necessary, or the developers did not think to add that feature. As an example, my code sets timer 1 clocking speed at 0.5uS (microseconds), yielding 2000 ticks per millisecond timing for events (this comes in handy when you want tight tolerance for angle, at 10,000+ RPM).
/****************************************************************************** * Copyright(c) 2016 Chaney Firmware * * History: * original creation [JAC] When the earth was cooling ********************************************************************************/ #include "types.h" ULong getTmr1(void); #define TICS_PER_mS 2000 #define mS_PER_SEC 1000 #define SEC_PER_MIN 60 #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 inline bool isEngCrkLow(void) { return ((PIND & (1<<PD2)) != 0); } inline bool isEngCamLow(void) { return ((PIND & (1<<PD3)) != 0); } static SWord rpm; SWord getRpm(void) { return rpm; } static ULong lastTime = 0; static ULong lastDiff = 0; static ULong preRpm = 0; void rtUpdateCrkCam(void) { rpm = lastDiff != 0 ? (SWord)(preRpm / lastDiff) : 0; } void initCrkCam(void) { EICRA = EICRA_INIT; EIMSK = EIMSK_INIT; preRpm = TICS_PER_MIN / getToothCount(); } ISR(INT0_vect) { ULong tmpTime = getTmr1(); if (isEngCrkRising() ^ isEngCrkLow()) { lastDiff = tmpTime - lastTime; lastTime = tmpTime; } } ISR(INT1_vect) { if (isEngCamRising() ^ isEngCamLow()) { setEngCamDetected(true); } } /* end of file */
Extras
Need to add a 1x1 calibration table for tooth count, the call to initCrkCam() to the onetime operation, and rtUpdateCrkCam() to the millisecond routine in timers.c as well. Just ask and I will provide what to do to accomplish these steps.
Top Comments