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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

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

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

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

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

  • http://essentialscrap.com/eink/

  • http://spritesmods.com/?art=einkdisplay

  • https://alexf.tech/blog/driving-ed060sc4lf-e-ink-screens/

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

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

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

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. И куча прочей никому не интересной ерунды.

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

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

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

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

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

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


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

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


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

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

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

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

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

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

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

Код
// ****************************************************
// 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
}

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

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 строк, каждая начинается так:

void start_row()
{
  CPH_IDLE
}

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

void stop_row()
{
  CPH_ACTIVE
  CKV_IDLE  
  Hclock();
  CKV_ACTIVE  
  OE_IDLE
  delay_us(2);  
  OE_ACTIVE  
  LE_ACTIVE
  LE_IDLE  
}

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

void end_frame()
{
GMODE_IDLE
delay_us(1);
Vclock();
}

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

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

код
<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 использовать.

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

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

Тупой драйвер из 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 ("nop\n\t");
    asm volatile ("nop\n\t");    
    CL_ACTIVE
    asm volatile ("nop\n\t");
    asm volatile ("nop\n\t");    
    CL_IDLE    
    CKV_ACTIVE  
    OE_IDLE
    delay_us(2);  
    OE_ACTIVE  
    LE_ACTIVE
    asm volatile ("nop\n\t");
    asm volatile ("nop\n\t");
    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, все надежно крепим двухсторонней лентой, вырезаем отверстия в задней стенке и закрываем.

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

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


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


Кроме самого драйвера, второй по сложности проблемой были картинки. Художник я еще тот, а натыренные в интернете картинки еще и преобразовать надо. Когда вконец замучился с готовыми программами для графики, плюнул и решил — лучше день потерять потом за пять минут долететь. И написал парочку полезных для меня питоновских сриптиков — один все векторные 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() + "_H\n")
f.write("#define " + filename.upper() + "_H\n")       
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("#endif\n")        
f.close()
        

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Источник: https://habr.com/ru/articles/749732/


Интересные статьи

Интересные статьи

Ноутбуки есть вероятно, у большинства читателей Хабра. Кто-то привык работать с ноутбуком без дополнительных экранов, кто-то подключает 1, 2 и больше дисплеев. Ну а кто-то покупает устройство с до...
Казалось бы, в 2023 году мобильный рынок уже давно заполонили одинаковые смартфоны, где меняются только технологии изготовления дисплеев, разрешение, железо, и иногда чуть-чуть корпус, но в целом ...
Пока готова только левая половинка сплита. Но оно работает! Лет 12-13 назад Лебедев представил клавиатуру Optimus Maximus, у которой в каждую клавишу был встроен миниатюрный экранчик. Обзор этой ...
Я не особо слежу за развитием устройств со складными/гибкими экранами. Но сейчас появилось на удивление много смартфонов с такими дисплеями. Совсем недавно это были, скорее, концепты и прототипы, а...
Сразу предупреждаю, что не собираюсь разводить холивары насчет преимуществ AVR-ассемблера перед С/Arduino, или даже перед BASCOM-AVR и MikroPascal for AVR — каждый инструмент уместен в св...