Open vroland opened 4 years ago
Sure, wouldn't mention it otherwise ;)
Great! - I was just going to make one or two boards as a Christmas present (epaper photo frame) but this is way too much fun to stop here!
@2dom Especially once you realize these 6" displays are same price as 1.54" waveshare displays. I am very tempted to buy bunch of them :D
@2dom I'm actually up to something similar :D And the dithering is a good idea. I guess you did the dithering on a desktop computer and not on the ESP32? For my picture frame version, I want to read JPEGs off a sd card requiring as little pre-processing as possible.
Regarding the flash chip: The SC7 (http://www.universaldisplay.asia/wp-content/uploads/2012/10/ED060SC7-2.0.pdf) has it described in its data sheet.
@vroland the 6" displays with 34pin connector all have the chip on the flex cable. Some have winbond branded chip, some have other (can't remember the brand now) but the chips look the same so I would assume they behave the same.
@vroland you can do the dithering simply by adding a parameter in your python script when compressing the image. ;)
Yes, but I for the picture frame I don't want people to have to run the script ;)
I implemented it on the esp
Floyd Steinberg is rather trivial to do
@2dom please share your code
Sure ... but it is pretty much hacked together at this point - lot's of room for improvements and optimizations :)
Libraries:
#include "Arduino.h"
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_types.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"
#include <stdio.h>
#include <string.h>
#define IS_SD
#ifdef IS_SD
#include "SD.h"
#include "FS.h"
#include "SPI.h"
#else
#include "SPIFFS.h"
#endif
#include "epd_driver.h"
#include <JPEGDecoder.h>
#ifdef IS_SD
SPIClass spiSD(HSPI);
#endif
uint8_t *img_buf;
uint8_t *source_buf;
uint16_t ep_width=1200;
uint16_t ep_height=825;
uint16_t this_pic=0;
double gamma_value = 1.2;
uint8_t gamme_curve[256];
/*====================================================================================
This sketch contains support functions to render the Jpeg images.
Created by Bodmer 15th Jan 2017
==================================================================================*/
// Return the minimum of two values a and b
#define minimum(a,b) (((a) < (b)) ? (a) : (b))
uint8_t find_closest_palette_color(uint8_t oldpixel)
{
return (round((oldpixel / 16)*16));
}
//====================================================================================
// Decode and paint onto the TFT screen
//====================================================================================
void jpegRender(int xpos, int ypos) {
// retrieve infomration about the image
uint16_t *pImg;
uint16_t mcu_w = JpegDec.MCUWidth;
uint16_t mcu_h = JpegDec.MCUHeight;
uint32_t max_x = JpegDec.width;
uint32_t max_y = JpegDec.height;
// Jpeg images are draw as a set of image block (tiles) called Minimum Coding Units (MCUs)
// Typically these MCUs are 16x16 pixel blocks
// Determine the width and height of the right and bottom edge image blocks
uint32_t min_w = minimum(mcu_w, max_x % mcu_w);
uint32_t min_h = minimum(mcu_h, max_y % mcu_h);
// save the current image block size
uint32_t win_w = mcu_w;
uint32_t win_h = mcu_h;
uint8_t mcu_h_scaled=mcu_h * ep_height / max_y;
// record the current time so we can measure how long it takes to draw an image
uint32_t drawTime = millis();
// save the coordinate of the right and bottom edges to assist image cropping
// to the screen size
max_x += xpos;
max_y += ypos;
// read each MCU block until there are no more
while ( JpegDec.read()) {
// save a pointer to the image block
pImg = JpegDec.pImage;
// calculate where the image block should be drawn on the screen
int mcu_x = JpegDec.MCUx * mcu_w + xpos;
int mcu_y = JpegDec.MCUy * mcu_h + ypos;
// check if the image block size needs to be changed for the right edge
if (mcu_x + mcu_w <= max_x) win_w = mcu_w;
else win_w = min_w;
// check if the image block size needs to be changed for the bottom edge
if (mcu_y + mcu_h <= max_y) win_h = mcu_h;
else win_h = min_h;
// copy pixels into a contiguous block
if (win_w != mcu_w)
{
for (int h = 1; h < win_h-1; h++)
{
memcpy(pImg + h * win_w, pImg + (h + 1) * mcu_w, win_w << 1);
}
}
unsigned long pixel=0;
for (uint16_t by=0; by<win_h;by++)
{
for (uint16_t bx=0; bx<win_w;bx++)
{
uint16_t this_pixel=pImg[pixel];
uint8_t red = ((((this_pixel) & 0xf800) >> 11) << 3);
uint8_t green = ((((this_pixel) & 0x07e0) >> 5) << 2);
uint8_t blue = (((this_pixel) & 0x001f) << 3);
uint8_t gray=(red+green+blue)/3;
//line_buf[(by+mcu_y)*max_x+bx+mcu_y]=gray;
uint16_t nearestMatch_y = (int)((by+mcu_y) * ep_height / max_y);
uint16_t nearestMatch_x=0;
if (max_x>max_y)
nearestMatch_x = (int)((bx+mcu_x) * ep_width / max_x);
else
nearestMatch_x = (int)((bx+mcu_x) * ep_height / max_y);
source_buf[nearestMatch_x+nearestMatch_y*ep_width]=gamme_curve[gray];
//epd_draw_pixel(nearestMatch_x,nearestMatch_y,gray,img_buf);
pixel++;
}
}
}
unsigned long pixel=0;
// Dithering
for (uint16_t by=0; by<ep_height;by++)
{
for (uint16_t bx=0; bx<ep_width;bx++)
{
int oldpixel = source_buf[pixel];
int newpixel = find_closest_palette_color(oldpixel);
int quant_error = oldpixel - newpixel;
source_buf[pixel]=newpixel;
if (bx<(ep_width-1))
source_buf[pixel+1] = min(255,source_buf[pixel+1] + quant_error * 7 / 16);
if (by<(ep_height-1))
{
if (bx>0)
source_buf[pixel+ep_width-1] = min(255,source_buf[pixel+ep_width-1] + quant_error * 3 / 16);
source_buf[pixel+ep_width] = min(255,source_buf[pixel+ep_width] + quant_error * 5 / 16);
if (bx<(ep_width-1))
source_buf[pixel+ep_width+1] = min(255,source_buf[pixel+ep_width+1] + quant_error * 1 / 16);
}
pixel++;
}
}
// Write to displayv
pixel=0;
//epd_draw_grayscale_image(epd_full_screen(), 0);
uint16_t scaled_width = (int)(max_x * ep_height / max_y);
uint16_t padding=0;
uint16_t right_end = ep_width;
if (max_x<max_y)
{
padding= (ep_width-scaled_width)/2;
right_end = scaled_width+padding;
}
for (uint16_t by=0; by<ep_height;by++)
{
for (uint16_t bx=0; bx<ep_width;bx++)
{
//epd_draw_pixel(bx+padding,by,0,img_buf);
if (bx<=right_end)
epd_draw_pixel(bx+padding,by,source_buf[pixel],img_buf);
if (bx<padding)
epd_draw_pixel(bx,by,128,img_buf);
if (bx>right_end)
epd_draw_pixel(bx,by,128,img_buf);
// else
pixel++;
}
}
// calculate how long it took to draw the image
drawTime = millis() - drawTime;
// print the results to the serial port
Serial.print ("Total render time was : ");
Serial.print(drawTime); Serial.println(" ms");
Serial.println("=====================================");
}
//====================================================================================
// This function opens the Filing System Jpeg image file and primes the decoder
//====================================================================================
void drawFSJpeg(const char *filename, int xpos, int ypos) {
Serial.println("=====================================");
Serial.print("Drawing file: "); Serial.println(filename);
Serial.println("=====================================");
// Open the file (the Jpeg decoder library will close it)
#ifdef IS_SD
File jpgFile = SD.open(filename); // File handle reference for SPIFFS
#else
fs::File jpgFile = SPIFFS.open( filename, "r"); // File handle reference for SPIFFS
#endif
// File jpgFile = SD.open( filename, FILE_READ); // or, file handle reference for SD library
if ( !jpgFile ) {
Serial.print("ERROR: File \""); Serial.print(filename); Serial.println ("\" not found!");
return;
}
// To initialise the decoder and provide the file, we can use one of the three following methods:
//boolean decoded = JpegDec.decodeFsFile(jpgFile); // We can pass the SPIFFS file handle to the decoder,
//boolean decoded = JpegDec.decodeSdFile(jpgFile); // or we can pass the SD file handle to the decoder,
#ifdef IS_SD
boolean decoded = JpegDec.decodeSdFile(filename); // or we can pass the SD file handle to the decoder,
#else
boolean decoded = JpegDec.decodeFsFile(filename); // or we can pass the filename (leading / distinguishes SPIFFS files)
#endif // The filename can be a String or character array
if (decoded) {
// render the image onto the screen at given coordinates
jpegRender(xpos, ypos);
}
else {
Serial.println("Jpeg file format not supported!");
}
jpgFile.close();
}
uint8_t file_count=0;
String file_names [256];
void list_files()
{
static File file;
Serial.println("list");
File root = SD.open("/");
file = root.openNextFile();
while (file) {
if (file.isDirectory()) {
Serial.print("DIR: ");
Serial.println(file.name());
}
else {
Serial.print("FILE: ");
Serial.print(file.name());
Serial.print(" SIZE: ");
Serial.println(file.size());
file_names[file_count]=file.name();
file_count++;
}
file = root.openNextFile();
}
Serial.println();
}
void setup() {
Serial.begin(115200);
epd_init();
#ifdef IS_SD
Serial.print("Initializing SD card...");
spiSD.begin(14, 34, 13, 15);
if (!SD.begin(15,spiSD )) {
Serial.println("initialization failed!");
return;
}
uint8_t cardType = SD.cardType();
if(cardType == CARD_NONE){
Serial.println("No SD card attached");
return;
}
Serial.print("SD Card Type: ");
if(cardType == CARD_MMC){
Serial.println("MMC");
} else if(cardType == CARD_SD){
Serial.println("SDSC");
} else if(cardType == CARD_SDHC){
Serial.println("SDHC");
} else {
Serial.println("UNKNOWN");
}
list_files();
this_pic=random(file_count-1);
Serial.println("Numer of files: " + String(file_count));
#else
Serial.print("Initializing SSPIFFS card...");
if (!SPIFFS.begin(true)) {
Serial.println("initialization failed!");
return;
}
#endif
Serial.println("initialization done.");
// put your setup code here, to run once:
img_buf = (uint8_t *)heap_caps_malloc(EPD_WIDTH * EPD_HEIGHT / 2, MALLOC_CAP_SPIRAM);
source_buf = (uint8_t *)heap_caps_malloc(EPD_WIDTH * EPD_HEIGHT, MALLOC_CAP_SPIRAM);
Serial.println("heap allocated");
//memcpy(img_buf, dragon_data, EPD_WIDTH * EPD_HEIGHT / 2);
Serial.println("img copied");
double gammaCorrection = 1.0 / gamma_value;
for (int gray_value =0; gray_value<256;gray_value++)
gamme_curve[gray_value]= round (255*pow(gray_value/255.0, gammaCorrection));
drawFSJpeg("/tiffany.jpg", 0, 0);
Serial.println("EP Power On");
epd_poweron();
Serial.println("EP Power clear");
// epd_draw_image(epd_full_screen(), img_buf, WHITE_ON_WHITE);
epd_clear();
epd_push_pixels(epd_full_screen(), 20, 0);
epd_push_pixels(epd_full_screen(), 20, 0);
epd_push_pixels(epd_full_screen(), 20, 0);
epd_draw_image(epd_full_screen(), img_buf, WHITE_ON_BLACK);
Serial.println("EP Power Off");
epd_poweroff();
}
void loop() {
const char * this_filename = file_names[this_pic].c_str();
drawFSJpeg(this_filename, 0, 0);
Serial.println("EP Power On");
epd_poweron();
Serial.println("EP Power clear");
// epd_draw_image(epd_full_screen(), img_buf, WHITE_ON_WHITE);
epd_clear();
Serial.println("EP Write image");
epd_push_pixels(epd_full_screen(), 20, 0);
epd_push_pixels(epd_full_screen(), 20, 0);
epd_push_pixels(epd_full_screen(), 20, 0);
epd_draw_image(epd_full_screen(), img_buf, WHITE_ON_BLACK);
Serial.println("EP Power Off");
epd_poweroff();
this_pic++;
if (this_pic==file_count)
this_pic=0;
delay(10000);
}
Another thought reg. improvements - work with a 8bit gray scale frame buffer in the driver and do the reduction to 4 bit per pixel on the fly / in the DMA. This would make it much simpler and faster to interact with the frame buffer
For low power, GPIO12 shouldn't be used at all or HIGH by default as it has built-in external pullup on ESP32-wrover
@2dom Nice, I'm curious to try that :) Regarding the external LUT: I just realized that I forgot that the EPD input is always 4 pixels in parallel, so to use an external LUT we would need a 16bit wide bus (or 32bit and 4GB! LUT for old / new), so that wouldn't be worth it. And using some kind of buffer like a shift register would make things slow again. So I guess I'll focus on low power then.
Also, I used 8bit buffers in the beginning and I think it would be a performance hit. This is because most large buffers live in the external PSRAM, so modifying pixels would rather be bandwidth-limited instead of compute-limited. To make using the 4bit buffers easier, I exposed some library functions. Futhermore, for drawing a buffer to the screen it has to be read 15 times from the PSRAM, and reading 7.5MB or 15MB in total definitely makes a difference there.
@2dom Would you mind making adding the picture frame code as an example? Just add a license and PR it? :) I think this "digital picture frame" is something quite a few people would be interested in.
Sure ... will do. Unfortunately I just killed by display (Ripped the flex cable). 2020 just keeps on giving ....
@2dom To the contrary. 2020 gives opportunities! This is a great opportunity for you to buy a flex cable and teach yourself some microsoldering :P
Oh I have tried :) But then I noticed another crack where all that serious wiring is going on....
@vroland Just noticed ... I used the Arduino framework and threw out a few things to make it work. Not sure of how much help the example would be like that?
Oh, this is unfortunate. I always tape them down first, that has helped me so far.
Well, we could just add a readme that links to the install instructions for the arduino IDF component. So it may not be for the beginner, but I think it may help some people. I think I will also use another JPEG decoder (the one I used for the album covers in the MPD example, and is also included with the IDF), since that allows to set a pre-scaling factor which the library you linked does not expose.
@mcer12 How is that new board of yours coming along ? ☺️ Just received my 6inch display...
@mcer12 @2dom I now have a (conecptually) final schematic in the v5 branch. The new board includes connectors for the ED060SC7 and ED060XC5 displays, auto-reset functionality and a LiPo charging circuit. As I am not that familiar with battery circuitry, could you have a look if there are any issues with it (esp. @mcer12 )?
Here is the schematic as PDF: epaper-breakout.pdf
@vroland looks fine, some suggestions:
Also, is the pinout of ESP32 and 74HC4094 to be considered final?
@2dom need final pinout first to be compatible ;)
@mcer12 Thanks for the feedback! I think the pinout should be mostly final, however, if I encounter something that's really in the way when routing, I might consider changing it. But I already started routing for the most important things, so it should most likely stay like this. What else are you adding to your board, btw? ;)
Schematics looks good - maybe name the 5V rail something different as it will be less if running on battery. Another thought towards simplification would be to remove the under/over voltage protection as most lipo packs these days have that build in? Or leave it in and open the door for non-protected cells (16850 etc)?
@2dom I thing I have some old cells without protection where it would be useful. But maybe a jumper to bypass protection if you don't need it so you don't need to populate it? On the other hand, this adds even more complexity.
Yeah ... you are probably right.
@vroland Personally, I would leave it to the user to optioanlly use external protection circuitry and remove it in the design (just add a note that external protection is required). Protection boards are widely available for 18650 batteries... but that's just my opinion.
Another thought would be to optimize the dump components to be included in the jlcpcb basic parts library for easy reproduction
@2dom most components have alternatives in jlcpcb parts library but you will have to source some of them either way. My board will probably be slightly better in terms of jlcpcb essembly service but you will still have to solder some number of components yourself.
@mcer12 Yeah - but I remember that I had to hand-solder some caps and resistors because they had some non-common value (was too cheap to pay the extra handling fee). Might make sense to check if all those need to be that specific value or something more standard / included in the basic parts library will do too.... (Same with mosfets etc, regulators, etc.)
Ok, I'll remove the protection circuitry then. This is the part of the circuit I am the most unfamiliar with anyway ;) With the V4 board all resistors and caps where JLCPCB basic parts in my order, yes, some components could be changed, like the coils. The diodes are extended parts as well, but the B5819W could be listed as alternative in the BOM. Maybe there are pin-compatible voltage regulators as well.
1. a a note to TP4056 for R9 to select value depending on the battery capacity. For something like 500mAh something around 6k should be used for example.
Hm, according to this data sheet https://dlnmh9ip6v2uc.cloudfront.net/datasheets/Prototyping/TP4056.pdf 2k is 580mA. Where do you get the 6k from?
I said "something like" 6k, that would give you around 220mA ;) The table is just examples for measure, you don't have to use the exact values. I like to charge the batteries slow (below 1C, 0.5C preferrably) for higher lifetime. 2k is perfectly fine for 18650 but way too high for say 250mAh lipo and OK for 500mAh I guess but not my choice.
Ah, right, that makes sense :) I didn't have any low-capacity cells lying around, so I didn't think of that. I'll go with 6.8k then.
@mcer12 @2dom epaper-breakout.pdf Iv'e removed the battery protection, changed the charge current resistor and added a regulator for the CH340C. Is there anything else I've missed?
@vroland Looks good to me. If you want less different components, you can use MCP1700 also for the CH340 it's pin compatible.
@vroland Oh one more thing since I mention the regulators. If you have an oscilloscope, I would check if the voltage on the main 3.3V doesn't dip when using wifi. MCP1700 is quite at the edge for ESP32 and I use large 470uF capacitor in my projects to compensate for the wifi current spikes.
Unfortunately I don't have an oscilloscope, it would definitely be useful sometimes. So far I did not encounter any problems when using wifi, but that doesn't have to mean anything. With a quick search I couldn't find any parts with a similarly low current consumption but more current output, so a bigger cap sounds like a workaround for now. Thanks for checking :)
@vroland I use ME6210 which should work well but I drop the large tantalum in anyway, it doesn't hurt anything... this one: C122309
Shouldn't the TP4056 be hooked up to VBUS instead of VIN ?
@2dom yes, also another diode should be used to separate CH340. I have it like this in all my battery projects: https://www.dropbox.com/s/tl8f4w1v52uxxoc/Schematic_9.7inch%20e-ink%20controller_2021-01-02.pdf?dl=0
I think it was bleeding some current from BAT to VCC and to the CH340, hence the second diode.
@2dom Good catch, thank you! @mcer12 The current is bleeding through the TP4056? It's data sheet says it could be up to 6uA. With two diodes you get quite a voltage drop from the 5V in... But I guess efficiency is more or less irrelevant when running from USB.
Yes, the TP4056 by itself has 6uA draw but CH340 draws quite a bit through it. As I remember it anyway, it's been a while since I designed this... you can replace the diode with 0ohm resistor and see what difference it makes.
If anything, the voltage drop should make work easier for the 3.3V regulator :)
The issue is that if even tiny voltage appears on VCC of the TP4056, CH340 will suck it up.
Ok, I've added the diode directly after the power supply for the tp4056, which would make it parallel to the power switcher (with the mosfet and other diode). Any issues with that?
You probably mean CH340? It should be in series with the second diode, as seen in my schematic ;)
Feel free to post useful resources, suggestions or show off your projects here. Older comments of this nature can be found in #21 .