LED bar graph/A build and a check/10. A knob and a bar
A build and a check · 10 of 11

A knob and a bar

A potentiometer into A0, the bar on D4 to D13, and one line of arithmetic in between. That line is where the build goes subtly wrong: the obvious mapping never lights the tenth segment.

The build

A potentiometer with its outer pins across 5 V and ground, its wiper on A0, and the bar wired exactly as before. analogRead gives a number from 0 to 1023; the bar has ten segments; everything interesting is in the line that turns one into the other.

The line that goes wrong

Everybody writes map(raw, 0, 1023, 0, 10), and it is wrong in one specific, maddening way. map() does integer arithmetic, so it computes raw × 10 / 1023 and truncates — which reaches 10 only when raw is exactly 1023.

Which reading lights the tenth segment
raw 1023 → 10
analogRead()1023
The line in the sketch
Reading
1023
Segments lit
10
Top slice
1 count
The obvious line loses the top segment. map() is integer arithmetic: raw × 10 / 1023 only reaches 10 when raw is exactly 1023. Every other segment gets about 102 counts and the tenth gets one, so a knob wound all the way round shows nine and flickers to ten. Nothing is wrong with the wiring, the potentiometer or the bar.

Nine of the ten segments get about 102 counts each and the tenth gets one. A knob wound all the way to the stop shows nine, flickers briefly to ten, and goes back. Nothing is wrong with the wiring, the potentiometer or the bar.

The fix

Map against a full scale you can actually reach, and clamp:

int level = map(raw, 0, FULL_SCALE, 0, SEGMENTS);
level     = constrain(level, 0, SEGMENTS);

With FULL_SCALE at 1000, the top slice is 24 counts wide instead of one, and constrain stops anything above it asking for an eleventh segment.

The same trick works for any input whose real maximum is short of the ADC's. Print the raw reading, move the input through its whole range, and use the largest number you see.

Bar or dot, in one expression

bool on = BAR_MODE ? (i < level) : (i == level - 1);

Bar mode lights everything below the level; dot mode lights only the level itself. Dot mode draws a tenth of the current, which is the mode to use if the display is going to sit lit.

Rewriting all ten, every time

show() writes all ten pins on every pass, whether they changed or not. That is deliberate: ten digitalWrite calls take a few microseconds, and code that tracks which segments changed is code that can be wrong about it. Redraw the whole bar and there is no state to go stale.

The code

bar_graph_knob.ino

Turn the knob and the bar follows. Press nothing and change nothing else: BAR_MODE at the top switches between a filling column and a single moving segment.

// Wiring for this sketch.
//
//   Resistor board COMMON  -> Uno 5V
//   Resistor board row     -> one row of the bar's pins, segment for segment
//   Bar's other row        -> Uno D4 (segment 1) … D13 (segment 10)
//   Potentiometer          -> one end to 5V, the other to GND, wiper to A0
//
// Active low: LOW lights a segment, HIGH puts it out.
//
// Arduino IDE: Tools > Board > Arduino Uno, and the port your board is on.
// No library.

const int firstPin   = 4;      // segment 1
const int lastPin    = 13;     // segment 10
const int SEGMENTS   = 10;
const int KNOB       = A0;
const int FULL_SCALE = 1000;   // not 1023 - see the article
const bool BAR_MODE  = true;   // false for a single moving segment

void setup() {
  for (int pin = firstPin; pin <= lastPin; pin++) {
    pinMode(pin, OUTPUT);
    digitalWrite(pin, HIGH);
  }
}

void show(int level) {
  for (int i = 0; i < SEGMENTS; i++) {
    bool on = BAR_MODE ? (i < level) : (i == level - 1);
    digitalWrite(firstPin + i, on ? LOW : HIGH);
  }
}

void loop() {
  int raw   = analogRead(KNOB);                       // 0 … 1023
  int level = map(raw, 0, FULL_SCALE, 0, SEGMENTS);   // 0 … 10
  level     = constrain(level, 0, SEGMENTS);
  show(level);
  delay(20);
}

If the top segment never lights, you have edited FULL_SCALE back to 1023. If the bar jitters between two segments with the knob still, add a little hysteresis — only redraw when the new level differs from the old one.

When it does not work

The top segment never lights

FULL_SCALE is back at 1023. map() is integer arithmetic, so raw × 10 / 1023 only reaches 10 when the reading is exactly 1023 — one count in a thousand. Dividing by 1000 and constraining is what gives the tenth segment a slice the same size as the others.

The bar flickers between two segments when the knob is still

The reading is sitting on a boundary and the last bit of the ADC is noise. Redraw only when the level changes, and ignore a change of one that reverses within a few readings. A 100 nF capacitor from the wiper to ground helps too.

The bar jumps to full as soon as I touch the knob

Check the potentiometer's outer pins: one to 5V and one to GND. With only one end connected, the wiper reads whatever it likes. If the bar runs backwards, swap those two wires.

It works with the knob but not with my sensor

FULL_SCALE is the point. A sensor that never reads above 700 should have FULL_SCALE set to 700, not 1000, or the bar only ever fills to seven. Print raw over Serial, turn the input through its whole range, and use the largest number you actually see.

Can I use this on an ESP32?

Yes, with two changes: the ADC there is 12-bit, so raw runs to 4095, and COMMON still needs 5 V rather than 3V3. Set FULL_SCALE to about 4000 and take the rail from the 5V or VIN pin.

Where this goes next

Four faults, one symptom, and the meter reading that separates them.

When nothing lights

Edit this page — content/books/led-bar-graph/a-knob-and-a-bar.mdx

Community

Questions about this product

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

Ask a question ↗

10-Segment LED Bar Graph Kit, 12 Bars in 6 Colours

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