这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » 【Let'sdo第1期-功率检测与控制系统DIY】部署边缘检测AI与卡尔曼滤波,

共5条 1/1 1 跳转至

【Let'sdo第1期-功率检测与控制系统DIY】部署边缘检测AI与卡尔曼滤波,监控信号数据相似度,实现自锁与打嗝模式--OrangeBig

菜鸟
2025-05-23 13:53:57     打赏

写在最前方

欢迎来到我的文章,

本篇所有代码,例程,驱动文件,

训练好的模型都放置在了末尾附件中供大家免费下载玩~

 

2358612099.gif


本次的信号识别依靠在单片机上部署NanoEdge AI 模型来进行电流信号相似度识别

图片为训练结果

 

image001.png


效果如下

效果1.gif

0c9cde401eca914b2e178cd7431fb21a.mp4.gif

信号的处理用到了卡尔曼滤波,效果如下~

image003.jpg

 

本篇中,我将利用单片机边缘检测模型完成两个进阶内容

部分演示如下

我将用到以下四款软件

image005.png

 

这里放出后两款链接,如有兴趣可自行下载

NanoEdge AI Studio:NanoEdge AI Studio - STMicroelectronics - STM32 AI

VOfa+:VOFA-Plus上位机 | VOFA-Plus上位机

 

工程建立在基础任务之上,请先确保可以正常使用,

详情参考前几篇的注意事项

开箱与简介:【Let'sdo第1期-功率检测与控制系统DIY】-开箱贴-OrangeBig大桔-电子产品世界论坛

基础任务1-LED翻转:【Let'sdo第1期-功率检测与控制系统DIY】-基础任务1-翻转LED-OrangeBig大桔-电子产品世界论坛

基础任务2-OLED显示:【Let'sdo第1期-功率检测与控制系统DIY】-基础任务2-OLED显示-OrangeBig大桔-电子产品世界论坛

基础任务3-INA219采集与显示:【Let'sdo第1期-功率检测与控制系统DIY】-基础任务3-INA219读取&显示-OrangeBig大桔-电子产品世界论坛

 

话不多说,开始

一、滤波

如上所述,这里用到了卡尔曼滤波

卡尔曼滤波是一种利用线性系统状态方程,

通过系统输入输出观测数据,对系统状态进行最优估计的算法。

它在信号处理、控制理论、导航等领域有着广泛应用。

 

1、核心思想与基本假设

核心思想:通过融合系统的动态模型(预测)和实际观测数据(校正),以递归方式估计系统状态,最小化估计误差的方差。

基本假设:

系统是线性的(或可近似线性化)。

噪声符合高斯分布(过程噪声和观测噪声均为白噪声)。

初始状态的估计误差已知。

2、数学模型与递归过程

卡尔曼滤波基于两个关键方程:预测方程和校正方程,通过迭代更新实现最优估计。

1> 状态空间模型

 

状态方程(系统动态模型): (x_k = A_kx_{k-1} + B_ku_k + w_k) 其中:

 

(x_k) 为 k 时刻的状态向量;

(A_k) 为状态转移矩阵;

(u_k) 为控制输入向量,(B_k) 为控制矩阵;

(w_k) 为过程噪声,服从高斯分布 (w_k sim N(0, Q_k))。

 

观测方程(测量模型): (z_k = H_kx_k + v_k) 其中:

 

(z_k) 为观测向量;

(H_k) 为观测矩阵;

(v_k) 为观测噪声,服从高斯分布 (v_k sim N(0, R_k))。

2> 递归过程(两个阶段)

阶段 1:预测(时间更新)

 

状态预测: (hat{x}_{k|k-1} = A_khat{x}_{k-1|k-1} + B_ku_k) (基于前一时刻的最优估计预测当前状态)。

 

 

误差协方差预测: (P_{k|k-1} = A_kP_{k-1|k-1}A_k^T + Q_k) (预测当前状态估计的不确定性)。

 

阶段 2:校正(测量更新)

 

卡尔曼增益计算: (K_k = P_{k|k-1}H_k^T(H_kP_{k|k-1}H_k^T + R_k)^{-1}) (确定观测数据对状态估计的权重,平衡预测与观测的不确定性)。

 

 

状态更新: (hat{x}_{k|k} = hat{x}_{k|k-1} + K_k(z_k - H_khat{x}_{k|k-1})) (结合观测数据修正预测状态,得到最优估计)。

 

 

误差协方差更新: (P_{k|k} = (I - K_kH_k)P_{k|k-1}) (更新当前状态估计的不确定性,为下一时刻做准备)。

 

3、优势

1>无需存储历史数据,仅需当前观测和前一时刻的估计,计算效率高,适合实时应用。

2>可处理噪声统计特性已知的系统,通过调整噪声协方差矩阵(Q、R)适应不同场景。

 

4、使用

1>添加程序,这里我放在了SOFTWARE目录下,将其添加到路径下,与屏幕驱动引入相同,头文件引入等,这里不赘述。

2>变量声明,首先需要声明变量,每个数据源设置一个变量,若使用一个,各个数据会相互影响,

KalmanFilter kf1;  // 在这里声明kf1变量
KalmanFilter kf2;  // 在这里声明kf2变量
KalmanFilter kf3;  // 在这里声明kf3变量,

3>初始化,将我们上一步声明的变量取地址,后面跟参数,

KalmanFilter_Init(&kf1,  1e-4, 1e-3, 0.0f, 1.0f);//电流
    KalmanFilter_Init(&kf2, 1e-5, 1e-3, 0, 1);//电压
    KalmanFilter_Init(&kf3, 5e-2, 1e-1, 29.30, 1.0);// 功率  暂时没有用到

4>滤波,调用函数

//float current = INA219_GetCurrent_mA();    //未滤波
  float current = KalmanFilter_Update(&kf1, INA219_GetCurrent_mA());  //使用kf1滤波

代码如下

/*
 * kalman.c
 *
 *  Created on: Feb 22, 2025
 *      Author: 25289
 */


#include "kalman.h"

void KalmanFilter_Init(KalmanFilter *kf, float q, float r, float initial_x, float initial_p) {
    kf->q = q;
    kf->r = r;
    kf->x = initial_x;
    kf->p = initial_p;
}

float KalmanFilter_Update(KalmanFilter *kf, float measurement) {
    // 预测步骤
    kf->p = kf->p + kf->q;

    // 计算卡尔曼增益
    kf->k = kf->p / (kf->p + kf->r);

    // 更新状态估计值
    kf->x = kf->x + kf->k * (measurement - kf->x);

    // 更新估计误差协方差
    kf->p = (1 - kf->k) * kf->p;

    return kf->x;
}
/*
 * kalman.h
 *
 *  Created on: Feb 22, 2025
 *      Author: 25289
 */

#ifndef KALMAN_FILTER_H
#define KALMAN_FILTER_H
#include "main.h"
typedef struct {
    float q;  // 过程噪声协方差
    float r;  // 测量噪声协方差
    float x;  // 状态估计值
    float p;  // 估计误差协方差
    float k;  // 卡尔曼增益
} KalmanFilter;

void KalmanFilter_Init(KalmanFilter *kf, float q, float r, float initial_x, float initial_p);
float KalmanFilter_Update(KalmanFilter *kf, float measurement);

#endif

这个函数以用于多种单片机中,可自行移植尝试。

 

 

二、串口发送

这里和串口接收分开写,前期还用不到接收,就不写明

如果使用官方板卡建立工程,那么可以跳过第一步的cubemx配置

1、cubemx配置

image007.png

 

使能串口2,设置为异步模式,其他保持默认,而后更新代码

2、重映射到printf函数

/* USER CODE BEGIN PD */
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
 
PUTCHAR_PROTOTYPE
{
HAL_UART_Transmit(&huart2, (uint8_t*)&ch,1,HAL_MAX_DELAY);
return ch;
}
/* USER CODE END PD */

3、滤波后发送到串口

将下列放置在while中,cureer为浮点数,请确保已经打开浮点数pritf

float current = KalmanFilter_Update(&kf1, INA219_GetCurrent_mA());
if(current<0.5)
{
current = 0;
} //去除误差
 
printf("%.2frn",current);
HAL_Delay(300);

三、VOFA简单配置

