Digital Tachometer for record player (LCD display)

Hi,
On the Falcon do you have a time frame to update the Rpm or you just sent it any time?


The rx sio is interrupt driven, so it can occur at any time. The ISR for the sio buffers the characters as they come in and sets a flag upon receipt of ASCII(13). The main routine constantly scans for the flag and if set, parses the stored string and takes appropriate action. If no data is received from the tach, no correction is ever applied. If data is received and is in the correct format, the decision to apply correction (and how much) is calculated for each received message.
 
Hi,
The reason why I use the 3 pulse interrupt it is to be ready to wait for the interrupt in the routine. This will remove the delay of the interrupt branch time service. It will give you a more steady Rpm ready from the sensor.

I just pored through the data sheet for the ATMega328; one of the strangest interrupt schemes I've ever seen. It is basically single level: When ANY interrupt occurs, the global flag is cleared disabling all other interrupts until the current ISR is complete. If other interrupts occurred during the first ISR, they are latched then processed after the 1st ISR is complete, in the order of their interrupt vector address (lowest address=highest priority). There is no way to assign a higher priority to a low level routine that counts microseconds (or that captures them when an external pin goes low) as in most of the 8051 derivatives. The software can set the global interrupt enable flag within an ISR that will allow nested interrupts, but there still is no priority and ANY interrupt source can interrupt ANY other ISR regardless of vector address.

You definitely want to keep your ISR as short as possible, but the Arduino operating system is using Timer0 for micros() and millis(), so there is no way to prevent these from delaying your code from reading the results. A delay is no problem, as long as it is consistent each time. The display bobble is caused by variable delays on each attempt to read the count. Putting lots of processing and output routines in your ISR prevents micros() and millis() from working properly.
 
Tauro0221 and Packgrog- Let me ask you guys something:

1. What attracted you to the Arduino platform?

2. Why did you not do this project in assembly language with a more conventional processor?

3. If there was an easy to use hardware platform based on an 8051 derivative with and IDE, would doing a project like this be more attractive in assembly?
 
My answers:
1. What attracted you to the Arduino platform?
Purely this project, and the progress made by others, particularly tauro0221.

2. Why did you not do this project in assembly language with a more conventional processor?
See above. Others had success already with the Arduino, it's cheap, and it's easy to learn from. I also have far more experience with C, which I used for the first few years of my career before moving to mostly database work and web scripting, than I ever did with assembly, which I only did in one class in college back in the mid-90's. I'm sure I COULD do it, but the learning curve would be much higher for me.

3. If there was an easy to use hardware platform based on an 8051 derivative with and IDE, would doing a project like this be more attractive in assembly?
*shrug* Could be. Depends on availability of parts, ease of learning the necessary code, lack of needing to solder anything (sadly a bigger deal for me, even though I do have a kit for doing so). I'm more a coder and Lego-style builder than a welder.

But we'll see how this goes once I update my setup tonight. Like I said, the numbers seemed much better with tauro0221's original sketch were better than with my first attempt, so it could be that the tinier ISR with micros() call on the first line will be enough to improve the wild variation in speed estimate.
 
Last edited:
Availability of parts, ease of learning the necessary code, lack of needing to solder anything (sadly a bigger deal for me, even though I do have a kit for doing so). I'm more a coder and Lego-style builder than a welder.

What if there was an Arduino-like platform (all hardware already mounted, USB interface, GPIO pins) that would accept the same "shields"; essentially an Ardruino clone, but based on an 8051 and all of the code would be written in assembly instead of C. Libraries of code to do almost anything (LED, LCD, serial functions, keypads, rotary encoders, EEPROMS, ADC, DAC, etc.).
 
Hi,
To:pyramid
Originally I was using the Zbasic software but they also used the 328P micro. But the difference I was using the PWM to control the Rpm. The problem it is that you need to do your own PCB board. Now you can buy an Arduino board for pennies than make your own costly board. Assembly language it is faster but you need some expertise using it. Basic is it simple to program but depending how the instructions are built that will depend the executing time that they will consume.

That is why asked how often you need to sent the data update to the Falcon controller. Do you need to sent it in a time frame or when you see a changes in the Rpm.
 
What if there was an Arduino-like platform (all hardware already mounted, USB interface, GPIO pins) that would accept the same "shields"; essentially an Ardruino clone, but based on an 8051 and all of the code would be written in assembly instead of C. Libraries of code to do almost anything (LED, LCD, serial functions, keypads, rotary encoders, EEPROMS, ADC, DAC, etc.).
I think that would be fascinating. I wonder what projects (apart from this one) would see a measurable benefit in going that route rather than the Arduino & C. There's clearly a benefit in THIS instance (though I do hope that it won't be THAT significant with my latest updates, that would be frustrating), but I'm not sure where else in basically student-level cheap equipment that the fraction of a millisecond improvement will be as big a deal. With enough uses, it could be as nice of a teaching tool as the Arduino likely has been.
 
