I2c + SH1106 с использованием только wire.h

Ну так смотришь описание, смотришь саму библиотеку на предмет чего использует, какие проблемы? Не нравится - “быстренько” пишешь своё, со своими граблями.

Так, подключил, работает, команда в строке 66 действительно не нужна, по крайней мере без нее работает.

Проблема: обновление дисплея занимает 120мс, что на мой взгляд слишком медленно. И судя по всему с библиотекой было бы так же. Есть решение?

Забыл, ВАЖНО!!! если у вас точно такие же модули - то на VCC только 3.3 вольт, ни в коем случае не 5! Быстро перегревается, потом артефачит, дальше, вероятно - смерть. У меня два экрана, потому был готов поэкспериментировать (не спалил если что))) При этом с i2c ничего не делал, вероятно, 5В логику поддерживает плата. Вот жалко было китайцу стаб в нее впаять, соотв. у кого на арду нет 3,3 приходится еще и DC-DC bec использовать

Решение, естественно, есть. Куда ж без него?
Например, заменить на дисплей с интерфейсом SPI.
Кстати, на какой скорости у Вас происходит обмен с дисплеем?
Ну и заодно уж: в чем именно состоит проблема 120 мс?

я, к сожалению, не знаю, как это определить.

  Wire.write(0xD5); // OLED_CLOCKDIV по всей видимости, делитель частоты внутренних часов, хз зачем
  Wire.write(0x80); //

как мне кажется, здесь частота задается для дисплея. В wire никаких команд на скорость не использовал. Вообще я честно не знаю настроек этого протокола, использовал только для pca9685, там пакеты гораздо меньше размером.

У меня на прохе микро заняты 2 вывода сериал для радиосвязи, 4 аналога для двух джойстиков, 3 цифры для кнопок, еще 2 для энкодера… и вот 2 для i2c. Я не проверял, но на spi может уже не хватить + не видел таких модулей экранов (с кнопками и энодером) на spi, а макетку заказывать точно не буду.

Во-первых слишком много времени занимает обработка, относительно остальных действий пульта, из-за чего обработать нажатия кнопок можно только прерываниями (иначе часто будут пропуски), но все выходы с поддержкой прерываний (по дефолту) уже заняты. Во-вторых это уменьшает количество отправок пакетов на робота в секунду до 7шт. Можно, конечно, обойти - отрисовывать дисплей только если были изменения на экране (сейчас это и делаю). Скорее всего сделаю в классе для радиомодулей хранение хеша последнего успешно принятого пакета, и буду перерисовывать только при его изменении. Но все равно хотелось бы ускорить вывод, как-то слишком медленно кажется

И еще момент. У меня при калибровке выбранного сервопривода, его вал управляется напрямую энкодером в пределах мин/макс длины импульса ШИМ. Прям writeMicroseconds. И чтобы по 1 единичке не крутить от 500 до 2500 я беру 3-ю степень значения энкодера, которое юзер успел накрутить между сеансами связи. Т.е. крутит потихой - так и будет +/- 1. Резко крутнул - значение сразу на 500 изменилось. При этом предполагалось, что накрученное значение выводится на экранчик с другими нужными для калибровки данными (какие кнопки нажать, чтобы применить текущую позицию как мин/макс). Но это до подключения экрана, по сериал в комп выводил, и работало как по маслу. А теперь, получается, что у меня дельта времени неравномерная будет, и как минимум придется после отрисовки энкодер обнулять, чтобы за время отрисовки пользователь не успел крутнуть на +100500 энкодер, или вообще разряд переполнить. Но все равно будет ущербный интерфейс, рывками, т.к. значение же каждый раз будет возвращаться от робота новое, т.е. перерисовка экрана потребуется

Я же выкладывал код, где дисплей работает максимально быстро для 16 МГц камней и работает на любых выводах !!!

Это можно сделать, минимум, двумя способами: теоретически и экспериментально.
Т.е. посмотреть в коде или посмотреть осциллографом сигнал SCL.

