Enumerating LCD segments with OpenCV for reverse-engineering
An LCD with unknown segment mapping is controlled by a microcontroller to activate all segments sequentially.
This sequence is captured on video by a camera and then processed on a PC using the computer vision library OpenCV (used via Python).
Once the segments are identified they can be mapped to the seven-segment digits on the LCD glass.
This mapping then allows the external microcontroller to display symbols (numerals and some letters) on the LCD digit positions.
If it's possible to obtain the segment state by listening to the LCD control lines of the original electronics, this mapping also would allow decoding of the digits into numbers - the display contents could be read "electronically".
The following image shows the result of the OpenCV processing:
125 LCD segments (3 of 128 are not used) as detected and labeled by OpenCV, overlayed on the "empty display" reference frame.
Some segments appear thin due to lower LCD contrast and thresholding.
Green numerals are the enumerated index, magenta shows the <segment>.<common>
addressing scheme used by the LCD controller.
(the image is full resolution - right-click and "Save Image As ...".)
Image processing
Only very basic image processing is needed: blob-detection.
This is made very simple through the functions available in OpenCV.
Background subtraction is used to isolate the current segment (although not strictly neccessary, using the right ROI).
The processing steps used are:
- conversion to grayscale and blur
- difference (subtraction from empty display as reference)
- thresholding
- erosion (to eliminate small artifacts)
- segmentation and labeling
Video demo
This video shows the process and visualises the intermediate processing steps with OpenCV.
Load and play video from or :
Results
The algorithm successfully detected and enumerated all LCD segments after some iterations of adjusting the binarization threshold value and accounting for multi-object segments by increasing the positional tolerance for detection.
Difficulties include
- uneven lighting and shadows
- viewing angle (uneven contrast)
- different segment sizes causing longer turn-on times and uneven turn-on of multi-segments (e.g. words)
- missing (unused) segments
- local thresholding to account for uneven brightness/contrast
- automatic tresholding (two-pass with analysis)
- peak intensity detection over several video frames to ensure only fully active segments are processed
- edge detection instead of blob-detection
- digit detection and automatic segment mapping
- faster processing and shorter video time by using a coded sequence for each segment ("temporal binary coding", i.e. each blinks out its index in binary, which would probably require only 8 video frames) instead of naive iteration
- ...
Update
I implemented some of the improvements mentioned above:
- peak intensity detection
- using a coded sequence for each segment
The former simply compares the number of pixels in two consecutive frames to detect the maximum contrast (segments fully dark).
This depends a little on segments to switch off faster than they switch on, as otherwise a frame containing more or larger active segments would cause an error. Alternatively blank frames could easily be inserted.
The latter uses each segment as serial data channel, i.e. each segment flashes out a binary code (it's own index number) which means the number of frames required is reduced from 128 to .
"+ 1" because the first segment (bit 0) otherwise would not be activated at all and as such could not be detected.
The sequence used to control the display looks like this:
127 0 11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 0: 10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010 1: 11001100110011001100110011001100110011001100110011001100110011001100110011001100110011001100110011001100110011001100110011001100 2: 11110000111100001111000011110000111100001111000011110000111100001111000011110000111100001111000011110000111100001111000011110000 3: 11111111000000001111111100000000111111110000000011111111000000001111111100000000111111110000000011111111000000001111111100000000 4: 11111111111111110000000000000000111111111111111100000000000000001111111111111111000000000000000011111111111111110000000000000000 5: 11111111111111111111111111111111000000000000000000000000000000001111111111111111111111111111111100000000000000000000000000000000 6: 11111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000 7: 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000The decoder only needs to group the detected segments by their position and convert the flashing sequence into binary.
In addition, it groups the multi-part segments.
Load and play video from or :
Hardware setup
The device under test in this experiment is a "PZEM-021 v5.0" panel meter with CPU and a dedicated LCD controller IC.
(For devices where there is no dedicated LCD controller, one is probably integrated into the CPU or ASIC as peripheral.
However, depending on the complexity of the LCD glass, it may also be possible to drive it with general purpose IO.)
The onboard microcontroller is held in reset by an external controller that then can write to the LCD controller, an HT1621.
Schematic overview of components and connections.
Source code
It's just a proof of concept and not a full-fledged, user-friendly application.
It's also available on GitHub at https://github.com/eleif/lcd-segmentation/ for you to fork and improve.
Python / OpenCV
This is the code to run on your PC. It requires Python and the module opencv
(and numpy
). See the comments for references.
I've added a small feature that prints the segment index to console on mouse click (right mouse button to end the line) - this speeds up mapping the segments to digits considerably.
#!python3
"""
https://eleif.net/lcd-segmentation.html (2018-10-23)
Enumerating LCD segments with OpenCV
Intended to help with reverse-engineering an undocumented LCD.
The LCD needs to be controlled in order to activate all segments/commons sequentially.
Each segment is detected and labeled from the video.
The labels can be used to map segments to display elements like seven-segment digits.
The mapping then allows writing (reading) display contents.
This is a proof of concept, not an application for end users.
Run and hit space to process at near live speed or hit right-arrow to step through frames.
There are some hard-coded parameters (marked with #TODO) and also globals ...
Requirements / Documentation
- Python 3
https://docs.python.org/3/
https://www.python.org/downloads/
- NumPy
https://docs.scipy.org/doc/numpy/
pip install numpy [--upgrade]
- OpenCV
https://docs.opencv.org/3.4/
https://pypi.org/project/opencv-python/
pip install opencv-python [--upgrade]
[pip install opencv-contrib-python]
"""
# modules
import sys # Python standard library
import numpy as np # required by OpenCV
import cv2 as cv # OpenCV 3
# settings
filename = "demo.mp4" # encoding affects possible seek positions
startframe = 0 # !!
endframe = 71 # !!
threshold = 45 # !!
segments = [] # store detected segments
counter = 0 # count consecutive frames without segments
def main():
""" top level """
print(versions())
print("filename", filename)
# frame source and background subtraction frame
init()
# loop
state = "pause"
while state != "quit":
# process frame?
if state in ["play","next","fast"]:
process()
# user input
key = cv.waitKeyEx( int(1000 / fps) )
# handle user input
state = state_machine(state, key)
# close frame source
deinit()
def init():
""" initialise video source and empty frame for background subtraction """
global cap, out, frame_first_gray, sum, fc, fps # TODO
cap = cv.VideoCapture(filename)
fps = cap.get(cv.CAP_PROP_FPS)
fw = int(cap.get(cv.CAP_PROP_FRAME_WIDTH))
fh = int(cap.get(cv.CAP_PROP_FRAME_HEIGHT))
fc = int(cap.get(cv.CAP_PROP_FRAME_COUNT))
print("width:", fw)
print("height:", fh)
print("fps:", fps)
print("frames:", fc)
# add all detected segments
sum = np.zeros((fh, fw), np.uint8) # grey
# seek
res = cap.set(cv.CAP_PROP_POS_FRAMES, startframe)
print("starting at frame:", startframe, "OK" if res else "ERROR")
if True: # TODO
# use first frame as background
res, frame_first = cap.read()
print("read frame:", startframe, "OK" if res else "ERROR (try another frame or file format)")
if not res:
cap.release()
cv.destroyAllWindows()
sys.exit()
else:
# use image as background
frame_first = cv.imread('background.png')
frame_first = cv.cvtColor(frame_first, cv.COLOR_BGRA2BGR)
if not frame_first.shape[:2] == (fh,fw):
print("frame size mismatch", (fh,fw), frame_first.shape[:2])
cap.release()
cv.destroyAllWindows()
sys.exit()
# display reference image
cv.imshow("First frame", frame_first)
# convert to gray and blur a little to remove noise
frame_first_gray = cv.cvtColor(frame_first, cv.COLOR_BGR2GRAY)
#frame_first_gray = cv.medianBlur(frame_first_gray,5)
frame_first_gray = cv.GaussianBlur(frame_first_gray, (5, 5), 0)
def deinit():
""" deinitialise OpenCV """
cap.release()
cv.destroyAllWindows()
def process():
""" get and process one video frame """
global sum, counter # TODO
# get frame number
fn = int(cap.get(cv.CAP_PROP_POS_FRAMES))
# end of video reached: don't process, stop (and wait for user)
if fn > endframe:
state = "end"
return
# get frame
res, frame = cap.read()
if not res:
print("ERROR (try another frame or file format)")
deinit()
sys.exit() # TODO
# convert to gray
frame_gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
# blur to reduce noise
#frame_gray = cv.medianBlur(frame_gray,5)
frame_gray = cv.GaussianBlur(frame_gray, (5, 5), 0) # TODO parameter
# subtract from reference frame
difference = cv.absdiff(frame_first_gray, frame_gray)
# threshold
_, thresholded = cv.threshold(difference, threshold, 255, cv.THRESH_BINARY)
# attempt to remove noise
kernel = np.ones((3, 3), np.uint8) # TODO parameter
filtered = cv.erode(thresholded, kernel, iterations=1)
# sum
sum = cv.add(sum, filtered)
# detect blobs
img_contours, contours, hierarchy = cv.findContours(
filtered, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
count = len(contours)
if count > 0:
# reset frame counter with no segments detected
counter = 0
# determine combined center of mass for all contours
# do it manually because "bad" contours can cause moments to have invalid centroid
c = 0
cx = 0
cy = 0
for i, contour in enumerate(contours):
# center of mass
for p in contour:
x = p[0][0]
y = p[0][1]
cx += x
cy += y
c += 1
cx = int(cx/c)
cy = int(cy/c)
# "adaptive" positional limits
if count > 1:
# higher tolerance for labels (multiple blobs spread out)
t = 50 # TODO parameter
else:
# normal tolerance for segments
t = 25 # TODO parameter
# prevent duplicates
try:
last = segments[-1]
if last[0] in range(cx-t, cx+t) and last[1] in range(cy-t, cy+t):
# detected previously
pass
else:
print("new {:3}".format( len(segments) ),[cx, cy])
segments.append([cx,cy])
except (IndexError, TypeError):
# no previous
print("first/missing {:3}".format( len(segments) ), cx, cy)
segments.append([cx,cy])
else:
# detect missing segments
counter += 1
if counter > 6: # TODO parameter - could be calculated from blink speed and video frame rate
print("missing segment around", fn-counter, fn)
segments.append(None)
counter = 0
# show detected segments in output
sum_inv = cv.bitwise_not(sum)
display = cv.bitwise_and(frame,frame,mask = sum_inv)
# show info overlays
cv.putText(display, 'frame: {}/{}'.format(fn, fc),
( 10,20), cv.FONT_HERSHEY_SIMPLEX, .5, (0,255,0), 1, cv.LINE_AA)
cv.putText(display, 'segments: {}'.format( len(segments) ),
(180,20), cv.FONT_HERSHEY_SIMPLEX, .5, (0,255,0), 1, cv.LINE_AA)
if count:
# green
cv.putText(display, 'c: {}'.format(count),
( 10,40), cv.FONT_HERSHEY_SIMPLEX, .5, (0,255,0), 1, cv.LINE_AA)
else:
# red (nothing detected, could be missing segment)
cv.putText(display, 'c: {}'.format(count),
( 10,40), cv.FONT_HERSHEY_SIMPLEX, .5, (0,0,255), 1, cv.LINE_AA)
# label segments
fontFace = cv.FONT_HERSHEY_SIMPLEX
fontScale = .5
thickness = 1
linetype = cv.LINE_8 #cv.LINE_AA
for i, s in enumerate(segments):
try:
# enumerate
text = '{}'.format( i )
retval, baseLine = cv.getTextSize( text, fontFace, fontScale, thickness )
cv.putText(display, text,
(segments[i][0]-int(retval[0]/2), segments[i][1]), fontFace, fontScale, (0,255,0), thickness, linetype)
# COM/SEG address
text = '{}.{}'.format( i//4, i%4 )
retval, baseLine = cv.getTextSize( text, fontFace, fontScale, thickness )
cv.putText(display, text,
(segments[i][0]-int(retval[0]/2), segments[i][1]+int(retval[1])), fontFace, fontScale, (255,0,255), thickness, linetype)
except TypeError as e:
# None encountered
pass
# display processing steps
cv.imshow("Current Frame", frame_gray)
cv.imshow("difference", difference)
cv.imshow("thresholded", thresholded)
cv.imshow("filtered", filtered)
cv.imshow("sum", sum)
cv.imshow("display", display)
# save
if False:
# lossless frames - use e.g. ffmpeg to create video
cv.imwrite( "output/{:04}_g.png".format(fn), frame_gray)
# cv.imwrite( "output/{:04}_d.png".format(fn), difference)
# cv.imwrite( "output/{:04}_t.png".format(fn), thresholded)
# cv.imwrite( "output/{:04}_f.png".format(fn), filtered)
# cv.imwrite( "output/{:04}_s.png".format(fn), sum)
# cv.imwrite( "output/{:04}.png".format(fn), display)
# left-click on segment to print number to console; right-click for newline
cv.setMouseCallback('display', mouse)
# mouse callback function (left-click on segment to print number to console; right-click for newline)
def mouse(event,x,y,flags,param):
# find closest segment in range and print segment number to console
if event == cv.EVENT_LBUTTONUP:
# calculate distances between the mouse click and all segments
ds = []
for i,s in enumerate(segments):
sx,sy = s[:2]
dx = x-sx
dy = y-sy
d = (dx**2 + dy**2)**.5
ds.append([i,d])
# find the smallest distance, i.e. the closest segment
m = min(ds, key=lambda _:_[1])
# print index if clicked close enough
if m[1] < 20: # TODO parameter click distance in px
print("{}, ".format(m[0]), end="" )
# new line on RMB
if event == cv.EVENT_RBUTTONUP:
print()
def state_machine(state, key):
""" determine state change from key code """
states = {
"pause" : {"esc":"quit", "space":"play", "right":"next", "left":"previous" },
"play" : {"esc":"quit", "space":"pause"},
"end" : {"esc":"quit" },
"quit" : {},
}
keys = {
27:"esc",
13:"enter", # not used
32:"space",
115:"s", # not used
2424832:"left", # not used
2555904:"right",
}
if key > 0:
# key pressed
try:
# print("key", key, "=", keys[key])
# print("state", state, "->", states[state][keys[key]])
state = states[state][keys[key]]
except KeyError as e:
print("unknown key", key)
else:
# no key pressed
if state == "next":
# just process one frame (pause automatically)
state = "pause"
return state
def versions():
""" return version strings """
p = "Python " + sys.version
n = "NumPy " + np.version.version
o = "OpenCV " + cv.__version__
return "\n".join([p,n,o]) + "\n"
if __name__ == "__main__":
main()
C/C++ (Arduino IDE)
This is the code I ran on a microcontroller to "hijack" the display in order to show all segments in sequence. It's just a proof of concept.
It includes the complete digit mapping (as well as definitions for the other segments and symbols), seven-segment glyphs (digits and some letters) and test functions to write to the digits.
Additionally, it can receive the serial data coming from the original controller - but it does not decode the information displayed.
The code does not use any hardware features, it's just "bit-banging" 4 IO lines: RESn to keep the original control IC in reset, and #CS, #WR and DATA for the LCD controller.
I've used the Arduino IDE with a "Digispark" clone, which is a small board with an 8-pin AVR/Microchip ATtiny85 microcontroller that has the "micronucleus" bootloader for programming via USB.
// V9821s controls HT1621 in PZEM-021 V5.0
//
// Arduino IDE/framework C/C++ code for an AVR microcontroller,
// a "Digispark" clone (AVR/Microchip ATtiny85 with "micronucleus" USB bootloader)
//
// The ATtiny85 is running at 16.5 MHz
// The bootloader takes a few seconds before the user program is executed!
//
// pins
#define HT_RES 5 // (requires AVR fuse to disable reset to use as IO)
// 4 // USB
// 3 // USB
#define HT_CS 2 //
#define HT_WR 1 //
#define HT_DATA 0 //
// global variables used in "loop"
int old = HIGH, edge = HIGH;
bool s = false;
unsigned long time;
// display memory (only lower nibble is used)
uint8_t ram[32] = {0};
uint8_t buf[32] = {0};
// seven segment display
// naming convention
// (clockwise from top)
// A
// F B
// G
// E C
// D
// seven segment bits
// (so a digit/letter can be encoded in a byte)
#define S_A 1
#define S_B 2
#define S_C 4
#define S_D 8
#define S_E 16
#define S_F 32
#define S_G 64
// 128
// seven segment glyphs
// (which segment is active in a digit/letter)
// digits
// G_x S_A S_B S_C S_D S_E S_F S_G
#define G_0 (S_A + S_B + S_C + S_D + S_E + S_F + 0 )
#define G_1 ( 0 + S_B + S_C + 0 + 0 + 0 + 0 )
#define G_2 (S_A + S_B + 0 + S_D + S_E + 0 + S_G)
#define G_3 (S_A + S_B + S_C + S_D + 0 + 0 + S_G)
#define G_4 ( 0 + S_B + S_C + 0 + 0 + S_F + S_G)
#define G_5 (S_A + 0 + S_C + S_D + 0 + S_F + S_G)
#define G_6 (S_A + 0 + S_C + S_D + S_E + S_F + S_G)
#define G_7 (S_A + S_B + S_C + 0 + 0 + 0 + 0 )
#define G_8 (S_A + S_B + S_C + S_D + S_E + S_F + S_G)
#define G_9 (S_A + S_B + S_C + 0 + 0 + S_F + S_G)
// G_x S_A S_B S_C S_D S_E S_F S_G
// letters
// G_x S_A S_B S_C S_D S_E S_F S_G
#define G_A (S_A + S_B + S_C + 0 + S_E + S_F + S_G) // A 0
#define G_B ( 0 + 0 + S_C + S_D + S_E + S_F + S_G) // b 1
#define G_C ( 0 + 0 + 0 + S_D + S_E + 0 + S_G) // c 2
#define G_D ( 0 + S_B + S_C + S_D + S_E + 0 + S_G) // d 3
#define G_E (S_A + 0 + 0 + S_D + S_E + S_F + S_G) // E 4
#define G_F (S_A + 0 + 0 + 0 + S_E + S_F + S_G) // F 5
#define G_G (S_A + S_B + S_C + S_D + 0 + S_F + S_G) // g 6
#define G_H ( 0 + 0 + S_C + 0 + S_E + S_F + S_G) // h 7
#define G_I ( 0 + S_B + S_C + 0 + 0 + 0 + 0 ) // I 8
#define G_J ( 0 + S_B + S_C + S_D + 0 + 0 + 0 ) // J 9
#define G_K ( 0 + 0 + 0 + 0 + 0 + 0 + S_G) // - 10
#define G_L ( 0 + 0 + 0 + S_D + S_E + S_F + 0 ) // L 11
#define G_M ( 0 + 0 + 0 + 0 + 0 + 0 + S_G) // - 12
#define G_N ( 0 + 0 + S_C + 0 + S_E + 0 + S_G) // n 13
#define G_O ( 0 + 0 + S_C + S_D + S_E + 0 + S_G) // o 14
#define G_P (S_A + S_B + 0 + 0 + S_E + S_F + S_G) // P 15
#define G_Q (S_A + S_B + S_C + 0 + 0 + S_F + S_G) // q 16
#define G_R ( 0 + 0 + 0 + 0 + S_E + 0 + S_G) // r 17
#define G_S (S_A + 0 + S_C + S_D + 0 + S_F + S_G) // S 18
#define G_T ( 0 + 0 + 0 + S_D + S_E + S_F + S_G) // t 19
#define G_U ( 0 + S_B + S_C + S_D + S_E + S_F + 0 ) // U 20
#define G_V ( 0 + 0 + S_C + S_D + S_E + 0 + 0 ) // v 21
#define G_W ( 0 + 0 + 0 + 0 + 0 + 0 + S_G) // - 22
#define G_X ( 0 + 0 + 0 + 0 + 0 + 0 + S_G) // - 23
#define G_Y ( 0 + S_B + S_C + S_D + 0 + S_F + S_G) // y 24
#define G_Z (S_A + S_B + 0 + S_D + S_E + 0 + S_G) // Z 25
// G_x S_A S_B S_C S_D S_E S_F S_G
// arrays with digits and letters
//
// ASCII offset is '0' or 48 or 0x30
const uint8_t digits[] = {
G_0, G_1, G_2, G_3, G_4, G_5, G_6, G_7, G_8, G_9,
};
// ASCII offset is 'A' or 65 or 0x41
const uint8_t letters[] = {
G_A, G_B, G_C, G_D, G_E, G_F, G_G, G_H, G_I, G_J,
G_K, G_L, G_M, G_N, G_O, G_P, G_Q, G_R, G_S, G_T,
G_U, G_V, G_W, G_X, G_Y, G_Z,
};
// bit indexes for LCD segments in LCD controller RAM
//
// 88.88 V 88.88 A
// voltage current
// 888.8 kW 8888 kWh
// power energy
//
// voltage
// A B C D E F G
const uint8_t v[4][7] = { { 3, 7, 5, 0, 1, 2, 6}, //
{ 11, 15, 13, 8, 9, 10, 14}, //
{ 19, 23, 21, 16, 17, 18, 22}, //
{ 27, 31, 29, 24, 25, 26, 30}};//
const uint8_t v_d = 12; // decimal 88.88
const uint8_t v_u = 28; // unit (V)
const uint8_t v_l = 4; // label
// current
const uint8_t c[4][7] = { { 35, 39, 37, 32, 33, 34, 38}, //
{ 43, 47, 45, 40, 41, 42, 46}, //
{ 51, 55, 53, 48, 49, 50, 54}, //
{ 59, 63, 61, 56, 57, 58, 62}};//
const uint8_t c_d = 44; // decimal 88.88
const uint8_t c_u = 60; // unit (A)
const uint8_t c_l = 36; // label
// power
const uint8_t p[4][7] = { { 64, 68, 70, 67, 66, 65, 69}, //
{ 72, 76, 78, 75, 74, 73, 77}, //
{ 80, 84, 86, 83, 82, 81, 85}, //
{ 88, 92, 94, 91, 90, 89, 93}};//
const uint8_t p_d = 87; // decimal 888.8
const uint8_t p_k = 111; // kilo
const uint8_t p_u = 103; // unit (W)
const uint8_t p_l = 79; // label
// energy
const uint8_t e[4][7] = { { 96,100,102, 99, 98, 97,101}, //
{104,108,110,107,106,105,109}, //
{112,116,118,115,114,113,117}, //
{120,124,126,123,122,121,125}};//
const uint8_t e_k = 127; // kilo
const uint8_t e_u = 119; // unit (Wh)
const uint8_t e_l = 95; // label
// set single bit with index <bit> in <ram>
void setbit(uint8_t bit){
ram[bit/4] |= 1<<(bit%4);
}//setbit
// clear single bit with index <bit> in <ram>
void clrbit(uint8_t bit){
ram[bit/4] &= ~(1<<(bit%4));
}//clrbit
// clear <ram>
void clear(){
for (uint8_t i=0; i<32; i++) {
ram[i] = 0;
}
}//clear
// write <c> from <glyphs[]> to <digit[]>
void write(uint8_t c, const uint8_t glyphs[], const uint8_t digit[7]){
for (uint8_t i=0; i<7; i++) {
if (glyphs[c] & (1<<i)) {
setbit( digit[i] );
}
else {
clrbit( digit[i] );
}
}
}//write
// show all bits/segments in <digit[]> sequentially
void test(const uint8_t digit[7]){
for (uint8_t i=0; i<7; i++) {
setbit( digit[i] );
writedisplay(ram);
delay(300);
}
}//test
// show all segments sequentially
void segments(){
for (uint8_t i=0; i<132; i++) {
setbit( i );
writedisplay(ram);
clrbit( i );
delay(300);
}
}//segments
// array to store active segments/bits by index
uint8_t bits[128] = {0};
// get active bits/segments from <in> and store their index in <out>, return the count
uint8_t getbits(uint8_t in[], uint8_t out[]){
uint8_t n=0;
for(uint8_t i=0; i<128; i++){
if ( in[i/4] & (1<<(i%4)) ) {
out[n++] = i;
}
}
return n;
}//getbits
// bit-bang synchronous serial for HT1621
// write all 32x4 bits from <data> successively
// (this assumes the display has been initialised)
void writedisplay(uint8_t data[]) {
digitalWrite(HT_CS, LOW);
// command
digitalWrite(HT_WR, LOW);
digitalWrite(HT_DATA, HIGH); // 1
digitalWrite(HT_WR, HIGH);
digitalWrite(HT_WR, LOW);
digitalWrite(HT_DATA, LOW); // 0
digitalWrite(HT_WR, HIGH);
digitalWrite(HT_WR, LOW);
digitalWrite(HT_DATA, HIGH); // 1
digitalWrite(HT_WR, HIGH);
// 6 bits start address 0b000000
digitalWrite(HT_DATA, LOW);
for(uint8_t n=0; n<6; n++){
digitalWrite(HT_WR, LOW);
digitalWrite(HT_WR, HIGH); // 0
}
// 32x4 bits, LSB first
for(uint8_t a=0; a<32; a++){
for(uint8_t d=0; d<4; d++){
// clock
digitalWrite(HT_WR, LOW);
// data
if ( (data[a]>>d)&1 ) {
digitalWrite(HT_DATA, HIGH);
}
else {
digitalWrite(HT_DATA, LOW);
}
// clock
digitalWrite(HT_WR, HIGH);
}
}
digitalWrite(HT_CS, HIGH);
} // writedisplay
// bit-bang synchronous serial for HT1621
// receive a synchronous serial data packet for HT1621 and store bits in <data>
// probably ONLY WORKS WITH panel meter "PZEM-021 v5.0" (timing, packets, line levels, ..)
void readdisplay(uint8_t data[]) {
uint8_t address = 0;
// wait for end of an ongoing "packet" (rising edge)
while( digitalRead(HT_CS) == LOW );
// wait for start of packet (falling edge)
while( digitalRead(HT_CS) == HIGH );
// command
// wait for rising edge
while( digitalRead(HT_WR) == HIGH );
while( digitalRead(HT_WR) == LOW );
if( digitalRead(HT_DATA) != HIGH ) return; // 1
while( digitalRead(HT_WR) == HIGH );
while( digitalRead(HT_WR) == LOW );
if( digitalRead(HT_DATA) != LOW ) return; // 0
while( digitalRead(HT_WR) == HIGH );
while( digitalRead(HT_WR) == LOW );
if( digitalRead(HT_DATA) != HIGH ) return; // 1
// address (6 bits, 5 used)
for(uint8_t i=0; i<6; i++){
// wait for rising edge
while( digitalRead(HT_WR) == HIGH );
while( digitalRead(HT_WR) == LOW );
// read bit
if( digitalRead(HT_DATA) == HIGH ){
// MSB first
address |= 0b100000>>i;
}
}
// data bits (multiples of 4)
while(1){
// try to read 4 bits
for(uint8_t i=0; i<4; i++){
// wait for falling edge or end of packet
while( digitalRead(HT_WR) == HIGH && digitalRead(HT_CS) == LOW );
// end of packet?
if( digitalRead(HT_CS) == HIGH ){
return;
}
// wait for rising edge
while( digitalRead(HT_WR) == LOW );
// read data bit
if( digitalRead(HT_DATA) == HIGH ){
//set
data[address] |= 1<<i;
}
else {
//clear
data[address] &= ~(1<<i);
}
}//for
// increment address for "successive address writing" mode
address++;
}//while
}//readdisplay
// "Arduino" framework init function
void setup() {
// remember this is run _after_ the bootloader has finished listening (about 6 seconds after power-on)
// nothing to do
}//setup
// "Arduino" framework main loop
void loop() {
// detect rising edge
edge = digitalRead(HT_CS);
if( edge && edge!=old) {
time = millis();
s = true;
}
old = edge;
// detect gap between display updates
if( s && ( (millis() - time) >= 15) ){
s = false;
// listen to display data (16 packets x 2x4 bits = 128 bits)
for(uint8_t n=0; n<16; n++){
readdisplay(buf);
}
// reset V9821S and ...
pinMode(HT_RES, OUTPUT);
//digitalWrite(5,LOW); // redundant
delay(10); // see V9821S datasheet
// ... take control of serial interface
pinMode(HT_CS, OUTPUT);
digitalWrite(HT_CS,HIGH); // prevent later V9821S glitch: it pulls #WR down once on CS falling edge in reset
pinMode(HT_WR, OUTPUT);
pinMode(HT_DATA, OUTPUT);
// get bit indexes from data
uint8_t k = getbits(buf,bits);
// shuffle the bits
randomSeed(7331);
for(uint8_t n=0; n<k; n++){
uint8_t r = random(n,k);
uint8_t tmp = bits[n];
bits[n] = bits[r];
bits[r] = tmp;
}
// clear the randomized segments
uint8_t d = 1000/k;
for(uint8_t n=0; n<k; n++){
buf[bits[n]/4] &= ~(1<<(bits[n]%4));
writedisplay(buf);
delay(d);
}
// show all segments sequentially
segments();
// show seven-segments sequentially
test(v[0]);
test(v[1]);
test(v[2]);
test(v[3]);
test(c[0]);
test(c[1]);
test(c[2]);
test(c[3]);
test(p[0]);
test(p[1]);
test(p[2]);
test(p[3]);
test(e[0]);
test(e[1]);
test(e[2]);
test(e[3]);
// show all digits
for (uint8_t d=0; d<10; d++) {
clear();
write(d, digits, v[0]);
write(d, digits, v[1]);
write(d, digits, v[2]);
write(d, digits, v[3]);
write(d, digits, c[0]);
write(d, digits, c[1]);
write(d, digits, c[2]);
write(d, digits, c[3]);
write(d, digits, p[0]);
write(d, digits, p[1]);
write(d, digits, p[2]);
write(d, digits, p[3]);
write(d, digits, e[0]);
write(d, digits, e[1]);
write(d, digits, e[2]);
write(d, digits, e[3]);
writedisplay(ram);
delay(400);
}
// "type" some text
#define TYPEDELAY 300
clear();
writedisplay(ram);
write('P'-'A', letters, v[0]);
writedisplay(ram); delay(TYPEDELAY);
write('Z'-'A', letters, v[1]);
writedisplay(ram); delay(TYPEDELAY);
write('E'-'A', letters, v[2]);
writedisplay(ram); delay(TYPEDELAY);
write('M'-'A', letters, v[3]);
writedisplay(ram); delay(TYPEDELAY);
write('X'-'A', letters, c[0]);
writedisplay(ram); delay(TYPEDELAY);
write(0, digits, c[1]);
writedisplay(ram); delay(TYPEDELAY);
write(2, digits, c[2]);
writedisplay(ram); delay(TYPEDELAY);
write(1, digits, c[3]);
writedisplay(ram); delay(TYPEDELAY);
// -
writedisplay(ram); delay(TYPEDELAY);
write('V'-'A', letters, p[1]);
writedisplay(ram); delay(TYPEDELAY);
write(5, digits, p[2]);
writedisplay(ram); delay(TYPEDELAY);
setbit(p_d);
writedisplay(ram); delay(TYPEDELAY);
write(0, digits, p[3]);
writedisplay(ram); delay(TYPEDELAY);
write('L'-'A', letters, e[0]);
writedisplay(ram); delay(TYPEDELAY);
write('C'-'A', letters, e[1]);
writedisplay(ram); delay(TYPEDELAY);
write('D'-'A', letters, e[2]);
writedisplay(ram); delay(TYPEDELAY);
// write('O'-'A', letters, e[3]);
// writedisplay(ram); delay(TYPEDELAY);
// release serial interface
pinMode(HT_CS, INPUT);
pinMode(HT_WR, INPUT);
pinMode(HT_DATA, INPUT);
// release V9821S
pinMode(HT_RES, INPUT);
// STOP
while(1);
} // gap detected
}//loop
// EOF
Let me know if this was useful to you and if/how you improved it.