How to use pull up resistors to read two tacho signals individually?

Hello everyone!

I want to read in the tacho signal of two pwm-fans using the Arduino Nano 33 IoT. To have a proper digital signal, I am using pull up resistors. However, because I've got two tacho signals I think I need two pull up resistors. I've already read quite a lot about pull up resistors but unfortunately I still don't feel confident with the topic so far yet.

The four-wire segments on the right are the two fans (yellow=tacho; blue=pwm).

The pull up resistors in the circuit are connected in parallel and after doing the math the total resistance should be 500 ohm. When I experimented with only one fan I've got good results using a 470 ohm resistor for the pull up.
Also, for the power supply I'm using 12V DC (2A) to power both fans as well as the arduino via the VIN-Pin.
Controling the fans speed via PWM is working fine using the "SAMD21turboPWM" library. I am using the WiFi-Module to connect to a MQTT-Server, as well.
Another thing to mention would probably be, that I want to keep the circuit as small as possible and I won't have any extra long wires. Also, the Arduino should be able to run 24/7.

Is this setup working out or are there any mistakes, errors or even things I can improve?

Thank you very much in advance!

I beg to differ. They are connected together on the lefthand ends and go to 3V3, but the righthand ends go to different pins on the board so they are not in parallel. In any case , 1K resistors are far too low in value, let alone 470 Ohms

Why not use INPUT_PULLUP in the pinMode() for the pins and activate the built in pullup resistors and eliminate the need for external resistors ?

Please post your full sketch that goes with the schematic, using code tags when you do

Oh okay, than this is my mistake by assuming, that they are connected in parallel. Thanks for the enlightenment!

I tried using the INPUT_PULLUP mode first, but I did get quite noisy results when trying to calculate the rpm values. Also, I couldn't find out if the Arduino Nano 33 IoT does have built in pullup resistors because I can't really read the schematics of it. As you've already noticed, I am not very good with electronics :smiley:

Here is my full code:

#include <SAMD21turboPWM.h>
#include <WiFiNINA.h>
#include <PubSubClient.h>
#include "Credentials.h"

// define pins and timers
#define TACHO_PIN_A 9
#define TACHO_PIN_B 3
#define PWM_PIN_A   7
#define PWM_PIN_B   4
#define PWM_TIMER   1

// define variables to calculate RPM
unsigned long lastTachoInterrupt_A = 0;       // in ms
unsigned long tachoInterruptDuration_A = 0;   // In ms
unsigned long rpm_A = 0;
unsigned long lastTachoInterrupt_B = 0;       // in ms
unsigned long tachoInterruptDuration_B = 0;   // In ms
unsigned long rpm_B = 0;

int initializePwmValue = 500;

// define variables to connect with the mqtt server via WiFi
const char* ssid = networkSSID;
const char* password = networkPASSWORD;
const char* mqttServer = mqttSERVER;
const char* mqttUsername = mqttUSERNAME;
const char* mqttPassword = thingKEY;
const char* mqttDeviceId = thingID;
const int mqttPort = 1883;

const int publishingRate = 1000;  // rate to publish in ms

// define mqtt pub/sub topics
char pwmSubTopic_A[] = "pwmDcControl_A";
char rpmPubTopic_A[] = "rpmState_A";
char pwmSubTopic_B[] = "pwmDcControl_B";
char rpmPubTopic_B[] = "rpmState_B";

// define global variables for mqtt calculations and/or operations
long lastMsg = 0;

TurboPWM pwm;
WiFiClient wifiClient;
PubSubClient client(wifiClient);

void setup_wifi()
{
  delay(10);

  Serial.println();
  Serial.print("Connecting to wifi ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }

  randomSeed(micros());

  Serial.println("");
  Serial.println("WiFi connected.");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

void reconnect()
{
  while (!client.connected())
  {
    Serial.print("connecting to mqtt...");

    if (client.connect(mqttDeviceId, mqttUsername, mqttPassword))
    {
      Serial.println("connected.");
      client.subscribe(pwmSubTopic_A);
      client.subscribe(pwmSubTopic_B);
    }
    else
    {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}

void callback(char* topic, byte* payload, unsigned int len)
{
  Serial.print("topic: ");
  Serial.println(topic);
  Serial.print("message: ");
  for (int i = 0; i < len; i++)
  {
    Serial.print((char)payload[i]);
  }
  Serial.println();

  char tmpBuffer[128];
  memcpy(tmpBuffer, payload, len);
  tmpBuffer[len] = '\0';

  // Convert it to integer
  char *end = nullptr;
  long value = strtol(tmpBuffer, &end, 10);

  // Check for conversion errors
  if (end == tmpBuffer)
  {
    Serial.println("Cannot convert payload to integer");
    return; // Conversion error occurred
  }
  int pwmValue = (int)value;
  // Clamp value between 0 and 1000
  if (pwmValue > 1000) {
    pwmValue = 1000;
  }
  else if (pwmValue < 0) {
    pwmValue = 0;
  }

  const int start = (int)strlen(mqttDeviceId) + 1;
  const int endIdx = (int)strlen(topic);
  char normalizedTopic[128];
  memcpy(normalizedTopic, &topic[start], endIdx);

  // set pwms duty cycle to the input value
  if (strcmp(normalizedTopic, pwmSubTopic_A) == 0) {
    pwm.analogWrite(PWM_PIN_A, pwmValue);
  }
  else if (strcmp(normalizedTopic, pwmSubTopic_B) == 0) {
    pwm.analogWrite(PWM_PIN_B, pwmValue);
  }
}

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  pinMode(PWM_PIN_A, OUTPUT);
  pinMode(TACHO_PIN_A, INPUT);
  pinMode(PWM_PIN_B, OUTPUT);
  pinMode(TACHO_PIN_B, INPUT);

  attachInterrupt(digitalPinToInterrupt(TACHO_PIN_A), tacho_A, FALLING);
  attachInterrupt(digitalPinToInterrupt(TACHO_PIN_B), tacho_B, FALLING);

  pwm.setClockDivider(1, false);
  pwm.timer(PWM_TIMER, 1, 0x3c0, false);
  pwm.enable(PWM_TIMER, true);
  pwm.analogWrite(PWM_PIN_A, initializePwmValue);
  pwm.analogWrite(PWM_PIN_B, initializePwmValue);

  setup_wifi();
  client.setServer(mqttServer, mqttPort);
  client.setCallback(callback);
}

void loop() {
  // put your main code here, to run repeatedly:
  if (!client.connected())
  {
    reconnect();
  }
  client.loop();

  Serial.print("RPM-A: ");
  Serial.print(rpm_A);
  Serial.ptint("\t");
  Serial.print("RPM-B: ");
  Serial.println(rpm_B);

  long now = millis();
  if (now - lastMsg > publishingRate)
  {
    lastMsg = now;

    char payLoad_A[4];
    itoa(rpm_A, payLoad_A, 10);
    client.publish(rpmPubTopic_A, payLoad_A);

    char payLoad_B[4];
    itoa(rpm_B, payLoad_B, 10);
    client.publish(rpmPubTopic_B, payLoad_B);
  }
}

void tacho_A() {
  unsigned long countMillis = millis();

  tachoInterruptDuration_A = (countMillis - lastTachoInterrupt_A);
  unsigned long freq = 1000 / tachoInterruptDuration_A;  // Frequenz in Hz
  rpm_A = (freq * 60) / 2;
  lastTachoInterrupt_A = countMillis;
}

void tacho_B() {
  unsigned long countMillis = millis();

  tachoInterruptDuration_B = (countMillis - lastTachoInterrupt_B);
  unsigned long freq = 1000 / tachoInterruptDuration_B;  // Frequenz in Hz
  rpm_B = (freq * 60) / 2;
  lastTachoInterrupt_B = countMillis;
}

Hi,

You won't see pull_up in the schematic of the board as they are actually inside the controller IC.

Thanks for producing clear circuit diagrams and using code tags.. :+1: :+1: :+1:

Tom.... :smiley: :+1: :coffee: :australia:

Ahh. Good to know. New thing learned. Thank you very much! :slight_smile:

With out a schematic It is hard to tell but so far it looks good. My preference is for external pull up resistors as the internal ones are not very stiff. The value of this internal pullup depends on the microcontroller used. On most AVR-based boards, the value is guaranteed to be between 20kΩ and 50kΩ. On the Arduino Due, it is between 50kΩ and 150kΩ. For the exact value, consult the datasheet of the microcontroller on your board. A common use for pull up resistors is for switches. Switch contacts oxidize over time and there resistance increases. Years ago we were taught to use at least 1mA through each contact to keep it "clean", that was more cost effective then contact cleaner. That forces a maximum resistance of 5K for 5V or 3300 for 3.3V so you are right on. The internal pullup if turned on will not hurt but can only help.

All variables updated inside ISRs should be declared volatile to prevent them being put into registers by the compiler which makes them unavailable to be updated under some circumstances

Thanks for the tip! So this refers only on these variables, right?

// define variables to calculate RPM
unsigned long volatile lastTachoInterrupt_A = 0;       // in ms
unsigned long volatile tachoInterruptDuration_A = 0;   // In ms
unsigned long volatile rpm_A = 0;
unsigned long volatile lastTachoInterrupt_B = 0;       // in ms
unsigned long volatile tachoInterruptDuration_B = 0;   // In ms
unsigned long volatile rpm_B = 0;

That looks right. Try it and see

Thanks, I will.
Another thought has come to my mind. The Input-Pins of the Arduino Nano 33 IoT can only use 3.3V as Input-Voltage. "Connecting more than 3.3V on IO pins will damage the board." (source: GitHub - ostaquet/Arduino-Nano-33-IoT-Ultimate-Guide: Arduino Nano 33 IoT - Ultimate guide).
Also, I've read that the tacho signal of fans usually output 5V (I don't have a measuring device to validate that). So do you think it is a good idea to use something like mosfets to reduce the voltage of the tacho signal to 3.3V?

It would certainly seem to be a good idea not to feed the Nano 33 input pins with 5V. Personally I would use a level shifter like this one SparkFun Logic Level Converter - Bi-Directional – Pimoroni or a simple resistor voltage divider

Thank you very much for your help. I will try everything out and post my results when I'm done! :slight_smile:

Someone supplied this info on the tacho signal used by most fans: They are usually open drain or open collector and so do not supply voltage since they sink current and so you must supply a pull-up resistor to your signal voltage. Also, the output is 2 pulses per revolution. I hope this helps you.

I organized myself a measurement device and found out, that the tacho signal do only output very very small voltage levels - peek values were around 50mV. It is just as @herbschwarz said. Thank you for your advice! :slight_smile:

I just tested everything and it works great!

Here's my updated code:

#include <SAMD21turboPWM.h>
#include <WiFiNINA.h>
#include <PubSubClient.h>
#include "Credentials.h"

// define pins and timers
#define TACHO_PIN_A 9
#define TACHO_PIN_B 3
#define PWM_PIN_A   7
#define PWM_PIN_B   4
#define PWM_TIMER   1

// define variables to calculate RPM
unsigned long volatile lastTachoInterrupt_A = 0;       // in ms
unsigned long volatile tachoInterruptDuration_A = 0;   // In ms
unsigned long volatile rpm_A = 0;
unsigned long volatile lastTachoInterrupt_B = 0;       // in ms
unsigned long volatile tachoInterruptDuration_B = 0;   // In ms
unsigned long volatile rpm_B = 0;

int initializePwmValue = 500;

// define variables to connect with the mqtt server via WiFi
const char* ssid = networkSSID;
const char* password = networkPASSWORD;
const char* mqttServer = mqttSERVER;
const char* mqttUsername = mqttUSERNAME;
const char* mqttPassword = thingKEY;
const char* mqttDeviceId = thingID;
const int mqttPort = 1883;

const int publishingRate = 1000;  // rate to publish in ms

// define mqtt pub/sub topics
char pwmSubTopic_A[] = "pwmDcControl_A";
char rpmPubTopic_A[] = "rpmState_A";
char pwmSubTopic_B[] = "pwmDcControl_B";
char rpmPubTopic_B[] = "rpmState_B";

// define global variables for mqtt calculations and/or operations
long lastMsg = 0;

TurboPWM pwm;
WiFiClient wifiClient;
PubSubClient client(wifiClient);

void setup_wifi()
{
  delay(10);

  Serial.println();
  Serial.print("Connecting to wifi ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }

  randomSeed(micros());

  Serial.println("");
  Serial.println("WiFi connected.");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

void reconnect()
{
  while (!client.connected())
  {
    Serial.print("connecting to mqtt.tingg.io...");

    if (client.connect(mqttDeviceId, mqttUsername, mqttPassword))
    {
      Serial.println("connected.");
      client.subscribe(pwmSubTopic_A);
      client.subscribe(pwmSubTopic_B);
    }
    else
    {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}

void callback(char* topic, byte* payload, unsigned int len)
{
  Serial.print("topic: ");
  Serial.println(topic);
  Serial.print("message: ");
  for (int i = 0; i < len; i++)
  {
    Serial.print((char)payload[i]);
  }
  Serial.println();

  char tmpBuffer[128];
  memcpy(tmpBuffer, payload, len);
  tmpBuffer[len] = '\0';

  // Convert it to integer
  char *end = nullptr;
  long value = strtol(tmpBuffer, &end, 10);

  // Check for conversion errors
  if (end == tmpBuffer)
  {
    Serial.println("Cannot convert payload to integer");
    return; // Conversion error occurred
  }
  int pwmValue = (int)value;
  // Clamp value between 0 and 1000
  if (pwmValue > 1000) {
    pwmValue = 1000;
  }
  else if (pwmValue < 0) {
    pwmValue = 0;
  }

  const int start = (int)strlen(mqttDeviceId) + 1;
  const int endIdx = (int)strlen(topic);
  char normalizedTopic[128];
  memcpy(normalizedTopic, &topic[start], endIdx);

  // set pwms duty cycle to the input value
  if (strcmp(normalizedTopic, pwmSubTopic_A) == 0) {
    pwm.analogWrite(PWM_PIN_A, pwmValue);
  }
  else if (strcmp(normalizedTopic, pwmSubTopic_B) == 0) {
    pwm.analogWrite(PWM_PIN_B, pwmValue);
  }
}

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  pinMode(PWM_PIN_A, OUTPUT);
  pinMode(TACHO_PIN_A, INPUT_PULLUP);
  pinMode(PWM_PIN_B, OUTPUT);
  pinMode(TACHO_PIN_B, INPUT_PULLUP);

  attachInterrupt(digitalPinToInterrupt(TACHO_PIN_A), tacho_A, FALLING);
  attachInterrupt(digitalPinToInterrupt(TACHO_PIN_B), tacho_B, FALLING);

  pwm.setClockDivider(1, false);
  pwm.timer(PWM_TIMER, 1, 0x3c0, false);
  pwm.enable(PWM_TIMER, true);
  pwm.analogWrite(PWM_PIN_A, initializePwmValue);
  pwm.analogWrite(PWM_PIN_B, initializePwmValue);

  setup_wifi();
  client.setServer(mqttServer, mqttPort);
  client.setCallback(callback);
}

void loop() {
  // put your main code here, to run repeatedly:
  if (!client.connected())
  {
    reconnect();
  }
  client.loop();

  long now = millis();
  if (now - lastMsg > publishingRate)
  {
    lastMsg = now;

    char payLoad_A[4];
    itoa(rpm_A, payLoad_A, 10);
    client.publish(rpmPubTopic_A, payLoad_A);

    char payLoad_B[4];
    itoa(rpm_B, payLoad_B, 10);
    client.publish(rpmPubTopic_B, payLoad_B);
  }
}

void tacho_A() {
  unsigned long countMillis = millis();

  tachoInterruptDuration_A = (countMillis - lastTachoInterrupt_A);
  unsigned long freq = 1000 / tachoInterruptDuration_A;  // Frequenz in Hz
  rpm_A = (freq * 60) / 2;
  lastTachoInterrupt_A = countMillis;
}

void tacho_B() {
  unsigned long countMillis = millis();

  tachoInterruptDuration_B = (countMillis - lastTachoInterrupt_B);
  unsigned long freq = 1000 / tachoInterruptDuration_B;  // Frequenz in Hz
  rpm_B = (freq * 60) / 2;
  lastTachoInterrupt_B = countMillis;
}

Also, I did not change anything in the circuit. But for the sake of completeness I will put the diagram in this answer, as well.

The pull up resistors are working fine with 1k ohm and I did not need any additional mosfets, level shifters or a resistor voltage divider.

I want to thank everyone that replied to this post and helped me improve my code and understanding the electronics around it more clearly! :slight_smile:

1 Like

Congratulations and thanks for letting us know.

I thought of letting you guys know, that I've found a solution to control my fans over TLS/SSL. Here's the post: