TK50 ultrasonic/Asking over a bus/07. Three bytes of micrometres
Asking over a bus · 07 of 11

Three bytes of micrometres

Over I2C or UART the whole conversation is one byte out and three bytes back. The three bytes are a distance, already worked out — in micrometres, which is the one thing nobody guesses and the reason readings come out ten times too big.

The whole conversation

Press the button to run one transaction, and move the slider to pose a distance.

One byte out, three bytes back
I2C 0x57
Distance to pose137.5 cm
Bytes back
3
Raw value
1,375,000 µm
One reading
150 ms
Nothing on the bus yet. The unit is micrometres, not millimetres. Three bytes reach 1,677.722 cm, which is far more range than the part has — so the resolution is spent on precision the sensor cannot deliver rather than on distance. Divide by 10 000 for centimetres. Divide by 1000 instead and every reading is ten times too big, which at arm’s length still looks like a believable number for a room.

There is no register map here and nothing to configure. The command set is one byte — 0x01 over I2C, 0xA0 over UART — and it means "take a reading". The answer is three bytes and it means "here is the distance". That is the entire protocol, in both directions, for both buses.

Which is an unusually pleasant thing about this part. Most I2C sensors want a configuration write, a conversion trigger, a status poll and then a read, and each of those is a chance to get a register address wrong.

The unit

The three bytes are a 24-bit number, most significant byte first, and the unit is micrometres.

um = (b[0] << 16) | (b[1] << 8) | b[2];
cm = um / 10000.0;

Nobody guesses micrometres. Millimetres is the natural assumption, and dividing by 1000 instead of 10 000 gives readings that are ten times too big — but smoothly, consistently, tracking the object correctly, so it reads as a calibration problem rather than a unit problem. A hand at 30 cm reports as 300 cm, which is a plausible number for a room, which is exactly why the mistake survives a first test.

Three bytes of micrometres reach about 16.8 metres. The sensor cannot see anything like that far, so the extra resolution is spent on precision rather than range — the reading comes back to the nearest micrometre, of a measurement good to about a centimetre.

Waiting, rather than asking

Both buses need a pause between the command and the read, because the measurement has to physically happen: a burst goes out, crosses the room and comes back.

There is no busy flag and no way to ask whether the answer is ready. So the protocol is ask, wait, read — and the wait is a plain delay. A bus measurement cycle is 100 ms; the supported library waits 150 ms, and the sketch here does the same.

If that matters to your loop, this is an argument for GPIO mode rather than for cleverness: there is nothing in the bus protocol to optimise.

What the vendor library does not do

It is worth knowing, because the library is what Lonely Binary's own documentation points you at and it is a reasonable place to start.

Its UART path reads only the bytes that have already arrived — while available() rather than waiting for three — so a reply that is one byte late leaves part of the value uninitialised, and the resulting distance is arbitrary. The sketch above insists on all three bytes and returns a failure instead, which is the change worth carrying over into your own code whatever else you copy.

One sensor per bus

The address is 0x57 and there is nothing on the board to change it — no pads, no solder blobs, no configuration register. Two of these on one I2C bus is two devices answering the same address, and the result is not a collision you can detect, it is a value that is neither reading.

That limit drives most of which mode for which job.

The code

bus_reading.ino

Reads the sensor over I2C on a board with J1 bridged. Change USE_I2C to 0 for UART on a board with J2 bridged instead; the three bytes and the arithmetic are identical either way.

// A distance over a bus: one command byte out, three bytes back.
//
// Wiring, sensor to board. Count the sensor's pins from the SQUARE PAD,
// which is GND:
//
//   TK50 GND  -> board GND
//   TK50 VCC  -> board 3V3 on an ESP32, ESP32-S3 or Pico; 5V on an Uno
//   TK50 ECHO -> I2C: board SDA.  UART: board RX.
//   TK50 TRIG -> I2C: board SCL.  UART: board TX.
//
// UART names are the sensor's own, so they cross: the sensor's TX (on the
// ECHO pin) goes to your board's RX.
//
// SOLDER FIRST. This sketch needs a bridged jumper and will read nothing
// on an out-of-the-box board:
//   I2C  -> bridge J1, leave J2 open
//   UART -> bridge J2, leave J1 open
// J2 is the upper pad. Power the board off to solder and on again after.
//
// Arduino IDE: no library beyond the bundled Wire. No special Tools
// settings. Serial Monitor at 115200.

#include <Wire.h>

#define USE_I2C 1        // 1 for I2C (J1 bridged), 0 for UART (J2 bridged)

const uint8_t  TK50_ADDR    = 0x57;   // Fixed. There is no address pad.
const uint8_t  CMD_I2C      = 0x01;   // "take a reading"
const uint8_t  CMD_UART     = 0xA0;   // the same command, over serial
const uint32_t UART_BAUD    = 9600;   // fixed by the sensor
const uint16_t SETTLE_MS    = 150;    // the burst, the flight, the echo

// Three bytes, most significant first, in MICROMETRES. 10 000 um = 1 cm.
const float UM_PER_CM = 10000.0;

void setup() {
  Serial.begin(115200);
#if USE_I2C
  Wire.begin();
#else
  Serial1.begin(UART_BAUD);   // Uno: use SoftwareSerial instead
#endif
}

// Returns centimetres, or -1 when the sensor did not answer.
float readDistanceCm() {
  uint8_t b[3];

#if USE_I2C
  Wire.beginTransmission(TK50_ADDR);
  Wire.write(CMD_I2C);
  if (Wire.endTransmission() != 0) return -1;   // nobody at that address

  delay(SETTLE_MS);

  // Ask for exactly three, and insist on getting three. Reading fewer and
  // using them anyway is how a dropped byte becomes a plausible distance.
  if (Wire.requestFrom(TK50_ADDR, (uint8_t)3) != 3) return -1;
  for (uint8_t i = 0; i < 3; i++) b[i] = Wire.read();
#else
  while (Serial1.available()) Serial1.read();   // clear anything stale
  Serial1.write(CMD_UART);

  delay(SETTLE_MS);

  if (Serial1.available() < 3) return -1;       // short reply, not a reading
  for (uint8_t i = 0; i < 3; i++) b[i] = Serial1.read();
#endif

  uint32_t um = ((uint32_t)b[0] << 16) | ((uint32_t)b[1] << 8) | b[2];
  return um / UM_PER_CM;
}

void loop() {
  float cm = readDistanceCm();

  if (cm < 0) {
    Serial.println("no answer");
  } else {
    Serial.print(cm, 2);
    Serial.println(" cm");
  }

  delay(100);   // One bus measurement cycle is 100 ms.
}

If every reading is 0.00 cm the three bytes never arrived — on I2C run an address scan and confirm 0x57 answers, and on UART confirm TX and RX are crossed. If the readings are ten times too big, the divisor is 1000 rather than 10 000.

When it does not work

An I2C scan finds nothing at 0x57

Three things, in order. Is J1 actually bridged and the board power-cycled since — an unbridged board is in GPIO mode and ignores the bus entirely. Are SDA and SCL the right way round: ECHO is SDA, TRIG is SCL. And does the bus have pull-up resistors? The sensor provides none, though most microcontroller boards do.

Can I put two of these on one I2C bus?

No. The address is 0x57 and there is no pad, solder blob or register to change it, so two sensors answer at once and neither reading survives. Use one per bus, a multiplexer, or give the second sensor a pair of GPIO pins instead.

Every distance is ten times too big

The unit is micrometres, not millimetres. Divide the 24-bit value by 10 000 to get centimetres. Dividing by 1000 gives millimetres-as-centimetres, which at arm's length still looks like a believable number for a room and is why this one survives testing.

UART returns garbage or nothing

Check the baud rate is 9600 and that TX and RX are crossed — the sensor's TX is on the pin marked ECHO and must reach your board's RX. On an Uno there is no spare hardware serial port, so use SoftwareSerial rather than the Serial that the USB cable is using.

Why wait 150 ms rather than poll for a ready flag?

Because there is no ready flag to poll. The command set is one byte, and the answer is three bytes with no status in them, so the only protocol available is to ask, wait long enough, and read. A bus measurement cycle is 100 ms, and the supported library waits 150 ms for margin.

Where this goes next

Five common projects, and which of the four modes each one wants.

Which mode for which job

Edit this page — content/books/ultrasonic-sensor/three-bytes-of-micrometres.mdx

Community

Questions about this product

See what other owners have asked, and read their solutions.

Ask a question ↗

Ultrasonic Distance Sensor

Loading discussions…

Discuss this article

Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.

Browse Modules and blocks on the forum