Skip to content

MagTag SSD1680 contrast fix (FPC-7519rev.b only)#10992

Merged
dhalbert merged 3 commits into
adafruit:mainfrom
mikeysklar:magtag-fpc7519-vcom20-v2
May 12, 2026
Merged

MagTag SSD1680 contrast fix (FPC-7519rev.b only)#10992
dhalbert merged 3 commits into
adafruit:mainfrom
mikeysklar:magtag-fpc7519-vcom20-v2

Conversation

@mikeysklar
Copy link
Copy Markdown

@mikeysklar mikeysklar commented May 8, 2026

MagTag units with the SSD1680 - FPC-7519rev.b display panel show bold, blurry text with the default firmware. This fix reads the correct VCOM voltage for that panel directly from its manufacturer register and applies it automatically. All other MagTag panels are unaffected.

forum thread

initial difference between earlier SSD1680 FPC-A005 (top) and new rev: FPC-7519rev.b (bottom)


IMG_0308


final difference same panels with PR applied:


IMG_0312

The FPC-7519rev.b panel (User ID byte 0xca) shows a dark gray background
with the default VCOM=0x28. Setting VCOM=0x20 (-1.5V) gives a light gray
background with solid black text.

Surgical fix: dedicated ssd1680_vcom20_display_start_sequence and
DISPLAY_SSD1680_COLSTART_8_VCOM20 type routed to case 0xca: only.
The 0x44 and 0x00 panels are untouched.
@mikeysklar mikeysklar changed the title MagTag SSD1680 FPC-7519rev.b: VCOM=0x20 contrast fix MagTag SSD1680 contrast fix (FPC-7519rev.b only) May 8, 2026
Testing across 0x28→0x20→0x1c→0x18→0x14→0x10 confirmed 0x14 as the
sweet spot: background matches the FPC-7619rev.b panel without the dark
halo fringe artifacts that appear below 0x14.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mikeysklar
Copy link
Copy Markdown
Author

Image for testing on new MagTag 7519rev.b with this patch.

magtag-fpc7519-vcom14.zip

Rename ssd1680_vcom20_display_start_sequence → ssd1680_vcom14_display_start_sequence
and DISPLAY_SSD1680_COLSTART_8_VCOM20 → DISPLAY_SSD1680_COLSTART_8_VCOM14 to match
the actual register value being set (0x14, not the earlier test value 0x20).

Add comment noting that VCOM=0x14 was independently confirmed by reading OTP register
0x2D on the physical panel — byte 1 of the response is 0x14, matching what the panel
manufacturer programmed as the recommended VCOM for this revision.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@dhalbert
Copy link
Copy Markdown
Collaborator

dhalbert commented May 9, 2026

The comment mentions a 7619rev.b instead of a 7519rev.b. However, I don't recollect any 7619's. I think that might be a typo. The pictures in the threads and in #10831 (superceded by #10836) only have 7519's, unless I missed one.

I tried this patch on my 7519 0x44 board. I'm not sure it's lighter but I would say it's still "blurry". I think the blurriness is easier to see on the weather demo: https://learn.adafruit.com/magtag-weather.

Here are a couple of pictures
First is original MagTag top, with a 0x44 7519 display below, both running 10.2.0.
orig-top-10 2 0-bottom

Second is the same boards, but the bottom is running a modified version of this PR that applies your VCOM14 change to both 0x44 and 0xca.
orig-top-VCOM14-bottom

@mikeysklar
Copy link
Copy Markdown
Author

mikeysklar commented May 9, 2026

Thanks for testing Dan on the ILI and SSD1680 0x44 variant.

I find this PR is only marginally better than the CircuitPython defaults. Not nearly as crisp as the orig SSD1680 or ILI0373.

I did discover the display has an OTP register which holds the recommended VCOM value and went with that.

// VCOM=0x14 (-1.0V) confirmed by reading the panel's OTP register (cmd 0x2D, byte 1 = 0x14).

Just to make sure I also did a brute-force version with a webcam and let claude cycle through other VCOM / border / etc. settings just to see if I could find something better.


IMG_0309


In terms of panel ribbon labels the FC-7619rev.b was the 0x44 you patched in February.


