Несколько лет назад сделал себе на Arduino блок мониторинга питания дачного котла от UPS. Как показала практика, связка Arduino MEGA + шилд на SIM900 со стандартными библиотеками работает очень нестабильно. Периодически всё зависает, само перегружается и т.д. Отладить это невозможно, поэтому стал искать другие варианты. В результате решил всё переделать на современных технологиях: взял за основу STM32 Bluepill, приобрел на али модуль SIM800L, но самое главное – весь код решил написать на Rust, купился на обещания его высокой надёжности. Что из этого получилось читайте дальше.

Функциональность

Несколько слов о том, что хотелось получить от новой самоделки.

У меня на даче стоит UPS Inelt, в котором есть порт RS232 для управления UPS. Он поддерживает довольно древний протокол обмена Megatec, с помощью которого можно выполнять несложные действия по управлению UPS и узнавать его текущее состояние. Управление меня не особо интересовало, а вот состояние работы знать хотелось: как там сетевое напряжение, живы ли батареи и пр. На рынке я не нашёл готовых решений, которые могли бы решить эту задачу, поэтому решил всё сделать сам. Всё в духе DIY.

Протокол Megatec
Megatec Protocol information
                        VERSION : 2.7

                        DATE : SEP. 30, 1995

		DATE	:V2.7	: JULY 30 ,1996

DATE	DESCRIPTION	MODIFY BY	
2.6	95-9-30	UPDATE "D" COMMAND (SS.SS -> SSS.SS)	Kevin Chiou	
2.7	96-8-01	Disable "D" COMMAND	Kevin Chiou	
				
A. General: This document specifies the RS232C communication protocol of
            the Advance-Intelligent UPS. The protocol provided the following
            features :

            1. Monitor charger status.
            2. Monitor battery status and condition.
            3. Monitor the utility status.
            4. Provide the power switch function for computer to turn on and
               off the utility on schedule for power saving.

            Computer will control information exchange by a query followed by
            <cr>. UPS will respond with information followed by a <cr> or
            action.

B. Hardware:

            BAUD RATE............... : 2400 bps
            DATA LENGTH.......... : 8 bits
            STOP BIT..................... : 1 bit
            PARITY........................ : NONE

            CABLING :

               COMPUTER                  UPS
            ===================================
                  RX   <----------   TX  (pin 9)
                  TX    ---------->  RX  (pin 6)
                  GND  <----------   GND (pin 7)

                  (9 pins female D-type connector)

C. COMMUNICATION PROTOCOL:

1. Status Inquiry:

Computer : Q1<cr>
        UPS      : UPS status data stream, such as
             (MMM.M NNN.N PPP.P QQQ RR.R S.SS TT.T  b7b6b5b4b3b2b1b0<cr>

        UPS status data stream :
	There should be a space character between every field for data
	separation. The meaning of each field is list as followed:

                a. Start byte    : (

		b.I/P voltage   : MMM.M
                  M is and integer number ranging from 0 to 9.
                  The unit is Volt.

                c.I/P fault voltage : NNN.N
                  N is and integer number ranging from 0 to 9.
                  The unit is Volt.

                  ** For OFF LINE UPS**

                    Its purpose is to identify a short duration voltage glitch
                  which cause OFF line UPS to go to Invter mode. If this occurs
                  input voltage will appear normal at query prior to glitch and
                  will still appear normal at next query.
                    The I/P fault voltage will hold glitch voltage till next
                  query. After query, the I/P fault voltage will be same as I/P
                  voltage until next glitch occurs.

                  ** For ON LINE UPS**

                    Its purpose is to identify a short duration utility fail
                  which cause ON line UPS to go to battery mode. If this occurs
                  input voltage will appear normal at query prior to fail and
                  will still appear normal at next query.
                    The I/P fault voltage will hold utility fail voltage till
                  next query. After query, the I/P voltage will be same as I/P
                  voltage until next utility fail occurs.

                d.O/P voltage   : PPP.P
                  P is an integer number ranging form 0 to 9.

                  The unit is Volt.

                e.O/P current   : QQQ
                  QQQ is a percent of maximum current, not an absolute value.

                f.I/P frequency : RR.R
                  R is an integer number ranging from 0 to 9.
                  The unit is HZ.

                g.Battery voltage : SS.S or S.SS
                  S is an integer number ranging from 0 to 9.
                    For on-line units battery voltage/cell is provided in the
                  form S.SS .
                    For standby units actual battery voltage is provided in
                  the form SS.S .
                    UPS type in UPS status will determine which reading was
                  obtained.

                h.Temperature   : TT.T
                  T is an integer number ranging form 0 to 9.
                  The unit is degree of centigrade.

                i.UPS Status    : <U>
                  <U> is one byte of binary information such as
                  <b7b6b5b4b3b2b1b0>.
 					Where bn is a ASCII character '0' or '1'.
.
UPS status :
Bit 	            Description                 	
 7  	1 : Utility Fail (Immediate)            	
 6  	1 : Battery Low                         	
 5  	1 : Bypass/Boost or Buck Active                 	
 4  	1 : UPS Failed                          	
 3  	1 : UPS Type is Standby (0 is On_line)  	
 2  	1 : Test in Progress                    	
 1  	1 : Shutdown Active                     	
 0  	1 : Beeper On                             	

j.Stop Byte     : <cr>

			Example: Computer : Q1<cr>
		    UPS      :
                         (208.4 140.0 208.4 034 59.9 2.05 35.0 00110000<cr>

                       Means    : I/P voltage is 208.4V.
                                  I/P fault voltage is 140.0V.
                                  O/P voltage is 208.4V.
                                  O/P current is 34 %.
                                  I/P frequency is 59.9 HZ.
                                  Battery voltage is 2.05V.
                                  Temperature is 35.0 degrees of centigrade.
                                  UPS type is on-line , UPS failed. Bypass
                                  active , and shutdown not active.

  2. Test for 10 seconds:

        Computer  : T<cr>
        UPS       : Test for 10 seconds and return to utility.

        If battery low occur during testing, UPS will return to
        utility immediately.

   3.Test until battery low :

        Computer  : TL<cr>
        UPS       : Test until battery low and return to utility.

   4.Test for specified time period :

        Computer  : T<n><cr>
        UPS       : Test for <n> minutes.

        a. During testing, UPS returns to utility immediately, if
           battery low occur.
        b. <n> is a number ranging from 01 to 99.

   5. Turn On/Off beep -- Toggle the UPS beeper :

        Computer  : Q<cr>

        When the AC power failed, UPS will generate a warning beep to
        inform the manager. Manager could toggle the warning beep by
        sending this command .

   6. Shutdown Command :

        Computer  : S<n><cr>
        UPS       : Shut UPS output off in <n> minutes.

        a. The UPS output will be off in <n> minutes, even if the
           utility power is present.
        b. If the battery low occurs before <n> minutes, the
           output is turned off immediately.
        c. After UPS shutdown, the controller of UPS monitors the
           utility power. If the utility is recovered, the UPS will wait
           for 10 seconds and connect the utility to output.
        d. <n> is a number ranging form .2, .3, ..., 01, 02, ..., up to 10.

        For example : S.3<cr> --- shut output off in (.3) minutes

   7. Shutdown and Restore Command :

        Computer  : S<n>R<m><cr>
        UPS       : Shut UPS output off in <n> minutes, and waiting
                    for <m> minutes then turn on UPS output again.

        a. The shutdown sequence is the same as the previous command.
           When the <m> minutes expired, the utility do not restore,
           the UPS will wait until utility restore.
        b. If UPS is in shutdown waiting state, the "C" command can
           let the shutdown procedure cancelled.
        c. If UPS is in restore waiting state, the "C" command can
           let the UPS output turned on, but UPS must be hold off at
           least 10 seconds. (if utility is present)
        d. <n> is a number ranging form .2, .3, ..., 01, 02, ..., up to 10.
        e. <m> is a number ranging form 0001 to 9999.

   8. Cancel Shutdown Command :

        Computer  : C<cr>
        UPS       : Cancel the SN<n><cr> and SN<n>R<m><cr> command.

        a. If UPS is in shut down waiting state, the shut down command
           is cancelled.
        b. If UPS is in restore waiting state, the UPS output is turned
           on, but UPS must be hold off at least 10 seconds.
           (if utility is present)

   9. Cancel Test Command :

        Computer  : CT<cr>
        UPS       : Cancel all test activity and connect the utility to
                    output immediately.

	10. UPS Information Command:

		Computer	: I<cr>
		UPS			: #Company_Name UPS_Model Version<cr>

	This function will make the UPS respond with the basic information
	about the company who manufacture the UPS, the model name of the
	UPS and the version number of the UPS firmware. The length of
	every field is listed as follows:
		Company_Name	: 15 characters, leave space if less than 15 characters
		UPS_Model		: 10 characters, leave space if less than 10 characters
		Version			: 10 characters, leave space if less than 10 characters
	
	There should be a space character between every field for separation.

	11. UPS Rating Information:
		
		Computer	: F<cr>
		UPS			: #MMM.M QQQ SS.SS RR.R<cr>

	This function makes the UPS answer the rating value of UPS. There
	should be a space character between every field for
	separation. The UPS's response contains the following information
	field:

			a. Rating Voltage	: MMM.M
			b. Rating Current	: QQQ
			c. Battery Voltage	: SS.SS or SSS.S
			d. Frequency		: RR.R

D. COMMAND SUMMARY:

ITEM	COMMAND 	           DESCRIPTION              	
 1a  	  D     	  Status Inquiry           *disable         	
 1  	  Q1     	  Status Inquiry                    	
 2  	  T     	  10 Seconds Test                   	
 3  	  TL    	  Test until Battery Low            	
 4  	  T<n>  	  Test for Specified Time Period    	
 5  	  Q     	  Turn On/Off beep                  	
 6  	  S<n>  	  Shut Down Command                 	
 7  	S<n>R<m>	  Shut Down and Restore Command     	
 8  	  C     	  Cancel Shut Down Command          	
 9  	  CT    	  Cancel Test Command               	
10	   I	  UPS Information Command	
11	   F	  UPS Rating Information	

E. Invalid Command/Information Handling

If the UPS receives any command that it could not handle, the UPS should
echo the received command back to the computer. The host should check if
the command send to UPS been echo or not. 
If there is any information field in the UPS's response which is
unavailable or not supported, the UPS should fill the field with '@'.

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

Пока проектировал электронику, решил добавить к модулю свой маленький UPS, т.е. микроконтроллер может у меня питаться либо от сети, либо от встроенной батареи. Раз такое дело, решил еще мониторить модулем напряжения питания сети и батареи.

Всё это было реализовано, как уже писал выше, на Arduino MEGA + SIM900. Теперь же нужно было всё это переделать на Bluepill с небольшими доработками схем питания и подключения датчиков. Аппаратные возможности bluepill существенно больше, чем Arduino, поэтому особых проблем с переходом нет, тем более, что многие контакты bluepill толерантны к 5 В сигналам. Единственная проблема, что в bluepill нет eeprom, поэтому пришлось подключить к конструкции дополнительную микросхему. В кладовке нашлась бесхозная 24LC16, поэтому использовал её, хотя мне нужно было всего несколько байт хранить. Как до меня дошло позже, эти байты можно было бы сохранить в ячейках SIM карты, но цели сделать оптимальное решение я не ставил. Наоборот, поскольку я решил использовать новые для себя технологии, то мне интересно было всё попробовать – испытать как можно больше аппаратных возможностей STM32 и попробовать embedded Rust в целях самообразования.

В конце статьи есть ссылка на репозитарий с исходным кодом, если посмотрите, то увидите, что я использовал очень многие блоки STM32. АЦП, таймеры, USART, прерывания, RTC и пр. Руки не дотянулись разве что до использования DMA и новомодной асинхронщины Rust.

Начало похода по граблям

Если на STM32 у меня уже был некоторый опыт программирования на C++, то Rust был совершенно в новинку. Вообще использовать Rust пришло в голову, когда наткнулся на несколько заметок, что кому-то удалось помигать светодиодом на bluepill используя Rust. К сожалению, более содержательных статей я тогда не встретил, поэтому решил для начала почитать поподробнее документацию Rust. Язык оказался довольно сложный для меня, надо прямо сказать, намного труднее того, что используется в Arduino. Самое печальное было то, что в книгах по Rust очень много уделяется внимания стандартным библиотекам, которые, что позже стало для меня неожиданностью, вообще не используются в embedded версии. Все эти String, Vec, сотни адаптеров и замыканий, которым посвящены тысячи страниц в документации вообще бесполезны. Печаль.

Но ради главного преимущества Rust, предсказуемого поведения программы, я готов был с этим всем примериться. Наверняка те, кто не потратил много бессонных ночей в поисках seg fault, не поймут меня, ну да и ладно.

Основным источником информации для меня стали исходники примеров использования библиотек на crates.io. Надо сказать, что даже имеющиеся там примеры нужно было допиливать чтоб они заработали на bluepill. Но тем интереснее и продуктивнее изучение языка. Начинающим сразу предлагаю изучить крейт heapless и использовать его методы вместо Vec, String стандартной библиотеки.

Далее в порядке важности, нужно разобраться в крейте stm32f1xx-hal. Собственно, благодаря ему обычно и идёт взаимодействие с оборудованием bluepill. Увидев слово HAL, сразу приходит мысль о STM32 CubeMX, но это обманчиво, тут HAL совсем другой. Другие функции, способы доступа к железу. Хорошо хоть на совсем низком уровне сохранили названия регистров и блоков микроконтроллера, иначе вообще ничего понять было бы невозможно без примеров. Документация на крейт есть, но довольно посредственная. Мне постоянно приходилось смотреть исходный код библиотеки для понимания как там всё реально работает.

Железо и драйверы

Отдельная тема — это поиск драйверов для всякого оборудования. Если для Arduino есть, видимо, драйвер для любого устройства, то с Rust не всё так радужно. Меня с самого начала интересовало, есть ли готовые библиотеки для eeprom, датчика температуры DS18B20, да и SIM800. К счастью, для памяти и датчика крейты есть, а для SIM800 всё пришлось делать самому. Для обучения самое то. Я изучил исходники ардуиновской библиотеки SIM900 и решил их творчески портировать на Rust в необходимом мне объёме.

Кстати, для DS18B20 есть несколько крейтов и я сперва взял первый попавшийся. И не попал. Он оказался очень нестабильным – датчик то считывал температуру, то выдавал ошибку CRC. Я сначала менял датчики, отключал прерывания, но ничего не помогало. В результате перешёл на другой крейт и всё заработало стабильно без танцев с бубном.

Надо сказать, что для работы с AT командами есть готовый крейт atat. Можно было попробовать его использовать. Но мне он показался сложным. Да и идеологически он обрабатывает ответы от модуля SIM800 не так, как это делает Arduino. Последний определяет окончание передачи по таймауту, а atat использует маркером конца передачи перевод строки. В результат добиваться работоспособности atat я не стал, а просто свой код написал.

Для меня основной головной болью работы с SIM800 оказались так называемые unsolicited result codes. Это когда вы посылаете в модуль какую-то команду и ждете в ответ ОК, а получаете сообщение о новом СМС или RING. Очень неудобно. К счастью в документации на SIM800 написано как большинство из них отключить. Но при этом теряется возможность принимать входящие звонки. В итоге я включаю возможность принимать RING только на определённое время ожидания звонка, в остальное время звонки отклоняются. Это несколько снизило удобство работы с модулем, поскольку иногда дозвониться до него с первого раза не удаётся, хотя и редко. Зато надёжность работы остальных команд возросла. И да, я использую довольно “наивный” способ выполнения AT команд: просто делается несколько последовательных попыток выполнить команду, если с первого раза не получилось.

С авторизацией звонков поступил просто – беру “доверенные” номера телефонов из первых трёх ячеек SIM карты. Модуль будет отвечать на звонки только с этих номеров и отправлять СМС на последний набранный номер.

Пришлось повозиться с проблемой связи между SIM800 и bluepill. Казалось бы, обычный RS232 и всё должно было бы сразу запуститься. Но нет, при независимом перезапуске или подаче питания на SIM800 и МК связь часто терялась. Пришлось использовать для устойчивого коннекта последовательную программную перезагрузку сначала SIM800, а потом, в случае неуспеха, и bluepill.

Что не понравилось

Больным вопросом оказалось быстрое распухание кода бинарника. Строчек 200 кода в отладочной сборке ещё влезали во флеш, а потом всё. Практически сразу пришлось установить несколько опций оптимизации и отлаживать уже релизную версию. Большой скачок в использовании памяти произошёл, как только я стал использовать математику с плавающей точкой. Библиотеки fp занимают около 9 кб, которые сразу прибавились к бинарнику. Точнее, всё перестало просто собираться и я не знал, что делать дальше. Помог stackoverflow, где посоветовали поставить опцию opt-level = "z". Только с ней и удалось сделать сборку. При этом финальный размер бинарника составил 45 килобайт при 1300 строках исходного кода. Не могу оценить эффективность компилятора, но такой размер не проблема даже для bluepill, где есть 64 Кб флеша.

Паттерны проектирования ООП. Тут у меня не сложилось со state machines. Хотелось бы всё сделать по канонам «лучших паттернов проектирования», тем более в коде я использую несколько машин состояний. Но вот плодить десяток структур и типажей, которые при переходе состояний пожирают друг друга мне совсем не понравилось. Отдельно стоит упомянуть отсутствие доступа в embedded к куче и, как следствие, таких вещей как Box<> и прочих контейнеров. Сейчас весь код машины состояний в “С” стиле умещается на странице, прост и понятен. Разубедите меня, если сможете.

Использование Result<> для возврата ошибок. Наверное это удобно, когда стек вызовов глубокий и активно применяется оператор “?”. В моем же случае всё довольно тривиально, а использование Result<> с разными типами ошибок только всё усложняет. Умные книги советуют для унификации передачи кодов ошибок опять же применять Box<>, которого в embedded нет. Ну или просто ради него не хочется делать свой аллокатор кучи и получать связанные с этим потенциальные проблемы.

Небольшой лайфхак

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

Код скетча
#include <SoftwareSerial.h>

SoftwareSerial mySerial(10, 11); // RX, TX
String stringOne = "";
unsigned long myTime;

void setup() {
  mySerial.begin(2400);
}

void loop() { // run over and over  
  stringOne = "";
  myTime = millis();
  for(;;) {
    if (mySerial.available()) {
      char c = mySerial.read();
      Serial.write(c);
      stringOne += c;
      //Serial.print("char read \r\n");
      if (c == '\r' || stringOne.length() > 100) {break;};
      myTime = millis();
    } else {
      if (millis() - myTime > 200)
        break;
    };
  }

  if (stringOne == "Q1\r") {
    if (millis() % 2) {
      mySerial.print("(216.6 216.6 219.6 000 50.0 2.22 48.0 00000001\r");
    } else {
      mySerial.print("(219.6 219.6 000.0 000 50.0 2.22 48.0 10000001\r");
    }
  };  
}

Работа с эмулятором оказалась очень удобна для отладки. Рекомендую.

Итого

Всё задуманное получилось! По ходу дела не обошлось без проблем, но где их нет? Можно сказать, что связка технологий вполне рабочая и подходит для DIY.

Теперь модуль присылает мне такие сообщения:

А вот и обещанная ссылка на исходники в GITHUB.

Комментарии (32)


  1. amarao
    11.12.2021 14:46
    +13

    Вы игнорируете лучшее в Rust. Типажи бесплатны в бинарном виде, но позволяют описать ожидания от входного типа без учёта его конкретики. В зависимости от того, что важнее - скорость или размер бинарного файла, вы можете использовать динамические трейты (dyn traits) или статические. Статические быстрее, но увеличивают размер бинарного файла, динамические делают дополнительный lookup по таблице методов, зато не вызывают распухания из-за мономорфизации.

    Использование пустых типов для усиления типизации - это одна из важных особенностей языка. Вы можете объявить типы без значений и требовать их себе на вход для обеспечения соблюдения протоколов вызова (например, можно иметь Token(), в отсутствтие которого вызов не возможен, а ownership которого позволяет автоматически избежать race conditions).

    Мой совет, нырнуть глубже во все эти Vec'и и сплайсы - в них сила, даже если в финале у вас не будет Vec'а.


    1. lesha108 Автор
      11.12.2021 14:55

      Спасибо! динамические типы dyn точно работают при отсутствии кучи?


      1. Frederica_Bernkastel
        11.12.2021 17:44
        +3

        &dyn Trait
        fn f(x: &dyn std::fmt::Display) {
          println!("{}", x);
        }
        
        fn main() {
          f(&"test");
          f(&1);
        }


  1. V1tol
    11.12.2021 15:56
    +5

    Использование Result<> для возврата ошибок. Наверное это удобно, когда стек вызовов глубокий и активно применяется оператор “?”

    Так же оператор ? можно использовать вместе с Optional, если ошибки не важны а нужно только наличие результата.

    Умные книги советуют для унификации передачи кодов ошибок опять же применять Box<>, которого в embedded нет.

    А ещё можно для этого использовать кастомный Enum, опционально для удобства обьявить его как ошибку (c помощью трейта Error) и оборачивать им все остальные ошибки.

    enum MyErrorCode {
        Failed = 1,
        ...
    }
    
    enum MyError {
        CodeError(MyErrorCode),
        ATError(ATLibraryError),
        STMError(STMLibraryError),
        ...
    }
    
    impl Error for MyError {
        ...
    }


    1. lesha108 Автор
      11.12.2021 16:10

      Интересная мысль, попробую


    1. mayorovp
      11.12.2021 19:39
      +4

      Добавлю: если реализовать для MyError типажи From<ATLibraryError> и From<STMLibraryError> — оператор "?" учтёт возможность такого преобразования.


      1. V1tol
        11.12.2021 20:51

        Ну я уже не стал перегружать деталями. В своих проектах я даже макрос писал, который генерировал сам enum, делал From для содержимого и прочие дополнительные description методы.


        1. Fenex
          11.12.2021 21:11
          +3

          Крейт `thiserror` это самое и делает из коробки, очень удобно.


          1. lesha108 Автор
            11.12.2021 21:32

            thiserror, как я понимаю, требует std, а она недоступна в embedded


          1. V1tol
            11.12.2021 21:34

            thiserror не всегда подходит, да я и не против потренироваться самому макросы писать.


  1. cepera_ang
    11.12.2021 17:39
    +3

    Про оптимизацию размера бинарников есть такая репа с советами.


    1. cepera_ang
      11.12.2021 18:48
      +10

      В частности, cargo bloat показывает, что половина бинарника состоит из стандартных функций форматирования (первые 12 функций форматирования в топе по размеру занимают 50% от общего файла):


      Заголовок спойлера
          Analyzing target\thumbv7m-none-eabi\release\sim800_ups_monitor
      
      File  .text    Size              Crate Name
      1.1%  20.9%  8.6KiB                std core::fmt::float::float_to_decimal_common_shortest
      0.8%  16.5%  6.8KiB                std core::fmt::float::float_to_decimal_common_exact
      0.1%   1.8%    750B                std core::str::slice_error_fail
      0.1%   1.6%    656B                std core::fmt::Formatter::write_formatted_parts
      0.1%   1.4%    598B                std <core::fmt::builders::PadAdapter as core::fmt::Write>::write_str
      0.1%   1.4%    588B                std core::fmt::num::<impl core::fmt::Debug for usize>::fmt
      0.1%   1.3%    564B                std core::fmt::Formatter::pad
      0.1%   1.3%    556B                std core::str::converts::from_utf8
      0.1%   1.1%    454B                std core::fmt::Formatter::pad_integral
      0.0%   0.9%    382B                std core::fmt::write
      0.0%   0.9%    370B                std core::fmt::Formatter::pad_formatted_parts
      0.0%   0.7%    298B                std core::fmt::builders::DebugStruct::field

      Тут обсуждается проблема и предлагаются решения.


      PS Как же всё-таки несравненно круто работать с растом. Чужой проект на совершенно другую архитектуру кросс-компилируется с первой попытки, без возни с зависимостями, мейкфайлами и кучей уникальных ошибок, которые возникают только на твоей уникальной конфигурации машины.


      1. cepera_ang
        11.12.2021 20:59
        +3

        Добавление в .cargo/config/


        [unstable]
        build-std = ["core"]
        build-std-features = ["panic_immediate_abort"]

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


        Заголовок спойлера
        section               size        addr
        .vector_table          304   134217728
        .text                23424   134218032
        .rodata               3328   134241456
        .data                    0   536870912
        .bss                    16   536870912
        .uninit                  0   536870928
        
            Analyzing target\thumbv7m-none-eabi\release\sim800_ups_monitor
        
        File  .text    Size              Crate Name
        0.4%  22.2%  5.1KiB sim800_ups_monitor sim800_ups_monitor::__cortex_m_rt_main
        0.2%   7.7%  1.8KiB               core core::fmt::float::float_to_decimal_common_exact
        0.1%   7.5%  1.7KiB               core core::fmt::float::float_to_decimal_common_shortest
        0.0%   2.4%    556B sim800_ups_monitor sim800_ups_monitor::TempControl::measure
        0.0%   2.0%    472B               core core::str::converts::from_utf8
        0.0%   1.9%    456B               core <core::fmt::builders::PadAdapter as core::fmt::Write>::write_str
        0.0%   1.9%    438B  compiler_builtins compiler_builtins::int::specialized_div_rem::u64_div_rem
        0.0%   1.6%    384B sim800_ups_monitor sim800_ups_monitor::Sim800::send_sms
        0.0%   1.5%    348B sim800_ups_monitor sim800_ups_monitor::Ups::measure
        0.0%   1.3%    312B  compiler_builtins compiler_builtins::float::mul::__mulsf3
        0.0%   1.3%    304B               core core::fmt::Formatter::pad_integral
        0.0%   1.3%    302B               core core::num::flt2dec::strategy::grisu::format_shortest_opt::round_and_weed
        0.0%   1.2%    284B               core core::fmt::write
        0.0%   1.1%    264B          eeprom24x eeprom24x::eeprom24x::<impl eeprom24x::Eeprom24x<I2C,PS,AS>>::read_byte
        0.0%   1.1%    260B               core <[T] as core::fmt::Debug>::fmt
        0.0%   1.1%    260B sim800_ups_monitor sim800_ups_monitor::Sim800::send_at_cmd_wait_resp
        0.0%   1.1%    248B               core core::fmt::Formatter::write_formatted_parts
        0.0%   1.1%    246B               core core::fmt::Formatter::pad
        0.0%   1.0%    244B      stm32f1xx_hal stm32f1xx_hal::adc::Adc<stm32f1::stm32f103::ADC1>::convert
        0.0%   1.0%    236B               core core::fmt::Formatter::pad_formatted_parts
        0.0%   0.9%    220B               core core::num::flt2dec::digits_to_dec_str
        0.0%   0.9%    220B               core core::num::flt2dec::strategy::grisu::format_exact_opt::possibly_round
        0.0%   0.9%    212B               core core::num::bignum::Big32x40::mul_digits::mul_inner
        0.0%   0.9%    212B sim800_ups_monitor sim800_ups_monitor::V220Control::measure_temp
        ...


        1. lesha108 Автор
          11.12.2021 21:10

          отлично!


          1. ToSHiC
            12.12.2021 02:11
            +2

            И ещё почти 4 килобайта можно сэкономить, если перейти с флотов на fixed point, 16.16 вам за глаза должно хватить.


  1. Indemsys
    11.12.2021 19:08

    Что то пропустил видимо в статье, но не понял чем компилируется исходник на Rust и чем отлаживается?


    1. lesha108 Автор
      11.12.2021 19:35
      +1

  1. m0tral
    11.12.2021 19:49
    +1

    Так и не увидел выводов: изначальный замысел в стабильности в embedded у вас вышел относительно С? Вообще слабое звено ч тут вижу SIM800, как и другие версии этих модулей.


    1. lesha108 Автор
      11.12.2021 19:52

      Да, я думаю у меня были косяки в коде на C. Длительные испытания планирую с весны, сейчас старый модуль работает. Там видно будет, достигну стабильности или нет


  1. TIGER535
    12.12.2021 09:52

    У меня есть вопрос, а зачем использовать Rust в таком небольшом проекте? Тут и чистого си вполне достаточно. Не то чтобы я критикую, интересна мотивация.

    Вопрос снят, нашёл в статье - из интереса и попробовать


    1. Indemsys
      12.12.2021 12:40

      Заметил вот такое высказывание из книги по ссылке

      Panicking is a core part of the Rust language.

      Похоже на что-то принципиально новое.


      1. cepera_ang
        12.12.2021 12:53

        Да, контролируемая паника вместо случайных проездов по памяти и разрушения всего вокруг — это и правда похоже на что-то принципиально новое.


        1. staticmain
          12.12.2021 18:52
          -2

          Есть мнение, что падение С-программы из-за SIGSEGV на «голом» железе и panic RUST там же ничем не отличаются — потому что на «голом» железе нет логов, пользователя, экрана, принтера, в общем любого способа вывода информации о том, почему произошла паника.


          1. lesha108 Автор
            12.12.2021 18:55
            +2

            На расте просто меньше вероятность написать программу, приводящую к разрушению памяти


            1. staticmain
              13.12.2021 11:52

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


              1. mayorovp
                13.12.2021 12:32
                +1

                Не придётся, благо занимающаяся этим группа не сидела без дела рассказывая какой замечательный Rust сам по себе, а давно придумала PAC (Peripheral Access Crate).


                Как и зачем оно устроено можно почитать тут: https://docs.rust-embedded.org/book/peripherals/index.html


                1. staticmain
                  13.12.2021 13:43
                  -2

                  а давно придумала PAC (Peripheral Access Crate).


                  Я открыл исходники. Магии не существует, там везде unsafe, единственный метод обращаться к конкретным адресам и разыменовывать их в Rust.


                  1. indestructable
                    13.12.2021 13:52
                    +3

                    Крейт ансейф, но его использование безопасно


          1. mayorovp
            12.12.2021 19:04
            +3

            Таки отличаются — если на железе нет MMU, то, скорее всего, никакого SIGSEGV не будет в принципе, и программа будет работать непредсказуемо.


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


    1. DarkEld3r
      12.12.2021 18:38

      У меня есть вопрос, а зачем использовать Rust в таком небольшом проекте?

      А почему размер проекта имеет значение? Если бы код писался на С++, а не на расте вопрос всё равно возник бы? Ну или можно вывернуть другую сторону: "зачем использовать С в таком небольшом проекте, разве асма недостаточно?"


      Признаюсь честно, я предвзят, но спрашиваю тоже без критики и агрессии. (:


      1. TIGER535
        12.12.2021 18:59
        -1

        В данном случае всё более менее норм, и против плюсов на МК ничего не имею, когда они применяются осознанно, с пониманием как это делать правильно и что можно а что недопустимо на МК, просто иногда веселит когда говорят, "мы тут решили помигать парой диодиков и прочитать ацп, с помощью ***какая-то новомодная фича***, но 256кБ это слишком мало для этого =)

        п.с. можно сразу отдельный комп поставить на I7


        1. WGH
          12.12.2021 19:45
          +3

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