記事公開日

抵抗器を測定できる小型チェッカーを自作

「ブレッドボードで回路を組んだあと、抵抗を片づけるのが面倒すぎる!」

ということで、リード付き抵抗器を手軽に測定できる小型チェッカーを作ってみました。

スイッチをONにして抵抗を差し込むと、すぐに値が表示されます。

ただし、この表示値はあえて正確にはしていません。

たとえば「10.1kΩ」や「9.9kΩ」は、どちらも「10kΩ」と表示するようにしています。

これは、このチェッカーが正確な抵抗値を測定するためのものではなく、「どの袋に戻すか」を素早く判断するためのものだからです。

あえて一般的な値に丸めることで、細かい誤差を気にせず、直感的に仕分けできるようにしました。

使用部品

  • ATTINY1604-SSN
  • Miniブレットボード/ZY-55 - AliExpress
  • OLEDディスプレイ/0.91inch
  • CR2032コイン電池ホルダー
  • 横型スライドスイッチ
  • ピンヘッダ/4Pin
  • XHコネクタ/3Pin(UPDI書き込み用)
  • 抵抗/10kΩ
  • 積層セラミックコンデンサ/0.1μF
  • 銅箔テープ

回路図

プログラム

#include <Wire.h>
#include <SSD1306Ascii.h>
#include <SSD1306AsciiWire.h>
#include "fonts/allFonts.h"

#define OLED_ADDR 0x3C
#define RREF      10000UL
#define ADC_PIN   PIN_PA7

SSD1306AsciiWire oled;

// =====================================
// 表示位置調整
// =====================================
const int8_t CENTER_OFFSET = +16;

// =====================================
// ADC / フィルタ設定
// =====================================
const uint8_t ADC_SAMPLES = 15;
const uint8_t ADC_AVG_START = 4;
const uint8_t ADC_AVG_END   = 10;
const uint16_t ADC_SAMPLE_DELAY_US = 250;

const uint8_t RX_FILTER_NUM = 1;
const uint8_t RX_FILTER_DEN = 4;

const uint16_t LOOP_DELAY_MS = 120;

const uint16_t OPEN_ADC_TH  = 1000;
const uint16_t SHORT_ADC_TH = 5;
const uint8_t OPEN_SHORT_CONFIRM = 3;

// =====================================
// 2倍表示時の仮想文字幅
// =====================================
uint8_t charWidth2x(char c) {
  if ((c >= '0' && c <= '9') || c == 'k' || c == 'M') {
    return 16;
  }
  if (c == '.') {
    return 8;
  }
  return 16;
}

uint8_t textWidth2x(const char *s) {
  uint8_t w = 0;
  for (uint8_t i = 0; s[i]; i++) {
    w += charWidth2x(s[i]);
  }
  return w;
}

// =====================================
// ユーティリティ
// =====================================
void sortU16(uint16_t *arr, uint8_t n) {
  for (uint8_t i = 0; i < n - 1; i++) {
    for (uint8_t j = i + 1; j < n; j++) {
      if (arr[j] < arr[i]) {
        uint16_t t = arr[i];
        arr[i] = arr[j];
        arr[j] = t;
      }
    }
  }
}

uint32_t absDiffU32(uint32_t a, uint32_t b) {
  return (a > b) ? (a - b) : (b - a);
}

uint32_t pow10u8(uint8_t n) {
  uint32_t p = 1;
  while (n--) {
    p *= 10UL;
  }
  return p;
}

// =====================================
// 未挿入メッセージ
// =====================================
void showInsertMessage() {
  oled.clear();
  oled.setFont(System5x7);
  oled.set1X();

  const char* line1 = "Insert";
  const char* line2 = "Resistor";

  int16_t x1 = (128 - (int16_t)strlen(line1) * 6) / 2;
  int16_t x2 = (128 - (int16_t)strlen(line2) * 6) / 2;

  if (x1 < 0) x1 = 0;
  if (x2 < 0) x2 = 0;

  oled.setCursor(x1, 0);
  oled.print(line1);

  oled.setCursor(x2, 2);
  oled.print(line2);
}

// =====================================
// ADC複数回読み + 中央付近平均
// =====================================
uint16_t readAdcFiltered() {
  uint16_t samples[ADC_SAMPLES];

  analogRead(ADC_PIN);
  delayMicroseconds(ADC_SAMPLE_DELAY_US);

  for (uint8_t i = 0; i < ADC_SAMPLES; i++) {
    samples[i] = analogRead(ADC_PIN);
    delayMicroseconds(ADC_SAMPLE_DELAY_US);
  }

  sortU16(samples, ADC_SAMPLES);

  uint32_t sum = 0;
  uint8_t count = 0;
  for (uint8_t i = ADC_AVG_START; i <= ADC_AVG_END; i++) {
    sum += samples[i];
    count++;
  }

  return (uint16_t)(sum / count);
}

