While playing around with the digital caliper, I found a 'secret', 4-pin port on the device. I was expecting it to be some kind of programming or data out port.
On 're-search with Google', I found that it indeed is a data out, it can be used to get the reading as it is displayed on the LCD.
I'm going to try to make an interface to get the data from the caliper and interpret the bit stream to the real value. The micro-controller board I'll be making could be sending the
data directly to a phone or computer via serial/bluetooth/wi-fi/2.4GHz NRF2L01. A computer/android software will be written to show the values, which will be the 'interface
programming assignment'.
For the communication part I will be using soft serial, as the Tiny44 doesn't support it natively.
Before I proceed further I need to know if I can get any data out of it and even if I get, whether I'll be able to interpret it, because chances are there that the device could be communicating
with some proprietary protocols.
So I started browsing again and found that many Chinese calipers are using a couple of common protocols. By the looks of it and the way the brand-name 'Neiko' sounds, the
digital caliper we have belongs to the 'Chinese caliper family'. I got confidence now!,
let's move on.
On further reading I found that the caliper's pins are vcc(+1.5v as it is powered from a button cell), DATA, CLOCK and GND.
So I started probing the pins using a DSO.
This is not exactly the direct reading from the caliper, this is the data from the caliper passed through a
Schmitt-Trigger configured as a logic level converter, to convert the low 1.5v
pulses from the caliper to 5v pulses so that we can feed it to the AVR chip.
The details of the Schmitt-Trigger is explained further down.
On further analysis I found that this pattern is identical to what this guy had here. So I can now interpret the data, we have to consider
Here are some pictures explaining the interpretation.
Now if we take the first 16 bits only and put them in reverse we get,
1111, 1101, 0100, 0010
Inverting them (1's compliment)
0000, 0010, 1011, 1101
Now
(0000001010111101)2 = (701)10
Which is the same as the caliper reading except for the decimal point.
So the setup is going to
Clock
and Data
.Clock
will be connected to INT0
or PB2.
Here is the circuit diagram I designed for this assignment,
it has the following features.
1.5v
logic to 5v logic for the micro-controller.
This is an interesting design using an Op-Amp. We could just use a transistor as a logic level converter, but this is not ideal as they may respond to the noise too. The Schmitt-Trigger
has two thresholds an upper threshold and a lower one. The Schmitt Trigger goes high/maximum voltage when it encounters an input above the upper threshold, and stays there
even if the noise or some unintended fluctuation brings it down below this value. The value goes low/minimum voltage only when the input goes below the lower threshold.
So if we set high and low thresholds as .9v and .1v respectively, for a 1v logic pulse, and a supply voltage of 5v, the output will become high only when it detects an
input greater than .9v and remains high as long as the input doesn't fall below .1v. Now, once it falls below .1v, the output goes to zero and stays there till it gets an
input greater than .9v. This is a neat way to clean some noise, also the Op-Amp provides high input impedance.
Here is an online calculator I used to calculate the values of the resistors used with the op-amp,
AD-8615 to configure it as a Schmitt-Trigger. The calculator is an
hyperphysics.phy-astr.gsu.edu page on Schmitt-Trigger.
Since the caliper is powered by a 1.5v
cell, I had assumed that the logic level/ logic high would be at 1.5v
. But I was wrong, Its only about 1v.
May be the cell is weak, but the caliper is working and the logic levels are 1v and 0v. So I had to change the Schmitt-Trigger design from the initial design for 1.5v
logic pulse, I had to change the resistor, R1 from 1KΩ
to 0.5KΩ
.
It's not a good idea to solder wires to the caliper, so I decided to make a connector.
First I tried making a connector using PCB. thickened at the contacts using the solder. This fits nicely but the contacts are not even/flat and hence not all the terminals
make contact at the same time.
So I thought about 3D printed connectors. And found out a design from
thingiverse. So I made the print and decided to add spring contacts salvaged from the scrap ink-jet printers. The spring contacts were soldered and then buried deep
into the grooves for the connector pins.
Here is the code which interprets the data and sent it via serial communication, I used the FTDI in an Arduino UNO to get the data to the computer and used Arduino IDE's serial monitor
to display the information. The serial communication is implemented with software serial code written by Neil,
hello.ftdi.44.echo.c. I extracted the relevant parts form hos code.
The chip is operating at 20MHz and the baud rate is 115200
.
#define F_CPU 20000000UL #include <avr/io.h> #include <stdlib.h> #include <avr/interrupt.h> #include <util/delay.h> #include <avr/pgmspace.h> #define output(directions,pin) (directions |= pin) // set port direction for output #define set(port,pin) (port |= pin) // set port pin #define clear(port,pin) (port &= (~pin)) // clear port pin #define pin_test(pins,pin) (pins & pin) // test for port pin #define bit_test(byte,bit) (byte & (1 << bit)) // test for bit set #define bit_delay_time 8.5 // bit delay for 115200 with overhead #define bit_delay() _delay_us(bit_delay_time) // RS232 bit delay #define half_bit_delay() _delay_us(bit_delay_time/2) // RS232 half bit delay #define char_delay() _delay_ms(10) // char delay #define serial_port PORTA #define serial_direction DDRA #define serial_pins PINA #define serial_pin_in (1 << PA0) #define serial_pin_out (1 << PA1) #define max_buffer 8 unsigned int fin_read_h, fin_read_l, fin_read; unsigned int temp_read; unsigned int last_read; unsigned int bit_count, sign_bit; void put_char(volatile unsigned char *port, unsigned char pin, char txchar) { // send character in txchar on port pin, assumes line driver (inverts bits) clear(*port,pin); // start bit bit_delay(); if bit_test(txchar,0) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,1) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,2) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,3) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,4) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,5) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,6) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,7) set(*port,pin); else clear(*port,pin); bit_delay(); set(*port,pin); // stop bit bit_delay(); //char_delay(); // char delay bit_delay(); } void put_string(volatile unsigned char *port, unsigned char pin, char *str) { // print a null-terminated string static int index; index = 0; do { put_char(port, pin, str[index]); ++index; } while (str[index] != 0); } int main(void) { static char buffer[max_buffer] = {0}; // set clock divider to /1 CLKPR = (1 << CLKPCE); CLKPR = (0 << CLKPS3) | (0 << CLKPS2) | (0 << CLKPS1) | (0 << CLKPS0); // initialize output pins set(serial_port, serial_pin_out); output(serial_direction, serial_pin_out); DDRB = 0b00000000; DDRA = 0b11001011; cli(); // disable global interrupts // initialize Timer1 the 16bit timer TCCR1A = 0; TCCR1B = 0; // Use CS10, CS11 and CS12 bits for 1/64 prescaler: TCCR1B |= (1 << CS10)|(1 << CS11); //enble int0 GIMSK |= (1<<INT0); //PCICR is GIMSK in attiny MCUCR = 1<<ISC01 | 0<<ISC00; // Trigger INT0 on falling edge sei(); // enable global interrupts: while(1) { // if (fin_read != last_read) { put_string(&serial_port, serial_pin_out, "new value "); itoa (fin_read_h, buffer, 10); if (sign_bit == 1) put_char(&serial_port, serial_pin_out, '-'); //sign put_string(&serial_port, serial_pin_out, buffer); put_char(&serial_port, serial_pin_out, '.'); //decimal point if (fin_read_l < 10) put_char(&serial_port, serial_pin_out, '0'); itoa (fin_read_l, buffer, 10); put_string(&serial_port, serial_pin_out, buffer); put_char(&serial_port, serial_pin_out, '\"'); put_char(&serial_port, serial_pin_out, 10); // new line last_read = fin_read; } } } ISR(INT0_vect) { PORTA ^= 0b01000000; //debugging LED, check mosi pin if ((TCNT1 > 10000)) { fin_read = temp_read; fin_read_h = temp_read/100; fin_read_l = temp_read - fin_read_h*100; temp_read = 0; bit_count = 0; sign_bit = 1; } else { if (bit_count < 15) { temp_read = temp_read>>1; // temp_read /= 2; //right shift if (~PINA & 0b00000100) { temp_read |= 32768; //2^15 } } else { if (bit_count == 19 && PINA & 0b00000100) //20th bit corresponds to sign bit sign_bit = 0; } bit_count += 1; } TCNT1 = 0; }
This program is not the perfect, need a little workaround, first of all I wanted the program to send the latest reading only if it is different from the last, else the code will sent
10 readings per second, like what it is doing right now.
Secondly, there is something wrong with my interpretation technique, the output is always ends with an even number, somehow it skips the odd number and rounds off to the
nearest even number, must be missing the least significant bit, which decided the if the final value if odd or even.
Now I need to send this data to a computer program/android app to display this value. Which will complete the interface designing assignment.
I have written a python App for displaying the the data from the caliper. I have modified the firmware a little bit to reduce the baud rate, to send only the value and to ignore the
sign-bit. The sign bit is not reliable at all, may be I'm not using the right one, but I have checked every bit, it doesn't seem to be working. The python code also filters
out any reading with garbage. The caliper is sending the data continuously (this was not intended, but now it's really helpful), with '\n'
in between
the readings. So if the application receives any data, which is not in the 'white list' (0-9, '.', '-', '\n'), the entire line is discarded. Also the app will update the
value only if there is a change in filtered value.
The following code is based on what I found here. I'm very new to python and coming from, c, I found the python a complicated. But with all the libraries for just about everything, it's resourceful for sure. I'm also thankful to Yadu and Vishnu for explaing the syntax and what is what.
#define F_CPU 20000000UL #include <avr/io.h> #include <stdlib.h> #include <avr/interrupt.h> #include <util/delay.h> #include <avr/pgmspace.h> #define output(directions,pin) (directions |= pin) // set port direction for output #define set(port,pin) (port |= pin) // set port pin #define clear(port,pin) (port &= (~pin)) // clear port pin #define pin_test(pins,pin) (pins & pin) // test for port pin #define bit_test(byte,bit) (byte & (1 << bit)) // test for bit set #define bit_delay_time 50 // bit delay for19200 with overhead #define bit_delay() _delay_us(bit_delay_time) // RS232 bit delay #define half_bit_delay() _delay_us(bit_delay_time/2) // RS232 half bit delay #define char_delay() _delay_ms(10) // char delay #define serial_port PORTA #define serial_direction DDRA #define serial_pins PINA #define serial_pin_in (1 << PA0) #define serial_pin_out (1 << PA1) #define max_buffer 8 unsigned int fin_read_h, fin_read_l, fin_read; unsigned int temp_read; unsigned int last_read; unsigned int bit_count, sign_bit; void put_char(volatile unsigned char *port, unsigned char pin, char txchar) { // send character in txchar on port pin, assumes line driver (inverts bits) clear(*port,pin); // start bit bit_delay(); if bit_test(txchar,0) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,1) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,2) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,3) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,4) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,5) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,6) set(*port,pin); else clear(*port,pin); bit_delay(); if bit_test(txchar,7) set(*port,pin); else clear(*port,pin); bit_delay(); set(*port,pin); // stop bit bit_delay(); //char_delay(); // char delay bit_delay(); } void put_string(volatile unsigned char *port, unsigned char pin, char *str) { // print a null-terminated string static int index; index = 0; do { put_char(port, pin, str[index]); ++index; } while (str[index] != 0); } int main(void) { static char buffer[max_buffer] = {0}; // set clock divider to /1 CLKPR = (1 << CLKPCE); CLKPR = (0 << CLKPS3) | (0 << CLKPS2) | (0 << CLKPS1) | (0 << CLKPS0); // initialize output pins set(serial_port, serial_pin_out); output(serial_direction, serial_pin_out); DDRB = 0b00000000; DDRA = 0b11001011; cli(); // disable global interrupts // initialize Timer1 the 16bit timer TCCR1A = 0; TCCR1B = 0; // Use CS10, CS11 and CS12 bits for 1/64 prescaler: TCCR1B |= (1 << CS10)|(1 << CS11); //enble int0 GIMSK |= (1<<INT0); //PCICR is GIMSK in attiny MCUCR = 1<<ISC01 | 0<<ISC00; // Trigger INT0 on falling edge sei(); // enable global interrupts: while(1) { itoa (fin_read_h, buffer, 10); // if (sign_bit == 1) // put_char(&serial_port, serial_pin_out, '-'); //sign put_string(&serial_port, serial_pin_out, buffer); put_char(&serial_port, serial_pin_out, '.'); //decimal point if (fin_read_l < 10) put_char(&serial_port, serial_pin_out, '0'); itoa (fin_read_l, buffer, 10); put_string(&serial_port, serial_pin_out, buffer); // put_char(&serial_port, serial_pin_out, '\"'); put_char(&serial_port, serial_pin_out, 10); // new line last_read = fin_read; } } ISR(INT0_vect) { PORTA ^= 0b01000000; //debugging LED, check mosi pin if ((TCNT1 > 10000)) { fin_read = temp_read; fin_read_h = temp_read/100; fin_read_l = temp_read - fin_read_h*100; temp_read = 0; bit_count = 0; sign_bit = 1; } else { if (bit_count < 15) { temp_read = temp_read>>1; // temp_read /= 2; //right shift if (~PINA & 0b00000100) { temp_read |= 32768; //2^15 } } bit_count += 1; } TCNT1 = 0; }
#base code from http://robotic-controls.com/learn/python-guis/tkinter-serial from serial import * from Tkinter import * serialPort = "/dev/ttyUSB0" baudRate = 19200 ser = Serial(serialPort , baudRate, timeout=0, writeTimeout=0) #make a TkInter Window root = Tk() root.wm_title("Caliper Interface") # make a text box to put the serial output log = Text ( root, width=30, height=30, takefocus=0) log.pack() #make our own buffer #useful for parsing commands #Serial.readline seems unreliable at times too serBuffer = "" val =serBuffer scrap = 0 def readSerial(): while True: c = ser.read() # attempt to read a character from Serial global scrap #was anything read? if len(c) == 0: break # get the buffer from outside of this function global serBuffer # check if character is a delimeter if c == '\r': c = '' # don't want returns. chuck it cc = ord(c) if (cc < 45 or cc > 57) and cc <> 10 : scrap = 1 if c == '\n': serBuffer += "\n" # add the newline to the buffer global val if (val != serBuffer and scrap <> 1): val = serBuffer #add the line to the TOP of the log log.insert('0.0', serBuffer) serBuffer = "" # empty the buffer scrap = 0 else: serBuffer += c # add to the buffer root.after(10, readSerial) # check serial again soon # after initializing serial, an arduino may need a bit of time to reset root.after(100, readSerial) root.mainloop()
Notice that the interface is showing the latest reading but only the absolute value. Also, filtering scheme works nice, no garbage now, all clean output.
And the best part! the text/reading is copyable.
Refined a bit more, this can be a used as cheap method for positioning on a machine, on an axis with small travel, like a small lathe or a small PCB mill.
If I add a Bluetooth interface with a couple of buttons, I could use this along with a matching application to make a wireless data logger. The app will generate CSV, the
button act like camera shutter, recording the current reading.
This is a base for a lot of cool future works. I'm really happy that it worked, except for the sign part, I've to figure it out.