Arduino, Electronics and Programming

CAT C4.4 Diesel Engine data logging and alarm system with HMI

(This article contains exerpts from my final thesis and it will be shared in its entirety, once time allows for full disclosure. The full report can be obtained by sending me a request.)

For a two-year study in electronics and embedded programming, as my final thesis, I have chosen to design and build an ECU and HMI for controlling/monitoring a Caterpillar C4.4 diesel engine.

Caterpillar C4.4 diesel engine

The engine was originally meant to be used as a genset engine, but ended up being installed in a fishing boat as a prime mover for the propeller. The engine comes with no electronic controls, so it was a perfect candidate for a custom built system.

There are two parts to the system, the ECU which collects sensor data, and the HMI which logs the data, visualizes it and features an alarm system with high and low limits.

The system consist of hardware and software. The hardware consists of two units, the ECU and a separate HMI unit which handles visualization, alarms and logging. The ECU reads data from engine sensors at a regular interval and sends the measured data to the HMI unit.

The software is written using a mix of C/C++ and Python. The engine sensor interfacing is programmed in C/C++ and the HMI is programmed using Qt4 Designer, PyQt4 and Python. The Arduino ecosystem provides a great number of tested and easy to use libraries, so instead of reinventing the wheel, open source software tools are used in order to speed up development. The Illustration below shows a diagram of the system.

System overview
System overview


The system collects the following data from the engine:

  • Cool water temperature (NTC)
  • Lubrucation oil temperature (NTC)
  • Engine room temperature (NTC)
  • Exhaust temperature (K-type TC)
  • Battery voltage
  • Brake power output (calculated)
  • RPM
ECU board version 0.3
ECU schematic v0.3

Microcontroller and RPM input

The brain of the ECU is an Atmel ATMega328P 8 bit microcontroller. I chose this processor because of familiarity, ease of coding, community support via the Arduino ecosystem and because it does the job. Another candidate was the ATMega32 which has an extra port and 2 extra ADC inputs and provides more options for future expansion. It requires a larger footprint and is a little more expensive but most importantly, to my knowledge, does not offer the same support from the Arduino compiler out of the box because it is not part of the Arduino product line. The 328P is sufficient, so I chose to go with what I know to be working. The RPM optocoupler is shown in illustration 4.1, connected to PD2 because the pin is external interrupt 0. The output from one of the generator windings is routed to the pin 1 on the optocoupler and pin 2 goes to ground. On the rising edge of the positive sine wave, the optocouplers internal LED will emit light and the interrupt pin on the MCU is pulled low.


The ECU continually sends engine data at 5 Hz. The embedded Linux device parses the incoming data, performs any data manipulation required, logs data and check for alarms and visualizes on screen. Data is displayed on 10 inch Beetronics 1980×1080 display with a touch interface.

HMI display

The HMI is programmed in Python by using Qt4 and PyQt4 on a Raspberry Pi 3 running Ubuntu Mate 16.04. The implementation of the graphical interface will not be described in detail.

The yellow windows is a console that displays system messages and alarms. When an alarm is active, the window changes color to red and a buzzer sound is activated from an external buzzer. Alarms are acknowledged by pressing the acknowledge button. The console is cleared by pressing the clear console button.

The green communication button in the left corner inducates the status of communication between the ECU and HMI. If communication is lost, the button turns red.

Alarm system

The alarm system is built by instantiating an alarm object for each ECU parameter that requires an alarm. Each object must have a low limit and high limit. At a specified interval, the incoming values from the ECU are checked if they are outside the specified range for each alarm object.

If a parameter is found to be outside the specified range, the yellow console window will turn red, an appropriate alarm message is displayed in the console and an alarm sound will start and continue until the Acknowledge button is pressed, after which the alarm sound will stop. The console window will remain red until the parameter is within the range specified in the alarm object.


Data logging is not performed at every received data event. Logging 5 times per second is unnecessary and would put needless wear on the SD card, fill up card space way too quickly and provide needlessly large data sets. Data is appended a date and time stamp

Conditions for logging:

  • Engine RPM must be more than 0
  • Logging interval is every 5 seconds

Engine temperature sensor interfacing

To understand how to interface to the sensors mounted on the engine, the first step was to figure out which type of sensors were used by Caterpillar. As the C4.4 engine was produced for industrial applications as a generator set, it turns out it was not fitted with temperature and pressure sensors, but instead temperature and pressure switches. The difference being that a switch opens or closes at a temperature or pressure for which the switch is designed. This meant that in order to get real temperatures from the engine’s cooling water, engine oil and exhaust, sensors had to be purchased and an adapter had to be manufactured to make it fit on the engine.

Engine cool water and oil temperature sensors are usually a simple thermistor or NTC (negative temperature coefficient), for example a 10k (@ 25 °C) resistor whose resistance drops significantly with increasing temperature, although in a nonlinear fashion. This is described in part in the electronics study’s textbook “Practical electronics for Inventors” by Paul Scherz and Simon Monk. Using such a sensor made a convenient way to interface it with a microcontroller using a simple voltage divider.

Engine pressure sensors actually actually work in much the same way. This type of sensor outputs a varying voltage between 0.5 V and 4.5 V. It works much like a potentiometer where a mechanical arm contacts a point on a half circular wire winding and thereby outputs a varying resistance as the arm moves across the winding. The winding is constructed in such a way that the pressure sensor will output a voltage that is linear to rising pressure.

The choice of NTC fell on a temperature sensor for a Toyota Aygo 1.0L gasoline engine. These are cheap (100 Kr) and easily accessible from multiple vendors. The 12×1,5mm threading on the sensor did not fit the threading on the engine, so a custom adapter was manufactured. No data sheet was available for the sensor, so some investigation had to be done to document the sensors characteristics.

NTC sensor used for cool water and oil temperature.

Defining the automotive temperature sensor characteristics

The negative temperature coefficient sensor does not have a linear relationship between resistance and temperature, so calculating temperature is not as simple as using the straight line equation y=ax+b. For thermistors, the temperature is calculated using an alternative model instead of the more precise third order Steinhart-Hart equation. See “Practical electronics for inventors” for more information. The alternative model:

eq 510 (5.1.0)

Rearranging 5.1.0 to find R at a given temperature when we have calculated the sensors beta value leaves the formula

eq 511(5.1.1)

The beta value is found in the datasheet for the sensor, but because the datasheet is not available, the beta value will have to be calculated. The temperature range of the medium which the sensor will measure is from 0 to 100 degrees C. To perform a calculation of the beta value for the sensor, an ice bath and a stove can be used to reference values 0 and 100 degrees. Temperature is given in Kelvin degrees in the formulas.

So, by having two points on the T/R curve, a beta value can be calculated. The formula for calculating the sensors beta value is found by rearranging formula 5.1.0 to give β:

eq 512(5.1.2)

Now we can calculate the beta value for the NTC which will be used to measure the cooling water temperature. The NTC forms part of a voltage divider circuit like shown in illustration 5.1.

Thermistor Vdiv.png

Gathering data for temperature sensor T/R characteristics curve in illustration 5.2 was done by placing the sensor in a water bath while not touching the bottom and heating it while stirring and recording values of resistance and temperature at different temperatures after the temperature had stabilized. The temperature reference probe was a calibrated K-type thermocouple and a UNI-T 325 temperature meter. The construction of the thermocouple was done in a thinner material compared to the NTC, so the reaction time from a change in temperature to the temperature probe actually measuring it would be smaller for the thermocouple. Because of this, the temperature stabilized at the respective temperatures before the value was noted.

RvsT curve ill52.png

The measured values which this curve is based on are shown in appendix 2.
When calculating the beta value using the measured data, the result will not be absolutely correct due to imprecision in the measured values, but close enough. I will use 3 values for calculating the beta value to check that the result is in the ball park. I will use 21,3 C as the T 0 and perform 3 calculations at 55, 83 and 100 degrees C. If the result is the same or close, the value is good.


After having researched various NTC’s I have found that a typical beta-value is 3450. The results from these caltulations leads me to believe that this is also the beta value for this type of sensor, so 3450 will be used in the calculations in the software.

Choosing the optimal fixed value resistor

Since the interface is a simple voltage divider circuit as shown in illustration 5.1, the fixed value resistor should be chosen with care to get the widest voltage range on the output and therefore the widest use of the ADC’s range. To establish the optimal value, calculations at different values of R1 have been performed and a curve has been plotted in illustration 4.4 to give a rough estimate of the curve top point which represents the maximum voltage difference in the temperature range.

diff R1 val vs dv

Illustration 4.4 shows that the resistor that gives the largest use of the ADC’s range is around 600 ohms. A 680 Ohm resistor will be used in the voltage divider circuit.

The output voltage when using different resistors was calculated using resistance at 25 and 100 degrees by applying Ohm’s law:

ohms law

The NTC interface circuit is shown in the illustration below.

NTC interface
NTC interface circuit

Counting engine RPM

This can be cone in several ways. A common methos is to use a pickup, which is either a capatitive or magnetic sensor that senses when the teeth of a gear or flywheel passes by like shown in the illustration below.

Speed/Injection timing wheel with pulse train output

Since there is no pickup on the engine used for this project, the method I have chosen is to use the frequency of the electrical generator. The generator frequency is proportional to its rotational speed and its rotational speed is fixed at a certain ratio to the engine’s rotational speed (not considering possible slip due to high load and a loose pulley belt).

If the ratio is 1:3 engine/generator, when the engine is running 1000 RPM, the generetor will run 3000 rpm and produce a square wave output on the generator’s W output.

The maximum rated speed of the engine is 3600 RPM, however, it will most likely never go that high under load, as it is overpowered by a large margin compared to the vessel hull size.

The maximum frequency received on the optocoupler input at a generator/engine ratio of 3:1 would be

Equation 5.3.2

The PC817 optocoupler can easily handle this input frequency.

Yellow trace AC input, blue trace enternal interrupt signal to MCU

The image above shows the generator sinewave in yellow and the pull-up signal on the MCU input pin. The signal on the MCU pin is held high by the internal pull-up resistor and when a positive voltage on the sinewave triggers the optocoupler, the MCU pin signal is pulled low, triggering an interrupt. The code then measures the time between two interrupts which equals a cycle period. And the RPM can then normally be calculated by

Equation 5.3.3

However, because of the generator/engine ratio, it is necessary to divide by the generator ratio also, so in this application

Equation 5.3.4


The communication system is set up so that the ECU sends a serial string with all the values shown in the table below every 200 ms. This action is controlled by Timer1. The protocol also allows commands to be sent to the ECU to get information from it. Commands sent to the ECU is sent as ASCII. Table 6.2 shows commands and responses. At each cycle through the main loop in the ECU code, the serial port is checked for incoming commands. If a command is present, it is parsed and appropriate action is taken. If an invalid command or parameter value is received, an error character ‘#’ is returned from the ECU.

The physical layer is handled by an RS232 connection, which allow device to device communication at speeds up to 115200 baud, however high speeds come at a price of increased transmission errors and cable length also play a factor in maximum possible baud rate. The RS232 “standard” has a maximum cable length of 15 meters which makes it suitable for the purpose of this project where maximum 5 meters is needed.

ECU to HMI comma separated serial string format

Serial data string

ECU sensor interface modules

  ECU software algorithm

ECU algorithm
ECU software algorithm

The illustration above shows the ECU software algorithm. Timer 1 sets the measureSensors flag high every 200 ms. When the flag is high, the routine for measuring the sensors is run once and the measureSensors flag is reset. If any serial command is received, the command is processed.

Initialization code

#include <TimerOne.h>
#include <EEPROMex.h>
const int maxAllowedWrites = 20;
const int memBase = 120;
#include <SmoothThermistor.h>
#define RPM_PIN 2
#define BOARD_PIN A4
#include <max6675.h>
int thermoCLK = 13;
int thermoCS = 11;
int thermoDO = 12;
MAX6675 Exhaust_Temp(thermoCLK, thermoCS, thermoDO);

// State flag
bool MeasureSensors = false;
double EXHAUST_TEMP = 0.0;
uint8_t EXH_COUNTER = 0;
uint16_t RPM_ARRAY[5] = {0,0,0,0,0};
volatile uint16_t RPM;
volatile unsigned long TIME1;
volatile unsigned long TIME2;

Setup code

void setup() {

 attachInterrupt(digitalPinToInterrupt(2), RPMCOUNTER, RISING);

Main loop code

void loop() {

    if (Serial.available())

      MeasureSensors = false;
      float CW_TEMP = CW_Temp.temperature();
      float OIL_TEMP = Oil_Temp.temperature();
      float ENGINE_ROOM_TEMP = Engine_Room_Temp.temperature();
      uint16_t avarage_rpm = AvgRpm(RPM);
     if(EXH_COUNTER >=2)
        EXH_COUNTER = 0;
        EXHAUST_TEMP = Exhaust_Temp.readCelsius();
        EXH_COUNTER +=1;
        float OIL_PRESSURE = getPres(OIL_PRESSURE_PIN);
        float BATTERY_VOLTAGE = getBatteryVoltage(BATTERY_VOLTAGE_PIN);
        attachInterrupt(digitalPinToInterrupt(2), RPMCOUNTER, FALLING);

The interrupts are enabled at the end to enable measuring an RPM measurement. The exhaust counter is used to speed up measurements by not updating the exhaust temperature each cycle. This was necessary due to the MAX6675 chip requiring more time between each measurement than the other sensors and gave erratic values if read each cycle.

Execution Interval

void intervalPassed(void)
    MeasureSensors = true;

Temperature sensors

Each temperature sensor which has an object oriented library has its own object instantiated. The 3 NTC’s and the thermocouple.

SmoothThermistor CW_Temp(A0, ADC_SIZE_10_BIT, 1975, 680, 3450, 25, 5);
SmoothThermistor Oil_Temp(A2, ADC_SIZE_10_BIT, 1975, 680, 3450, 25, 5);
SmoothThermistor Engine_Room_Temp(A1, ADC_SIZE_10_BIT, 10000, 9909, 3950, 25, 5);
int thermoCLK = 13;
int thermoCS = 11;
int thermoDO = 12;
MAX6675 Exhaust_Temp(thermoCLK, thermoCS, thermoDO);

The values passed to the object are(pin, ADC resolution, RT, R1, Beta, T, number of samples to average)

Counting engine revolutions per minute

Counting RPM can de done by in different ways. One method is to count pulses in a certain time span, for example one second. The number of pulses is equal to the frequency. However since the update rate is 5 times per second, this would create some problems. There are many ways of achieving this goal and I wanted to limit the use if interrupts during the execution of “MeasureSensors” code.

After pondering about a smarter way of achieving this, I decided on the following solution. It measures only a single cycle whici is all that is needed to determine the input frequency and then RPM.

void RPMCOUNTER(void)
 if(!TIME1)  // On the first rising sinewave
   TIME1 = micros();
 else       // On the second rising sinewave
   TIME2 = micros();
   int T = TIME2 - TIME1;
   RPM = 1/(T*0.000001) * 60/3;
   TIME1 = 0;
   TIME2 = 0;

After the “MeasureSensors” code is run, the external interrupt is turned on and the period of a single cycle is measured in microseconds. A frequency and RPM can now be calculated, including the gen/eng ratio.

Some filtering on the RPM is performed by a moving average function.

uint16_t AvgRpm(int rpm)
 int i, avg_rpm=0;
   RPM_ARRAY[i] = RPM_ARRAY[i-1];
 RPM_ARRAY[0] = rpm;
 // Alternative way

   avg_rpm +=RPM_ARRAY[i];

Battery voltage

float getBatteryVoltage(uint8_t aPin)
   int i,raw=0;
   raw += analogRead(aPin);
   raw /= 3;
   float battery_voltage = map(raw,747, 828, 11600, 13185.00) * 0.001;


Alarm module (Python)

The HMI features an alarm system which tells the user if engine parameters are outside a specified range. For example, if cool water temperature increases beyond a preset maximum value, the yellow console window turns red, an alarm message is printed in the console to identify which parameter is outside the defined range and an alarm will sound. By pressing the acknowledge button, the alarm sound will stop, but the console window will stay red until the parameter returns to within the defined range. The cooling water, this could be, for example -10 to 95 degrees C.

A basic alarm object was created that can check if a parameter is outside its defined range and raise a flag if it is the case. The limit flags are routinely checked by Qtimers and an alarm status byte is updated which contains the status of all alarm. Both limits are not used for all parameters.

The implementation of the alarm system is done by checking if values are outside their specified range at a regular interval by running the Alarm.checkAlarmLimits(value) for all alarms.

An alarm status byte is created which has a bit for each alarm. In the event of a parameter being outside its range, the respective bit is set in the alarm status byte. After checking all alarm limits, the byte is checked to see if any bit is set. If no bit is set, no action is taken. If a bit IS set, an alarmHandler function is executed and each bit is checked and an alarm message corresponding to the alarm bit is printed. Below is instantiation code for cool water and oil temperature alarm objects and timers and the function that checks for alarms and sets a bit if an alarm condition is reached.

Alarm object and timers init (2 examples)

self.cw_temp_alarm = alarm.Alarm("CW_temp", 0, 88)
self.cw_temp_alarm_bit = 0x0
self.cw_temp_alarm_timer = QtCore.QTimer()

self.oil_temp_alarm = alarm.Alarm("OilTemp", -10, 95)
self.oil_temp_alarm_bit = 0x1
self.oil_temp_alarm_timer = QtCore.QTimer()

Check for alarms code

def checkAlarms(self):
  self.alarmControlByte = 0x00
  # 1
  if self.cw_temp_alarm.highLimitReached:
    self.alarmControlByte |= (1 << self.cw_temp_alarm_bit)
    self.alarmControlByte &= ~(1 << self.cw_temp_alarm_bit)
  # 2
  if self.oil_temp_alarm.highLimitReached:
    self.alarmControlByte |= (1 << self.oil_temp_alarm_bit)
    self.alarmControlByte &= ~(1 << self.oil_temp_alarm_bit) 
  if self.alarmStatusByte:
    GPIO.OUTPUT(self.ALARM_PIN, GPIO.HIGH) // start alarm sound
  if not self.alarmStatusByte:
    self.ui.textEdit.setStyleSheet(_fromUtf8("background-color: rgb(255, 255, 127);"))

Alarm handler code

def alarmHandler(self):
  if self.alarmStatusByte:
    time = strftime("%Y-%m-%d,%H:%M:%S,", localtime(None))
  # if CW alarm bit is set and
  if self.alarmStatusByte & (1 << self.cw_temp_alarm_bit) and (not (self.alarmAcknowledgeByte & (1<< self.cw_temp_alarm_bit))):
    self.ui.textEdit.setStyleSheet(_fromUtf8("background-color: rgb(255, 0, 0);"))
    self.ui.textEdit.insertPlainText("\n"+time+"\n ALARM\nCOOL WATER TEMPERATURE IS TOO HIGH!\n")
    log.write_error_log(str(time+"CW TEMP HIGH ALARM,"+str(self.cool_water_temp)))
  if self.alarmStatusByte & (1 << self.oil_temp_alarm_bit) and (not (self.alarmAcknowledgeByte & (1 << self.oil_temp_alarm_bit))):
    self.ui.textEdit.setStyleSheet(_fromUtf8("background-color: rgb(255, 0, 0);"))
    self.ui.textEdit.insertPlainText("\n"+time+"\n ALARM\nOIL TEMPERATURE IS TOO HIGH!\n")
    log.write_error_log(str(time+"OIL TEMP HIGH ALARM,"+ str(self.oil_temp)))



The photo above shows the complete system under test. The HMI screen, the big box containing the Raspberry Pi, power supplies and so on, and the small box containing the ECU with sensors attached.


More co tome…