כרטיס תקשורת ASK — רכיב החומרה הייחודי להתמחות

✦ בנוי על ידי פהד גאנם ✦

λ = cf
ℓ = λ4
Tb = 1Rb
BW ≈ 2·Rb
L = 20·log10(d)
+ 20·log10(f) − 147.55
Pr = Pt − L − Lx
M = Pr − S
0x2D
1.0 kbit/s
433.92 MHz
17.3 cm
10 dBm
100 m
−105 dBm
40 dB
—
—
—
—
—
—
—
—
—
—
—
—


λ = cf = 299,792,458433.92·106 = 0.6909 m
λ = 69.09 cm

ℓ = λ4 = 69.094 = 17.27 cm

20·log10(d) = 20·log10(100) = 40.00 dB
20·log10(f) = 20·log10(433.92·106) = 172.75 dB
L = 40.00 + 172.75 − 147.55 = 65.20 dB

Pr = Pt − L − Lx = 10 − 65.20 − 40 = −95.20 dBm

M = Pr − S = −95.20 − (−105) = 9.80 dB

λ = 69.09 cm
ℓ = λ4 = 17.27 cm
L = 65.20 dB
Pr = −95.20 dBm
M = 9.80 dB > 0

Vout = Vin · R2R1 + R2
I = VinR1 + R2
VIH = 0.75 · VDD = 0.75 · 3.3 V = 2.475 V
6 ms
5 V
10 kΩ
20 kΩ
—
—
—
—

Vout = Vin · R2R1 + R2 = 5 V · 20 kΩ10 kΩ + 20 kΩ = 3.333 V
I = VinR1 + R2 = 5 V30 kΩ = 0.167 mA
VIH = 0.75 · 3.3 V = 2.475 V
Vout − VIH = 3.333 V − 2.475 V = 0.858 V
Vout − VDD = 3.333 V − 3.3 V = 0.033 V
Vmin = 5 V · 0.95 · 20 kΩ1.05 · 10 kΩ + 0.95 · 20 kΩ
Vmin = 5 V · 19 kΩ29.5 kΩ = 3.220 V
Vout = 5 V · 10 kΩ10 kΩ + 10 kΩ = 2.500 V
Vout − VIH = 2.500 V − 2.475 V = 0.025 V
Vmin = 5 V · 0.95 · 10 kΩ1.05 · 10 kΩ + 0.95 · 10 kΩ
Vmin = 5 V · 9.5 kΩ20 kΩ = 2.375 V
2.375 V < 2.475 V

Vout = Vin · R2R1 + R2
L = λ4 = c4 · f
—
—
—


Vout = Vin · R2R1 + R2 = 5 · 2010 + 20 = 3.33 V

VIH = 0.75 · VDD = 0.75 · 3.3 = 2.475 V
3.33 V > 2.475 V

λ = cf = 299,792,458 m/s433,920,000 Hz = 0.6909 m

L = λ4 = 69.09 cm4 = 17.27 cm
LM35 · +Vs5 V
LM35 · GNDGND
LM35 · OUTGPIO 34
TX · VCC5 V
TX · GNDGND
TX · DATAGPIO 25
TX · ANT17.3 cm
RX · VCC5 V
RX · GNDGND
RX · VCC + GNDC
RX · DATAGPIO 27
RX · ANT17.3 cm
MAX7219 · V+5 V
MAX7219 · GNDGND
MAX7219 · DINGPIO 23
MAX7219 · CLKGPIO 18
MAX7219 · LOADGPIO 5

n = 16 + 8 + 8 + 8·L + 8
t = n · T
T4 < Δt < 3·T4
3·T4 ≤ Δt < 5·T4
0x2D
1
23.4 °C
0x37
± 0 %
—
—
—
—

n = 16 + 8 + 8 + 8·4 + 8 = 72
t = n · T = 72 · 1 ms = 72 ms
Nh = 2 · n = 2 · 72 = 144
Th = T2 = 1000 µs2 = 500 µs
x = 23.4 · 10 = 234
xhi = ⌊x256⌋ = ⌊234256⌋ = 0 = 0x00
xlo = 234 − 0·256 = 234 = 14·16 + 10 = 0xEA
T4 = 1000 µs4 = 250 µs
3·T4 = 3 · 1000 µs4 = 750 µs
5·T4 = 5 · 1000 µs4 = 1250 µs
Δ = 500 µs − 250 µs = 250 µs
ΔT = 250 µs1000 µs = 0.25 = 25 %
n = 72 bit
t = 72 ms = 144 × 500 µs
23.4 °C = 0x00 0xEA
250 µs < Δt½ < 750 µs ≤ Δt1 < 1250 µs
Δ = ± 25 %
ESP32 · Arduino C++
// AskLink core - Manchester-coded frames for a 433 MHz ASK (OOK) module pair.
// Frame on the air:  16 preamble bits '1' | sync 0x2D | LEN | payload | CRC-8
// Manchester: '1' = low then high, '0' = high then low (the edge sits mid-bit).

const uint8_t  ASK_SYNC     = 0x2D;
const uint8_t  ASK_MAX_LEN  = 16;     // payload bytes
const uint8_t  ASK_PREAMBLE = 16;     // '1' bits that let the receiver gain settle
const uint16_t ASK_MAX_HALF = 2 * (ASK_PREAMBLE + 8 * (ASK_MAX_LEN + 3));

// CRC-8, polynomial 0x07, start value 0. Check: "123456789" gives 0xF4.
uint8_t crc8(const uint8_t *d, uint8_t n) {
  uint8_t c = 0;
  for (uint8_t i = 0; i < n; i++) {
    c ^= d[i];
    for (uint8_t b = 0; b < 8; b++)
      c = (c & 0x80) ? (uint8_t)((c << 1) ^ 0x07) : (uint8_t)(c << 1);
  }
  return c;
}

// ---- pure logic: no pins, no timers. Testable without any radio. ----

static uint16_t putBit(bool one, bool *out, uint16_t n) {
  out[n++] = !one;                    // first half
  out[n++] = one;                     // second half
  return n;
}

static uint16_t putByte(uint8_t v, bool *out, uint16_t n) {
  for (int8_t i = 7; i >= 0; i--) n = putBit((v >> i) & 1, out, n);
  return n;
}

// Fills 'out' with the line level of every half-bit. Returns how many, 0 on bad length.
uint16_t askEncode(const uint8_t *data, uint8_t len, bool *out) {
  if (len == 0 || len > ASK_MAX_LEN) return 0;
  uint8_t body[ASK_MAX_LEN + 1];
  body[0] = len;
  for (uint8_t i = 0; i < len; i++) body[i + 1] = data[i];

  uint16_t n = 0;
  for (uint8_t i = 0; i < ASK_PREAMBLE; i++) n = putBit(true, out, n);
  n = putByte(ASK_SYNC, out, n);
  for (uint8_t i = 0; i <= len; i++) n = putByte(body[i], out, n);
  n = putByte(crc8(body, len + 1), out, n);
  return n;
}

class AskDecoder {
 public:
  void begin(uint32_t bitUs) { bitUs_ = bitUs; last_ = 0; ready_ = false; crcErrors_ = 0; reset(); }

  // Call on every level change; 'level' is the level AFTER the edge.
  void onEdge(bool level, uint32_t nowUs) {
    uint32_t dt = nowUs - last_;
    last_ = nowUs;
    if (ready_) return;                               // last frame not collected yet
    if (dt > bitUs_ / 4 && dt < bitUs_ * 3 / 4) {      // half a bit
      mid_ = !mid_;
      if (mid_) pushBit(level);
    } else if (dt >= bitUs_ * 3 / 4 && dt < bitUs_ * 5 / 4) {   // a whole bit
      mid_ = true;                                    // a long gap always ends mid-bit
      pushBit(level);
    } else {
      reset();                                        // noise, or the gap between frames
    }
  }

  bool available() const { return ready_; }
  uint16_t crcErrors() const { return crcErrors_; }

  // Copies the payload out and frees the decoder for the next frame.
  uint8_t read(uint8_t *out) {
    if (!ready_) return 0;
    uint8_t len = buf_[0];
    for (uint8_t i = 0; i < len; i++) out[i] = buf_[i + 1];
    ready_ = false;
    return len;
  }

 private:
  void reset() { synced_ = false; mid_ = false; sh_ = 0; nbits_ = 0; nbytes_ = 0; }

  void pushBit(bool b) {
    sh_ = (uint8_t)((sh_ << 1) | (b ? 1 : 0));
    if (!synced_) {
      if (sh_ == ASK_SYNC) { synced_ = true; nbits_ = 0; nbytes_ = 0; }
      return;
    }
    if (++nbits_ < 8) return;
    nbits_ = 0;
    buf_[nbytes_++] = sh_;
    if (buf_[0] == 0 || buf_[0] > ASK_MAX_LEN) { reset(); return; }
    if (nbytes_ == buf_[0] + 2) {                     // LEN + payload + CRC all in
      if (crc8(buf_, buf_[0] + 1) == buf_[nbytes_ - 1]) ready_ = true;
      else crcErrors_++;
      reset();
    }
  }

  uint32_t bitUs_, last_;
  volatile bool ready_;
  bool synced_, mid_;
  uint8_t sh_, nbits_, nbytes_;
  uint8_t buf_[ASK_MAX_LEN + 2];
  uint16_t crcErrors_;
};

// ===== 6.4  Self-test of the core: no radio, no second board, only Serial =====
// The encoder output is turned into edges 500 us apart and fed straight into
// the decoder. Expected on the serial monitor at 115200 baud:
//   frame: 144 half-bits = 72 bits = 72 ms on the air
//   test 1 (round trip): PASS
//   test 2 (one half-bit flipped): PASS, frame rejected

const uint32_t BIT_US  = 1000;                 // 1 kbit/s
const uint32_t HALF_US = BIT_US / 2;
const uint16_t FLIP_AT = 70;                   // a half-bit inside the payload

const uint8_t EXAMPLE[4] = { 0x01, 0x00, 0xEA, 0x37 };   // node 1, 23.4 degC, counter

bool       halfBits[ASK_MAX_HALF];
AskDecoder decoder;

// Feeds a half-bit list to the decoder exactly as the receiver pin would:
// one onEdge() call per level change, time-stamped on a 500 us grid.
void playFrame(const bool *h, uint16_t n, uint32_t t0) {
  bool level = false;                          // the line idles low
  for (uint16_t i = 0; i < n; i++) {
    if (h[i] == level) continue;               // no change, so no edge
    level = h[i];
    decoder.onEdge(level, t0 + (uint32_t)i * HALF_US);
  }
  if (level) decoder.onEdge(false, t0 + (uint32_t)n * HALF_US);   // back to idle
}

bool samePayload(const uint8_t *a, const uint8_t *b, uint8_t n) {
  for (uint8_t i = 0; i < n; i++) {
    if (a[i] != b[i]) return false;
  }
  return true;
}

void setup() {
  Serial.begin(115200);
  delay(500);
  decoder.begin(BIT_US);

  uint16_t n = askEncode(EXAMPLE, sizeof(EXAMPLE), halfBits);
  Serial.printf("frame: %u half-bits = %u bits = %lu ms on the air\n",
                n, n / 2, (unsigned long)(n / 2) * BIT_US / 1000);

  // Test 1: what goes into the encoder must come out of the decoder.
  uint8_t got[ASK_MAX_LEN];
  uint8_t len = 0;
  playFrame(halfBits, n, 100000UL);
  if (decoder.available()) len = decoder.read(got);
  bool ok1 = (n == 144) && (len == sizeof(EXAMPLE)) && samePayload(got, EXAMPLE, len);
  Serial.printf("test 1 (round trip): %s\n", ok1 ? "PASS" : "FAIL");

  // Test 2: one damaged half-bit must not produce a frame.
  halfBits[FLIP_AT] = !halfBits[FLIP_AT];
  playFrame(halfBits, n, 300000UL);            // well after the first frame ended
  bool ok2 = !decoder.available();
  Serial.printf("test 2 (one half-bit flipped): %s\n",
                ok2 ? "PASS, frame rejected" : "FAIL, frame accepted");
}

void loop() {
}

N = 8·(n + 3)
PER = 1 − (1 − p)N
Ploss = PERk
Tf = (16 + N)·Tb
Tbusy = k·Tf + (k − 1)·Tgap
D = TbusyTp
1.0·10⁻³
3
4 B
0
0
0 / 0
—
—
—
—
0
—
—
—
—
—
—
—
—
—
—
—
—


N = 8·(n + 3) = 8·(4 + 3) = 56

PER = 1 − (1 − p)N
PER = 1 − (1 − 0.001)56 = 1 − 0.99956
PER = 1 − 0.9455 = 0.0545

Ploss = PERk = 0.05453 = 1.62·10−4
1Ploss = 11.62·10−4 ≈ 6200
6200 · 2 s = 12400 s ≈ 3.4 h

Tf = (16 + N)·Tb = (16 + 56)·1 ms = 72 ms
Tbusy = k·Tf + (k − 1)·Tgap
Tbusy = 3·72 ms + 2·30 ms = 276 ms

D = TbusyTp = 276 ms2000 ms = 0.138 = 13.8 %
N = 56
PER = 0.0545 = 5.45 %
Ploss = 1.62·10−4
Tbusy = 276 ms
D = 13.8 %
ESP32 · Arduino C++ · sender (sensor node)
// AskLink core - Manchester-coded frames for a 433 MHz ASK (OOK) module pair.
// Frame on the air:  16 preamble bits '1' | sync 0x2D | LEN | payload | CRC-8
// Manchester: '1' = low then high, '0' = high then low (the edge sits mid-bit).

const uint8_t  ASK_SYNC     = 0x2D;
const uint8_t  ASK_MAX_LEN  = 16;     // payload bytes
const uint8_t  ASK_PREAMBLE = 16;     // '1' bits that let the receiver gain settle
const uint16_t ASK_MAX_HALF = 2 * (ASK_PREAMBLE + 8 * (ASK_MAX_LEN + 3));

// CRC-8, polynomial 0x07, start value 0. Check: "123456789" gives 0xF4.
uint8_t crc8(const uint8_t *d, uint8_t n) {
  uint8_t c = 0;
  for (uint8_t i = 0; i < n; i++) {
    c ^= d[i];
    for (uint8_t b = 0; b < 8; b++)
      c = (c & 0x80) ? (uint8_t)((c << 1) ^ 0x07) : (uint8_t)(c << 1);
  }
  return c;
}

// ---- pure logic: no pins, no timers. Testable without any radio. ----

static uint16_t putBit(bool one, bool *out, uint16_t n) {
  out[n++] = !one;                    // first half
  out[n++] = one;                     // second half
  return n;
}

static uint16_t putByte(uint8_t v, bool *out, uint16_t n) {
  for (int8_t i = 7; i >= 0; i--) n = putBit((v >> i) & 1, out, n);
  return n;
}

// Fills 'out' with the line level of every half-bit. Returns how many, 0 on bad length.
uint16_t askEncode(const uint8_t *data, uint8_t len, bool *out) {
  if (len == 0 || len > ASK_MAX_LEN) return 0;
  uint8_t body[ASK_MAX_LEN + 1];
  body[0] = len;
  for (uint8_t i = 0; i < len; i++) body[i + 1] = data[i];

  uint16_t n = 0;
  for (uint8_t i = 0; i < ASK_PREAMBLE; i++) n = putBit(true, out, n);
  n = putByte(ASK_SYNC, out, n);
  for (uint8_t i = 0; i <= len; i++) n = putByte(body[i], out, n);
  n = putByte(crc8(body, len + 1), out, n);
  return n;
}

class AskDecoder {
 public:
  void begin(uint32_t bitUs) { bitUs_ = bitUs; last_ = 0; ready_ = false; crcErrors_ = 0; reset(); }

  // Call on every level change; 'level' is the level AFTER the edge.
  void onEdge(bool level, uint32_t nowUs) {
    uint32_t dt = nowUs - last_;
    last_ = nowUs;
    if (ready_) return;                               // last frame not collected yet
    if (dt > bitUs_ / 4 && dt < bitUs_ * 3 / 4) {      // half a bit
      mid_ = !mid_;
      if (mid_) pushBit(level);
    } else if (dt >= bitUs_ * 3 / 4 && dt < bitUs_ * 5 / 4) {   // a whole bit
      mid_ = true;                                    // a long gap always ends mid-bit
      pushBit(level);
    } else {
      reset();                                        // noise, or the gap between frames
    }
  }

  bool available() const { return ready_; }
  uint16_t crcErrors() const { return crcErrors_; }

  // Copies the payload out and frees the decoder for the next frame.
  uint8_t read(uint8_t *out) {
    if (!ready_) return 0;
    uint8_t len = buf_[0];
    for (uint8_t i = 0; i < len; i++) out[i] = buf_[i + 1];
    ready_ = false;
    return len;
  }

 private:
  void reset() { synced_ = false; mid_ = false; sh_ = 0; nbits_ = 0; nbytes_ = 0; }

  void pushBit(bool b) {
    sh_ = (uint8_t)((sh_ << 1) | (b ? 1 : 0));
    if (!synced_) {
      if (sh_ == ASK_SYNC) { synced_ = true; nbits_ = 0; nbytes_ = 0; }
      return;
    }
    if (++nbits_ < 8) return;
    nbits_ = 0;
    buf_[nbytes_++] = sh_;
    if (buf_[0] == 0 || buf_[0] > ASK_MAX_LEN) { reset(); return; }
    if (nbytes_ == buf_[0] + 2) {                     // LEN + payload + CRC all in
      if (crc8(buf_, buf_[0] + 1) == buf_[nbytes_ - 1]) ready_ = true;
      else crcErrors_++;
      reset();
    }
  }

  uint32_t bitUs_, last_;
  volatile bool ready_;
  bool synced_, mid_;
  uint8_t sh_, nbits_, nbytes_;
  uint8_t buf_[ASK_MAX_LEN + 2];
  uint16_t crcErrors_;
};

// ===========================================================================
// 6.5  Sensor node - the SENDER.  GPIO 25 -> DATA input of the transmitter.
// One reading every 2 s, sent 3 times, 30 ms apart. Nobody answers: this node
// never learns whether any copy arrived, so it simply says everything 3 times.
// ===========================================================================

const int      PIN_TX    = 25;
const uint32_t HALF_US   = 500;      // half a bit at 1 kbit/s
const uint32_t PERIOD_MS = 2000;     // one reading every 2 s
const uint8_t  COPIES    = 3;        // every reading goes out 3 times
const uint32_t GAP_MS    = 30;       // silence between copies
const uint8_t  NODE_ID   = 1;

bool     halfBits[ASK_MAX_HALF];     // line level of every half-bit of one frame
uint32_t lastReading = 0;
uint8_t  counter     = 0;            // rolling counter: the same in all 3 copies
int16_t  testTenths  = 200;          // TEST RAMP 20.0 .. 30.0 C - not a sensor yet.
                                     // The LM35 replaces it in 6.6.

// Plays one frame on the pin. It BLOCKS for the whole frame: 72 ms for 4 bytes.
// Acceptable here because this node has nothing else to do while it talks.
void sendFrame(const uint8_t *data, uint8_t len) {
  uint16_t n = askEncode(data, len, halfBits);
  for (uint16_t i = 0; i < n; i++) {
    digitalWrite(PIN_TX, halfBits[i] ? HIGH : LOW);
    delayMicroseconds(HALF_US);
  }
  digitalWrite(PIN_TX, LOW);         // carrier off: the air is free again
}

void setup() {
  Serial.begin(115200);
  pinMode(PIN_TX, OUTPUT);
  digitalWrite(PIN_TX, LOW);
  lastReading = millis();
}

void loop() {
  // millis() schedules the readings, so the 2 s period does not stretch by
  // the time the sending itself takes.
  if (millis() - lastReading < PERIOD_MS) return;
  lastReading += PERIOD_MS;

  uint8_t payload[4];
  payload[0] = NODE_ID;
  payload[1] = (uint8_t)(testTenths >> 8);       // temperature in tenths, high byte
  payload[2] = (uint8_t)(testTenths & 0xFF);     // low byte
  payload[3] = counter;

  for (uint8_t i = 0; i < COPIES; i++) {
    sendFrame(payload, 4);
    if (i + 1 < COPIES) delay(GAP_MS);           // short and bounded: 2 gaps of 30 ms
  }

  Serial.printf("sent #%u  T=%.1f C  payload %02X %02X %02X %02X  x%u\n",
                counter, testTenths / 10.0,
                payload[0], payload[1], payload[2], payload[3], COPIES);

  counter++;                                     // wraps 255 -> 0 by itself
  testTenths++;
  if (testTenths > 300) testTenths = 200;
}
ESP32 · Arduino C++ · receiver (display node)
// AskLink core - Manchester-coded frames for a 433 MHz ASK (OOK) module pair.
// Frame on the air:  16 preamble bits '1' | sync 0x2D | LEN | payload | CRC-8
// Manchester: '1' = low then high, '0' = high then low (the edge sits mid-bit).

const uint8_t  ASK_SYNC     = 0x2D;
const uint8_t  ASK_MAX_LEN  = 16;     // payload bytes
const uint8_t  ASK_PREAMBLE = 16;     // '1' bits that let the receiver gain settle
const uint16_t ASK_MAX_HALF = 2 * (ASK_PREAMBLE + 8 * (ASK_MAX_LEN + 3));

// CRC-8, polynomial 0x07, start value 0. Check: "123456789" gives 0xF4.
uint8_t crc8(const uint8_t *d, uint8_t n) {
  uint8_t c = 0;
  for (uint8_t i = 0; i < n; i++) {
    c ^= d[i];
    for (uint8_t b = 0; b < 8; b++)
      c = (c & 0x80) ? (uint8_t)((c << 1) ^ 0x07) : (uint8_t)(c << 1);
  }
  return c;
}

// ---- pure logic: no pins, no timers. Testable without any radio. ----

static uint16_t putBit(bool one, bool *out, uint16_t n) {
  out[n++] = !one;                    // first half
  out[n++] = one;                     // second half
  return n;
}

static uint16_t putByte(uint8_t v, bool *out, uint16_t n) {
  for (int8_t i = 7; i >= 0; i--) n = putBit((v >> i) & 1, out, n);
  return n;
}

// Fills 'out' with the line level of every half-bit. Returns how many, 0 on bad length.
uint16_t askEncode(const uint8_t *data, uint8_t len, bool *out) {
  if (len == 0 || len > ASK_MAX_LEN) return 0;
  uint8_t body[ASK_MAX_LEN + 1];
  body[0] = len;
  for (uint8_t i = 0; i < len; i++) body[i + 1] = data[i];

  uint16_t n = 0;
  for (uint8_t i = 0; i < ASK_PREAMBLE; i++) n = putBit(true, out, n);
  n = putByte(ASK_SYNC, out, n);
  for (uint8_t i = 0; i <= len; i++) n = putByte(body[i], out, n);
  n = putByte(crc8(body, len + 1), out, n);
  return n;
}

class AskDecoder {
 public:
  void begin(uint32_t bitUs) { bitUs_ = bitUs; last_ = 0; ready_ = false; crcErrors_ = 0; reset(); }

  // Call on every level change; 'level' is the level AFTER the edge.
  void onEdge(bool level, uint32_t nowUs) {
    uint32_t dt = nowUs - last_;
    last_ = nowUs;
    if (ready_) return;                               // last frame not collected yet
    if (dt > bitUs_ / 4 && dt < bitUs_ * 3 / 4) {      // half a bit
      mid_ = !mid_;
      if (mid_) pushBit(level);
    } else if (dt >= bitUs_ * 3 / 4 && dt < bitUs_ * 5 / 4) {   // a whole bit
      mid_ = true;                                    // a long gap always ends mid-bit
      pushBit(level);
    } else {
      reset();                                        // noise, or the gap between frames
    }
  }

  bool available() const { return ready_; }
  uint16_t crcErrors() const { return crcErrors_; }

  // Copies the payload out and frees the decoder for the next frame.
  uint8_t read(uint8_t *out) {
    if (!ready_) return 0;
    uint8_t len = buf_[0];
    for (uint8_t i = 0; i < len; i++) out[i] = buf_[i + 1];
    ready_ = false;
    return len;
  }

 private:
  void reset() { synced_ = false; mid_ = false; sh_ = 0; nbits_ = 0; nbytes_ = 0; }

  void pushBit(bool b) {
    sh_ = (uint8_t)((sh_ << 1) | (b ? 1 : 0));
    if (!synced_) {
      if (sh_ == ASK_SYNC) { synced_ = true; nbits_ = 0; nbytes_ = 0; }
      return;
    }
    if (++nbits_ < 8) return;
    nbits_ = 0;
    buf_[nbytes_++] = sh_;
    if (buf_[0] == 0 || buf_[0] > ASK_MAX_LEN) { reset(); return; }
    if (nbytes_ == buf_[0] + 2) {                     // LEN + payload + CRC all in
      if (crc8(buf_, buf_[0] + 1) == buf_[nbytes_ - 1]) ready_ = true;
      else crcErrors_++;
      reset();
    }
  }

  uint32_t bitUs_, last_;
  volatile bool ready_;
  bool synced_, mid_;
  uint8_t sh_, nbits_, nbytes_;
  uint8_t buf_[ASK_MAX_LEN + 2];
  uint16_t crcErrors_;
};

// ===========================================================================
// 6.5  Display node - the RECEIVER.
// Receiver DATA (5 V) -> 10k / 20k divider (3.33 V) -> GPIO 27.
// A frame that fails the CRC never leaves the decoder, so this code can only
// ever see good frames. Its own job: drop the duplicate copies, count the
// readings that never came, and notice when the data on show has grown old.
// ===========================================================================

const int      PIN_RX    = 27;
const uint32_t BIT_US    = 1000;     // 1 kbit/s
const uint8_t  NODE_ID   = 1;
const uint32_t PERIOD_MS = 2000;                 // the sender's reporting period
const uint32_t STALE_MS  = 3 * PERIOD_MS + 500;  // 3 readings in a row missing

AskDecoder decoder;

bool     haveReading = false;        // false until the first good frame
uint8_t  lastCounter = 0;
uint32_t readings    = 0;            // different readings delivered
uint32_t missed      = 0;            // readings that never arrived
uint32_t lastGoodMs  = 0;
bool     staleShown  = false;

// Runs on every edge of the data line - also on the noise edges that the
// receiver module produces whenever nobody is transmitting.
void IRAM_ATTR isr() { decoder.onEdge(digitalRead(PIN_RX), micros()); }

void setup() {
  Serial.begin(115200);
  pinMode(PIN_RX, INPUT);
  decoder.begin(BIT_US);
  attachInterrupt(PIN_RX, isr, CHANGE);
}

void handleFrame(const uint8_t *p, uint8_t len) {
  // The CRC-8 lets about 1 random bad frame in 256 through: check what we can.
  if (len != 4 || p[0] != NODE_ID) return;

  uint8_t counter = p[3];
  lastGoodMs = millis();             // any good copy proves the link is alive
  staleShown = false;

  // The 3 copies of one reading carry the same counter: keep the first only.
  if (haveReading && counter == lastCounter) return;

  // A jump in the counter is the number of readings lost on the way.
  // The uint8_t cast makes the wrap 255 -> 0 come out as 0 missed.
  // (256 or more lost in a row, or a sender restart, cannot be told apart.)
  if (haveReading) missed += (uint8_t)(counter - lastCounter - 1);

  haveReading = true;
  lastCounter = counter;
  readings++;

  int16_t tenths = (int16_t)((p[1] << 8) | p[2]);
  Serial.printf("#%u  T=%.1f C  readings=%lu  missed=%lu  crcErrors=%u\n",
                counter, tenths / 10.0,
                (unsigned long)readings, (unsigned long)missed, decoder.crcErrors());
}

void loop() {
  if (decoder.available()) {
    uint8_t buf[ASK_MAX_LEN];
    noInterrupts();                  // the ISR must not touch the buffer now
    uint8_t n = decoder.read(buf);
    interrupts();
    handleFrame(buf, n);
  }

  // No acknowledgement exists, so silence is the only alarm there is.
  if (haveReading && !staleShown && millis() - lastGoodMs > STALE_MS) {
    staleShown = true;
    Serial.println("no frame for 3 periods - the value on show is OLD");
  }
}

V = 10 mV/°C · T
n = V1 mV
tout = 3 · Trep
23.4 °C
6 s
—
—
—
—
—
—
—
—
—
—
—
—
—
—
—
—
—
—


V = S · T = 10 mV/°C · 23.4 °C = 234 mV

ΔV = 10 mV/°C · 0.1 °C = 1 mV
n = VΔV = 234 mV1 mV = 234

hi = ⌊n256⌋ = ⌊234256⌋ = 0 = 0x00
lo = n − 256 · hi = 234 − 256 · 0 = 234 = 0xEA
n = 0x00EA

n = 256 · hi + lo = 256 · 0 + 234 = 234
d10 = ⌊n100⌋ = ⌊234100⌋ = 2
d1 = ⌊n − 100 · d1010⌋ = ⌊3410⌋ = 3
d0.1 = 34 − 10 · 3 = 4

tout = N · Trep = 3 · 2 s = 6 s
ESP32 · Arduino C++
// 6.6  Sensor node: LM35 -> 4-byte payload -> 433.92 MHz ASK link.
// ESP32 core 3.x. The link code below is the shared AskLink core, unchanged.

// AskLink core - Manchester-coded frames for a 433 MHz ASK (OOK) module pair.
// Frame on the air:  16 preamble bits '1' | sync 0x2D | LEN | payload | CRC-8
// Manchester: '1' = low then high, '0' = high then low (the edge sits mid-bit).

const uint8_t  ASK_SYNC     = 0x2D;
const uint8_t  ASK_MAX_LEN  = 16;     // payload bytes
const uint8_t  ASK_PREAMBLE = 16;     // '1' bits that let the receiver gain settle
const uint16_t ASK_MAX_HALF = 2 * (ASK_PREAMBLE + 8 * (ASK_MAX_LEN + 3));

// CRC-8, polynomial 0x07, start value 0. Check: "123456789" gives 0xF4.
uint8_t crc8(const uint8_t *d, uint8_t n) {
  uint8_t c = 0;
  for (uint8_t i = 0; i < n; i++) {
    c ^= d[i];
    for (uint8_t b = 0; b < 8; b++)
      c = (c & 0x80) ? (uint8_t)((c << 1) ^ 0x07) : (uint8_t)(c << 1);
  }
  return c;
}

// ---- pure logic: no pins, no timers. Testable without any radio. ----

static uint16_t putBit(bool one, bool *out, uint16_t n) {
  out[n++] = !one;                    // first half
  out[n++] = one;                     // second half
  return n;
}

static uint16_t putByte(uint8_t v, bool *out, uint16_t n) {
  for (int8_t i = 7; i >= 0; i--) n = putBit((v >> i) & 1, out, n);
  return n;
}

// Fills 'out' with the line level of every half-bit. Returns how many, 0 on bad length.
uint16_t askEncode(const uint8_t *data, uint8_t len, bool *out) {
  if (len == 0 || len > ASK_MAX_LEN) return 0;
  uint8_t body[ASK_MAX_LEN + 1];
  body[0] = len;
  for (uint8_t i = 0; i < len; i++) body[i + 1] = data[i];

  uint16_t n = 0;
  for (uint8_t i = 0; i < ASK_PREAMBLE; i++) n = putBit(true, out, n);
  n = putByte(ASK_SYNC, out, n);
  for (uint8_t i = 0; i <= len; i++) n = putByte(body[i], out, n);
  n = putByte(crc8(body, len + 1), out, n);
  return n;
}

class AskDecoder {
 public:
  void begin(uint32_t bitUs) { bitUs_ = bitUs; last_ = 0; ready_ = false; crcErrors_ = 0; reset(); }

  // Call on every level change; 'level' is the level AFTER the edge.
  void onEdge(bool level, uint32_t nowUs) {
    uint32_t dt = nowUs - last_;
    last_ = nowUs;
    if (ready_) return;                               // last frame not collected yet
    if (dt > bitUs_ / 4 && dt < bitUs_ * 3 / 4) {      // half a bit
      mid_ = !mid_;
      if (mid_) pushBit(level);
    } else if (dt >= bitUs_ * 3 / 4 && dt < bitUs_ * 5 / 4) {   // a whole bit
      mid_ = true;                                    // a long gap always ends mid-bit
      pushBit(level);
    } else {
      reset();                                        // noise, or the gap between frames
    }
  }

  bool available() const { return ready_; }
  uint16_t crcErrors() const { return crcErrors_; }

  // Copies the payload out and frees the decoder for the next frame.
  uint8_t read(uint8_t *out) {
    if (!ready_) return 0;
    uint8_t len = buf_[0];
    for (uint8_t i = 0; i < len; i++) out[i] = buf_[i + 1];
    ready_ = false;
    return len;
  }

 private:
  void reset() { synced_ = false; mid_ = false; sh_ = 0; nbits_ = 0; nbytes_ = 0; }

  void pushBit(bool b) {
    sh_ = (uint8_t)((sh_ << 1) | (b ? 1 : 0));
    if (!synced_) {
      if (sh_ == ASK_SYNC) { synced_ = true; nbits_ = 0; nbytes_ = 0; }
      return;
    }
    if (++nbits_ < 8) return;
    nbits_ = 0;
    buf_[nbytes_++] = sh_;
    if (buf_[0] == 0 || buf_[0] > ASK_MAX_LEN) { reset(); return; }
    if (nbytes_ == buf_[0] + 2) {                     // LEN + payload + CRC all in
      if (crc8(buf_, buf_[0] + 1) == buf_[nbytes_ - 1]) ready_ = true;
      else crcErrors_++;
      reset();
    }
  }

  uint32_t bitUs_, last_;
  volatile bool ready_;
  bool synced_, mid_;
  uint8_t sh_, nbits_, nbytes_;
  uint8_t buf_[ASK_MAX_LEN + 2];
  uint16_t crcErrors_;
};

// ---- application: the sensor node ----

const int      PIN_TX      = 25;      // DATA input of the transmitter module
const int      PIN_LM35    = 34;      // LM35 output, ADC1, input-only pin
const uint32_t BIT_US      = 1000;    // 1 kbit/s
const uint8_t  NODE_ID     = 1;
const uint32_t REPORT_MS   = 2000;    // a new reading every 2 s
const uint8_t  COPIES      = 3;       // every reading is sent three times
const uint32_t COPY_GAP_MS = 30;
const uint8_t  SAMPLES     = 16;

bool     halves[ASK_MAX_HALF];
uint8_t  counter    = 0;              // rolling counter: lets the receiver drop copies
uint32_t lastReport = 0;

// LM35: 10 mV per degree, so 1 mV is 0.1 degree. The averaged millivolt
// reading IS the temperature in tenths of a degree - no division, no float.
// With a single positive supply this circuit cannot read below 0 degrees.
int16_t readTenths() {
  uint32_t sum = 0;
  for (uint8_t i = 0; i < SAMPLES; i++) sum += analogReadMilliVolts(PIN_LM35);
  return (int16_t)((sum + SAMPLES / 2) / SAMPLES);    // rounded average
}

// Blocks for the whole frame: 72 bits at 1 ms = 72 ms for a 4-byte payload.
// Acceptable here because this node has nothing else to do while it sends.
void sendFrame(const uint8_t *data, uint8_t len) {
  uint16_t n = askEncode(data, len, halves);
  for (uint16_t i = 0; i < n; i++) {
    digitalWrite(PIN_TX, halves[i] ? HIGH : LOW);
    delayMicroseconds(BIT_US / 2);
  }
  digitalWrite(PIN_TX, LOW);          // carrier off between frames
}

void setup() {
  Serial.begin(115200);
  pinMode(PIN_TX, OUTPUT);
  digitalWrite(PIN_TX, LOW);
}

void loop() {
  uint32_t now = millis();
  if (now - lastReport < REPORT_MS) return;
  lastReport = now;

  int16_t tenths = readTenths();
  uint8_t payload[4];
  payload[0] = NODE_ID;
  payload[1] = (uint8_t)((uint16_t)tenths >> 8);      // high byte first (big-endian)
  payload[2] = (uint8_t)((uint16_t)tenths & 0xFF);
  payload[3] = counter++;

  for (uint8_t c = 0; c < COPIES; c++) {
    sendFrame(payload, 4);
    if (c + 1 < COPIES) delay(COPY_GAP_MS);
  }

  Serial.printf("%d mV = %d tenths   payload %02X %02X %02X %02X\n",
                tenths, tenths, payload[0], payload[1], payload[2], payload[3]);
}
// 6.6  Display node: 433.92 MHz ASK link -> MAX7219 eight-digit display.
// ESP32 core 3.x. The link code below is the shared AskLink core, unchanged.

#include <SPI.h>

// AskLink core - Manchester-coded frames for a 433 MHz ASK (OOK) module pair.
// Frame on the air:  16 preamble bits '1' | sync 0x2D | LEN | payload | CRC-8
// Manchester: '1' = low then high, '0' = high then low (the edge sits mid-bit).

const uint8_t  ASK_SYNC     = 0x2D;
const uint8_t  ASK_MAX_LEN  = 16;     // payload bytes
const uint8_t  ASK_PREAMBLE = 16;     // '1' bits that let the receiver gain settle
const uint16_t ASK_MAX_HALF = 2 * (ASK_PREAMBLE + 8 * (ASK_MAX_LEN + 3));

// CRC-8, polynomial 0x07, start value 0. Check: "123456789" gives 0xF4.
uint8_t crc8(const uint8_t *d, uint8_t n) {
  uint8_t c = 0;
  for (uint8_t i = 0; i < n; i++) {
    c ^= d[i];
    for (uint8_t b = 0; b < 8; b++)
      c = (c & 0x80) ? (uint8_t)((c << 1) ^ 0x07) : (uint8_t)(c << 1);
  }
  return c;
}

// ---- pure logic: no pins, no timers. Testable without any radio. ----

static uint16_t putBit(bool one, bool *out, uint16_t n) {
  out[n++] = !one;                    // first half
  out[n++] = one;                     // second half
  return n;
}

static uint16_t putByte(uint8_t v, bool *out, uint16_t n) {
  for (int8_t i = 7; i >= 0; i--) n = putBit((v >> i) & 1, out, n);
  return n;
}

// Fills 'out' with the line level of every half-bit. Returns how many, 0 on bad length.
uint16_t askEncode(const uint8_t *data, uint8_t len, bool *out) {
  if (len == 0 || len > ASK_MAX_LEN) return 0;
  uint8_t body[ASK_MAX_LEN + 1];
  body[0] = len;
  for (uint8_t i = 0; i < len; i++) body[i + 1] = data[i];

  uint16_t n = 0;
  for (uint8_t i = 0; i < ASK_PREAMBLE; i++) n = putBit(true, out, n);
  n = putByte(ASK_SYNC, out, n);
  for (uint8_t i = 0; i <= len; i++) n = putByte(body[i], out, n);
  n = putByte(crc8(body, len + 1), out, n);
  return n;
}

class AskDecoder {
 public:
  void begin(uint32_t bitUs) { bitUs_ = bitUs; last_ = 0; ready_ = false; crcErrors_ = 0; reset(); }

  // Call on every level change; 'level' is the level AFTER the edge.
  void onEdge(bool level, uint32_t nowUs) {
    uint32_t dt = nowUs - last_;
    last_ = nowUs;
    if (ready_) return;                               // last frame not collected yet
    if (dt > bitUs_ / 4 && dt < bitUs_ * 3 / 4) {      // half a bit
      mid_ = !mid_;
      if (mid_) pushBit(level);
    } else if (dt >= bitUs_ * 3 / 4 && dt < bitUs_ * 5 / 4) {   // a whole bit
      mid_ = true;                                    // a long gap always ends mid-bit
      pushBit(level);
    } else {
      reset();                                        // noise, or the gap between frames
    }
  }

  bool available() const { return ready_; }
  uint16_t crcErrors() const { return crcErrors_; }

  // Copies the payload out and frees the decoder for the next frame.
  uint8_t read(uint8_t *out) {
    if (!ready_) return 0;
    uint8_t len = buf_[0];
    for (uint8_t i = 0; i < len; i++) out[i] = buf_[i + 1];
    ready_ = false;
    return len;
  }

 private:
  void reset() { synced_ = false; mid_ = false; sh_ = 0; nbits_ = 0; nbytes_ = 0; }

  void pushBit(bool b) {
    sh_ = (uint8_t)((sh_ << 1) | (b ? 1 : 0));
    if (!synced_) {
      if (sh_ == ASK_SYNC) { synced_ = true; nbits_ = 0; nbytes_ = 0; }
      return;
    }
    if (++nbits_ < 8) return;
    nbits_ = 0;
    buf_[nbytes_++] = sh_;
    if (buf_[0] == 0 || buf_[0] > ASK_MAX_LEN) { reset(); return; }
    if (nbytes_ == buf_[0] + 2) {                     // LEN + payload + CRC all in
      if (crc8(buf_, buf_[0] + 1) == buf_[nbytes_ - 1]) ready_ = true;
      else crcErrors_++;
      reset();
    }
  }

