تطوير جهاز قياس IRIS

صورة

تحياتي مجتمع الهبر. أطلقت شركتنا مؤخرًا جهاز القياس والتحكم IRIS في السوق. بصفتي المبرمج الرئيسي لهذا المشروع ، أود أن أخبرك عن تطوير البرنامج الثابت للجهاز (وفقًا لمدير المشروع ، لا تزيد البرامج الثابتة عن 30 ٪ من إجمالي حجم العمل من الفكرة إلى الإنتاج الضخم). ستكون المقالة مفيدة بشكل أساسي للمطورين المبتدئين من حيث فهم تكاليف العمالة لمشروع "حقيقي" والمستخدمين الذين يريدون "النظر تحت غطاء المحرك".



الغرض من الجهاز



IRIS هو جهاز قياس متعدد الوظائف. يعرف كيف يقيس التيار (مقياس التيار) والجهد (الفولتميتر) والقوة (الواطميتر) وعدد من الكميات الأخرى. يتذكر KIP IRIS قيمهم القصوى ، ويكتب الذبذبات. يمكن العثور على وصف مفصل للجهاز على موقع الشركة.



قليلا من الإحصائيات



توقيت



الالتزام أولاً بـ SVN: 16 مايو 2019

الإصدار: 19 يونيو 2020.

* هذا هو وقت التقويم ، وليس التطوير بدوام كامل طوال الفصل الدراسي. كانت هناك انحرافات عن مشاريع أخرى ، وتوقعات بالمواصفات الفنية ، وتكرارات الأجهزة ، وما إلى ذلك.



يلتزم



الرقم في SVN: 928 من

أين يأتي هذا؟

1) أنا مؤيد للالتزام المصغر أثناء التطوير

2) التكرارات في الفروع للأجهزة والمحاكي

3) التوثيق

لذا فإن الرقم الذي يحتوي على حمولة على شكل كود جديد (فرع جذع) لا يزيد عن 300.

صورة



عدد سطور الكود



تم جمع الإحصائيات بواسطة الأداة المساعدة cloc مع المعلمات الافتراضية ، باستثناء مصادر HAL STM32 و ESP-IDF ESP32.

صورة

البرنامج الثابت STM32: 38334 سطرًا من التعليمات البرمجية. منها:

60870-5-101: 18751

ModbusRTU: 3859

Oscilloscope: 1944

Archiver: 955

ESP32 الثابتة: 1537 سطرًا من التعليمات البرمجية.



مكونات الأجهزة (الأجهزة الطرفية المعنية)



يتم تنفيذ الوظائف الرئيسية للجهاز في البرنامج الثابت STM32. البرنامج الثابت ESP32 مسؤول عن اتصالات Bluetooth. يتم الاتصال بين الرقائق عبر UART (انظر الشكل في الرأس).

NVIC هي وحدة تحكم بالمقاطعة.

IWDG - مؤقت الوكالة الدولية للطاقة لإعادة تشغيل الشريحة في حالة توقف البرنامج الثابت.

الموقتات - تحافظ مقاطعات المؤقت على نبضات قلب المشروع.

EEPROM - ذاكرة لتخزين معلومات الإنتاج والإعدادات والقراءات القصوى ومعاملات معايرة ADC.

I2C هي واجهة للوصول إلى شريحة EEPROM.

NOR - ذاكرة لتخزين أشكال الموجة.

QSPI هي واجهة للوصول إلى شريحة ذاكرة NOR.

RTC - تضمن ساعة الوقت الفعلي مسار الوقت بعد إيقاف تشغيل الجهاز.

ADC - ADC.

RS485 هي واجهة تسلسلية للاتصال عبر بروتوكولات ModbusRTU و 60870-101.

DIN ، DOUT - مدخلات ومخرجات منفصلة.

زر - زر على اللوحة الأمامية للجهاز لتبديل المؤشر بين القياسات.



هيكلة البرمجيات



وحدات البرامج الرئيسية



صورة



قياس تدفق البيانات



صورة



نظام التشغيل



