Развлекаемся с электрофоретическими дисплеями

в 5:07, , рубрики: Разработка под Arduino
Развлекаемся с электрофоретическими дисплеями - 1

Как-то перебирая старый хлам в попытке навести порядок, я наткнулся на старую электронную книжку, купленную больше 15 лет тому назад. Когда-то я ей активно пользовался, но, несмотря на то, что операционка была на базе Linux, она была очень склонна к подвисаниям. Разогнутая скрепка, с помощью которой можно было сбросить устройство, стала ее непременным атрибутом. Потом я купил другую книжку, уже за смешные деньги, но у нее никаких проблем не наблюдалось.

А старая лежала, постепенно превращаясь в тыкву. Нет, с электроникой было все в порядке, и даже аккумулятор не умер за это время. Но вот приятный на ощупь мягкий пластик задней крышки превратился в какую-то сопливую субстанцию, которую в руки без омерзения уже не взять.

В голову пришла идея сделать из нее на кухню что-то типа рамки, которая будет показывать прогноз погоды, температуру дома, на улице и в гараже. Питание хотелось бы иметь батарейное, поэтому обычные дисплеи отпадали. Нужен был именно e-ink дисплей, и старая читалка была извлечена из забвения и немедленно разобрана.

Развлекаемся с электрофоретическими дисплеями - 2

В ней был обнаружен дисплей LB060S01-RD02, который вроде как, должен быть совместим с популярным ED060SC4, использующийся во многих моделях книг. Со спецификациями большая проблема — те, что были найдены, очень неполные и что-то разработать, используя только их, просто нереально. Информации катастрофически мало. Но не перевелись хакеры на земле — что нашей, что ихней. Что один человек сделал, другой сломать завсегда сможет. Вот что нашлось по этой теме в интернете:

Со всеми находками вроде бы можно и приступать. Но имея всего один дисплей — это как-то ненадежно: один я сломаю, второй потеряю, а с третьим, может, что-то и выйдет. Решил, что неплохо бы найти несколько одинаковых дохлых книг — теоретически их можно купить недорого, и не надо искать разъемы для дисплеев, их тоже можно вытащить из книги. Может, в них еще какие ништяки найдутся. Ну и если из нескольких дохлых книг хоть как-то удастся запустить одну — можно и протокол в случае чего попытаться проанализировать. И вот я набрел на товары — кто-то в Англии продавал штук 8 или 10 Kindle. Но там был аукцион и кто-то перебил ту цену, которую я готов был заплатить за этот хлам. В Америке нашелся еще один лот — 5 штук Nook, у одной явно сломан экран, остальные выглядят прилично.

За все просили 25 баксов, столько же доставка и еще НДС придется заплатить. Короче, все это вылилось мне чуть больше 60 долларов. Заплатил, стал ждать. Китайская почта нервно курит в сторонке по сравнению с американской — заплатил приличную сумму и везли они посылку больше месяца.
За это время я спроектировал печатную плату для дисплея, заказал ее в Китае с самой дешевой и медленной доставкой, получил — а с Америки посылки все нет и нет.

Развлекаемся с электрофоретическими дисплеями - 3

В конце-концов, почта все-таки доставила посылку. Аккумуляторы у всех книжек были мертвые совсем-совсем, что давало надежду, что при наличии питания какое-то устройство заработает. Но оставь надежду всяк, сюда входящий — признаки жизни подала только одна книга. И это был последний выдох господина Пэже — после несколько минут демонстрации возможностей, книжка больше никогда признаков жизни не подавала.

Развлекаемся с электрофоретическими дисплеями - 4

Barnes and Noble Nook от большинства книжек отличается наличием маленького жидкокристаллического цветного дисплея с сенсорным экраном ниже основного 6-дюймового. У него есть WiFi, и, как опция, модем для подключения к сотовой сети.

Что там можно найти:

  1. Основной дисплей ED060SC4(LF), 6 дюймов, 600x800

  2. Дополнительный дисплей CLAA035JA01CW, 480x144. Никакой документации на него я не нашел, так что вещь бесполезная

  3. Процессор S3C6410XH-53

  4. Контроллер основного дисплея S1D13521

  5. SD карточка в роли флеш-памяти

  6. SDRAM Hynix — H5MS1G62MFP-J3M

  7. Сотовый модем Sierra Wireless MC8777V

  8. WM8350 — Wolfson AudioPlusTM Stereo CODEC with Power Management

  9. WiFi PW621

  10. И куча прочей никому не интересной ерунды.

Развлекаемся с электрофоретическими дисплеями - 5
Развлекаемся с электрофоретическими дисплеями - 6

Старые e-ink дисплеи для работы требуют целый пучок напряжений питания — 3.3 Вольта, ±15 Вольт, -20 Вольт и +22 Вольта, которые требуют определенной последовательности включения.

Развлекаемся с электрофоретическими дисплеями - 7
Развлекаемся с электрофоретическими дисплеями - 8

И такое питание останавливает многих, желающих поэкспериментировать.
Есть специальные микросхемы для питания таких дисплеев, например TPS65186.

Развлекаемся с электрофоретическими дисплеями - 9

В Nook и моей старой Readee используется TPS65186, с точки зрения экономичности далеко не лучшее решение.

Развлекаемся с электрофоретическими дисплеями - 10
Развлекаемся с электрофоретическими дисплеями - 11

Мы пойдем своим путем, и соорудим модуль питания из желудей и палок. И тут Чук и Гек спешат к нам на помощь! Или это кто-то другой — может, Чип и Дейл? Но вообще-то без разницы, на самом деле это SEPIC и Cuk с зарядовым насосом наперевес, которые с помощью одной дешевой микросхемы повышающего преобразователя (например, MT3608) позволяют получить сразу два напряжения ±15 Вольт.

