Мультиплексирование вывода данных на дисплей с параллельным портом

    Мультиплексирование шины данных дисплея с параллельным выводом и последовательного порта Ардуино.

    Статья описывает способ мультиплексного использования порта D микропроцессора ATMEL 328P (Ардуино НАНО) с целью обеспечения попеременного побайтного вывода в дисплей и обмена по последовательному каналу.

    Собрал я как-то прибор для контроля уровня угарного газа (СО) из ненужных элементов – дисплей от Нокии N95, Ардуино НАНО с несправными портами (D3 и D11, пробиты в результате неудачного замыкания на +400 вольт при отладке генератора высокого напряжения), платы воспроизведения звуковых фрагментов и датчика на угарный газ MQ7. Все эти детали в той или иной степени были неисправны (кроме датчика) и никакого применения в других проектах найти не могли. Как ни странно, оказалось, что прибор очень полезен при использовании печки на даче. Лето 2019 года выдалось нежарким и печку я топил практически каждый день в течении пары недель в июле, соединяя приятное (медитирование на пламя) с полезным (утилизацией попиленных мусорных деревьев). Контролировать режимы горения оказалось очень легко, все манипуляции с заслонками сразу отражались на показаниях прибора, что позволяло управлять печкой разумно. Прибор в этой статье не описывается, в интернете таких устройств предостаточно на любой вкус. Отличительной особенностью моего прибора является функция постоянного контроля исправности датчика СО на основе сравнения запомненной эталонной кривой и получаемой в реальном масштабе времени, а также высокая скорость реакции на изменение уровня СО, достигнутая сравнением запомненных на предыдущем цикле данных с текущими.

    Фокус этой статьи на увеличении скорости обмена процессора и дисплея с параллельным байтовым обменом данными.

    Дисплей имеет параллельный байтовый обмен и, несмотря на использование всех известных мне способов увеличения скорости обмена, вывод на него оказался довольно медленным. Основная причина – необходимость побитного вывода байта данных на разные биты разных портов, так как Ардуино Нано не имеет ни одного полноценного порта шириной в один байт. Этот режим вывода требует грубо в 8 раз больше времени по сравнению с командой записи байта в регистр. У НАНО имеется единственный полноценный порт D, но его младшие биты используются для аппаратного последовательного порта, по которому происходит загрузка скетчей в процессор и обмен скетча с хост-машиной.

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

    Предлагаемый способ позволяет значительно увеличить скорость интегрального обмена с дисплеем (около 3-х раз по моим измерениям). Под словом «интегральный» имеется в виду, что измерялись суммарные времена выполнения операций отрисовки экрана на макро-уровне. Вероятно, что измерение времен на уровне атомарных операций ввода-вывода дало бы существенно больший выигрыш (в районе одного порядка).

    Измерения выполнялись на специально собранном макете (см. рисунок 1) следующим способом:

    1. В тестовой программе расставлялись метки времени с выводом на хост-машину.
    2. Дисплей присоединялся к выводам D2 — D9, загружалась тестовая программа, в которой вывод байта осуществлялся путем распределения байта по битам.
    3. Дисплей присоединялся к выводам D0 – D7, загружалась тестовая программа, в которой вывод байта осуществлялся командой PORTD=data.

    image

    image
    Рисунок 1. Фотографии макета для отработки мультиплексирования вывода

    Программы совершенно одинаковые, переключение способа вывода осуществлялось сменой имен подпрограмм SendDat и SendCom на SendDat1 и SendCom1 соответственно.
    Вывод программы на встроенный сериал монитор записывался в OneNote и анализировался.

    image
    Рисунок 2. Измерение времени вывода на экран в режиме побайтного вывода

    image
    Рисунок 3. Измерение времени вывода на экран в режиме побитного вывода

    Результаты измерений сведены в таблицу 1.

    image
    Таблица 1. Интегральный выигрыш в скорости обмена

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

    Исследование мануала по процессору дало следующую информацию: включение режима последовательного порта перехватывает управление ножками D0 и D1 на аппаратном уровне. Это значит, что попытки управления ножками из скетча не дадут нужного результата.

    Дальнейшее изучение вопроса показало, что, если не включать в скетче последовательный порт командой Serial.open(), то весь порт D остается в распоряжении пользователя. Можно перевести порт в режим вывода по всем ногам командой DDRD=0xFF и выводить весь байт одновременно командой PORTD=data, где переменная data содержит выводимые данные.

    Перевести порт D в режим вывода достаточно один раз (в Setup). Последующие включения-выключения режима последовательного обмена не влияют на режим порта D – он так и остается в режиме параллельного вывода 8 бит. При включении режима последовательного обмена выводы D0 и D1 перейдут в режим приема и передачи соответственно. На выводе D1 появится «1» независимо от предыдущего состояния бита D1, и эта «1» будет на этом выводе все время пока включен режим последовательной передачи, кроме моментов передачи символов. При выключении режима последовательной передачи выводы D0 и D1 перейдут в состояние вывода и на них появятся сигналы из регистра вывода. Если в регистре вывода на месте D1 имеется «0», то на выводе будет сформирован отрицательный перепад, который приведет к передаче паразитного символа в последовательный канал.

    Рассмотрим теперь вопрос – а не помешает ли такое использование порта D загрузке программ? При загрузке программы процессор сбрасывается импульсом, который генерируется контроллером USB порта FT232RL (либо его аналогом CH340) при выставлении сигнала DTR. Сигнал DTR переходит из 1 в 0 и отрицательный перепад через конденсатор сбрасывает процессор. После сброса процессор включает последовательный порт, запускает загрузчик и принимает код программы. Итак – нормальной загрузке скетча изменение режима работы порта D не мешает.
    Если в скетче требуется вывод в сериал порт, то достаточно команды Serial.open() перед командами вывода.

    Однако есть тонкость. Заключается она в том, что вход RxD микросхемы FT232RL остается присоединенным к выводу TxD и данные, идущие на дисплей, принимаются и пересылаются далее в хост-машину. Данные эти выглядят как шум, хотя на самом деле им не являются (рисунок 4).

    image
    Рисунок 4. Вид экрана в режиме побайтного вывода без блокирования

    Бороться с этим ненужным сигналом можно двумя путями.

    Первый путь – программный. Заключается он в том, что в скетче перед выводом используется команда Serial.println() для создания новой строки перед выводом полезной информации. Это облегчит программе в хост машине анализ входящих строк и выделение полезной информации от скетча.

    Второй путь – аппаратный. Вход RxD FT232RL подсоединен к выходу TxD через резистор 1 кОм. Чтобы заблокировать передачу информации достаточно присоединить вход RxD FT232RL к «1». Сделать это проще всего одним из свободных выводов Ардуино. Я использовал вывод D8. Для выполнения этого действия я припаял к выводу 7 резистора RP1B номиналом 1 кОм проводок с разъемом на конце, проведя его через отверстия в плате с целью механической фиксации. На рисунке 5 это соединение показано красной линией, на рисунке 6 приведена фотография места пайки.

    image
    Рисунок 5. Часть схемы Ардуино нано

    image
    Рисунок 6. Место пайки дополнительного провода в Ардуино НАНО

    Механизм этот работает так: после сброса ножка D8 находится в режиме высокоимпедансного входа и штатной работе механизма загрузки программ в плату Ардуино не мешает.

    Когда в скетче надо начать управлять дисплеем, то вывод D8 переводится в режим активного вывода, на нем выставляется «1» (это блокирует передачу данных от вывода TxD Atmel328P на вывод RxD FT232RL) и после этого выполняется команда Serial.end();. Порядок действий важен, так как после выключения режима последовательной передачи на выводе TxD появится бит D1, который сохранился в выходном регистре порта D от предыдущей записи байта в этот порт. Если бит D1 был «0», то при выключении режима последовательной передачи ножка процессора переключится из «1» в «0» и это породит передачу паразитного символа по последовательному каналу.

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

    Когда в скетче требуется включить передачу данных по сериал порту, надо включить режим последовательной передачи и выключить блокировку прохождения сериал данных путем установки режима чтения на выводе D8.

    Для выполнения этих задач в скетч добавлены две подпрограммы:

    void s_begin()
    {
      Serial.begin(115200); // Включаем управление выводом TxD от модуля USART. Нога TxD переходит в "1", нога RxD становится входом
      pinMode(8, INPUT); // Отключаем подтяжку входа RxD FT232RL к "1", разрешая прохождение сериал данных на вход RxD FT232RL
     }
    

    void s_end()
    {
      Serial.flush(); //Ждем конца передачи 
      pinMode(8, OUTPUT); //Подтягиваем вход FT232RL к "1" отключая передачу данных пока идет управление дисплеем. Без этого 
      D8_High;            //будут передаваться паразитные данные 
      Serial.end(); // закрываем сериал канал. В этот момент на ноги TxD и RxD начинают выводится биты D0(RxD) и D1(TxD) порта D 
    }
    

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

    Подробнее
    Реклама

    Комментарии 39

      0

      Если Вы выводили в побитном режиме посредством digitalWrite, то конечно, функция весьма медленная из-за множества всяких проверок (да и с режимом обработки прерываний она вольно обращается :-)
      Если точно знать, что делать, то можно вот так оформить побитный вывод, который займет считанные машинные циклы (в проекте "нанокомпьютера", откуда этот отрывок, мне была важна именна неприкосновенность прерываний):


      void pinWrite(uint8_t pin, uint8_t val) {
        switch (pin) {
          case 3://1 PD3 0x0B
            if (val == 1) {
              asm volatile ("sbi 0x0B, 3 \n"); //HIGH
            }
            else {
              asm volatile ("cbi 0x0B, 3 \n"); //LOW
            }
            break;
          case 4://2 PD4
            if (val == 1) {
              asm volatile ("sbi 0x0B, 4 \n"); //HIGH
            }
            else {
              asm volatile ("cbi 0x0B, 4 \n"); //LOW
            }
            break; 
      ....
        0
        Я использовал макроподстановки типа #define D0_High PORTD |=B00000001, которые делают то же самое, что и предложенный Вами способ.
          0

          Нет, не совсем тоже самое.
          PORTD |=B00000001 эквивалентно PORTD = PORTD | B00000001, т.е. требуется дополнительная операция.


          Это можно проверить, отключив через platform.txt оптимизацию кода:


          запись в порт с учетом его состояния


          void setup() {
            // put your setup code here, to run once:
            PORTD |= 0x58;
          }
          void loop() {
            // put your main code here, to run repeatedly:
          }

          Скетч использует 514 байт (1%) памяти устройства


          прямая запись в порт


          void setup() {
            // put your setup code here, to run once:
            PORTD = 0x58;
          }
          void loop() {
            // put your main code here, to run repeatedly:
          }

          Скетч использует 506 байт (1%) памяти устройства


          А sbi и cbi меняют конкретные биты порта, совсем не трогая остальные.

            0
            Не буду спорить :) в любом случае параллельный вывод 1 байта быстрее 8-ми выводов бита с логическими операциями определения какой бит надо выставить.
              +1
              Команда PORTD |=B00000001 и будет оттранслирована компилятором в SBI, а PORTD |= 0x58 нет, так как затрагивает более одного бита. Во всяком случае, это точно работает с компилятором GCC, но в Arduino, вроде, он и используется.
                0

                Спасибо, интересно посмотреть, как g++ будет вести себя в этом случае в различных режимах оптимизации.

                  0
                  Ну, если интересно, то смотрите (правда я пользовался не g++, а gcc, но думаю в данном случае без разницы). Исходный код:
                  #include <avr/io.h>
                  
                  int main(void)
                  {
                  	PORTD |= 0x01;
                  
                  	return 0;
                  }
                  

                  Вообще без оптимизации (O0):
                  0000007a <main>:
                    7a:	cf 93       	push	r28
                    7c:	df 93       	push	r29
                    7e:	cd b7       	in	r28, 0x3d	; 61
                    80:	de b7       	in	r29, 0x3e	; 62
                    82:	8b e2       	ldi	r24, 0x2B	; 43
                    84:	90 e0       	ldi	r25, 0x00	; 0
                    86:	2b e2       	ldi	r18, 0x2B	; 43
                    88:	30 e0       	ldi	r19, 0x00	; 0
                    8a:	f9 01       	movw	r30, r18
                    8c:	20 81       	ld	r18, Z
                    8e:	21 60       	ori	r18, 0x01	; 1
                    90:	fc 01       	movw	r30, r24
                    92:	20 83       	st	Z, r18
                    94:	80 e0       	ldi	r24, 0x00	; 0
                    96:	90 e0       	ldi	r25, 0x00	; 0
                    98:	df 91       	pop	r29
                    9a:	cf 91       	pop	r28
                    9c:	08 95       	ret
                  

                  С любой включенной опцией оптимизации (O1, O2, O3, Os) получается одно и то же:
                  0000007a <main>:
                    7a:	58 9a       	sbi	0x0b, 0	; 11
                    7c:	80 e0       	ldi	r24, 0x00	; 0
                    7e:	90 e0       	ldi	r25, 0x00	; 0
                    80:	08 95       	ret
                  

                    0

                    ))))))) Мой результат на 9 минут раньше.

                      0
                      Так мне и лет больше. Зато у меня сравнение разных режимов оптимизации, как Вы и просили, а у Вас только O0 и Os.

                      Я контроллерами AVR редко пользуюсь, поэтому долго искал, где же у меня лежит среда разработки для них. А ведь еще параллельно и работать приходится.
                        0

                        image

                          0

                          P.S. Оптимизатор не всегда придерживается требуемой стратегии s
                          (хотя конечно gnu.org дает уклончивый ответ — "except those that often increase code size" :-)


                          PORTD |= 0x03;

                          дешевле (по размеру) было бы заменить (4 байта, 4 цикла)


                          sbi    0x0b, 0
                          sbi    0x0b, 1

                          а не: (6 байтов, но 3 цикла)


                          in    r24, 0x0b
                          ori    r24, 0x03
                          out    0x0b, r24 
                            0
                            Тут может быть гораздо более неприятный момент: две интсрукции sbi атомарны (каждая по отдельности) и не чуствительны к изменению в процессе выполнения других битов регистра из какого-нибудь прерывания. Чего нельзя сказать о конструкции in — ori — out. Именно в подобных случаях и приходится периодически контролировать код, генерируемый компилятором.
                    0

                    Так и есть при включенной оптимизации (результаты моей проверки):


                    PORTD = 0x01;

                    -O0


                      ae:    8b e2           ldi    r24, 0x2B    ; 43
                      b0:    90 e0           ldi    r25, 0x00    ; 0
                      b2:    21 e0           ldi    r18, 0x01    ; 1
                      b4:    fc 01           movw    r30, r24
                      b6:    20 83           st    Z, r18

                    Скрытый текст

                    -Os


                      a6:    81 e0           ldi    r24, 0x01    ; 1
                      a8:    8b b9           out    0x0b, r24    ; 11

                    PORTD |= 0x03;

                    -O0


                      ae:    8b e2           ldi    r24, 0x2B    ; 43
                      b0:    90 e0           ldi    r25, 0x00    ; 0
                      b2:    2b e2           ldi    r18, 0x2B    ; 43
                      b4:    30 e0           ldi    r19, 0x00    ; 0
                      b6:    f9 01           movw    r30, r18
                      b8:    20 81           ld    r18, Z
                      ba:    23 60           ori    r18, 0x03    ; 3
                      bc:    fc 01           movw    r30, r24
                      be:    20 83           st    Z, r18

                    -Os


                      a6:    8b b1           in    r24, 0x0b    ; 11
                      a8:    83 60           ori    r24, 0x03    ; 3
                      aa:    8b b9           out    0x0b, r24    ; 11
                    

                    PORTD |= 0x01;
                    

                    -O0


                      ae:    8b e2           ldi    r24, 0x2B    ; 43
                      b0:    90 e0           ldi    r25, 0x00    ; 0
                      b2:    2b e2           ldi    r18, 0x2B    ; 43
                      b4:    30 e0           ldi    r19, 0x00    ; 0
                      b6:    f9 01           movw    r30, r18
                      b8:    20 81           ld    r18, Z
                      ba:    21 60           ori    r18, 0x01    ; 1
                      bc:    fc 01           movw    r30, r24
                      be:    20 83           st    Z, r18

                    -Os


                      a6:    58 9a           sbi    0x0b, 0    ; 11
                    0
                    Проверил, заменил макроподстановки на прямой ассемблерный код, время цикла изменилось незначительно: вместо 213 стало 195 мС.
                  0
                  Как то давно для своих проектов использовал такие макросы
                  #define digitalWriteC(pin,val)\
                   if (val) { *((volatile uint8_t *) port_to_output_PGM[digital_pin_to_port_PGM[pin]]) |= (digital_pin_to_bit_mask_PGM[pin]);}\
                   else {*((volatile uint8_t *) port_to_output_PGM[digital_pin_to_port_PGM[pin]]) &= ~(digital_pin_to_bit_mask_PGM[pin]);}
                  
                  #define pinModeC(pin,mode)\
                    if (mode == INPUT) { \
                      *((volatile uint8_t *) port_to_mode_PGM[digital_pin_to_port_PGM[pin]]) &= ~(digital_pin_to_bit_mask_PGM[pin]);\
                      *((volatile uint8_t *) port_to_output_PGM[digital_pin_to_port_PGM[pin]]) &= ~(digital_pin_to_bit_mask_PGM[pin]);\
                    } else if (mode == INPUT_PULLUP) {\
                      *((volatile uint8_t *) port_to_mode_PGM[digital_pin_to_port_PGM[pin]]) &= ~(digital_pin_to_bit_mask_PGM[pin]);\
                      *((volatile uint8_t *) port_to_output_PGM[digital_pin_to_port_PGM[pin]]) |= (digital_pin_to_bit_mask_PGM[pin]);\
                    } else {\
                      *((volatile uint8_t *) port_to_mode_PGM[digital_pin_to_port_PGM[pin]]) |= (digital_pin_to_bit_mask_PGM[pin]);\
                    };
                  
                  inline uint8_t digitalReadC (uint8_t pin) __attribute__((always_inline));
                  uint8_t digitalReadC (uint8_t pin)
                   {
                     if (*((volatile uint8_t *) port_to_input_PGM[digital_pin_to_port_PGM[pin]]) & (digital_pin_to_bit_mask_PGM[pin])) {return HIGH;} else {return LOW;};  
                   };
                  


                  Работают корректно только если номера пинов и портов — константы. GCC всегда транслирует в sbi и cbi если это возможно. При этом Изменений в программе минимум. Всего то нужно использовать digitalWriteC вместо digitalWrite.
                    0

                    Забавно, что SBI/CBI таки не всегда быстрее, хотя и компактнее.

                  0
                  У Константина Чижова есть восхитительный шаблонный класс в его библиотеке mcucpp, который генерит минимально возможную последовательность записи данных в произвольный набор ножек.
                    0
                    Вот ещё один человек скоро прозреет от ардуиновых либ и окажется что все фокусы и приемы на ардуине дают 2-3 прирост а нормальный подход сразу дал бы увеличение скорости раз так в 30 не менее :)
                    Иногда таки полезно знать ассемблерные команды которые в один такт делают то что адруновая либа детает за примерно полторы тысячи тактов а результат то тот же.
                    Вы делаете по сути программную эмуляцию готовой апаратной функции а как все знают программная эмуляция всегда впринципе медленнее апаратной а тут ещё и ардуино который и в программных эмуляциях лидер по тормозам :)
                      +1
                      Поясните свою мысль: хотите ли Вы сказать, что команда PORTD=data транслируется в полторы тысячи тактов из-за ардуиновых либ? Я пока уверен, что эта команда транслируется в одну ассемблерную :)
                        0
                        А Ваша уверенность зиждется на анализе полученного в результате программного кода или на твердой уверенности в мощи ардуины?
                          0
                          Ну раз Вы отвечаете вопросом на вопрос — и я поступлю так же: Вы конечно же проанализировали уже, во что превращается команда PORTD=data и готовы показать обществу результат своих изысканий?
                            0
                            Я пока уверен, что эта команда транслируется в одну ассемблерную
                            Это вопрос был? Ну тогда ок, вопросов больше нет.
                            Значит, Вас точно не должно смущать, что формат вывода в порт у AVR выглядит так: OUT A,Rr, где Rr — регистр, а не данные. А уж во что там сконвертит GCC в итоге — это вообще дело десятое.
                              0

                              Как показала проверка, фантазии компилятору не занимать :-) — в одном случае он использует доступ к портам через общее адресное пространство (смещение на 0x20), а в другом — как к порта ввода-вывода.

                          0
                          digitalwrite транслируется в ту самую тыщу тактов :)
                          а portD=data явно не то что нужно! мы же не всем портом разом рулить хотим а только одним битом так? значить сначала надо отключить прерывания(в них ведь могут дергать порты и биты портов так?) потом считать кудато состояние порта на сейчас, потом применить битовую маску разную для установки или снятия бита, потом записать полученный байт назад в порт и снова разрешить прерывания… видите как не просто даже простое изменение бита? cbi sbi делает это все за один такт. Ещё вопросы?
                            0
                            Вы статью читали или просто по мотивам выступаете? :)

                            Я рулю всем байтом разом одной командой. Статья именно об этом: как на Нано использовать весь порт D одновременно.

                            Так понятно?
                        0
                        совсем не ясно Зачем вы накладываете на себя ограничения в виде полноценного порта?
                        ну нет порта: -возьмите полпорта В
                        ну будет две команды:
                        PORTD=dataD & maskD;
                        PORTB=dataB & maskB;
                        зато без «огорода»
                          0
                          Я захотел сделать байтовый вывод максимально быстро — т.е. одной командой. Вывод в два полубайта — две команды и они выполняются в два раза дольше.
                            0
                            Если Вы хотели сделать вывод максимально быстро, то не следует перед выводом каждого байта подтверждать состояние управляющей ноги.
                              0
                              Я не понял Вас. ПОясните. О какой управляющей ноге Вы пишете?
                                0
                                D11_High; в SendDat()
                                  0
                                  Эта нога управляет режимом Data\Command при управлении дисплеем.
                                  Обычно за байтом команды идут несколько байт данных.
                                  Действительно, наверно можно оптимизировать и выставлять ногу один раз.
                                  Подумаю на досуге :)
                                    0
                                    Проверил, перенес манипуляции D11 в SendCom:
                                    3.1 раза перешло в 3.6 раза.
                                0
                                дада дольше на 1 микросекунду
                                стоит ли однамиллионная секунды того огорода что вы нагородили?
                                стоит ли она того времени, что вы затратили на отладку мультиплексирования?
                                зачем усложнять? (возможно это вызов? я например для увлекательности, при решении, понижаю себе IQ, возможно без мультиплексирования не было бы этой статьи)
                                  0
                                  Это просто развлечение техногика. Исследование возможностей железки. Мне это доставляет удовольствие.
                                  Если же подходить к вопросу с точки зрения здравого смысла, то ни использованный процессор, ни дисплей не оптимальны. Сейчас использовать Ардуино Нано практического смысла нет — его прекрасно заменяет Blue Pill на STM32F103, у которого цена меньше, размер незначительно больше, процессор существенно мощнее.
                                    0

                                    Даже без эксперимента понятно, что вывод байта ассемблерным кодом будет работать быстрее, чем побитовый ногодрыг на С++ :)

                                      +1
                                      «Практика — критерий истины» (с) В.И.Ленин ;)
                                        –1
                                        Вот кто-кто, а В.И.Ленин уж лучше бы ограничился теоретическими измышлениями, а не проверял их на практике.
                              0
                              Спасибо, интересно, как раз думал поэкспериментировать с таким дисплеем…
                              А MQ-7 не используйте для таких целей, на что он реагирует и что измеряет это большой вопрос.)) Из всей этой серии как-то приспособил MQ2 под контроль утечки баллонного газа, помог найти утечку в подводных шлангах. А для таких серьезных вещей как СО\СО2 использую Winsen-овские датчики, они конечно подороже, но хоть понятно что измеряют.
                                0
                                Про свойства MQ7 я в курсе — некогда провел достаточно подробный цикл измерений в лаборатории газов с эталонными газами. ЕГо конечно нельзя использовать в качестве измерителя концентрации конкретного газа, но как показометр общего загрязнения — вполне можно.
                                Для контроля СО2 использую датчики К-30 на NDIR технологии.

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое