Readout Shahe Digital Scales with Arduino

There are a lot of cheap sliding calipers made in China, you may buy on Ebay, Aliexpress or wherever. Most of them have a (sometimes hidden) possibility to transmit the position value, usually via a synchronous serial protocol. There are quite some articles in the internet, describing hardware and protocol details, see Links.

I tried out a SHAHE Vertical Type Digital Linear Scale which has a Mini-USB socket to connect a remote display also sold by SHAHE. This page documents my experiences with Shahe Digital Scales and readout of position values with an Arduino.


Caliper Description and Tear Down

SHAHE offers two types of what they call "vertical (and horizontal) type digital linear scales":
One is a scale and slider made from stainless steel.
The other one is a scale made from aluminium with a plastic slider.
Here I describe the one made from steel.
A tear down of the aluminium model can be found


Data Output Connection

The caliper has a Mini-USB socket where power supply and data transmission lines may be accessed.
The signals on the socket have nothing to do with USB! Do not connect to a USB device.
The signals on the USB socket are (see picture):

Sadly for data line USB socket pin 4 (Id pin) is used which does not have a wire in a normal USB cable. To make the connection you have to make your own cable or find one that has all five wires connected.

The power supply is a CR2032 3V lithium cell and you may supply 3V via the USB socket, but I recommend to remove the battery if supplying power externally. I suppose the battery is not protected against charging or rapid discharge and may explode if an external supply is connected.
I don't know how much voltage may be applied as power supply but I would not go much higher than 3V. The 3.3V from an Arduino seem to be alright.

The signal lines, clock and data seem have a voltage swing of 0 to 1.5V. Low level is very near 0V and high around 1.6V with 3.3V power supply.

The data transmission is continuing even when switched off.



The SHAHE calipers use a variant of the protocol already described elsewhere ([1], [2], ).

The protocol is a typical synchronous serial protocol.
The data stream is composed of 24 bit packets. The data bits are valid on the falling edges of the clock signal.
The packets are divided into 6 nibbles (4 bits) each separated by a 0.58 ms pause.
The packets themselves are separated by a pause of around 100ms. Transmission of a complete packet lasts 8.6 ms.
Bit 0 is always set.
Bits 1 to 15 or maybe more represent the binary number of 100th of millimeters (in metric mode) of the position of the scale, least significant bit first. In inch mode the number represents 1/2000 inch.
Bit 22 represents the sign of the position report.

Below two oscillographs are shown illustrating serial communication. Top curve is the clock signal, lower curve is data signal. Data signal represents position 0.00 mm.

The top graph has a screen width of 10ms and shows a single 24 bits data packet.
The lower graph has a screen width of 200ms and shows two packet bursts.

The differences to the protocols described elsewhere are:
The sign is represented by bit 22 instead of bit 20.
It looks like there is no bit in the data packet indicating whether the selected units are mm or inches. Pressing the units button switches the transmitted data to represent 1/2000 inch but there is no flag indicating this. (The scale always uses mm and is set to position 0.00 when powering up.)
There is no evidence for a back channel allowing remote setting of zero or selecting units.


Arduino Interfacing

I used an Arduino Micro board which has an ATmega32u4 controller chip. The ATmega32u4 has an integrated USB controller which allows for transmitting the position reports from the caliper to a host computer.

The high level of the scale signals with around 1.5V is very near to the input high threshold voltage of the ATmega32u4 at a supply voltage of 5V (s. ATmega32u4 datasheet). Connecting the scale directly to the Arduino's inputs may work or not.
To be able to reliably read the scale's signals it is necessary to insert a level converter to interface with the microcontroller. A single transistor circuit like the one shown below is enough, mostly any general purpose NPN transistor would do.
(ATmega32u4 input-low-voltage: 0.2VCC - 0.1V, ATmega32u4 input-pullup-resistance: min 20kΩ, BC548B current gain hFE: 200. At a supply voltage VCC of 5.0V a base resistor of over 1MΩ would theoretically be adequate but I needed to go to as low as 56kΩ to get sufficiently steep signal slopes.)

Be aware of the fact that this circuit inverts the logic, this must be considered when implementing the software. Input pullups need to be enabled on the Arduino input pins and a low needs to interpreted as a 1 and vice versa.

Unlike as reported from similar scales (shumatech), there doesn't seem to exist a kind of back channel to set zero or select units.

To be able to set zero, I added a circuit to switch power supply to the scale. The SHAHE scales seem to always power on with current position set as zero and with mm units. See below for power switching circuit schematic.


Arduino Software

An input pin interrupt based software has been written to sample and interpret the data stream.

The clock signal from the scale is fed into a (external) pin interrupt capable pin on the Arduino. External interrupt capable pins on Arduino Micro are pins 0, 1, 2, 3 and 7. Pins 2 and 3 are also used by TWI (I2C), Pins 0 and 1 are also used for the USART Rx and Tx lines. So I chose pin 7.
The interrupt service routine (ISR) samples the data bit and collects the 24 bits of a single data packet into a static variable. Packet start is detected by requiring a minimum time between two consecutive clock pulses.

The main loop polls for a packet complete state indicated from the ISR, copies the packet bits, interprets the packet and shows the result.

The code makes use of the DirectIO library by Michael Marchetti to provide rapid access to microcontroller pin IO.

  ArduMicro_Test.cpp - Arduino sketch for testing pin interrupt handling.

  Copyright (C)2018 Lukas Zimmermann, Basel

  This library is free software; you can redistribute it and/or
  modify it under the terms of the CC BY-NC-SA 3.0 license.
  Please see the included documents for further information.

#include <Arduino.h>
#include <DirectIO.h>

#if ARDUINO >= 100
  #define LED_PIN 13

#define DRO_CLK  7
#define DRO_DATA 6

Input<DRO_CLK> dro_clk_pin(false); // deaktivate pullup
Input<DRO_DATA> dro_data_pin(false); // deaktivate pullup
Output<LED_PIN> ledpin;

void Blink(byte PIN, uint16_t DELAY_MS, uint16_t loops) {
  for (byte i = 0; i < loops; i++) {
    digitalWrite(PIN, HIGH);
    digitalWrite(PIN, LOW);

 * Returns the hexadecimal character representation of the low 4 bits of the
 * given byte.
char hex_nibble(uint8_t b) {
  b &= 0xf;
  if (b < 10)
    return b + '0';
    return b - 10 + 'a';

unsigned long now_millis;

// minimum gap in microseconds between two falling edges on clock line to be
// sure to have a packet start.
#define PCKT_START_GAP 50000  // 20ms
// protocol states
#define PCKT_READY 1
#define PCKT_FAIL 4

volatile uint8_t protocol_state = PCKT_READY;
volatile uint8_t bit_cnt;
volatile uint8_t nibble_bit;
volatile uint8_t nibble_cnt;
volatile uint8_t nibbles[6];

volatile unsigned long now_micros;
volatile unsigned long lastDROClk_micros;
volatile unsigned long irq_cnt = 0;
volatile uint8_t dro_data;

void isrDROClk() {
  now_micros = micros();
  ledpin = HIGH;
  if (protocol_state == PCKT_READY) {
    // wait for the absence of any clock pulse for more than PCKT_START_GAP
    // microseconds to identify start of packet transfer.
    if (now_micros - lastDROClk_micros > PCKT_START_GAP) {
      protocol_state = PCKT_IN_PROGRESS;
      bit_cnt = 0;
      nibble_cnt = 0; // least significant nibble arrives first
      nibble_bit = 0;
      nibbles[0] = 0;

  if (protocol_state == PCKT_IN_PROGRESS) {
    // sample data line several times to avoid glitches

    dro_data = dro_data_pin;
    dro_data += dro_data_pin;
    dro_data += dro_data_pin;
    dro_data = digitalRead(DRO_DATA);
    dro_data += digitalRead(DRO_DATA);
    dro_data += digitalRead(DRO_DATA);
    bit_cnt++; // count clock edges since packet start detection

    if (dro_data != 0 && dro_data != 3) {
      // require 3 equal samples of the data line, otherwise indicate packet
      // protocol failure
      protocol_state = PCKT_FAIL;

    } else {
      // valid data bit sample, collect to packet
      if (nibble_bit > 3) {
        nibble_bit = 0;
        nibbles[nibble_cnt] = 0;
      // collect bit value of 1 if bit sample was HIGH. Sequence is least
      // significant bit first.
      nibbles[nibble_cnt] >>= 1;
      if (dro_data == 3) nibbles[nibble_cnt] += 8;

    if (bit_cnt >= 24) {
      // data packet consists of 24 bits, when there where 24 clock edges since
      // packet start detection then packet is complete
      protocol_state = PCKT_COMPLETE;
  ledpin = LOW;

  // record the time of call of this interroupt routine
  lastDROClk_micros = now_micros;

void setup() {

  pinMode(LED_PIN, OUTPUT);
  Blink(LED_PIN, 500, 5);

  uint8_t cnt = 0;
  // for Leonardo/Micro/Zero wait for connection on USB
  while (!Serial && cnt < 5) { cnt++; } // Wait for serial port to be available

  // make shure to have an empty serial receive buffer

  // SPI library should disable interrupts which could interfer with SPI
  // transactions
  pinMode(DRO_CLK, INPUT);
  pinMode(DRO_DATA, INPUT);
  //dro_data_bit_mask = 1 << digitalPinToBitMask(DRO_DATA);
  //dro_data_port = digitalPinToPort(DRO_DATA);
  attachInterrupt(digitalPinToInterrupt(DRO_CLK), isrDROClk, FALLING);

  Serial.println(F("setup complete!"));

  now_millis = millis();
  lastDROClk_micros = micros();

void loop() {
  now_millis = millis();

  static uint16_t loops = 0;
  static unsigned long irqcnt;
  static uint8_t scale_data[6];

  //Blink(LED_PIN, 1, 1);

  static uint16_t pktcnt = 0;
  static uint16_t bcnt;
  static uint8_t state;
  cli();           // disable interrupts
  state = protocol_state;
  bcnt = bit_cnt;
  irqcnt = irq_cnt;
  sei();           // enable interrupts

  Serial.print(loops); Serial.print(" ");
  Serial.print(irqcnt); Serial.print(" ");

  if (state == PCKT_COMPLETE) {
    Serial.print(pktcnt); Serial.print(" ");
    Serial.print(bcnt); Serial.print(" ");
    cli();           // disable interrupts
    for (int i = 0; i < 6; i++) {
      scale_data[i] = nibbles[i];
    protocol_state = PCKT_READY;
    sei();           // enable interrupts

    // print nibbles starting with most significant one.
    for (int i = 5; i >= 0; i--) {
    Serial.print(" "); Serial.println(irqcnt);

    long pos = 0;
    for (int i = 4; i >= 1; i--) {
      pos += scale_data[i];
      pos <<= 4;
    pos += scale_data[0];
    pos >>= 1;
    if (scale_data[5] & 0b00000010) pos = -pos;

    if (scale_data[5] & 0b00001000)
      Serial.println(F(" in/2000"));
      Serial.println(F(" mm/100 "));

  } else if (state == PCKT_FAIL) {
    Serial.println(F("packet failed "));
    cli();           // disable interrupts
    protocol_state = PCKT_READY;
    sei();           // enable interrupts

I have implemented a more elaborated version of the software for simultaneous decoding of two scales which uses a TFT 320x240 pixel display with SPI ILI9341 controller to display the scale readings. It aswell supports a touch screen and a realtime clock based on DS3231 chip to transmit a time stamped reading via USB to a connected computer.
If you are interested in that version, please send me an email.



This site maintained by:
My public PGP key
last updated: 2019-01-02 Valid CSS! Valid XHTML 1.0 Strict