Сначала короткая предыстория появления этого поста. Относительно давно, помигав светодиодом, захотелось сделать что-то полезное. Так появился Беспроводной программируемый по Wi-Fi комнатный термостат с монитором качества воздуха и другими полезными функциями. Как назло, в это время перестал работать мой промышленный термостат. Меня выручила еще сырая поделка, наспех спрятанная в картонную коробочку. За время отопительного сезона напрягал лишь один недостаток прототипа – это необходимость таскать по квартире удлинитель 220В и кабель, который всегда путался под шваброй ногами. Поэтому решил сделать нечто похожее, но автономное, притом, с питанием от батареек, как в серийном образце.


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


Приступая к задаче, для меня было очевидно одно – вряд ли программы промышленных автономных устройств составлены на платформе Arduino IDE. Где все спрятано в громоздкие тяжеловесные библиотеки, а простые коды (скетчи) занимают в редакторе несколько десятков строк, делая работу в этой среде комфортной и не требующей особых усилий. Уточню сразу – дальше речь о выборе языка программирования между Ардуино, Си или Ассемблером. "Язык Ардуино"- это сленг для краткости. Нет такого языка программирования. Если увидите тут и дальше "язык Ардуино", то — это "Arduino IDE — интегрированная среда разработки для Windows, MacOS и Linux, разработанная на Си и C ++"(Википедия).


«… направу ехати — женату быти; налеву ехати — богату быти»


В начале пути меня оптимистично настроила статья Почему многие не любят Arduino. Ниже, для наглядности, картинка оттуда с кодом «мигалки».




Пример слева написан в платформе Arduino IDE, а справа — работа непосредственно с регистрами. Скетч выглядит несколько компактней, чем та же «мигалка», но с использованием регистров.


На изображении ниже — компиляция кода «мигалки» на Ассемблере. Как видно, былая компактность испарилась – количество строк в 3 раза больше, чем в Ардуино.





Итак, с 2 картинок выше видно – размер памяти, занимаемой в контроллере кодом «мигалки» одним светодиодом, написанным в платформе Ардуино, составляет 1030 байт, на Си – 176 байт, на Ассемблере – 42 байта.


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


барометр-термометр на Си
/*
    На распутье - Ардуино, Cи или Ассемблер?
    https://habr.com/ru/post/547752/ 
*/

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include "bmp180/bmp180.c"
#include "uart.c"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "nokia/nokia5110.h"

int main(void) {

    serial_init();

    DDRD |= (1 << 7);  //pin 13, atmega328p
    PORTD &= ~(1 << 7);
    nokia_lcd_init();

    while (1) {
        init_sensor(bmp180_mode_0);
        calculate();

        PORTD |= (1 << 7);
        _delay_ms(100);

        printf("Temperature: %.2f C, Pressure: %.2f Pa, \n", (float) bmp_180.temperature / 10, (float)bmp_180.pressure);

        nokia_lcd_clear();
        nokia_lcd_write_string("789",1);
        nokia_lcd_set_cursor(0, 10);
        nokia_lcd_write_string("22.2", 3);
        nokia_lcd_render();
        _delay_ms(2000);

        PORTD &= ~(1 << 7);
        _delay_ms(100);

        int a=54325;
        char buffer[20];
        itoa(a,buffer,2);   // here 2 means binary
        printf("Binary value = %s\n", buffer);

        itoa(a,buffer,10);   // here 10 means decimal
        printf("Decimal value = %s\n", buffer);

        itoa(a,buffer,16);   // here 16 means Hexadecimal
        printf("Hexadecimal value = %s\n", buffer);

    }
    return 0;
}

В проект входят следующие компоненты: контроллер ATMEGA328P, модуль давления-температуры BMP180 и дисплей Nokia 3110. ATMEGA328P принимает инфу с датчика BMP180 и после преобразований отображает ее на дисплее Nokia 3110, затем спит. Сон задается сторожевым таймером Watchdog. Проект собирается в Atmel Studio 7 и эмулируется в Proteus 8 Pro. Этот проект Atmel Studio был создан для отладки кода в Proteus'e. В библиотеке Proteus 8 Pro модуля BMP280 нет, поэтому пришлось составить код с включением BMP180. Светодиод в коде — для наглядности, чтобы придать динамику статичной картинке.




Ниже — электрическая схема устройства. При монтаже схемы обращайте внимание на функциональное назначение выводов контроллера и модулей. Подключение кварца — XTAL1, XTAL2 (ATMEGA328P). Уточню, схему барометра-термометра на BMP180 я "в железе" не собирал, поэтому тут могут проявиться проблемы, которые не видны при эмуляции в Proteus'e.





Для скачивания zip-файла проекта в Atmel Studio 7 перейдите по ссылке – тут все виртуальные проекты и коды программ из этой публикации.


Файлы прошивок *.hex находятся в папках Debug соответствующего проекта Atmel Studio 7. В архиве есть проект барометра-термометра на BMP280. Его электрическая схема такая же, как и у барометра-термометра на BMP180. Проект успешно собирается в Atmel Studio 7 и работает "в железе". Для работы "в железе" пришлось внести изменения в строке #define BMP280_ADDR 0x77 файла библиотеки bmp280.c, а именно: заменить начальный адрес 0x77 на 0x76. Не забудьте сделать эту корректировку, если будете использовать в своих проектах код барометра-термометра на BMP280, с подключенной библиотекой bmp280.c.


Ниже — код этого же барометра-термометра в платформе Arduino IDE. Естественно, с другими библиотеками.


барометр-термометр в Arduino IDE
/*
    На распутье - Ардуино, Cи или Ассемблер?
    https://habr.com/ru/post/547752/ 
*/

#include <SPI.h>
#include <LowPower.h>
#include <SimpleTimer.h>
#include <Adafruit_BMP280.h>
#include <Adafruit_GFX.h> //https:esp8266.ru/forum/threads/esp8266-5110-nokia-lcd.1143/#post-16942
#include <Adafruit_PCD8544.h> //https:esp8266.ru/forum/threads/esp8266-5110-nokia-lcd.1143/#post-16942

Adafruit_BMP280 bmp280;
float Press, Tin; //давление, температура

Adafruit_PCD8544 display = Adafruit_PCD8544(5, 7, 6);

void setup() {
  Serial.begin(9600);

  display.begin();
  // display.clearDisplay();
  display.setContrast(60); // установка контраста

  while (!bmp280.begin(BMP280_ADDRESS - 1)) {
    Serial.println(F("Could not find a valid BMP280 sensor, check wiring!"));
    delay(100);
  }
}

void loop() {
  // измерение температуры, давления
  Tin = bmp280.readTemperature();
  Press = bmp280.readPressure() / 133.3;
  Serial.println("Temperature: " + String(Tin) + "*C");
  Serial.println("Pressure: " + String(Press) + "mm Hq");

  display.clearDisplay();
  //давление, мм рт.ст.
  {
    display.setTextSize(1);
    display.setCursor(20, 5);
    display.println (Press, 0); // нет знаков после запятой
    display.setCursor(41, 5);
    display.println("mmHq");
  }
  //температура, *C
  {
    display.setTextSize(2);
    display.setCursor(15, 20);
    display.println (Tin, 1); // один знак после запятой
    display.setCursor(66, 20);
    display.println("C");
  }
  display.display();

  LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);
}

Ресурсы, потребляемые программой барометра-термометра на Си и в Arduino IDE наглядно показаны на картинке:




Как видно, эти примеры потребляют 5954 байт (С) и 12956 байт (Arduino IDE) в Flash. Соотношение изменилось с 6-ти раз для «мигалки» до 2-х с небольшим. К сожалению, линейной зависимости нет – чем объемней код, тем меньше соотношение размеров памяти Ардуино к Си. В идеале на этой картинке должен присутствовать 3 столбец с кодом на Ассемблере, но такого кода в Интернете я не нашел, а составить код самому мне пока не под силу.


Попутно замечу, что использование компилируемых в Arduino IDE библиотек и функций на С/С++ особо имеет смысл в тех случаях, когда размер занимаемой памяти превышает или близок к размеру памяти контроллера. Мне, например, часто удается уходить от предупреждения: Недостаточно памяти, программа может работать нестабильно.





Теперь посмотрим еще один вариант – это цифровой термометр-гигрометр на AM2302 (DHT22), ATtiny13 и MAX7219, код которого составлен на Ассемблере.


Автор статьи задался целью разработать простой термометр-гигрометр выполненном на одном из самых «маленьких» микроконтроллеров — ATtiny13 с весьма скромными характеристиками – 1Кб программной памяти, 64 байтами ОЗУ и 5 интерфейсными выводами. Он решил эту непростую задачку, выбрав Ассемблер, заодно вспомнив те далекие времена, когда код можно было составлять на низкоуровневых языках, используя машины типа ZX-Spectrum.


Ниже скриншот со сборкой данного кода в Atmel Studio 7.





