问候,哈勃社区。最近,我公司在市场上推出了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中。
感谢您的关注。