四、药仓状态检测算法:基于重量传感器的药量检测、基于红外对管的药片掉落检测

各位同学,咱们今天聊点实在的。

药盒里的药到底还剩多少?病人有没有按时吃药?这两个问题,是智能药盒最核心的痛点。你想想看,如果连药量都测不准,那所谓的“智能”就是个摆设。

我个人习惯把药仓状态检测分成两大流派:称重派光感派。前者告诉你“还剩多少”,后者告诉你“刚才有没有掉下来”。两者结合,才能做到心里有数。

4.1 基于重量传感器的药量检测

重量传感器,说白了就是个高精度的电子秤。但放在药盒里,它有几个坑要特别注意。

4.1.1 传感器选型与原理

我建议用电阻应变式压力传感器,也就是我们常说的称重传感器。它的原理很简单:金属丝受力变形,电阻值跟着变。通过惠斯通电桥把电阻变化转成电压信号,再经过ADC采样,就能算出重量。

选型时重点关注三个参数:

  • 量程:一般药盒单仓装药量在50g~200g之间,选500g量程比较安全。别选太大,否则小药片的变化量测不出来。
  • 精度:至少0.1g分辨率。有些缓释片一颗就0.5g,精度不够直接废掉。
  • 温漂:这个我吃过亏。有一次项目在北方冬天测试,室温从25℃降到5℃,传感器读数漂了2g多。后来加了温度补偿算法才搞定。

核心公式:

实际重量 = (ADC原始值 - 零点偏移) × 标定系数 × 温度补偿因子

4.1.2 标定与校准流程

每一颗传感器都不一样,必须做标定。我的做法是:

  1. 零点标定:空仓时记录ADC值,作为基准。
  2. 满量程标定:放一个已知重量的标准砝码(比如100g),记录ADC值。
  3. 多点标定:如果要求高,就做5~10个点的曲线拟合。我一般用最小二乘法做线性回归。

嗯,这里要注意:标定砝码一定要用标准件。我曾经有个同事用硬币代替,结果不同年份的硬币重量不一样,标定出来全是错的。

4.1.3 滤波与去抖算法

重量传感器的数据天生带噪声。振动、气流、甚至病人呼吸都能让读数跳来跳去。我常用的滤波策略:

// 滑动平均滤波 - 我项目里最常用的
#define FILTER_WINDOW 8
static uint16_t buffer[FILTER_WINDOW] = {0};
static uint8_t index = 0;

uint16_t weight_filter(uint16_t raw_value) {
    uint32_t sum = 0;
    buffer[index++] = raw_value;
    if (index >= FILTER_WINDOW) index = 0;
    
    for (int i = 0; i < FILTER_WINDOW; i++) {
        sum += buffer[i];
    }
    return (uint16_t)(sum / FILTER_WINDOW);
}

除了滑动平均,我还推荐中值滤波。特别是当药盒被不小心碰了一下,出现一个尖峰噪声时,中值滤波能完美干掉它。滑动平均反而会把噪声扩散到好几个采样点。

我的经验:先做中值滤波(窗口3~5),再做滑动平均(窗口8~16)。两级滤波下来,数据基本稳如老狗。

4.1.4 药量计算与余量预警

有了稳定的重量值,药量计算就简单了:

// 单颗药片重量已知,比如0.5g
float pill_weight = 0.5;  // 单位:克
int pill_count = (int)(current_weight / pill_weight);

// 余量预警逻辑
if (pill_count <= 3) {
    // 触发低药量告警
    alert_low_medication();
}

但这里有个坑:药片重量不是绝对均匀的。同一批药片,每颗可能差个0.02g。如果药盒里只有几颗药,误差会被放大。我建议在算法里加一个置信度判断:当计算出的药片数量小于5颗时,直接显示“余量不足”,而不是显示具体数字。

4.2 基于红外对管的药片掉落检测

重量检测告诉你“还剩多少”,但病人到底有没有把药吃下去?这就需要红外对管来确认了。

4.2.1 红外对管的工作原理

