Дисплей часов управляется esp32

Недавно я увлекся миром микроконтроллеров и решил сделать простой проект часов, чтобы увидеть, что к чему. Первая попытка. Я использовал нанофреймворк .Net, так как днем ​​я кодирую C #, но я не мог понять, как заставить его работать достаточно быстро.

Я перенес свой код на C ++ и использовал PlatformIO для сборки и развертывания. Это работает, и я полностью устранил проблему мерцания, которая у меня была, но поскольку я не использовал C более десяти лет и никогда особо не погружался в C ++, Я действительно мог бы использовать некоторые рекомендации по передовому опыту и тому подобное.

По мере роста моего проекта мне нужно будет поместить свои классы в отдельные файлы .hpp (Google предлагает, чтобы C ++ продолжал традицию C помещать интерфейс в файл заголовка, а реализацию в файл .cpp — я немного разочарован, VSCode не не предлагаю мне этот простой рефакторинг, поэтому я чувствую, что упускаю что-то очевидное). Но кроме этого, что еще мне не хватает?

Enums мне показалось немного неуместным. В C # я бы сделал myEnum++. Портирование foreach вызвал у меня головную боль. я нашел std::for_each() но в конце концов я решил forсинтаксис был на самом деле более читабельным. Мой std::array инициализация, вероятно, также немного шаткая.

Чтобы запустить это во всей красе: требуется контроллер ESP32, декодер TI CD74HC4511E BCD и 4-значный 7-сегментный дисплей LiteOn LTC-2723Y. Наверное, неплохо включить семь резисторов на всякий случай.

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_spi_flash.h"
#include "driver/gpio.h"
#include <array>
#include <algorithm>

extern "C"
{
  void app_main(void);
}

// Outputs a 4-bit value to a BCD decoder
class BCDWriter
{
public:
  BCDWriter(gpio_num_t d0, gpio_num_t d1, gpio_num_t d2, gpio_num_t d3)
  {
    _bits = {d0, d1, d2, d3};
    for (auto pin = _bits.begin(); pin != _bits.end(); pin++)
    {
      gpio_pad_select_gpio(*pin);
      gpio_set_direction(*pin, GPIO_MODE_OUTPUT);
    }
  }

  void Write(short value)
  {
    for (auto pin = _bits.begin(); pin != _bits.end(); pin++)
    {
      gpio_set_level(*pin, (value & 1) > 0 ? 1 : 0);
      value >>= 1;
    }
  }

private:
  std::array<gpio_num_t, 4> _bits;
};

// Controls a four digit 7 segment display (e.g. LiteOn LTC-2723Y)
// Assumes common cathode (set digit pin low to activate that digit's display)
class QuadDigitDisplay
{
public:
  enum QuadDigit
  {
    First,
    Second,
    Third,
    Fourth,
    Indicators
  };

  QuadDigitDisplay(gpio_num_t cc1, gpio_num_t cc2, gpio_num_t cc3, gpio_num_t cc4, gpio_num_t l)
  {
    _pins = {cc1, cc2, cc3, cc4, l};
    for (auto pin = _pins.begin(); pin != _pins.end(); pin++)
    {
      gpio_pad_select_gpio(*pin);
      gpio_set_direction(*pin, GPIO_MODE_OUTPUT);
    };
  }

  void SetHigh(QuadDigit quadDigit)
  {
    gpio_set_level(_pins[quadDigit], 1);
  }

  void SetLow(QuadDigit quadDigit)
  {
    gpio_set_level(_pins[quadDigit], 0);
  }

private:
  std::array<gpio_num_t, 5> _pins;
};

// Display "12:34" test output on the display
void app_main()
{
  printf("Hello PlatformIO!n");
  QuadDigitDisplay quadDisp(GPIO_NUM_26, GPIO_NUM_22, GPIO_NUM_18, GPIO_NUM_27, GPIO_NUM_19);
  BCDWriter bcd(GPIO_NUM_0, GPIO_NUM_17, GPIO_NUM_2, GPIO_NUM_5);

  QuadDigitDisplay::QuadDigit quadDigit = QuadDigitDisplay::QuadDigit::First;
  while (true)
  {
    uint8_t number = quadDigit == QuadDigitDisplay::QuadDigit::Indicators ? (uint8_t)2 : (uint8_t)quadDigit;
    bcd.Write(number);
    quadDisp.SetLow(quadDigit);
    vTaskDelay(5 / portTICK_PERIOD_MS);
    quadDisp.SetHigh(quadDigit);

    if (quadDigit == QuadDigitDisplay::QuadDigit::Indicators)
    {
      quadDigit = QuadDigitDisplay::QuadDigit::First;
    }
    else
    {
      quadDigit = QuadDigitDisplay::QuadDigit(quadDigit + 1);
    }
  }
}

2 ответа
2

Делать class QuadDigitDisplay делай, что он говорит

В QuadDigitDisplay class на самом деле не обрабатывает отображение четырех цифр, он только устанавливает контакты, которые позволяют отображать каждую цифру. Было бы неплохо, если бы этот класс действительно обрабатывал все необходимое для управления четырехзначным дисплеем.

В идеале ваша основная функция выглядит так:

void app_main()
{
    QuadDigitDisplay quadDisp(GPIO_NUM_26, ...);

    // Display 11:11, 22:22 and so on in a loop
    while (true) {
        for (int i = 0; i < 10; i++)
            quadDisp.setDigits({i, i, i, i});
            vTaskDelay(1000 / portTICK_PERIOD_MS);
        }
    }
}

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

class QuadDigitDisplay
{
public:
  QuadDigitDisplay(gpio_num_t cc1, ...): _pins{cc1, ...}, _bcd{d0, ...} {
    // Set pins to output
    for (auto pin: _pins) {
      gpio_pad_select_gpio(pin);
      gpio_set_direction(pin, GPIO_MODE_OUTPUT);
    };

    // Create the display task
    xTaskCreate(DisplayDigits, "display digits", 100, this, 1, &_task);
  }

  ~QuadDigitDisplay() {
    // Stop the display task
    vTaskDelete(_task);

    // Set pins to input
    for (auto pin: _pins) {
      gpio_set_direction(pin, GPIO_MODE_INPUT);
    };
  }

  void SetDigits(const std::array<int, 4> &digits) {
      _digits = digits;
  }

private:
  static void DisplayDigits(void *arg) {
    QuadDigitDisplay *self = arg;

    while (true) {
      for (auto digit = 0; digit < 4; ++digit) {
        self->_bcd.Write(self->_digits[digit]);
        gpio_set_level(self->_pins[digit], 0);
        vTaskDelay(5 / portTICK_PERIOD_MS);
        gpio_set_level(self->_pins[digit], 1);
      }
    }
  }

  TaskHandle_t _task{};
  std::array<int, 4> _digits;
  std::array<gpio_num_t, 5> _pins;
  BCDWriter _bcd;
};

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

Рассмотрите возможность удаления enum

Перечисление, значения которого в основном равны ONE, TWO, THREE или FIRST, SECOND, THIRD не очень полезно. Просто используйте для этого обычное целое число, они отлично подходят для счета! Также рассмотрите возможность использования отдельной переменной для хранения номера вывода для индикаторов, поскольку ее назначение действительно отличается от целей самих цифр.

Используйте диапазон для

Перенос foreach вызвал у меня головную боль. я нашел std::for_each() но в конце концов я решил, что синтаксис на самом деле более читабельный.

Начиная с C ++ 11, вы можете выполнять «foreach» с помощью обычного for заявление, как я уже показал в коде выше. Это называется цикл for на основе диапазона, и вы используете его так:

std::array<gpu_num_t, 5> _pins;
...
for (auto pin: _pins) {
  gpio_pad_select_gpio(pin);
  gpio_set_direction(pin, GPIO_MODE_OUTPUT);
};

  • Для меня перечисление — это ограничение. Обратите внимание, что он содержит элемент под названием «Индикатор». Это не просто число. В идеале я хотел бы сказать «инициализировать первым членом моего перечисления» и спросить: «равно ли текущее значение последнему члену перечисления?». Конечно, int будет работать, но действительных чисел всего пять. Было бы здорово, если бы язык / компилятор помогли мне защитить это автоматически. В Паскале я бы использовал Low () и High () (с набором) IIRC.

    — 9Rune5

  • Я люблю это, кстати. Намного лучше. 🙂

    — 9Rune5

  • Установка пинов для ввода в деструктор — это помогает плате работать немного лучше, когда она программируется? У меня возникли некоторые проблемы с фреймворком nano, так как я был вынужден отключить заземляющие контакты при программировании моей платы с помощью USB.

    — 9Rune5



  • 1

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

    — Г. Сон

Вот некоторые вещи, которые могут помочь вам улучшить вашу программу.

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

Оба конструктора можно немного переписать в более современном стиле:

BCDWriter(gpio_num_t d0, gpio_num_t d1, gpio_num_t d2, gpio_num_t d3)
: _bits{d0, d1, d2, d3}
{ /* the rest of the constructor */ }

Использовать «диапазон for«и упростите свой код

Код включает в себя ряд таких строк:

for (auto pin = _bits.begin(); pin != _bits.end(); pin++)

Более простой способ выразить это в современном C ++:

for (auto pin : _bits)

Используйте логические выражения логически

Код для Write содержит эту строку:

gpio_set_level(*pin, (value & 1) > 0 ? 1 : 0);

Это могло быть намного проще:

gpio_set_level(pin, value & 1);

Переосмыслить интерфейс

Эти классы на самом деле мало что делают, кроме как в качестве заполнителей для номеров контактов. Было бы намного лучше, если бы мы могли использовать более удобный интерфейс. Я бы предположил, что это имеет смысл для QuadDigitDisplay чтобы содержать как катоды, так и цифры и текущую отображаемую цифру. Тогда у него может быть метод diplay который принимает в качестве аргументов цифру и значение. С таким классом мы могли бы переписать app_main:

void app_main()
{
    printf("Hello PlatformIO!n");
    QuadDigitDisplay quadDisp({GPIO_NUM_26, GPIO_NUM_22, GPIO_NUM_18, GPIO_NUM_27, GPIO_NUM_19}, {GPIO_NUM_0, GPIO_NUM_17, GPIO_NUM_2, GPIO_NUM_5});
    std::array<short, 5> values{0, 1, 2, 3, 2 /* colon indicator */};

    while (true)
    {
        for (short i = 0; i < 5; ++i) {
            quadDisp.display(i, values[i]);
            vTaskDelay(5 / portTICK_PERIOD_MS);
        }
    }
}

Класс можно было бы записать так:

class QuadDigitDisplay
{
public:
  QuadDigitDisplay(std::array<gpio_num_t, 5> cathodes, std::array<gpio_num_t, 4> digits)
  : cathodes{cathodes}
  , digits{digits}
  {
    for (auto pin : cathodes) {
      gpio_pad_select_gpio(pin);
      gpio_set_direction(pin, GPIO_MODE_OUTPUT);
    }
    for (auto pin : digits) {
      gpio_pad_select_gpio(pin);
      gpio_set_direction(pin, GPIO_MODE_OUTPUT);
    }
  }
  
  void display(unsigned short digit, short value) {
      // undisplay currently active digit
      gpio_set_level(cathodes[active_digit], 1);
      // set new active digit
      active_digit = digit;
      // set digit outputs
      for (auto pin : digits) {
          gpio_set_level(pin, value & 1);
          value >>= 1;
      }
      // now display new active digit
      gpio_set_level(cathodes[active_digit], 0);
  }

private:
  std::array<gpio_num_t, 5> cathodes;
  std::array<gpio_num_t, 4> digits;
  unsigned short active_digit = 0;
}

Обратите внимание, что digit а также value обоим потребуется проверка диапазона в реальном коде. Это всего лишь образец.

Рассмотрим дальнейшую абстракцию

Даже после рефакторинга выше есть некоторое дублирование. Можно было создать GPIO_Outpin класс, который будет обрабатывать настройку, хранить пин-код и предоставлять operator= для установки значения булавки.

Вот как это может выглядеть:

class GPIO_Outpin {
public:
    GPIO_Outpin(gpio_num_t pin) : pin{pin} {
      gpio_pad_select_gpio(pin);
      gpio_set_direction(pin, GPIO_MODE_OUTPUT);
    }
    void operator=(bool value) {
        gpio_set_level(pin, value);
    }
private:
    gpio_num_t pin;    
};

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

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