- 2025-03-26
-
回复了主题帖:
《汽车电子硬件设计》阅读报告(3)
Jacktang 发表于 2025-3-19 07:45
防反接保护的图6-6的MOS管的画的有意思
是的,这个设计考虑周全。不仅要能够起到反接保护的作用,同时还需要考虑出现故障的情况时,该故障能不能被后续的采样口检测到
- 2025-03-17
-
发表了主题帖:
《汽车电子硬件设计》阅读报告(3)
这次报告向各位专家老师分享《汽车电子硬件设计》的第三部分,这部分作为全书的核心,深入探讨了具体电子模块的设计方法与原则。由于汽车电子模块设计对实际经验的依赖性较强,因此在遵循书中原理性指导的同时,结合实践是实现更优设计的关键。
1.低压电源设计
汽车电子模块中的低压电源肩负着将蓄电池电压转换为逻辑电路常用5V或3.3V电压的重要任务,其设计涵盖防反接保护、浪涌抑制电路、电压监测电路以及低压转换电路四个部分。书中以典型的12V转5V转换电源电路结构为例,为我们呈现了各部分的详细构成。
图中各个部分的构成如下:防反接电路通过二极管D1和功率防反接模块实现;浪涌抑制通过大功率 TVS 管 VZ1,配合共模电感 L1 限制电流变化速度,以及后续的 RS 和 Cf1 滤波来实现浪涌抑制,最前面的防静电电容 C1 和 C2 也参与其中;电压监测主要通过电压采样电阻Rvd1和Rvd2,以及相关的滤波电容、去耦电容和旁路电容Cf4,Cf2,Cf3实现;低压转换电路主要使用LDO和稳压管实现。
现在对每一模块的设计进行介绍,由于篇幅限制很难面面俱到,主要分享书中给出的典型设计方法。
(a)防反接保护
防反接保护是低压电源设计的首要防线,旨在防止电源反接对电路造成损坏。常见的设计是以二极管为核心,但二极管存在电压降且在大电流下易发热。书中提出了更优的解决方案 —— 采用 PMOS 管或继电器。以 PMOS 管反接保护为例,其优势在于通过简单拉低栅极即可驱动,且导通电阻小,因此在功率电源端的保护电路中广泛应用。
如图中VF为PMOS管,如果反接则电路处于断开状态。而该电路的设计限制主要在于:
首先,PMOS 管导通时,VT 的基极需为高电平,此时 VF 处于截止状态,这就要求 VF 上的压降不能达到 VT 开通的压降,因此需严格限制 VF 的 Rdson。
其次,采样电路需通过 DIG_VBATT_P 端口监测电压以诊断电路状态,必须根据 VT 的基极和集电极电流确定故障时的电流 IC 和 Rlm4 的乘积,既要能被单片机识别为高电平,又不能过大以免损坏单片机端口,计算如下。
此外,由于 PMOS 反向耐压有限,在反接或遭遇浪涌时,需借助 VZ 和 Rlm1 进行能量泄放,以保护 PMOS。最后,还需考虑 PMOS 的发热问题,这需要综合核算导通电流和导通电阻,合理选择器件,计算如下。
综合下来,需要通过计算以后的参数需求对器件进行选型。
(b)浪涌抑制电路
静电保护主要通过静电电容来实现。设计时需确保静电瞬间产生的电荷能被静电电容完全吸收。如下为静电电容的模型,其选择考虑可以参考之前针对元器件的参数选择部分。
在设计浪涌抑制电路时,TVS 管的选型至关重要。其设计电压参数主要包括最大反向工作电压 VRWM、击穿电压 VBR 和最大钳位电压 VC,其中击穿电压是重点考虑因素,而 VRWM 一般在 0.8 - 0.9 倍的 VBR。电流参数则包括漏电流 IR 和最大反向脉冲峰值电流 IPP。最大钳位电压和反向脉冲峰值电流共同决定了反向峰值脉冲功率 PPP,对于不同的脉冲波形、脉冲时间和温度,实际应用环境下的 TVS 管脉冲功率需参考特定曲线。
TVS 管的功率可通过特定模型进行计算,确保其额定瞬态脉冲功率大于所加最大瞬态浪涌功率。
(c)电压监测电路
电压监测模块主要通过ADC采样实现,通过分别对电源电压和地线电压进行测量,可以得到理想情况下的电压。
但是由于电阻误差、端口电流泄露、模数转换的比特误差等等,进行如下的修正,按照1/3的分压比,1%的电阻精度,±2%的电压精度,以及±3bit的比特位误差,1μA的泄露电流来算,得到如下表格。
以电子模块 9 - 16V 的工作情况为例,理想条件下 ADC 采集范围应为 457 - 812,但考虑误差后,需修正到 420 - 875 作为正常工作的判决门限。然而,这种修正可能导致电压为 18V 时仍被误判为正常,因此需进一步提升 ADC 的采样精度,可采用将电阻的精度和温度分离等方法。
同时,为避免误差或上下限电压工作点附近的交流波动,通常需设计迟滞门限,防止电源模块在不同工作状态之间频繁切换。这通常通过以高速比较器为核心的过电压和欠电压电路来实现。
(d)低压转换电路
最后是电源转换的核心部分——低压稳压器(LDO)的设计,该模块通常已经有相当成熟的电源芯片,我们需要根据使用场景参数要求进行合理选型。
书中提到主要的参数依据为:
输出电压。不仅需要考虑电压值,也要考虑电压的精度;
最大输出电流。输出电流越大的成本越高、封装越大,需要核算抵押逻辑电路的电流需求进行选择;
最小电压差。一般选择0.5V左右的最小电压差的LDO;
接地电流。LDO在输出管电流为0时,实际消耗的工作电流。
集成功能。比如使能端口、看门狗电路、复位单元等。
除此之外,也需要关注LDO的散热问题,其功耗主要是来自调整管的功耗和偏置电路的功耗,通过如下的式子进行估计。
2.汽车电子输入电路
汽车电子的输入电路处理部分主要分为数字信号和模拟信号两大类。其中,数字信号根据电平特性又可细分为高电平有效信号和低电平有效信号。高电平有效信号无需接地,而低电平有效信号则需解决地线回路问题。
书中以低电平有效信号的输入电路为例,给出了分析和设计电路的流程,如下:
确认开关模型和线束模型。开关和线束通常可以被抽象成上面的表格和如下的模型;
确认模块环境和建立设计约束。比如湿电流、对电源和地的短路保护、单片机的电平约束、钳位和注入电流、静电和传导干扰;
通过设计约束构建基本的电路拓扑。将约束转换成对应的电路参数并进行正向的设计;
对电路进行验证。包括对电路内部参数检验和对电路能承受外部模型的极限值的验证。
以低电平有效电路为例,采用如下的基本结构,其中可设计的参数包括上拉电阻RP,分压电阻RVD1和RVD2,滤波电容CF1和CF2。
同时,我们考虑将设计约束转换成计算条目,并通过下表中的电路验证内容来进行检验,将其合并到整个电路的模型中。
基于此模型,画出开关导通时的等效模型图,列出输入端口和单片机端口的输入输出电流公式,可以用矩阵的表示方法来列出端口电压和单片机引脚上的电压。
由于其中存在许多未知参数,因此需要先通过简化得出大致范围,在根据选择的数值进行验证迭代。具体如下:
(a)上拉电阻的选择
根据以下湿电流的计算式
要求在9V电压下达到1mA,式中:
VBATT为电源电压,范围为9~16V,在该条件下取9V;
VR_DP为反接保护压降,一般为二极管,范围为0.4~1.2V,以最极端情况来取1.2V;
VSW_DP为开关管压降,一般为PNP,范围为0~0.3V,以最极端情况取0.3V;
VGFF为地偏移,范围为-1~1V,以最极端情况取1V;
RON为开关导通电阻,这里认为是100Ω。
这样就能得到对该约束条件的RP最大值为6.4kΩ。然后考虑电阻的功耗,依旧取最极端情况VBATT=16V,VR_DP=0.4V,VSW_DP=0V,VGFF=1V,而确定1210电阻的封装下,散热能力容许功耗256mW,可以计算得到最小值为0.864kΩ。根据电阻精度表中可得的电阻值,最终可以确定为3.3kΩ。
(b)分压电阻的选取和电阻比的计算
根据如下的式子联立
产生高低电平的阈值:VIH_MIN取值为0.7倍VDD;VIL_MAX取值为0.3倍VDD,并考虑系统波动在1%~2%,得出阈值为3.43V和1.53V,计算得到在VBATT=9V,VR_DP=1.2V,VSW_DP=0.3V,VIH_MIN=3.43V时,计算出分压比β最小值为0.84(一般会留一定的裕量,取1.4以上)。
根据以下的单片机注入电流的式子
选取最极端情况下VBATT=16V,VR_DP=0.4V,VSW_DP=0V,VCLAMPED为供电电源加上钳位二极管压降0.3V,5V系统电压存在1~2%的误差,范围为4.9~5.1V,因此VCLAMPED最极端情况为5.2V。假定设计注入电流最大为100μA,以及β=1.5,根据此约束条件可以取得RVD1的最小值为69kΩ。
同时当开关断开时,滤波电容CF2上的电荷会通过RVD2泄放,故电阻值不宜过大。
可以根据充放电时间常数的式子的约束,一般设计充放电时间常数为5ms,CF2一般取10nF,根据此约束得到RVD1最大值为300kΩ。因此在该范围内可以选取的电阻值可以是RVD1=100kΩ,RVD2=150kΩ;或者RVD1=200kΩ,RVD2=300kΩ。
(c)防静电电容的计算
需要按照规范设计,CF1和静电模型的输出电容分压,最终取值为10nF。
根据设计的到的参数RP=3.3kΩ,RVD1=100kΩ,RVD2=150kΩ,可以通过外部到内部和内部到外部两种方式验证。
外部到内部是根据既定的模型参数计算工作情况,复杂的电压函数可以通过软件进行多元函数微分求极值的方式来验证范围。比如在上述设计下,可以计算出开关闭合时单片机引脚最高电压为0.852V,小于低电平的阈值1.53V,可以被正确识别为低电平;而在开关断开时,单片机引脚上的电压为3.946V,大于高电平的阈值3.43V,可以被正确识别为高电平;开关的湿电流为2.417mA,满足大于1mA的设计需求;上拉电阻功耗为84.498mW,小于256mW的设计需求;最后注入单片机的电流为79.945μA,同样满足设计需求。
而从内部到外部的方法则是通过计算电路的极限负载来完成,根据模块的临界阈值可以计算出端口电压在何种情况下会被识别为高电平和低电平。比如在上述的参数设计中,端口电压低于2.213V就能保证开关被识别为低电平,高于6.472V就能够被识别为高电平,对应最小低电平输入电阻和最大高电平输入电阻为275Ω和28.5kΩ。
3.汽车电子输出电路
输出电路最主要分为信号输出和功率输出,常用的开关器件如下表中所示。在功率输出方面,常用的开关器件包括电磁继电器和智能功率开关等,其中智能功率开关因集成多种保护功能而被广泛应用。书中对智能功率器件进行了重点介绍和深入分析,该器件具备欠电压保护、过电压保护、过电流保护、短路保护、过热保护等多重保护功能,能够有效提升系统的稳定性和可靠性。
智能功率开关的功耗分析分为常导通情况和 PWM 导通情况两种。常导通情况下的功耗计算相对直接,可依据特定公式进行。而在 PWM 导通情况下,由于智能功率开关的驱动集成且电压上升率固定,可按照细分的三阶段上升沿斜率进行分段计算,计算过程如下。
最后将三段求和可以获取整个功率损耗,并根据开关的热阻核算其是否可以控制温度在开关热保护的温度以下运行。如果存在散热的问题,则需要调整散热功率、调整器件型号或者改变散热方式。
而对于电磁继电器,主要是通过线圈和触点两个方面进行参数分析,其中线圈的工作电压遵循下图中的变化关系,同时由于线圈等效电阻的存在,还有最大连续施加电压的限制,以避免过大电流导致线圈的高温度应力而损坏;而触点参数主要包括接触电阻、电压、电流以及相应吸合释放的时间参数;同时还有比如绝缘电阻、击穿电压、浪涌耐压等安全性参数。
在对继电器参数分析时,首先考虑其电压参数,主要包含吸合电压、释放电压和最高电压。这些电压参数在冷启动和热启动时有所差别,冷启动指线圈刚开始工作的条件,温度基本与环境温度一致;而热启动则是其工作一段时间以后,可能由于发热而高于环境温度,对电压有更加苛刻的条件,可以看如下示意图。
因此在设计时需要考虑电磁继电器整体温升对电阻的影响,其来源主要是线圈等效电阻的发热和触点等效电阻的发热,负载上的温升也可以通过如下经验公式估计。
继电器线圈的浪涌抑制是另一个设计考量,因为在开关断开的瞬间会有较大的直流浪涌,需要进行抑制。但是过度的抑制可能会导致继电器释放时间过长,触点断开变慢和触电反弹加剧。书中给出了一系列抑制线圈浪涌的设计参考。
对于触点的防护,主要需考虑感性负载在关断瞬间产生的反电动势造成的浪涌,书中也给出了反电压抑制电路的设计参考。
4.全书总结
本次对《汽车电子硬件设计》一书的分享至此结束,后续部分对主控单元设计进行了简要介绍,涵盖了一些单片机基础及制图设计相关内容。但由于本书主要聚焦于电子器件,故此部分较为简略,在此不再赘述。
总体而言,这本书具有极强的理论指导意义,书中不仅提供了大量详实的设计案例分析,而且对各类设计参数以及汽车电子模块的设计规格和要求阐述得十分细致入微。初读此书,便能深切感受到其对工程实际考量的深刻洞察,为读者提供了丰富的借鉴思路与实践指导。毫无疑问,这本书在未来的实际工作中将发挥重要作用,可作为说明手册和工具书反复查阅,为汽车电子硬件设计持续提供智慧支持。此次阅读收获颇丰,是一次极具价值的知识探索之旅。
-
回复了主题帖:
《汽车电子硬件设计》阅读报告(2)
Jacktang 发表于 2025-3-14 07:23
这本书功率MOSFET的部分讲的还是比较深的
确实,功率MOSFET是汽车电子模块非常重要的部分
- 2025-03-13
-
发表了主题帖:
《汽车电子硬件设计》阅读报告(2)
本帖最后由 Aclicee 于 2025-3-13 22:39 编辑
年后开始因为频繁外出出差,阅读进度受到了一些影响,也很少关注论坛,所以阅读报告的分享有一些延迟,非常抱歉!不过最近终于有了一些空闲时间,我打算把《汽车电子硬件设计》这本书的后半部分学习完成。今天向各位老师汇报书本的第二部分内容。在此之前,我想先对功率MOSFET的部分做一个补充汇报。
在上一篇阅读报告发布后,有朋友留言讨论有关MOSFET开启和关断的过程,书中给出了栅极和漏极电流与电压的变化波形。这里我进一步分享一下,以解决当时的疑问。书中的示意图展示了一个理想MOSFET开启过程,该过程包含以下4个阶段:
Vgs上升直至达到阈值电压Vth。此时,栅极电流主要用于给栅极电容Cgs充电。由于MOSFET尚未导通,Vds保持高电压应力状态,漏极电流为零;
Vgs继续上升并超过Vth,MOSFET进入饱和区。在此区域,导通电阻相对较大,因此Vds下降不明显。随着Vgs的升高,漏极电流Ids逐渐增加,与Vgs呈正相关。
当Ids达到最大值时,Vgs保持稳定。此时,栅极电流主要给Cgd充电,同时Cds的能量开始释放,导致Vds逐步下降。MOSFET从饱和区向线性区过渡,导通电阻也随之逐渐减小。这一阶段Vgs的稳定是由于米勒效应所致,即漏极电流达到最大时,Vgs不会立刻继续上升。
Vgs继续升高,栅极电流同时给Cgs和Cgd充电,MOSFET完全退出饱和区,进入线性区。此时,MOSFET以较小的导通电阻维持导通状态。
在关断过程中,同样会观察到类似的米勒平台现象。这一分析有助于深入理解MOSFET的工作特性及其在电路中的动态行为,对汽车电子硬件设计中的开关电路优化具有重要意义。
接下来分享书中的第二部分内容,主要是有关汽车电子的开发流程和可靠性分析。
1.汽车电子开发流程
汽车质量保证标准的不断升级推动了IATF 16949体系架构的发展。该体系以顾客为中心,包含产品先期质量策划(APQP)、生产件批准程序(PPAP)、潜在失效模式及影响分析(FMEA)、测量系统分析(MSA)和统计过程控制(SPC)五个核心方面。这些工具共同构成了IATF 16949的核心,对于提升汽车行业的质量管理水平和竞争力具有重要意义。
产品先期质量策划:APQP是一种结构化的方法,用于产品从设计到生产的整个过程。它分为五个阶段:计划和确定项目、产品设计和开发、过程设计和开发、产品和过程的确认、反馈评估和纠正措施。在汽车电子开发中,APQP确保每个阶段都有明确的目标和任务,例如在产品设计阶段,需要考虑电子模块的功能需求、环境适应性和可靠性要求;在过程设计阶段,要规划生产流程、设备选型和质量控制点。
生产件批准程序:PPAP用于验证生产过程是否能够稳定生产出符合客户要求的产品。在汽车电子中,PPAP要求提交一系列文件和样品,确保电子模块在大规模生产前满足所有质量标准。
潜在失效模式及影响分析:FMEA是一种预防性的分析工具,用于评估产品或过程中潜在的失效模式及其影响,并采取相应的预防措施。在汽车电子开发中,FMEA分为设计FMEA(DFMEA)和过程FMEA(PFMEA)。DFMEA关注产品设计可能存在的缺陷,如电路设计中的单点故障、元件选型不当等;PFMEA则关注生产过程中的潜在失效,如焊接质量问题、装配错误等。
测量系统分析:MSA用于评估测量系统的准确性和可靠性,确保测量数据的质量。在汽车电子中,MSA关注测量系统的重复性和再现性,即同一个测量设备在相同条件下多次测量的波动,以及不同测量设备或人员测量结果的一致性。通过MSA,可以确保用于质量控制的测量系统能够提供可信的数据,从而有效监控生产过程和产品质量。
统计过程控制:SPC利用统计方法对生产过程进行监控,及时发现并解决潜在的质量问题。在汽车电子生产中,SPC通过收集和分析生产过程中的数据,如关键尺寸、电气参数等,来评估过程的稳定性和能力。
这五大工具是IATF 16949的核心,对于提高汽车行业的质量管理水平和竞争力将起到重要作用。那么具体在执行电子模块开发和设计的时候,书中也介绍了一个V型开发过程,完整的包含需求分析、顶层设计、细节设计、系统实施、单元测试、集成测试和设计验证等阶段。
在设计和开发流程中,除了满足设计要求外,可靠性是另一个至关重要的因素。FMEA、MSA和SPC等工具被用于探讨器件可能产生的误差、异常或潜在失效的可能性。例如,FMEA通过系统地分析潜在的失效模式及其影响,帮助设计团队提前识别风险并采取相应的预防措施。MSA则关注测量系统的准确性和可靠性,确保测试结果的真实性和可信度。SPC利用统计方法对生产过程进行监控,及时发现并解决潜在的质量问题。此外,“循环设计”理念也强调了对产品质量和失效问题的持续分析与改进,以不断提升产品的可靠性和性能。
2.硬件设计的可靠性
可靠性是指产品在规定的期限和条件下,能够实现预定功能的能力。它是衡量产品质量和性能的重要指标,尤其在汽车电子硬件设计中具有至关重要的地位。可靠性通常通过失效率和平均无失效时间(MTBF)两个关键参数来判断。
大多数元器件的失效率遵循特定的时间函数,呈现出典型的浴盆曲线特性。在早期使用阶段,失效率相对较高,这些失效通常是由设计缺陷、原材料问题或制造工艺瑕疵引起的。随着时间推移,产品进入正常运行阶段,失效率会降至较低的稳定水平。此时的失效多为偶然失效,主要由外部环境因素、使用失误或材料自身弱点等引起。然而,随着运行时间的进一步增长,元器件会逐渐出现磨损和老化现象,失效率也随之重新升高。
失效率的预测是可靠性分析的重要环节,通常基于大量数据统计来进行。书中提供了各种电子元器件失效率的统计数据和计算公式,设计人员可以根据不同影响因素的叠加效应,综合分析和预测整个系统的失效率。通过这种基于数据的方法,可以在设计阶段提前识别潜在的可靠性问题,从而采取相应的改进措施。
需要注意的是,失效是一个复杂的概念,电子元件的失效可能表现出多种形式,如短路、开路或参数偏移等。书中进一步给出了不同元器件各类失效模式的统计数据,帮助设计人员在设计时对关键失效模式进行重点关注和预防。
对于失效问题的解决,书中提出了多种方法。其中,调整元器件的应力负荷是一种有效手段,因为元器件的失效率通常与其所承受的各种应力直接相关。如果发现失效率不理想,可以通过降低应力负荷来提高可靠性。更简单直接的方式是采用降额设计策略,即选择的器件在实际工作中承受的应力低于其额定值,留有一定的裕量。例如,书中建议二极管的工作电流控制在额定电流的80%以内,反向电压控制在额定电压的70%以内。这种设计方法能够有效降低元器件的失效风险,提高整个系统的可靠性。
除了在正常工作条件下的失效,器件还可能因为极端的工作条件而失效。书中提到了针对这种失效的最坏情况分析,主要可以通过极值分析、平方根分析以及蒙特卡洛分析来尽可能还原器件可能工作的各类场景。极值分析考虑了器件参数的极端值,以评估在最不利条件下器件的性能。平方根分析则用于评估参数变化对系统性能的影响,特别是在参数变化较大时。蒙特卡洛分析通过随机抽样器件参数的分布,模拟大量可能的工作场景,从而统计出失效的概率分布。
基于以上器件可能遭遇的失效情况,进一步需要分析不同的失效情况可能造成的影响,也就是前文提到的潜在失效模式及影响分析(FMEA)。书中指出,FMEA过程会通过严重度(S)、频度(O)以及探测度(D)三个维度来对潜在失效影响进行量化,三者都是介于1-10之间的整数,三者的乘积即为风险系数(Risk Priority Number, RPN)。因此,做好FMEA的关键在于正确定义产品和过程特性,识别高风险失效模式并对其进行优化。以下给出了FEMA分析的流程。
作为汽车电子模块,还有一个需要考虑的问题就是热的问题。散热不周可能导致元器件的损毁,或者寿命的缩短,也需要被包含在可靠性设计之内。热设计是一个非常大的课题,书中主要介绍了理论的稳态散热计算方法,用于给设计提供基本的参考。主要包含以下几个步骤:
估算热损耗功率:主要利用直流稳态的计算方法,考虑元器件所有功耗转化为热量。这是非常粗略的估算方法,实际情况会更加复杂,还有极端工作环境都没有考虑在内;
估算散热功率:主要以热阻来表征元器件阻止热流通过的能力,主要包括元器件内核到外壳、外壳到环境以及内核到电路板等多个散热途径。在简略分析中,会大致集总成一个元器件到环境的热阻值进行估计。数据手册中所说的最大允许耗散功率通常是针对常温测试条件给出的,所以实际通过热阻计算得出的最大允许耗散功率要比数据手册上的参考值小很多,需要注意。
计算结温:结温广义指元器件内部温度最高的部分,即通过产热和散热的比值换算以后,依旧无法散出而积累在元器件中使得器件内温度升高。不同器件和工作环境会对产热和散热功率的比值有限制(比如小于70%~100%),也对最高的结温有限制(比如比元器件允许的极限温度低15~20℃)。
当然这个分析是比较理想化的,热特性和热传播的模型本身就是非常复杂的,同时书中也提到了电路板的印制线也存在发热的情况,因此对于汽车电子模块的热分析还是有不断探索和优化的空间。
- 2025-02-11
-
发表了主题帖:
《汽车电子硬件设计》阅读报告(1)
本帖最后由 Aclicee 于 2025-2-11 21:31 编辑
2025年1月27日拿到的书本,书本包装完好,塑封完整,纸张也非常有质感,印刷清晰,图文并茂。过年期间也是抽出一段时间翻阅了一下书籍,按照约定进行第一次测评报告的分享。由于这本书和之前阅读的实践教程不太一样,更加偏向于理论知识,所以在报告中很难按照步骤面面俱到的解读,仅摘取我们在阅读中觉得比较有价值的精华部分。
这次分享主要涵盖的内容是书本的前三章,重点关注汽车电子系统的概况和组成部分、汽车电子的应用环境,以及这些环境可能对电子元器件造成的影响,对整个系统设计的要求进行初步的了解。
1.汽车电子系统介绍
现代汽车电子系统通过多层级硬件协同实现环境感知、决策控制与人机交互功能,其核心模块可分为以下六类:
传感器系统:传感器是汽车电子系统的“感官”,广泛应用于动力总成、底盘、车身系统等领域。它们负责采集汽车运行中的各种工况信息,如车速、温度、压力等,并将其转化为电信号传输给计算机,以便发动机和其他系统处于最佳工作状态。从发展趋势来看,传感器正朝着小型化、轻量化、集成化、智能化的方向发展。此外,多传感器融合技术将成为主流,摄像头、雷达、激光雷达和超声波传感器的组合将为自动驾驶提供更全面的感知能力。
汽车开关组件:汽车开关作为各种控制器的操纵件,负责整车60%以上功能的开启和关闭,比如旋转式开关、触摸式开关等,通过不同的操作方式实现对汽车各种功能的控制。汽车开关也在不断朝着智能化与电子化方向发展,以更好地适应消费者的使用习惯和需求,提升驾驶的便利性和安全性。集成化与虚拟化也将成为重要趋势,开关将更多地集成在中控大屏中,以软开关的形式存在,既能降本又满足内饰整体简洁化和智能化的要求。
控制模块:控制模块是汽车电子系统的大脑,负责接收传感器输入的信号,并根据预先设计的程序计算各种参数,然后向执行器输出指令,实现对汽车各系统的控制。常见的控制模块包括发动机控制模块(ECM)、车身控制模块(BCM)等。目前电子控制模块开始朝多系统集中的方向发展,集成显著优化系统架构。这不仅能够提升系统的灵活性和可扩展性,还能为用户提供更加个性化的驾驶体验。
执行器:执行器是根据控制模块输出的指令,完成各种预定的动作,如控制燃油喷射、点火、制动等的电路负载。未来,执行器将朝着高精度与高可靠性的方向发展。随着汽车智能化和自动驾驶技术的发展,对执行器的精度和可靠性要求将越来越高。
HMI交互界面:HMI设备是汽车与驾驶员及乘客之间进行交互的桥梁,包括中控屏、仪表盘、语音控制系统等。它们用于显示车辆信息、提供娱乐功能、接收用户指令等。未来,HMI设备将朝着更加智能化和人性化的方向发展,增强现实(AR)和虚拟现实(VR)技术也将逐渐应用于HMI设备中,为用户带来更加沉浸式的交互体验。
汽车线束拓扑:汽车线束是汽车电路的网络主体,用于连接汽车各电子设备和控制模块,实现电能和信号的传输。它在汽车电子系统中起着至关重要的作用,确保各部件之间的通信和供电。随着技术的进步,汽车线束将朝着轻量化和小型化的方向发展。同时,线束将集成更多智能诊断功能,支持故障预警和远程监控。
综上而言,对于目前汽车电子系统的各个模块,轻量化和集成化是必然趋势,智能化和人性化也是必然需求。
另外书中也提到了汽车电子的整体产业链的概况。整个汽车电子产业链涵盖了从原材料供应、零部件制造、系统集成到整车装配的各个环节:
上游主要包括半导体芯片、传感器、电子元器件等原材料供应商,这些供应商为汽车电子系统提供了基础的硬件支持。
中游则是汽车电子零部件制造商,他们负责将原材料加工成各种电子零部件,如控制模块、执行器等。
下游是汽车电子系统集成商和整车制造商,他们将各种零部件组装成完整的汽车电子系统,并将其安装到汽车上。
随着汽车电子化和智能化的发展,汽车电子产业链也在不断演变。上游供应商需要不断提升芯片和传感器的性能,以满足汽车电子系统对高精度、高可靠性和低功耗的要求。中游制造商则需要加强与上游供应商的合作,共同研发高性能的电子零部件。下游的系统集成商和整车制造商则需要更加注重软件和系统的集成能力,以实现汽车电子系统的智能化和个性化。
2. 汽车电子应用环境
汽车电子系统在运行过程中需要面对多种复杂的环境条件,这些条件对电子模块的性能和可靠性提出了严峻挑战。以下是一些书中提及的主要环境因素及其对汽车电子系统的影响:
气候与化学环境:气候与化学环境是汽车电子系统面临的首要挑战之一。高温环境,如发动机舱内可达125℃,容易导致芯片老化、电迁移等问题,进而使模块性能下降甚至失效。而在低温环境下,如-40℃,电子模块的塑料和橡胶部件可能变脆,电池性能也会下降,影响模块的启动和运行。此外,温度的快速变化会引发热应力,使芯片封装材料和核心部分的连接部位变得脆弱,甚至断裂。高湿度环境,则会使水蒸气进入电子模块,导致绝缘损坏、器件腐蚀和材料劣化。同时,清洗剂、盐雾等化学物质也会腐蚀电子模块的外壳和内部元件,影响其使用寿命。
机械负荷:机械负荷是汽车电子系统在运行过程中需要应对的另一大挑战。汽车行驶时的振动会使电子模块的元器件松动、脱焊,影响电气连接的可靠性。此外,汽车碰撞或运输过程中的冲击可能导致模块外壳破裂、内部元件损坏,甚至导致电路短路。
电气负荷:电气负荷对汽车电子系统的正常运行同样至关重要。汽车启动时,电池电压可能降至6-7V,而充电系统故障时电压可能超过18V。这种电压波动要求电子模块能够承受较宽的电压范围,否则可能导致模块损坏或功能异常。此外,大电流负载的导通与断开会在电源线上产生电压尖峰,如发电机“抛负载”现象,可能损坏电子模块。
电磁兼容环境:电磁兼容环境是汽车电子系统需要重点关注的领域。车内各种电气设备以及外部无线信号都会产生电磁干扰。这些干扰可能影响电子模块的正常工作,甚至导致系统故障。因此,汽车电子模块需要具备良好的电磁兼容性,以抵御外部电磁干扰并减少自身对其他设备的干扰。
为了评估上述环境因素对汽车电子模块的安全和性能的影响,通常会进行一系列严格的测试实验。以下提及了一些常见的测试方法及其针对的模块和测试方式:
温度测试:比如恒定温度测试和温度循环测试,旨在评估电子模块在极端温度条件下或者温度快速变化的环境中的性能和可靠性。
机械冲击测试:机械冲击测试的目的是评估模块在机械冲击条件下的结构完整性和电气性能,确保模块在遭受机械冲击时仍能保持正常工作。这一点是汽车电子模块往往会忽略的。
供电不理想测试:供电不理想测试用于评估模块在电压波动和浪涌条件下的工作性能。通过电源模拟器施加过电压、欠电压和浪涌电压,观察模块在这些条件下的响应和恢复能力来确保模块在复杂的电气环境下仍能稳定运行。
传导干扰测试:传导干扰测试用于评估模块对电磁干扰的抗扰度。测试方法是根据国际标准(如ISO 7637和ISO 16750),使用电磁干扰测试设备对模块施加不同类型的干扰脉冲,检查其在干扰条件下的功能状态。通过这种测试,可以确保模块在电磁干扰环境下仍能正常工作。
通过上述测试方法,可以全面评估汽车电子模块在各种复杂环境条件下的性能和可靠性,从而确保其在实际使用中的安全性和稳定性。这些测试不仅有助于发现潜在问题,还能为模块的设计和改进提供重要依据。
3. 汽车电子基本元件
我们作为电力电子领域的学生,有关元器件的基本常识相对是比较熟悉的。但是在实验室和在实际产品的应用之间,我们依旧存在比较大的跨度。这些元件不仅需要满足实验室环境下的性能要求,还需要在复杂的工业环境中保持稳定性和可靠性。书中对这部分的内容做了非常好的补充,详细介绍了汽车电子系统的基本元件及其规范,包括AECQ等级体系、ROHS标准、芯片湿度敏感等级等,并针对不同元件的选择和使用提供了详细的指导。
AECQ等级体系:汽车电子元件可靠性测试的规范,确保元件能够在极端环境下正常工作。AECQ等级体系根据元件的使用环境和可靠性要求进行分类,如AEC-Q100针对集成电路,AEC-Q200针对被动元件。这些标准涵盖了高温、低温、湿度、机械冲击等多种测试条件,确保元件在汽车生命周期内的可靠性。
ROHS标准:规定了电子元件中某些有害物质(如铅、汞、镉等)的最大含量。这一标准旨在减少电子废弃物对环境的影响,同时提高元件的安全性。汽车电子元件必须符合ROHS标准,以确保其在生产、使用和废弃处理过程中的环境友好性。
芯片湿度敏感等级:用于分类芯片对湿度的敏感程度。高湿度环境可能导致芯片封装材料吸湿膨胀,进而影响芯片的电气性能和可靠性。汽车电子系统中使用的芯片通常需要较高的湿度敏感等级,以确保在高湿度环境下的稳定性。
对于不同的元器件选择,书中也提到了不少规范和选取法则:
电阻:厚膜电阻通过丝网印刷技术制造,具有成本低、可靠性高的特点,但精度相对较低,适合一般电路中的限流和分压应用;而薄膜电阻采用真空沉积技术,精度高、温度系数小,适用于高精度电路,如传感器信号调理电路。电阻的精度直接影响电路的性能。在汽车电子系统中,高精度电阻用于关键的信号处理和反馈电路,以确保系统的稳定性和准确性。电阻在工作时会产生热量,需要根据工作环境温度选择合适的功率等级。降功率曲线描述了电阻在不同温度下的最大允许功率,以防止过热损坏。汽车电子系统中存在各种瞬态电压,如启动时的电压浪涌。防浪涌电阻能够承受这些瞬态电压,保护电路免受损坏。
电容:陶瓷电容具有高频特性好、温度稳定性高的特点,适用于高频滤波和去耦电路;电解电容容量大,但温度特性和寿命相对较差,常用于电源滤波和储能;钽电容体积小、容量大、温度稳定性好,适合高密度、高可靠性电路。使用时还需要考虑环境因素的影响。例如,电解电容在高温环境下寿命会缩短,陶瓷电容在高频下可能出现自谐振现象。因此,选择电容时需要考虑其在实际工作环境中的性能偏差。
二极管:主要功能包括整流、稳压和保护电路。整流二极管利用其单向导电性,将交流电转换为直流电,广泛应用于汽车发电机的整流电路中。在选择整流二极管时,需要考虑其正向压降和反向耐压能力。正向压降越小,能量损耗越低,而反向耐压能力则需满足电路中的最大电压需求。稳压二极管则用于电压稳压和过压保护,确保电路在电压波动时仍能稳定工作。选择稳压二极管时,需根据电路需求选择合适的稳压值,并考虑其功耗,以防止因过热而损坏。此外,在汽车电子系统中,二极管还需具备良好的抗浪涌能力和可靠性,以应对启动时的电压浪涌和复杂的电气环境。
晶体管: 主要用于放大信号和控制开关。在放大电路中,晶体管的电流增益是关键参数,它决定了放大倍数的大小。汽车电子系统中的传感器信号调理电路通常需要高增益晶体管来放大微弱信号。在开关电路中,晶体管的最大集电极电流和功耗是重要的考量因素。此外,晶体管的开关速度也会影响电路的性能,尤其是在高频应用中。在汽车电子系统中,晶体管还需要具备良好的温度稳定性和抗干扰能力,以应对复杂的工况和电磁环境。
功率MOSFET:功率MOSFET是汽车电子系统中常用的开关元件,因其低导通电阻和高开关速度而被广泛应用。在汽车电子系统中,功率MOSFET常用于电机驱动、电源管理等高功率应用。低导通电阻可以显著降低功率损耗,提高系统的效率,这对于电动汽车的续航里程和燃油车的燃油经济性至关重要。高开关速度则使得功率MOSFET能够快速切换状态,适用于高频开关电源和脉宽调制(PWM)控制电路。然而,功率MOSFET的寄生参数对其性能有显著影响。寄生电容会影响开关速度和高频性能,过大的寄生电容会导致开关损耗增加,降低系统效率。栅极电荷是另一个重要参数,它影响驱动电路的设计。较大的栅极电荷需要更强的驱动能力,否则可能导致开关速度下降,增加开关损耗。此外,栅极电感、源极电感和漏极电感也可能引起电压尖峰和振荡,影响电路的稳定性和可靠性。在汽车电子系统中,功率MOSFET还需要具备良好的抗浪涌能力和可靠性,以应对启动时的电压浪涌和复杂的电气环境。
- 2025-01-20
-
回复了主题帖:
【入围名单】《汽车电子硬件设计》
个人信息无误,确认可以完成评测计划。
- 2025-01-17
-
回复了主题帖:
《大语言模型开发:用开源模型开发本地系统》阅读报告(3)
风尘流沙 发表于 2025-1-17 17:30
楼主,您好,大语言模型开发:用开源模型开发本地系统
有中文版下载链接吗??
谢谢您
这个是论坛测评活动申请的试读,是实体书来的,不知道有没有电子版。
- 2025-01-15
-
回复了主题帖:
《大语言模型开发:用开源模型开发本地系统》阅读报告(2)
ljg2np 发表于 2024-12-26 11:28
个人感觉transformer模型在NLP得到成功应用之后,逐渐扩大其应用范围,用在电池周期充放电数据的分析研 ...
是的,因为本质上都是时序推理的问题。电池的数据相比于自然语言,还更加具有物理意义上的关联性。
-
发表了主题帖:
《大语言模型开发:用开源模型开发本地系统》阅读报告(3)
在前两周阅读《大语言模型开发》这本书时,发现书中提供了使用Llama2模型进行训练和微调的具体案例,具有很强的实践参考价值。书中说明Llama-2-7b模型的代码可通过在Hugging Face网站申请获得,于是我们在该网站提交了申请,认为其开源性质应该能够顺利使用。但由于期末考核任务繁重,一直没有关注申请进度。直到前两天准备更新报告时,才发现申请并未通过。
因此,本次报告我们只能基于书中的理论知识,纸上谈兵地向各位老师汇报我们的学习成果。另外,不知道其他朋友是否成功申请过,或者是否遇到过类似的情况,也请大家分享一下经验。
我们将按照书中介绍的模型训练过程对其中涉及的关键模块逐一介绍,整个训练流程的组成部分如下图所示:
1. 预训练模型
在模型训练过程中,预训练模型是不可或缺的一部分。我们通常不会从头开始构建一个大语言模型,因为这需要巨大的计算资源和数据。因此,我们主要通过Hugging Face的transformers库来引入预训练模型。transformers库是一个非常强大的工具,它提供了数以千计的预训练模型,支持多种语言的文本分类、信息抽取、问答、摘要、翻译、文本生成等任务。它提供了便于快速下载和使用的API,让你可以把预训练模型用在给定文本、在本地的数据集上微调。
在我们的案例中,我们使用了AutoModelForCausalLM类来加载预训练的因果语言模型(Causal Language Model)。这个类允许我们从预训练模型库中加载模型,并支持多种参数以自定义加载过程。具体到我们的代码示例中,我们使用了from_pretrained方法来加载'meta-llama/Llama-2-7b-hf'模型。这个方法通过from_pretrained字段配置我们需要的预训练模型,并且可以通过quantization_config参数来选择模型的量化精度。
model = AutoModelForCausalLM.from_pretrained(
'meta-llama/Llama-2-7b-hf',
cache_dir="/kaggle/working/",
trusr_remote_code=True,
device_map='auto',
quantization_config=bnb_config,
)
2. 量化技术
有关模型的量化技术,书中也有所介绍。量化技术是模型优化的重要手段之一,它通过减少模型参数的位宽来减少存储空间和加速计算,同时降低能耗。在我们的代码中,我们使用了load_in_4bit参数来决定是否使用4位量化精度的模型。
由此,我们便引入了第二个比较重要的成分,就是配置量化精度的BitsAndBytesConfig模型,用于设置模型的量化参数。通过这个类,我们可以灵活地配置模型的量化精度、分位数等超参数,从而在减少模型存储空间和加速计算的同时,尽量保持模型的性能。
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type='nf4',
bnb_4bit_compute_dtype='float16',
bnb_4bit_use_double_quant=True,
)
3. 分词器
除此之外,我们还需要使用分词器来处理自然语言任务。分词器(Tokenizer)在自然语言处理(NLP)中是一个关键组件,负责将文本字符串转换成模型可以处理的结构化数据形式,通常是将文本切分成“tokens”或单词、短语、子词等单位。这些tokens是模型理解文本的基础。分词器的类型和复杂性可以根据任务需求而变化,从简单的基于空格的分割到更复杂的基于规则或机器学习的分词方法。
分词器的工作流程通常包括两个关键步骤:
文本分解:将原始文本分割成更细的粒度单元,这些单元可以是单词级别、子词级别,甚至是字符级别。这一步的目标是将文本分解为可以被模型理解并处理的基本单元。
编码映射:将这些基本单元转换为模型可以理解的数值形式,最常见的形式是整数序列。这样,我们就可以将这些数值输入到模型中,让模型进行学习和预测。
此外,分词器还会在序列的开始和结束添加特殊标记,如BERT中的CLS和SEP用于特定任务的序列分类或区分输入片段。有时为了确保输入序列的一致长度,分词器可以对较短的序列进行填充,对较长的序列进行截断。
我们主要是通过AutoTokenizer这个类来进行调用。AutoTokenizer 是 Hugging Face transformers 库中的一个实用工具类,主要用于自动加载与特定预训练模型相匹配的分词器。AutoTokenizer 能够自动识别出模型应该使用哪种类型的分词器,并加载对应的配置文件和词汇表。这使得开发者不需要手动指定分词器类型,简化了代码编写过程。一旦分词器被正确加载,它可以对输入文本进行预处理,包括分词、添加特殊标记、编码转换、填充和截断等操作。
tokenizer = AutoTokenizer.from_pretrained(
'meta-llama/Llama-2-7b-hf',
trust_remote_code=True,
)
tokenizer.pad_token = tokenizer.eos_token
我们也可以输入一些文本内容查看其分词的效果:
inputs = tokenizer("Hello, my dog is cute", return_tensors="pt")
labels = torch.tensor([1]).unsqueeze(0)
outputs = model(**inputs, labels=labels)
loss = outputs.loss
logits = outputs.logits
4. 训练方法
由于大模型参数量众多,我们即便在有预训练模型的基础上也不会去大量训练所有的模型参数,只会对其中部分进行微调。这里我们就会使用到LoRA这个方法,其只会对少量额外的模型参数进行训练,示意图如下:
LoRa(Low-Rank Adaptation)是一种用于对预训练大语言模型进行微调的高效方法。它通过引入两个低秩矩阵A和B来减少需要训练的参数数量,从而在保持模型性能的同时,显著降低训练成本。具体来说,如果原始参数矩阵W的大小为d×d,LoRA 会引入两个大小分别为d×r和r×d的矩阵,其中r是一个远小于d的秩。通过这种方式,LoRA大幅减少了需要训练的参数数量。
我们会从peft这个库来调用LoRa方法,peft(Parameter-Efficient Fine-Tuning)是一个用于高效微调大语言模型的库。它提供了多种参数高效的微调方法,包括 LoRa、Prefix-Tuning 等。peft 库的主要目标是通过减少需要训练的参数数量,提高微调的效率和效果。
以下是书中使用的LoRa的参数配置,其中lora_target_modules这个参数就是指出我们会对Llama模型的哪些部分进行训练,与上面的示意图也是对应的。
lora_alpha=16,
lora_dropout=0.1,
lora_r=16,
lora_bias='all',
model_type='llama',
lora_target_modules = [
'q_proj',
'k_proj',
'v_proj',
'o_proj',
'gate_proj',
'up_proj',
'down_proj'
]
我们可以通过LoRaConfig来应用这些参数设置:
peft_config = LoraConfig(
lora_alpha=lora_alpha,
lora_dropout=lora_dropout,
r=lora_r,
bias=lora_bias,
task_type='CAUSAL_LM',
inference_mode=False,
target_modules=lora_target_modules,
)
model = get_peft_model(model, peft_config)
5. 增量模型训练
接下来我们就可以使用LoRA方法来微调我们的模型参数,我们主要是通过调用tansforemres库中的TrainingArguments和Trainer这两个类。它们是Hugging Face中配置训练参数和用于训练和评估模型的类,封装了训练循环,支持多种功能,如分布式训练、混合精度训练、早停(early stopping)、学习率调度等。有关训练参数的配置主要包括:
output_dir = 'your_dir'
optim_type = 'adamw_8bit'
learning_rate = 0.0005
weight_decay = 0.002
per_device_train_batch_size = 1
per_device_eval_batch_size = 1
gradient_accumulation_steps = 16
warmup_steps = 5
save_steps = 100
logging_steps = 100
然后我们调用TraningArguments来对这些应用这些参数设置:
traing_args = TrainingArguments(
output_dir=output_dir,
evaluation_strategy='epoch',
optim=optim_type,
learning_rate=learning_rate,
weight_decay=weight_decay,
per_device_train_batch_size=per_device_train_batch_size,
per_device_eval_batch_size=per_device_eval_batch_size,
gradient_accumulation_steps=gradient_accumulation_steps,
do_train=True,
warmup_steps=warmup_steps,
save_steps=save_steps,
logging_steps=logging_steps,
)
trainer=Trainer(
model=model,
args=training_args,
train_dataset=data_train,
eval_dataset=data_test,
tokenizer=tokenizer,
)
可以看到这些参数的命名也还是比较一目了然的。最后使用如下代码就可以使用LoRA进行训练了。
trainer.train()
6. 模型合并
在使用 LoRa 方法进行微调后,我们通常需要将微调后的增量模型参数合并到原始的预训练模型中。这样,我们可以得到一个完整的、微调后的模型,才可以用于推理或其他任务。整个完整的训练流程我们总结如下:
加载预训练模型和分词器:首先,我们需要加载原始的预训练模型和分词器。
应用 LoRa 配置:将 LoRa 配置应用到预训练模型中,进行微调。
训练模型:使用训练数据对模型进行微调。
合并模型:将微调后的 LoRa 增量参数合并到原始模型中,得到一个完整的微调模型。
示意图如下:
合并模型的代码如下:
model = get_peft_model(model, lora_config)
model.save_pretrained('path_to_save_lora_model')
model = PeftModel.from_pretrained(model, 'path_to_save_lora_model')
model = model.merge_and_unload()
model.save_pretrained('path_to_save_merged_model')
tokenizer.save_pretrained('path_to_save_merged_model')
merged_model = AutoModelForCausalLM.from_pretrained('path_to_save_merged_model', trust_remote_code=True)
merged_tokenizer = AutoTokenizer.from_pretrained('path_to_save_merged_model', trust_remote_code=True)
通过上述步骤,我们成功完成了基于预训练模型和LoRa方法的大模型训练尝试。
7. 全书阅读总结
本次《大语言模型开发》的试读活动已接近尾声。本书不仅提供了详尽的理论阐述,还辅以实用的代码示例,极大地助力了我们的学习与实践,我愿意给予高度评价。然而,正如古语所言,“纸上得来终觉浅,绝知此事要躬行”,在实际操作过程中,我们深知还会遭遇诸多挑战与问题。我们也会再去积极获得Llama2的调用权限,或获取更多开源项目资源,以便进一步深化我们的研究与实践。活动结束后,我们仍将不懈努力,持续探索与学习,并及时向各位老师汇报我们的最新进展。
- 2024-12-23
-
发表了主题帖:
《大语言模型开发:用开源模型开发本地系统》阅读报告(2)
按照我们的阅读计划,本次报告向各位老师汇报我们在Transformer架构领域的实践成果。在上一次的分享中我们解读了书中的代码,因为时间关系没有具体尝试。今天,我们将填补这一空白,具体展示Transformer在实际项目中的应用。
回顾过往,我们在《人工智能实践教程》中利用较为简单的多层卷积神经网络(CNN),完成了一个基于电池电压和电流特征预测电池当前周期容量的小型演示项目。在此基础上,我们想进一步探索了Transformer模型,利用其在时序信号上下文推理和特征提取方面的优势,尝试预测电池未来容量的变化趋势。
我们的核心原理是利用Transformer对上下文关联性的推理能力,将电池容量变化趋势视为具有前后关联的序列。通过输入一小段历史数据,模型能够编码这些信息,并预测下一周期的容量。随后,这些预测结果将作为新的输入,推动模型进行更进一步的预测。通过这种方式,我们可以不断扩展输出序列,生成具有预见性的输出结果。
在这一领域,已有众多研究工作为我们提供了宝贵的参考。本次分享,我们将以一篇经典的论文为蓝本,尝试复现这一过程。论文的链接和相关代码已附在报告末尾可以参考:【GitHub - XiuzeZhou/RUL: Transformer Network for Remaining Useful Life Prediction of Lithium-Ion Batteries】
1. 数据来源
我们选取了马里兰大学CALCE(Center for Advanced Life Cycle Engineering)提供的在线数据集中的CS2系列电池周期充放电数据作为我们的研究对象。这些数据可以通过访问CALCE的官方网站【Battery Research Data | Center for Advanced Life Cycle Engineering (umd.edu)】进行下载。
最终可以得到锂电池的容量与周期数的关系。
2. 网络架构
接下来,我们将详细介绍文章中实现电池容量预测功能所采用的具体网络架构。我们的模型由两部分组成:前级的去噪编码器和后级的Transformer解码器。幸运的是,论文作者已经将代码开源,我们在此基础上进行了一些修改,以测试不同参数组合的性能。因此,我们不会在文章中粘贴代码,而是提供一些关键的截图。感兴趣的读者可以访问作者的GitHub页面自行下载代码。
首先,我们来看编码器部分。因为电池容量时时间序列,并不涉及自然语言处理的词嵌入,所以这里的编码器本质上由两层全连接网络构成。在输入第一层网络之前,我们对输入信号叠加噪声信号,并进行线性变换和非线性激活,得到一个中间变量,该变量作为去噪编码的信号。然后,我们将这个中间变量再次进行线性变换,以得到输出。
因此,我们的前向传播计算流程如下:input + noise -> FC1 + ReLU -> FC2 -> output。在这里,"mask"指的是叠加噪声的部分,"encode"表示第一个全连接层和非线性激活,得到去噪编码的结果,而"decode"代表第二个全连接层,输出重新变化回来的输入数据。在训练过程中,我们的目标是最小化输入和输出之间的差距,从而实现去噪编码和解码的过程。中间去噪后的向量将作为Transformer解码器的输入。
接下来是位置编码,它用于标记位置信息。具体编码方式可以参考典型的Transformer论文,即通过正弦和余弦函数的编码过程,然后将计算得到的位置信息叠加在输入上。
最后,文章设计了Transformer解码器。根据经典的Transformer架构,解码器本质上是由多个注意力机制和残差网络组成的解码模块的串联(在我们的教程中,只使用了单个模块)。虽然这里直接调用的是encoder模块,但可以看到,由于Transformer架构中编码器和解码器的模块结构是类似的,我们直接调用了PyTorch库中的TransformerEncoderLayer。
将上述所有过程串联起来,前向计算的过程就变得非常清晰:首先分别计算去噪声的编码和位置编码,然后将它们相加作为Transformer的输入。调用Transformer中的编码器输出结果后,再进行一次全连接操作,以得到最终的预测结果。
这样的设计能够很好地对应文章中给出的网络架构。
3. 训练过程
在定义好网络架构之后,我们利用PyTorch库中提供的自动微分功能来执行反向传播,无需手动编写复杂的梯度计算代码。在我们的实验中,选择了Adam优化器来更新网络权重,这是一种广泛使用的优化算法,因其自适应学习率的特性而受到青睐。至于损失函数,我们选择了均方误差(MSELoss),它衡量预测结果与实际目标之间的差异,是回归问题中常用的损失函数。
训练的具体流程如下:
模型验证:在训练过程中,我们定期使用测试集中预先划分的最初window_size个数据点作为起点,逐个生成后续的容量衰退趋势,并与实际测试集数据进行对比,以此来评估模型的性能。
训练效率:得益于模型参数量适中以及架构的简洁性,训练过程的效率相当令人满意。参数量的控制和合理的架构设计,使得模型训练既高效又稳定。
4. 测试结果
在完成了模型的训练和验证之后,我们得到了一些初步的测试结果。虽然文章中提供的量化指标看似乐观,但实际上我们的预测结果并不如预期理想。
具体来说,我们注意到了几个关键问题:
量化指标的局限性:文章中提到的指标,如通过预测的剩余使用寿命(RUL)与实际RUL相减后除以实际RUL得到的比值,虽然数值上看似很小,但实际上误差可能高达40-50个周期。这表明我们的模型在实际应用中可能存在较大的偏差。
预测曲线与实际曲线的对比:通过对比预测的容量变化曲线和实际曲线,我们发现两者之间的误差相当明显。尤其是在选择70%的标称容量作为截止点时,虽然两者看似重合,但如果选择80%的标称容量作为截止点,误差则显著增加。
窗口长度的影响:我们初步考虑窗口长度可能过短,因为我们仅使用了64个数据点进行预测。增加窗口长度后,虽然模型性能有所改善,但普适性较差,部分曲线的预测效果有所提升,而其他曲线的预测效果仍然不尽人意。此外,位置编码的运算规则限制了输入维度,只能调整为2的次方。
注意力头和隐层规模的影响:增加注意力头对模型性能的提升并不明显。
增加隐层的规模以后,效果有了显著改善。然而,我们也意识到单纯增加隐层规模可能会导致过拟合。
后续改进方向:我们认为后续的提升空间主要在于调整网络架构和改进训练算法。教程中还涉及了优化超参数的部分,我们计划进一步探索其他参数的优化空间。
总的来说,虽然我们能够复现文章中的内容,但效果并不理想,且文章中采用的衡量指标存在一定的取巧。基于当前的网络结构,我们将继续探索可能的改进方法,以期达到更准确的预测效果。
5. 总结
我们尝试通过构建包含去噪编码器和Transformer解码器的网络架构,旨在捕捉电池容量变化的复杂模式,并预测其未来趋势。
在实践过程中,我们遇到了一些挑战。尽管初步的量化指标显示了乐观的结果,但深入分析后发现,模型的预测精度并不如预期。我们识别出几个关键问题,包括量化指标的局限性、预测曲线与实际曲线之间的显著误差、窗口长度的选择、以及模型结构对预测性能的影响。
针对这些问题,我们进行了一系列的调整和优化。我们尝试增加窗口长度、调整注意力头的数量、以及扩大隐层规模,以期提高模型的预测能力。虽然这些调整在一定程度上改善了模型性能,但我们认识到,为了实现更准确的预测,还需要在网络架构和训练算法上进行更深入的探索和改进。
总结来说,虽然我们能够复现文章中的内容,但实际效果表明,我们还有很长的路要走。我们将继续学习,探索优化超参数的方法,并尝试不同的网络结构,以期在未来的研究中取得更好的成果。我们相信,通过不断的努力和创新,我们能够克服当前的挑战,实现更精准的电池容量预测。
附上文章的引用信息:
@article{chen2022transformer,
title={Transformer network for remaining useful life prediction of lithium-ion batteries},
author={Chen, Daoquan and Hong, Weicong and Zhou, Xiuze},
journal={Ieee Access},
volume={10},
pages={19621--19628},
year={2022},
publisher={IEEE}
}
- 2024-12-07
-
发表了主题帖:
《大语言模型开发:用开源模型开发本地系统》阅读报告(1)
本帖最后由 Aclicee 于 2024-12-7 12:28 编辑
我们于在11月24日收到的《大语言模型开发:用开源模型开发本地系统》一书,书籍包装完整,印刷清晰,装帧精美,给人以良好的第一印象。
按照既定计划,我们今天向各位老师汇报对书本第一部分的阅读报告,这部分内容聚焦于Transformer模型的详细构成。由于时间限制,我们尚未将Transformer模型应用于具体任务的操作实践,敬请期待我们在后续报告中对实际操作的深入探讨。
在深入剖析Transformer模型之前,本书对Pytorch和深度学习的基础方法进行了全面的介绍。鉴于我们在之前的《人工智能实践教程》测评中已经详细汇报过这部分内容,并且论坛中已有众多专家老师的宝贵分享,我们将不再对此进行重复讨论。
Transformer架构以其独特的设计在自然语言处理领域占据着举足轻重的地位。一个典型的Transformer模型由以下几个核心部分组成:自注意力机制、多头自注意力、位置编码、前馈神经网络、归一化层以及残差连接。接下来的报告中,我们将按照Transformer的工作流程,逐一深入学习这些组成部分。
在这些组件中,位置编码和注意力机制尤为关键。位置编码的引入是为了使Transformer能够理解输入序列中各元素之间的前后关系,而注意力机制则赋予模型聚焦于更为重要的信息维度的能力,从而深刻理解输入数据的相关性和影响。这两个模块是Transformer模型能够准确捕捉序列特征并进行有效预测的关键。
1. 词嵌入
词嵌入是自然语言处理领域中的一项关键技术,它通过无监督学习算法从大规模文本数据中提取词汇的向量表示。这种表示能够捕捉词汇之间的语义和句法关系,为后续的模型训练和语言理解任务提供基础。
在Transformer模型中,词嵌入是与模型参数一同训练得到的,这样可以在特定任务的上下文中优化词嵌入,以更好地适应任务需求。
Transformer模型的词嵌入由两部分组成:标记嵌入(Token Embeddings)和位置编码(Positional Encoding)。
标记嵌入:这部分将输入序列中的每个标记(token)映射到一个高维空间中的向量。通过这种方式,语义上相似的标记在向量空间中的距离更近,从而反映出它们之间的相似性。
位置编码:由于Transformer模型本身不具备处理序列顺序的能力,位置编码被引入以赋予模型对输入序列中标记位置的感知能力。这使得模型能够区分相同标记在不同位置时的上下文含义。
Transformer模型中的位置编码通常采用正弦和余弦函数的组合来实现。这种方法利用了三角函数的周期性特性,为每个位置生成唯一的编码,从而蕴含位置信息。具体公式如下:
其中,pos表示位置, i表示维度,d_model表示模型的维度。这种编码方式确保了不同位置的编码向量具有不同的值,且随着位置的变化,编码向量呈现出周期性变化。
以下是实现正弦位置编码的参考代码:
import torch
import torch.nn as nn
def precompute_freq_cis(dim, seqlen, theta=10000.0):
freqs = 1.0 / (theta**(torch.arange(0, dim, 2)[:(dim//2)].float() / dim))
t = torch.arange(seqlen)
freqs = torch.outer(t, freqs).float()
return freqs
embedding_dim = 8
sequence_length = 5
embeddings = torch.randn(sequence_length * embedding_dim).view(sequence_length, embedding_dim)
freqs = precompute_freq_cis(embedding_dim, sequence_length)
pe = torch.zeros((sequence_length, embedding_dim))
pe[:,0::2] = torch.sin(freqs)
pe[:,1::2] = torch.cos(freqs)
pe_out = token_embedding + pe
这段代码首先创建了一个位置编码矩阵,然后通过正弦和余弦函数为每个位置生成了一个唯一的编码。这样,即使是相同的词,在句子中不同位置的编码也会有所不同,从而帮助模型理解词序的重要性。
2. 自注意力机制
自注意力机制(Self-Attention Mechanism),也称为内部注意力机制,是一种允许输入序列中的每个元素根据其与其他元素的相似度动态分配权重的技术。这种机制对于处理长序列数据尤为重要,因为它能够捕捉序列内部的长距离依赖关系,同时提高了模型的计算效率和可解释性。
自注意力机制的计算过程涉及以下步骤:
映射到查询(Q)、键(K)和值(V)向量:输入序列首先被映射到查询向量(Q)、键向量(K)和值向量(V)。这一步骤通常通过线性变换实现,其中权重是可学习的。
计算注意力分数:通过计算Q和K的点积来衡量序列中各元素之间的相似度,得到一个原始的注意力分数矩阵。
Softmax归一化:对原始注意力分数应用Softmax函数进行归一化,使得每一行的和为1。这一步骤确保了模型在计算加权平均时,能够根据元素间的相似度合理分配权重。
加权平均:将归一化后的注意力分数与值向量V相乘,得到加权平均的结果。这一步骤反映了每个输入元素根据其与其他元素的相似度关系,调整其嵌入表示。
公式可以表示为:
其中d_k表示值向量的维度。以下是自注意力机制的初步实现代码:
query_matrix = nn.Linear(embedding_dim, embedding_dim)
key_matrix = nn.Linear(embedding_dim, embedding_dim)
value_matrix = nn.Linear(embedding_dim, embedding_dim)
query_vectors = query_matrix(embeddings)
key_vectors = key_matrix(embeddings)
value_vectors = value_matrix(embeddings)
scores = torch.matmul(query_vectors, key_vectors.transpose(-2,-1)) / torch.sqrt(torch.tensor(embedding_dim, dtype=torch.float32))
softmax = nn.Softmax(dim=-1)
attention_weights = softmax(scores)
output = torch.matmul(attention_weights, value_vectors)
在实际应用中,Transformer模型通常采用多头自注意力机制,即同一输入序列被分割成多个Q、K、V的组合,每个“头”独立计算注意力,最后将所有头的输出拼接起来。这种设计增强了模型对不同子空间信息的捕捉能力。
num_attention_heads = 2
output_copy = output.clone()
m_output = torch.concat((output, output_copy), dim=1)
output_matrix = nn.Linear(num_attention_heads*embedding_dim, num_attention_heads*embedding_dim)
out_vectors = output_matrix(m_output)
print(embeddings)
print(out_vectors)
为了加速自注意力机制的计算过程,可以采用ColumnParallelLinear和RowParallelLinear等方法进行并行运算。这些技术通过将矩阵分解为多个较小的子矩阵,利用现代硬件的并行处理能力,从而提高计算效率。
3. 残差连接和归一化
在深度学习中,随着网络层数的增加,梯度消失或梯度爆炸的问题会导致训练深层网络变得困难。残差连接(Residual Connection)和层归一化(Layer Normalization)是解决这些问题的两种有效机制,它们在Transformer模型中发挥着重要作用。
残差连接最初在ResNet中提出,用于缓解深层网络训练中的梯度消失问题。其核心思想是将输入直接添加到网络中某一层的输出上,这样网络就可以学习到恒等映射(Identity Mapping),从而使得深层网络的训练变得更加容易。在Transformer中,残差连接用于将多头自注意力层或前馈网络层的输入与输出相加,然后再进行后续的层归一化和下一层的计算。
公式表示为:
其中,FeedForward(X)表示经过一层或多层网络的变换。
层归一化(Layer Normalization)是一种归一化技术,它对每个样本的每个层的输出进行归一化处理,使得输出的分布具有稳定的均值和方差。与批量归一化(Batch Normalization)不同,层归一化是在单个样本的层级上进行归一化,而不是在整个批次上。这使得层归一化对于小批量大小或非独立同分布的数据更加鲁棒。
层归一化的计算公式为:
其中,x_i是第个i样本的输出,μ_i和σ_i分别是该样本输出的均值和方差,ε是一个很小的常数,防止分母为零,γ和β是可学习的参数,用于对归一化后的输出进行缩放和平移。
在Transformer中,层归一化通常在残差连接之后进行,用于减少层与层之间数据分布的差异,加速训练过程,并保持数据的分布稳定性。
4. 前馈网络
前馈网络(Feed-Forward Network,FFN)是Transformer模型中的关键组件之一,主要用于对自注意力模块的输出进行深度特征提取和非线性变换。这一步骤对于模型学习和表示复杂的语义信息至关重要,能够有效提升模型的性能。
在Transformer中,每个前馈网络由两个线性变换组成,中间夹着一个ReLU激活函数。具体来说,前馈网络的结构可以描述为:
第一个线性变换:输入序列首先通过一个线性层进行变换,这个线性层通常具有较多的神经元,允许模型捕捉更丰富的特征表示。
非线性激活:第一个线性变换的输出通过ReLU激活函数进行非线性映射,这有助于引入非线性因素,使模型能够学习和表示更复杂的函数。
第二个线性变换:经过非线性激活的输出再通过另一个线性层进行变换,最终产生前馈网络的输出。
数学上,前馈网络的计算可以表示为:
其中,W_1和W_2是可学习的权重矩阵,b_1和b_2是可学习的偏置项,激活函数为ReLU。
前馈网络在Transformer中的作用主要体现在以下几个方面:
特征提取:通过线性变换,前馈网络能够从自注意力模块的输出中提取更深层次的特征表示。
非线性建模:ReLU激活函数的引入使得模型能够捕捉输入数据中的非线性关系,增强模型的表达能力。
性能提升:前馈网络通过增加模型的深度和复杂度,有助于提升模型在各种自然语言处理任务上的性能。
适应性:前馈网络的两个可学习的线性变换使得模型能够根据不同的任务和数据特性进行适应性调整。
5. 损失函数
在Transformer模型中,损失函数的设计对于衡量模型预测的准确性和优化模型参数至关重要。交叉熵损失函数因其在处理分类问题中的有效性而被广泛采用。
交叉熵损失函数衡量的是模型预测的概率分布与真实标签分布之间的差异。在自然语言处理任务中,如语言模型或机器翻译,交叉熵损失函数用于计算模型输出的 logits 与真实标签之间的差异。损失函数定义如下:
其中,p(x)是真实标签的分布(通常是 one-hot 编码),q(x)是模型预测的概率分布。
为了评估模型对未来信息的预测能力,特别是在自回归任务中,我们使用上三角掩码(Mask)来屏蔽未来位置的信息。这种掩码确保模型在预测当前位置的输出时,只能依赖于当前位置和之前位置的信息,而不能利用未来位置的信息。
在实际的前向计算中,上三角掩码应用于注意力分数矩阵,将未来位置的分数设置为一个非常大的负数(例如,通过添加一个负无穷大的值),这样在应用 Softmax 函数时,这些位置的权重将接近于零,从而不会对模型的预测产生影响。
掩码的应用不仅局限于自回归任务,它还可以用于处理填充(Padding)问题,确保模型在处理不同长度的序列时不会将填充位置的无意义信息纳入考虑。此外,掩码还可以用于防止在序列生成任务中的信息泄露,例如在机器翻译中,掩码可以确保模型在生成当前词时不会看到未来的词。
6. Llama2对原始Transformer架构的改造
Llama2模型是由Meta AI开发的一系列大型语言模型,旨在通过大规模数据训练和复杂的模型结构提升自然语言处理任务的性能。
Llama2模型对原始Transformer架构进行了一些改造,我们根据书中的介绍总结如下:
旋转位置编码(RoPE): Llama2模型采用了旋转位置编码(RoPE)来替代传统的位置编码方法。RoPE通过将位置信息编码为旋转矩阵,使模型能够更有效地捕捉序列中元素之间的位置关系。这种方法利用了复数的旋转特性,通过预计算频率和复数的函数、重塑函数以及应用旋转嵌入的函数来实现。具体而言,RoPE通过将位置信息转换为旋转向量,并使用可学习的参数来调整旋转角度,提高了模型对位置信息的敏感性。
分组查询机制(Group Query Attention): Llama2模型在注意力机制上采用了分组查询机制。这种机制通过将输入序列分成若干组,并对每组进行独立的自注意力计算,提高了模型对序列中不同部分的关注度。同时,GQA技术还引入了查询(Query)的概念,通过将输入序列中的每个元素与查询进行匹配,使模型能够更好地理解输入序列中的重要信息。这种技术提高了Llama2模型在长序列处理任务中的性能和准确性。
预归一化(Pre-normalization): 与原始Transformer中的后置归一化不同,Llama2模型采用了前置层归一化策略,即在每个子层(自注意力层和前馈网络)的输入之前进行层归一化。这种策略有助于提高训练过程中的稳定性,尤其是在模型参数初始化阶段,可以降低梯度爆炸的风险。
SwiGLU激活函数: Llama2模型在前馈网络的激活函数上进行了创新,将原始的ReLU替换为SwiGLU。SwiGLU是基于Swish激活函数的GLU变体,它提供了更好的梯度流动和可能的性能提升。SwiGLU激活函数的公式为SwiGLU(x, W, V) = Swish_β(xW) ⊗ (xV),其中Swish_β(x) = x * sigmoid(βx),⊗为逐元素乘。SwiGLU的优势在于其处处可微的非线性特性,以及通过门机制控制信息通过的比例,来让模型自适应地选择哪些特征对预测下一个词有帮助。
这些改造使得Llama2模型在处理复杂的自然语言任务时,具有更高的效率和更好的性能。
- 2024-11-21
-
回复了主题帖:
共读入围:《大语言模型开发:用开源模型开发本地系统》
个人信息无误,确认可以完成评测计划
- 2024-10-13
-
发表了主题帖:
【Follow me第二期】任务汇总帖
本帖最后由 Aclicee 于 2024-11-16 14:26 编辑
大家好,我是Aclicee,很高兴能够参加EEworld和DigiKey的Follow me活动。今天向各位老师报告一下本期活动的任务汇总。在本期活动中,我们物料清单是:
Arduino UNO R4 WiFi、LTR329环境光传感器、SHT40温湿度传感器以及一条Qwiic缆线
项目简介:
本次活动主要围绕Arduino UNO R4 WiFi开发板进行学习和实践。项目涉及了开发环境的搭建、基础编程任务(如LED点亮、串口打印、驱动点阵LED、DAC生成正弦波、OPAMP放大DAC信号、ADC采样上传上位机显示),以及进阶任务(如Arduino Wifi连接并通过MQTT协议接入HomeAssistant平台)。此外,还包括了扩展任务,即多模态传感器(LTR-329环境光传感器和SHT40温湿度传感器)的使用并上传数据到HomeAssistant平台。
1. 入门任务:开发环境搭建
【【Follow me第二期】入门任务 - 开发环境配置+LED点亮+串口打印+LED点阵驱动 - DigiKey得捷技术专区 - 电子工程世界-论坛 (eeworld.com.cn)】
我们可以选择使用在线编辑器或者本地的IDE来编译和上传Arduino的代码。打开Arduino的官方网站就可以看到对应的使用方式。这里以使用在线的编辑器Arduino Cloud Editor为例,我们打开编辑器的网站,根据指示安装一个Arduino Cloud Agent插件,即可使用在线编辑器。与我们之前使用的一些单片机开发环境相比,Arduino的配置过程显得尤为简单直观。完成环境配置后,将Arduino开发板通过USB线连接至电脑,即可开始编写代码并进行烧录。
2. 入门任务:LED点亮以及串口打印
【【Follow me第二期】入门任务 - 开发环境配置+LED点亮+串口打印+LED点阵驱动 - DigiKey得捷技术专区 - 电子工程世界-论坛 (eeworld.com.cn)】
我们点击Create Sketch来编写第一个Arduino程序,可以看到我们的开发板可以被编辑器自动识别。我们打开官方提供的例程库,可以看到点亮LED的示例代码。不难发现,Arduino程序主要由两个部分组成:setup()和loop()。setup()函数负责初始化设置,如配置管脚模式和初始化外设,它仅在程序开始时执行一次。随后,程序将进入loop()函数,这是程序的主循环,所有持续执行的任务都在这里进行。
设计思路:在“Blink”例程中,我们首先将LED对应的管脚设置为输出模式。这样,我们就可以通过改变该管脚的电平状态(高电平或低电平),来控制LED的点亮与熄灭。在loop()函数中,我们通过编写代码,使LED以一定的频率闪烁。
将上述代码复制到我们的工程中,并烧录到Arduino开发板上。烧录过程中,开发板上的两盏LED指示灯会短暂点亮,以指示烧录过程正在进行。烧录完成后,只有我们指定的LED会按照预设的频率反复点亮和熄灭,实现闪烁效果。
串口打印功能的实现与LED点亮类似。在setup()函数中,我们需要初始化串口通信,并设置适当的波特率。在loop()函数中,我们编写代码,使开发板能够周期性地通过串口发送字符串信息。
3. 基础任务:驱动点阵LED
【【Follow me第二期】入门任务 - 开发环境配置+LED点亮+串口打印+LED点阵驱动 - DigiKey得捷技术专区 - 电子工程世界-论坛 (eeworld.com.cn)】
设计思路: 与单个LED的控制相似,点阵LED的控制原理也是通过设置对应位置的管脚电平来控制LED的点亮或熄灭。然而,由于涉及的LED数量较多,我们无法对每个LED进行单独配置,因此需要采用一种编码规则来实现有效的控制。我们可以参考官方提供的使用教程,有matrix.renderBitmap()和matrix.loadFrame()两种编码方式,分别使用单独每一个LED位置赋二进制数和将连续四个LED位置转换成16进制两种方式。
在浏览例程时,我们注意到“GameOfLife”例程提供了动态效果的实现。受此启发,我们制作了一个动态点亮笑脸图案的小动画。该动画的原理是随机选择矩阵中的行和列进行点亮或熄灭,然后在主体循环中不断刷新matrix.renderBitmap()以展示动态效果。
除了动态图案,我们还尝试了使用LED矩阵显示字符。这主要借助于ArduinoGraphics.h库。在“TextWithArduinoGraphics”例程中,我们看到可以使用matrix.beginDraw()和matrix.endDraw()构建显示框架,并设置了字体大小和内容。通过matrix.textScrollSpeed()函数,我们可以控制字符的滚动速度和方向。
综上所述,我们实现了LED矩阵的多种驱动方式和功能。我们将随机生成笑脸图案的代码放在setup()函数中,作为一次性的开屏动画。动画结束后,开发板将循环播放字符串滚动效果。
4. 基础任务:DAC生成正弦波
【【Follow me第二期】基础任务 - DAC+OPAMP+ADC以及上位机波形显示 - DigiKey得捷技术专区 - 电子工程世界-论坛 (eeworld.com.cn)】
设计思路:由于需要直观地展示波形,而我们目前缺乏便携式示波器,因此我们将采用本地Arduino IDE内置的串口绘图工具作为替代方案。我们的思路是使用analogWave库,并通过配置使其通过DAC输出波形。然后再调用ADC模块,将DAC输出的模拟信号再转回数字信号,并通过串口发送。通过配置串口的传输波特率并开启串口,发送的数据将基于ADC采集得到。
5. 基础任务:OPAMP放大DAC信号
【【Follow me第二期】基础任务 - DAC+OPAMP+ADC以及上位机波形显示 - DigiKey得捷技术专区 - 电子工程世界-论坛 (eeworld.com.cn)】
设计思路:我们参考官方文档中的Arduino内置运算放大器的管脚配置,输入信号连接至A1(运放的正输入端),A2作为负输入端,A3则作为输出端。我们的思路是设计如图的电压放大器,根据其增益计算公式,选择R1和R2两个均为10kΩ的电阻。电路连接方式如下:A0作为DAC的输出,与A1相连,作为运放的同相输入端;A2作为运放的反相输入端,通过R1和R2电阻分别连接至GND和A3。
在代码中,我们需要配置好OPAMP模块,调用OPAMP.h头文件,并设置运放的工作模式为高速模式。由于ADC读取的数值来源于运放的输出端口,我们将analogRead端口绑定为A3。我们设计的是一个二倍放大器,从ADC采样到的电压从原来的半量程(8位,约128)增加到了满量程(255左右),成功实现了输出电压的翻倍。
6. 基础任务:ADC采样上传上位机显示
【【Follow me第二期】基础任务 - DAC+OPAMP+ADC以及上位机波形显示 - DigiKey得捷技术专区 - 电子工程世界-论坛 (eeworld.com.cn)】
这一过程在前两个任务中已有展示,在此将对之前的DAC输出信号和经OPAMP放大后的信号进行汇总显示,从而直观地展示放大器的效果。在图中,蓝线代表DAC的输出信号,而红线代表经OPAMP放大后的信号,直观地展示了二倍电压放大的效果。
7. 进阶任务:ArduinoWifi连接并通过MQTT协议接入到HomeAssistant平台
【【Follow me第二期】进阶任务 - WiFi+MQTT协议连接智能家居HA平台 - DigiKey得捷技术专区 - 电子工程世界-论坛 (eeworld.com.cn)】
我们首先需要安装并配置Home Assistant(HA),考虑到便利性和易用性,我们选择使用Docker拉取HA容器。拉取完成后,我们需要配置Docker容器在本地的存储位置,并运行容器。运行上述命令后,我们可以在Docker Engine中看到名为homeassistant的容器。通过访问http://localhost:8123,可以进入Home Assistant的Web界面。
然后我们需要配置EMQX平台,EMQX平台是一个高性能、可扩展的MQTT消息服务器。类似的,我们还是采用docker拉取容器的方式。拉取完成后,我们可以使用以下命令来运行EMQX容器。此时可以看到docker的容器列表中新增了emqx容器。使用https://localhost:18083端口可以打开EMQX平台,输入默认的账号密码即可登录EMQX Dashboard。
接下来,我们需要在EMQX Dashboard中创建客户端认证,并添加用户作为HA平台的接入。我们需要在HA平台中配置MQTT服务,填写相关的配置信息,包括EMQX平台的IP、用户名和密码等。这样,HA平台就可以通过MQTT协议与EMQX平台进行通信了。EMQX平台的在线连接数来从0变为1,说明HA平台已经成功接入。
设计思路:在此基础上,我们使用Arduino的Wifi模块,使其接入HA平台。我们的思路是使用WiFiS3库来配置WiFi模块,使用WiFi.begin启用WiFi并连接手机热点。然后,我们需要安装MQTT和HomeAssistant相关的库文件,我们引入ArduinoHA.h头文件,以便使用HomeAssistant平台。我们使用HADevice和HAMqtt来创建HA设备启用MQTT协议。我们实例化了几个HA订阅的传感器,比如按钮、模拟传感器和更新时间传感器等,这些可以根据后面任务的需求进行调整。
完成后需要在程序的主循环中通过mqtt.loop()使其使用MQTT协议进行信息的发送。后面我们就仿照例程中发送传感器数据的方式,添加了两个每间隔1000ms进行一次更新采集数据和时间的代码。全部代码完成以后,将其烧录,同时启动docker中的HA容器和EMQX容器,我们可以看到Arduino首先成功连接了wifi,并输出wifi的相关信息,然后尝试建立MQTT的连接并且成功连接,Arduino通过串口发送了采集到的传感器电压,以及更新时间。
8. 扩展任务:多模态传感器的使用并上传数据到HA平台
【【Follow me第二期】扩展任务 - LTR329+SHT40传感器的使用并上传数据到HA平台 - DigiKey得捷技术专区 - 电子工程世界-论坛 (eeworld.com.cn)】
任务使用的外部传感器为LTR-329环境光传感器和SHT40温湿度传感器,关键在于掌握这两种传感器的配置和数据传输的方法。为了简化连接过程,我们采用了Qwiic缆线连接方式,将传感器与Arduino开发板相连。
有关两个传感器的配置方法已经被分别封装在“Adafruit LTR329 and LTR303”和“Adafruit SHT4X”库中,我们都暂时保持默认即可。由于我们只采购了一条Qwiic缆线,这里以温湿度传感器为例,介绍如何实现数据采集和上传到HA平台。
设计思路:我们的思路是在此前实现的Arduino连接HA平台的基础上,将新添加的传感器实例化为模拟信号传感器,然后初始化SHT4X模块,我们把全部配置参数都设成默认即可。最后在主循环中,我们直接使用例程中的温湿度数据获取的函数,然后通过Sensor.setValue()函数显示到HA界面上。
完成代码编写后,我们启动Docker Engine,打开HA和EMQX平台的容器,并将代码烧录到Arduino。测试结果显示,我们可以看到网络连接和MQTT连接的信息,并确认了SHT40传感器的连接。当我们打开HA平台时,可以看到设备增加了新的传感器,并且更新了数据单位信息,数据也会随时间不断更新。
以上就是本次Follow me活动的所有任务实现过程的分享,感谢大家。
项目心得:
Arduino的开发环境配置过程简单直观,无论是使用在线编辑器还是本地IDE,都能快速上手。整个项目过程中,通过实际操作和问题解决,提升了编程能力、硬件操作技能以及对物联网平台的熟悉度。通过不断学习和实践,对Arduino平台和物联网技术有了更深入的理解,为未来在相关领域的进一步探索打下了坚实的基础。
演示视频:
【【Follow me第二季第二期】任务汇总(by Aclicee)-【Follow me第二季第二期】任务汇总(by Aclicee)-EEWORLD大学堂】
程序源码:
代码链接【Follow me第二季第二期Code by Aclicee-嵌入式开发相关资料下载-EEWORLD下载中心】,也随附在本帖下:
-
上传了资料:
Follow me第二季第二期Code by Aclicee
-
加入了学习《【Follow me第二季第二期】任务汇总(by Aclicee)》,观看 【Follow me第二季第二期】任务汇总(by Aclicee)
- 2024-10-08
-
发表了主题帖:
【Follow me第二期】扩展任务 - LTR329+SHT40传感器的使用并上传数据到HA平台
扩展任务为连接外部的环境光或者温湿度传感器,采集环境数据,然后通过Arduino上传到智能家居HA平台。在完成先前任务的基础上,本任务的难度相对较低。关键在于掌握如何配置环境光传感器LTR-329和温湿度传感器SHT40,并访问它们采集的数据。一旦这些传感器被正确配置,我们便可以利用在进阶任务中学到的代码,将模拟传感器的数据采集和上传流程应用于实际的传感器,从而实现数据的实时上传。
1. 环境光传感器LTR329的使用
为了实现环境光数据的采集,我们选用了LTR-329环境光传感器。关于该传感器的具体参数和管脚配置,可以参考官方提供的规格书【5591.pdf (digikey.com)】。为了简化连接过程,我们采用了Qwiic缆线连接方式,将传感器与Arduino开发板相连。
Arduino平台已经提供了LTR-329传感器的库支持。通过Arduino IDE左侧的Library manager搜索“Adafruit LTR329 and LTR303”,即可找到并安装对应的库。
安装库之后,我们可以在例程中查看库函数的具体使用方法。首先,我们需要在代码中包含Adafruit_LTR329_LTR303.h头文件,并实例化传感器对象ltr。后面是一些配置的代码,比如确认连接、设置传感器增益、采样时间间隔等等,我们都暂时保持默认即可。
在主循环中,我们通过调用ltr.readBothChannels(visible_plus_ir, infrared)来获取传感器采集的参数。LTR-329传感器提供两个通道的数据:可见光与红外光的叠加值,以及单独的红外光值。
由于我们使用了Qwiic缆线连接,因此在代码开头需要包含Arduino Wire.h头文件,并在初始化时确认缆线的连接可用。完整的代码如下:
#include "Adafruit_LTR329_LTR303.h"
#include <Wire.h>
Adafruit_LTR329 ltr = Adafruit_LTR329();
void setup() {
Serial.begin(9600);
Serial.println("Adafruit LTR-329 advanced test");
if ( ! ltr.begin(&Wire1) ) {
Serial.println("Couldn't find LTR sensor!");
while (1) delay(10);
}
Serial.println("Found LTR sensor!");
ltr.setGain(LTR3XX_GAIN_2);
Serial.print("Gain : ");
switch (ltr.getGain()) {
case LTR3XX_GAIN_1: Serial.println(1); break;
case LTR3XX_GAIN_2: Serial.println(2); break;
case LTR3XX_GAIN_4: Serial.println(4); break;
case LTR3XX_GAIN_8: Serial.println(8); break;
case LTR3XX_GAIN_48: Serial.println(48); break;
case LTR3XX_GAIN_96: Serial.println(96); break;
}
ltr.setIntegrationTime(LTR3XX_INTEGTIME_100);
Serial.print("Integration Time (ms): ");
switch (ltr.getIntegrationTime()) {
case LTR3XX_INTEGTIME_50: Serial.println(50); break;
case LTR3XX_INTEGTIME_100: Serial.println(100); break;
case LTR3XX_INTEGTIME_150: Serial.println(150); break;
case LTR3XX_INTEGTIME_200: Serial.println(200); break;
case LTR3XX_INTEGTIME_250: Serial.println(250); break;
case LTR3XX_INTEGTIME_300: Serial.println(300); break;
case LTR3XX_INTEGTIME_350: Serial.println(350); break;
case LTR3XX_INTEGTIME_400: Serial.println(400); break;
}
ltr.setMeasurementRate(LTR3XX_MEASRATE_200);
Serial.print("Measurement Rate (ms): ");
switch (ltr.getMeasurementRate()) {
case LTR3XX_MEASRATE_50: Serial.println(50); break;
case LTR3XX_MEASRATE_100: Serial.println(100); break;
case LTR3XX_MEASRATE_200: Serial.println(200); break;
case LTR3XX_MEASRATE_500: Serial.println(500); break;
case LTR3XX_MEASRATE_1000: Serial.println(1000); break;
case LTR3XX_MEASRATE_2000: Serial.println(2000); break;
}
}
void loop() {
bool valid;
uint16_t visible_plus_ir, infrared;
if (ltr.newDataAvailable()) {
valid = ltr.readBothChannels(visible_plus_ir, infrared);
if (valid) {
Serial.print("CH0 Visible + IR: ");
Serial.print(visible_plus_ir);
Serial.print("\t\tCH1 Infrared: ");
Serial.println(infrared);
}
}
delay(100);
}
运行上述代码后,我们可以通过串口监视器观察到光照强度信息的输出。随着环境光线的变化,例如手动遮挡光源,示数也会相应变化。
动态的数据接收过程请参考随附的视频。
2. 温湿度传感器SHT40的使用
有了环境光传感器的经验,温湿度传感器只需要如法炮制即可。与环境光传感器类似,我们可以通过Arduino的库来简化集成过程。首先,通过Arduino IDE的Library manager搜索并安装“Adafruit SHT4X”库。这个库为SHT40传感器提供了封装好的接口。
打开其中的例程,其实现的过程和环境光传感器是一样的。我们可以在代码中包含Adafruit_SHT4X.h头文件,并实例化一个SHT40传感器对象sht4。
主循环部分,主要是通过sht4.getEvent(&humidity, &temp)来获取温湿度的读数,然后通过串口分别发送。同样,我们因为通过Qwiic缆线来连接开发板和传感器,包含对应的头文件并检查确认其连接即可,具体的代码如下:
#include "Adafruit_SHT4x.h"
#include <Wire.h>
Adafruit_SHT4x sht4 = Adafruit_SHT4x();
void setup() {
Serial.begin(9600);
while (!Serial)
delay(10); // will pause Zero, Leonardo, etc until serial console opens
Serial.println("Adafruit SHT4x test");
if (! sht4.begin(&Wire1)) {
Serial.println("Couldn't find SHT4x");
while (1) delay(1);
}
Serial.println("Found SHT4x sensor");
Serial.print("Serial number 0x");
Serial.println(sht4.readSerial(), HEX);
// You can have 3 different precisions, higher precision takes longer
sht4.setPrecision(SHT4X_HIGH_PRECISION);
switch (sht4.getPrecision()) {
case SHT4X_HIGH_PRECISION:
Serial.println("High precision");
break;
case SHT4X_MED_PRECISION:
Serial.println("Med precision");
break;
case SHT4X_LOW_PRECISION:
Serial.println("Low precision");
break;
}
// You can have 6 different heater settings
// higher heat and longer times uses more power
// and reads will take longer too!
sht4.setHeater(SHT4X_NO_HEATER);
switch (sht4.getHeater()) {
case SHT4X_NO_HEATER:
Serial.println("No heater");
break;
case SHT4X_HIGH_HEATER_1S:
Serial.println("High heat for 1 second");
break;
case SHT4X_HIGH_HEATER_100MS:
Serial.println("High heat for 0.1 second");
break;
case SHT4X_MED_HEATER_1S:
Serial.println("Medium heat for 1 second");
break;
case SHT4X_MED_HEATER_100MS:
Serial.println("Medium heat for 0.1 second");
break;
case SHT4X_LOW_HEATER_1S:
Serial.println("Low heat for 1 second");
break;
case SHT4X_LOW_HEATER_100MS:
Serial.println("Low heat for 0.1 second");
break;
}
}
void loop() {
sensors_event_t humidity, temp;
uint32_t timestamp = millis();
sht4.getEvent(&humidity, &temp);// populate temp and humidity objects with fresh data
timestamp = millis() - timestamp;
Serial.print("Temperature: "); Serial.print(temp.temperature); Serial.println(" degrees C");
Serial.print("Humidity: "); Serial.print(humidity.relative_humidity); Serial.println("% rH");
Serial.print("Read duration (ms): ");
Serial.println(timestamp);
delay(1000);
}
运行上述代码后,我们可以通过串口监视器观察到温湿度信息的输出。随着环境温湿度的变化,示数也会相应变化。温度传感器的结果将和扩展任务一同展示。动态的数据接收过程请参考随附的视频。
3. 扩展任务(必做):通过外部传感器上传信息到HA,并通过HA面板显示数据
在成功集成环境光传感器LTR-329和温湿度传感器SHT40之后,我们的目标是将这些传感器的数据上传至Home Assistant (HA) 平台,并在HA界面上显示这些数据。由于我们只有一条Qwiic缆线,我们将以温湿度传感器SHT40为例进行演示。
我们删除了上个任务中打印wifi信息的相关函数,因为我们暂时不需要显示这些,来精简一下代码。我们包含了所有必要的头文件,并开始配置HA平台。我们将新添加的传感器实例化为模拟信号传感器,具体代码如下:
HASensorNumber analogSensor("AnalogInput", HASensorNumber::PrecisionP1);
HASensorNumber uptimeSensor("Uptime");
HASensorNumber TempSensor("Temperature", HASensorNumber::PrecisionP2);
HASensorNumber HumidSensor("Humid", HASensorNumber::PrecisionP3);
//HASensorNumber LightSensor("Light", HASensorNumber::PrecisionP2);
HAButton buttonA("myButtonA");
HAButton buttonB("myButtonB");
然后我们在初始化完wifi模块和MQTT的连接以后,具体方法参考上一则报告【【Follow me第二期】进阶任务 - WiFi+MQTT协议连接智能家居HA平台 - DigiKey得捷技术专区 - 电子工程世界-论坛 (eeworld.com.cn)】,给HA界面上各传感器的显示增加更多细节,包括进行命名的区分以及增加具体的单位,代码如下:
analogSensor.setName("Analog voltage");
analogSensor.setUnitOfMeasurement("V");
uptimeSensor.setName("Update Time");
uptimeSensor.setUnitOfMeasurement("s");
TempSensor.setName("Temperature");
TempSensor.setUnitOfMeasurement("degrees C");
HumidSensor.setName("Humidity");
HumidSensor.setUnitOfMeasurement("% rH");
//LightSensor.setUnitOfMeasurement("lkx");
最后初始化SHT4X模块,我们把全部配置参数都设成默认,然后合并成一个SHT40_init()函数。
在主循环中,我们直接使用例程中的温湿度数据获取的函数,然后通过Sensor.setValue()函数显示到HA界面上。具体代码如下:
sensors_event_t humidity, temp;
sht4.getEvent(&humidity, &temp);
Serial.print("Temperature: "); Serial.print(temp.temperature); Serial.println("degrees C");
Serial.print("Humidity: "); Serial.print(humidity.relative_humidity); Serial.println("% rH");
TempSensor.setValue(temp.temperature);
HumidSensor.setValue(humidity.relative_humidity);
最后完整的代码如下:
#include <WiFiS3.h>
#include <ArduinoHA.h>
#include <Wire.h>
#include "arduino_secrets.h"
#include "analogWave.h"
#include "Adafruit_SHT4x.h"
#include "Adafruit_LTR329_LTR303.h"
///////please enter your sensitive data in the Secret tab/arduino_secrets.h
char ssid[] = SECRET_SSID; // your network SSID (name)
char pass[] = SECRET_PASS; // your network password (use for WPA, or use as key for WEP)
int status = WL_IDLE_STATUS; // the WiFi radio's status
unsigned long lastUpdateAt = 0;
int freq = 1;
WiFiClient client;
HADevice device(MQTT_CLIENT_ID);
HAMqtt mqtt(client, device);
HASensorNumber analogSensor("AnalogInput", HASensorNumber::PrecisionP1);
HASensorNumber uptimeSensor("Uptime");
HASensorNumber TempSensor("Temperature", HASensorNumber::PrecisionP2);
HASensorNumber HumidSensor("Humid", HASensorNumber::PrecisionP3);
//HASensorNumber LightSensor("Light", HASensorNumber::PrecisionP2);
HAButton buttonA("myButtonA");
HAButton buttonB("myButtonB");
analogWave wave(DAC);
Adafruit_SHT4x sht4 = Adafruit_SHT4x();
Adafruit_LTR329 ltr = Adafruit_LTR329();
void setup() {
//Initialize serial and wait for port to open:
Serial.begin(9600);
while (!Serial) {
; // wait for serial port to connect. Needed for native USB port only
}
// check for the WiFi module:
if (WiFi.status() == WL_NO_MODULE) {
Serial.println("Communication with WiFi module failed!");
// don't continue
while (true);
}
String fv = WiFi.firmwareVersion();
if (fv < WIFI_FIRMWARE_LATEST_VERSION) {
Serial.println("Please upgrade the firmware");
}
// attempt to connect to WiFi network:
while (status != WL_CONNECTED) {
Serial.print("Attempting to connect to WPA SSID: ");
Serial.println(ssid);
// Connect to WPA/WPA2 network:
status = WiFi.begin(ssid, pass);
// wait 10 seconds for connection:
delay(10000);
}
// you're connected now, so print out the data:
Serial.print("You're connected to the network");
Serial.println("\nStart connecting to MQTT server");
if (!mqtt.begin(MQTT_SERVER, MQTT_PORT, MQTT_USERNAME, MQTT_PASSWORD)){
Serial.print("Connection falied");
Serial.print(mqtt.getState());
Serial.println("Try again in 5 seconds");
delay(5000);
}
wave.sine(freq);
wave.amplitude(0.5);
analogReadResolution(14);
device.setName("Arduino");
device.setSoftwareVersion("1.0.0");
buttonA.setIcon("mdi:fire");
buttonA.setName("Click me A");
buttonB.setIcon("mdi:home");
buttonB.setName("Click me B");
analogSensor.setName("Analog voltage");
analogSensor.setUnitOfMeasurement("V");
uptimeSensor.setName("Update Time");
uptimeSensor.setUnitOfMeasurement("s");
TempSensor.setName("Temperature");
TempSensor.setUnitOfMeasurement("degrees C");
HumidSensor.setName("Humidity");
HumidSensor.setUnitOfMeasurement("% rH");
//LightSensor.setUnitOfMeasurement("lkx");
SHT40_init();
//LTR329_init();
}
void loop() {
// check the network connection once every 10 seconds:
mqtt.loop();
if ((millis() - lastUpdateAt) > 1000) { // 1000ms debounce time
uint16_t reading = analogRead(A0);
float voltage = reading * 5.f / 16383.f; // 0.0V - 5.0V
Serial.print("Volt:");
Serial.println(voltage);
analogSensor.setValue(voltage);
unsigned long uptimeValue = millis() / 1000;
Serial.print("Uptime:");
Serial.println(uptimeValue);
uptimeSensor.setValue(uptimeValue);
sensors_event_t humidity, temp;
sht4.getEvent(&humidity, &temp);
Serial.print("Temperature: "); Serial.print(temp.temperature); Serial.println("degrees C");
Serial.print("Humidity: "); Serial.print(humidity.relative_humidity); Serial.println("% rH");
TempSensor.setValue(temp.temperature);
HumidSensor.setValue(humidity.relative_humidity);
// bool valid;
// uint16_t visible_plus_ir, infrared;
// if (ltr.newDataAvailable()) {
// valid = ltr.readBothChannels(visible_plus_ir, infrared);
// if (valid) {
// Serial.print("CH0 Visible + IR: ");
// Serial.print(visible_plus_ir);
// Serial.print("\t\tCH1 Infrared: ");
// Serial.println(infrared);
// }
// LightSensor.setValue(visible_plus_ir);
// }
lastUpdateAt = millis();
}
}
void SHT40_init() {
Serial.println("Adafruit SHT4x test");
if (! sht4.begin(&Wire1)) {
Serial.println("Couldn't find SHT4x");
while (1) delay(1);
}
Serial.println("Found SHT4x sensor");
Serial.print("Serial number 0x");
Serial.println(sht4.readSerial(), HEX);
sht4.setPrecision(SHT4X_HIGH_PRECISION);
switch (sht4.getPrecision()) {
case SHT4X_HIGH_PRECISION:
Serial.println("High precision");
break;
case SHT4X_MED_PRECISION:
Serial.println("Med precision");
break;
case SHT4X_LOW_PRECISION:
Serial.println("Low precision");
break;
}
sht4.setHeater(SHT4X_NO_HEATER);
switch (sht4.getHeater()) {
case SHT4X_NO_HEATER:
Serial.println("No heater");
break;
case SHT4X_HIGH_HEATER_1S:
Serial.println("High heat for 1 second");
break;
case SHT4X_HIGH_HEATER_100MS:
Serial.println("High heat for 0.1 second");
break;
case SHT4X_MED_HEATER_1S:
Serial.println("Medium heat for 1 second");
break;
case SHT4X_MED_HEATER_100MS:
Serial.println("Medium heat for 0.1 second");
break;
case SHT4X_LOW_HEATER_1S:
Serial.println("Low heat for 1 second");
break;
case SHT4X_LOW_HEATER_100MS:
Serial.println("Low heat for 0.1 second");
break;
}
}
void LTR329_init() {
Serial.println("Adafruit LTR-329 advanced test");
if ( ! ltr.begin(&Wire1) ) {
Serial.println("Couldn't find LTR sensor!");
while (1) delay(10);
}
Serial.println("Found LTR sensor!");
ltr.setGain(LTR3XX_GAIN_2);
Serial.print("Gain : ");
switch (ltr.getGain()) {
case LTR3XX_GAIN_1: Serial.println(1); break;
case LTR3XX_GAIN_2: Serial.println(2); break;
case LTR3XX_GAIN_4: Serial.println(4); break;
case LTR3XX_GAIN_8: Serial.println(8); break;
case LTR3XX_GAIN_48: Serial.println(48); break;
case LTR3XX_GAIN_96: Serial.println(96); break;
}
ltr.setIntegrationTime(LTR3XX_INTEGTIME_100);
Serial.print("Integration Time (ms): ");
switch (ltr.getIntegrationTime()) {
case LTR3XX_INTEGTIME_50: Serial.println(50); break;
case LTR3XX_INTEGTIME_100: Serial.println(100); break;
case LTR3XX_INTEGTIME_150: Serial.println(150); break;
case LTR3XX_INTEGTIME_200: Serial.println(200); break;
case LTR3XX_INTEGTIME_250: Serial.println(250); break;
case LTR3XX_INTEGTIME_300: Serial.println(300); break;
case LTR3XX_INTEGTIME_350: Serial.println(350); break;
case LTR3XX_INTEGTIME_400: Serial.println(400); break;
}
ltr.setMeasurementRate(LTR3XX_MEASRATE_200);
Serial.print("Measurement Rate (ms): ");
switch (ltr.getMeasurementRate()) {
case LTR3XX_MEASRATE_50: Serial.println(50); break;
case LTR3XX_MEASRATE_100: Serial.println(100); break;
case LTR3XX_MEASRATE_200: Serial.println(200); break;
case LTR3XX_MEASRATE_500: Serial.println(500); break;
case LTR3XX_MEASRATE_1000: Serial.println(1000); break;
case LTR3XX_MEASRATE_2000: Serial.println(2000); break;
}
}
完成代码编写后,我们启动Docker Engine,打开HA和EMQX平台的容器,并将代码烧录到Arduino。测试结果显示,我们可以看到网络连接和MQTT连接的信息,并确认了SHT40传感器的连接。传感器开始通过串口向上位机发送温湿度信息(截图时我已经把连接线断开了,所以显示了未连接)。
当我们打开HA平台时,可以看到设备增加了新的传感器,并且更新了数据单位信息,数据也会随时间不断更新。动态的展示请参考随附的视频。
至此我们完成了本次Follow me活动的全部任务,汇总帖和视频正在全力整合中。
[localvideo]ca5798636437d85f40691a7ef6521192[/localvideo]
- 2024-10-07
-
发表了主题帖:
【Follow me第二期】进阶任务 - WiFi+MQTT协议连接智能家居HA平台
本帖最后由 Aclicee 于 2024-10-7 22:41 编辑
进阶任务主要使用的是Arduino UNO R4 WiFi板子的ESP32S3 WiFi模块。尽管我对这一领域的知识尚浅,但通过与Arduino、上位机软件、HomeAssistant(HA)这一开源智能家居平台,以及MQTT协议和EMQX消息服务器的深入交互,我得以构建了一个多平台联动的智能系统。这一过程充满了挑战,但也充满了成就感。在本文中,我将分享我的探索之旅,包括遇到的困难和解决方案,希望能为同样走在这条道路上的你提供一些启示和帮助。
1. HA平台的安装和配置
为了实现我们智能家居项目的核心功能,我们首先需要安装并配置Home Assistant(HA),这是一个流行的开源智能家居平台。详细的安装和使用教程可以在【Home Assistant (home-assistant.io)】找到。按照“Get Started”的指引,我们可以进入官方教程,了解HA支持的多种安装方式。
考虑到便利性和易用性,我们选择使用Docker进行安装。Docker可以从【Docker: Accelerated Container Application Development】下载。安装并注册完成后,确保Docker Engine在Docker Desktop中启动。如果你需要科学上网来访问Docker服务,请确保相应的配置已经完成。
接下来,我们将在Docker中拉取HA的镜像。根据【Linux - Home Assistant (home-assistant.io)】中的“Installation with Docker”部分,我们首先在命令行界面(CMD)中执行docker search home-assistant命令,以查看可用的容器目录。以下是具体的命令输出:
我们选择列表中的第一个镜像,并通过执行docker pull homeassistant/home-assistant命令来拉取它。请注意,拉取过程可能需要一些时间,并且有时会因为网络问题而中断。如果遇到失败,只需重新尝试即可。
拉取完成后,我们需要配置Docker容器在本地的存储位置,并运行容器。与官方教程稍有不同,由于我们已经提前拉取了容器,因此不需要再次从镜像源下载。我们可以直接使用以下命令来运行容器:
docker run -d --name homeassistant -v /path/to/local/config:/config -p 8123:8123 homeassistant/home-assistant
请将/path/to/local/config替换为你本地存放容器配置文件的实际路径,并将端口号8123替换为你希望使用的端口。
运行上述命令后,你可以在Docker Engine中看到名为homeassistant的容器。通过访问http://localhost:8123,你可以进入Home Assistant的Web界面。
在首次访问时,系统会提示你创建你的智能家居。按照提示完成一系列信息注册后,重新登录Home Assistant,你就可以进入主界面了。
完成后重新登录HomeAssistant,主页显示的相关信息如下:
2. EMQX平台的安装和配置
MQTT协议是一种轻量级的、基于发布/订阅模式的消息传输协议,广泛用于物联网和分布式系统中。它具有简单易实现、支持多种服务质量(QoS)、报文精简、基于TCP/IP等特性,特别适合于带宽有限和网络不稳定的环境。EMQX平台是一个高性能、可扩展的MQTT消息服务器,支持大规模分布式物联网设备的连接,能够实时处理和移动大量消息和事件流数据。
EMQX的具体信息可以在其官方网站【EMQX 文档】查看,里面也提供了使用MQTT协议的相关教程。我们还是采用docker拉取容器的方式,参考【通过 Docker 运行 EMQX | EMQX文档】的文档说明。我们在指令栏中输入docker search emqx来查找可供拉取的容器列表。
没有看到说明文档中提到的带版本号的emqx容器,就直接拉取第一个根目录,使用docker pull emqx来进行拉取。
拉取完成后,我们可以使用以下命令来运行EMQX容器:
docker run -d --name emqx -p 1883:1883 -p 8083:8083 -p 8084:8084 -p 8883:8883 -p 18083:18083 -v /path/to/local/config:/opt/emqx/data emqx/emqx:5.8.0
请将/path/to/local/config替换为您本地存放容器配置文件的实际路径。此时可以看到docker的容器列表中新增了emqx容器。
使用https://localhost:18083端口可以打开EMQX平台,参考以下文档进行初步的测试【快速开始 | EMQX文档】,平台默认的账号是admin,密码是public,即可登录EMQX Dashboard,界面如下:
接下来,我们需要在EMQX Dashboard中创建客户端认证,并添加用户作为HA平台的接入。
完成后,我们需要在HA平台中配置MQTT服务,填写相关的配置信息,包括EMQX平台的IP、用户名和密码等。EMQX节点的名称可以在集群概览中查看,比如这边是emqx@172.17.0.2。
通过HA平台的设置菜单,配置新的设备与服务,选择MQTT。
配置完成后,我们可以通过HA平台的设置菜单,配置新的设备与服务,选择MQTT,并填写相关的配置信息。这样,HA平台就可以通过MQTT协议与EMQX平台进行通信了。
最后,我们可以通过检查EMQX平台的在线连接数来验证HA平台是否已经成功接入EMQX集群。如果连接数从0变为1,说明HA平台已经成功接入。接下来,我们可以通过MQTT协议将Arduino接入EMQX平台,实现Arduino和HA之间的通信和数据传输。
3. 进阶任务(必做):ArduinoWifi连接并通过MQTT协议接入到HomeAssistant平台
我们将探讨如何将Arduino UNO R4 WiFi开发板通过WiFi连接,并利用MQTT协议接入HomeAssistant平台。这一过程不仅涉及到硬件的配置,还包括了软件的集成,是对我们技能的一次全面考验。
首先,我们需要启动Arduino的WiFi模块。可以参考官方文档【docs.arduino.cc/tutorials/uno-r4-wifi/wifi-examples】,里面提供了Arduino连接Wifi的示例。我们创建一个arduino_secret.h的保密文件,用于存储连接WiFi路由器的敏感信息,如网络名称(SSID)和密码等。
由于我在学校连接校园网需要特定的账号和密码,这一部分在Arduino中实现较为复杂,因此我们选择了一个更为简便的方法:使用手机作为热点,让Arduino通过热点进行连接。
我们直接采用官方例程中的代码,配置WiFi模块。主要包括以下几个部分:引入必要的头文件WiFiS3.h;使用WiFi.begin(ssid, pass)启用WiFi并连接;确认WiFi连接状态并打印相关信息,如网络名称、IP地址、MAC地址等。
#include <WiFiS3.h>
#include "arduino_secrets.h"
///////please enter your sensitive data in the Secret tab/arduino_secrets.h
char ssid[] = SECRET_SSID; // your network SSID (name)
char pass[] = SECRET_PASS; // your network password (use for WPA, or use as key for WEP)
int status = WL_IDLE_STATUS; // the WiFi radio's status
void setup() {
//Initialize serial and wait for port to open:
Serial.begin(9600);
while (!Serial) {
; // wait for serial port to connect. Needed for native USB port only
}
// check for the WiFi module:
if (WiFi.status() == WL_NO_MODULE) {
Serial.println("Communication with WiFi module failed!");
// don't continue
while (true);
}
String fv = WiFi.firmwareVersion();
if (fv < WIFI_FIRMWARE_LATEST_VERSION) {
Serial.println("Please upgrade the firmware");
}
// attempt to connect to WiFi network:
while (status != WL_CONNECTED) {
Serial.print("Attempting to connect to WPA SSID: ");
Serial.println(ssid);
// Connect to WPA/WPA2 network:
status = WiFi.begin(ssid, pass);
// wait 10 seconds for connection:
delay(10000);
}
// you're connected now, so print out the data:
Serial.print("You're connected to the network");
printCurrentNet();
printWifiData();
}
void loop() {
// check the network connection once every 10 seconds:
delay(10000);
printCurrentNet();
}
void printWifiData() {
// print your board's IP address:
IPAddress ip = WiFi.localIP();
Serial.print("IP Address: ");
Serial.println(ip);
// print your MAC address:
byte mac[6];
WiFi.macAddress(mac);
Serial.print("MAC address: ");
printMacAddress(mac);
}
void printCurrentNet() {
// print the SSID of the network you're attached to:
Serial.print("SSID: ");
Serial.println(WiFi.SSID());
// print the MAC address of the router you're attached to:
byte bssid[6];
WiFi.BSSID(bssid);
Serial.print("BSSID: ");
printMacAddress(bssid);
// print the received signal strength:
long rssi = WiFi.RSSI();
Serial.print("signal strength (RSSI):");
Serial.println(rssi);
// print the encryption type:
byte encryption = WiFi.encryptionType();
Serial.print("Encryption Type:");
Serial.println(encryption, HEX);
Serial.println();
}
void printMacAddress(byte mac[]) {
for (int i = 0; i < 6; i++) {
if (i > 0) {
Serial.print(":");
}
if (mac[i] < 16) {
Serial.print("0");
}
Serial.print(mac[i], HEX);
}
Serial.println();
}
将代码烧录到Arduino中,我们可以看到WiFi模块成功连接到了手机热点,并且打印出了网络信息。同时手机上也显示ESP32S3模块的连接。至此完成了wifi模块的使用。
接下来,我们需要在Arduino IDE中安装MQTT和HomeAssistant相关的库文件。可以在Library管理器中搜索并安装home-assistant-integration库。
安装好所有所需的库文件和插件,我们可以查看一下其中涉及MQTT相关操作的例程,因为后面可选任务涉及光照和温湿度传感器的使用,所以我们以模拟传感器的例程为参考【arduino-home-assistant/examples/sensor-analog/sensor-analog.ino at main · dawidchyrzynski/arduino-home-assistant · GitHub】,我们使用Arduino内置的DAC来暂时模拟一下传感器的输出。
由于我们已经配置好了EMQX平台,接下来需要在arduino_secret.h文件中增加EMQX平台的用户名、密码等信息,具体如下:
//arduino_secrets.h header file
#define SECRET_SSID "Galaxy S24+ 6927"
#define SECRET_PASS "LYQ1234567890"
#define MQTT_SERVER "192.168.40.250"
#define MQTT_PORT 1883
#define MQTT_CLIENT_ID "arduino"
#define MQTT_USERNAME "admin"
#define MQTT_PASSWORD "admin"
#define TOPIC_SUBSCRIBE "UNO/arduino/sensor"
因为EMQX平台是基于电脑的IP搭建的,其中MQTT_SERVER后的地址就是当前电脑无线网络的IP地址,可以在指令栏中通过ipconfig/all来查询,如下:
将相关信息全部录入arduino_secret.h头文件,然后仿照上面的例程。因为例程中使用的Ethernet以太网,所以我们只需要学习其中涉及MQTT相关的部分,将其合并到上面的Wifi连接的程序中。
现在,我们将WiFi连接代码与MQTT代码整合在一起。首先,我们引入ArduinoHA.h头文件,以便使用HomeAssistant平台。
我们使用HADevice device(MQTT_CLIENT_ID)和HAMqtt mqtt(client, device)来创建HA设备启用MQTT协议。然后我们实例化了几个HA订阅的传感器,比如按钮、模拟传感器和更新时间传感器等,这些可以根据后面任务的需求进行调整。也可以通过device.XX或者某个具体定义的传感器的字段进行修改,来自定义传感器在HA平台的UI上呈现的形式(比如图标、是否附加单位等)。
以上的初始设置完成以后,我们在Wifi代码的基础上增加以下代码:
Serial.println("\nStart connecting to MQTT server");
if (!mqtt.begin(MQTT_SERVER, MQTT_PORT, MQTT_USERNAME, MQTT_PASSWORD)){
Serial.print("Connection falied");
Serial.print(mqtt.getState());
Serial.println("Try again in 5 seconds");
delay(5000);
}
核心为mqtt. begin()用于启动MQTT的连接,需要输入服务器的地址、端口以及我们预设好的用户名和密码。完成后需要在程序的主循环中通过mqtt.loop()使其使用MQTT协议进行信息的发送。后面我们就仿照例程中发送传感器数据的方式,添加了两个每间隔1000ms进行一次更新采集数据和时间的代码,完整代码如下:
#include <WiFiS3.h>
#include <ArduinoHA.h>
#include "arduino_secrets.h"
#include "analogWave.h"
///////please enter your sensitive data in the Secret tab/arduino_secrets.h
char ssid[] = SECRET_SSID; // your network SSID (name)
char pass[] = SECRET_PASS; // your network password (use for WPA, or use as key for WEP)
int status = WL_IDLE_STATUS; // the WiFi radio's status
unsigned long lastUpdateAt = 0;
int freq = 1;
WiFiClient client;
HADevice device(MQTT_CLIENT_ID);
HAMqtt mqtt(client, device);
HASensorNumber analogSensor("myAnalogInput", HASensorNumber::PrecisionP1);
HASensorNumber uptimeSensor("myUptime");
HAButton buttonA("myButtonA");
HAButton buttonB("myButtonB");
analogWave wave(DAC);
void setup() {
//Initialize serial and wait for port to open:
Serial.begin(9600);
while (!Serial) {
; // wait for serial port to connect. Needed for native USB port only
}
// check for the WiFi module:
if (WiFi.status() == WL_NO_MODULE) {
Serial.println("Communication with WiFi module failed!");
// don't continue
while (true);
}
String fv = WiFi.firmwareVersion();
if (fv < WIFI_FIRMWARE_LATEST_VERSION) {
Serial.println("Please upgrade the firmware");
}
// attempt to connect to WiFi network:
while (status != WL_CONNECTED) {
Serial.print("Attempting to connect to WPA SSID: ");
Serial.println(ssid);
// Connect to WPA/WPA2 network:
status = WiFi.begin(ssid, pass);
// wait 10 seconds for connection:
delay(10000);
}
// you're connected now, so print out the data:
Serial.print("You're connected to the network");
printCurrentNet();
printWifiData();
Serial.println("\nStart connecting to MQTT server");
if (!mqtt.begin(MQTT_SERVER, MQTT_PORT, MQTT_USERNAME, MQTT_PASSWORD)){
Serial.print("Connection falied");
Serial.print(mqtt.getState());
Serial.println("Try again in 5 seconds");
delay(5000);
}
wave.sine(freq);
wave.amplitude(0.5);
analogReadResolution(14);
device.setName("Arduino");
device.setSoftwareVersion("1.0.0");
buttonA.setIcon("mdi:fire");
buttonA.setName("Click me A");
buttonB.setIcon("mdi:home");
buttonB.setName("Click me B");
}
void loop() {
// check the network connection once every 10 seconds:
mqtt.loop();
if ((millis() - lastUpdateAt) > 1000) { // 1000ms debounce time
uint16_t reading = analogRead(A0);
float voltage = reading * 5.f / 16383.f; // 0.0V - 5.0V
Serial.print("Volt:");
Serial.println(voltage);
analogSensor.setValue(voltage);
unsigned long uptimeValue = millis() / 1000;
Serial.print("Uptime:");
Serial.println(uptimeValue);
uptimeSensor.setValue(uptimeValue);
lastUpdateAt = millis();
}
}
全部代码完成以后,将其烧录,同时启动docker中的HA容器和EMQX容器,并通过对应的端口号打开其网页。烧录需要花费很多时间,电脑因为开了太多的东西已经卡顿不堪了,最终结果如下:
我们可以看到Arduino首先成功连接了wifi,并输出wifi的相关信息,然后尝试建立MQTT的连接并且成功连接,Arduino通过串口发送了采集到的传感器电压,以及更新时间。
在HA平台的服务器上,我们可以看到Arduino已经成功接入并开始更新采样到的电压和时间信息。
至此,我们成功完成了Arduino通过WiFi连接并通过MQTT协议接入HomeAssistant平台的任务,属实不易。具体信息的动态展示可以查看随附的视频。
[localvideo]4b8611fb8c5a29c98ec2adee00885522[/localvideo]
- 2024-10-06
-
发表了主题帖:
【Follow me第二期】基础任务 - DAC+OPAMP+ADC以及上位机波形显示
前阵子因为事务繁忙,没来得及更新Follow me的相关测评任务。趁着国庆假期有一些闲暇,把相关任务再完成完成。
本次活动的基础任务主要涉及Arduino内置的DAC,ADC还有运算放大器OPAMP的使用。这些组件对于电子项目的开发至关重要,它们能够处理模拟信号,为数字世界提供精确的数据。
1. 本地IDE的安装
由于本次测评需要直观地展示波形,而我们目前缺乏便携式示波器,因此我们将采用Arduino IDE内置的串口绘图工具(Serial Plotter)作为替代方案。这一工具能够将串口传输的数据实时转化为波形图,为缺乏专业设备的用户提供了极大的便利。
为了使用串口绘图工具,我们首先需要安装Arduino IDE。用户可以从Arduino官方网站的【Software | Arduino】页面下载安装包。安装完成后,您可以在IDE的右上角轻松找到串口绘图工具的入口。
在基础任务中,我们将通过串口发送采集到的数据至IDE,并利用串口绘图工具将这些数据绘制成波形图,以此模拟示波器的功能。
2. 基础任务(必做):用DAC生成正弦波
为了更好地理解和运用DAC模块,我们将参考官方文档【Arduino UNO R4 WiFi Digital-to-Analog Converter (DAC) | Arduino Documentation】,了解如何利用Arduino的内部DAC模块生成特定形状的波形。我们将使用analogWave库,并通过配置使其通过DAC输出波形。
我们计划将输出波形的频率设定为100Hz,并将输出幅度设置为量程的0.5倍(即1.65V)。尽管Arduino理论上能够输出这样的正弦波,但受限于没有示波器,我们无法直观地观察输出波形。
#include "analogWave.h"
analogWave wave(DAC);
int freq = 100;
void setup() {
// put your setup code here, to run once:
wave.sine(freq);
wave.amplitude(0.5);
}
为解决这一问题,我们将调用ADC模块,将DAC输出的模拟信号再转回数字信号,并通过串口发送。通过配置串口的传输波特率并开启串口,发送的数据将基于ADC采集得到。我们通过Serial来配置串口的传输波特率并打开串口,其发送的数据将由ADC采样得到。
Arduino的DAC默认输出口设为A0,我们将使用analogRead绑定A0来读取DAC输出的电平。ADC发送的数字信号为二进制数据,Arduino支持的数字信号最高14位精度,我们可以使用analogReadResolution函数进行调整。
相比于使用示波器,使用ADC采样并通过串口发送数据绘制波形时,可能面临波特率与采样频率不匹配的问题。例如,初始设置的串口波特率为2000000,采样位数为14位,代码示例如下:
#include "analogWave.h"
analogWave wave(DAC);
int freq = 100;
void setup() {
// put your setup code here, to run once:
Serial.begin(2000000);
analogReadResolution(14);
wave.sine(freq);
wave.amplitude(0.5);
}
void loop() {
// put your main code here, to run repeatedly:
int value = analogRead(A0);
Serial.println(value);
}
观察到波形中出现了许多台阶,这可能是由于串口发送速率过快,导致相同数据的重复发送。串口的print和println函数在绘制波形时,默认以数据采集的个数为分度,因此重复数据形成了台阶。
由于ADC采样频率有上限,我们尝试降低串口波特率以获得更直观的波形展示。将ADC采样精度降低至8位以便换算,最终选定波特率为250000,绘制结果如下:
尽管波形上的台阶仍然存在,但长度已显著缩短,验证了之前的假设。此时,波形曲线大约每隔二至三个点就会更新一次数据,表明需要更精细地调整波特率。由于可用的波特率并不是连续可调的,如果使用115200波特率,波形将仅在几个点上出现台阶,如下图所示,图形非常不美观;而使用更低的波特率必将导致某些采样点缺失。
为避免数据重复发送,我们最终选择在循环中加入了1ms的延时。当然,我们也可以通过计算波特率和采样频率来准确计算数据发送的间隔,并使用delayMicroseconds函数实现微秒级精确延时。最终的完整代码如下:
#include "analogWave.h"
analogWave wave(DAC);
int freq = 100;
void setup() {
// put your setup code here, to run once:
Serial.begin(250000);
analogReadResolution(8);
wave.sine(freq);
wave.amplitude(0.5);
}
void loop() {
// put your main code here, to run repeatedly:
int value = analogRead(A0);
Serial.println(value);
delay(1);
}
在IDE的串口绘制工具中,我们得到了相对完整的波形,虽然它不是完美的正弦波,但幅度与设置的一致(使用8位精度,最大值为255,0.5倍量程,最大幅值应在128左右,最高点可能未被采集或发送):
动态图像可以在后面的结果视频中查看。在后续的模块绘制任务中,我们将按照这个波特率和采样精度对串口和ADC进行配置。
3. 基础任务(必做):用OPAMP放大DAC信号
在进行OPAMP模块的任务之前,我们首先参考了官方文档【Arduino UNO R4 WiFi OPAMP | Arduino Documentation】,,以了解Arduino内置运算放大器的管脚配置。里面介绍了Arduino运放的管脚配置如下:
文档中是以一个电压跟随器为例的,其中输入信号连接至A1(运放的正输入端),A2作为负输入端,A3则作为输出端。为了实现电压跟随效果,我们将A2和A3直接连接,这样A3的输出电压将恒等于A1的输入电压。
若要实现电压放大功能,我们可以通过在A2和A3之间分配不同比例的电阻来实现预定比例的电压放大。常用的同相放大电路图如下:
根据理想运放的“虚短”和“虚断”特性,很容易推导得到如下表达式,即放大的倍数为(1+R2/R1)。以一个两倍的电压放大电路为例,我们只需要取R1与R2相等,就能够实现一个两倍的放大。
我们选择R1和R2均为10kΩ的电阻。电路连接方式如下:A0作为DAC的输出,与A1相连,作为运放的同相输入端;A2作为运放的反相输入端,通过R1和R2电阻分别连接至GND和A3。
在代码中,我们需要配置好OPAMP模块,调用OPAMP.h头文件,并设置运放的工作模式为高速模式。由于ADC读取的数值来源于运放的输出端口,我们将analogRead端口绑定为A3。完整的代码如下:
#include "analogWave.h"
#include <OPAMP.h>
analogWave wave(DAC);
int freq = 100;
void setup() {
// put your setup code here, to run once:
Serial.begin(250000);
analogReadResolution(8);
wave.sine(freq);
wave.amplitude(0.5);
OPAMP.begin(OPAMP_SPEED_HIGHSPEED);
}
void loop() {
// put your main code here, to run repeatedly:
int value = analogRead(A3);
Serial.println(value);
delay(1);
}
最终的结果如图所示:
由于我们设计的是一个二倍放大器,所以从ADC采样到的电压从原来的半量程(8位,约128)增加到了满量程(255左右),成功实现了输出电压的翻倍。动态的图像可以在后面的结果视频中查看。
4. 基础任务(必做):用ADC采集并上传到上位机显示
在本任务中,我们将通过ADC模块采集信号,并利用上位机进行显示,以直观地呈现信号的变化。这一过程在前两个任务中已有展示,本次任务将对之前的DAC输出信号和经OPAMP放大后的信号进行汇总显示,从而直观地展示放大器的效果。
由于DAC输出和OPAMP输出位于不同的管脚,我们需要分别对这两个信号进行ADC模拟读取。值得注意的是,当通过串口交替传输这两个信号时,不能连续使用println函数,因为这会导致串口绘图工具将它们视为同一个变量进行绘制,如下:
为了避免这一问题,我们需要在数据之间手动插入换行或空格来进行区分。最终的代码示例如下:
#include "analogWave.h"
#include <OPAMP.h>
analogWave wave(DAC);
int freq = 100;
void setup() {
// put your setup code here, to run once:
Serial.begin(250000);
analogReadResolution(8);
wave.sine(freq);
wave.amplitude(0.5);
OPAMP.begin(OPAMP_SPEED_HIGHSPEED);
}
void loop() {
// put your main code here, to run repeatedly:
int dacvalue = analogRead(A0);
int opampvalue = analogRead(A3);
Serial.print(dacvalue);
Serial.print(" ");
Serial.println(opampvalue);
delay(1);
}
最终结果如下:
在图中,蓝线代表DAC的输出信号,而红线代表经OPAMP放大后的信号,直观地展示了二倍电压放大的效果。动态的图像可以在后面的结果视频中查看。
有一个疑问就是,DAC的输出是否一定绑定在A0这个端口,是否可以通过配置信息来进行修改?之前因为疏忽忘记把A0和A1这条给连在一起了,导致输出的不正常,所以想到除了我们这样在外电路飞线连接的方式,是否有在Arduino内部直接进行配置连接的方式呢?
[localvideo]5d7e005a98463fe63789514f3ccfdce7[/localvideo]
- 2024-10-05
-
回复了主题帖:
共读颁奖:《人工智能实践教程——从Python入门到机器学习》
管理辛苦了!
- 2024-09-15
-
发表了主题帖:
【Follow me第二期】入门任务 - 开发环境配置+LED点亮+串口打印+LED点阵驱动
我们非常有幸参加本期的Follow me活动,能够亲自体验Arduino这一享誉盛名的开源硬件平台。Arduino以其易用性和灵活性,成为全球电子爱好者和教育者的首选工具,期待本次活动有所收获。
1. 开箱
9月6日,我们收到了DigiKey提供的包裹,内含Arduino UNO R4 WiFi开发板一套,以及SHT40温湿度传感器和LTR-329光照传感器的扩展板各一块。此外,还包括了一条用于连接的数据线。在开箱过程中,我们注意到两个传感器扩展板均附带了排针,这表明用户可以通过焊接操作,将传感器直接连接至开发板的排母上,从而实现快速部署。
我们对所收到的产品进行了仔细检查,确认所有配件均齐全且完好无损。开发板的制造工艺精良,细节处理得当,体现了制造商的高标准和对品质的严格要求。值得一提的是,开发板还附带了一个亚克力底座,这一设计不仅提升了产品的美观度,更重要的是,它能够有效防止开发板背面的管脚在操作过程中发生误触,确保了操作的安全性。
2. 入门任务(必做):搭建环境
在进行Arduino开发之前,搭建一个稳定且高效的开发环境是至关重要的。鉴于之前没有使用过Arduino,我们首先查阅了其数据手册【ABX00087-datasheet.pdf (digikey.com)】。该手册不仅详尽地列出了板卡的技术参数、管脚功能及其布局,还提供了基本的使用指南,为我们的后续开发工作奠定了基础。
Arduino提供了两种主要的开发工具:本地IDE和在线编辑器。用户可以通过访问Arduino的官方网站【Arduino - Home】,在顶部菜单中找到对应开发板的使用说明,并下载所需的编辑器。此外,左侧菜单中的Learn Arduino部分提供了不同编辑器的初步教程,为初学者提供了详尽的指导。
本地编辑器的下载地址可以在Software模块找到【Software | Arduino】,不过作为初步尝试,我们选择先Arduino的在线编辑器开发一些短平快的小demo。用户可以通过Software模块顶部的Go To Cloud Editor选项进入在线编辑器,同时,相关的使用教程也在【Using the Arduino Cloud Editor | Arduino Documentation】中提供。
在使用在线编辑器之前,需要注册一个Arduino账户,并安装Arduino Create Agent插件。安装并激活插件后,其图标将显示在电脑右下角的工具栏中。
至此,Arduino在线编辑器的环境配置已基本完成。与我们之前使用的一些单片机开发环境相比,Arduino的配置过程显得尤为简单直观。
完成环境配置后,将Arduino开发板通过USB线连接至电脑,即可开始编写代码并进行烧录。这一过程的便捷性,再次体现了Arduino平台的易用性。
3. 入门任务(必做):LED点亮以及串口打印
LED点亮和串口打印功能这两项基础功能是电子项目开发中不可或缺的部分,对于初学者而言,掌握它们是进入更高级应用的前提。
在Arduino的在线编辑器中,通过点击左侧菜单的Sketches选项,我们可以创建一个新的工程。对于我们这样初次接触Arduino编程的用户,可能会感到无从下手。此时,可以利用编辑器左侧的Examples菜单,其中包含了丰富的示例代码,为我们提供了宝贵的参考。
我们以“Blink”例程为例,该例程展示了如何控制LED的点亮与熄灭。Arduino程序主要由两个函数组成:setup()和loop()。setup()函数负责初始化设置,如配置管脚模式和初始化外设,它仅在程序开始时执行一次。随后,程序将进入loop()函数,这是程序的主循环,所有持续执行的任务都在这里进行。
在“Blink”例程中,我们首先将LED对应的管脚设置为输出模式。这样,我们就可以通过改变该管脚的电平状态(高电平或低电平),来控制LED的点亮与熄灭。在loop()函数中,我们通过编写代码,使LED以一定的频率闪烁。
将上述代码复制到我们的工程中,并烧录到Arduino开发板上。烧录过程中,开发板上的两盏LED指示灯会短暂点亮,以指示烧录过程正在进行。烧录完成后,只有我们指定的LED会按照预设的频率反复点亮和熄灭,实现闪烁效果。
串口打印功能的实现与LED点亮类似。在setup()函数中,我们需要初始化串口通信,并设置适当的波特率。在loop()函数中,我们编写代码,使开发板能够周期性地通过串口发送字符串信息。
我们将LED点亮和串口打印的代码合并到一个工程中,进行烧录,具体代码如下:
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
Serial.begin(9600);
}
void loop() {
Serial.println("Hello EEWorld!");
digitalWrite(LED_BUILTIN, HIGH);
delay(1000);
digitalWrite(LED_BUILTIN, LOW);
delay(1000);
}
烧录完成后,开发板上的LED将按照预设频率闪烁,同时,通过打开编辑器的串口监视器,我们可以观察到开发板发送的字符信息。
4. 基础任务(必做):驱动12x8点阵LED
接下来尝试驱动一下LED的点阵。与单个LED的控制相似,点阵LED的控制原理也是通过设置对应位置的管脚电平来控制LED的点亮或熄灭。然而,由于涉及的LED数量较多,我们无法对每个LED进行单独配置,因此需要采用一种编码规则来实现有效的控制。
我们首先需要引入Arduino_LED_Matrix.h这个头文件,它包含了控制矩阵LED所需的函数。这个库可以通过Arduino IDE的Libraries菜单搜索并添加到项目中。
查看教程中有关LED Matrix的部分【Using the Arduino UNO R4 WiFi LED Matrix | Arduino Documentation】,我们学习了如何使用这个库来控制LED点阵。相关的例程也可以在LED Matrix库中找到。大致学习了一下教程和例程,对点阵进行编码的方式可以总结为如下两种。
第一种是直接构建一个8x12的数组,每个元素代表一个LED的状态,0代表熄灭,1代表点亮。这种方法直观且易于理解。例如,我们参考了“DisplaySingleFrame”例程,设计了一个笑脸图案,如下:
在循环主体中,我们使用matrix.renderBitmap()函数来驱动LED矩阵,将数组中的0和1映射到对应的LED位置,实现图案的显示。这个Bitmap非常生动形象的概括了点亮LED的过程,即将各1位的0或1比特映射到对应的LED矩阵位置。
第二种方法是对整个矩阵进行简化表达,使用三个32位的uint32_t变量来表示整个矩阵。我们将上述数组按照4位一组转换成16进制数,从而简化了代码的复杂性,如下:
在主体循环中,我们使用matrix.loadFrame()函数来驱动LED矩阵,这种方法使得代码更加简洁和优雅。Frame表示已经把整个图像框起来打包好了,是一个整体。
在浏览例程时,我们注意到“GameOfLife”例程提供了动态效果的实现。受此启发,我们制作了一个动态点亮笑脸图案的小动画。该动画的原理是随机选择矩阵中的行和列进行点亮或熄灭,然后不断刷新新的Bitmap以展示动态效果。
为了避免因随机性导致的长时间卡顿,我们引入了一个矩阵来记录已经随机过的位置。这样,下一次遇到重复位置时,我们可以跳过,从而避免了循环中的卡顿。通过统计与目标图案点亮位置相同的个数,一旦匹配完成,即结束随机点亮过程,不必再等待所有位置都被随机遍历一遍了。
即便如此,有时候最后几个位置还是比较慢才会随机到。主要是C语言很久没使用了,平时写Python和Matlab习惯了,一下子遇到C语言不太会写了,写的非常笨重,循环套循环的,希望大家批评指正。具体代码如下:
#include "ArduinoGraphics.h"
#include "Arduino_LED_Matrix.h"
ArduinoLEDMatrix matrix;
#define ROWS 8
#define COLUMNS 12
#define BITNUM 24
uint8_t Figure[8][12] = {
{ 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0 },
{ 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0 },
{ 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1 },
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
{ 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1 },
{ 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0 },
{ 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0 }
};
void setup() {
Serial.begin(115200);
matrix.begin();
uint8_t initFigure[ROWS][COLUMNS] = {0};
uint8_t currentFrame[ROWS][COLUMNS] = {0};
int count = 0;
for (int i = 0; i < ROWS*COLUMNS; i++) {
bool placed = false;
while (!placed) {
int row = random(ROWS);
int col = random(COLUMNS);
if (initFigure[row][col] == 0) {
initFigure[row][col] = 1;
if (Figure[row][col] == 1) {
currentFrame[row][col] = 1;
count++;
}
placed = true;
}
if (count == 30){
break;
}
}
matrix.renderBitmap(currentFrame, ROWS, COLUMNS);
}
}
下图是其随机生成过程中以及快要画完时候的LED矩阵。
除了动态图案,我们还尝试了使用LED矩阵显示字符。这主要借助于ArduinoGraphics.h库。在“TextWithArduinoGraphics”例程中,我们学习了如何使用matrix.beginDraw()和matrix.endDraw()构建显示框架,并设置了字体大小和内容。通过matrix.textScrollSpeed()函数,我们可以控制字符的滚动速度和方向。具体代码如下:
void loop() {
matrix.beginDraw();
matrix.stroke(0xFFFFFFFF);
matrix.textScrollSpeed(200);
const char text[] = " Hello EEWorld! ";
matrix.textFont(Font_5x7);
matrix.beginText(0, 1, 0xFFFFFF);
matrix.println(text);
matrix.endText(SCROLL_LEFT);
matrix.endDraw();
delay(500);
}
显示的结果如下:
综上所述,我们实现了LED矩阵的多种驱动方式和功能。我们将随机生成笑脸图案的代码放在setup()函数中,作为一次性的开屏动画。动画结束后,开发板将循环播放字符串滚动效果。
具体的实验过程和效果,可参考随附的视频演示。
[localvideo]d98f5afefadc17e0b205f9fe24c2b00e[/localvideo]