// =====================================
// ADC -> 抵抗値
// Rx = Rref * a / (1023 - a)
// =====================================
uint32_t adcToResistance(uint16_t a) {
  if (a >= 1023) return 0xFFFFFFFFUL;
  if (a == 0) return 0;

  return (uint32_t)RREF * a / (1023UL - a);
}

// =====================================
// 表示更新用しきい値
// =====================================
uint32_t getUpdateThreshold(uint32_t prevRx) {
  if (prevRx < 1000UL) {
    return 10;
  } else if (prevRx < 10000UL) {
    uint32_t t = prevRx / 50UL;
    if (t < 20) t = 20;
    return t;
  } else if (prevRx < 100000UL) {
    uint32_t t = prevRx * 3UL / 100UL;
    if (t < 500) t = 500;
    return t;
  } else if (prevRx < 1000000UL) {
    uint32_t t = prevRx * 5UL / 100UL;
    if (t < 5000) t = 5000;
    return t;
  } else {
    uint32_t t = prevRx * 8UL / 100UL;
    if (t < 50000) t = 50000;
    return t;
  }
}

// =====================================
// E12系列へ吸着
// 1.0, 1.2, 1.5, 1.8, 2.2, 2.7,
// 3.3, 3.9, 4.7, 5.6, 6.8, 8.2, 10.0
// =====================================
uint32_t snapToE12(uint32_t rx) {
  if (rx == 0) return 0;

  static const uint16_t e12x10[13] = {
    10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82, 100
  };

  uint32_t norm = rx;
  uint8_t exp10 = 0;

  while (norm >= 100UL) {
    norm /= 10UL;
    exp10++;
  }

  uint32_t best = e12x10[0];
  uint32_t bestDiff = absDiffU32(norm, e12x10[0]);

  for (uint8_t i = 1; i < 13; i++) {
    uint32_t diff = absDiffU32(norm, e12x10[i]);
    if (diff < bestDiff) {
      bestDiff = diff;
      best = e12x10[i];
    }
  }

  return best * pow10u8(exp10);
}

// =====================================
// 表示文字列作成
//
// unit:
//   0 = Ω
//   1 = kΩ(小数1桁)
//   2 = kΩ(整数)
//   3 = MΩ(小数1桁)
// =====================================
void buildDisplayText(uint32_t rx, uint8_t &unit, uint32_t &val, char *buf, uint8_t bufSize) {
  buf[0] = '\0';
  unit = 0;
  val = 0;

  uint32_t dispRx;

  // 1kΩ未満ではE12丸めをしない
  if (rx < 100UL) {
    dispRx = rx;
  } else {
    dispRx = snapToE12(rx);
  }

  if (dispRx < 1000UL) {
    unit = 0;
    val = dispRx;
    ultoa(val, buf, 10);
    return;
  }

  if (dispRx < 10000UL) {
    unit = 1;
    val = dispRx / 100UL;  // 0.1kΩ単位

    ultoa(val / 10UL, buf, 10);
    uint8_t idx = strlen(buf);

    if (idx < bufSize - 3) {
      buf[idx++] = '.';
      buf[idx++] = '0' + (val % 10UL);
      buf[idx++] = 'k';
      buf[idx]   = '\0';
    }
    return;
  }

  if (dispRx < 1000000UL) {
    unit = 2;
    val = dispRx / 1000UL;  // 整数kΩ

    ultoa(val, buf, 10);
    uint8_t idx = strlen(buf);

    if (idx < bufSize - 1) {
      buf[idx++] = 'k';
      buf[idx]   = '\0';
    }
    return;
  }

  unit = 3;
  val = dispRx / 100000UL;  // 0.1MΩ単位

  ultoa(val / 10UL, buf, 10);
  uint8_t idx = strlen(buf);

  if (idx < bufSize - 3) {
    buf[idx++] = '.';
    buf[idx++] = '0' + (val % 10UL);
    buf[idx++] = 'M';
    buf[idx]   = '\0';
  }
}

// =====================================
// 表示描画
// =====================================
void drawResistanceText(const char *buf) {
  const uint8_t OLED_W   = 128;
  const uint8_t OMEGA_W2 = 16;

  uint8_t w_num   = textWidth2x(buf);
  uint8_t total_w = w_num + OMEGA_W2;

  int16_t x = (OLED_W - total_w) / 2;
  x += CENTER_OFFSET;
  if (x < 0) x = 0;

  oled.setFont(Arial14);
  oled.set2X();
  oled.clear();

  oled.setCursor(x, 0);
  oled.print(buf);

  int16_t xOmega = x + w_num;
  oled.setCursor(xOmega, 0);
  oled.print((char)0xCE);
  oled.print((char)0xA9);
}