Код устройства на Ассемблере занимает 738 байт памяти в контроллере. Безусловно, программа барометра-термометра, о котором шла речь выше, будь ее код составлен на Ассемблере, заняла бы больше места. По нескольким причинам — в схеме реализовано управление дисплеем Nokia3110 по интерфейсу SPI (это 5 линий связи, тут – 3), связь с датчиком BMP280 осуществляется по протоколу I2C (2 линии, тут – 1) и дополнительные символы, которые позволяют не гадать – температура это или другой параметр.


Из того, что я нашел в Интернете, можно утверждать, Ассемблер даст выигрыш в размере кода для относительно больших проектов процентов 10-20 по сравнению с Си. Но надо учитывать, что в больших проектах Си может уменьшить размер кода за счёт лучшей оптимизации.


Код Ассемблера выполняется практически на машинном уровне: один цикл – одна команда. В качестве аргумента приведу пример из справочника по командам ассемблера AVR. Установка бита в регистре ввода/вывода — SBI A, b. Эта команда устанавливает заданный бит в регистре ввода-вывода. На выполнение этой операции контроллерами megaAVR потребуется 2 цикла и на tinyAVR, XMEGA — 1 цикл. Для схемы с контроллером ATtiny13 и резонатором 9,6 МГц выполнение команды займет один цикл, то есть 1/9600000 Гц = 0,104 мксек.


Выполнение похожей операции на языке Си, например, задать состояние порта — PORTB = 32; займет в этой же схеме не меньше времени. А о Ардуино и говорить нечего – там придется выполнить объемную функцию void digitalWrite(uint8_t pin, uint8_t val);. Подробно о размерах кода в Си и Ардуино читайте тут.


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


Теперь о энергосбережении немножко издали. Вспомним, что код Ассемблера выполняется на машинном уровне: один цикл – одна или несколько команд. в зависимости от типа контроллера. Это — десятые доли микросекунды. То есть, на выполнение программы с размером несколько десятков байт уйдут единицы-десятки микросекунд. Дальше контроллер бесконечно будет крутить этот набор «0» и «1», затрачивая энергию на перезаряд емкости затворов сотен полевых транзисторов, на которых построен кристалл контроллера, а также чтение и записи данных в его память. Длительность периода повтора будет зависеть только от размера кода в памяти контроллера, неважно на каком языке он составлен. Просто на Assembler'е он будет наименьшим, а в Arduino IDE – наибольшим. Соответственно, период цикла для кода на Assembler'е – наименьший, в Arduino IDE – наибольший.


Уменьшить эти затраты можно остановив процессор или программно уменьшив частоту его работы. В Ассемблере переход в "спящий" режим сна выполняет функция управления контроллером SLEEP. В других можно использовать функцию WDT (WatchDog Timer), а в Ардуино еще и функцию LowPower.powerDown (SLEEP_1S, ADC_OFF, BOD_OFF), заодно отключив все лишнее, что не используется в конкретной задаче. В эффективности этой функции сможет убедиться каждый, заменив в скетче «мигалки» (скетч — на картинке вначале статьи) функцию отсчета времени delay(1000); этой функцией и включив в разрыв питания контроллера амперметр. Да, не забудьте подключить библиотеку LowPower.h. На Си это сделал автор этой статьи. Ток в цепи питания attiny13a с паузой — 1,5мА, со сном — 240мкА. Потребление в 6(!) раз меньше.


Допустим, вы намерены собрать барометр-термометр и задумываетесь о энергосбережении. Понятно, что давление/температура в заданной разрядности не изменятся за несколько минут, которые для контроллера целая вечность. Ему можно выделить это время для сна. После сна он снова выполнит свою работу: примет информацию с датчика, преобразует в понятные для человека циферки и выведет все это на дисплей. И в таком режиме «работа-сон» он будет крутиться, пока не сядут батарейки. Объем «работы» контроллера, вернее время, которое контроллер будет занят выполнением работы, зависит от того, на каком языке составлена программа барометра-термометра. Если есть возможность загрузить в контроллер код на выбор – ArduinoIDE, C, Assembler, с одинаковым временем «сна», то в каком из трех предложенных вариантов батарейки сядут раньше (позже)? Мой ответ – ArduinoIDE (Assembler).


Так куда же идти? На мой взгляд, для любителей, как я, – это платформа Arduino IDE с низкоуровневыми вставками. Тем же, кому тесно в Arduino IDE, — в С. Хотя коды на С можно оптимизировать иногда до размеров не намного больше, чем в Assembler’е, все-таки для понимания работы контроллера стоит напрячься и освоить азы Assembler’а. Ведь полезность знаний – это аксиома.


Спасибо за внимание. Всего наилучшего!


Ссылки по теме