2024年3月

惯性传感器单元 IMU

IMU 是 Inertial Measurement Unit 的缩写, 直接翻译过来就是惯性测量单元, 常见的有单独的三轴加速度(Accelerometer)计 ADXL345, L3G4200D, L3GD20等, 单独的三轴角速度计(又称陀螺仪, Gyroscope) LIS3DH, L3GD20H, BMG160, 以及包含了加速度计和陀螺仪的六轴运动传感器 MPU6050, MPU6500, MPU6881, BMI160等, 以及带电子罗盘的九轴运动传感器 MPU9250, MPU9255等.

在判断物体在空间中的姿态以及运动轨迹时, 用得最多的是加速度和角速度传感器. 加速度传感器可以计算倾角, 陀螺仪可以计算角速度, 这两种传感器各自的特点为

  • 陀螺仪: 动态特性好, 因为测量噪声(误差)的存在, 以及各向灵敏度的差异, 通过积分计算角度会累积误差, 导致结果越来越不准
  • 加速度计: 不会累积误差, 所以准确度有保证, 但是动态响应差, 不适用于角度变化快速的场景.

如果设备运动速度较慢, 作用于系统的加速度力主要是重力, 可以使用加速度计来计算倾角, 利用重力矢量及其在加速度计轴上的投影(即对应读数)来确定倾角. 由于重力是恒定加速度, 如果存在额外的恒定加速度也会影响计算结果. 额外的恒定加速度包括发动机持续加速以及设备自身匀速的旋转等.

通过加速度计算倾角

静止和慢速物体因为主要加速度来源为重力, 用加速度传感器可以计算得到物体的倾角.

单轴数据计算倾角

假设X轴上测到的加速度值为
\(a_x\)
定义倾角
\(α\)
为X轴与水平面(与重力矢量垂直的平面)的夹角, 计算为

\[α = sin^{-1}(\frac{a_x}{g})
\]


\(a_x = 0\)
时, 倾角为0, X轴处于水平位置, 当
\(a_x = g\)
时, 倾角90°, 处于垂直位置, 当
\(a_x\)
的值很小时, 可以用近似公式
\(sin(α) ≈ α\)
, 于是

\[α ≈ k(\frac{a_x}{g})
\]

比例系数k用于角度的线性近似计算

\(a_x\)
的读数匹配
\(sin(α)\)
曲线, 读数值范围为
\(-1g ~ 1g\)
, 在读数为0(水平位置)时灵敏度最高, 在读数为
\(+-1g\)
时灵敏度最低.

双轴加速度数据计算倾角

单轴无法判断方向, 因为在倾角为
\(α\)

\(180° - α\)
时读数是一样的. 如果增加一个与X轴垂直的轴, 假定为Z轴, 且XZ轴形成的平面垂直于水平面, 那么XZ轴在这个平面里形成的对水平面的倾角就可以判断方向, 并且结果较为精确, 因为总会有一个轴处于灵敏度较高的区间.

当XZ轴形成的平面
垂直于水平面
时, X轴倾角可以用两个轴的读数进行计算.

\[α = tan^{-1}(\frac{a_x}{a_z})
\]

\(a_x\)
为0时, 倾角为0, X轴处于水平, 当
\(a_z\)
为0时要注意避免零除. 如果XZ轴平面不垂直于水平面, 这个结果会小于实际的倾角, 倾斜越厉害误差越大.

三轴加速度数据计算倾角

假设传感器Z轴垂直朝下, X轴朝正前方, 则X轴与水平面之间的夹角为俯仰角(pitch)
\(α\)
, Y轴与水平面间的夹角为横滚角(roll)
\(β\)
, 航向角yaw需要地磁传感器, 无法通过加速度传感器计算. Z轴垂直于X轴和Y轴, 和两轴数值是相关的, 并没有独立性, 仅用于判断设备上下的朝向. 令Z轴与水平面的夹角为
\(γ\)

重力加速度在XYZ三个轴上的投影即为三个轴传感器的读数, 可以将三轴倾角和重力加速度想像为一个斜立的长方体, 长方体的对角线为重力加速度, 对角线就是这个角对应的三条边, 倾角的计算方法为

\[α = sin^{-1}(\frac{a_x}{g})
\]

\[β = sin^{-1}(\frac{a_y}{g})
\]

\[γ = sin^{-1}(\frac{a_z}{g})
\]

带运动加速度的倾角计算

上面的计算方式适合相对静止和慢速的场景, 当物体受作用于多个外力时, 作用于传感器的综合加速度为重力与各外力的叠加, 此时加速度的方向就不是重力的方向, 上面的计算方式就不适用了, 因为综合加速度可能比
\(g\)
更大或更小.

对于一个物体, 整体加速度等于三轴加速度的矢量和, 其大小
\(G\)
可以通过三个向量的大小计算得到

\[G = \sqrt[2]{{a_x}^2 + {a_y}^2 + {a_z}^2}
\]

由此可以得到运动状态下倾角的计算

\[α = sin^{-1}(\frac{a_x}{G})
\]

\[β = sin^{-1}(\frac{a_y}{G})
\]

\[γ = sin^{-1}(\frac{a_z}{G})
\]

因为
\(a_x\)
,
\(G\)
,
\(\sqrt[2]{{a_y}^2 + {a_z}^2}\)
三个矢量形成直角三角形, 上面的式子可以也可以用
\(tan^{-1}\)
计算

\[α = tan^{-1}(\frac{a_x}{\sqrt[2]{{a_y}^2 + {a_z}^2}})
\]

\[β = tan^{-1}(\frac{a_y}{\sqrt[2]{{a_x}^2 + {a_z}^2}})
\]

\[γ = tan^{-1}(\frac{a_z}{\sqrt[2]{{a_x}^2 + {a_y}^2}})
\]

此时的倾角并非相对重力加速度的倾角, 而是相对物体整体加速度矢量的倾角, 例如物体向前(X轴方向)加速运动时, 整体加速度方向会向后倾斜, 当物体左转时, 离心力会导致整体加速度方向向右倾斜. 计算此时的姿态倾角, 可以用于帮助物体在当前的运动状态上保持平衡.

互补滤波

通过加速度传感器(Accelerometer)可以使用反三角函数
\(sin^{-1}\)

\(tan^{-1}\)
求静止和慢速运动物体的倾角, 对于高速运动的物体, 需要结合陀螺仪的角速度读数快速响应倾角变化. 对于这两种传感器读数的结合, 通常采用互补滤波算法.

互补滤波就是在短时间内采用陀螺仪得到的角度做为最优值, 定时用加速度值来校正陀螺仪的得到的角度. 加速度计要滤掉高频信号, 陀螺仪要滤掉低频信号, 互补滤波器就是根据传感器特性不同, 通过不同的滤波器, 相加得到整个频带的信号.

互补滤波的公式为

\[α_n = k * (α_{n-1} + \delta_α ∗ dt) + (1 - k) ∗ α^{\prime}
\]

其中

  • \(α_n\)
    互补计算得到的角度
  • \(α_{n-1}\)
    前一次计算得到的角度
  • \(\delta_α\)
    陀螺仪得到的角速度
  • \(dt\)
    两次计算的时间间隔
  • \(α^{\prime}\)
    通过加速度计得到的倾角
  • \(k\)

    \(1-k\)
    为加权系数, 和为 1

加权系数的确定. 在 《The Balance Filter》 中提到关于加权系数的求解公式, 先设滤波器的加权系数为
\(α\)
, 时间常数为为
\(τ\)
, 运行周期为
\(dt\)
, 那么公式为

\[α = \frac{τ}{τ+dt}
\]

运行周期 dt 根据运行周期确定, 如果互补滤波器方法的调用频率为 200次每秒, 那么
\(dt = \frac{1000ms}{200} = 5ms\)

时间常数
\(τ\)
的取值根据系统的实际需求调整,
不同的系统的
\(τ\)
值不一定相同.
\(τ\)
取值越大则陀螺仪权重越大,
\(τ\)
取值越小则加速度传感器的权重越大. 通常互补滤波器对陀螺仪的权重会大些, 以降低加速度传感器中噪声的影响. 例如互补滤波器运行间隔为 10ms, 时间常数
\(τ =0.49\)
, 那么此时加权系数为:

\[α = \frac{τ}{τ+dt} = \frac{0.49}{0.49 + 0.01} = 0.98
\]

C语言代码

// a = tau / (tau + dt)  
// acc = 加速度传感器数据 
// gyro = 陀螺仪数据 
// dt = 运行周期

float angle;
float a;

float ComplementaryFliter(float acc, float gyro, float dt)
{
  a = 0.98;
  angle = a * (angle + gyro * dt) + (1 - a) * (acc);
  return angle;
}

在实际应用中, 因为加速度计和角速度计读取的数据存在很大的噪音, 直接使用会造成反馈的不稳定(抖动), 需要在计算前通过卡尔曼滤波器等进行平滑.

参考

前言

2023年是充满机遇与挑战的一年,也是个人成长最多的一年。这一年发生了很多事情,经历了挑战,大开了眼界,有勇气去喊停,没有结局也可即兴。

2023年回顾

  • 忙碌的工作 -> 裸辞
  • 续任微软最有价值专家
  • 生财有术 -> 郑子铭的月度思考
  • 旅行
  • 格局面授
  • 就业
  • 热辣滚烫
  • 猫猫狗狗

忙碌的工作 -> 裸辞

2022年9月
入职了一家外企,担任
TL
的角色,负责技术架构,实现,维护等等,期间加入了多个技术团队参与了多个大型企业项目的开发,最忙碌的时候连续加班三个月,周末无休,强度拉满。在这期间锻炼了团队管理能力,客户期望管理能力,技术架构能力,英语沟通能力,抗压能力等等。

这一段工作经历可以算是我毕业工作五年来收获最丰富的一段,也是个人能力提升最快的。但是凡事有利有弊,长期的高强度工作让我感觉身心不适,也没有了工作的热情与向往,最终在
2023年7月
我选择了裸辞。

感激自己平时有理财的好习惯,也还没有房贷车贷,所以有勇气喊停,给自己一段时间去思考未来的规划。

续任微软最有价值专家

2023年7月
我收到来自微软最有价值专家官方的邮件,恭喜我续任微软MVP成功。这是我第二年获得微软最有价值专家认证,非常感激大家的支持与信赖,我也会继续努力和技术社区的小伙伴们一起为.NET社区做贡献。

生财有术 -> 郑子铭的月度思考

2023年4月
我参与了
生财有术
三天体验营,感觉大开眼界,体验结束后果断付费加入。在这个组织中我阅读了很多分享,参与了风向标共创,也在航海实战中划水,同时也创建了自己的知识星球 ->
郑子铭的月度思考
,用于记录自己的月度思考,感兴趣的可以私聊加入,一起记录分享自己的思考。

旅行

今年去了两趟远途旅行,分别是柳州和成都,都是高铁往返,非常便利。

柳州

高铁广州南站直达柳州站

第一站前往
大华干捞粉
,物美价廉,分量十足。

饭后回酒店休息,下午前往
马鞍山公园
,建议晚上再过来。

爬完山后前往
云岭58冰豆腐花
,附近的街坊们都喜欢过来吃一碗,味道超赞。

晚饭选择
金弟炒螺蛳粉
,点了一份炒螺蛳粉和炒鸭脚,一试难忘,念念不忘。

第二天前往
青云市场
,品味了各式各样的美食,其中
韦姐卷粉
yyds,值得一试。

晚上前往
窑埠古镇
,适合拍照,非常漂亮,值得一去。

成都

高铁广州南站直达成都南站

第一站前往
肥婆饭局
,超级下饭,超级划算,还有小礼物。

第二天前往
成都博物馆
,感受成都历史。

参观完成都博物馆之后来到了
人民公园
,品尝了
钟水饺
,非常不错。

饭后来到
宽窄巷子
散步,非常热闹。

夜宵选择
眷蜀冰社
的糍粑冰粉 +
素芬掌中宝火锅串串香

第三天前往
三星堆博物馆
,感受三星堆文化。

第四天继续参观成都博物馆,参观完之后在前往火锅的路上品尝了
凉糕
,非常好吃。

接着前往
渝少侠·小院火锅
打卡成都火锅

饭后前往
锦里
,在
英雄三国
碰巧遇到川剧变脸,大饱眼福。

夜宵前往
贺记蛋烘糕
,品尝了锋哥推荐的口味,味道绝了,吊打无名路边摊,强烈推荐。

最后一天前往
鑫记婆婆兔
,打包
招牌冷吃兔
回家。

格局面授

2023年10月
参与了格局面授,疫情一晃三年,又见到熟悉的赵老师。

收获了
八段锦
,个人成长与财富,教育与健康,对人生有了跟多的思考。

记得我是
2018年5月
加入
格局
,在大四实习的时候每天上班路上都会在
喜马拉雅
上听赵老师的音频。

当你看到我所看到的世界,你将重新认识整个世界。

这么多年来,格局打开了我的眼界,也让我对人生和梦想有了更多的思考和感悟,一步一步践行格局精神,也一步一步实现了自己的梦想,感恩。

就业

2023年7月
裸辞之后,我给了自己一段时间规划未来的人生,在
生财有术
阅读了很多文章,在
郑子铭的月度思考
记录了自己的感悟,最终觉得还是需要找一份工作。

我发现裸辞之后的心态和带薪休假时完全不同,没有那种想着四处走走逛逛的欣喜,可能是经济实力的原因。

随后我开始寻找目标企业目标岗位,投递简历,准备面试,期间共参与了四场面试。

第一场面试是朋友介绍的一家初创公司,基本免试通过,但是我感觉不是很合适自己,所以拒绝了。

第二场面试和面试官畅聊了一个小时,虽然有一些问题自己回答得不是很满意,但是整体感觉还是不错的,可是等了很久也没有面试结果反馈,我也不确定是岗位不招人了,还是其他原因,最终不了了之。

第三场面试需要先完成面试官的算法题,然后现场手撸代码,由于缺乏这方面的经验以及面试官对面试者的技术期待比较高,最终半小时就结束了面试。

第四场面试就比较顺利,双方也聊得很不错,直接询问入职时间,最终拿到了offer。

热辣滚烫

最近看了
贾玲
导演的电影《热辣滚烫》,本来一开始对过度宣传的影片兴趣不大,还好后来在
B站
看了电影解说,有了一点兴趣,于是便买票前往电影院观看。

观影过程热泪盈眶,可能我对梦想的力量比较认可,也对贾玲的付出比较触动,我感觉这部电影可以影响很多的人,给很多人在某些时刻带来力量,强烈安利。

猫猫狗狗

今年也认识了很多猫猫狗狗,毛孩子们的治愈力量是无穷无尽的,尽管大多数家长都不让孩子养宠物,但是无法阻挡年轻人对他们的喜爱。

2024 年计划

  • 好好工作
  • 热爱生活
  • 追逐梦想

总结

简简单单,与我常在,感恩

知识共享许可协议

本作品采用
知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接:
http://www.cnblogs.com/MingsonZheng/
),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

如有任何疑问,请与我联系 (MingsonZheng@outlook.com) 。

写在开头

昨天在写《
HashMap很美好,但线程不安全怎么办?ConcurrentHashMap告诉你答案!
》这篇文章的时候,漏了一个知识点,直到晚上吃饭的时候才突然想到,关于ConcurrentHashMap在存储Key与Value的时候,是否可以存null的问题,按理说这是一个小问题,但build哥却不敢忽视,尤其在现在很多面试官都极具挑剔的环境下,万一同学们刷到了咱的博客,回答中遗漏了这个小细节,错过了面试官的考验,那咱可就成罪人了。
接下来我们就将HashMap、Hashtable、ConcurrentHashMap这三集合类的键值是否可以null的问题,放一起对比去学习一下。

Hashtable的键值与null

虽然我们在讲解HashMap与Hashtable作对比时,已经说了Hashtable在存储key与value时均不可为null,但当时的侧重点全在HashMap身上,就没有详细的解释原因,下面我们跟进put源码中去一探缘由。

【源码解析1】

public synchronized V put(K key, V value) {
        // 确认值不为空
        if (value == null) {
            throw new NullPointerException(); // 如果值为null,则抛出空指针异常
        }
 
        // 确认值之前不存在Hashtable里
        Entry<?,?> tab[] = table;
        int hash = key.hashCode(); // 如果key如果为null,调用这个方法会抛出空指针异常
        int index = (hash & 0x7FFFFFFF) % tab.length;//计算存储位置
 
        //遍历,看是否键或值对是否已经存在,如果已经存在返回旧值
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
 
        addEntry(hash, key, value, index);
        return null;
    }

通过Hashtable的put底层源码,我们可以看到,方法体内,首先就对value值进行的判空操作,如果为空则抛出空指针异常;其次在计算hash值的时候,直接调用key的hashCode()方法,若keynull,自然也会报空指针异常,因此,我们在调用put方法存储键值对时,key与value都非null。

HashMap的键值与null

我们同样也通过HashMap的put方法去分析它的底层源码,先上代码。

【源码解析2-hash()】

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

在计算hash值的时候,hashmap中通过三目运算符做了空值处理,直接返回0,这样最终计算出key应该存储在数组的第一位上,且key是唯一性呢,因此,key最多存一个null;

【源码解析3】

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    // 数组
    HashMap.Node<K,V>[] tab; 
    // 元素
    HashMap.Node<K,V> p; 

    // n 为数组的长度 i 为下标
    int n, i;
    // 数组为空的时候
    if ((tab = table) == null || (n = tab.length) == 0)
        // 第一次扩容后的数组长度
        n = (tab = resize()).length;
    // 计算节点的插入位置,如果该位置为空,则新建一个节点插入
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    ///
}

回归putVal()方法,我们逐句阅读后也没有发现对于value值为null的处理与限定,因此,它可以存储为null的value值,我们知道HashMap的键值对特点如同身份证与人名一样,key等同于身份证,全国唯一,而value值等同于人名,可以重复,比如全国有上万个叫张伟的,所以value值也就同样允许存储多个null。

ConcurrentHashMap的键值与null

很多同学们可能会以为ConcurrentHashMap不过是HashMap在多线程环境下的版本,底层实现都一致,只是多了加锁的操作,所以二者对于null的允许程度是一样。
如果你是这样想,那可就完全错了,对于ConcurrentHashMap来说,它也不允许存储键值对为null的数据。
Doug Lea(ConcurrentHashMap的设计者)曾这样说道:

The main reason that nulls aren't allowed in ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps) is that ambiguities that may be just barely tolerable in non-concurrent maps can't be accommodated. The main one is that if map.get(key) returns null, you can't detect whether the key explicitly maps to null vs the key isn't mapped. In a non-concurrent map, you can check this via map.contains(key), but in a concurrent one, the map might have changed between calls.

大致的意思是,在单线程环境中,不会存在一个线程操作该 HashMap 时,其他的线程将该 HashMap 修改的情况,可以通过 contains(key)来做判断是否存在这个键值对,从而做相应的处理;
而在多线程环境下,可能会存在多个线程同时修改键值对的情况,这时是无法通过contains(key)来判断键值对是否存在的,这会带来一个二义性的问题,Doug Lea说二义性是多线程中不能容忍的!

啥是二义性?
咱们通俗点讲就是一个结果,2种释义,就好比我们通过get方法获取值的时候,返回一个null,其实我们是无法判断是值本身为null还是说集合中就没这个值!

所以说,ConcurrentHashMap的key和value均不可为null。

结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得
留言+点赞+收藏
呀。原创不易,转载请联系Build哥!

如果您想与Build哥的关系更近一步,还可以关注俺滴公众号“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!

前言

今天大姚给大家分享一个.NET 全能 Cron 表达式解析类库,支持 Cron 所有特性:TimeCrontab。

Cron表达式介绍

Cron表达式是一种用于配置定时任务的时间表达式。它由一系列字段组成,每个字段代表任务在不同时间维度的调度规则。Cron 表达式常用于各种系统中,如操作系统的定时任务、应用程序的定时调度、数据备份等。

项目特点

  • 支持 Cron 所有特性
  • 超高性能
  • 易拓展
  • 很小,仅 4KB
  • 无第三方依赖
  • 开源、跨平台
  • 高质量代码和良好单元测试
  • 支持.NET Framework 3.5+及后续版本

项目源代码

项目安装

创建一个名为
TimeCrontabExercise
的.NET 8 控制台应用。

搜索:
TimeCrontab
NuGet包安装。

快速入门

using TimeCrontab;

namespace TimeCrontabExercise
{
    internal class Program
    {
        static void Main(string[] args)
        {
            //常规格式:分 时 天 月 周
            var crontab = Crontab.Parse("* * * * *");
            var nextOccurrence = crontab.GetNextOccurrence(DateTime.Now);

            //支持年份:分 时 天 月 周 年
            var crontab1 = Crontab.Parse("* * * * * *", CronStringFormat.WithYears);
            var nextOccurrence1 = crontab1.GetNextOccurrence(DateTime.Now);

            //支持秒数:秒 分 时 天 月 周
            var crontab2 = Crontab.Parse("* * * * * *", CronStringFormat.WithSeconds);
            var nextOccurrence2 = crontab2.GetNextOccurrence(DateTime.Now);

            //支持秒和年:秒 分 时 天 月 周 年
            var crontab3 = Crontab.Parse("* * * * * * *", CronStringFormat.WithSecondsAndYears);
            var nextOccurrence3 = crontab3.GetNextOccurrence(DateTime.Now);

            // Macro 字符串
            var secondly = Crontab.Parse("@secondly"); //每秒 [* * * * * *]
            var minutely = Crontab.Parse("@minutely"); //每分钟 [* * * * *]
            var hourly = Crontab.Parse("@hourly"); //每小时 [0 * * * *]
            var daily = Crontab.Parse("@daily"); //每天 00:00:00 [0 0 * * *]
            var monthly = Crontab.Parse("@monthly"); //每月 1 号 00:00:00 [0 0 1 * *]
            var weekly = Crontab.Parse("@weekly"); //每周日 00:00:00 [0 0 * * 0]
            var yearly = Crontab.Parse("@yearly"); //每年 1 月 1 号 00:00:00 [0 0 1 1 *]
            var workday = Crontab.Parse("@workday"); //每周一至周五 00:00:00 [0 0 * * 1-5]
        }
    }
}

项目源码地址



更多项目实用功能和特性欢迎前往项目开源地址查看