Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру 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, и, как опция, модем для подключения к сотовой сети.
Что там можно найти:
Основной дисплей ED060SC4(LF), 6 дюймов, 600x800
Дополнительный дисплей CLAA035JA01CW, 480x144. Никакой документации на него я не нашел, так что вещь бесполезная
Процессор S3C6410XH-53
Контроллер основного дисплея S1D13521
SD карточка в роли флеш-памяти
SDRAM Hynix — H5MS1G62MFP-J3M
Сотовый модем Sierra Wireless MC8777V
WM8350 — Wolfson AudioPlusTM Stereo CODEC with Power Management
WiFi PW621
И куча прочей никому не интересной ерунды.
Старые 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, в том числе и те, о которых речь идет тут. Собственно, эта статья — сборная солянка их нескольких, опубликованных или готовящихся к публикации статей там.
А тут вдруг велосипеды за мальчиков стали давать — отчего не попробовать?