此时可以打开VOfa+进行数据收集选择当前对应端口号,串口,波特率115200

image009.png

 

其他默认即可,按左上角按钮开启串口

image011.png

 

查看有无数据进入

image013.png

 

左侧选择第四项控件,第一个就是示波器

image015.png

 

拖动到右侧tab栏,右击全部填充

 

image017.png

 

再右击,如图将数据引入Y轴

 

image019.png

 

 

可以通过修改输入传入是否滤波观察对比滤波前后的样子

 

image021.jpg


 

 

三、模型训练

完成上两部分准备工作后,我们就可以进行模型的训练了

新建项目需要通过串口进行数据采集,

简单介绍一下

 

Nanoedge AI Studio是用于STM32部署边缘AI的软件,Studio可生成四种类型的库: 异常检测、单分类、多分类、预测。

它支持所有类型的传感器,所生成的库不需要任何云连接,可以直接在本地学习与部署,支持STM32所有MCU系列。

 

image022.png


意味着你可以将模型部署在任意的MCU,大小限制可以自行选择

其他家的mcu不确定能不能支持这里的模型,等我有空试试

 

图片1.png

 

正式开始前请先关闭VOfa+的串口连接

 

1、新建模型训练工程

打开NanoEdge AI Studio,点击NEW PROJECT

image024.png

 

 

选择第一项,

大致意思的使用设备学习检测信号中的异常使用模型识别数据中的异常模式,

该模型可以在学习阶段适应并逐步收集知识,以预测潜在的异常行为。

我们需要用到检测,选择这个即可

 

image026.png


 

设置工程名称,选择MCU或板卡等,数据类型选第二项Current电流

 

image028.png


Save

2、信号采集

进入这个界面,上面是正常信号,下面是异常信号,配置方式相同,

 

image030.png


  以正常信号为例

 

image032.png


串行USB信号,文件导入和采集器,

我们刚刚使用串口发送电流信号,因此选择串行USB信号,

image034.png

 

其他默认,如果你只想采集需要的数据条数,勾选最多数据限制,

确保你的电机正常运转,点击START开始

image036.png

 

采集完成导入数据

 

完成后再进行异常信号采集,

需要注意一点,请将电机在故障状态下运行,例如负载过大限制转速等。

其他步骤与上述正常采集相同,这里不赘述

3、模型训练

选择左上角添加,打开如下界面,勾选正常与异常数据后开始训练

image038.png

 

整个过程相对较慢

image040.png

 

运行时主要使用CPU,设置中也没有GPU的相关选项,

此过程大概不需要显卡(截至2025年5月23日 5.02版本)

参考我这里的配置`

13th Gen Intel(R) Core(TM)i9-13900H 加 32G内存

105条正常信号,229条异常信号

总共用时两2小时12分25秒

image042.png

 

先看一下跑完的样子

这是整个模型的参数

质量指数99.3分,质量高还是很高的

准确率99.8%,

占用RAM 0.1kb

占用flash 1.9kb

image044.png

 

 

这个是演变图,蓝色是质量指数,绿色是精准度,蓝色是RAM,红色是flash

这里能实时且直观的展示整个模型训练的状态

image046.png

 

这个是训练轨迹

image048.png

 

这个是他的历史迭代

 

image050.png

 

还有其他图像这里就不展示了,如果你训练过yolo模型会发现这个过程的演变和迭代是很相似的

4、模型测试与继续训练

image054.png

 

这个过程可以继续对模型进行训练,也可以进行测试

模型训练结果我比较满意,就不继续训练了

我们来测试

点击本地运行

image056.png

 

先是对正常信号进行采集学习,确保电机正常工作后,点击开始

  

image058.png


采集足够的信号后点击进行GO TO DETECTION转到检测

 

image060.png

 

整个过程很准确,效果如下

测试效果GIF

 

效果1.gif


5、获取模型与部署

按照如下勾选,我们是单模型,需要程序接口,数据类型是浮点数

而后点击部署库

image062.png

 

将的得到的压缩包解压后得到如下文件,主要用到这两个文件,一个是库,另一个是调用的api接口

image064.png 

 

 

image066.png


 

软件功能很多,后续有空会写一篇更详细的文章

为什么不是现在?因为我好几天做各个,没有好好休息了

谅解~~

 

图片30.png

 

一、工程构建

1、文件配置,如图

这样放置是个人习惯,你也可以将它放置在任意顺眼的地方

image066.png

 

添加路径到源文件

image068.png

 

包含

image070.png

 

库路径

image072.png

 

库名称,命名为neai

image074.png

 

2、添加必要程序

参考生成伪代码

/* Includes --------------------------------------------------------------------*/
#include "NanoEdgeAI.h"
/* Number of samples for learning: set by user ---------------------------------*/
#define LEARNING_ITERATIONS 40
float input_user_buffer[DATA_INPUT_USER * AXIS_NUMBER]; // Buffer of input values

/* Private function prototypes defined by user ---------------------------------*/
/*
 * @brief Collect data process
 *
 * This function is defined by user, depends on applications and sensors
 *
 * @param sample_buffer: [in, out] buffer of sample values
 * @retval None
 * @note   If AXIS_NUMBER = 3 (cf NanoEdgeAI.h), the buffer must be
 *         ordered as follow:
 *         [x0 y0 z0 x1 y1 z1 ... xn yn zn], where xi, yi and zi
 *         are the values for x, y and z axes, n is equal to
 *         DATA_INPUT_USER (cf NanoEdgeAI.h)
 */
void fill_buffer(float input_buffer[])
{
    /* USER BEGIN */
    /* USER END */
}

/* -----------------------------------------------------------------------------*/
int main(void)
{
    /* Initialization ------------------------------------------------------------*/
    enum neai_state error_code = neai_anomalydetection_init();
    uint8_t similarity = 0;

    if (error_code != NEAI_OK) {
        /* This happens if the library works into a not supported board. */
    }

    /* Learning process ----------------------------------------------------------*/
    for (uint16_t iteration = 0 ; iteration < LEARNING_ITERATIONS ; iteration++) {
        fill_buffer(input_user_buffer);
        neai_anomalydetection_learn(input_user_buffer);
    }

    /* Detection process ---------------------------------------------------------*/
    while (1) {
        fill_buffer(input_user_buffer);
        neai_anomalydetection_detect(input_user_buffer, &similarity);
        /* USER BEGIN */
    /*
    * e.g.: Trigger functions depending on similarity
    * (blink LED, ring alarm, etc.).
    */
        /* USER END */
    }
}


引用头文件后和定义后,添加必要参数,

写入缓存

image076.png

 

学习,这里我用OLED显示进行的过程,由于学习速度太快,我这里用了延时~

image078.png

 

learnp4.gif


更新事件

image080.png

 

吐槽一下,重新生成代码后中文会乱码,一直都是,#¥%……%¥

串口回传数据

printf("%d,%.2f,%.2f,%.2frn",similarity,current,busVoltage,power);

 

整个工程框架构建完成

详细过程请参考附件的工程

 三、加入中断接收

回到cubeMX,找到串口,使能串口中断

image082.png

 

重新生成后在while前添加开启中断

image084.png

重新定义中断回传函数,先判断是不是我们的串口2发送过来的

然后在里面写入我们的需求,

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
     if (huart == &huart2) {
        // 1. 存储当前接收的字符到缓冲�?
        received_buffer[dataindex] = received;

        // 2. �?查是否接收到换行符(命令结束标志�?
        if (received == '\n' && dataindex > 0) {
          // 2.1 在回车符位置添加字符串结束符(假设前�?个字符是\r�?
          if (received_buffer[dataindex-1] == '\r')
          {
            received_buffer[dataindex-1] = '\0';
          }
          else
          {
            received_buffer[dataindex] = '\0';
          }

          // 2.2 打印接收到的命令(调试用�?
          printf("%s\r\n", received_buffer);

          // 2.3 回传接收到的数据(去除\r\n�?
          //HAL_UART_Transmit(&huart2, received_buffer, dataindex, 100);

          // 2.4 命令处理逻辑
          if (strcmp((char*)received_buffer, "A") == 0)
          {
                similarity_limit = 95;
                printf("设置�?95相似度显示\r\n");// 接收到A,输�?1
          }
          else if (strcmp((char*)received_buffer, "B") == 0)
          {
                similarity_limit = 90;
                printf("设置�?90相似度显示\r\n");
          } else if (strcmp((char*)received_buffer, "C") == 0)
          {
                similarity_limit = 60;
                printf("设置�?60相似度显示\r\n");
          }
          else if (strcmp((char*)received_buffer, "U") == 0)
          {
                comu_usart = 1;
                printf("�?启数据实时回传\r\n");
          }
          else if (strcmp((char*)received_buffer, "V") == 0)
          {
                comu_usart = 0;
                printf("关闭数据实时回传\r\n");
          }
          else if (strcmp((char*)received_buffer, "Q") == 0)
          {
              switch_clock = 0;
                printf("已关闭自锁模式\r\n");
          }
          else {
            printf(" %s不是命令,A,B,C设置相似度,U开启数据回传,V关闭数据回传,Q关闭自锁\r\n", received_buffer);  // 未知命令提示
          }


          // 2.5 重置缓冲区索�?
          dataindex = 0;
        }
        else {
          // 3. 未收到结束符,继续接收(索引递增,防止溢出)
          dataindex++;
          if (dataindex >= 255) {
            dataindex = 0;
            printf("串口出错\r\n");
          }
        }

        // 4. 重新�?启接收中断,等待下一个字�?
        HAL_UART_Receive_IT(&huart2, &received, 1);
      }
}


其中回传末尾重新开启中断

 

image095.gif


这里我以\r\n作为判断

 

image090.png


判断前需要将\r\n去除掉否则会判断失败


串口结束与清除位数

image094.png

 中断接收完成

五、自锁模式与打嗝模式

以下是自锁模式与打嗝模式,请分别注释后使用

if(similarity<similarity_limit)
            {
                printf("信号异常\r\n");
                SWITCH_OFF();

                switch_clock = 1;  //异常判断标志位

                //自锁实现
//                while (switch_clock == 1)
//                {
//                    printf("自锁模式开启\r\n");
//                    HAL_Delay(500);
//
//                }

                //打嗝模式
                    printf("打嗝模式开启\r\n");
                    HAL_Delay(1000);
                    if(similarity>similarity_limit)
                    {
                        switch_clock = 0;
                    }


            }


            else
            {
                SWITCH_ON();
                //printf("信号正常\r\n");    //按需打开

            }


其中switch_clocks是全局变量,会根据中断接收到的字符进行更新,从而实现自锁或打嗝


继电器函数定义

image.png

/*
 * switch.c
 *
 *  Created on: May 21, 2025
 *      Author: 25289
 */
#include "switch.h"
#include "main.h"


void SWITCH_ON(void)
{
    // 将引脚设置为高电平
    HAL_GPIO_WritePin(my_SWITCH_GPIO_Port, my_SWITCH_Pin, GPIO_PIN_SET);

}
void SWITCH_OFF(void)
{
    // 将引脚设置为低电平
    HAL_GPIO_WritePin(my_SWITCH_GPIO_Port, my_SWITCH_Pin, GPIO_PIN_RESET);

}

定义的继电器GPIO,


效果演示

上电学习

e461086172680b799652695074b30d50.mp4.gif

开启回传

295457df.mp4.gif


自锁,发送Q恢复

b81fb0c992de0aad814635c447a035d2.mp4.gif

过流,恢复后一秒正常

be7a482a2d6b70fbbf67642825010763.mp4.gif

直接触发

295dde893a1f49e738da557f70905e4c.mp4.gif

工程,模型以及代码附件我放这里了,免费哦~

这是个文件.zip


以上就是Let'sdo第1期-功率检测与控制系统DIY的全部内容,

好几天没合眼,我要去补觉啦~

图片41.png

再见~~

 




关键词: 滤波          自锁     打嗝     检测     NanoEdge AI    

助工
2025-05-23 14:29:38     打赏
2楼

真大佬


助工
2025-05-23 16:32:07     打赏
3楼

牛逼!膜拜大佬!


管理员
2025-06-13 10:01:06     打赏
4楼

真棒!


助工
2025-06-13 13:49:02     打赏
5楼

厉害


共5条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]