0x 我与电子设计

从一年级到四年级,我一共参加了三次大学生电子设计竞赛,这份经历十分难得也同样很难忘,自从二年级获得全国大学生电子设计竞赛全国一等奖之后,我本着带带学弟学妹的心态和吴佳昱一起给低年级的学弟学妹们上一些培训课。但这些分享比起隔壁自动化学院实验室的传承差的还是很多,学院对电子设计竞赛的重视自我上一年级的时候才起步,很多东西都还在探索阶段,也许学院差的是一个或者两个,能有时间做出贡献的人。比如我认识的取得非常好的竞赛成绩,但是却选择就业的肖智中。

我和吴佳昱虽然都已经推免,但并不是大家想的推免之后就十分轻松,这只不过是另一段辛苦修行的开始。

Banner

其实参赛除了“带带学弟”这个冠冕堂皇的理由之外,还是自己想再拿个一等奖,这样从一年级开始我就连续获得三次一等奖,那就很神了,这次比赛的时候我的代码风格与前两次完全不同,以往都是毫无感情的堆砌代码,而这次更像是把我三年来所学融会贯通,在不上 OS 的情况下使用了任务调度、动态阈值、平滑滤波等等算法,但遗憾的是翻车了,翻车的原因有很多,下面慢慢总结吧。

1x A:无线运动传感器节点设计

一年级参加省电子设计竞赛的时候我就发现,虽然名义上是省赛,但是全国各地的题目都差不多。二年级参加国赛后发现比省赛要严格的多,而且多了很多环节,比如综合测评,四天三夜发挥能力,缺少睡眠,再来个一天不吃饭完成设计,我相信能坚持到综合测评的人都会对这段经历有着刻苦铭心的经历,毕竟一个人一般这辈子只能有一次,最多两次综合测评的机会。

今年的赛题出的时间很特殊,居然是提前一天晚上八点,当天晚上就确定了题目,因为心电信号的模拟和采集在器件清单公布之后我们就调试完了,毕竟这道题很好猜,肯定也有一大堆人和我们一样因为这个原因选择了 A 题,这就是入坑的开始,因为心电信号的采集不仅仅是最难的,而且仅是该题目的四分之一。

赛题要求是基于 TI 模拟前端芯片 ADS1292 和温度传感器 LMT70 设计制作无线运动传感器节点,节点采用电池供电,要求能稳定采集和记录使用者的心电信息、体表温度和运动信息。

有关赛题的具体需求详见:https://leiblog.wang/static/2020-10-05/2020年TI杯江苏省大学生电子设计竞赛题.zip

本次电子设计竞赛编写的程序可以在这里下载:

  1. https://leiblog.wang/static/2020-10-26/EUDC_Project1.zip
  2. https://leiblog.wang/static/2020-10-26/STM32F4.zip

两个文件分别代表着两种不同的解决方案(我也不知道能不能跑得起来啦)

1.1x 心电信号

首先,心电信号的采集使用的是赛题指定的 ADS1292,理论上该芯片是专门用于心电,不会存在什么问题。

为了在 TFT-LCD 上显示波形,做了个简单的示波器,该示波器由我之前完成本校的 Project3.4 改编而来:https://github.com/LeiWang1999/stm32f407_hdb3

关于画波形其实我们讨论了两种解决方案:

  • 都塞在 while 循环里,一次采集一个点,每采集到一个点就刷新示波器。
  • 一次采集 800 个点,绘制一幅完整的图。

我其实倾向第二个解决方案,但是刚开始考虑到处理速度,就上了第一种,为后来的数字滤波埋下了巨坑。

用模拟器在 ADS1292 上表现十分优秀,但是换成真人信号差异很大,主要是因为两点:

  1. 引入了 50Hz 左右的工频躁声
  2. 引入了白噪声
  3. 模拟器输入比真实测量信号幅度大数百倍

对第一个噪声,因为心电信号主要是 0.2Hz-2Hz(1 秒 5 下-1 秒 0.5 下),所以设计一个低通滤波器就完事了,但是 50Hz 的低通还是很难做对的,更因为统一采购买的 ADS1292 没有片上的模拟滤波器,还需要占用 MPU 不多的算力制作数字滤波。

至于数字滤波器的选择,我很久之前写过一篇 blog:Matlab、图像 IIR、FIR 滤波,经过滤波器会产生一定的延时,阶数越高,延时越大,由于相同指标下,FIR 的阶数比 IIR 的阶数要高,所以其产生的延时越大,效果也越差一点。但是由于 FIR 是线性相位,每一个点的延时都是相同的, 所以很好进行修正。

所以在设计滤波器的时候其实是一个权衡利弊的过程,IIR 消耗较少的算力能达到很好的效果,但是会带来难以修复的相移,具体体现在心电图的关键点的位置发生偏移。而 FIR 虽然没有这个问题,但效果较好的 FIR 可能会高达一百阶数。这里吐槽学弟,划水设计了两天的滤波器,还没设计出来,但相信他也从设计过程中得到了不小的收获,总的来说,我们设计数字滤波器的过程如下:

  1. 通过串口先把有噪声的心电信号数据缓存到 PC 端,然后通过 Matlab 画图。
  2. 在 Matlab 的 shell 里输入 FDATools,进入图形化的数字滤波器设计,通过 Matlab 脚本语言进行滤波验证。
  3. 设计完滤波器后,导出为 C 语言的头文件。
  4. 在 PC 端验证 C 程序的正确性,然后在 STM32 端运行。

为整个过程设计的最终的 filter 语言程序如下:

// filter.c
#include "filter.h"

double iir_filter(struct IIR_Filter_State *filter_state, int *a, int *b, int x_0)
{
    int i;
    double tmp = 0;

    //xy值更新
    for (i = FILTER_ORDER; i > 0; i--)
    {
        filter_state->x[i] = filter_state->x[i - 1];
        filter_state->y[i] = filter_state->y[i - 1];
    }
    filter_state->x[0] = x_0;

    //计算输出滤波数值
    for (i = 0; i <= FILTER_ORDER; i++)
    {
        tmp += (b[i] * filter_state->x[i]);
    }
    for (i = 1; i <= FILTER_ORDER; i++)
    {
        tmp -= (a[i] * filter_state->y[i]);
    }
    tmp /= a[0];

    filter_state->y[0] = tmp;

    return tmp;
}
// filter.h
#ifndef INCLUDE_FILTER_H_
#define INCLUDE_FILTER_H_

#define FILTER_ORDER 20
struct IIR_Filter_State{

    float x[FILTER_ORDER+1];
    float y[FILTER_ORDER+1];
};

double iir_filter(struct IIR_Filter_State *filter_state, int *a, int *b, int x_0);

/*
 * Expected path to tmwtypes.h
 * H:\Program Files\MATLAB\R2016b\extern\include\tmwtypes.h
 */
/****
 * Warning - Filter coefficients were truncated to fit specified data type.
 *   The resulting response may not match generated theoretical response.
 *   Use the Filter Design & Analysis Tool to design accurate
 *   single-precision filter coefficients.
 ****/
#endif
// main.c
#include <stdio.h>
#include "filter.h"
#define LENGTH 9518
signed int IIR_B[21] = {
    1842804555, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0,
    0, 0, -1842804555};
signed int IIR_A[21] = {
    2147483647, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0,
    0, 0, -1538125463};

int main()
{
    FILE *data, *dout; /***文件指针***/
    int i;
    int in_data[LENGTH];
    double filter_out[LENGTH];
    struct IIR_Filter_State state1;
    if ((data = fopen("people.txt", "r")) == NULL) //打开测试数据文件
    {
        printf("lowpass_input.txt file does not exist!\n");    //文件打开失败
        if ((data = fopen("lowpass_input.txt", "w+")) == NULL) //生成测试数据文件
        {
            // printf("cannot create file lowpass_input.txt!\n"); //数据文件生成失败,退出
            getchar();
        }
        else //文件打开成功
        {
            fclose(data);
            getchar();
        }
        return 0;
    }

    if ((dout = fopen("iir20_people.txt", "w+")) == NULL)
    {
        printf("Cannot open lowpass_out.txt!\n");
        fclose(dout);
        return 0;
    }

    //读取数据
    for (i = 0; i < LENGTH; i++)
    {
        fscanf(data, "%d ", &in_data[i]);
        // printf("cur id: %d, curIndata:%d \r\n", i, in_data[i]);
    }
    printf("Data read success!\n");
    //滤波进行时
    //初始化低通滤波器状态结构体
    for (i = 0; i < FILTER_ORDER + 1; i++)
    {
        state1.x[i] = 0;
        state1.y[i] = 0;
    }
    //iir滤波函数
    for (i = 0; i < LENGTH; i++)
    {
        filter_out[i] = iir_filter(&state1, IIR_A, IIR_B, in_data[i]);

        // printf("cur id: %d, curIndata:%d, curFilter_out: %lf \r\n", i, in_data[i], filter_out[i]);

    }
    printf("Lowpass Filter compute finish!\n");
    //保存滤波结果
    for (i = 0; i < LENGTH; i++)
    {
        fprintf(dout, "%lf ", filter_out[i]);
    }
    printf("Save finish!\n");
    //保存滤波结果
    //关闭文件
    fclose(data);
    fclose(dout);
    //getchar();
    return 0;
}
%% display wave
% C_read = dlmread('people.txt');
C_read = dlmread('iir20_people.txt');
plot(C_read);