  uint32_t bitUs_, last_;
  volatile bool ready_;
  bool synced_, mid_;
  uint8_t sh_, nbits_, nbytes_;
  uint8_t buf_[ASK_MAX_LEN + 2];
  uint16_t crcErrors_;
};

// ---- application: the display node ----

const int      PIN_RX     = 27;       // receiver DATA, through the 10k / 20k divider
const int      PIN_MOSI   = 23;       // the three display lines go through a level shifter
const int      PIN_SCK    = 18;
const int      PIN_CS     = 5;
const uint32_t BIT_US     = 1000;
const uint8_t  NODE_ID    = 1;
const uint32_t TIMEOUT_MS = 6000;     // three report periods of 2 s

// MAX7219 driven through raw registers, Code-B decode on all eight digits.
// On the common eight-digit module register 0x01 is the rightmost digit;
// check the module you hold.
class Max7219 {
 public:
  explicit Max7219(uint8_t cs) : cs_(cs) {}

  void begin() {
    pinMode(cs_, OUTPUT);
    digitalWrite(cs_, HIGH);
    write(REG_TEST, 0x00);            // display test off
    write(REG_SCAN, 0x07);            // scan all eight digits
    write(REG_DECODE, 0xFF);          // Code-B on every digit
    write(REG_INTENSITY, 0x04);       // moderate brightness
    for (uint8_t d = 0; d < 8; d++) write(REG_DIGIT0 + d, CODE_BLANK);
    write(REG_SHUTDOWN, 0x01);        // 1 = normal operation
  }

  // 234 -> "    23.4", -50 -> "    -5.0". Right-aligned, blanks elsewhere.
  // Any int16_t fits: the longest is -3276.8, six positions out of eight.
  void showTenths(int16_t tenths) {
    int32_t v = tenths;               // 32 bits, so that -32768 can be negated
    bool neg = v < 0;
    if (neg) v = -v;
    uint8_t pos = 0;                  // 0 = rightmost digit
    write(REG_DIGIT0 + pos++, (uint8_t)(v % 10));             // tenths
    v /= 10;
    write(REG_DIGIT0 + pos++, (uint8_t)(v % 10) | DP);        // units, with the point
    v /= 10;
    while (v > 0) {
      write(REG_DIGIT0 + pos++, (uint8_t)(v % 10));
      v /= 10;
    }
    if (neg) write(REG_DIGIT0 + pos++, CODE_MINUS);
    while (pos < 8) write(REG_DIGIT0 + pos++, CODE_BLANK);
  }

  // "----": there is no valid reading to show.
  void showLost() {
    for (uint8_t d = 0; d < 8; d++) write(REG_DIGIT0 + d, d < 4 ? CODE_MINUS : CODE_BLANK);
  }

 private:
  static const uint8_t REG_DIGIT0    = 0x01;   // 0x01..0x08
  static const uint8_t REG_DECODE    = 0x09;
  static const uint8_t REG_INTENSITY = 0x0A;
  static const uint8_t REG_SCAN      = 0x0B;
  static const uint8_t REG_SHUTDOWN  = 0x0C;
  static const uint8_t REG_TEST      = 0x0F;
  static const uint8_t CODE_MINUS    = 0x0A;
  static const uint8_t CODE_BLANK    = 0x0F;
  static const uint8_t DP            = 0x80;   // bit 7 lights the decimal point

  void write(uint8_t reg, uint8_t value) {
    SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
    digitalWrite(cs_, LOW);
    SPI.transfer(reg);
    SPI.transfer(value);
    digitalWrite(cs_, HIGH);          // the rising edge latches the 16 bits
    SPI.endTransaction();
  }

  uint8_t cs_;
};

AskDecoder decoder;
Max7219    display(PIN_CS);

bool     linkUp      = false;         // nothing received yet
uint32_t lastRxMs    = 0;
uint8_t  lastCounter = 0;

void IRAM_ATTR onRxEdge() { decoder.onEdge(digitalRead(PIN_RX), micros()); }

void setup() {
  Serial.begin(115200);
  SPI.begin(PIN_SCK, -1, PIN_MOSI, -1);         // no MISO: the display is write-only here
  display.begin();
  display.showLost();                 // honest start: no reading yet

  decoder.begin(BIT_US);
  pinMode(PIN_RX, INPUT);
  attachInterrupt(digitalPinToInterrupt(PIN_RX), onRxEdge, CHANGE);
}

void loop() {
  if (decoder.available()) {
    uint8_t buf[ASK_MAX_LEN];
    noInterrupts();
    uint8_t n = decoder.read(buf);
    interrupts();

    // The decoder hands over only frames whose CRC matched, so a frame that
    // failed the CRC can never reach the display. (A CRC-8 still lets about
    // one random corrupted frame in 256 through - hence the checks below.)
    if (n == 4 && buf[0] == NODE_ID) {
      bool fresh = !linkUp || buf[3] != lastCounter;   // a copy has the same counter
      lastRxMs = millis();            // a copy still proves that the link is alive
      linkUp = true;
      lastCounter = buf[3];
      if (fresh) {
        int16_t tenths = (int16_t)(((uint16_t)buf[1] << 8) | buf[2]);
        display.showTenths(tenths);
        Serial.printf("node %u  counter %u  %d tenths\n", buf[0], buf[3], tenths);
      }
    }
  }

  // Silence longer than the timeout: say so instead of showing an old number.
  // The third report is due exactly at 6000 ms, so a report that arrives a
  // little late still trips it; raise the constant slightly to forgive that.
  if (linkUp && millis() - lastRxMs >= TIMEOUT_MS) {
    linkUp = false;
    display.showLost();
    Serial.println("link lost");
  }
}
🏠 ♿