#第七届立创电赛#2022暑期训练营项目解析:基于STM32的USB电能采集表设计与实现
第七届立创电赛项目解析手把手教你做USB电能采集表最近有不少朋友在问想参加电子设计比赛或者自己做个实用的工具有没有什么既有意思又能学到东西的项目正好我之前研究过一个“USB电能采集表”的项目它用STM32单片机能测量USB口的电压、电流还能计算功率、统计用了多少电功能挺全的。今天我就把这个项目的设计思路和实现方法掰开揉碎了讲给大家听就算你是刚接触嵌入式的新手跟着做也能做出来。这个项目能干什么呢简单说它就是一个USB“电表”。你把它插在手机充电器、充电宝或者电脑USB口和设备之间它就能实时显示电压、电流帮你算出功率还能统计一段时间内总共消耗了多少电能比如给手机充一次电用了多少度电。更实用的是它还能把测量到的数据存起来方便你回头查看历史记录。项目里还做了简单的波形显示虽然精度受屏幕限制但看个趋势没问题。下面咱们就从硬件到软件一步步来看它是怎么实现的。1. 项目功能与设计思路首先咱们得搞清楚这个采集表具体要完成哪些任务心里有个谱后面设计硬件和写程序才不会跑偏。1.1 核心功能清单根据项目要求这个USB电能采集表需要实现以下四个核心功能实时测量与显示能测量最大6V的电压和最大5A的电流并实时计算、显示功率功率电压×电流。电能计量与存储不仅能看瞬时值还要能像家里的电表一样累计消耗的电能。并且这个累计的电能值要能存起来断电再开机也不能丢。历史数据查询存起来的数据要能翻出来看比如可以查看之前8次的测量记录。波形绘制在屏幕上画出电压、电流或功率随时间变化的曲线方便观察趋势。为了实现人机交互项目使用了两个按键START和HISTORY和一块屏幕通过不同的按键操作在不同的功能界面之间切换。1.2 操作逻辑详解操作逻辑是软件设计的骨架理解清楚了代码结构就清晰了。整个系统主要有三个界面电压电流测量界面主界面显示内容实时电压、电流、功率。短按START/HISTORY切换显示电压波形或电流波形。长按START进入“电能计量界面”。长按HISTORY翻转屏幕显示方向横屏/竖屏。电能计量界面进入方式在主界面长按START。显示内容当前电压、电流、累计电能、计时时间、电流方向是设备在用电还是在放电。单击START开始计时电能累计正式开始。单击HISTORY记录一次快照。将此刻的电压、电流、电能值和时间保存下来。最多能存8次存满后新的会覆盖最早的。长按START退出此界面并自动将8条历史记录保存到芯片内部的存储器中。历史电能查询界面进入方式在主界面单击HISTORY。显示内容以分页形式显示之前保存的8条记录每条记录包含电压、电流、电能和记录时间。单击START向后翻页下一页。单击HISTORY向前翻页上一页。长按START清除所有历史记录。长按HISTORY退出查询界面回到主界面。注意这里的“长按”和“短按”单击是按键检测的关键后面软件部分会详细讲如何用程序区分它们。2. 硬件设计要点与安全考量虽然原文没有给出详细的电路图但我们可以从描述中推断出关键的硬件组成部分和重要的安全注意事项。2.1 主要硬件模块一个完整的USB电能采集表硬件上通常包含以下几部分主控芯片MCU项目用的是STM32系列单片机。从它使用“内部FLASH模拟EEPROM”这个功能来看这是一款STM32非常典型的特性。STM32负责运行所有程序处理数据控制显示和按键。电压/电流采样电路电压采样通常使用电阻分压网络。因为USB电压最高5V项目支持到6V需要用两个电阻把电压按比例降低到STM32的ADC模数转换器可以安全测量的范围通常是0-3.3V。电流采样有两种常见方式。一是使用采样电阻如毫欧级电阻串联在电流回路中测量电阻两端的压降再根据欧姆定律IU/R算出电流。二是使用电流传感器芯片如ACS712它集成了采样和信号调理输出一个与电流成正比的电压信号。无论哪种最终输出都是一个电压信号给STM32的ADC。ADC通道STM32需要至少两个ADC通道一个接处理后的电压信号一个接处理后的电流信号。人机交互部分一个显示屏如OLED或TFT LCD用于显示两个物理按键。USB接口与供电需要两个USB-A母座一个输入一个输出以及为STM32和屏幕供电的LDO低压差线性稳压器电路通常从输入USB的5V降压到3.3V。2.2 非常重要的安全与散热提醒原文作者在测试时提到了一个非常关键的细节这里我必须着重强调因为这关系到会不会烧坏设备甚至引发危险“5V/5A输入带负载测试10分钟USB口附近差不多有60多度”“如果要用的话使用范围尽量控制在5V2A以内吧毕竟长时间USB扛不住。”问题根源USB接口和连接线本身有很小的内阻。当通过大电流如5A时根据焦耳定律发热量 I² × R会在接口和线材上产生显著的发热。长时间工作可能导致USB接口塑料部分软化、触点氧化甚至存在安全隐患。给你的建议明确使用范围这个设计更适合用于测量和监控而不是作为大功率设备的长期供电通路。比如测一下充电宝的输出能力、看看小风扇的功耗是没问题的。功率预留如果你要自己设计PCB务必给电流走线预留足够的宽度并且考虑在USB接口的电源引脚附近增加散热焊盘或辅以少量散热硅脂。选用优质部件使用质量好的、带功率标识的USB公对母线缆和接口。实时监控好在我们的设备本身就能显示电流使用时如果发现电流持续很大比如超过2A就要留意发热情况了。3. 软件设计与代码实现这是项目的核心咱们来详细拆解每个功能是怎么用代码实现的。3.1 ADC采样与数据处理测量准不准首先看采样。原文提到的方法是“开了两个通道因为一个采电压一个采集电流没有开启连续转换模式我是用定时器每10MS触发一次采样采20个点非连续做一个滤波处理所以计量数据差不多是1S刷新5次。”我来翻译和解释一下这个设计为什么用定时器触发而不是连续转换连续转换模式是ADC一个接一个不停地转换会占用大量CPU时间和总线资源。用定时器触发比如STM32的TIMx_TRGO事件可以精确控制采样间隔。这里设定为每10ms0.01秒采一次样非常规整有利于后续计算和滤波。“采20个点非连续”是什么意思我理解作者的意思是每次定时器触发ADC转换时并不是只转换1次而是让ADC连续转换20次针对一个通道或者通过DMA连续采集两个通道各20个点。这20个点是在极短时间内快速采完的对于STM32的ADC采20个点可能就几百微秒。这20个点是一组“原始数据”目的是为了做滤波。滤波处理直接读一次ADC值噪声可能很大。常见的简单滤波方法是取平均值。把这快速采集的20个点的电压值或电流值加起来除以20得到一个更稳定、更平滑的测量值。这样每10ms我们能得到一对滤波后的电压、电流值。数据刷新率每10ms得到一组新数据那么1秒钟就能得到100组数据。但原文说“1S刷新5次”我猜可能是为了显示更稳定程序里又做了一个滑动平均或者每200ms即20组数据才更新一次显示和计算。这样屏幕上的数字不会跳得太快易于观察。伪代码思路如下// 定时器中断服务函数每10ms触发一次 void TIMx_IRQHandler(void) { if (TIM_GetITStatus(TIMx, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIMx, TIM_IT_Update); // 触发ADC开始一组转换例如通过DMA采集20对点 ADC_SoftwareStartConv(ADC1); } } // ADC采集完成中断或DMA传输完成中断 void ADC1_IRQHandler(void) { // 读取DMA缓冲区中的20个电压值和20个电流值 // 分别对它们求和并取平均值 filtered_voltage average(adc_voltage_buffer, 20); filtered_current average(adc_current_buffer, 20); // 将滤波后的值存入另一个缓冲区用于后续的显示和计算 // ... }3.2 按键扫描与状态识别按键处理是嵌入式里的基本功但做好也不容易。原文说“没有采用外部中断用的是扫描式按键利用定时器去扫通过时间来判断按键长按和短按。”这是一个非常经典且可靠的方案。外部中断虽然响应快但在有抖动和需要识别长按时软件处理起来反而麻烦。定时扫描法思路清晰定时扫描设置一个定时器比如每5ms中断一次。在中断服务函数里去读取连接按键的GPIO引脚的电平。消抖处理当检测到引脚电平变为按下状态通常是低电平时不要立即认为按键按下而是等待一段时间比如20ms再次检测。如果仍然是按下状态才确认按键真的被按下了。这能滤除机械触点闭合瞬间的抖动。计时与状态判断确认按键按下后开始一个计时器可以用一个变量累加定时中断次数。如果在一定时间内比如500ms内检测到按键释放了就判定为短按单击。如果按下时间超过设定的阈值比如1秒就判定为长按并触发长按对应的功能。按键释放后重置状态等待下一次按下。// 按键结构体用于管理每个按键的状态 typedef struct { uint8_t current_state; // 当前物理电平 uint8_t last_state; // 上次扫描的物理电平 uint8_t filtered_state; // 消抖后的稳定状态 uint32_t press_duration; // 按下持续时间以扫描周期为单位 uint8_t short_press_flag; // 短按事件标志 uint8_t long_press_flag; // 长按事件标志 } Key_Typedef; Key_Typedef key_start, key_history; // 定时器中断服务函数5ms一次 void SysTick_Handler(void) { key_scan(key_start, GPIO_START_PIN); key_scan(key_history, GPIO_HISTORY_PIN); // ... 其他定时任务 } // 按键扫描函数 void key_scan(Key_Typedef *key, GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) { key-current_state GPIO_ReadInputDataBit(GPIOx, GPIO_Pin); // 读取引脚电平 // 消抖连续几次读到相同状态才改变 filtered_state if (key-current_state ! key-last_state) { key-debounce_counter 0; } else { key-debounce_counter; if (key-debounce_counter DEBOUNCE_TIME) { key-filtered_state key-current_state; } } key-last_state key-current_state; // 状态判断 if (key-filtered_state KEY_PRESSED) { // 按键处于稳定按下状态 key-press_duration; if (key-press_duration LONG_PRESS_TIME) { key-long_press_flag 1; // 触发长按标志 } } else { // 按键处于稳定释放状态 if (key-press_duration 0 key-press_duration LONG_PRESS_TIME) { key-short_press_flag 1; // 触发短按标志 } key-press_duration 0; // 释放后重置计时 } }在主循环里你只需要检查short_press_flag和long_press_flag然后执行相应的界面切换或功能操作即可。3.3 电能计算与存储这是项目的核心算法和数据管理部分。电能计算 功率 P 电压 U × 电流 I。电能 E 是功率对时间的积分。在数字系统中我们用累加来近似积分。 每次得到一组新的滤波后的电压U(k)和电流I(k)就计算瞬时功率P(k) U(k) * I(k)。 电能增量ΔE P(k) * Δt其中Δt是这次采样和上次采样的时间间隔比如固定的10ms。 总电能E_total E_total ΔE。 这就是最基本的电能计量方法。更复杂的还会考虑功率因数等但对于直流USB设备这个算法足够了。数据存储与查询 原文的方案简洁有效内存数组在RAM中定义一个二维数组data[8][4]。8行代表最多存8条记录4列分别存放电压、电流、电能、时间。float history_data[8][4]; // 假设用float存储记录操作在“电能计量界面”每次单击HISTORY键就将当前的电压、电流、累计电能和当前计时时间存入数组的下一行。存储到FLASHSTM32芯片内部都有FLASH存储器用来存放程序。我们可以划出一小部分最后一页或几页来模拟EEPROM存放这些数据。在长按START退出电能计量界面时调用FLASH写入函数将整个history_data数组的数据写入芯片的特定FLASH地址。// 伪代码保存数据到FLASH void Save_History_To_Flash(void) { FLASH_Unlock(); // 解锁FLASH编程 // 擦除要写入的FLASH扇区必须先擦后写 FLASH_EraseSector(FLASH_SECTOR_11, VOLTAGE_RANGE_3); // 将history_data数组的数据逐个写入FLASH uint32_t flash_address FLASH_BASE_ADDRESS; for(int i0; i8; i) { for(int j0; j4; j) { // 将float类型数据转换为32位整数或以字节形式写入 uint32_t data_word *(uint32_t*)history_data[i][j]; FLASH_ProgramWord(flash_address, data_word); flash_address 4; // 地址增加4字节 } } FLASH_Lock(); // 上锁FLASH }上电读取单片机每次开机时在程序初始化部分从那个特定的FLASH地址把数据读出来再恢复到history_data数组中。这样历史记录就实现了掉电保存。查询与清除在“历史电能查询界面”通过按键翻页其实就是读取history_data数组不同行的数据并显示。长按START清除记录就是将数组清零并再次擦写FLASH区域。提示FLASH擦写次数有限通常约1万到10万次不要频繁写入。本项目只在退出时保存一次是合理的。切忌在循环里不停地保存。3.4 波形绘制与界面管理波形绘制受限于屏幕分辨率原文提到Y轴只有32个点。基本思路是在内存中维护一个数组作为显示缓冲区比如waveform_buffer[128]对应屏幕X轴的128个点。每次刷新数据时比如每200ms将最新的电压或电流值根据量程转换为屏幕Y轴坐标0-31放入缓冲区末尾并将整个缓冲区数据向左移动一位或循环覆盖。在屏幕上将这个缓冲区里的点用线连接起来就形成了波形图。界面管理UI是一个状态机。根据当前所处的界面主界面、计量界面、查询界面和接收到的按键事件短按START、长按HISTORY等决定下一个要显示哪个界面并调用相应的显示绘制函数。4. 项目总结与优化思考跟着上面思路走下来一个完整的USB电能采集表项目就清晰了。它涵盖了嵌入式开发的几个关键环节ADC采样模拟信号获取、定时器应用精准定时触发、按键处理人机交互、数据存储FLASH操作、UI状态机界面管理以及基础电路知识。如果你要自己动手实现或者在此基础上优化可以考虑以下几点提高测量精度校准ADC的参考电压对分压电阻使用更高精度的型号软件上可以实施更复杂的校准算法如两点校准。优化存储除了存储8次快照还可以考虑存储一段时间的详细用电日志如每分钟的平均功率和累计电能但这需要更大的存储空间可能需要外置SPI Flash或SD卡。增强通信功能为STM32增加USB-CDC或串口功能将实时数据发送到电脑上位机进行更专业的分析和图表绘制。完善安全保护在硬件上增加输入过压、过流保护电路在软件上设置电流超限报警。这个项目麻雀虽小五脏俱全非常适合作为STM32的进阶实战练习。希望这篇解析能帮你理清思路。在实际动手时你可能会遇到ADC读数不稳、FLASH写入失败、屏幕显示乱码等各种问题别怕那正是学习的过程。调试的过程就是经验积累的过程。祝你制作顺利