مع الأخذ في الاعتبار قيود مقدار ذاكرة الفلاش (يقدم نظام التشغيل الحمل الزائد) والبساطة النسبية للجهاز ، فقد تقرر التخلي عن استخدام نظام التشغيل والتغلب على المقاطعات. تم بالفعل تسليط الضوء على هذا النهج في مقالات عن حبري أكثر من مرة ، لذلك سأقدم فقط مخططات انسيابية للمهام داخل المقاطعات مع أولوياتها.

صورة



عينة من الرموز. تأخر إنشاء المقاطعة في STM32.



//     6
  HAL_NVIC_SetPriority(CEC_IRQn, 6, 0);
  HAL_NVIC_EnableIRQ(CEC_IRQn);

//  
HAL_NVIC_SetPendingIRQ(CEC_IRQn);

// 
void CEC_IRQHandler(void) {
// user code
}




عرض PWM 7 قطاعات



يحتوي الجهاز على سطرين من 4 أحرف لكل منهما ، ما مجموعه 8 مؤشرات. تحتوي شاشات العرض المكونة من 7 مقاطع على 8 خطوط بيانات متوازية (A ، B ، C ، D ، E ، F ، G ، DP) وخطان لتحديد اللون (أخضر وأحمر) لكل منهما.

صورة



تخزين الموجي



يتم تنظيم التخزين وفقًا لمبدأ المخزن المؤقت الدائري مع فتحات 64 كيلو بايت لكل شكل موجة (حجم ثابت).



ضمان سلامة البيانات في حالة حدوث إغلاق غير متوقع



في EEPROM ، تتم كتابة البيانات في نسختين مع مجموع اختباري إضافي في النهاية. إذا تم إيقاف تشغيل الجهاز في لحظة تسجيل البيانات ، فستظل نسخة واحدة على الأقل من البيانات سليمة. يضاف المجموع الاختباري أيضًا إلى كل شريحة من بيانات راسم الذبذبات (القيم المقاسة عند مدخلات ADC) ، لذلك سيكون المجموع الاختباري غير الصحيح للشريحة علامة على نهاية مخطط الذبذبات.



التوليد التلقائي لنسخة البرنامج



1) قم

بإنشاء ملف version.fmt: #define SVN_REV ($ WCREV $)

2) قبل إنشاء المشروع ، أضف الأمر (لـ System Workbanch):

SubWCRev $ {ProjDirPath} $ {ProjDirPath} /version.fmt $ {ProjDirPath} /version.h

بعد تنفيذ هذا الأمر ، سيتم إنشاء ملف version.h برقم التثبيت الأخير.



هناك فائدة مماثلة لـ GIT: GitWCRev. /version.fmt ./main/version.h

#define GIT_REV ($ WCLOGCOUNT $)

يسمح لك هذا بمطابقة الالتزام وإصدار البرنامج بشكل لا لبس فيه.



محاكي



لان بدأ تطوير البرنامج الثابت قبل ظهور النسخة الأولى من الأجهزة ، ثم بدأت كتابة جزء من الكود كتطبيق وحدة تحكم على جهاز كمبيوتر.

صورة

المزايا:

- التطوير والتصحيح لجهاز الكمبيوتر أسهل من التطوير المباشر على الأجهزة.

- القدرة على توليد أي إشارات دخل.

- القدرة على تصحيح أخطاء العميل على جهاز كمبيوتر بدون أجهزة. يتم تثبيت برنامج التشغيل com0com على جهاز الكمبيوتر ، مما يؤدي إلى إنشاء زوج من منافذ com. يبدأ أحدهما المحاكي ، والآخر يربط العميل.

- يساهم في العمارة الجميلة ، لأن يجب عليك تحديد واجهة الوحدات المعتمدة على الأجهزة وكتابة تطبيقين



عينة من الرموز. تطبيقان لقراءة البيانات من eeprom.




uint32_t eeprom_read(uint32_t offset, uint8_t * buf, uint32_t len);
ifdef STM32H7
uint32_t eeprom_read(uint32_t offset, uint8_t * buf, uint32_t len)
{
  if (diag_isError(ERR_I2C))
    return 0;
	if (eeprom_wait_ready()) {
		HAL_StatusTypeDef status = HAL_I2C_Mem_Read(&I2C_MEM_HANDLE, I2C_MEM_DEV_ADDR, offset, I2C_MEMADD_SIZE_16BIT, buf, len, I2C_MEM_TIMEOUT_MS);
		if (status == HAL_OK)
			return len;
	}
	diag_setError(ERR_I2C, true);
  return 0;
}
#endif
#ifdef _WIN32
static FILE *fpEeprom = NULL;
#define EMUL_EEPROM_FILE "eeprom.bin"
void checkAndCreateEpromFile() {
	if (fpEeprom == NULL) {
		fopen_s(&fpEeprom, EMUL_EEPROM_FILE, "rb+");
		if (fpEeprom == NULL)
			fopen_s(&fpEeprom, EMUL_EEPROM_FILE, "wb+");
		fseek(fpEeprom, EEPROM_SIZE, SEEK_SET);
		fputc('\0', fpEeprom);
		fflush(fpEeprom);
	}
}
uint32_t eeprom_read(uint32_t offset, uint8_t * buf, uint32_t len)
{
	checkAndCreateEpromFile();
	fseek(fpEeprom, offset, SEEK_SET);
	return (uint32_t)fread(buf, len, 1, fpEeprom);
}
#endif




تسريع نقل البيانات (الأرشفة)



لزيادة سرعة تنزيل أشكال الموجة ، تمت أرشفتها قبل إرسالها. تم استخدام مكتبة uzlib كأرشيف . يتم تفريغ هذا التنسيق في C # في سطرين من التعليمات البرمجية.



عينة من الرموز. أرشفة البيانات.




#define ARCHIVER_HASH_BITS (12)
uint8_t __RAM_288K archiver_hash_table[sizeof(uzlib_hash_entry_t) * (1 << ARCHIVER_HASH_BITS)];

bool archive(const uint8_t* src, uint32_t src_len, uint8_t* dst, uint32_t dst_len, uint32_t *archive_len)
{
	struct uzlib_comp comp = { 0 };
	comp.dict_size = 32768;
	comp.hash_bits = ARCHIVER_HASH_BITS;
	comp.hash_table = (uzlib_hash_entry_t*)&archiver_hash_table[0];
	memset((void*)comp.hash_table, 0, sizeof(archiver_hash_table));
	comp.out.outbuf = &dst[10]; // skip header 10 bytes
	comp.out.outsize = dst_len - 10 - 8; // skip header 10 bytes and tail(crc+len) 8 bytes
	comp.out.is_overflow = false;

	zlib_start_block(&comp.out);
	uzlib_compress(&comp, src, src_len);
	zlib_finish_block(&comp.out);
	if (comp.out.is_overflow)
		comp.out.outlen = 0;

	dst[0] = 0x1f;
	dst[1] = 0x8b;
	dst[2] = 0x08;
	dst[3] = 0x00; // FLG
	// mtime
	dst[4] =
		dst[5] =
		dst[6] =
		dst[7] = 0;
	dst[8] = 0x04; // XFL
	dst[9] = 0x03; // OS

	unsigned crc = ~uzlib_crc32(src, src_len, ~0);
	memcpy(&dst[10 + comp.out.outlen], &crc, sizeof(crc));
	memcpy(&dst[14 + comp.out.outlen], &src_len, sizeof(src_len));
	*archive_len = 18 + comp.out.outlen;

	if (comp.out.is_overflow)
		return false;
	return true;
}




عينة من الرموز. تفريغ البيانات.



// byte[] res; //  
                        using (var msOut = new MemoryStream())
                        using (var ms = new MemoryStream(res))
                        using (var gzip = new GZipStream(ms, CompressionMode.Decompress))
                        {
                            int chunk = 4096;
                            var buffer = new byte[chunk];
                            int read;
                            do
                            {
                                read = gzip.Read(buffer, 0, chunk);
                                msOut.Write(buffer, 0, read);
                            } while (read == chunk);

                            //msOut.ToArray();//    
                        }




حول التغييرات الدائمة في المعارف التقليدية



ميمي من الإنترنت:

- لكنك وافقت على الاختصاصات!

- مهمة فنية؟ كنا نظن أن المعارف التقليدية كانت "وجهة نظر" ، ولدينا العديد منها.



عينة من الرموز. التعامل مع لوحة المفاتيح.




enum {
	IVA_KEY_MASK_NONE,
	IVA_KEY_MASK_ENTER = 0x1,
	IVA_KEY_MASK_ANY   = IVA_KEY_MASK_ENTER,
}IVA_KEY;
uint8_t keyboard_isKeyDown(uint8_t keyMask) {
	return ((keyMask & keyStatesMask) == keyMask);
}


بعد النظر في مثل هذا الجزء من الكود ، قد تفكر في سبب تكديسه بالكامل ، إذا كان هناك زر واحد فقط في الجهاز؟ في الإصدار الأول من TK ، كان هناك 5 أزرار وبمساعدتها تم التخطيط لتنفيذ تعديل الإعدادات مباشرة على الجهاز:


enum {
	IVA_KEY_MASK_NONE  = 0,
	IVA_KEY_MASK_ENTER = 0x01,
	IVA_KEY_MASK_LEFT  = 0x02,
	IVA_KEY_MASK_RIGHT = 0x04,
	IVA_KEY_MASK_UP    = 0x08,
	IVA_KEY_MASK_DOWN  = 0x10,
	IVA_KEY_MASK_ANY   = IVA_KEY_MASK_ENTER | IVA_KEY_MASK_LEFT | IVA_KEY_MASK_RIGHT | IVA_KEY_MASK_UP | IVA_KEY_MASK_DOWN,
}IVA_KEY;


لذلك ، إذا وجدت شذوذًا في الكود ، فلن تحتاج إلى تذكر المبرمج السابق على الفور بكلمات سيئة ، ربما في ذلك الوقت كانت هناك أسباب لمثل هذا التنفيذ.



بعض مشاكل التنمية



انتهى التدفق



يحتوي المتحكم الدقيق على 128 كيلوبايت من ذاكرة فلاش. في مرحلة ما ، تجاوز بناء التصحيح هذا الحجم. اضطررت إلى تمكين التحسين من خلال الحجم - O إذا كان تصحيح الأخطاء على الأجهزة مطلوبًا ، فقد تم إجراء تجميع خاص مع تعطيل بعض وحدات البرامج (modbas ، 101).



خطأ بيانات QSPI



في بعض الأحيان ، عند قراءة البيانات عبر qspi ، ظهر بايت "إضافي". اختفت المشكلة بعد زيادة أولوية المقاطعات qspi.



خطأ في بيانات راسم الذبذبات



لان يتم إرسال البيانات عن طريق DMA ، وقد لا "يراها" المعالج ويقرأ البيانات القديمة من ذاكرة التخزين المؤقت. تحتاج إلى إجراء التحقق من ذاكرة التخزين المؤقت.



عينة من الرموز. التحقق من ذاكرة التخزين المؤقت.




//           QSPI/DMA
SCB_CleanDCache_by_Addr((uint32_t*)(((uint32_t)&data[0]) & 0xFFFFFFE0), dataSize + 32);
//    ADC/DMA      CPU
SCB_InvalidateDCache_by_Addr((uint32_t*)&s_pAlignedAdcBuffer[0], sizeof(s_pAlignedAdcBuffer));




مشاكل ADC (قراءات مختلفة من التشغيل إلى التشغيل)



من التبديل إلى التشغيل ، ظهر في الجهاز إزاحة مختلفة للقراءات الحالية (بترتيب 10-30 مللي أمبير). ساعد الحل الزملاء من Kompel في شخص فلاديسلاف بارسوف وألكساندر كفاشين الذي شكرهم عليه كثيرًا.



عينة من الرموز. تهيئة ADC.



//         
HAL_ADCEx_Calibration_SetValue (&hadc1, ADC_SINGLE_ENDED, myCalibrationFactor[0]);
HAL_ADCEx_Calibration_SetValue (&hadc1, ADC_DIFFERENTIAL_ENDED, myCalibrationFactor[1]);
HAL_ADCEx_LinearCalibration_SetValue (&hadc1, &myLinearCalib_Buffer[0]);




عرض الخلفية



على الشاشات "الفارغة" المكونة من 7 أجزاء ، بدلاً من الإغلاق الكامل ، ظهرت إضاءة ضعيفة. والسبب هو أن شكل الموجة في العالم الحقيقي ليس مثاليًا ، وإذا قمت بتشغيل الكود gpio_set_level (0) ، فهذا لا يعني أن مستوى الإشارة تغير على الفور. تم القضاء على التوهج عن طريق إضافة PWM إلى خطوط البيانات.



خطأ Uart في HAL



بعد حدوث خطأ Over-Run ، توقف UART عن العمل. تم إصلاح المشكلة باستخدام تصحيح HAL:



عينة من الرموز. تصحيح HAL.



---    if (((isrflags & USART_ISR_ORE) != 0U)
---        && (((cr1its & USART_CR1_RXNEIE_RXFNEIE) != 0U) ||
---            ((cr3its & (USART_CR3_RXFTIE | USART_CR3_EIE)) != 0U)))
+++    if ((isrflags & USART_ISR_ORE) != 0U)
    {
      __HAL_UART_CLEAR_FLAG(huart, UART_CLEAR_OREF);




الوصول إلى البيانات غير المحاذاة



ظهر الخطأ فقط على الأجهزة في تجميع بمستوى تحسين -Os. بدلاً من البيانات الحقيقية ، يقرأ عميل modbus الأصفار.



عينة من الرموز. خطأ في قراءة البيانات غير المحاذاة.



	float f_value;
	uint16_t registerValue;
	//     registerValue  0
	//registerValue = ((uint16_t*)&f_value)[(offsetInMaximeterData -
	//	offsetof(mbreg_Maximeter, primaryValue)) / 2];

	//     memcpy  
    memcpy(& registerValue, ((uint16_t*)&f_value) + (offsetInMaximeterData -
        offsetof(mbreg_Maximeter, primaryValue)) / 2, sizeof(uint16_t));




البحث عن أسباب HardFault



إحدى أدوات الاستثناء التي أستخدمها هي نقاط المراقبة. أبعثر نقاط المراقبة حول الكود ، وبعد ظهور الاستثناء ، أقوم بالاتصال بمصحح الأخطاء وأرى النقطة التي مرت بها الشفرة.



عينة من الرموز. SET_DEBUG_POINT (__ LINE__).



//debug.h
#define USE_DEBUG_POINTS
#ifdef USE_DEBUG_POINTS
//     SET_DEBUG_POINT1(__LINE__)
void SET_DEBUG_POINT1(uint32_t val);
void SET_DEBUG_POINT2(uint32_t val);
#else
#define SET_DEBUG_POINT1(...)
#define SET_DEBUG_POINT2(...)
#endif

//debug.c
#ifdef USE_DEBUG_POINTS
volatile uint32_t dbg_point1 = 0;
volatile uint32_t dbg_point2 = 0;
void SET_DEBUG_POINT1(uint32_t val) {
  dbg_point1 = val;
}
void SET_DEBUG_POINT2(uint32_t val) {
  dbg_point2 = val;
}
#endif

//     :
SET_DEBUG_POINT1(__line__);




نصائح للمبتدئين



1) ألق نظرة على أمثلة التعليمات البرمجية. بالنسبة لـ esp32 ، يتم تضمين أمثلة مع SDK. بالنسبة إلى stm32 في تخزين HAL STM32CubeMX \ STM32Cube_FW_H7_V1.7.0 \ Projects \ NUCLEO-H743ZI \ Examples \

2) Google: كتيب البرمجة <your chip> ، دليل المرجع الفني <your chip> ، ملاحظة التطبيق <your chip> ، ورقة البيانات <your رقاقة>.

3) إذا كانت لديك أي صعوبات فنية ولم تساعد النقاط الأعلى 2 ، فلا يجب إهمال الاتصال بالدعم ، بل بالموزعين الذين لديهم اتصال مباشر بمهندسي الشركة المصنعة.

4) الأخطاء ليست فقط في التعليمات البرمجية الخاصة بك ، ولكن أيضًا في HAL الخاص بالشركة المصنعة.



شكرآ لك على أهتمامك.



All Articles