Вот яркий пример того, что публиковать маленький огрызок кода абсолютно бессмысленно - по нему ничего сказать нельзя.
Хотя, если верить строке комментария, к частоте I2C это никакого отношения не имеет.
Но для того, чтобы дальше обсуждать скорость передачи, нужно выяснить, какой контроллер Вы используете, потому как скорость передачи I2C у разных контроллеров выставляется по-разному.
Для 32-разрядных контроллеров существует специальная команда Wire.setClock(400000);.
А для контроллеров AVR обычно нужно напрямую записывать значение в регистр TWBR. Для скорости передачи 400к значение должно быть 12, но можете поэкспериментировать и с другими значениями. Для AVR можно попытаться работать на скорости до 880к, но устойчивая работа при этом, естественно, не гарантируется.

Ага, значит, с контроллером определились - AVR.

Можете привести конкретные цифры, сколько должно быть, а также обоснование этих цифр?

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

Если подойти по уму, то возможностей для ускорения работы здесь выше крыши.
Я, например, много писал для MIDI устройств. Что подразумевает живое исполнение. А при живом исполнении человек не чувствует задержки, если она не превышает 2 мс. Вот из этой цифры я исходил. И каждый раз (в двух разных проектах - на разных МК и разных дисплеях) этого удавалось добиться. Основной метод - отправлять информацию на экран не одним большим куском, а массой маленьких фрагментов.

1 лайк

Просто весь код был чуть выше # 9. Ни в коем случае не осуждаю за то, что не усмотрели.

Строке комментария верить не стоит, сам писал (но там точно что-то про делитель часов чипа дисплея).

да, про контроллер не указал нигде ранее - мой косяк

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

Это понимаю, но потребуется анализ на стороне пульта, иначе сломается другая идеология - пульт не хранит у меня состав меню робота, а получает от робота по воздуху 5 строк для вывода на экран (идея в том, чтобы никогда не заливать в пульт пункты меню и не следить за их соответствием в пульте и в роботе)

Мог неправильно понять. Это для варианта, когда скорость обновления самого экрана некритична? т.е. я бы мог, например отрисовывать за один loop по 16 бит, храня в глобальной переменной индекс текущего пакета. Вообще любопытная идея, наверняка на глаз не сильно будет заметно посекторное обновления. Сейчас же не заметно)) И при этом я получу почти фиксированную дельту времени, что тоже плюс

Это означает, что я могу поднять частоту i2c, или нет?

у вас, если правильно понял, другой чип дисплея, а значит и другой протокол. Т.е. мне нужно очень внимательно изучить ваш код, используя знания, которых у меня на данный момент нет))) но спасибо, я, на самом деле, в закладочки те странички прикопал

библиотека от Adafruit позволяет устанавливать скорость обмена при инициализации, держит ли дисплей такую скорость, вопрос?

Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET,1000000, 100000);

Я не понимаю значения в строках 29 и 34… я думал это указания колонок “с” и “по” которые дисплей будет ожидать данные, и что 2 и 16 это максимальный охват для дисплея 128 шириной, но как-то числа не бьются ни впрямую, ни кратными… Причем уменьшил b до 6 и дисплей зарисовал диагоналкой 3/4 экрана, без смещений, то есть как минимум недостача данных ошибку у него не вызывает… Но скорее всего лишь потому, что после заполнения страницы идет команда на смену страницы и он этот факт “проглатывает”

 // полностью обновить дисплей
void updatesc() {

  // перевод в режим приема команд
  Wire.beginTransmission(_address);
  Wire.write(0x80); // OLED_ONE_COMMAND_MODE
  Wire.write(0x00); // OLED_COMMAND_MODE
  Wire.endTransmission();
  //delayMicroseconds(2);
  // перевод в режим получения данных
  Wire.beginTransmission(_address);
  Wire.write(0x80); // OLED_ONE_COMMAND_MODE
  Wire.write(0x40); // OLED_DATA_MODE
  Wire.endTransmission();
  // delayMicroseconds(2);
  
  // 8 горизонтальных страниц памяти экрана
  for (uint8_t p = 0; p < 8; p++) {
    
    // выбор страницы
    Wire.beginTransmission(_address);
    Wire.write(0x80); // OLED_ONE_COMMAND_MODE
    Wire.write(0xB0 + p); // set page address
    Wire.endTransmission();
    // delayMicroseconds(2);
    // наверное, адрес относительно страницы, с которого начнем заполнять ОЗУ
    Wire.beginTransmission(_address);
    Wire.write(0x80); // OLED_ONE_COMMAND_MODE
    Wire.write(2); // set lower column address
    Wire.endTransmission();
    // delayMicroseconds(2);
    // наверное, адрес относительно страницы, до которого будем заполнять ОЗУ
    Wire.beginTransmission(_address);
    Wire.write(0x80); // OLED_ONE_COMMAND_MODE
    Wire.write(16); // set higher column address
    Wire.endTransmission();
    // delayMicroseconds(2);

    // отправляем 8 посылок по 16 байт из-за ограничений Wire.h
    
    // 8 блоков
    for (uint8_t b = 0; b < 8; b++) {
    
      // режим приема данных
      Wire.beginTransmission(_address);
      Wire.write(0x40); // OLED_DATA_MODE
      
      // 16 колонок в блоке
      for (uint8_t c = 0; c < 16; c++) Wire.write(B00000001 << c / 2); // тут нужно подставить соответствующий байт-столбец, координаты [b * 8 + c, p * 8]
      
      Wire.endTransmission();
      // delayMicroseconds(2);
      
    }
    
  }
  
}

Как именно нужный сектор выбрать не пойму, в даташите https://www.elecrow.com/download/SH1106%20datasheet.pdf картинка с адресацией подрывает мне мозг:


Какое еще D0?.. откуда они взяли 83, если дисплей 128, а чип до 132 в ширину?

Ускорило вывод до 44мс примерно, что уже прям неплохо. С адресацией внутри страниц все еще хотел бы разобраться

а если до мегагерца разогнать?

было бы 8мс, но увы экран 1мгц уже не воспринимает…

дурень я… 83H это у них просто шестнадцатеричная запись числа (по умолчанию чип может 0-131 точек в ширину, т.е. 132), а D это инвертирование по горизонтали скорее всего

Из интересного, вовсе необязательно в начале перерисовки экрана посылать команды в режиме Wire.write(0x80); // OLED_ONE_COMMAND_MODE, можно и Wire.write(0x00); // OLED_COMMAND_MODE режим отправки команд нон-стопом

А вот править сегменты внутри 1 страницы как будто бы нельзя, lower column address и higher column address это вроде бы адреса старта страницы (старшие и младшие биты как бы), а не границы страницы.
В целом, 8 страниц сейчас рисуются за 41мс, т.е. если разбить на вывод по 1 странице за loop() то отнимет 5-6 мс от цикла, что уже прям ок ок

экран универсален (i2c, spi) видимо i2c не позволяет, там выше @Komandir предлагал решение

1 лайк

Через аппаратный i2c быстрее 880000 не запустить. Если выставить 1000000, то в регистр запишется число, которое остановит сам i2c на дуне. Дисплей может и 5000000.

1 лайк

Вот и я про то, что делитель дисплея никакого отношения к скорости передачи МК иметь не может.

Вы передаете в дисплей 1024 байта только “чистой” информации, не считая служебной. То есть “как до подключения дисплея” быть не может никак.

Давайте отделять мух от котлет.
У Вас графический экран на 8192 пикселя. Именно эти пиксели Вы передаете на дисплей по протоколу I2C с некоторой скоростью.
И еще у Вас есть 5 строк текста, которые Вы ни на дисплей, ни по I2C не передаете.
Поэтом их получение и обработку, возможно, надо учитывать в общем балансе времени, но конкретно к передаче информации на дисплей они никакого отношения не имеют.

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

Чтобы ответить на этот вопрос, минимум надо знать, на какой частоте Вы работаете в настоящее время.

1 лайк

Вы всерьез полагаете, что 83 и 0х83 - это одно и то же число?

Все ясно. Для 400к это вполне нормальный результат.
Значит, до этого у Вас было 100к.

В данном случае не может Ардуина. Максимальная скорость для нее примерно 880к при TWBR = 2;. Если попытаться записать в TWBR число 0, то не получается сигнала, удовлетворяющего I2C. Собственно, на осциллографе вообще происходит что-то непонятное.
К софтверной реализации от Komandir это не относится. Но там свои сложности.

Не могу подтвердить эту информацию.
Возможно, какие-то экземпляры (партии…) дисплеев и могут, но лично мне удавалось добиться устойчивой работы только не более чем на 2000000.

1 лайк

Я не знаю i2c и его настроек, но прекрасно понимаю, что такое основание системы исчисления, просто не понял их запись 83H, рядом вон с буквой D обозначения используются в тоже не совсем понятной мне форме (где-то команда, а где-то разряд байта кажется)

Но тем не менее не понял, что именно за адресА передаются после Адреса страницы… Но могу это пережить, т.к. организовал вывод по 1й странице за итерацию loop() и мне не понравилось. Сейчас запихал обновление дисплея целиком за 40мс, сразу после получения данных с робота, по ощущениям все успевается.

косвенно имеют, т.к. строки текста целиком не попадают в страницу памяти, побитово ИЛИкаю их, предварительно обрезав << >>, сейчас как раз решу, сколько именно строчек буду выводить и оптимизирую получение индексов в PROGMEM массиве шрифта. Да, это значит, что у меня не используется буфер - сразу из прогмема в i2c, с небольшим предварительным преобразованием. Но это уже дело вкуса, как обозвать. Для меня вывод полученных строк на экран, как функция, начинается сразу после того, как эти строки текста получены от приемника…

Уже экспериментальным путем установил, что можно до 400000

Когда будет двигаться, он фиксированный ответ будет отправлять в пульт, состоящий из 1 строки (остальные уже в пульте будут принимать по умолчанию значение пустой строки)

Обычно связь как делают, я так понимаю: в пульте пишут меню робота и наборы кодов команд, которые отправляются при их выборе. А в роботе прописывают обработчики этих кодов. При этом мне не совсем понятно, как пульт узнает, что робот, например, не готов принимать эти команды))) В любом случае протокол жесткий, и требует соответствие версий в пульте и роботе.

В моей реализации вся идеология в том, что робот будет в соответствии со своим текущим состоянием отдавать видимую область меню в пульт в виде набора строк, именованных в пакете “L0”, “L1” … “L5” (скорее всего будет в итоге шесть строчек, каждая не длиннее 23 символов), а пульт будет выводить эти строки на экран (либо сообщение “Connection lost…”), а в ответ передавать только значения органов управления “RX”, “RY”, “LX”, “LY”, “A”, “B”, “C”, “E”. Робот сам разберется, что с этим делать.

пульт 1 раз прошил - и он универсален, а разные роботы выдают свои опции - это как пример. И заливать прошу только в робота тоже удобнее при добавлении новых функций.

Вообще, это все не “будет”, а уже работает, просто надо в классы обрамить элегантно, чуть переписать и оптимизировать.

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

Немного приврал … но 2.5 я выжимал - https://forum.arduino.ru/t/provel-test-oled-displej-128x64-na-chipe-ssd1306-podklyuchen-cherez-i2c-soplyami-bez-pajki-i-rabotaet-s-chastotoj-271-fps

Надо будет на rp2040 ещё попробовать с её DMA и PIO …