SIM800L + STM32 Bluepill + Rust. Как оно?

в 11:01, , рубрики: DIY, diy или сделай сам, embedded, Rust, SIM800, stm32, программирование микроконтроллеров
SIM800L + STM32 Bluepill + Rust. Как оно? - 1

Несколько лет назад сделал себе на 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 в необходимом мне объёме.

SIM800L + STM32 Bluepill + Rust. Как оно? - 2

Кстати, для 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 rn");
      if (c == 'r' || stringOne.length() > 100) {break;};
      myTime = millis();
    } else {
      if (millis() - myTime > 200)
        break;
    };
  }

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

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

Итого

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

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

SIM800L + STM32 Bluepill + Rust. Как оно? - 3

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

https://github.com/lesha108/sim800_ups_monitor

Автор:
lesha108

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js