Измерение на Ардуино, получение, сохранение в текстовый файл и визуализация данных в среде Lazarus (язык pascal)

В данной статье рассмотрим решение задачи построения измерительной системы на Ардуино. Её задачами является:
1. Измерение аналогово (или цифрового) сигнала с датчика; 
2. Отправка на персональный компьютер (ПК) в виде строки по интерфейсу USB; 
3. Получение строки в программе Lazarus и выделение из него отправленного числа;
4. Визуализация полученных чисел в форме графика в зависимости от времени;
5. Сохранение в текстовый файл.
Надо отметить, что в Интернет сложно найти комбинированную информацию по этой задаче!

Для начинающих крайне рекомендуется вначале ознакомиться с предыдущими статьями по теме программирования в Lazarus:

ВВОД, СОХРАНЕНИЕ В ТЕКСТОВЫЙ ФАЙЛ И ВИЗУАЛИЗАЦИЯ ДАННЫХ В СРЕДЕ LAZARUS

ВВОД, СОХРАНЕНИЕ В БИНАРНЫЙ ФАЙЛ И ВИЗУАЛИЗАЦИЯ ДАННЫХ В СРЕДЕ LAZARUS

Сделано в последней на данный момент версии Lazarus 2.0.8 (в других вервиях возможны незначительное изменения)
Для доступа к последовательному порту будет использован модуль synaser. Файлы synaser.pas, synafpc.pas, synautil.pas, нужно поместить в папку с проектом.

Вначале необходимо создать пустой проект и настроить его в меню Проект -> Параметры проекта. 
Сохранить проект в отдельной папке: Проект -> Сохранить проект как…
На форму поместить следующие элементы:
1. TEdit (из вкладки Standart) – Edit1 для ввода имени файла 
2. TLabel (из вкладки Standart) – Label1 для отображения надписи “Файл”, Label2 для отображения надписи “Порт”
2. TButton (из вкладки Standart) – StartB для старта мониторинга порта, StopB для остановки мониторинга
3. TToggleBox (из вкладки Standart) – RecTB кнопка с фиксацией для разрешения записи в файл
3. TChаrt (из вкладки Chart)
Должно получиться так: 

Далее нужно создать серию для визуализации: Правой кнопкой мыши по графику -> Редактор диаграммы -> Добавить -> График

Теперь нужно создать обработку событий в исходном коде программы.
Понадобятся три процедуры:
1. Инициализации procedure TForm1.FormShow(Sender: TObject); 
В инспекторе объектов выбрать Form1 -> вкладка События -> OnShow (нажать …) Функция автоматически создается средой 
2. Обработка нажания на кнопку Старт procedure TForm1.StartBClick(Sender: TObject);
На форме совершить двойное нажатие на кнопку StartB 
3. Обработка нажания на кнопку Стоп procedure TForm1.StopBClick(Sender: TObject);
На форме совершить двойное нажатие на кнопку StopB

Для доступа к компонентам порта нужно дописать в раздел uses исходного кода модуля: synaser,jwawinbase, jwawinnt.

Затем нужно задать объект доступа к порту (ser: TBlockSerial;) и признак разрешения мониторинга порта (mon:boolean;). Все это объявляется в разделе глобальных переменных var (перед implementation).

Теперь необходимо принять протокол передачи информации. Например, такой: Начало посылки отмечается символом “$”, потом идет измерение первого канала символами, затем отмечаем окончание этого измерения символом “;”, если требуется отправить большее число измерений каналов, то тут можно добавлять еще измерения, разделяя их “;”. 
Посылка заканчивается невидимыми символами окончания строки и возврата каретки, которые тоже отправляются в порт. Поэтому всегда используется символ начала посылки, а так как число символов в одном измерении заранее не известно, то приходится примерять символ окончания измерения.
Как можно заметить протокол передачи данных в текстовом режиме не отличается экономностью, однако легко читается глазами в мониторе порта без перекодировки. Коротко выглядит так: $DDDD; (где D – разряд десятичного числа)

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

Полный исходный код Pascal модуля Lazarus с пояснениями представлен ниже:

unit Unit1;
 
{$mode objfpc}{$H+}
 
interface
 
uses
  Classes, SysUtils, FileUtil, TAGraph, TASeries, Forms, Controls, Graphics,
  Dialogs, StdCtrls,
  synaser,jwawinbase, jwawinnt; // модули подключения последовательного порта
 
type
 
  { TForm1 }
 
  TForm1 = class(TForm)
    Chart1LineSeries1: TLineSeries;
    Edit1: TEdit;
    Label1: TLabel;
    Label2: TLabel;
    StopB: TButton;
    StartB: TButton;
    Chart1: TChart;
    COMselectCB: TComboBox;
    RecTB: TToggleBox;
    procedure FormShow(Sender: TObject);
    procedure StartBClick(Sender: TObject);
    procedure StopBClick(Sender: TObject);
  private
 
  public
 
  end;
 
var
  Form1: TForm1;
  ser: TBlockSerial; // Переменная доступа к последовательному порту
  mon:boolean; // переменная хранит признак мониторинга порта
 
implementation
 
{$R *.lfm}
 
{ TForm1 }
// Инициализация
procedure TForm1.FormShow(Sender: TObject);
var
i:Integer;   // переменная текущего номера порта
Phandle:Thandle; // объект доступа к порту
begin
     // Процедура определения занятого порта
     for i:=1 to 30 do  // цикл перебора номеров порта от 1 до 30
     begin
          // Создание соединения с последовательным портом номер i
          Phandle:=CreateFile(Pchar('COM'+intToStr(i)), Generic_Read or Generic_Write,0,nil, open_existing,file_flag_overlapped,0);
          // Проверка на доступность соединения
          if Phandle<>invalid_handle_value then // если доступно соединение
          begin
               COMselectCB.Items.Add('COM'+ IntToStr(i)); // Добавить порт в список доступных
               CloseHandle(Phandle); // Закрыть соединение
          end;
     end;
     COMselectCB.ItemIndex:=COMselectCB.Items.Count-1; // Выбрать последний доступный порт
     mon:=false; // Запретить мониторинг порта
end;
// Кнопка старт
procedure TForm1.StartBClick(Sender: TObject);
const
  Fs = 200; // Установка частоты дискретизации АЦП
var
    fid:TextFile; // Переменнаы доступа к файлу
    data:String; // Строка для хранения считанных данных из порта
    value:AnsiString; // Строка хранения текущей пачки данных из порта
    waiting:integer; // переменная для хранения количества полученных бит в буфере чтения
    val:Double;// Переменная для хранения принятого числа
    st:string; // Строка для хранения вырезанных символов с принятым числом
    t:Double; // Текущая точка времени в сек
begin
  ser:= TBlockSerial.Create; // Создание объекта последовательного порта в памяти
  ser.RaiseExcept := True; // Установка прерывания в случае ощибок соединения
  ser.LinuxLock := False; // это требуется для работы в Linux.
  AssignFile(fid,Edit1.Text+'.csv'); // Связать файловую переменную с именем файла имя.csv, имя получается из поля ввода Edit1
  Rewrite(fid);  // Создается или перезаписывается выбранный файл
  ser.Connect(COMselectCB.Text); // открытие порта
  ser.Config(19200, 8, 'N', 0, false, false); // указываются параметры передачи данных Ардуино (скорость 19200 bpm, 8бит в пакете, без контроля четности)
  mon:=true; // Разрешить мониторинг порта
  Chart1LineSeries1.Clear; // Очистка графика
  Chart1.Extent.XMin:=0;  // Установка точки отсчета на графике времени от 0 сек
  Chart1.Extent.XMax:=10; // Установка предела по оси времени на 10 сек
  data:=''; // Очистка строки
  t:=0; // Обнуление времени
  while mon do  // Цикл пока разрешен мониторинг
  begin
       sleep(100); // задержка 100 мс для срабатывания устройства
       waiting:=ser.WaitingData; // сохранение в переменную сколько символов ждут во входном порту
       SetLength(value,waiting); // Установить размер строки value равной количеству ждущих бит waiting
       ser.RecvBuffer(@value[1], waiting); // Получение всех ждущих бит из буфера чтения порта
       data:=data+value; // добавление полученных данных в хранилище принятых данных data для обработки
       // Обработка данных (в т.ч. парсинг) принятых данных
       while pos(';',data)>0 do // Цикл пока в строке data есть символ разделитель значений
       begin
          st := copy(data, pos('$',data)+1, pos(';',data)-4); // Копирование из строки data символов между метками начала пакета $ и разделителя ;
          data:= copy(data,pos(';',data)+1, length(data)-pos(';',data)+1); // Копирование в строку data всех символов, кроме скопированных
          val:=StrToFloatDef(st,-1); // Перевод принятых и вырезанных символов в число
          if RecTB.Checked then Writeln(fid,FloatToStr(val)); // Если переключатель записи активирован - записать в файл
          t:=t+1/Fs; // Добавить следующее время исходя из установленной Fs. Следующее время увеличивается на период равный обратный частоте дискретизации.
          if val>=0 then Chart1LineSeries1.AddXY(t,val); // Если переведенное число больше 0, то вывод его на график.
          // Просмотр графика с фиксированным временным окном
          if t> Chart1.Extent.XMax then // Если текущее время стало больше установленого предела на графике, то ...
          begin
            Chart1.Extent.XMax:=t;  // Установить текущий максимум равный текущему времени
            Chart1.Extent.XMin:=t-10; // Установить текущий минимум равный текущему времени - 10 сек (окно времени)
            Chart1LineSeries1.Delete(0); // Удаление первого элемента в серии графика
          end;
       end;
      Application.ProcessMessages; // Позволить приложению отобразить новые данные
  end;
  CloseFile(fid); // Закрыть файл
  ser.CloseSocket; // Закрыть порт
  FreeAndNil(ser); // Освободить и удалить объект порта
end;
// Кнопка стоп
procedure TForm1.StopBClick(Sender: TObject);
begin
  mon:=false; // Запретить мониторинг порта
end;
 
end.
Теперь нужно собрать тестовый макет на базе Ардуино. Здесь представлен пример на Arduino UNO, однако должно работать на всех базовых моделях.

В среде Arduino IDE необходимо создать новый проект: Файл -> Новый
Затем сохранить его: Файл -> Сохранить как…
Затем можно написать программу для микроконтроллера.
Данная программа должна через строго определенное время выполнять аналого-цифровое преобразование (АЦП) и отправлять посылку в порт по определенному выше протоколу. Внимание! Использование обычной задержки delay не обеспечивает пригодной точности задания частоты дискретизации.

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

Код Arduino:

// Объявление внешних подключений
const int analogInPin = A0;  // Разъем для подключения аналогового датчика
const int Fs = 200;          // Частота дискретизации сигнала (Гц)
// Объявление переменных
int sensorValue = 0;         // Результат измерения
uint32_t ms_old = 0;         // Время предыдущего измерения
// Процедура инициализации
void setup() {
  Serial.begin(19200);          // Настройка скорости передачи на ПК (бод)
  pinMode( analogInPin, INPUT );// Настройка ввода аналоговой части 
  ms_old = millis();          // Инициализировать предыдущее время измерения текущим временем в мс
}
// Главный цикл работы
void loop() {
    if (millis()-ms_old >= (1000/Fs)){ // Ожидание периода дискретизации
      sensorValue = analogRead(analogInPin); // Провести АЦП
      ms_old = millis(); // Сразу после АЦП нужно отметить время измерения
      // Реализация протокола передачи
      Serial.print('$'); // Отправить начало посылки в ПК
      Serial.print(sensorValue, DEC); // Отправить результат измерения в ПК
      Serial.println(';');  // Отправить конец одного измерения в ПК
      // Сюда можно добавить еще несколько измерений через ";"
 
    }
}

После этого нужно подключить макет к персональному компьютеру и не забыть выбрать правильный порт Инструменты -> Порт, а также используемую версию Ардуино: Инструменты -> Плата.

Если при загрузке не возникло ошибок, можно проверить выходные данные в мониторе порта среды Ардуино (выбрать частоту передачи 19200).
Затем можно проверить программу в Lazarus (не забыв закрыть монитор порта!).

Проверяем программу:
Нажав Старт начнется мониторинг сигнала, нажав после на Запись, начнется регистрация в файл. Если покрутить потенциометр, то график будет изменяться. По окончании нужно нажать на Стоп.

Если нажималась Запись, то в папке с проектом появится файл data.csv. Его можно открыть в текстовом редакторе или в табличном процессоре типа Excel. 

Все работает! 

При возникновении вопросов, пишите в комментариях здесь или в сообществе ВК!

(с) Роман Исаков

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *