اگر در بخش های قبلی ما را همراهی کرده باشید، قصد ساخت یک دیتالاگر مفید و کاربردی را با بردهای آماده آردینو را داشتیم. در بخش اول در مورد سخت افزار پروژه توضیح دادیم، در بخش دوم در مورد نرم افزار پروژه توضیح دادیم و پروژه را تکمیل کردیم. در این بخش کدهای ارائه شده را بررسی می کنیم.
با اجرای فایل DataLoggerMinimal کلاس Application اجرا می شود و دو متد setup و loop فراخوانی می شوند.
کلاس Application :
Application::Application()
: dht(3, DHT22), rtc(), modeSelector(), storage(), logSystem(0, &storage)
{
}
این کلاس تمام اجزای برنامه را به هم متصل کرده و یک فایل برای برنامه ایجاد می کند. این کلاس با ایجاد فضانام های مناسب از تداخل نام ها با یکدیگر جلوگیری می کند.
متد Setup :
void Application::setup()
{
// Initialize the serial interface.
Serial.begin(57600);
// Initialize all libraries
Wire.begin();
dht.begin();
rtc.begin();
modeSelector.begin();
storage.begin();
در ابتدای این متد تنظیمات اولیه اجزای برنامه صورت می گیرد. پس از این مرحله، بخش سرآمد (هدر) برای پورت سریال ارسال می شود :
// Write some initial greeting.
Serial.println(F(“Lucky Resistor’s Data Logger Version 1”));
Serial.println(F(“————————————–“));
Serial.flush();
در بخش بعدی فعال بودن کلاک سیستم بررسی می شود و در صورتی که کلاک غیر فعال باشد، به عنوان خطا با چشمک زدن LED به کاربر هشدار داده می شود :
if (!rtc.isrunning()) {
Serial.println(F(“Warning! RTC is not running.”));
signalError(3);
}
مد خواندن :
// Check the mode.
if (modeSelector.getMode() == ModeSelector::Read) {
Serial.print(F(“Read selected. Sending “));
const uint32_t numberOfRecords = logSystem.currentNumberOfRecords();
Serial.print(numberOfRecords);
Serial.println(F(” records.”));
for (uint32_t i = 0; i < numberOfRecords; ++i) {
LogRecord record = logSystem.getLogRecord(i);
record.writeToSerial();
}
Serial.println(F(“Finished successfully. Enter sleep mode.”));
Serial.flush();
اگر توسط کلید انتخاب حالت، حالت خواندن انتخاب شود، همه ی موارد ذخیره شده توسط پورت سریال خوانده می شود :
set_sleep_mode(B010); // Enter power-down mode.
cli(); // no interrupts to wake the cpu again.
sleep_mode(); // enter sleep mode.
پس از این مرحله، میکروکنترلر در مد توان پایین قرار می گیرد. همه ی وقفه ها توسط دستور cli() غیرفعال می شوند و میکروکنترلر فقط توسط وقفه های خارجی که توسط کلیدها فعال می شوند از حالت توان پایین خارج می شود.
مد فرمت :
} else if (modeSelector.getMode() == ModeSelector::Format) {
Serial.println(F(“Format (!) selected. Format is starting in ~10 seconds.”));
// using LED on pin 13 to blink aggresively.
pinMode(SIGNAL_LED, OUTPUT);
digitalWrite(SIGNAL_LED, LOW);
for (int8_t i = 1; i <= 10; ++i) {
Serial.print(i);
Serial.println(“…”);
Serial.flush();
for (int8_t j = 0; j < i; ++j) {
digitalWrite(SIGNAL_LED, HIGH);
delay(50);
digitalWrite(SIGNAL_LED, LOW);
delay(100);
}
delay(1000);
}
Serial.println(F(“Erasing all logged records…”));
logSystem.format();
Serial.println(F(“Format finished successfully. Enter sleep mode.”));
Serial.flush();
set_sleep_mode(B010); // Enter power-down mode.
cli(); // no interrupts to wake the cpu again.
sleep_mode(); // enter sleep mode.
با اجرای بخش اول این کد، LED شروع به چشمک زدن می کند. سپس متد فرمت فراخوانی می شود و پس از آن میکروکنترلر وارد حالت توان پایین می شود.
مد خواندن :
با وارد شدن به مد ذخیره سازی (log mode)، مطابق توضیحات بخش قبلی این آموزش، محاسباتی مانند تعداد موارد ذخیره سازی شده (record) و اطلاعات مفید دیگری محاسبه شده و از طریق پورت سریال به سمت کاربرد فرستاده می شود:
} else {
// Write about the logging mode.
Serial.print(F(“Logging selected. Interval = “));
Serial.println(modeSelector.getIntervalText());
Serial.print(F(“Maximum records: “));
Serial.println(logSystem.maximumNumberOfRecords());
Serial.print(F(“Current records: “));
Serial.println(logSystem.currentNumberOfRecords());
// calculate how long we can record data.
const uint32_t availableRecords = logSystem.maximumNumberOfRecords()-logSystem.currentNumberOfRecords();
Serial.print(F(“Avaliable records: “));
Serial.println(availableRecords);
const uint32_t recordingTime = (availableRecords*modeSelector.getInterval());
Serial.print(F(“Recording time: “));
sendDurationToSerial(recordingTime);
Serial.println();
Serial.print(F(“Current time: “));
_currentTime = rtc.now();
sendDateTimeToSerial(_currentTime);
Serial.println();
const DateTime recordingEndTime = DateTime(_currentTime.unixtime() + recordingTime);
Serial.print(F(“Recording end time: “));
sendDateTimeToSerial(recordingEndTime);
Serial.println();
در بخش بعدی کد پایه مربوط به LED خروجی تنظیم شده و LED خاموش می شود.
// Enable the red led as output.
pinMode(SIGNAL_LED, OUTPUT);
digitalWrite(SIGNAL_LED, LOW);
در مرحله بعدی، تایمر شماره دو میکروکنترلر را در پایین ترین سرعت شمارش تنظیم می کنیم. از این تایمر برای فعال کردن میکروکنترلر پس از وارد شدن به مد توان مصرفی پایین (sleep mode) استفاده می شود. در بخش بعدی زمان های تاخیر تایمر RTC را برای انجام عملیات ذخیره سازی بعدی بر روی 1/10 و با مقدار حداکثر 60 ثانیه تنظیم می کنیم.
// Keep the sleep interval between 1s and 1m
_sleepDelay = min(modeSelector.getInterval() / 10, 60);
سپس زمان اندازه گیری بعدی را تنظیم می کنیم.
// Set the next record time.
_nextRecordTime = DateTime(_currentTime.unixtime() + modeSelector.getInterval());
تنظیمات مد توان مصرفی پایین :
همان طور که در بخش اول این آموزش توضیح دادیم، هدف ما ساخت دیتالاگری است که قادر باشد به مدت طولانی (در حد چند سال!) بتواند بدون نیاز به تعویض باطری کار کند. برای این منظور باید توان مصرفی تاحد امکان کاهش پیدا کند. به منظور کاهش توان مصرفی از مد sleep میکروکنترلر استفاده می کنیم و از وقفه به منظور خارج شدن از این حالت استفاده می کنیم. به دلیل این که برد Pro Trinket وقفه خارجی ندارد، از تایمر RTC به این منظور استفاده می کنیم.
ابتدا روتین وقفه را ایجاد می کنیم:
// Create an empty interrupt for timer2 overflow.
// The interrupt is only used to wake from sleep.
EMPTY_INTERRUPT(TIMER2_OVF_vect)
مد Log و متد setup ، تایمر 2 را برای تولید سرریز تنظیم می کنیم :
// Prepare the timer2 to wake from sleep.
ASSR = 0; // Synchronous internal clock.
TCCR2A = _BV(WGM21)|_BV(WGM20); // Normal operation. Fast PWM.
TCCR2B |= _BV(CS22)|_BV(CS21)|_BV(CS20); // Prescaler to 1024.
OCR2A = 0; // Ignore the compare
OCR2B = 0; // Ignore the compare
TIMSK2 = _BV(TOIE2); // Interrupt on overflow.
sei(); // Allow interrupts.
بقیه تنظیمات در متد Powersave انجام می شود :
void Application::powerSave(uint16_t seconds)
{
// Go to sleep (for 1/60s).
SMCR = _BV(SM1)|_BV(SM0); // Power-save mode.
const uint32_t waitIntervals = (seconds*61); // This is almost a second.
for (uint32_t i = 0; i < waitIntervals; ++i) {
TCNT2 = 0; // reset the timer.
SMCR |= _BV(SE); // Enable sleep mode.
sleep_cpu();
SMCR &= ~_BV(SE); // Disable sleep mode.
}
}
متد LOOP :
در ابتدا مقادیر سنسور را دریافت می کنیم :
void Application::loop()
{
// Read the values from the sensor
const uint8_t humidity = dht.readHumidity();
const uint8_t temperature = dht.readTemperature();
زمان و تاریخ ذخیره سازی تنظیم می شوند :
// Write the record
LogRecord logRecord(_currentTime, temperature, humidity);
if (!logSystem.appendRecord(logRecord)) {
// storage is full
signalError(5);
}
سپس منتظر زمان بعدی برای ذخیره سازی می مانیم :
// Wait until we reached the right time.
while (true) {
powerSave(_sleepDelay);
_currentTime = rtc.now();
const int32_t secondsToNextRecord = _nextRecordTime.unixtime()-_currentTime.unixtime();
if (secondsToNextRecord<_sleepDelay) { if (secondsToNextRecord > 0) {
powerSave(secondsToNextRecord);
}
break;
}
}
در انتهای این متد، محاسبات مربوط به ذخیره سازی صورت می گیرد :
// Read the current time for the log entry.
_currentTime = rtc.now();
// Increase the next record time. This will keep the timing stable, even
// if we do not wake up precise at the right time.
_nextRecordTime = DateTime(_nextRecordTime.unixtime() + modeSelector.getInterval());
کلاس ذخیره سازی (Storage) :
در این بخش کدهای لازم برای ذخیره سازی داده های دریافت شده در حافظه نوشته می شود :
class Storage
{
public:
Storage();
~Storage();
public:
void begin();
uint32_t size();
uint8_t readByte(uint32_t index);
void writeByte(uint32_t index, uint8_t data);
};
در پروژه اول از کدهای کتابخانه حافظه EEPROM آردوینو به منظور پیاده سازی این متد استفاده می شود :
uint32_t Storage::size()
{
return EEPROM.length();
}
void Storage::writeByte(uint32_t index, uint8_t data)
{
EEPROM.update(index, data);
}
uint8_t Storage::readByte(uint32_t index)
{
return EEPROM.read(index);
}
در این کد از متد update به جای متد write به منظور کاهش تعداد دفعات نوشتن در EEPROM استفاده شده است.
کلاس انتخاب حالت (MODESELECT) :
در این کلاس کدهای مربوط به کلید انتخاب حالت و وقفه های زمانی لازم نوشته می شود. در ابتدا ثابت های مورد نیاز نوشته می شوند :
#define MODE_SELECTOR_PIN_D1 4
#define MODE_SELECTOR_PIN_D2 5
#define MODE_SELECTOR_PIN_D4 6
#define MODE_SELECTOR_PIN_D8 8
سپس متد را می نویسیم :
class ModeSelector
{
public:
enum Mode {
Log,
Read,
Format
};
public:
ModeSelector();
~ModeSelector();
public:
void begin();
Mode getMode();
uint32_t getInterval();
String getIntervalText();
private:
uint8_t _selectedValue;
};
مقادیر در متد begin و توسط getselectedvalue خوانده می شوند :
void ModeSelector::begin()
{
pinMode(MODE_SELECTOR_PIN_D1, INPUT_PULLUP);
pinMode(MODE_SELECTOR_PIN_D2, INPUT_PULLUP);
pinMode(MODE_SELECTOR_PIN_D4, INPUT_PULLUP);
pinMode(MODE_SELECTOR_PIN_D8, INPUT_PULLUP);
delay(100);
_selectedValue = getSelectedValue();
}
Getselectedvalue بسیار ساده نوشته شده است. چون از مقاومت Pull-Up میکروکنترلر استفاده شده است، سطح منطقی صفر به معنای 1 و سطح منطقی یک به معنای 0 است.
namespace {
uint8_t getSelectedValue()
{
uint8_t result = 0;
if (digitalRead(MODE_SELECTOR_PIN_D1) == LOW) {
result |= B0001;
}
if (digitalRead(MODE_SELECTOR_PIN_D2) == LOW) {
result |= B0010;
}
if (digitalRead(MODE_SELECTOR_PIN_D4) == LOW) {
result |= B0100;
}
if (digitalRead(MODE_SELECTOR_PIN_D8) == LOW) {
result |= B1000;
}
return result;
}
}
کلاس LOGSYSTEM :
از این کلاس به منظور خلاصه سازی فرایند خواندن و نوشتن مقادیر ذخیره شده استفاده می شود. شروع این بخش با تعریف ثابت مورد نیاز است :
#define LOG_SYSTEM_YEAR_BASE 2015
بخش بعدی مربوط به تعریف کلاس Log record است. از این بخش فقط به منظور ارسال مقادیر به سیستم و یا خواندن مقادیر استفاده می شود و چگونگی ذخیره سازی در حافظه در این بخش نوشته نشده است :
class LogRecord
{
public:
LogRecord(const DateTime &dateTime, int8_t temperature, uint8_t humidity);
LogRecord();
~LogRecord();
public:
bool isNull() const;
DateTime getDateTime() const;
int8_t getTemperature() const;
int8_t getHumidity() const;
void writeToSerial() const;
private:
DateTime _dateTime;
int8_t _temperature;
uint8_t _humidity;
};
متدهای لازم نیز در این بخش تعریف می شوند :
class LogSystem
{
public:
LogSystem(uint32_t reservedForConfig, Storage *storage);
~LogSystem();
public:
uint32_t maximumNumberOfRecords() const;
uint32_t currentNumberOfRecords() const;
LogRecord getLogRecord(uint32_t index) const;
bool appendRecord(const LogRecord &logRecord);
void format();
private:
uint32_t _reservedForConfig;
Storage *_storage;
uint32_t _currentNumberOfRecords;
uint32_t _maximumNumberOfRecords;
};
پیاده سازی :
پس از تعریف کلاس Log record ، یک استراکچر یا ساختار به شرح زیر تعریف می شود :
struct InternalLogRecord
{
uint8_t year : 7; // 00-99
uint8_t month : 4; // 0-11
uint8_t day : 5; // 0-30
uint16_t time : 14; // 0-8639 (multiply with 10 to get seconds per day)
uint8_t humidity : 7; // 0-100
int8_t temperature : 7; // -64 – +63
uint8_t crc : 4; //
};
متد زیر شروع ذخیره سازی بعدی در حافظه را تعیین می کند :
inline uint32_t getRecordStart(uint32_t offset, uint32_t index)
{
return offset + (sizeof(InternalLogRecord) * index);
}
دو متد بعدی، متدهای خام لازم برای انجام عملیات خواندن، نوشتن و پاک کردن را فراهم می کنند :
inline InternalLogRecord getInternalRecord(Storage *storage, uint32_t offset, uint32_t index)
{
InternalLogRecord record;
uint8_t *recordPtr = reinterpret_cast<uint8_t*>(&record);
uint32_t storageIndex = getRecordStart(offset, index);
for (uint8_t i = 0; i < sizeof(InternalLogRecord); ++i) { *recordPtr = storage->readByte(storageIndex);
++storageIndex;
++recordPtr;
}
return record;
}
inline void setInternalRecord(Storage *storage, uint32_t offset, InternalLogRecord *record, uint32_t index)
{
uint8_t *recordPtr = reinterpret_cast<uint8_t*>(record);
uint32_t storageIndex = getRecordStart(offset, index);
for (uint8_t i = 0; i < sizeof(InternalLogRecord); ++i) { storage->writeByte(storageIndex, *recordPtr);
++storageIndex;
++recordPtr;
}
}
void zeroInternalRecord(Storage *storage, uint32_t offset, uint32_t index)
{
uint32_t storageIndex = getRecordStart(offset, index);
for (uint8_t i = 0; i < sizeof(InternalLogRecord); ++i) { storage->writeByte(storageIndex, 0);
++storageIndex;
}
}
از reinterpret_cast به منظور بررسی ساختار داخلی به عنوان یک اشاره گر به آرایه ها استفاده می کنیم.
از الگوریتم CRC مطابق زیر به منظور حفاظت از داده ها استفاده می شود :
uint8_t getCRCForInternalRecord(InternalLogRecord *record)
{
uint16_t crc = 0xFFFF;
InternalLogRecord recordForCRC = *record;
recordForCRC.crc = 0;
uint8_t *recordPtr = reinterpret_cast<uint8_t*>(&recordForCRC);
for (uint8_t i = 0; i < sizeof(InternalLogRecord); ++i) { crc = _crc16_update(crc, *recordPtr); ++recordPtr; } return (crc>>12) ^ ((crc&0x0f00)>>8) ^ ((crc&0x00f0)>>4) ^ (crc&0x000f);
}
از isInternalRecordValid() به منظور بررسی داده ها برای اطمینان از صحیح بودن آنها استفاده می شود :
bool isInternalRecordValid(InternalLogRecord *record)
{
if (record->year > 99 ||
record->month > 11 ||
record->day > 30 ||
record->time > 8639 ||
record->humidity > 100) {
return false; // out of range.
}
const uint8_t crc = getCRCForInternalRecord(record);
return crc == record->crc;
}
بخش Constructor :
در این بخش، حافظه به منظور بررسی نمونه های ذخیره شده اسکن می شود تا به یک نمونه غیر صحیح برسیم. تعداد نمونه های صحیح درون حافظه RAM ذخیره سازی می شود. این فرایند ذخیره سازی مقداری زمان بر است اما باید توجه شود که سرعت عملکرد در سیستم های دیتالاگر اهمیت چندانی ندارد.
LogSystem::LogSystem(uint32_t reservedForConfig, Storage *storage)
: _reservedForConfig(reservedForConfig), _storage(storage), _currentNumberOfRecords(0), _maximumNumberOfRecords(0)
{
// Calculate the maximum number of records.
_maximumNumberOfRecords = (storage->size() – reservedForConfig) / sizeof(InternalLogRecord);
// Scan the storage for valid records.
uint32_t index = 0;
InternalLogRecord record = getInternalRecord(_storage, _reservedForConfig, index);
while (!isInternalRecordNull(&record)) {
if (!isInternalRecordValid(&record)) {
break;
}
++index;
record = getInternalRecord(_storage, _reservedForConfig, index);
}
_currentNumberOfRecords = index;
}
متد GET :
در این متد داده ها ثبت شده خوانده می شوند و برای LogRecord آماده می شوند.
LogRecord LogSystem::getLogRecord(uint32_t index) const
{
if (index >= _currentNumberOfRecords) {
return LogRecord();
}
InternalLogRecord record = getInternalRecord(_storage, _reservedForConfig, index);
const uint8_t hours = (record.time / 360);
const uint8_t minutes = (record.time / 6) % 60;
const uint8_t seconds = (record.time % 6) * 10;
uint16_t year = record.year + (LOG_SYSTEM_YEAR_BASE/100*100);
if (record.year < (LOG_SYSTEM_YEAR_BASE%100)) {
year += 100;
}
DateTime dateTime(year, record.month+1, record.day+1, hours, minutes, seconds);
return LogRecord(dateTime, record.temperature, record.humidity);
}
متد الحاق (Append) :
در این متد عملیات نوشتن record یا داده ها ذخیره شده صورت می پذیرد. در این فرایند ابتدا محل اندیس بعدی (index+1) پاک شده و سپس در اندیس فعلی (index) نوشته می شود. این فرایند دارای اطمینان بالاتری نسبت به روش های دیگر است.
bool LogSystem::appendRecord(const LogRecord &logRecord)
{
if (_currentNumberOfRecords >= _maximumNumberOfRecords) {
return false;
}
// zero the following record if possible
if (_currentNumberOfRecords+1 < _maximumNumberOfRecords) {
zeroInternalRecord(_storage, _reservedForConfig, _currentNumberOfRecords+1);
}
// convert the record into the internal structure.
InternalLogRecord internalRecord;
memset(&internalRecord, 0, sizeof(InternalLogRecord));
const DateTime dateTime = logRecord.getDateTime();
internalRecord.year = dateTime.year() % 100;
internalRecord.month = dateTime.month() – 1;
internalRecord.day = dateTime.day() – 1;
uint16_t timeValue = static_cast<uint16_t>(dateTime.hour()) * 360;
timeValue += static_cast<uint16_t>(dateTime.minute()) * 6;
timeValue += dateTime.second() / 10;
internalRecord.time = timeValue;
internalRecord.humidity = logRecord.getHumidity();
internalRecord.temperature = logRecord.getTemperature();
internalRecord.crc = getCRCForInternalRecord(&internalRecord);
setInternalRecord(_storage, _reservedForConfig, &internalRecord, _currentNumberOfRecords);
_currentNumberOfRecords++;
return true;
}
متد فرمت :
این متد تنها دو مورد ذخیره شده اول در حافظه را پاک می کند.
void LogSystem::format()
{
zeroInternalRecord(_storage, _reservedForConfig, 0);
zeroInternalRecord(_storage, _reservedForConfig, 1);
}
—————————————————————————————————-
امیدوارم این سه مجموعه آموزشی در مورد ساخت دیتالاگر توسط بردهای آردوینو برایتان مفید بوده باشد.
منبع :
https://luckyresistor.me
اگر این نوشته برایتان مفید بود لطفا کامنت بنویسید.