Развлекаемся с электрофоретическими дисплеями - 12

А более высокое напряжение требует совсем маленького тока, так что получившиеся напряжения просто умножим на 2 с помощью зарядового насоса на базе популярного и дешевого таймера 555 и сделаем параллельный стабилизатор напряжения на базе тоже недорогих LM431. А вот тут будьте осторожны — LM431 и другие клоны имеют максимальное допустимое напряжение 36 Вольт. А безымянные 431 китайского производства с Али сгорают уже при 12 Вольтах, будьте внимательны, я нарвался на это.

Развлекаемся с электрофоретическими дисплеями - 13
Развлекаемся с электрофоретическими дисплеями - 14

В более современных e-ink дисплеях — типа тех, что используются в электронных ценниках, которые можно спереть из ближайшего магазина иногда купить на ebay по смешным ценам, используется более удобная система питания.

Развлекаемся с электрофоретическими дисплеями - 15

Там тоже нужно много напряжений, но они формируются при помощи внешнего транзистора, катушки и нескольких диодов и конденсаторов. И все это контролируется самим контроллером дисплея, ни о чем думать не надо.

Развлекаемся с электрофоретическими дисплеями - 16

С дисплеями произошла неожиданная задержка — разъемы для подключения дисплея оказались совсем не такими, как я рассчитывал, и протестировать идею сразу возможности не было, пришлось новую плату заказывать.

Развлекаемся с электрофоретическими дисплеями - 17

Пока ждал плату, обзавелся электронными ценниками, но давайте отделим мух от котлет и о них я расскажу немножко позднее?

Быстро сказка сказывается, но дело делается подольше. В конце-концов, пришли заказанные печатные платы и теперь можно заняться программным обеспечением. То, что описано ниже, касается только ED060SC4(LF) или LB060S01-RD02. Отличие в названии на одну букву, даже ту, что в скобке — и работать ничего не будет. Когда я задумал это безнадежное занятие, мне казалось, что я нашел кучу информации и исходников, и запустить этот дисплей проблемой не будет, несмотря на очень убогие спецификации от производителей. Не тут-то было, в каких-то местах описание оказалось на дисплей, название которого отличалась на одну букву, а в других — алгоритм просто не работал.

Целых два дня я тупо перебирал очередность разных сигналов и задержки между ними, не зная, жив ли еще дисплей. Не было даже намеков на появившуюся точку. И в один прекрасный момент — вуаля! — получите целую линию мусора. Всё, больше ничего и не надо, все остальное, дело техники, теперь он никуда не денется.

Развлекаемся с электрофоретическими дисплеями - 18

Теперь расскажу, как все-таки удалось запустить это чудо. На истину в последней инстанции я не претендую, как заработало — так и опишу.

Для прорисовки экрана нужны следующие сигналы — сначала начало кадра, потом 600 строк, каждая имеет старт, потом данные и стоп. Завершается все концом кадра. За один пиксель отвечает два бита, комбинация 01 — черный цвет, 10 — белый, остальные — без изменения.
Таким образом, для одной строки из 800 пикселей нужно передать 200 байт.

Схема простая — источник питания, описанный в ранее, STM32F103C8 для управления дисплеем и ESP32 на все про все, но пока его не используем.

Развлекаемся с электрофоретическими дисплеями - 19

Ардуним для начала — определяем макросы, которые будут управлять сигналами.

Код
// ****************************************************
// power control
// en positive PB6
// en negative PB7
// en VDD      PB8
//
#define INIT_POWER GPIOB->regs->CRL  &=  0x00FFFFFF;
                   GPIOB->regs->CRL  |=  0x11000000;
                   GPIOB->regs->CRH  &=  0xFFFFFFF0;
                   GPIOB->regs->CRH  |=  0x00000001;
                   GPIOB->regs->BRR  = 0x01C0;                 

#define PWR_VDD_ON  GPIOB->regs->BSRR = 0x0100;
#define PWR_VDD_OFF GPIOB->regs->BRR = 0x0100;
#define PWR_POS_ON  GPIOB->regs->BSRR = 0x0040;
#define PWR_POS_OFF GPIOB->regs->BRR = 0x0040;
#define PWR_NEG_ON  GPIOB->regs->BSRR = 0x0080;
#define PWR_NEG_OFF GPIOB->regs->BRR = 0x0080;
#define PWR_ALL_ON  GPIOB->regs->BSRR = 0x01C0;
#define PWR_ALL_OFF GPIOB->regs->BRR = 0x01C0;
// ****************************************************

// SPV and SPH high, and all other pins low
  // data
  #define DATA_PORT  GPIOA
  // CL  LE OE CPH GMODE SPV CKV
  // CL PB9
  #define CL_PORT  GPIOB  
  #define CL_PIN   9  
  // LE PC13  
  #define LE_PORT  GPIOC  
  #define LE_PIN   13  
  // OE PC14  
  #define OE_PORT  GPIOC  
  #define OE_PIN   14  
  // CPH PC15  
  #define CPH_PORT  GPIOC  
  #define CPH_PIN   15  
  // GMODE PB0  
  #define GMODE_PORT  GPIOB  
  #define GMODE_PIN   0  
  // CKV PCB2  
  #define CKV_PORT  GPIOB  
  #define CKV_PIN   2  
  // SPV PB1  
  #define SPV_PORT  GPIOB  
  #define SPV_PIN   1  

// DATA
// PC0...PC7
// PA0...PA7
#define INIT_DATA  DATA_PORT->regs->CRL  = 0x33333333; DATA_PORT->regs->BRR  = 0xFF;  

#define INIT_CL    CL_PORT->regs->CRH &= ~(0xF << ((CL_PIN &0x7)<<2)); CL_PORT->regs->CRH |= 0x3 << ((CL_PIN &0x7)<<2);
#define CL_IDLE    CL_PORT->regs->BRR  = 1<<CL_PIN;  // reset
#define CL_ACTIVE  CL_PORT->regs->BSRR = 1<<CL_PIN;  // set


#define INIT_LE    LE_PORT->regs->CRH &= ~(0xF << ((LE_PIN &0x7)<<2)); LE_PORT->regs->CRH |= 0x3 << ((LE_PIN &0x7)<<2);
#define LE_IDLE    LE_PORT->regs->BRR  = 1<<LE_PIN;  // reset
#define LE_ACTIVE  LE_PORT->regs->BSRR = 1<<LE_PIN;  // set


#define INIT_GMODE    GMODE_PORT->regs->CRL &= ~(0xF << ((GMODE_PIN &0x7)<<2)); GMODE_PORT->regs->CRL |= 0x3 << ((GMODE_PIN &0x7)<<2);
#define GMODE_IDLE    GMODE_PORT->regs->BRR  = 1<<GMODE_PIN;  // reset
#define GMODE_ACTIVE  GMODE_PORT->regs->BSRR = 1<<GMODE_PIN;  // set

#define INIT_CKV    CKV_PORT->regs->CRL &= ~(0xF << ((CKV_PIN &0x7)<<2)); CKV_PORT->regs->CRL |= 0x3 << ((CKV_PIN &0x7)<<2);
#define CKV_IDLE    CKV_PORT->regs->BRR  = 1<<CKV_PIN;  // reset
#define CKV_ACTIVE  CKV_PORT->regs->BSRR = 1<<CKV_PIN;  // set

#define INIT_SPV    SPV_PORT->regs->CRL &= ~(0xF << ((SPV_PIN &0x7)<<2)); SPV_PORT->regs->CRL |= 0x3 << ((SPV_PIN &0x7)<<2);
#define SPV_IDLE    SPV_PORT->regs->BRR  = 1<<SPV_PIN;  // reset
#define SPV_ACTIVE  SPV_PORT->regs->BSRR = 1<<SPV_PIN;  // set


#define INIT_OE    OE_PORT->regs->CRH &= ~(0xF << ((OE_PIN &0x7)<<2)); OE_PORT->regs->CRH |= 0x3 << ((OE_PIN &0x7)<<2);
#define OE_IDLE    OE_PORT->regs->BRR  = 1<<OE_PIN;  // reset
#define OE_ACTIVE  OE_PORT->regs->BSRR = 1<<OE_PIN;  // set

#define INIT_CPH    CPH_PORT->regs->CRH &= ~(0xF << ((CPH_PIN &0x7)<<2)); CPH_PORT->regs->CRH |= 0x3 << ((CPH_PIN &0x7)<<2);
#define CPH_IDLE    CPH_PORT->regs->BRR  = 1<<CPH_PIN;  // reset
#define CPH_ACTIVE  CPH_PORT->regs->BSRR = 1<<CPH_PIN;  // set



void Vclock()
{
  CKV_ACTIVE
  CKV_IDLE    
}

void Hclock()
{
  CL_ACTIVE
  CL_IDLE    
}

void PowerOn(void)
{
  PWR_VDD_ON
  delay(30);     
  PWR_NEG_ON
  delay(5);     
  PWR_POS_ON  
}

void PowerOff(void)
{
  PWR_POS_OFF    
  delay(5);     
  PWR_NEG_OFF
  delay(5);     
  PWR_VDD_OFF
}

Старт кадра выглядит так:

Развлекаемся с электрофоретическими дисплеями - 20
void start_frame()
{
  GMODE_ACTIVE
  delay_us(1);
  CKV_ACTIVE
  delay_us(1);
  SPV_IDLE 
  delay_us(1);
  CKV_IDLE  
  delay_us(1);
  CKV_ACTIVE
  delay_us(1);
  SPV_ACTIVE
  delay_us(1);
}

Потом передаем 600 строк, каждая начинается так:

Развлекаемся с электрофоретическими дисплеями - 21
void start_row()
{
  CPH_IDLE
}
Развлекаемся с электрофоретическими дисплеями - 22

Завершение строки очень критично, чуть что не так — и никакого изображения не будет:

Развлекаемся с электрофоретическими дисплеями - 23
void stop_row()
{
  CPH_ACTIVE
  CKV_IDLE  
  Hclock();
  CKV_ACTIVE  
  OE_IDLE
  delay_us(2);  
  OE_ACTIVE  
  LE_ACTIVE
  LE_IDLE  
}

И завершаем кадр:

Развлекаемся с электрофоретическими дисплеями - 24
void end_frame()
{
GMODE_IDLE
delay_us(1);
Vclock();
}

Все вместе образует вот такое безобразие:

Развлекаемся с электрофоретическими дисплеями - 25

И, наконец, тестовый код:

код
<code>uint8_t data;


void setup()
{
  INIT_POWER

  GPIOB->regs->CRH  &=  0xFFFFF0FF;    //pinMode(PB10, OUTPUT); 2MHz
  GPIOB->regs->CRH  |=  0x00000200;  

  INIT_DATA
  INIT_CL
  CL_IDLE

  INIT_GMODE
  GMODE_IDLE

  INIT_CKV
  CKV_IDLE 

  INIT_SPV
  SPV_ACTIVE 

  INIT_OE
  OE_IDLE

  INIT_CPH
  CPH_ACTIVE

  INIT_LE
  LE_IDLE  

  data=0xaa;
}



void loop() 
{
  uint8_t tdata;

  GPIOB->regs->BSRR  = 1<<10;  // LED on

  PowerOn();
  delay(100);  
 

  for (uint16_t k=0; k<6; k++)
  {
    static bool even=false;
    start_frame();
    // write frame
    for (uint16_t i=0; i<600; i++)
    {
      // wrire row
      if (even) tdata = data;  
      else      tdata = ~data;    
      if((i & 0x3f)==0) even = !even;
      start_row();
      //data = 0;   
      for (uint16_t j=0; j<200; j++)
      {
        if((j & 0x0f)==0) tdata = ~tdata;
        DATA_PORT->regs->BRR  = 0xFF;  
        DATA_PORT->regs->BSRR  = tdata;  
        Hclock();      
      }
      stop_row();
    }
  }
  data = ~data;
  

  end_frame();

  delay(10);  
  PowerOff();
  GPIOB->regs->BRR  = 1<<10;  // LED off
  delay(2000);  
}

Извиняйте, если уж сильно тривиально все выглядит. Если бы мне все это кто-то рассказал до того, как я эту возню затеял…
Может, кому другому поможет.

А это то, что этот тест делает — рисует шахматную доску. Смотрим скорость обновления — получается более, чем достойно, судите сами:

За один цикл прорисовки черные точки получить не удастся, получаются серые. У меня для получения черного цвета нужно повторить передачу кадра раз 6 — для пущей надежности я передаю его 8 раз.
В реальном устройстве присутствует lookup table, где хранятся данные о длительностях сигналов в зависимости от температуры и интенсивности черного. Почему-то производители сделали большой секрет из этих данных, поэтому имеем то, что имеем. У реального контроллера дисплея куча памяти, где он хранит изображение и следующее формируется в зависимости от предыдущего — не нужно полностью стирать изображение и потом рисовать все по новой — обновление происходит с учетом текущего состояния, поэтому его можно выполнить быстрее. Но оперативной памяти нужно много — ведь у экрана 480 000 пикселей.
По схеме видно, что микроконтроллер у меня дохленький, у него всего 20К ОЗУ и 64К флеш.
Но я использую буфер длиной всего 200 байт, и каждую строку просчитываю перед выводом.
Что-то подобное я использовал много лет назад, когда делал полетный контроллер для игрушечного самолетика и выводил изображение поверх картинки с видеокамеры — там, если склероз не подводит, вообще у контроллера (это был какой-то MSP430) было 4К ОЗУ. И сделано было еще интереснее — пока контроллер прямого доступа выбрасывал одну строку, процессор просчитывал следующую. В итоге использовалось 4 буфера — два для белого и 2 для черного.

Это было небольшое отступление. В реальных дисплеях производители обычно используют 16 градаций серого, но у любителей получается и 32 использовать.

Развлекаемся с электрофоретическими дисплеями - 26

Но мне это не надо, двух уровней черного для моих целей достаточно и я легко это сделаю использованием количества перезаписываний кадра.

Развлекаемся с электрофоретическими дисплеями - 27

Следующий шаг - добыча погоды из интернета и формирование полного кадра, но для этого будет использоваться ESP32, а STM32 будет играть роль тупого драйвера. Печатную плату надо снова переделать — контрольные точки уже не нужны, зато надо добавить управление питанием, планируется использовать литиевый аккумулятор. Нужно добавить зарядку и позаботиться о уменьшении потребления энергии устройством.

Развлекаемся с электрофоретическими дисплеями - 28
Развлекаемся с электрофоретическими дисплеями - 29
Тупой драйвер из STM32
#include <Arduino.h>
#include <SPI.h>
#include <libmaple/spi.h>
#include <libmaple/nvic.h>

void VSYNC_IRQHandler(void);
void HSYNC_IRQHandler(void);
void POWER_IRQHandler(void);

// ****************************************************
// power control
// en positive PB6
// en negative PB7
// en VDD      PB8
//
#define INIT_POWER GPIOB->regs->CRL  &=  0x00FFFFFF;
                   GPIOB->regs->CRL  |=  0x11000000;
                   GPIOB->regs->CRH  &=  0xFFFFFFF0;
                   GPIOB->regs->CRH  |=  0x00000003;
                   GPIOB->regs->BRR  = 0x01C0;                 

#define PWR_VDD_ON  GPIOB->regs->BSRR = 0x0100;
#define PWR_VDD_OFF GPIOB->regs->BRR = 0x0100;
#define PWR_POS_ON  GPIOB->regs->BSRR = 0x0040;
#define PWR_POS_OFF GPIOB->regs->BRR = 0x0040;
#define PWR_NEG_ON  GPIOB->regs->BSRR = 0x0080;
#define PWR_NEG_OFF GPIOB->regs->BRR = 0x0080;
#define PWR_ALL_ON  GPIOB->regs->BSRR = 0x01C0;
#define PWR_ALL_OFF GPIOB->regs->BRR = 0x01C0;
// ****************************************************

// SPV and SPH high, and all other pins low
  // data
  #define DATA_PORT  GPIOA
  // CL  LE OE CPH GMODE SPV CKV
  // CL PB9
  #define CL_PORT  GPIOB  
  #define CL_PIN   9  
  // LE PC13  
  #define LE_PORT  GPIOC  
  #define LE_PIN   13  
  // OE PC14  
  #define OE_PORT  GPIOC  
  #define OE_PIN   14  
  // CPH PC15  
  #define CPH_PORT  GPIOC  
  #define CPH_PIN   15  
  // GMODE PB0  
  #define GMODE_PORT  GPIOB  
  #define GMODE_PIN   0  
  // CKV PCB2  
  #define CKV_PORT  GPIOB  
  #define CKV_PIN   2  
  // SPV PB1  
  #define SPV_PORT  GPIOB  
  #define SPV_PIN   1  

// DATA
// PC0...PC7
// PA0...PA7
#define INIT_DATA  DATA_PORT->regs->CRL  = 0x33333333; DATA_PORT->regs->BRR  = 0xFF;  

#define INIT_CL    CL_PORT->regs->CRH &= ~(0xF << ((CL_PIN &0x7)<<2)); CL_PORT->regs->CRH |= 0x3 << ((CL_PIN &0x7)<<2);
#define CL_IDLE    CL_PORT->regs->BRR  = 1<<CL_PIN;  // reset
#define CL_ACTIVE  CL_PORT->regs->BSRR = 1<<CL_PIN;  // set


#define INIT_LE    LE_PORT->regs->CRH &= ~(0xF << ((LE_PIN &0x7)<<2)); LE_PORT->regs->CRH |= 0x3 << ((LE_PIN &0x7)<<2);
#define LE_IDLE    LE_PORT->regs->BRR  = 1<<LE_PIN;  // reset
#define LE_ACTIVE  LE_PORT->regs->BSRR = 1<<LE_PIN;  // set


#define INIT_GMODE    GMODE_PORT->regs->CRL &= ~(0xF << ((GMODE_PIN &0x7)<<2)); GMODE_PORT->regs->CRL |= 0x3 << ((GMODE_PIN &0x7)<<2);
#define GMODE_IDLE    GMODE_PORT->regs->BRR  = 1<<GMODE_PIN;  // reset
#define GMODE_ACTIVE  GMODE_PORT->regs->BSRR = 1<<GMODE_PIN;  // set

#define INIT_CKV    CKV_PORT->regs->CRL &= ~(0xF << ((CKV_PIN &0x7)<<2)); CKV_PORT->regs->CRL |= 0x3 << ((CKV_PIN &0x7)<<2);
#define CKV_IDLE    CKV_PORT->regs->BRR  = 1<<CKV_PIN;  // reset
#define CKV_ACTIVE  CKV_PORT->regs->BSRR = 1<<CKV_PIN;  // set

#define INIT_SPV    SPV_PORT->regs->CRL &= ~(0xF << ((SPV_PIN &0x7)<<2)); SPV_PORT->regs->CRL |= 0x3 << ((SPV_PIN &0x7)<<2);
#define SPV_IDLE    SPV_PORT->regs->BRR  = 1<<SPV_PIN;  // reset
#define SPV_ACTIVE  SPV_PORT->regs->BSRR = 1<<SPV_PIN;  // set


#define INIT_OE    OE_PORT->regs->CRH &= ~(0xF << ((OE_PIN &0x7)<<2)); OE_PORT->regs->CRH |= 0x3 << ((OE_PIN &0x7)<<2);
#define OE_IDLE    OE_PORT->regs->BRR  = 1<<OE_PIN;  // reset
#define OE_ACTIVE  OE_PORT->regs->BSRR = 1<<OE_PIN;  // set

#define INIT_CPH    CPH_PORT->regs->CRH &= ~(0xF << ((CPH_PIN &0x7)<<2)); CPH_PORT->regs->CRH |= 0x3 << ((CPH_PIN &0x7)<<2);
#define CPH_IDLE    CPH_PORT->regs->BRR  = 1<<CPH_PIN;  // reset
#define CPH_ACTIVE  CPH_PORT->regs->BSRR = 1<<CPH_PIN;  // set


#define VSYNC PA9
#define HSYNC PA8
#define POWER_EN PB11

bool request_powerOn;
bool request_powerOff;
bool active;


void Vclock()
{
  CKV_ACTIVE
  CKV_IDLE    
}


void PowerOn(void)
{
  SPV_ACTIVE 
  CPH_ACTIVE
  //
  PWR_VDD_ON
  delay(10);
  PWR_NEG_ON    
  delay(10);  
  PWR_POS_ON  
}

void PowerOff(void)
{
  // reset all sygnals!
  DATA_PORT->regs->BRR  = 0xFF;  
  CL_IDLE
  GMODE_IDLE
  CKV_IDLE 
  SPV_IDLE 
  OE_IDLE
  CPH_IDLE
  LE_IDLE  
  //
  PWR_POS_OFF    
  delay(10);  
  PWR_NEG_OFF
  delay(10);  
  // PWR_VDD_OFF
}

void setup()
{
  INIT_POWER

  GPIOB->regs->CRH  &=  0xFFFFF0FF;    //pinMode(PB10, OUTPUT); 2MHz
  GPIOB->regs->CRH  |=  0x00000200;  

  INIT_DATA
  INIT_CL
  CL_IDLE

  INIT_GMODE
  GMODE_IDLE

  INIT_CKV
  CKV_IDLE 

  INIT_SPV
  SPV_ACTIVE 

  INIT_OE
  OE_IDLE

  INIT_CPH
  CPH_ACTIVE

  INIT_LE
  LE_IDLE  

  pinMode(VSYNC, INPUT);
  GPIOB->regs->BRR  = 1<<10;  // LED off
  attachInterrupt(VSYNC, VSYNC_IRQHandler, FALLING);  

  pinMode(HSYNC, INPUT);
  attachInterrupt(HSYNC, HSYNC_IRQHandler, FALLING);    

  pinMode(POWER_EN, INPUT);
  attachInterrupt(POWER_EN, POWER_IRQHandler, CHANGE);    


  // MOSI PB15
  // MISO PB14
  // SCK PB13
  // SS PB12

  GPIOB->regs->CRH &= ~((0xF << 7*4) | (0xF << 6*4)| (0xF << 5*4) | (0xF << 4*4));
  GPIOB->regs->CRH |= ((GPIO_CR_MODE_INPUT | GPIO_CR_CNF_INPUT_FLOATING) << 7*4)
                   |  ((GPIO_CR_CNF_AF_OUTPUT_PP | GPIO_CR_MODE_OUTPUT_50MHZ) << 6*4)
                   |  ((GPIO_CR_MODE_INPUT | GPIO_CR_CNF_INPUT_FLOATING) << 5*4)
                   |  ((GPIO_CR_MODE_INPUT | GPIO_CR_CNF_INPUT_FLOATING) << 4*4); 
  RCC_BASE->APB1ENR |= RCC_APB1ENR_SPI2EN;      //SPI2


  SPI2->regs->CR1 |= SPI_CR1_SPE | SPI_CR1_BR_PCLK_DIV_2;                //Baud rate = Fpclk/2 , enabled, slave
  nvic_irq_set_priority(NVIC_SPI2, 3);
  nvic_irq_enable(NVIC_SPI2);

  request_powerOn = false;
  request_powerOff = false;
  active = false;
}



void loop() 
{
  if(request_powerOn)
  {
    request_powerOn = false;    
    PowerOn();
    GPIOB->regs->BSRR  = 1<<10;  // LED on    
    active = true;
  }

  if(request_powerOff)
  {
    request_powerOff = false;
    PowerOff();
    GPIOB->regs->BRR  = 1<<10;  // LED off    
    active = false;
  }
  //if(!active) asm("wfi");
}


extern "C" void __irq_spi2(void)
{    
  uint8_t data = SPI2->regs->DR;
  DATA_PORT->regs->BRR  = 0xFF;  
  DATA_PORT->regs->BSRR  = data;      
  CL_ACTIVE
  CL_IDLE    
}

void VSYNC_IRQHandler(void)
{
  // if faling egde
  if (EXTI_BASE->FTSR & (1<<9))
  {
    // start frame
    GMODE_ACTIVE
    delay_us(1);
    CKV_ACTIVE
    delay_us(1);
    SPV_IDLE 
    delay_us(1);
    CKV_IDLE  
    delay_us(1);
    CKV_ACTIVE
    delay_us(1);
    SPV_ACTIVE
    delay_us(1);
    // *********
    EXTI_BASE->FTSR &= ~(1<<9);  // faling egde disabled
    EXTI_BASE->RTSR |= (1<<9);  // rising edge enabled
    SPI2->regs->SR &= ~SPI_SR_RXNE;
  }
  else
  {
    // end frame
    GMODE_IDLE
    delay_us(1);
    Vclock();
    // ***********
    EXTI_BASE->RTSR &= ~(1<<9);  // rising egde disabled
    EXTI_BASE->FTSR |= (1<<9);  // faling edge enabled
  }
}


void HSYNC_IRQHandler(void)
{
 // if faling egde
  if (EXTI_BASE->FTSR & (1<<8))
  {
    // start row
    CPH_IDLE
    EXTI_BASE->FTSR &= ~(1<<8);  // faling egde disabled
    EXTI_BASE->RTSR |= (1<<8);  // rising edge enabled
    SPI2->regs->CR2 |= SPI_CR2_RXNEIE;
  }
  else
  {
    // end row
    CPH_ACTIVE
    CKV_IDLE  
    asm volatile ("nopnt");
    asm volatile ("nopnt");    
    CL_ACTIVE
    asm volatile ("nopnt");
    asm volatile ("nopnt");    
    CL_IDLE    
    CKV_ACTIVE  
    OE_IDLE
    delay_us(2);  
    OE_ACTIVE  
    LE_ACTIVE
    asm volatile ("nopnt");
    asm volatile ("nopnt");
    LE_IDLE  
    EXTI_BASE->RTSR &= ~(1<<8);  // rising egde disabled
    EXTI_BASE->FTSR |= (1<<8);  // faling edge enabled
    SPI2->regs->CR2 &= ~SPI_CR2_RXNEIE;
  }
    
}  

void POWER_IRQHandler(void)
{
  if((GPIOB->regs->IDR & (1<<11)) == 0) request_powerOn = true;
  else                                  request_powerOff = true;
}

С ESP32 не все так просто — при попытке выделить массив под кадр больше 96К, линкер отказывается работать, заявляя что все, памяти у него для вас больше нет. А где же ваши хваленые 320K Data RAM? При попытке выбросить SPI пакет, непрерывного потока не получается — после каждых 64 байта, кажется, идет разрыв несколько микросекунд. Но кому сейчас легко — будем бороться. Причина нашлась в библиотеке, но править не хочется — при очередном обновлении все правки накроются медным тазом. Вроде и так работает, за микросекундами гнаться нет нужды.

Где брать погоду — еще одна проблема. Меньше всего ограничений у open-meteo.com, но они, как настоящие джентльмены, меняют правила по ходу игры. А поначалу смотрелось неплохо — регистрация не нужна, информацию можно брать по максимуму и безо всяких ограничений на запросы. Прогноз, надо сказать, не особо точный для нашей деревни и еще и глюки имеются — как-то за окном шел дождь, а они на осадки на текущий день прогнозировали отрицательную величину. Интересно, это как?
Наверно, самый популярный сайт — это openweathermap.org, но они очень хотят денег и всячески затрудняют жизнь халявщиков. И необходимость регистрации и ограничение на запросы не радуют, хотя ограничения на запросы вполне разумные. Но информации open-meteo дает больше, и генерировать запрос можно прямо на сайте — натыкаешь крыжиков и можно копировать готовую строку запроса. На нем все-таки и остановлюсь, если не понравится — будем подумать.

Очередная платка пришла, собираем все до кучи. Со старых добрых времен у меня валяется целая куча батареек для самолетиков, некоторые никогда не использовались — но уже больше десяти лет назад куплены. Из одной 2S батарейки делаем 2P, все надежно крепим двухсторонней лентой, вырезаем отверстия в задней стенке и закрываем.

Развлекаемся с электрофоретическими дисплеями - 30

На форму отверстий не обращайте внимания, с моей атаксией я и стакан чая пронести, не расплескав, не могу. Ну и проблемы со зрением - бинокулярное не работает, но это еще далеко не все проблемы. Так что что-то сделать руками — большая проблема, а спаять плату — это вообще квест не для слабонервных.
И самое главное — это все-равно фальш-крышечка, сверху она закроется декоративной крышкой и вообще видно ничего не будет. К сожалению, и USB разъема для зарядки тоже.

Собственно и все, приходи кума любоваться.

Развлекаемся с электрофоретическими дисплеями - 31

Остававшееся отверстие из-под LCD — явление временное, по размерам оно очень хорошо подходит под красно-черный 2.9-дюймовый e-ink дисплей, который будет общаться с Home Assistant — на большом-то дисплее места уже нет.

Развлекаемся с электрофоретическими дисплеями - 32

Кроме самого драйвера, второй по сложности проблемой были картинки. Художник я еще тот, а натыренные в интернете картинки еще и преобразовать надо. Когда вконец замучился с готовыми программами для графики, плюнул и решил — лучше день потерять потом за пять минут долететь. И написал парочку полезных для меня питоновских сриптиков — один все векторные svg файлы в папке преобразует в монохромные BMP заданных размеров, а второй из всех получившихся картинок делает один исходный файл, который можно использовать в С коде. Насчет дня я немного соврал, понадобилось пару часов. Ну а кто пишет программы на питоне не раз в два года, несколько минут потратит.

svg2bmp.py
import os
import cairosvg
from PIL import Image

for file in os.listdir('.'):
    if os.path.isfile(file) and file.endswith(".svg"):
        name = file.split('.svg')[0]
        
        cairosvg.svg2png(url=name+'.svg',write_to=name+'.png', output_height=128, output_width=128)
        img = Image.open(name+'.png')    
            
        # Transparence replace with white  
        if (img.mode=='RGBA'):            
            new_img = Image.new("RGBA", img.size, "WHITE")
            new_img.paste(img, mask=img)
            img = new_img
                       
        img = img.convert('1') # change to black and white image               
             
        if os.path.exists(name+'.bmp'): 
            os.remove(name+'.bmp')  
               
        img.save(name+'.bmp')           
        os.remove(name+'.png')

bmp_folder2code.py
import os
import sys
from PIL import Image

if len(sys.argv) != 2:
    print("Usage: python3 %s [h file name]" % (sys.argv[0],))
    sys.exit(-1)
    
filename = os.path.splitext(sys.argv[1])[0]
f = open(filename + ".h", "w")
f.write("#ifndef " + filename.upper() + "_Hn")
f.write("#define " + filename.upper() + "_Hn")       
f.write("#include <stdint.h>n")
f.write("n")

for file in os.listdir('.'):
    if os.path.isfile(file) and file.endswith(".bmp"):
        name = file.split('.bmp')[0]
        
        print(file)
        img = Image.open(file)
        w, h = img.size          
        f.write("n// "+file+" "+str(w)+"x"+str(h)+"n") 
        f.write("const uint8_t " + name + "[] = {") 
        for y in range(h):
            f.write("n  ") 
            for x in range(0, w, 8):
                byte = 0
                for i in range(8):
                    byte = byte << 1
                    if img.getpixel((x+i,y))==0:
                        byte = byte | 0x01
                        
                f.write('0x%02x, ' % byte) 
        
        
        img.close()
        f.write("n")
        f.write("};n")
        
f.write("#endifn")        
f.close()
        

Дело сразу пошло намного быстрее, и правка кода из-за непонравившейся картинки стала секундным делом. Сильно только не критикуйте, я ни разу ни питоновский программист и использую его раз в пару лет, с удивлением обнаруживая, что уже все поменялось и в последней версии питона старый скрипт уже не работает.

Так, про мух я рассказал, давайте займемся котлетами. Хотя все это происходило в один и тот же промежуток времени — как это называется? Кажется, time-sharing and multi-tasking?

Как я уже написал, в это время я приобрел красно-черные электронные ценники. Они очень популярны у самодельщиков в Германии, время от времени в своей микроконтроллерной конференции собирается группа товарищей, покупает дешево партию из 200-500 ценников и потом весело их делят.

Развлекаемся с электрофоретическими дисплеями - 33

В партии 500 штук они обходятся в 1 евро. Мне делится было не с кем, поэтому я дико переплатил — за партию в 50 штук заплатил 120 евро, это если с доставкой. Но у продавца, видимо, была напряженка с упаковочным материалом, поэтому, чтобы ценники не болтались в коробке, он напихал их до упора — штук 60 пришло, кажется. И еще сверху болтались два вида ценников по две штуки с дисплеями поменьше.

Развлекаемся с электрофоретическими дисплеями - 34

За такие деньги, это очень неплохое приобретение — вы получаете неплохую коробочку для самоделок, две батарейки CR2540 (практически не разряженные, даже в худшем случае напряжение больше 3.1 вольта), красно-черный 2.9 дюймовый дисплей HINK-E029A17 с разрешением 296x128 пикселей (контроллер SSD1675) какой-то микроконтроллер с обозначением SEM9110 (о нем будет ниже), SPI флешка емкостью 1Мбайт.

Развлекаемся с электрофоретическими дисплеями - 35
Развлекаемся с электрофоретическими дисплеями - 36

На плате есть место для NXP NFC контроллера, но он не установлен. Но NFC антенна имеется. Кроме того, целых две антенны на 2.4ГГц — одна нарисована на плате, вторая установлена на торце коробки. Зачем две — даже не спрашивайте.

В начале народ подключал к этому дисплею свои платки, как правило с ESP32 или ESP8266. Готовые библиотеки для ардуино легко находятся, будут работать как есть или нет — вопрос. Пишут, что работают, но не без бубна. Я пытался кое из каких библиотек заимствовать код инициализации дисплея — не заработало.

Потом товарищ Дмитрий открыл ящик Пандоры — детали тут и тут.
Перевод одной из его статей я где-то видел на Хабре, и там в комментариях сам автор тоже отметился.
Ему попался аналогичный ценник, а дальше ему сопутствовала удача. Удача, конечно, тут вещь нужная, но лотерейный билетик купить для начала все равно надо.
Для начала ему удалось найти настоящее имя микроконтроллера — под личиной SEM9110 скрывался блин уголовник ZBS243. Это мало что дает, документации на процессор нигде нет. Но опять свезло — он нашел картинку с надписями на корейском языке. Оказалось, что микропроцессор на древнем ядре 8051 (кому-то древнем, а для меня воспоминания о молодости) с 64 КБ флэш-памяти, 8 КБ XRAM, 256 байт iRAM, тактовой частотой 16 МГц и блоком Zigbee на борту.

Развлекаемся с электрофоретическими дисплеями - 37

Вам хватит такой картинки, чтобы на 90% взломать микроконтроллер? Ему хватило, но свезло еще два раза: сначала ему удалось приобрести программатор для этого процессора и память программ оказалась не заблокирована.
Историю вскрытия можете почитать сами, ссылки я дал выше.

Кроме всего прочего, дисплей, который отображает только черный и красный цвета, он заставил отображать еще несколько градаций серого. Правда, ценой времени — если в нормальном режиме такой дисплей обновляет изображение 15 секунд, то дисплею с серыми цветами нужна уже практически минута.

Все программное обеспечение он выложил на своем сайте — качайте, пользуйтесь. Кроме того, он сделал шлюз, и изображение на такую этикетку можно закачивать через Zigbee, но нужен модуль с CC2531. Протокол получился несовместимый с Home Assistant, но еще не вечер. Не спешите его покупать, у истории есть продолжение. Попутно он проанализировал протокол программатора — теперь такой программатор каждый может сделать задешево.
А схему ценника я нарисовал, опустивши высокочастотную часть, если будете сами писать программы — пригодится.

Развлекаемся с электрофоретическими дисплеями - 38

А теперь идем к немцам. Здесь вы найдете информацию, как сделать самому программатор из ESP32 или ESP8266. Программное обеспечение работает не лучшим образом, руки чесались все переписать. Но лень победила, как есть — тоже можно пользоваться.

Программатор, в частности, управляет питанием этикетки — на печатной плате вы можете видеть, что там туча конденсаторов большой емкости, для подключения нужно использовать достаточно мощный транзистор и ограничить ток заряда конденсаторов. Впрочем, схему я нарисовал — пользуйтесь. Не нравятся транзисторы — поставьте какой-нибудь LDO со входом разрешения.

Развлекаемся с электрофоретическими дисплеями - 39

Дальше — опять идем к немцам. Я находил пару вариантов шлюза wifi — zigbee. Один из ценников можно использовать для доступа к остальным (я как раз один дисплей испоганил, остальное то целое), его надо только подключить к ESP32

Где-то было и подключение этого шлюза к Home Assistant.

Я этими шлюзовыми делами не занимался — в таком виде, как есть, мне оно не надь. Потом буду сам переделывать, если энтузазизьма хватит.

Берем первый ценник трясущимися от нетерпения руками, разбираем, пробуем подключить к внешнему источнику питания. Как известно, спешка нужна при ловле блох — путаю полярность и устройство выпускает волшебный дым. Теперь оно годится только для того, чтобы потренироваться в отклейке платы от дисплея. Тренировка прошла неудачно — несмотря на то, что плата предварительно прогревалась, дисплей треснул. Зато теперь видно — они склеены всего-лишь навсего небольшим куском двухсторонней клейкой ленты. Но дюже клейкая — где они такую берут?

Развлекаемся с электрофоретическими дисплеями - 40

Ко второму ценнику подпаиваем провода к контактным площадкам — все замечательно, но каждая вторая стока исчезла.

Развлекаемся с электрофоретическими дисплеями - 41

Все, хорош их ломать, хотя их и много, но все равно жалко. Делаем держалку для пого-контактов. Можно напечатать на принтере или вырезать из оргстекла остро заточенным лазером. Теперь можно жить не опасаясь за целостность ценников.

Развлекаемся с электрофоретическими дисплеями - 42

Мне хотелось бы эти ценники использовать как простой черно-белый дисплей, безо всяких серостей, но чтобы обновление было быстрое. Видел на youtube видео, где частичное обновление у такого контроллера дисплея происходит за долю секунды. Даже ссылка на код была — но не заработало. Пока получилось сделать обновление за три секунды. Но работает и черный, и красный. Как так вышло — сам не понимаю. LUT, которые нужно записать в контроллер дисплея — это что-то близкое к магии, учитывая отсутствие нормальной документации. Теоретически, эти таблицы с временными диаграммами и необходимыми напряжениями, хранятся в OTP того же контроллера, и контроллер может их использовать, причем выбирает одну из множества таблиц зависимости от температуры. Датчик температуры в контроллере имеется, хотя можно подключить и внешний.

Смотрите, что у меня вышло:

Развлекаемся с электрофоретическими дисплеями - 43

Кодом загромождать статью не буду, на mysku будет укороченный вариант статьи только про ценники, там будет много кода.

Думал, что никогда уже не буду писать на Хабре — какой от него прок пенсионеру? Свои статьи я публикую на mysku.club. Скажете, это там совсем не по теме? Может быть и скорее всего так. Тем не менее, и на мою пенсию по инвалидности, кроме меня, кормятся супруга и две прелестных котейки. На развлечения лишних средств особо не остается. И все мои самоделки за последние годы профинансированы Mysku, в том числе и те, о которых речь идет тут. Собственно, эта статья — сборная солянка их нескольких, опубликованных или готовящихся к публикации статей там.
А тут вдруг велосипеды за мальчиков стали давать — отчего не попробовать?

Автор: Владимир

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js