IRIS测量装置的开发

图片

问候,哈勃社区。最近,我公司在市场上推出了IRIS测控装置。作为该项目的主要程序员,我想向您介绍设备固件的开发(根据项目经理的说法,从构思到批量生产,固件所占的工作量不超过总工作量的30%)。对于理解“真实”项目的人工成本和想要“深入了解”的用户而言,本文对于新手开发人员而言主要有用。



设备用途



IRIS是一种多功能测量设备。他知道如何测量电流(安培表),电压(伏特表),功率(瓦特表)和许多其他量。KIP IRIS会记住其最大值,并写示波形图。该设备的详细说明可以在公司的网站上找到。



一点统计



定时



首次致力于SVN:2019年5月16日。

发布:2020年6月19日。

*这是日历时间,而不是整个学期的全职开发。其他项目有干扰,对技术规范的期望,硬件迭代等。



提交



SVN中的数字:928

这是哪里来的?

1)我在开发过程中是微提交的支持者

2)硬件和仿真器分支中的重复项

3)文档

因此,带有有效载荷的新代码形式(中继分支)的数量不超过300。

图片



代码行数



cloc 实用程序使用默认参数收集统计信息,但不包括HAL STM32和ESP-IDF ESP32源。

图片

STM32固件:38,334行代码。其中:

60870-5-101:18751

ModbusRTU:3859

示波器:1944

存档器:955

ESP32固件:1537行代码。



硬件组件(涉及的外围设备)



该设备的主要功能在STM32固件中实现。 ESP32固件负责蓝牙通信。芯片之间的通信通过UART进行(请参见标题中的图)。

NVIC是中断控制器。

IWDG-看门狗定时器,用于在固件挂起时重新启动芯片。

计时器-计时器中断使项目保持心跳。

EEPROM-用于存储生产信息,设置,最大读数,ADC校准系数的存储器。

I2C是用于访问EEPROM芯片的接口。

NOR-用于存储波形的存储器。

QSPI是用于访问NOR存储器芯片的接口。

RTC-实时时钟提供关闭设备后的时间安排。

ADC-ADC。

RS485是用于通过ModbusRTU和60870-101协议进行连接的串行接口。

DIN,DOUT-离散量输入和输出。

按钮-设备前面板上的按钮,用于在测量之间切换指示。



软件架构



主要软件模块



图片



测量数据流



图片



操作系统



考虑到闪存数量的限制(OS引入了开销)和设备的相对简单性,决定放弃使用操作系统并通过中断来解决。在Habré上的文章中已经多次强调了这种方法,因此,我将仅给出中断内部任务的流程图及其优先级。

图片



样例代码。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段显示



该设备有两行,每行四个字符,共8个指示器。7段显示器具有8条并行数据线(A,B,C,D,E,F,G,DP)和每条2条颜色选择线(绿色和红色)。

图片



波形储存



该存储组织为一个循环缓冲区,每个波形具有64 KB插槽(固定大小)。



在意外关闭的情况下确保数据完整性



在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 $)

这可以使您明确地匹配提交和软件版本。



仿真器



因为 固件的开发是在第一个硬件实例出现之前开始的,然后一部分代码开始作为PC上的控制台应用程序编写。

图片

优点:

-PC开发和调试比直接在硬件上容易。

-产生任何输入信号的能力。

-在没有硬件的PC上调试客户端的能力。com0com驱动程序安装在PC上,它将创建一对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();//    
                        }




关于传统知识的永久变更



来自互联网的Meme:

-但是您批准了职权范围!

-技术任务?我们认为传统知识是一种“观点”,其中有几种。



样例代码。键盘处理。




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 KB的闪存。在某些时候,调试版本超出了此数量。我必须启用按卷-Os进行优化。如果需要在硬件上进行调试,则通过禁用某些软件模块(modbas,101st)进行特殊组装。



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 mA)。来自Kompel的Vladislav Barsov和Alexander Kvashin的同事为该解决方案提供了帮助,对此深表感谢。



样例代码。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消除了杂光。



HAL中的Uart错误



发生溢出错误后,UART停止工作。该问题已通过HAL补丁解决:



样例代码。修补HAL'a。



---    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中。对于HAL存储中的stm32 STM32CubeMX \ STM32Cube_FW_H7_V1.7.0 \ Projects \ NUCLEO-H743ZI \示例\

2)Google:编程手册<您的芯片>,技术参考手册<您的芯片>,应用说明<您的芯片>,数据表<您的芯片>。

3)如果存在一些技术困难,并且最重要的2个问题没有帮助,则您不应忽略联系支持,而应该与直接与制造商公司的工程师联系的分销商。

4)错误不仅存在于您的代码中,而且还存在于制造商的HAL中。



感谢您的关注。



All Articles