以上步骤可以滤除工频噪声(大概),然后第二个白噪声通过滑动窗格实现,在下面将检测 R、S、Q 三个点的时候会讲到。

第三个问题,针对模拟器和真人输入信号的幅值不匹配,我们做了一个动态归一化,对读入的点取最大和最小,然后把数值归一化到-1 到 1 之间。

$$
re = \frac{input-Min}{Max-Min}
$$

这一步其实也是必须的,因为在判断 RSQ 点的时候会有一定程度的抖动,如果单纯以幅值来判断就不能同时满足模拟信号和真人信号一起检测的目的,而归一化会把两组数据压缩到相同比例。

最后阐述 RSQ 三点检测的解决方案,因为 S 点和 Q 点具有相同的特征,所以容易混淆,我们率先检测 R 点。

首先做一个平滑滤波器(其实就是取四个点做个平均),这一步也顺带可以把白噪声滤除。

我们选取一个大小合适的滑窗

在滑窗内找一个 localmaxium,这就很简单对吧,求一个向左的梯度和向右的梯度。但是因为抖动可能会误判很多点,于是我们还加上了一个阈值条件,当这个 localmaxium 在这个阈值(比如最大值的 0.7 倍)处才算正确的值。

当我们在窗格内成功找到了 localmaxium,我们分别在最大值的左边和右边找到两个最小值所在位置,那就是我们的 Q 点和 S 点了。

测频率也很简单,可以根据两个 R 点之间的点的数目除上采样时间就行了。

1.2x 温度传感

温度传感器,直接用 ADC 驱动加个简单的滤波就行了,只是最后测量出来的数据和拿温度计测量出来的数据还是有些差距,我觉得不是算法的问题,而是队友拿温度计的握法出了点问题。

1.3x 步数与距离

关于使用加速度计测量部署和运动距离的算法,我们刚开始的解决方案是使用 JY-901 复现了一篇论文的算法,论文的地址如下:

https://www.analog.com/media/cn/analog-dialogue/volume-44/number-2/articles/pedometer-design-3-axis-digital-acceler_cn.pdf

论文很人性化,不仅提供了测量步数的思想,也提供了测量距离的方案,就是算法难写了一点,包括数字滤波、动态阈值、滑动窗格、最大轴的选择等等,总之最后搞出来了但是并不是很准。

(PS,为什么用 JY901?受老师抬爱给了我们一个比 MPU6050 还多三个轴的传感器,可是回过头来发现 MPU6050 的 DMP 模块已经集成了步数检测的电路,只需要读寄存器就好了,我晕)

最后我们还是换成了 MPU6050,但自己手写测步数等算法也不是没有收获,因为 MPU6050 的寄存器里没有提供距离的寄存器。

1.4x 无线传输

这里,欸要做个 APP,实在是懒得做。

我们的解决方案是用了两块 STM32F407,显示程序都是一样的,只需要使用蓝牙传输数据就可以啦。

1.5x 利用简单任务调度实现融合

接下里的问题就是,怎么把这么多功能都融合到一个程序里?

如果使用了 OS 来做,就不用考虑这个问题了,OS 支持分时调度,在裸机上跑很容易把整个程序都塞到 While 循环里,会产生很多问题,尤其是影响滤波器的性能,在我们的工程里,做了一个简单的任务调度算法(感谢万老师,操作系统没白选):


typedef struct
{
	void (*fTask)(void);
	uint64_t uNextTick;
	uint32_t uLenTick;
} sTask;

static sTask mTaskTab[] =
{
		{Task_SysTick, 0, 0},
		{ecg_process, 0, 5}, // 10ms执行一次
		{step_process, 0, 200},
		{prompt_process, 0, 1000}
};
	while(1)
	{
		// Task Loop
		for (i = 0; i < ARRAYSIZE(mTaskTab); i++)
		{
			if (mTaskTab[i].uNextTick <= GetTimingTick())
			{
				mTaskTab[i].uNextTick += mTaskTab[i].uLenTick;
				mTaskTab[i].fTask();
			}
		}
	}

具体可以参考这篇:https://blog.csdn.net/weixin_43637946/article/details/83857316