Teensy 3.0 - Engine Management System

Status
Not open for further replies.

hellandsen90

New member
Hi to all the forumers!

I'm currently in the process of planning my bachelors thesis, and therefore I'm seeking advice on how to utilize this little microcontroller in the best way possible.
I've done some reasearch and concluded that this badass of a microcontroller probably is performant enough for our needs.
We're planning on making an engine management system, with spark and injection control.

That being said, I need some guidance from the more experienced out there, so let's get down to business:
C or C++? Will there be a big performance penalty using C++? We're used to object oriented programming, and would prefer to use C++ if that's possible.

Interrupts... I've sorta figured out how to use interrupts on the digital pins, so that'll be okay. However, we need to process 8000 of those a second for getting RPM and crank position.
Will this be an issue? As far as I can tell the microcontroller will be able to handle this with ease, but I just wanted to confirm that.

When it comes to writing the pins for the injectors and ignition, we will need a precision of 20 microseconds (1 degree at 8000 RPM). A total of four writes per revolution.
I don't think this will be an issue either, but I just want to be sure we're not getting into trouble here.

We will also need serial communication and reading of analog ports, but this can be low performant (5 updates a second is enough).

My biggest concern as of now is: How does one go about synchronising all this? One obviously would not want the serial reading to interfere with the timing of the ignition, etc.
My understanding, as of now, is that utilizing the built in timer with interrupts will be a viable option for setting the output pins, and also using interrupts on the input pins for reading RPM.
These have priority settings as far as i've understood, and utilizing this we could prioritize the parts that are crucial to correct engine operation.

Does anyone have any experiences with making timing-critical applications on the teensy 3?
What are good approaches for doing time-critical operations? Is it possible to use some kind of interrupt along with the timer to trigger our outputs?

For the injectors we will have an output quite like a PWM signal, with "cycle time = 2 * RPM" and duty cycle according to our calculated fuel requirement.
The sparks will just be a brief on and off signal on a digital pin.
For sensing the RPM there will be a hall effect sensor with a 64 teeth wheel with two missing teeth, to allow us to know both RPM and current angle.

Could we utilize most of the arduino-compatible libraries without losing to much performance? Or should we implement everything to make it very project specific and performant?
I'm thinking things like serial, ADC read, digitalWrite(), etc. Are these things we should hardcode in our code, instead of using the libraries?

Any insights would be greatly appreciated!

Thanks in advance
 
Last edited:
Hi again,