I think that would be fascinating. I wonder what projects (apart from this one) would see a measurable benefit in going that route rather than the Arduino & C. There's clearly a benefit in THIS instance (though I do hope that it won't be THAT significant with my latest updates, that would be frustrating), but I'm not sure where else in basically student-level cheap equipment that the fraction of a millisecond improvement will be as big a deal. With enough uses, it could be as nice of a teaching tool as the Arduino likely has been.

One of the reasons I'm thinking about this as a possibility is the Arduino (and for that matter, the C language) isolates the user from understanding the inner workings of the µP. Maybe that's a good thing in some cases, but I think an Arduino clone based on assembly would be a valuable learning tool.

The ATMega328 has a lot of capability that is hidden by the Arduino IDE. The capability is there to access all of it, but you have to delve into the details of the processor and control registers; at that point, you might as well go assembly.

I have to say that I'm really disappointed in the way the 328 handles interrupts. To me, it is quite limiting, not just in this application, but almost any M2M design.

Anyway, sorry to go off-topic.
 
Anyway, sorry to go off-topic.
Seems like an entirely valid discussion in the context of the thread. Might be worthy of a spin-off discussion elsewhere, but it applies here too.

Again, *HOPEFULLY* my latest tweaks will be enough to get acceptably close to the accuracy of the Roadrunner. I'll give it a try tonight. If it still bounces too much, but is closer to proper measurement, maybe I'll try going back to only updating once every 3 pulses.
 
That is why asked how often you need to sent the data update to the Falcon controller. Do you need to sent it in a time frame or when you see a changes in the Rpm.

OK, I misunderstood the context of the question.

All of the decision making is done by the Falcon (if/when to apply correction, which direction and how much). The RR makes no determination; it only reports the current speed, taken every rev. If the speed holds steady at 33.333, it does no harm to report that every 1.8s, the Falcon reads the data, then decides to do nothing. It makes that determination on each report from the tach. So if you sent the speed once per minute, the Falcon would only apply correction for one rev (if needed), then do nothing for 58.2 seconds since it has not received any further updates. It made sense to provide the PSU with updates on each rev as the control algorithm is based on that premise. If you sent updates every minute, I doubt the speed accuracy would be very good, but it would tend to keep it from drifting far off, unless the errors were quite large each time (the maximum correction applied each reported speed reading is ~±1.4Hz).

The Falcon is not "expecting" any updates. If it receives one, it processes it and (possibly) acts on it. If it stops receiving updates, it just holds the current frequency output where it is.

Hope that explains it adequately.
 
Last edited:
OK, latest and greatest. Much improved (despite one odd blip). Turntabulator app now tells me 33.30 and 44.97. I think that's livable, even if the app turns out to be more correct than the tach. I'm really not sure what else I could do that would make much difference. I genuinely cannot get the ISR any shorter.

I put back a small delay on the loop just to try to limit the calls to micros(), though I'm not sure if that matters one way or the other. I'm not sure what else I could do to refine this further. Suggestions welcome!

https://youtu.be/IZhpSJ3L3x4

Code:
// diyAudio Arduino LCD Tachometer with Serial Output ("Ostrich")
//
// A turntable tachometer sketch, including integration with Phoenix Engineering's
// Falcon or Eagle power supplies. External communication is through the default
// serial port connection (D0, D1, though only D1 (TX) is actually used). PSU
// integration requires a RS232 shield or custom converter. Connection to PSU is
// with a 3.5mm (headphone jack) TRS connector, with TX port from DB9 connector
// attached to the Ring connector (red wire) on the TRS jack. Thanks to Pyramid
// (Phoenix Eng) for assist and communication specs.
// Base tachometer functionality based on design from diyAudio user tauro0221.

#include <LiquidCrystal.h>

#define SENSOR_PIN 3 // Interrupt input pin for sensor on Arduino UNO MUST be pin 3.

// Initialize the LCD library with the numbers of the interface pins.
LiquidCrystal lcd(12, 11, 6, 5, 4, 2);

// Global Variables
volatile unsigned long last_pulse_time = 0; // Timestamp of latest interrupt pulse from sensor.
unsigned long prev_pulse_time = 0; // Timestamp of previous interrupt pulse from sensor.
byte cycle_count = 2;

void setup() {
  // Set up the LCD's number of columns and rows:
  lcd.begin(16, 2);
  lcd.clear();
  lcd.setCursor(0, 0);  // Set cursor at first character, first line.
  lcd.print("diyAudio Ostrich");

  pinMode(SENSOR_PIN, INPUT_PULLUP); // Sensor setup
  attachInterrupt(digitalPinToInterrupt(SENSOR_PIN), sensor_pulse, FALLING);

  Serial.begin(9600);  // Open the serial port at 9600 bps
}

void loop() {
  if (last_pulse_time > prev_pulse_time) {
    calculate_rpm();
    prev_pulse_time = last_pulse_time;
  }
  // Show waiting message after 5 seconds of no new pulses, but only if pulses were detected.
  else if (micros() - last_pulse_time > 5000000 && cycle_count > 0) {
    lcd.setCursor(0, 1);  // Set cursor at first character, second line.
    lcd.print(" Awaiting Signal");
    cycle_count = 0;
  }
  delay(100);
}

// Sensor pulse completion interrupt callback handler. Keep this as lightweight as possible.
void sensor_pulse() {
  last_pulse_time = micros();
}

void calculate_rpm() {
  if (cycle_count < 2) {
    // Build up a couple of cycles to limit poor reporting.
    cycle_count++;
    lcd.setCursor(0, 1);
    lcd.print("  RPM:          "); // Clear the Awaiting Signal message, prep for RPM update
  }
  else {
    float rpm = 60000000.0/(last_pulse_time - prev_pulse_time);
    lcd.setCursor(7, 1); // Update second line, after "  RPM: ".
    lcd.print(rpm, 4);

    // Send update to Phoenix PSU via serial port (D0 and D1) in format XX.XXX[lf][cr]
    if ((10.0 <= rpm) && (rpm < 100.0)) {
      char outstr[8];
      dtostrf(rpm, 6, 3, outstr);
      outstr[6] = '\n';
      outstr[7] = '\r';

      Serial.print(outstr);
    }
  }
}
 

Attachments

  • diyaudio_ostrich_tach_3.txt
    2.7 KB · Views: 137
To Packgrog: What about getting some reading and do an average of the Rpm. The reading from the last change looks good.
As Pyramid said, might be fine for display (particularly if not connected to a Phoenix PSU), but not good for sending to the Falcon. I *COULD* send different RPM calculations to the LCD and Serial, but that would mean doing the RPM calculation twice, as well as doing more variable assignments for the buffer and average. Maybe it wouldn't matter as long as its in the loop instead of the ISR, but I think I'd rather keep the whole thing as concise (and explicit, as clearly it helps to see the severity of bounce) as possible.
 
Anything in your main loop will not affect the accuracy of micros() as the routine that updates the count is interrupt driven. The reason it got skewed when you were doing a lot of processing in your ISR, is the '328 has only one level of interrupt priority, so as long as you were in your ISR, the routine that accumulates the count for micros() could not function. I think you did it exactly right the way it is written now.

You wouldn't have to do the RPM calculation twice (and it wouldn't matter if you did). Do it once, send the immediate result to the SIO, then stuff the value in a float array. Maintain a pointer that gives you the current save-to index into the array, increment it, and have it rollover at 8->1 so it creates a circular queue that always overwrites the oldest value. To average the display reading, sum the 8 float values in the array and divide by 8 for the display.

Define a Boolean constant at startup so if someone does not want averaging, they can set the value to .F.. Users can also change the length of the array to change the amount of smoothing.
 
Configurable smoothing would certainly be an argument for investing in that LCD/button shield... If I were to build another one, I'd probably be inclined to go that route.

I wish there was something more that I could do with the current code/hardware to improve the accuracy further, but I think that's as shaved down as can be without, as you say, going with assembly and a different CPU.
 
I don't think you will get it much more accurate than that. The difference between 33.333 and 33.334 is only 54µSec. The platter could actually be varying in speed that small amount, it could be a small amount of trigger ambiguity or varying software delays (or some combination of all three).

In the RR I used the timer2 interrupt set to the highest priority (it could not be masked out) and 16bit auto reload mode so the count was extremely stable (I also used a TCXO with 2.5PPM accuracy). My timer "tick" was 33.333 uSec, so not nearly as critical as your micros(), but it still produces a slight shift in the speed reading from rev to rev and I'm only displaying to 3 places right of the decimal.
 
I have tried my display but no luck because I have no knowledge of Android. This is first time for me to get hand on Android. I tried to write the codes to the board. But I kept getting error messages. Finally, I gave it up and bought a Red Lion tachometer off eBay. Red lion tachometer can be 0.01 digit accuracy. It is not like a lot of cheap units on eBay. I am still waiting for the unit and hope it will work fine.