红外对管由发射管接收管组成。发射管持续发出红外光,接收管检测光强。当药片从中间掉落时,光线被遮挡,接收管信号会瞬间下降。

说白了,这就是个光栅。药片穿过光路,产生一个脉冲信号。我们通过检测这个脉冲,就能确认“有一颗药掉下去了”。

注意:红外光容易受环境光干扰。特别是阳光直射时,接收管可能饱和。我建议在结构上做遮光处理,或者在软件里做环境光自适应校准。

4.2.2 信号调理与阈值判断

接收管的输出是模拟信号,需要经过比较器转成数字电平。我常用的电路设计:

  • 接收管输出接一个电压比较器(比如LM393)
  • 比较器的参考电压设为遮挡时信号幅度的50%
  • 输出端加一个施密特触发器,防止抖动

软件上,我建议做动态阈值。因为红外管会老化,发射功率会慢慢下降。固定阈值用久了就不准了。

// 动态阈值更新算法
#define THRESHOLD_RATIO 0.6  // 阈值设为背景光强的60%

uint16_t background_level;  // 无遮挡时的背景光强
uint16_t threshold;

void update_threshold(void) {
    // 每次检测到药片掉落结束后,更新背景值
    threshold = (uint16_t)(background_level * THRESHOLD_RATIO);
}

bool detect_pill_fall(uint16_t current_level) {
    if (current_level < threshold) {
        // 光线被遮挡,检测到药片
        return true;
    }
    return false;
}

4.2.3 防误触发与去抖处理

红外对管最怕什么?误触发。灰尘、手指、甚至一只小飞虫飞过,都可能产生遮挡信号。

我的避坑方案:

  1. 脉冲宽度判断:药片掉落的时间是固定的(比如10~30ms)。如果遮挡时间小于5ms或大于100ms,直接忽略。
  2. 连续确认机制:连续采样3次都检测到遮挡,才认为是一次有效掉落。
  3. 互斥逻辑:如果重量传感器没有检测到重量变化,红外检测到的掉落也暂时不确认。两者互相印证。

我曾经踩过的坑:有一次客户反馈药盒总是“多报”吃药次数。排查了三天,发现是药盒放在空调出风口下面,冷风导致红外管表面结露,光路被水雾遮挡。后来在结构上加了个加热丝,问题解决。

4.2.4 双传感器融合策略

重量传感器和红外对管,单独用都有缺陷。重量传感器测不准单次掉落(因为药片太轻,变化量小),红外对管容易误触发。但两者结合,效果1+1>2。

我设计的融合逻辑:

重量变化 红外检测 结论 动作
有变化 有掉落 确认吃药 记录+更新药量
有变化 无掉落 可能误触或漏药 触发异常告警
无变化 有掉落 红外误触发 忽略,不记录
无变化 无掉落 正常待机 无操作

这个表格看起来简单,但实际实现时要注意时间窗口对齐。重量变化有延迟(因为滤波),红外检测是实时的。我一般设一个500ms的时间窗口,在这个窗口内同时满足两个条件,才算一次有效吃药。

4.3 实际项目中的避坑指南

最后,分享几个我亲身踩过的坑:

  • 震动干扰:药盒放在桌子上,病人取药时手一抖,重量数据就跳。我后来加了加速度传感器,检测到震动时暂停重量采样。
  • 药片粘连:有些药片表面有糖衣,容易粘在一起。红外检测可能只看到一个遮挡信号,但实际掉下去两颗。我建议在结构上增加一个振动机构,让药片分离。
  • 电池电压影响:红外管的发射功率和电池电压直接相关。电池快没电时,发射光变弱,接收信号也变弱。我做了电压补偿,实时调整阈值。

我的个人习惯:每次做药仓检测算法,我都会先搭一个测试台,用1000次以上的模拟吃药来验证算法的准确率。准确率低于99.5%的算法,我不会让它进量产。

好了,这一章的内容就到这里。药仓状态检测,说白了就是“称重+光感”的组合拳。下一章我们会聊药盒的通信协议设计,到时候见。