vroland / epdiy

EPDiy is a driver board for affordable e-Paper (or E-ink) displays.
https://vroland.github.io/epdiy-hardware/
GNU Lesser General Public License v3.0
1.34k stars 187 forks source link

Resources and Suggestions #31

Open vroland opened 4 years ago

vroland commented 4 years ago

Feel free to post useful resources, suggestions or show off your projects here. Older comments of this nature can be found in #21 .

mcer12 commented 3 years ago

Sure, wouldn't mention it otherwise ;)

2dom commented 3 years ago

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!

mcer12 commented 3 years ago

@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

vroland commented 3 years ago

@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.

vroland commented 3 years ago

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.

mcer12 commented 3 years ago

@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.

mcer12 commented 3 years ago

@vroland you can do the dithering simply by adding a parameter in your python script when compressing the image. ;)

vroland commented 3 years ago

Yes, but I for the picture frame I don't want people to have to run the script ;)

2dom commented 3 years ago

I implemented it on the esp

2dom commented 3 years ago

Floyd Steinberg is rather trivial to do

mcer12 commented 3 years ago

@2dom please share your code

2dom commented 3 years ago

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);

}
2dom commented 3 years ago

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

mcer12 commented 3 years ago

For low power, GPIO12 shouldn't be used at all or HIGH by default as it has built-in external pullup on ESP32-wrover

vroland commented 3 years ago

@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.

vroland commented 3 years ago

@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.

2dom commented 3 years ago

Sure ... will do. Unfortunately I just killed by display (Ripped the flex cable). 2020 just keeps on giving ....

mcer12 commented 3 years ago

@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

2dom commented 3 years ago

Oh I have tried :) But then I noticed another crack where all that serious wiring is going on....

2dom commented 3 years ago

@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?

vroland commented 3 years ago

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.

2dom commented 3 years ago

@mcer12 How is that new board of yours coming along ? ☺️ Just received my 6inch display...

vroland commented 3 years ago

@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

mcer12 commented 3 years ago

@vroland looks fine, some suggestions:

  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.
  2. I suggest CH340C instead of G, it's the same thing (1:1 pin compatible) except it doesn't require crystal+10pf caps. And costs the same.
  3. For low power CH340 needs it's own regulator and only be powered when USB is attached (using a schottky diode). I use XC6206 for these things because cheap :)
  4. I use B5819WS on high voltage boosters, works well for me and is tiny package. Would use the large diodes only on low voltage because of high current.

Also, is the pinout of ESP32 and 74HC4094 to be considered final?

mcer12 commented 3 years ago

@2dom need final pinout first to be compatible ;)

vroland commented 3 years ago

@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? ;)

2dom commented 3 years ago

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)?

vroland commented 3 years ago

@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.

2dom commented 3 years ago

Yeah ... you are probably right.

mcer12 commented 3 years ago

@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.

2dom commented 3 years ago

Another thought would be to optimize the dump components to be included in the jlcpcb basic parts library for easy reproduction

mcer12 commented 3 years ago

@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.

2dom commented 3 years ago

@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.)

vroland commented 3 years ago

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.

vroland commented 3 years ago
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?

mcer12 commented 3 years ago

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.

vroland commented 3 years ago

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.

vroland commented 3 years ago

@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?

mcer12 commented 3 years ago

@vroland Looks good to me. If you want less different components, you can use MCP1700 also for the CH340 it's pin compatible.

mcer12 commented 3 years ago

@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.

vroland commented 3 years ago

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 :)

mcer12 commented 3 years ago

@vroland I use ME6210 which should work well but I drop the large tantalum in anyway, it doesn't hurt anything... this one: C122309

2dom commented 3 years ago

Shouldn't the TP4056 be hooked up to VBUS instead of VIN ?

mcer12 commented 3 years ago

@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

mcer12 commented 3 years ago

I think it was bleeding some current from BAT to VCC and to the CH340, hence the second diode.

vroland commented 3 years ago

@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.

mcer12 commented 3 years ago

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 :)

mcer12 commented 3 years ago

The issue is that if even tiny voltage appears on VCC of the TP4056, CH340 will suck it up.

vroland commented 3 years ago

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?

mcer12 commented 3 years ago

You probably mean CH340? It should be in series with the second diode, as seen in my schematic ;)