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.
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
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
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.
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.
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.
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.
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.
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
Questions about this product
See what other owners have asked, and read their solutions.
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.