1. 项目缘起:为什么Jetson Orin开发者需要一个“C”型UPS电源模块?
最近在折腾Jetson Orin Nano做边缘AI项目,最头疼的问题不是算力,也不是模型部署,而是供电。项目需要部署在移动机器人上,或者放在一个没有稳定市电的户外环境里,比如农业监测、野外巡检。市面上的成品UPS要么体积巨大,要么接口不匹配,要么就是价格贵得离谱。更重要的是,很多UPS的通信和控制逻辑是黑盒,你根本不知道它什么时候会突然断电,导致你的Orin设备在关键时刻“猝死”,数据丢失、文件系统损坏,这种痛想必很多搞边缘计算的朋友都经历过。
于是,一个想法就冒出来了:能不能自己做一个专为Jetson Orin这类嵌入式AI平台设计的UPS电源模块?目标很明确:体积要小,能塞进紧凑的机箱;电量要足,至少保证设备在断电后能撑个十几二十分钟完成安全关机;最关键的是,要“智能”。设备得能实时知道UPS的剩余电量、输入电压状态,这样才能在断电时触发自定义的关机脚本,而不是被动等待电池耗尽。这就是“UPS Power Module (C)”这个项目的初衷。这里的“(C)”在我这里,更多是指一个可定制、可通信的版本。
这个模块的核心逻辑并不复杂:它本质上是一个带有电池管理、DC-DC升降压和I2C通信接口的电路板。平时,外部适配器(比如12V)供电,同时给锂电池充电,并通过升降压电路输出稳定的5V给Jetson Orin。一旦外部电源断开,电路无缝切换到电池供电,继续输出5V。同时,主控MCU(比如GD32F105)通过I2C总线,将电池电压、电流、剩余电量百分比、输入电源状态等信息实时上报给Jetson Orin。Orin上跑一个后台守护进程,一旦检测到“外部电源断开”且“电池电量低于阈值”,就自动执行sudo shutdown -h now,实现优雅关机。
2. 核心架构与硬件选型:从需求到原理图
要做一个靠谱的UPS,硬件是地基。整个模块可以拆解为几个核心部分:电池及保护、充电管理、升降压转换、主控与通信、状态监测。每一部分的选择都直接关系到最终方案的可靠性、成本和体积。
2.1 电池选型与保护电路:为什么是21700?
首先看储能单元。项目关键词里提到了“21700”,这几乎是目前能量密度和放电能力平衡得最好的锂离子电池型号之一。相比更常见的18650,21700直径21mm,长度70mm,单节容量轻松做到4000mAh以上,持续放电电流也能达到10A左右,完全能满足Jetson Orin Nano(峰值功耗10-15W)的需求。
注意:直接使用裸锂离子电池是极其危险的,必须搭配保护板。保护板的核心是DW01+8205这类专用芯片,它能防止电池过充(超过4.25V)、过放(低于2.5V)、过流和短路。在设计时,保护板的充电(C-)和放电(P-)端口要分开引出,分别接到充电管理和系统负载的输入端。
2.2 充电与升降压方案:如何实现无缝切换?
这是整个模块的“心脏”。我们需要一个能在有外部电源时给电池充电、同时给系统供电,断电时由电池给系统供电的电路。常见的做法有两种:一是使用带有Power Path(电源路径管理)功能的专用芯片,如TI的BQ25703;二是用“充电芯片+升降压芯片+MOSFET切换电路”自己搭建。
对于DIY项目,第二种方案更灵活、成本也更可控。充电芯片可以选择支持4.2V截止的单节锂电池充电管理IC,比如TP5100,它外围简单,电流可调。升降压芯片则要选支持宽电压输入、输出电流足够的型号,比如SY8303(支持2.8V-22V输入,3V-20V输出,最大3A电流)。Jetson Orin需要稳定的5V,SY8303完全胜任。
无缝切换的逻辑通过MOSFET实现:当外部电源(VIN)存在时,一个PMOS管导通,系统负载由VIN经升降压芯片提供;同时,VIN也通过一个二极管(或理想二极管控制器)给充电芯片供电。当VIN断开,PMOS关断,电池电压通过另一个PMOS或理想二极管控制器“顶替”上去,继续为升降压芯片供电。由于MOSFET切换速度在微秒级,对于Orin这样的负载,电压跌落几乎察觉不到,实现了“零延时”切换。
2.3 主控MCU与I2C通信:GD32F105的实战考量
状态监控和通信需要一个大脑。关键词里提到了“GD32F105”,这是一颗基于Cortex-M3内核的国产MCU,性价比高,外设丰富。它在这里的核心任务有三个:
- 采集模拟量:通过ADC通道实时读取电池电压、电池充电/放电电流(通过采样电阻和运放)、输入电压(VIN)。
- 计算电量:实现一个简单的库仑计算法,通过积分电流和时间来估算电池剩余容量(SOC)。虽然不精准,但对于判断“低电量关机”足够了。
- I2C从机通信:将采集到的数据(电压、电流、SOC%、输入状态)通过I2C总线暴露给主机(Jetson Orin)。
选择I2C协议是因为它在嵌入式领域太通用了,Jetson Orin的40pin扩展口上就有现成的I2C总线(例如I2C1),接线只需要SDA、SCL两根线,再加一个接地。MCU作为从设备,需要设定一个7位I2C从机地址(例如0x50)。Orin上的用户态程序(用Python或C写)就可以通过smbus库轻松读取这些数据。
实操心得:I2C总线上一定要加上拉电阻!这是无数人踩过的坑。SDA和SCL线是开漏输出,必须通过电阻上拉到电源电压(比如3.3V),否则信号无法拉高,通信根本建立不起来。电阻值通常在4.7kΩ到10kΩ之间,根据总线电容和速度调整。如果总线上设备多或线长,可以适当减小电阻值。
2.4 电平转换与隔离:5V与3.3V的共处之道
这里有一个细节:Jetson Orin的GPIO电平是1.8V或3.3V(取决于具体引脚),而我们的GD32F105大概率也工作在3.3V。但升降压芯片的反馈、使能等引脚可能是5V逻辑。如果模块内部有5V和3.3V两个域,就需要考虑电平转换。
对于I2C这种双向总线,简单的用两个NMOS管搭成的电平转换电路就非常经典且好用(关键词里也提到了“用二极管搭成的i2c电平转换电路”,但MOSFET方案更优)。它利用MOSFET的对称性,能实现3.3V和5V两侧的双向通信,且几乎无延迟。如果系统内干扰较大,或者为了安全起见,也可以在Jetson Orin的I2C引脚和UPS模块的I2C接口之间加入一个数字隔离器(如ADI的ADuM1250),实现电源和地的隔离,防止地环路噪声或意外高压损坏昂贵的Orin主板。
3. 固件设计:让MCU“会说话”的代码逻辑
硬件搭好了,下一步是让GD32F105“活”起来。固件程序可以用Keil MDK或STM32CubeIDE(GD32与STM32兼容性很好)开发。程序结构并不复杂,但有几个关键点需要仔细处理。
3.1 ADC采样与数据处理
ADC需要以一定的频率(比如1Hz)循环采样电池电压、电流和输入电压。这里最大的坑是噪声。开关电源(升降压电路)会产生高频噪声,如果直接采样,数值会跳得厉害。
我的做法是:
- 硬件滤波:在每个ADC采样通道前,加入一个RC低通滤波器(例如1kΩ电阻 + 0.1uF电容),截止频率约1.6kHz,能滤除大部分开关噪声。
- 软件滤波:采样后采用滑动平均滤波或中值滤波。我常用的是连续采样10次,去掉最大最小值,再取平均。这样得到的值就稳定多了。
- 校准:由于电阻分压精度、运放偏移等因素,ADC读出的原始值需要转换到实际电压。最好在板子上留出测试点,用万用表测量实际电压,然后在代码里做两点校准(
y = k*x + b),修正系数k和b。
电流采样通常使用毫欧级采样电阻(如10mΩ)和专用电流采样放大器(如INA199)。MCU读取的是放大后的电压,再根据放大倍数和采样电阻值换算成电流。注意电流有正负(充电为正,放电为负),运放电路要设计成能输出中间电平(如1.65V)代表0A。
3.2 库仑计与SOC估算
精确的电量计非常复杂,需要电池建模和大量参数。但对于UPS这种对精度要求不极致的场景,一个简化的库仑计算法足够用了。
核心思想是:剩余容量 = 初始容量 - 累计放电电量 + 累计充电电量。
- 在代码里定义一个变量
remaining_capacity_mAh,初始值设为电池标称容量(如4000mAh)。 - 在主循环中,每隔一个固定时间(如1秒),读取当前电流
I(单位mA)。 - 如果
I > 0(放电),则remaining_capacity_mAh -= (I * 1秒 / 3600)。 - 如果
I < 0(充电),则累加充电电量,但通常设置一个上限,即充满时remaining_capacity_mAh回到标称容量。 - SOC百分比 =
(remaining_capacity_mAh / 标称容量_mAh) * 100%。
这个算法有两个明显问题:第一,初始容量不准和累计误差会导致“漂移”;第二,它没考虑电池电压。因此,我们需要引入“电压校正”。当电池电压很高(如>4.1V)时,强制将SOC设为100%;当电池电压很低(如<3.3V)时,强制将SOC设为0%,并立即关闭输出以保护电池。在中间区域,则以库仑计结果为主。这样虽然不完美,但非常实用。
3.3 I2C从机实现与寄存器映射
要让Jetson Orin读取数据,我们需要在MCU端实现I2C从机协议。GD32的硬件I2C外设配置为从机模式相对简单。关键在于设计一套清晰的“寄存器映射表”。
我们可以把MCU内部的一块内存区域(比如一个结构体)映射为一系列I2C可访问的寄存器。例如:
- 寄存器0x00-0x01:电池电压(16位无符号整数,单位mV)。
- 寄存器0x02-0x03:电池电流(16位有符号整数,单位mA,正为放电)。
- 寄存器0x04:SOC百分比(8位无符号整数,0-100)。
- 寄存器0x05:状态寄存器(bit0: 1=外部电源正常,0=电池供电;bit1: 1=正在充电;bit2: 1=电池低电量告警)。
当Jetson Orin作为主机发起读操作,指定从机地址(0x50)和寄存器地址后,GD32的I2C中断服务程序就需要识别这个地址,并将对应寄存器的数据通过SDA线发送出去。这样,在Orin上,读取UPS状态就变成了简单的i2c_read_byte_data(0x50, 0x04)来获取电量百分比。
避坑指南:I2C通信失败,十有八九是时序问题。务必用逻辑分析仪抓一下SDA和SCL的波形,对照I2C协议时序图,检查起始信号、停止信号、ACK应答位是否符合规范。GD32的硬件I2C有时在从机模式下对时钟拉伸(Clock Stretching)的支持需要特别注意,如果遇到主机等待超时,可以尝试在从机处理数据时手动拉低SCL线以延长时钟周期。
4. Jetson Orin端软件配置:从读取数据到安全关机
硬件和固件都准备好了,最后一步是在Jetson Orin上编写“大脑”,让它知道何时该关机。
4.1 启用I2C总线与安装工具
首先,确保Orin的I2C总线已经启用。对于Jetson Orin Nano/NX,40pin接头上的I2C1总线通常是默认可用的。可以通过命令检查:
sudo apt update sudo apt install i2c-tools # 查看I2C总线设备 i2cdetect -l # 扫描I2C1总线上所有设备,应能看到地址0x50 sudo i2cdetect -y -r 1如果看不到i2c-1设备,可能需要修改设备树(Device Tree)来启用它,这涉及到/boot/目录下的dtb文件,操作需谨慎。
4.2 编写Python监控守护进程
用Python写一个后台守护进程是最方便的选择。使用smbus2这个库来操作I2C。
import time import smbus2 import subprocess import logging class UPSMonitor: def __init__(self, bus=1, address=0x50): self.bus = smbus2.SMBus(bus) self.address = address logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') def read_register(self, reg): """从指定寄存器读取一个字节""" try: return self.bus.read_byte_data(self.address, reg) except Exception as e: logging.error(f"Failed to read I2C register {reg}: {e}") return None def read_word(self, reg): """从指定寄存器读取一个字(两个字节,假设为小端序)""" try: data = self.bus.read_i2c_block_data(self.address, reg, 2) return data[0] | (data[1] << 8) except Exception as e: logging.error(f"Failed to read word from I2C register {reg}: {e}") return 0 def check_and_act(self): """检查UPS状态并采取行动""" # 1. 读取状态寄存器 status = self.read_register(0x05) if status is None: return external_power = (status & 0x01) == 0x01 battery_low = (status & 0x04) == 0x04 # 2. 读取电池电量和电压(用于日志) soc = self.read_register(0x04) voltage_mv = self.read_word(0x00) current_ma = self.read_word(0x02) # 注意有符号 logging.info(f"UPS Status: ExtPower={'ON' if external_power else 'OFF'}, SOC={soc}%, Voltage={voltage_mv/1000:.2f}V, Current={current_ma}mA") # 3. 核心逻辑:如果外部断电且电池电量低,则关机 if not external_power and battery_low: logging.critical("External power lost AND battery low! Initiating safe shutdown...") # 执行关机命令,这里可以加入数据保存、服务停止等自定义脚本 subprocess.run(['sudo', 'shutdown', '-h', 'now']) # 如果不想立即关机,可以设置一个延迟,比如2分钟后关机 # subprocess.run(['sudo', 'shutdown', '-h', '+2', "UPS Battery Low, shutting down"]) if __name__ == "__main__": monitor = UPSMonitor() while True: monitor.check_and_act() time.sleep(5) # 每5秒检查一次将这段代码保存为ups_monitor.py,并设置为系统服务(使用systemd),让它开机自启。
4.3 配置Systemd服务实现开机自启
为了让监控脚本在后台稳定运行,我们需要创建一个systemd服务。
- 创建服务文件:
sudo nano /etc/systemd/system/ups-monitor.service - 输入以下内容:
[Unit] Description=Jetson Orin UPS Monitor Service After=network.target [Service] Type=simple User=root ExecStart=/usr/bin/python3 /home/your_username/ups_monitor.py Restart=on-failure RestartSec=5s [Install] WantedBy=multi-user.target- 启用并启动服务:
sudo systemctl daemon-reload sudo systemctl enable ups-monitor.service sudo systemctl start ups-monitor.service sudo systemctl status ups-monitor.service # 检查状态现在,你的Jetson Orin就具备了“UPS感知”能力,能在断电且电量不足时自动安全关机,最大程度保护你的数据和硬件。
5. 调试、优化与进阶思考
把板子焊好,代码烧录进去,服务跑起来,这只是第一步。真正的挑战往往在调试和优化阶段。
5.1 上电时序与“打嗝”现象
一个常见的问题是“打嗝”,即系统在电池和外部电源切换的瞬间不断重启。这通常是由于上电时序或负载突变引起的。比如,当外部电源插入时,如果充电电流瞬间很大,可能导致输入电压被拉低,触发升降压芯片欠压保护而重启。
解决方案:
- 软启动:确保升降压芯片的软启动(Soft-start)功能被启用,让输出电压缓慢上升,避免冲击电流。
- 输入/输出电容:在升降压芯片的输入和输出端并联足够容量(如100uF)且低ESR的钽电容或陶瓷电容,提供瞬时电流缓冲。
- 逻辑控制:用MCU控制一个MOSFET,在检测到外部电源稳定接入后,再延迟几百毫秒才使能系统5V输出,避开冲击阶段。
5.2 I2C通信稳定性与抗干扰
在复杂的电磁环境中,I2C这种低速开漏总线很容易受干扰,导致通信错误甚至死锁。
加固措施:
- 布线:尽量让SDA、SCL走线平行且靠近,远离电源等噪声源。如果线长超过10cm,考虑使用双绞线。
- 上拉电阻:如前所述,必须加上拉电阻。在干扰强的环境,可以尝试减小阻值到2.2kΩ,以增强驱动能力。
- 软件容错:在Python守护进程中加入重试机制。如果一次读取失败,不要立即判定为故障,可以重试3-5次。连续多次失败后再记录错误。
- 看门狗:在GD32的固件中加入独立看门狗(IWDG),万一程序跑飞,能自动复位,避免I2C从机“死机”导致Orin读不到数据。
5.3 功耗优化与续航提升
对于移动应用,UPS模块自身的功耗也至关重要。GD32F105在运行时的功耗有几十mA,如果一直全速运行,会白白消耗电池电量。
优化点:
- MCU睡眠模式:GD32F105支持多种低功耗模式。我们的数据采样和I2C响应不需要实时性很高。可以配置MCU进入休眠模式(Sleep或Stop模式),通过定时器(如RTC)每1秒唤醒一次,进行ADC采样和计算,然后判断是否有I2C请求(可以通过I2C地址匹配中断唤醒),处理完再进入休眠。这样能将MCU的平均电流从几十mA降到1mA以下。
- 外围电路断电:当系统由电池供电且处于待机状态时,可以通过MCU控制MOSFET,关断不必要的周边电路(如状态指示灯、备用传感器)的电源。
- 升降压芯片效率:选择在轻载时效率高的DC-DC芯片。查看芯片数据手册的“效率 vs 负载电流”曲线,确保在Orin待机(可能只有1-2W)时,转换效率仍在85%以上。
5.4 功能扩展:从监控到控制
基本的UPS模块已经完成,但还有很多可以扩展的方向:
- 网络状态上报:除了本地关机,GD32还可以通过一个串口转Wi-Fi/4G模块(如ESP-01S或SIM800C),在断电或低电量时向远程服务器发送告警信息。
- 多电池管理:如果需要更长续航,可以设计为支持2节或更多21700电池并联。这时需要更复杂的电池均衡电路,或者直接选用支持多串的电池管理芯片(BMS)。
- 集成环境传感器:反正MCU还有多余的IO和ADC,可以集成一个温湿度传感器(如SHT30)或气压计,让UPS模块同时成为环境监测节点。
- 自定义关机脚本:在Orin的关机命令前,可以触发更复杂的脚本,比如将内存中的AI推理结果紧急写入SSD、向其他设备发送同步关机信号、保存系统状态快照等。
这个“UPS Power Module (C)”项目,从构思到实现,贯穿了电源设计、嵌入式编程和Linux系统管理多个环节。它解决的是一个非常具体的工程问题,但其中涉及的硬件选型、电路设计、通信协议和软件交互,却是嵌入式开发中非常通用的技能。自己动手做一遍,远比买一个成品理解得更透彻,当你的机器人在野外因为这套自制UPS而成功保存了关键数据时,那种成就感是无可替代的。