After looking through the datasheet a bit, drinking some coffee, and getting all my thoughts together, I've come up with a timing solution I think might just work.
The interrupts from the RPM sensor will serve as the basis to calculate our current RPM and find when the engine's first cylinder is at TDC.
This will then trigger a software interrupt( if that's possible? ) which will be the basis of our timing. The ISR handling this interrupt will then utilize two of the PIT's.
One timer for the ignition and one for the injection. These will be fed the time until the next "happening" ignition wise, or injection wise.
The ISR handling each of these timers will then do what's necessary (set the output high, for example) and set the time until the next event(setting it low, setting a different pin high, etc).
This process of handling the interrupt and setting new timer-durations will then continue until all actions are performed for the engine-cycle when the whole process will be restarted by a new software interrupt.

The issue here is calculating the time between the different tasks. There is no guarantee that these will come in the same order all the time (for example an injector might need to stay open until after the next one is opened, but not if the RPM's and load are low).
This could probably be solved by having three PIT's, one for ignition, one for injectors on, and one for injectors of. One will then only need to set the start-time for each of the operations, and then set the PIT for another (totalRevolutionTime / noOfIgnites).
Anyone have any thoughts one this? Have anyone utilized the PIT's to solve some time critical issues?

A different solution would be to have a periodically timed interrupt triggering every 10 microseconds. Assuming we set our clock frequency to 96 MHz, this would give us 960 instructions for each of these cycles(if I'm not mistaking?).
We could then have the ISR for this timer update a time variable, and check if anything should be set high or low. After completing this the program would go on doing it's usual stuffs, calculations and communication.
How fast this would be processed will then be greatly dependent on how many instructions are left after doing our interrupt processing.
We would still need the RPM-updating interrupt though. (Or maybe we could read the pin each 10 microsecond cycle? My guess is that would give us unnecessary overhead).

Any thoughts would be greatly appreciated.

Offtopic: If anyone's wondering what we're aiming at, it's a DIY engine tuning ecu. Ultra low-cost (<300$).
With android application for a tablet or phone, allowing real-time tuning, logging, and monitoring.
It will have a BeagleBone Black as a Wifi-hotspot for connecting the tablet, and for allowing tuning also from this device.
 
I suspect you might be able to do all of that with one Teensy 3.0 with careful programming. But it may make sense to use multiple processors, each that only do one thing (like count rpms) and then report to the boss microprocessor when something interesting happens. That way you don't have multiple things competing for attention during narrow timing windows.

I am doing some Christmas ornaments using neopixel rings, and I recently started using ATtiny85's. I bought 10 of these for $14, and you need a programmer to program these. I have an Uno that I use for the programmer, but you can get standalone ISP programmers as well. Compared to the Teensy, the ATtiny85 is rather primitive, but for doing a single job it is ok (for example, counting rotations, you might not use interrupts, just do busy polling) and wake up the boss when you get to 10,000 or so.

The Teensy 3.0 also has a real time clock if you put in an appropriate crystal.
 
Last edited:
Wow, so many questions. Let's see how many I can do quickly.... (or not so quickly?)

Use C++. It's about the same speed-wise, and all the supporting code is already a mix of C and C++, so if you restrict yourself to C only, you'll be denying yourself a lot of the really nice features.

Are you placing 60 sensing positions for every rev? Or will 8000 RPM with 1 sensing position be only 133 interrupts per second?

Teensy 3.0 can pretty easily handle 8000 interrupts per second anyway, with the obvious caveat that your interrupt code better take less than 125 us.

My biggest concern as of now is: How does one go about synchronising all this?

Well, there are a lot of ways.

But first, from the nature of your questions, I suspect you're greatly underestimating the amount of computational power that available. Obviously you want to write efficient code, but at the same time, you can take advantage of the speed to do things in fairly simple ways, which will make this project much easier.

For example, you might call micros() inside the pin interrupt and store it to a volatile variable, which would be read by your normal loop code, and set a flag, which your loop code would be regularly checking to know when new data has been written to that variable.

If you're going to fire spark plugs at some particular point in the cycle, you could just do all the math with 32 bit integers (on Teensy 3.0, a 32 bit int is native, so there's no speed advantage to using only 16 or 8 bits) and compute when do the firing. Then your loop code could just monitor the micros() time (while also checking for new data) and when it's the appropriate moment, do the firing operation, or whatever else needs to be done.

This sort of "do the math" approach is nice, because you can spew lots of numbers very rapidly to the Arduino Serial Monitor about what you're doing and check them manually with a calculator or perhaps software, or whatever other engine model applies to your project.

Does anyone have any experiences with making timing-critical applications on the teensy 3?
What are good approaches for doing time-critical operations? Is it possible to use some kind of interrupt along with the timer to trigger our outputs?

Yes, of course there are ways to do such things. The "FTM" timers in Teensy 3.0 have some pretty incredible features. But they're also fairly complex to use.

I'd highly recommend doing this the simplest possible way first (advice which could be applied to most software projects).

For the injectors we will have an output quite like a PWM signal, with "cycle time = 2 * RPM" and duty cycle according to our calculated fuel requirement.

This is only 267 Hz. If you do it the simplest way, software looping checking micros(), you might have slight errors in the range of several microseconds, but to keep that in perspective, a few microseconds compared to 267 Hz is 0.01% error.

Could we utilize most of the arduino-compatible libraries without losing to much performance?

Yes, probably. Far better to get it working first, then optimize.

Especially for an academic project where a hard deadline trumps all else, any somewhat working result early are far better than nothing working at the deadline!


Regarding the approach in message #2, I would recommend starting with a far simpler approach that makes minimal use of interrupts. I'd also recommend starting with just a single sensor and try to implement only a tachometer first. In fact, first just acquire the micro() time in the attachInterrupt called function, and have your main program print it: extremely simple. Just do that first and get a stream of numbers printing on your screen, one for each time the sensor detects a change. Then build from that point, computing elapsed time, RPM, etc. Then compute target times, then add in code to act upon those.

Save your work along the way, kind of like save/restore points in some video game. If your time runs out, as it does in virtually all student projects, the more points where you saved something that actually worked, the more impressive your academic writeup will be.

Interrupts, and especially timer interrupts, can be a seductive trap for student projects. Or for any projects, for that matter. At first, it seems like interrupts are a really useful building block upon which to base your project. Indeed truly amazing things can be done with them. But they are a difficult tool to wield properly. Aside from all the thorny issues of thread safety for sharing data, interrupts add tremendous complexity to any project that makes debugging far more difficult.

I would highly recommend using just 1 interrupt with attachInterrupt, and in the interrupt handler, at least initially, limit yourself to only acquiring the micros() timestamp when the interrupt occurred, and set a flag so your loop() can tell when the timestamp changes. Do as much as possible in your main program. Try to do as little as possible in interrupts.

At the very end, when things are working but you're getting software-based jitter, that's when you should consider interrupts for other stuff. By that point, you'll have a lot more experience, and a lot of working code built up. Both of those will guide you towards making far more effective use of other interrupts than you could hope to do at the beginning of the project.


I should qualify this advice somewhat, that it really applies to controlling under about 60000 RPM (or about 1 millisecond per rev). Teensy3 has some amazing hardware features. There are timers with very advanced features, even incredibly fast DMA channels. A libraries like OctoWS2811 (controlling 3000+ LEDs at video refresh speed) is a good example where you must leverage such hardware to achieve incredible speeds. But as you can see from the 1200+ page manual, these hardware features are quite complex.

Don't make your project hardware and more complex than necessary. Start simple. And whatever you do, save often, whenever you have working incremental progress. Virtually all student projects run out of time, so plan for a chain of incremental progress that demonstrates you deserve a top grade!
 
What can I say Paul, thanks for putting things into perspective!

Reading your reply I realized I've probably been confusing micros and millis quite a lot when calculating precision demands.
Your advice sure makes a lot sense. I was thinking that this was some kind of really high-computational hyper-precision thing, when in reality it's really not. There's really not that much computation that needs to be done(fuel amount from one quite simple formula, basically).
Everything else is pulled from tables, requiring close to no computation at all. I guess I've sorta lost track of the capabilities of this new hardware when reading about earlier implementations with 10MHz microcontrollers that hasd 8 bit precision, and how they used all types of crazy techniques to squeeze the most of their hardware.

The only place where interrupts really make any sense after reading your comment is when reading the RPM with the hall effect sensor, since this is crucial for correct operation (this gives us both RPM and time of cylinder one at top dead center, due to the missing tooth in the trigger-wheel).

Surely it is better to get something working, and then tweak things as necessary when things start coming together. You really put the whole injection timing thing into perspective. That will for sure be a piece of cake compared to ignition timing. That is where timing is crucial.
We really should be able to achieve +/- one degree precision, but even at 8000 RPM that will still be 20 microseconds. That should be no problem at all.

Making things more complex than necessary is for sure not a good idea. There sure will be enough work to keep us busy. We're looking at an extensive android application, a java application for the computer, communication between these and the teensy, and a whole lot of hardware interfacing. Add to this a shitload of documentation, meetings with our counselors, classes, and general issues that tend to appear, and we're looking at one stressing semester.

Thanks Paul!
 
kis for efi project

Most of the first 2 decades of fuel injection cars used the signal from the distributor. If the motor has one, save the effort of a timing wheel and decoding. The precision is plenty good enough for common cars. That's the simplest way to do do it. kis = keep it simple, stupid


What can I say Paul, thanks for putting things into perspective!

Reading your reply I realized I've probably been confusing micros and millis quite a lot when calculating precision demands.
Your advice sure makes a lot sense. I was thinking that this was some kind of really high-computational hyper-precision thing, when in reality it's really not. There's really not that much computation that needs to be done(fuel amount from one quite simple formula, basically).
Everything else is pulled from tables, requiring close to no computation at all. I guess I've sorta lost track of the capabilities of this new hardware when reading about earlier implementations with 10MHz microcontrollers that hasd 8 bit precision, and how they used all types of crazy techniques to squeeze the most of their hardware.

The only place where interrupts really make any sense after reading your comment is when reading the RPM with the hall effect sensor, since this is crucial for correct operation (this gives us both RPM and time of cylinder one at top dead center, due to the missing tooth in the trigger-wheel).

Surely it is better to get something working, and then tweak things as necessary when things start coming together. You really put the whole injection timing thing into perspective. That will for sure be a piece of cake compared to ignition timing. That is where timing is crucial.
We really should be able to achieve +/- one degree precision, but even at 8000 RPM that will still be 20 microseconds. That should be no problem at all.

Making things more complex than necessary is for sure not a good idea. There sure will be enough work to keep us busy. We're looking at an extensive android application, a java application for the computer, communication between these and the teensy, and a whole lot of hardware interfacing. Add to this a shitload of documentation, meetings with our counselors, classes, and general issues that tend to appear, and we're looking at one stressing semester.

Thanks Paul!
 
Most of the first 2 decades of fuel injection cars used the signal from the distributor. If the motor has one, save the effort of a timing wheel and decoding. The precision is plenty good enough for common cars. That's the simplest way to do do it. kis = keep it simple, stupid

Quite a necro-bump ;-) But apart from that, it all got solved in the end :)

Timing did not end up being a problem. We used interrupts for reading the trigger wheel sensor on the cranck shaft, and we just freewheeled the main loop, polled the time, and activated/deactivated the outputs according to planned on/off time. At the end of the loop we performed the necessary calculations for the things that had now been turned on or off.

The most important lesson: make sure there's no floating point calculations. That took waaaay to long and messed up our timing. Using only integer maths gave us a bit of rounding errors, so there was some juggling with the order of operations performed needed to get a decent resolution. In the end we had about 0.2 degree rounding errors at 6000 RPM. More than good enough.

The second most important lesson: Oscilloscope for debugging. We had one dedicated output on the teensy we used only for debugging the timing issues. Having an oscilloscope allowed us to measure the on and off time on the injectors etc, and was absolutely necessary to get anything working.

We are now in the process of making a mark2, with printed circuit boards, improved timings, better GUI on the computer, improved android app for using a tablet as dash-logger, etc. This mark2 will be used on a 400bhp drift car. It'll be really interesting ;-) if there's any questions; fire at will :)

PS: I expect there'll be a code drop in the not to distant future. The java-bits need some cleanup first though, or we'll look like complete amateours (damn GUI builders, nice when you're time-restricted, but not afterwards).
 
hellandsen,

I am considering a similar but much smaller and less complicated project and would like to look at your code, if possible.

The project is mapped timing: 2d lookup of advance. (much simpler than full fuel injection). Distributor input (hall sensor on interrupt), MAP (analog input for load) and a single output to dwell and fire the coil. The goal is to replace the original ignition module with this which has more 'smarts' than the original module.

Keith
 
Hi, Keith

Well, first of all, making a ignition advance controller as you are talking about won't be to much trouble. You only need two inputs, MAP and distributor input, as you said. The MAP is just a linear pressure sensor (which I'm sure you're aware), and since you're using a distributor you only need to calculate angular velocity (and thus RPM) from the distributor input with no regard to the engines absolute position (since the distributor will handle what spark plug gets which spark).
Here is an example of how we once upon a time decoded the trigger input (some changes has been made, but the essence is the same). Keep in mind we used a missing tooth trigger wheel and had no distributor (we run COP and wasted spark), and thus needed to know the absolute position of the engine to know which coil to fire

Code:
void TriggerHandler::trigger_ISR(){
	currentTimeMicro = micros();
	currentIntervalTime = currentTimeMicro-lastTimeMicro;
	lastTimeMicro = currentTimeMicro;

	unsigned long smaller;
	unsigned long larger;

	if(currentIntervalTime>lastIntervalTime){
		larger = currentIntervalTime;
		smaller = lastIntervalTime;
		thisLargerThanLast = true;
	}
	else{
		larger = lastIntervalTime;
		smaller = currentIntervalTime;
		thisLargerThanLast = false;
	}
	lastIntervalTime = currentIntervalTime;

	int tolerance = (5*smaller)/10;


	if(larger<(smaller+tolerance)){
		currentPair = MatchedPair;
		outOfTolerance = false;
	}
	else if(lastWasWideNarrow){
		if(larger<(((smaller*13)/10)+tolerance)){
			currentPair=MatchedPair;
			outOfTolerance=false;
		}
	}
	else{
		int missingWidth = (smaller*((missingTeeth*10)+10))/10;
		int afterMissingWidth = (smaller*(100))/100;
		
		if(thisLargerThanLast){
			if((larger<=(missingWidth+(tolerance*2))) && (larger > (missingWidth-(tolerance*2)))){
				currentPair=NarrowWide;
				outOfTolerance=false;
			}
			else{
				Serial.print("not smaller which is prob. Larger:   ");
				Serial.print(larger);
				Serial.print("    Smaller:  ");
				Serial.println(smaller);
				outOfTolerance=true;
			}
		}
		else{
			if((larger<=(afterMissingWidth+(tolerance*2))) && (larger > (afterMissingWidth-(tolerance*2)))){
				currentPair=WideNarrow;
				outOfTolerance=false;
			}
			else{
				Serial.print("smaller which is prob. Larger     ");
				Serial.print(larger);
				Serial.print("    Smaller:  ");
				Serial.println(smaller);

				outOfTolerance=true;
			}
		}
		
	}

	if(!outOfTolerance){
		if(currentPair ==NarrowWide){
			if(currentTooth!= totalTeeth-missingTeeth){
				Serial.print("NAr ikkje rett antal tenner fQr ny runde:   ");
				Serial.println(currentTooth);
			}
			currentTooth = 1;

			if(!camTriggered&&hasCamSensor){
				camRound=false;
				camLessCount++;
			}

			else if(camTriggered&&hasCamSensor){
				camRound=true;
				camTriggered=false;
				camLessCount=0;
			}
			else{
				camLessCount++;
			}

			if(camLessCount>3){
				camRound=true;
				hasCamSensor=false;
				camLessCount--;
				camLessCount--;
			}
			else{
				camRound=false;
			}
		}
		else if(lastWasNarrowWide && currentPair == WideNarrow){
			currentTooth = 2;
			if(hasPreviousSyncPoint==true){
				unsigned long timeDifference = (currentTimeMicro-lastSyncTime);
				triggerVars->realTimeVariables[RPM] = (60000000/timeDifference);
				Serial.print("Current RPM:    ");
				Serial.println(triggerVars->realTimeVariables[RPM]);
			}
			lastSyncTime = currentTimeMicro;
			hasPreviousSyncPoint = true;
		}
		else{
			currentTooth++;
		}
	}
	else{
		Serial.println("Out of tolerance");
		hasPreviousSyncPoint=false;
	}

	if(currentPair!=NarrowWide){
		lastWasNarrowWide=false;
	}
	else{
		lastWasNarrowWide=true;
	}
	if(currentPair!=WideNarrow){
		lastWasWideNarrow=false;
	}
	else{
		lastWasWideNarrow=true;
	}

And then you just need to check regularly if you need to turn on the coil (ignition advance in the cell the engine is currently in subtracted dwell time) and turn off the coil (the right amount of ignition advance has been met -> SPARK)

Other things to consider is if you want to hard-code the values in the 2d table in the teensy program, or if you want to control it from a computer (via serial communication) and store the values in EEPROM (so you don't have to load the values each time you start the car)

And one last thing: you need an appropriate circuit to charge and fire the coil, since it requires quite a bit of current and makes some quite high voltage peaks when it fires (>100V). We have up until now used the Bosch 124 (which is a dumb ignition coil driver), but for the next car I will be using a set of ISL9V5036P3's. I can post how that goes if anyone is interested.


Best regards,
Emil, one of the three guys who made the first version of the Teensy EMS, and the guy who has continued working on it since.


Edit: And sorry for the late reply, hope you've made some headway without us (it ain't rocket science)
 
Last edited:
Status
Not open for further replies.
Back
Top