Panel                    | User ID byte 0 | colstart | Approx. era                  | CP fix
-------------------------|----------------|----------|------------------------------|----------------
IL0373                   | 0xff           | N/A      | 2020 – mid 2025              | always worked
FPC-A005 20.06.15 TRX    | 0x00           | 0        | Mid 2025 (first SSD1680)     | always worked
FPC-7519rev.b            | 0x44           | 8        | early 2026                   | fixed in 10.1.3
FPC-7519rev.b            | 0xca           | 8        | Spring 2026 – present        | this PR

@dhalbert
Copy link
Copy Markdown
Collaborator

dhalbert commented May 9, 2026

Aha, so you have a board with a "7619" ribbon cable. I have not seen one of these in person or in photos.

@mikeysklar
Copy link
Copy Markdown
Author

I only have two MagTags.

Both SSD1680 based:

  1. FPC-A005
  2. FPC-7519rev.b

I don't know if the label FPC-7619rev.b even truly exists, but that is what is the original board.c before I started mucking with it this week.

Regardless of ribbon label there are 0x44 and 0xca variants. Both require the coloffset=8 and have some blur / contrast needs.

@dhalbert
Copy link
Copy Markdown
Collaborator

dhalbert commented May 9, 2026

I don't know if the label FPC-7619rev.b even truly exists, but that is what is the original board.c before I started mucking with it this week.

I think that is just a typo that should be fixed. I think they are all 7519's.

There is something about the newer panels that is apparently not as good as the older ones. The gray cloud is nice and uniform on the old displays, and speckled on the new ones. This is not a photo artificact; it is how it looks:
new image vs old
image

@bablokb
Copy link
Copy Markdown

bablokb commented May 9, 2026

Have you tried changing the repeat count value? I do this in my board.c for the Pimoroni Badger2350 which also uses a SSD1680. The repeat count has an effect on contrast and also on update speed (it basically changes the number of times the display "flashes" during update). The implementation allows the user to change this at runtime.

During development, I tried all kinds of waveforms: from the Adafruit-driver, from Wasveshare and from Pimoroni. It seems like VCOM is not the only thing to take into account.

@mikeysklar
Copy link
Copy Markdown
Author

mikeysklar commented May 9, 2026

@dhalbert - that is a good comparison with the clouds. I'm going to try a few more settings and complex patterns. I have some test code that allows me to quickly drop in different settings without full rebuilds.

@bablokb - good point about repeat count. Testing various other parameters now.

RP - Repeat Period
TP - Timing
FR - frame rate
XON = all-gates-on

IMG_0313

@mikeysklar
Copy link
Copy Markdown
Author

mikeysklar commented May 9, 2026

Made some good progress with test patterns and weather example has improved. It is more crisp and the contrast is close. Still some speckle noise.

Thoughts?

test build used for these examples:

adafruit_magtag_2.9_grayscale-fpc7519-vcom24-g2sr04.uf2.zip

test pattern

IMG_0329 (1)



weather example:

IMG_0330

Made changes to support both 0x44 and 0xca with these settings. I've only tested on 0xca.

Before After
VCOM 0x14 0x24
G2 Frames 2 1
G2 SR2 0x05 0x04

Terminology / Changes in this round:

LUT — Waveform table voltage pulses to apply to each pixel per refresh cycle.

VCOM — Common electrode bias voltage; higher value = softer drive = lighter output. Raised from 0x140x24 to reduce over-drive and suppress speckle in gray areas.

G2 — Final settle phase of the LUT that sharpens blacks after the main refresh. Shortened from 2 frames → 1 frame to reduce differential drive between adjacent pixels.

G2 SR2 — Bitmask controlling which pixel transition types get driven during G2. Narrowed from 0x05 (white→black + white→white) → 0x04 (white→black only), so the settle phase only acts on pixels actually transitioning to black, eliminating speckle in dithered gray regions.

@bablokb
Copy link
Copy Markdown

bablokb commented May 11, 2026

Thoughts?

@mikeysklar: In the weather example, the lower image seems to be 1 bit only?!

@mikeysklar
Copy link
Copy Markdown
Author

@bablokb - I see the same the loss of grey fill inside the clouds.

Probably from the G2 change from 2 -> 1. I can try and get that back, but will need to turn some other knobs.

@dhalbert
Copy link
Copy Markdown
Collaborator

The black areas still look fuzzy. Another good example is the lines, which have crisp borders on the original, but seem to have random pixels turned on outside the lines in the newer models. Is this maybe some voltage or contrast setting issue?
image
image
The picture is a little blurry, but it's clear examining with a magnifier that there is noise at the edge of the lines.

@dhalbert
Copy link
Copy Markdown
Collaborator

dhalbert commented May 11, 2026

An interesting comment from an LLM:

A useful diagnostic clue: if the fuzziness is stable and repeatable in the same way every refresh, suspect wrong waveform/init. If it varies from refresh to refresh, suspect power/analog noise or panel degradation.

Looking at the speckles in the cloud in the weather example, they reappear at the same place each time. There are some accidental groups and shapes caused by speckles (e.g., an accidental curved line) that make this clear. I used a magnifier to look closely.

EDIT: more discussion, this time with Claude 4.7, after I said it was almost certainly an LUT issue:

A few LUT-level levers, roughly ordered by which usually moves the needle most:

  1. Stronger activation/scramble phase at the start.
    The first segment of a full-refresh waveform is supposed to "unstick" particles by hammering them with alternating ±VH pulses so every pixel reaches a known state regardless of where it started. If that phase is too short, pixels with slightly different starting distributions (from prior content, manufacturing variance, or trapped charge) end up at slightly different final reflectances → speckle in fixed positions, fuzzy edges where the prior image had transitions. Add more activation cycles, or increase pulse amplitude/duration. Cost: longer total refresh time.

  2. Add (or extend) full-flush phases: drive all pixels to solid black, then solid white, then black again, before driving to target. A B-W-B-W flush homogenizes the starting state for the gray-rendering phase. Many cheap/fast LUTs skip or shorten this; adding it cleans up edge artifacts dramatically because the previous-frame geometry is erased before gray rendering begins.

  3. Use a source-aware (matrix) LUT instead of a target-only LUT. A proper EPD LUT is indexed by [source_gray][target_gray][frame]. If the LUT only cares about the target level, pixels that were previously black vs. previously white land at different final reflectances when both are driven to "light gray" — that's exactly the fuzzy-edge symptom. Building/using the full matrix is the structural fix.

  4. Enforce DC balance per transition. Every (source → target) sequence should integrate to zero net voltage-time. Imbalance accumulates as trapped charge in the pixel TFT/backplane, which biases some pixels permanently and shows up as a fixed-position speckle pattern that worsens with use. Audit the LUT and add compensating opposite-polarity pulses where needed.

  5. Tune the final dwell time on the gray-rendering segment. Intermediate gray levels require precise partial-migration of particles. Too short → pixels under-shoot non-uniformly (mobility varies pixel-to-pixel, becomes speckle); too long → overshoot. This is the segment most worth sweeping experimentally against a flat-gray test pattern.

  6. Temperature compensation. Particle mobility is strongly temperature-dependent. If the LUT is tuned for ~25 °C and you're running cold/hot, the activation and dwell phases are effectively wrong, and the symptoms look exactly like (1) and (5). If there's a temp sensor, switch LUTs or scale frame counts.

If I had to pick one to try first: lengthen the activation phase and add a B-W-B-W flush before gray rendering. That single change typically cleans up both the speckle on flat gray and the ghost-edge fuzziness, at the cost of a lower refresh. From there, (3) and (4) are the structural follow-ups if you want it fully clean.

@bablokb
Copy link
Copy Markdown

bablokb commented May 11, 2026

Here is a nice writeup on how LUTs work: https://github.com/antirez/uc8151_micropython#what-i-learned-about-setting-waveformsluts-for-edps. It is for a different driver chip, but it explains the basic principles very well.

Does Adafruit have vendor information about the specific SSD1680 and the necessary LUTs for the display? This seems to vary from vendor to vendor and without these specs writing a driver is more like a trial&error experience. And there are so many parameters so it is unlikely to find a correct solution.

@mikeysklar
Copy link
Copy Markdown
Author

mikeysklar commented May 11, 2026

Ended up using GxEDP2_4G (GDEM029T94) settings almost entirely.