// =====================================
// setup
// =====================================
void setup() {
  delay(200);

  Wire.begin();
  delay(50);

  oled.begin(&Adafruit128x32, OLED_ADDR);
  oled.setFont(Arial14);
  oled.set2X();
  oled.clear();

  showInsertMessage();
}

// =====================================
// loop
// =====================================
void loop() {
  static bool prevOpenScreen = true;

  static uint8_t openCount  = 0;
  static uint8_t shortCount = 0;

  static uint32_t filteredRx = 0;
  static uint32_t prevRxShown = 0;
  static uint32_t prevVal = 0xFFFFFFFFUL;
  static uint8_t  prevUnit = 0xFF;

  uint16_t a = readAdcFiltered();

  bool isOpenNow  = (a >= OPEN_ADC_TH);
  bool isShortNow = (a <= SHORT_ADC_TH);

  if (isOpenNow) {
    if (openCount < 255) openCount++;
  } else {
    openCount = 0;
  }

  if (isShortNow) {
    if (shortCount < 255) shortCount++;
  } else {
    shortCount = 0;
  }

  if (isOpenNow || isShortNow) {
    if ((openCount >= OPEN_SHORT_CONFIRM) || (shortCount >= OPEN_SHORT_CONFIRM)) {
      if (!prevOpenScreen) {
        showInsertMessage();
        prevOpenScreen = true;
      }

      filteredRx = 0;
      prevRxShown = 0;
      prevVal = 0xFFFFFFFFUL;
      prevUnit = 0xFF;
    }

    delay(LOOP_DELAY_MS);
    return;
  }

  if (prevOpenScreen) {
    oled.clear();
    prevOpenScreen = false;
  }

  uint32_t rxRaw = adcToResistance(a);

  if (filteredRx == 0) {
    filteredRx = rxRaw;
  } else {
    filteredRx =
      (
        filteredRx * (RX_FILTER_DEN - RX_FILTER_NUM) +
        rxRaw * RX_FILTER_NUM
      ) / RX_FILTER_DEN;
  }

  uint32_t rx = filteredRx;

  if (prevRxShown != 0) {
    uint32_t diff = absDiffU32(rx, prevRxShown);
    uint32_t threshold = getUpdateThreshold(prevRxShown);

    if (diff < threshold) {
      delay(LOOP_DELAY_MS);
      return;
    }
  }

  char buf[16];
  uint8_t unit;
  uint32_t val;

  buildDisplayText(rx, unit, val, buf, sizeof(buf));

  if (val == prevVal && unit == prevUnit) {
    delay(LOOP_DELAY_MS);
    return;
  }

  prevVal = val;
  prevUnit = unit;
  prevRxShown = rx;

  drawResistanceText(buf);

  delay(LOOP_DELAY_MS);
}

小型のATTINY402も試しましたが、容量が足りなかったため、ATTINY1604を使用しました。

回路解説

分圧回路で電圧を読み取り、そこから抵抗値を計算しています。

基準抵抗として使用している10kΩと、測定する抵抗が同じ10kΩだった場合、マイコンは約1.5Vを計測します。

測定する抵抗値が基準抵抗より大きくなると、マイコンが計測する電圧も高くなります。

逆に、測定する抵抗値が基準抵抗より小さくなると、マイコンが計測する電圧は低くなります。

組み込み

基板とケースは、JLCPCBに注文しました。

→ 3Dプリントを外注してみた

特に難しいところはありませんが、抵抗のリードを差し込むブレッドボード部分だけ、少し工夫しています。

縦方向の列はどこに差しても導通するようにしたかったため、ブレッドボードを分解し、内部を銅箔テープで接続しました。

掛かった費用

  • 基板代:100円
  • ケース代:500円
  • OLEDディスプレイ:500円
  • IC(ATTINY1604):100円
  • CR2032電池&ケース:200円
  • スライドスイッチ:30円
  • ブレッドボード:200円
  • その他(抵抗/コンデンサ/コネクタ等):100円

総費用:約1,730円

関連情報

運営者プロフィール
コダマ

職業はIT系フリーランス。過去、電子配線業務の経験が10年ある為、はんだづけも得意です。宮崎県在住、40歳・2児の父親。

プロが教える!イチからわかるハンダ付けのコツ(工学社)の著者です。

カテゴリー