Урок 78. Продолжение предыдущего урока. Доработка программы визуализации параметров в виде стрелочных приборов на круглом дисплее RP2040-LCD-1.28. Оптимизация скорости вычислений, использование двух ядер, прерывания по таймеру, подключение энкодера.

Программирование RP2040-LCD-1.28

Продолжение предыдущего урока о визуализации параметров на круглом дисплее RP2040-LCD-1.28 в виде стрелочных приборов. В нем оптимизируем скорость вычислений, научимся применять прерывание по таймеру и использовать 2 ядра на RP2040, подключим к устройству энкодер, создадим проект прибора с двумя стрелками.
В уроке затронуты самые разные темы. Можно рассматривать его, как обучение  программированию RP2040 в среде Arduino IDE.

Предыдущий урок     Список уроков     Следующий урок

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

  • Картинка выводится на экран дисплея недостаточно быстро.
  • Кроме вывода на экран изображения стрелочного прибора необходимо выполнять много других задач, требующих значительных вычислительных ресурсов. Например, измерение и цифровую фильтрация аналоговых сигналов. При наличии в микроконтроллере RP2040 двух ядер логично на одном реализовать визуализацию, а второе использовать для основных вычислений.
  • Для установки значений параметров практически в любом устройстве необходим инкрементальный энкодер. В предыдущем уроке о программной поддержке энкодера я не рассказывал.
  • В любой достаточно сложной программе не обойтись без использования прерывания по таймеру. Хотя бы для сканирования кнопок и контактов энкодера. Организация прерывания по таймеру в микроконтроллере RP2040 - еще один вопрос, не затронутый в предыдущем уроке.

Решению этих проблем посвящен объемный текущий урок. Как сопутствующую задачу рассматриваю обучение  программированию RP2040 в среде Arduino IDE.

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

Давайте последовательно решать эти проблемы.

 

Оптимизация скорости вычислений при визуализации параметров в виде стрелочных приборов.

Сначала надо определиться какие операции занимают сколько времени.

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

Добавил метки времени между основными операциями визуализации. Вот скетч с измерением времени на выполнение отдельных операций.

 

Зарегистрируйтесь и оплатитеВсего 60 руб. в месяц за доступ ко всем ресурсам сайта!
 

Получились следующие результаты.

Измерение времени

Операция Время на выполнение
Загрузка фоновой картинки 23 мс
Вывод значения напряжения в числовом виде 2 мс
Вывод картинки рыбы с поворотом 290 мс
Перегрузка окончательной картинки в дисплей 216 мс
Общее время формирования изображения на дисплее 531 мс

Больше 0,5 секунд на формирование кадра изображения! Давайте уменьшать.

Загрузка фоновой картинки происходит в следующей строке

for( int i=0; i < Imagesize; i++ ) *((unsigned char *)BlackImage + i) = scale[i]; // фоновая картинка

Данные по байтам считываются из массива с картинкой scale в область памяти BlackImage.

Микроконтроллер у нас 32х разрядный. Если перегружать 16ти разрядными словами, должно получиться быстрее.

for( int i=0; i < 240 * 240; i++ ) *(BlackImage + i) = * ((uint16_t *)scale + i); // фоновая картинка

Так и есть.

Измерение времени выполнения операций

Время на загрузку фоновой картинки стало 15 мс, было 23.

Курочка по зернышку клюет, как говорила одна моя знакомая, взяв 6 взяток при верной восьмерной в преферансе.

Значения напряжения в числовом виде выводится за 2 мс. Оптимизация этой операции не особенно скажется на окончательном результате. Оставим ее в покое.

Оптимизация времени выполнения поворота картинки.

При текущей реализации эта операция самая длительная 291 мс. А картинок может быть несколько. В нашем проекте 2 стрелки.

Основное время операции уходит на вычисление координат для 230400  точек (480*480).

Вычисления производятся в формате с плавающей запятой.

x= ((float)(i - 240) * cosAlfa + (float)(j - 240) * sinAlfa  + 240.5) / 2;
y=  (((float)(i - 240) * sinAlfa * -1) + (float)(j - 240) * cosAlfa + 240.5) / 2;

Операции с плавающей запятой – это пожиратели вычислительных ресурсов процессоров.

У нас RP2040 – очень быстрый 32х разрядный микроконтроллер с тактовой частотой 133 мГц. Тем не менее, на вычисление координат одной точки уходит 291000 / 230400 = 12,6 мкс.

Простая сумма двух чисел, которая при целочисленном формате выполняется за одну машинную команду, в формате с плавающей запятой требует:

  • денормализацию мантиссы (сдвиг 24х разрядных чисел);
  • сумму мантисс;
  • нормализацию мантисс (сдвиг 24х разрядных чисел);
  • коррекцию порядка.

По просьбе одного любопытного молодого человека я написал статью на тему: Переход от вычислений типа float к int (целочисленным)

Прочитав ее, он ответил: “Спасибо информативно, но пока пытаюсь понять. Нужно немного времени. Я, честно говоря, не знаком с таким типом вычислений. Перечитал несколько раз.”

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

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

Кто хочет разобраться в деталях, прочитайте статью. А я коротко поясню, как поступил в данной задаче.

Итак, при вычислении тригонометрических функций для матрицы поворота переведем их в формат с фиксированной запятой. Эти вычисления производятся 1 раз за кадр изображения, поэтому время выполнения их некритично.

int16_t cosAlfa= (int16_t)(cos(PI * 2 * (1.25 - turn)) * 16384.); // << 14
int16_t sinAlfa= (int16_t)(sin(PI * 2 * (1.25 - turn)) * 16384.); // << 14

Так как их значения тригонометрических функций лежат в диапазоне -1 … 1, выбираем следующий формат.

15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
знак 1 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14

В формате с плавающей запятой мы умножили число на 214 (16384), тем самым 14 дробных разряда мантиссы перевели в целочисленный формат.

Вычисление координат производим в формате с фиксированной запятой, для процессора, который не знает, где запятая, это целочисленное вычисление.

uint32_t x= ((i - 240) * cosAlfa + (j - 240) * sinAlfa  + 3940352 ) >> 15;
uint32_t y= (( (j - 240) * cosAlfa - (i - 240) * sinAlfa ) + 3940352 ) >> 15;

Для того, чтобы получить целые значения координат, в конце вычислений мы сдвинули результат на 14 разрядов вправо и еще на один разряд, чтобы реализовать деление на 2, заданное в исходной формуле.

Полный вариант такого принципа вычислений можете посмотреть по ссылке.

 

Зарегистрируйтесь и оплатитеВсего 60 руб. в месяц за доступ ко всем ресурсам сайта!
 

А вот результат.

Lesson78_3

Время на вывод картинки рыбы с поворотом стало 54 мс, а было 291 мс. Т.е. время вычисления уменьшилось более чем в 5 раз.

Остается перегрузка окончательной картинки в дисплей. Реализуется стандартной функцией.

LCD_1IN28_Display(BlackImage);

Вроде, делать нечего, надо смириться. Но, время приличное 215 мс.

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

В общем, я залез в функции стандартной библиотеки и, как следствие, получил следующий результат.

Измерение времени выполнения операций

Время на перегрузку картинки в дисплей стало 46 мс, было 215 мс. Сократилось почти в 5 раз.

Общее время формирования изображения на дисплее стало 117 мс. Перед оптимизацией было 531 мс.

 

Зарегистрируйтесь и оплатитеВсего 60 руб. в месяц за доступ ко всем ресурсам сайта!
 

Рыба стала крутиться значительно быстрее.

 

Использование двух ядер на RP2040.

В среде Arduino IDE это делается просто.

void setup() {
  // инициализация переменных, установка режимов для первого ядра
}

void setup1() {
  // инициализация переменных, установка режимов для второго ядра
}

void loop() {
  // код, исполняемый на первом ядре
}

void loop1() {
  // код, исполняемый на втором ядре
}

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

Для передачи данных между процессами на разных ядрах я создал структуры.

struct displayParameters {
  float turnKoeff1; // коэффициент поворота картинки 1
  float turnKoeff2; // коэффициент поворота картинки 2
  float value;      // значение на экране
  uint8_t mode;   // режим отображения
};

struct displayParameters preparationData;       // подготовленные данные
volatile struct displayParameters transferData; // передача данных

В объявлении структуры задал 2 коэффициента поворота картинок (их может быть две), значение, которое надо выводить внизу экрана и переменную mode – режим.

Последняя переменная может использоваться для изменения алгоритма вывода данных на экран. Но ее обязательная функция – синхронизация процессов вычисления (ядро 1) и визуализации (ядро 2).

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

Алгоритм связи между программами разных ядер такой.

  • Программа вычисления данных (ядро 1) вычисляет необходимые для визуализации данные и формирует их в структуре preparationData.
  • Далее она дожидается, пока переменная mode структуры transferData станет равной 0. Это означает, что программа второго ядра считала все данные структуры. И, возможно, работает с ними, возможно, ждет новых данных от первого ядра.
  • Программа вычисления данных (ядро 1) перегружает данные из структуры preparationData  в transferData. После этого она может заниматься новыми вычислениями и формировать данные в структуре preparationData.
  • На втором ядре в цикле опрашивается переменная transferData.mode  и, при ненулевом значении (есть новые данные), обрабатывает их и выводит на экран.
  • Затем программа визуализации (ядро 2) сбрасывает переменную transferData.mode, говоря первому ядру, что можно загружать новые данные.

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

У меня несколько упрощенный вариант. Вычисления синхронизированы с визуализацией.

void loop() {
  if( transferData.mode == 0 ) {
    // вычисление данных
    preparationData.turnKoeff1 = MIN_TURN +  (voltage / 6.) * (MAX_TURN - MIN_TURN);
    preparationData.value= voltage;
    preparationData.mode= 1;

    transferData.turnKoeff1 = preparationData.turnKoeff1;
    transferData.value = preparationData.value;
    transferData.mode = preparationData.mode;
    . . . . .
  }
}

void loop1() {
  if( transferData.mode != 0 ) {
    // визуализация
    transferData.mode=0;
}

Полная реализация программы с использованием двух ядер есть здесь.

 

Зарегистрируйтесь и оплатитеВсего 60 руб. в месяц за доступ ко всем ресурсам сайта!
 

Прерывание по таймеру в RP2040.

Я не знаток программирования RP2040, этот микроконтроллер знаю плохо.

Начал искать в интернете примеры реализации прерывания по таймеру RP2040 в среде Arduino IDE. Оказалось это не так просто. Для Ардуино вариантов работы с таймером RP2040 не много.

Приемлемым посчитал способ с использованием программного интерфейса приложения низкого уровня (Low-level Timer Hardware API) для этого микроконтроллера. Arduino IDE поддерживает его функции.

Таймер микроконтроллера RP2040 представляет собой 64х разрядный счетчик, который считает импульсы периодом 1 мкс (частота 1 мГц). К нему через цифровые компараторы подключены 4 32х разрядных регистра (Alarms - регистры тревоги). При совпадении значения счетчика и любого регистра тревоги вырабатывается соответствующее прерывание.

В официальной документации для RP2040 есть пример, в котором по каждому прерыванию alarm-регистра считывается текущее значение счетчика, к нему прибавляется время периода прерывания и новое значение заносится в регистр тревоги.

Попробовал, запустил с периодом 500 мкс. В прерывании устанавливаю флаг, в основном цикле считаю. Когда набегает секунда (2000 тиков) вывожу в последовательный порт звездочку.

if( flg == true ) {
  flg=false;
  timeCounter++;
  if( timeCounter >= 2000 ) {
    timeCounter=0;
    Serial.println("*");
  }
}

Неточный получился таймер.  Время заметно отстает.

Результат работы таймера

Это и понятно. Отрезок времени с начала прерывания до инициализации нового прерывания не учитывается. А еще могут вмешиваться другие прерывания.

Если хочешь, чтобы что-то было сделано хорошо, сделай это сам.

Изменил алгоритм так, что новое время, которое заносится в регистр тревоги, вычисляется не из текущего значения счетчика, а из предыдущего значения регистра тревоги. Прерывание по таймеру заработало идеально.

Результат работы таймера

Разница между временем соседних секунд - это не точная работа Serial. Главное, что ошибка времени не накапливается, как в предыдущем примере.

Вот как выглядит мое решение задачи. Объявление переменных я не указал.

#include <hardware/timer.h>
#include <hardware/irq.h>

static void alarm_irq(void) {
  hw_clear_bits(&timer_hw->intr, 1u << ALARM_NUM);
  currentAlarm += TIMER_INTERRUPT_PERIOD;
  timer_hw->alarm[ALARM_NUM] = currentAlarm;
  flg=true;
}

void setup() {
  hw_set_bits(&timer_hw->inte, 1u << ALARM_NUM);
  irq_set_exclusive_handler(ALARM_IRQ, alarm_irq);
  irq_set_enabled(ALARM_IRQ, true);
  currentAlarm= (uint32_t)(timer_hw->timerawl +   TIMER_INTERRUPT_PERIOD);
  timer_hw->alarm[ALARM_NUM] = currentAlarm;
}

Полный скетч с прерыванием по таймеру RP2040 можно загрузить по ссылке.

 

Зарегистрируйтесь и оплатитеВсего 60 руб. в месяц за доступ ко всем ресурсам сайта!
 

Использование инкрементального энкодера с дисплеем RP2040-LCD-1.28.

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

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

Я использовал свою библиотеку Encod_er.h из урока 55.

Для проверки. Подключил библиотеку

#include <Encod_er.h>

Создал экземпляр класса. Для физического подключения энкодера использовал выводы 0 и 1.

Encod_er encoder( 0, 1, 4);

В обработчик прерывания по таймеру включил метод сканирования состояния энкодера.

static void alarm_irq(void) {
  . . . . .
  encoder.scanState();
}

После этого стали доступны свойства и методы, определяющие состояние энкодера.

Для проверки вывожу позицию энкодера при ее каждом изменении.

void loop() {
  currentPosition= encoder.read();
    if( currentPosition != prevPosition ) {
    Serial.println(currentPosition);
    prevPosition= currentPosition;
  }
  . . . .
}

Результат работы энкодера

Вот скетч для тестирования энкодера.

 

Зарегистрируйтесь и оплатитеВсего 60 руб. в месяц за доступ ко всем ресурсам сайта!
 

Вольтметр и рыба, управляемая энкодером.

Осталось собрать все вместе и получить такую картинку.

Окончательный вид проекта

Вот окончательный скетч.

 

Зарегистрируйтесь и оплатитеВсего 60 руб. в месяц за доступ ко всем ресурсам сайта!
 

Пояснять особенно нечего.

На первом ядре:

Измеряется напряжение.

Если кнопка энкодера удерживается нажатой, то анализируются признаки encoder.timeRight и encoder.timeLeft, которые показывают, с какой скоростью произошел поворот энкодера. В зависимости от их значений положения рыбы-стрелки изменяется с разной скоростью.

Медленное вращение позволяет устанавливать стрелку точно. При быстром вращении стрелка перескакивает с большей скоростью.

if ( encoder.timeRight < 10 ) voltageSet += 0.5;
else if ( encoder.timeRight < 40 ) voltageSet += 0.1;
else voltageSet += 0.01;

В зависимости, нажата ли кнопка энкодера, решается, какое значение выводить внизу экрана измеренное или заданное.

if( encoderButton.flagPress == true ) {
  preparationData.value= voltageSet;
  preparationData.mode= 2;
}
else {
  preparationData.value= voltage;
  preparationData.mode= 1;
}

При визуализации на втором ядре накладываются сразу 2 картинки.

drawTurnImage(BlackImage, arrow_0, transferData.turnKoeff1, 65535); // стрелка
drawTurnImage(BlackImage, fish_0, transferData.turnKoeff2, 248); // рыба

Работа устройства выглядит так.

Я вращаю переменный резистор, и вслед за этим поворачивается стрелка.

Когда нажимаю кнопку энкодера, внизу экрана измеренное значение меняется на заданное.

Поворот энкодера при нажатой кнопке энкодера вызывает поворот рыбы и изменение заданного значения.

Регулярное сканирование АЦП.

Чтобы еще приблизить задачу к практическому применению необходимо подкорректировать еще одну проблему.

В предыдущих скетчах измерение аналогового сигнала производилось в цикле loop().

for( int i=0; i < 256; i++ ) avCode += analogRead(A0);

Просто считывались подряд 256 значений АЦП, а затем усреднялись, пересчитывались в напряжение.

Самое распространенное решения в программах Ардуино, но не самое лучшее. Хотя, обычно еще хуже. Просто одно измерение делают.

  • Во первых на время чтения АЦП работа в цикле loop() подвисает.
  • И еще, выборка нескольких значений АЦП необходима для фильтрации аналоговых помех. Главный источник помех – это питающая сеть частотой 50 Гц. Значит, выборки надо производит через равные интервалы времени и усреднять через время кратное периоду сети, т.е. 20 мс.

Удобнее всего это делать в фоновом режиме в прерывании по таймеру.

static void alarm_irq(void) {
  . . . . .
  avCode += analogRead(A0);
  avCounter++;
  if( avCounter >= 120 ) {
    avRes = avCode;
    avCounter=0;
    avCode=0;
  }
}

Через каждые 120 * 0,5 = 60 мс в переменной avRes появляется новый код АЦП. В процессе вычисления данных он пересчитывается в напряжение.

Вот окончательный скетч проекта.

 

Зарегистрируйтесь и оплатитеВсего 60 руб. в месяц за доступ ко всем ресурсам сайта!
 

 

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

Предыдущий урок     Список уроков     Следующий урок

0

Автор публикации

не в сети 3 часа

Эдуард

280
Комментарии: 1936Публикации: 197Регистрация: 13-12-2015

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Нажимая кнопку "Отправить" Вы даёте свое согласие на обработку введенной персональной информации в соответствии с Федеральным Законом №152-ФЗ от 27.07.2006 "О персональных данных".