A005 on top and 7519 on bottom with changes applied. All speckle traces are gone and lines are clean. The 7519 is looking crisper at this point than the original SSD1680 A005.

IMG_0333
Setting Pre-PR (A005, all panels) FPC-7519 (0x44 / 0xca)
colstart 0 8
VCOM 0x2c 0x28 0x24
VS L0 [0:2] 2a 60 15 20 48 01 ← GxEPD2 L3 (black)
VS L1 [0:2] 20 60 10 08 48 10 ← GxEPD2 L1 (lighter gray)
VS L2 [0:2] 28 60 14 02 48 04 ← GxEPD2 L2 (darker gray)
VS L3 [0:2] 00 60 00 40 48 80 ← GxEPD2 L0 (white)
VS L4 [0:1] 00 90 00 00
G0 timing 00 02 00 05 14 00 00 0A 19 00 03 08 00 00
G1 timing 1E 1E 00 00 00 00 01 14 01 00 14 01 00 03
G2 timing 00 02 00 05 14 00 00 0A 03 00 08 19 00 00
G3 timing 00 00 00 00 00 00 00 01 00 00 00 00 00 01
XON [0:5] 24 22 22 22 23 32 22 22 22 22 22 22
RESET pulse 200µs low + 10ms settle (boot)
LUT source Ad-hoc (0x60 pattern) GxEPD2_4G (GDEM029T94): L0↔L3 swapped, L1/L2 unchanged; 0x600x48 (DC balance)

CircuitPython with 0x44 and 0xca (7519rev.b) support attached.

adafruit_magtag_2.9_grayscale-l1l2fix.zip

@dhalbert
Copy link
Copy Markdown
Collaborator

Yes, lines and font characters are much crisper, and there is no "haze" of noise surrounding the lines further out. Grays are still speckled, but I don't know if improving that is possible.

@mikeysklar
Copy link
Copy Markdown
Author

mikeysklar commented May 11, 2026

Did some gray smoothing.

adafruit_magtag_2.9_grayscale-l1l2fix.uf2.gz


Quick comparison of what the 7519 (0x44 / 0xca) looked like when we started:


Screenshot 2026-05-11 at 4 30 04 PM


Where it is now:


Screenshot 2026-05-11 at 2 46 00 PM

@dhalbert
Copy link
Copy Markdown
Collaborator

@mikeysklar Do you feel this is ready to merge?

This PR is against main. From a support point of view, is this OK with you? It seems like main is working fine for MagTag right now. I will make sure it's in 10.3.0-alpha.2. Or do you want to do it against 10.2.x branch? I am planning on a 10.2.1 very soon.

@mikeysklar
Copy link
Copy Markdown
Author

mikeysklar commented May 12, 2026

Yes, this PR is ready to be released as part of 10.3.0-alpha.2.

I've done zero testing with the 10.2.x stable. I'm okay with doing a testing on that today if you would like to see it get into stable. Your call.

Copy link
Copy Markdown
Collaborator

@dhalbert dhalbert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all the research on how to improve the appearance. This is really good for MagTag users.

@dhalbert dhalbert merged commit 6612dec into adafruit:main May 12, 2026
17 checks passed
@mikeysklar
Copy link
Copy Markdown
Author

@dhalbert and @bablokb thank you for the guidance.

Comment thread ports/espressif/boards/adafruit_magtag_2.9_grayscale/board.c
@bablokb
Copy link
Copy Markdown

bablokb commented May 13, 2026

@mikeysklar thank you for the many tests and the insights you provided. I will try to update the Badger2350 as well. It is on my todo-list but not with high priority.

Do you plan to update the generic SSD1680 driver (or at least the examples) as well? E.g. there is a examples/ssd1680_2.9_grayscale_magtag2025.py which uses the old VCOM/LUTs.

@mikeysklar
Copy link
Copy Markdown
Author

mikeysklar commented May 13, 2026

@bablokb - Yes. Need to update two locations. @dhalbert had suggested Arduino lib too.

Fortunately the Adafruit_CircuitPython_MagTag is abstracted high-level so no touch there.

I'll setup the PRs today.

@brentru
Copy link
Copy Markdown
Member

brentru commented May 13, 2026

cc @tyeth - you may want to take a look at this for WS!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants