Недавно я увлекся миром микроконтроллеров и решил сделать простой проект часов, чтобы увидеть, что к чему. Первая попытка. Я использовал нанофреймворк .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 ответа
Делать 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);
};
Вот некоторые вещи, которые могут помочь вам улучшить вашу программу.
Предпочитайте современные инициализаторы для конструкторов
Оба конструктора можно немного переписать в более современном стиле:
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;
};
Для меня перечисление — это ограничение. Обратите внимание, что он содержит элемент под названием «Индикатор». Это не просто число. В идеале я хотел бы сказать «инициализировать первым членом моего перечисления» и спросить: «равно ли текущее значение последнему члену перечисления?». Конечно, int будет работать, но действительных чисел всего пять. Было бы здорово, если бы язык / компилятор помогли мне защитить это автоматически. В Паскале я бы использовал Low () и High () (с набором) IIRC.
— 9Rune5
Я люблю это, кстати. Намного лучше. 🙂
— 9Rune5
Установка пинов для ввода в деструктор — это помогает плате работать немного лучше, когда она программируется? У меня возникли некоторые проблемы с фреймворком nano, так как я был вынужден отключить заземляющие контакты при программировании моей платы с помощью USB.
— 9Rune5
В целом хорошей практикой является установка контактов в режим высокого импеданса, когда они больше не нужны, и если вы можете выбрать, имеет ли контакт подтягивающий или понижающий резистор, установите его на тот, который вызывает устройство. бездействовать. Это может быть не актуально, если ваша программа работает в бесконечном цикле и никогда не завершается, поскольку в этом случае деструкторы никогда не вызываются.
— Г. Сон