文章目录
- 一、项目简介与系统设计
-
- [1.1 项目背景](#1.1 项目背景)
- [1.2 系统整体架构](#1.2 系统整体架构)
- [1.3 硬件组成清单](#1.3 硬件组成清单)
- [1.4 核心功能说明](#1.4 核心功能说明)
- [1.5 系统工作流程](#1.5 系统工作流程)
- 二、开发环境搭建
-
- [2.1 软件工具安装](#2.1 软件工具安装)
-
- [2.1.1 KEIL MDK-ARM安装](#2.1.1 KEIL MDK-ARM安装)
- [2.1.2 STM32CubeMX安装](#2.1.2 STM32CubeMX安装)
- [2.1.3 驱动安装](#2.1.3 驱动安装)
- [2.2 创建STM32CubeMX工程](#2.2 创建STM32CubeMX工程)
-
- [2.2.1 新建工程](#2.2.1 新建工程)
- [2.2.2 系统时钟配置](#2.2.2 系统时钟配置)
- [2.2.3 GPIO配置](#2.2.3 GPIO配置)
- [2.2.4 定时器PWM配置](#2.2.4 定时器PWM配置)
- [2.2.5 I2C配置](#2.2.5 I2C配置)
- [2.2.6 串口配置](#2.2.6 串口配置)
- [2.2.7 ADC配置(备用电压检测)](#2.2.7 ADC配置(备用电压检测))
- [2.2.8 工程设置与代码生成](#2.2.8 工程设置与代码生成)
- 三、硬件电路设计详解
-
- [3.1 电源电路](#3.1 电源电路)
- [3.2 INA226接线说明](#3.2 INA226接线说明)
- [3.3 OLED接线说明](#3.3 OLED接线说明)
- [3.4 MOS管驱动电路](#3.4 MOS管驱动电路)
- [3.5 按键电路](#3.5 按键电路)
- [3.6 蜂鸣器电路](#3.6 蜂鸣器电路)
- [3.7 LED指示电路](#3.7 LED指示电路)
- [3.8 完整接线图](#3.8 完整接线图)
-
- [3.8.1 STM32引脚分配总表](#3.8.1 STM32引脚分配总表)
- 四、底层驱动代码实现
-
- [4.1 工程文件结构](#4.1 工程文件结构)
- [4.2 新建文件:Core/Inc/ina226.h](#4.2 新建文件:Core/Inc/ina226.h)
- [4.3 新建文件:Core/Src/ina226.c](#4.3 新建文件:Core/Src/ina226.c)
- [4.4 新建文件:Core/Inc/oled.h](#4.4 新建文件:Core/Inc/oled.h)
- [4.5 新建文件:Core/Src/oled.c](#4.5 新建文件:Core/Src/oled.c)
- [4.6 新建文件:Core/Inc/key.h](#4.6 新建文件:Core/Inc/key.h)
- [4.7 新建文件:Core/Src/key.c](#4.7 新建文件:Core/Src/key.c)
- [4.8 新建文件:Core/Inc/charger.h](#4.8 新建文件:Core/Inc/charger.h)
- [4.9 新建文件:Core/Src/charger.c](#4.9 新建文件:Core/Src/charger.c)
- 五、主程序main.c修改
-
- [5.1 修改后的main.c](#5.1 修改后的main.c)
- 六、编译与下载
-
- [6.1 KEIL工程设置](#6.1 KEIL工程设置)
-
- [6.1.1 添加源文件](#6.1.1 添加源文件)
- [6.1.2 编译选项设置](#6.1.2 编译选项设置)
- [6.1.3 编译工程](#6.1.3 编译工程)
- [6.2 烧录程序](#6.2 烧录程序)
- 七、系统调试与测试
-
- [7.1 串口调试](#7.1 串口调试)
- [7.2 功能测试步骤](#7.2 功能测试步骤)
-
- [7.2.1 基本检测测试](#7.2.1 基本检测测试)
- [7.2.2 预充电测试](#7.2.2 预充电测试)
- [7.2.3 恒流充电测试](#7.2.3 恒流充电测试)
- [7.2.4 恒压充电测试](#7.2.4 恒压充电测试)
- [7.2.5 保护功能测试](#7.2.5 保护功能测试)
- [7.3 校准指南](#7.3 校准指南)
-
- [7.3.1 电压校准](#7.3.1 电压校准)
- [7.3.2 电流校准](#7.3.2 电流校准)
- [7.4 常见问题排查](#7.4 常见问题排查)
- 八、扩展与优化建议
-
- [8.1 功能扩展方向](#8.1 功能扩展方向)
- [8.2 性能优化建议](#8.2 性能优化建议)
- 九、总结
一、项目简介与系统设计
1.1 项目背景
本项目将手把手带你制作一个基于STM32F103C8T6的智能充电器。该充电器能够实时检测充电电压和电流,通过PWM控制充电功率,并在OLED屏幕上显示充电状态。系统具备过压保护、过流保护、反接保护等功能,适合作为嵌入式开发的入门实战项目。
1.2 系统整体架构
220V交流电源
开关电源模块
降压转换电路
STM32F103C8T6主控
INA226电压电流检测模块
PWM驱动电路
MOS管功率调节
电池负载
0.96寸OLED显示屏
按键输入模块
蜂鸣器报警模块
LED状态指示
1.3 硬件组成清单
| 序号 | 元器件名称 | 型号/规格 | 数量 | 用途说明 |
|---|---|---|---|---|
| 1 | 微控制器 | STM32F103C8T6最小系统板 | 1 | 主控芯片 |
| 2 | 电压电流检测 | INA226模块 | 1 | I2C接口高精度测量 |
| 3 | OLED显示屏 | 0.96寸 SSD1306 I2C | 1 | 显示充电信息 |
| 4 | MOS管驱动模块 | IRF520 MOS管模块 | 1 | PWM功率调节 |
| 5 | 降压模块 | LM2596可调降压模块 | 1 | 提供系统电源 |
| 6 | 按键开关 | 6x6mm轻触开关 | 3 | 设置/确认/切换 |
| 7 | 蜂鸣器 | 有源蜂鸣器模块 | 1 | 报警提示 |
| 8 | LED灯珠 | 5mm红色LED+220Ω电阻 | 3 | 状态指示 |
| 9 | 电阻 | 10KΩ | 5 | 上拉电阻 |
| 10 | 电容 | 100μF/25V电解电容 | 2 | 电源滤波 |
| 11 | 电容 | 100nF独石电容 | 3 | 去耦电容 |
| 12 | 肖特基二极管 | SS34 | 2 | 防反接保护 |
| 13 | 保险丝 | 自恢复保险丝500mA | 1 | 过流保护 |
| 14 | 电源适配器 | 12V/2A | 1 | 系统供电 |
| 15 | 接线端子 | 2P 5.08mm间距 | 4 | 输入输出接线 |
| 16 | 杜邦线 | 公母各一包 | 若干 | 电路连接 |
| 17 | 万用板 | 7x9cm双面 | 1 | 电路搭建 |
1.4 核心功能说明
- 电压检测功能:通过INA226芯片实时检测电池端电压,测量范围0-36V,精度可达1mV
- 电流检测功能:通过INA226内置16位ADC检测充电电流,采样电阻0.1Ω,精度可达0.1mA
- PWM控制功能:STM32输出PWM信号控制IRF520 MOS管,调节充电功率,频率设为20kHz
- 三段式充电:预充电(恒流小电流)→恒流充电→恒压充电,智能切换充电阶段
- 保护功能:过压保护(4.25V)、过流保护(1A)、反接保护、超时保护(10小时)
- 显示功能:OLED实时显示电压、电流、功率、充电进度、充电状态等信息
- 按键设置:支持设置充电截止电压和充电电流上限
1.5 系统工作流程
未连接
已连接
是
否
是
否
否
是
否
是
否
是
否
是
过压
过流
反接
系统上电初始化
初始化外设
GPIO/I2C/TIM/PWM/ADC
检测电池
是否连接?
OLED显示等待连接
指示灯闪烁
读取电池电压
电压是否
低于3.0V?
预充电模式
小电流100mA充电
PWM占空比5%
持续读取电压
电压是否
达到3.0V?
恒流充电模式
设定电流500mA
PID调节PWM占空比
实时读取电压电流
电压是否
达到4.2V?
温度是否
过高?
暂停充电
等待降温
恒压充电模式
保持电压4.2V
逐渐减小电流
读取充电电流
电流是否
小于50mA?
充电完成
切断输出
蜂鸣器提示
持续监测电压
电压是否
低于4.0V?
是否检测到
异常状态?
过压保护
立即切断输出
蜂鸣器长鸣
过流保护
立即切断输出
蜂鸣器长鸣
反接保护
MOS管不导通
OLED显示错误信息
等待手动复位
二、开发环境搭建
2.1 软件工具安装
2.1.1 KEIL MDK-ARM安装
前往KEIL官网下载MDK-ARM软件(版本推荐5.38及以上),安装过程选择默认选项即可。安装完成后需要安装STM32F1系列的芯片支持包。打开KEIL软件,点击菜单栏的Pack Installer图标,在Devices标签页中找到STMicroelectronics下的STM32F103C8,点击Install安装对应的DFP包。
2.1.2 STM32CubeMX安装
从ST官网下载STM32CubeMX(推荐6.9.0以上版本),该工具可以图形化配置外设并生成初始化代码。安装时选择默认路径,安装完成后打开软件,通过Help菜单下的Manage embedded software packages下载STM32F1系列HAL库包。
2.1.3 驱动安装
将ST-Link或USB转串口下载器连接到电脑,系统会自动识别并安装驱动。如果没有自动安装,需要下载CH340或CP2102驱动手动安装。安装完成后在设备管理器中可以查看到对应的串口号。
2.2 创建STM32CubeMX工程
2.2.1 新建工程
打开STM32CubeMX,点击New Project,在搜索框中输入STM32F103C8,选择STM32F103C8Tx,点击Start Project进入配置界面。
2.2.2 系统时钟配置
进入Pinout & Configuration标签页,选择System Core下的RCC,将HSE设置为Crystal/Ceramic Resonator(使用外部8MHz晶振)。然后进入Clock Configuration标签页,按照以下参数配置时钟树:
- 外部晶振HSE:8MHz
- PLL Source Mux:HSE
- PLL Mul:x9(8MHz×9=72MHz)
- System Clock Mux:PLLCLK
- APB1 Prescaler:/2(最大36MHz)
- APB2 Prescaler:/1(72MHz)
最终系统时钟SYSCLK=72MHz,APB1=36MHz,APB2=72MHz。
2.2.3 GPIO配置
在Pinout view中配置以下GPIO引脚:
- PC13:设置为GPIO_Output,标签命名为LED_STA,初始电平High(板上LED低电平点亮)
- PA0:设置为GPIO_Input,标签命名为KEY_SET,内部上拉Pull-up
- PA1:设置为GPIO_Input,标签命名为KEY_UP,内部上拉Pull-up
- PA2:设置为GPIO_Input,标签命名为KEY_DOWN,内部上拉Pull-up
- PB0:设置为GPIO_Output,标签命名为BUZZER,初始电平Low
- PB12:设置为GPIO_Output,标签命名为LED_RED,初始电平Low
- PB13:设置为GPIO_Output,标签命名为LED_GREEN,初始电平Low
- PB14:设置为GPIO_Output,标签命名为LED_BLUE,初始电平Low
2.2.4 定时器PWM配置
选择TIM2,在TIM2 Mode and Configuration中将Clock Source设为Internal Clock,Channel2设置为PWM Generation CH2。参数配置如下:
- Prescaler:72-1(将72MHz分频为1MHz)
- Counter Period:50-1(1MHz/50=20kHz PWM频率)
- Auto-reload preload:Enable
- Pulse:25(初始占空比50%)
- CH Polarity:High
TIM2_CH2对应的引脚为PA1,用于输出PWM信号控制MOS管。
2.2.5 I2C配置
选择I2C1,Mode选择I2C,配置参数:
- I2C Speed Mode:Fast Mode
- Clock Speed:400000 Hz
- 其余参数保持默认
I2C1_SCL引脚为PB6,I2C1_SDA引脚为PB7,用于连接INA226和OLED显示屏。注意这两个设备需要不同的I2C地址,INA226默认地址0x40,OLED默认地址0x3C。
2.2.6 串口配置
选择USART1,Mode选择Asynchronous,参数配置:
- Baud Rate:115200 Bits/s
- Word Length:8 Bits
- Parity:None
- Stop Bits:1
USART1_TX引脚为PA9,USART1_RX引脚为PA10,用于调试信息输出。
2.2.7 ADC配置(备用电压检测)
虽然本项目主要使用INA226进行电压电流检测,但为了展示ADC的使用,我们额外配置一个ADC通道用于检测输入电源电压。选择ADC1,在IN0通道(PA0引脚)上使能,参数配置:
- ADCs_Common_Settings:Independent mode
- Resolution:12 bits
- Data Alignment:Right alignment
- Sampling Time:239.5 Cycles
2.2.8 工程设置与代码生成
进入Project Manager标签页,进行以下设置:
- Project Name:SmartCharger_STM32F103
- Project Location:选择你的工作目录
- Application Structure:Basic
- Toolchain / IDE:MDK-ARM
- Min Version:V5.32
在Code Generator标签页中勾选以下选项:
- Copy only the necessary library files
- Generate peripheral initialization as a pair of '.c/.h' files per peripheral
- 取消勾选Generate backup previously generated files when re-generating
完成所有配置后,点击右上角的GENERATE CODE生成代码。在弹出的对话框中点击Open Project打开KEIL工程。
三、硬件电路设计详解
3.1 电源电路
系统输入使用12V/2A的直流电源适配器,经过LM2596降压模块将电压降至5V给STM32和OLED供电。LM2596模块输入端并联一个100μF/25V电解电容和100nF独石电容进行滤波,输出端同样并联一个100μF/16V电解电容和100nF独石电容。
充电回路从12V直接引出,经过自恢复保险丝500mA、SS34肖特基二极管(防反接)后连接到IRF520 MOS管的输入端。MOS管的输出端通过INA226模块的采样电阻连接到电池正极。在电池端并联一个100μF/25V电解电容起到稳压作用,同时并联一个10KΩ电阻作为假负载,防止空载时电压虚高。
3.2 INA226接线说明
INA226模块有6个引脚,接线如下:
- VCC → 3.3V(STM32板载3.3V)
- GND → GND
- SCL → PB6(I2C1_SCL)
- SDA → PB7(I2C1_SDA)
- Vin+ → 充电回路正极(电池端正极)
- Vin- → 电池正极(经过采样电阻)
注意:INA226的VCC使用3.3V供电,但Vin+和Vin-可以承受高达36V的共模电压,完全满足锂电池充电电压范围。模块上的采样电阻为0.1Ω,最大可测量3.2A电流,满足本项目需求。模块的I2C地址通过A0和A1引脚设置,默认悬空为0x40。
3.3 OLED接线说明
0.96寸OLED模块(SSD1306驱动)接线如下:
- VCC → 3.3V
- GND → GND
- SCL → PB6(I2C1_SCL)
- SDA → PB7(I2C1_SDA)
注意OLED的I2C地址为0x3C,与INA226的0x40不冲突,两个设备可以共用I2C1总线。
3.4 MOS管驱动电路
IRF520 MOS管模块有三个引脚:
- SIG → PA1(TIM2_CH2 PWM输出)注意这里不能直接用PA1,因为TIM2_CH2实际上是PA1,但前文已将PA1配置为按键,需要调整。我们改用PA0作为按键,使用TIM2_CH1(PA0)输出PWM,或者使用TIM2_CH2对应的引脚是PA1,我们保持按键在PB10等其他引脚。此处我们确认引脚分配:TIM2_CH2 → PA1,按键移到PB10、PB11、PB12(重新分配)。
修正后的引脚分配(重要调整):
- PA1:TIM2_CH2 PWM输出(接MOS管SIG)
- PB10:KEY_SET
- PB11:KEY_UP
- PB12:KEY_DOWN
- PB0:BUZZER
- PB13:LED_RED
- PB14:LED_GREEN
- PB15:LED_BLUE
- PC13:LED_STA(板上LED)
IRF520 MOS管模块接线:
- VCC → 12V电源正极
- GND → 12V电源负极(与STM32共地)
- SIG → PA1(PWM信号)
输入端子接12V电源和GND,输出端子经过INA226连接到电池。
3.5 按键电路
三个轻触开关一端接地,另一端分别接PB10、PB11、PB12,同时通过10KΩ电阻上拉到3.3V。未按下时为高电平,按下时为低电平。通过软件消抖处理。
3.6 蜂鸣器电路
有源蜂鸣器模块(低电平触发)接线:
- VCC → 3.3V
- GND → GND
- I/O → PB0
3.7 LED指示电路
三个5mm LED分别串联220Ω限流电阻:
- 红色LED正极 → PB13 → 限流电阻 → 3.3V(共阳极接法,低电平点亮)
- 绿色LED正极 → PB14 → 限流电阻 → 3.3V
- 蓝色LED正极 → PB15 → 限流电阻 → 3.3V
3.8 完整接线图
3.8.1 STM32引脚分配总表
| 引脚 | 功能 | 连接目标 |
|---|---|---|
| PA1 | TIM2_CH2 PWM | IRF520 SIG |
| PA9 | USART1_TX | 串口调试 |
| PA10 | USART1_RX | 串口调试 |
| PB0 | GPIO_Output | 蜂鸣器 |
| PB6 | I2C1_SCL | INA226 SCL + OLED SCL |
| PB7 | I2C1_SDA | INA226 SDA + OLED SDA |
| PB10 | GPIO_Input | KEY_SET |
| PB11 | GPIO_Input | KEY_UP |
| PB12 | GPIO_Input | KEY_DOWN |
| PB13 | GPIO_Output | LED_RED |
| PB14 | GPIO_Output | LED_GREEN |
| PB15 | GPIO_Output | LED_BLUE |
| PC13 | GPIO_Output | 板载LED |
| 3.3V | 电源输出 | INA226 VCC + OLED VCC |
| GND | 电源地 | 所有模块GND共地 |
四、底层驱动代码实现
4.1 工程文件结构
在KEIL工程中创建以下文件组织结构:
SmartCharger_STM32F103/
├── Core/
│ ├── Inc/
│ │ ├── main.h
│ │ ├── gpio.h
│ │ ├── i2c.h
│ │ ├── tim.h
│ │ ├── usart.h
│ │ ├── adc.h
│ │ ├── ina226.h # 新增
│ │ ├── oled.h # 新增
│ │ ├── charger.h # 新增
│ │ └── key.h # 新增
│ └── Src/
│ ├── main.c
│ ├── gpio.c
│ ├── i2c.c
│ ├── tim.c
│ ├── usart.c
│ ├── adc.c
│ ├── ina226.c # 新增
│ ├── oled.c # 新增
│ ├── charger.c # 新增
│ └── key.c # 新增
4.2 新建文件:Core/Inc/ina226.h
此文件为INA226电压电流检测模块的头文件,定义了I2C地址、寄存器地址、校准参数以及功能函数声明。
c
/**
* @file ina226.h
* @brief INA226电压电流检测模块驱动头文件
* @author SmartCharger Project
* @date 2025
*/
#ifndef __INA226_H
#define __INA226_H
#ifdef __cplusplus
extern "C" {
#endif
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "i2c.h"
/* INA226 I2C地址定义 --------------------------------------------------------*/
/* A0和A1均接GND时地址为0x40,具体需根据模块实际连接修改 */
#define INA226_ADDR_WRITE 0x80 /* 0x40左移1位 + 写位0 */
#define INA226_ADDR_READ 0x81 /* 0x40左移1位 + 读位1 */
#define INA226_I2C_TIMEOUT 100 /* I2C通信超时时间(ms) */
/* INA226寄存器地址定义 ------------------------------------------------------*/
#define INA226_REG_CONFIG 0x00 /* 配置寄存器 */
#define INA226_REG_SHUNT_VOLT 0x01 /* 分流电压寄存器 */
#define INA226_REG_BUS_VOLT 0x02 /* 总线电压寄存器 */
#define INA226_REG_POWER 0x03 /* 功率寄存器 */
#define INA226_REG_CURRENT 0x04 /* 电流寄存器 */
#define INA226_REG_CALIB 0x05 /* 校准寄存器 */
#define INA226_REG_MASK_EN 0x06 /* 屏蔽/使能寄存器 */
#define INA226_REG_ALERT_LIMIT 0x07 /* 报警限值寄存器 */
#define INA226_REG_MANUFACT_ID 0xFE /* 制造商ID寄存器 */
#define INA226_REG_DIE_ID 0xFF /* 芯片ID寄存器 */
/* INA226配置寄存器位定义 ----------------------------------------------------*/
/* 工作模式 */
#define INA226_MODE_POWERDOWN 0x0000 /* 掉电模式 */
#define INA226_MODE_SHUNT_TRIG 0x0001 /* 分流电压触发测量 */
#define INA226_MODE_BUS_TRIG 0x0002 /* 总线电压触发测量 */
#define INA226_MODE_SHUNT_BUS_TRIG 0x0003 /* 分流和总线电压触发测量 */
#define INA226_MODE_SHUNT_CONT 0x0005 /* 分流电压连续测量 */
#define INA226_MODE_BUS_CONT 0x0006 /* 总线电压连续测量 */
#define INA226_MODE_SHUNT_BUS_CONT 0x0007 /* 分流和总线电压连续测量(常用) */
/* 电压转换时间设置 */
#define INA226_CT_140us 0x0000 /* 转换时间140us */
#define INA226_CT_204us 0x0001 /* 转换时间204us */
#define INA226_CT_332us 0x0002 /* 转换时间332us */
#define INA226_CT_588us 0x0003 /* 转换时间588us */
#define INA226_CT_1100us 0x0004 /* 转换时间1.1ms(默认) */
#define INA226_CT_2116us 0x0005 /* 转换时间2.116ms */
#define INA226_CT_4156us 0x0006 /* 转换时间4.156ms */
#define INA226_CT_8244us 0x0007 /* 转换时间8.244ms */
/* 采样平均次数 */
#define INA226_AVG_1 0x0000 /* 1次平均 */
#define INA226_AVG_4 0x0001 /* 4次平均 */
#define INA226_AVG_16 0x0002 /* 16次平均 */
#define INA226_AVG_64 0x0003 /* 64次平均 */
#define INA226_AVG_128 0x0004 /* 128次平均 */
#define INA226_AVG_256 0x0005 /* 256次平均 */
#define INA226_AVG_512 0x0006 /* 512次平均 */
#define INA226_AVG_1024 0x0007 /* 1024次平均(默认) */
/* INA226校准参数 ------------------------------------------------------------*/
#define INA226_SHUNT_RESISTANCE 0.1f /* 采样电阻阻值,单位:欧姆 */
#define INA226_MAX_CURRENT 3.2f /* 最大期望电流,单位:安培 */
#define INA226_CURRENT_LSB 0.0001f /* 电流LSB值,单位:安培/bit */
#define INA226_CALIB_VALUE 0x0A00 /* 校准值(根据公式计算) */
/* INA226数据结构体 ----------------------------------------------------------*/
typedef struct {
float shunt_voltage; /* 分流电压,单位:mV */
float bus_voltage; /* 总线电压,单位:V */
float current; /* 电流值,单位:A */
float power; /* 功率值,单位:W */
uint16_t config_reg; /* 配置寄存器值 */
uint16_t calib_reg; /* 校准寄存器值 */
} INA226_DataTypeDef;
/* 函数声明 ------------------------------------------------------------------*/
uint8_t INA226_Init(void);
uint8_t INA226_Reset(void);
uint8_t INA226_WriteRegister(uint8_t reg_addr, uint16_t reg_value);
uint8_t INA226_ReadRegister(uint8_t reg_addr, uint16_t *reg_value);
uint8_t INA226_ReadData(INA226_DataTypeDef *ina_data);
float INA226_GetBusVoltage(void);
float INA226_GetShuntVoltage(void);
float INA226_GetCurrent(void);
float INA226_GetPower(void);
uint8_t INA226_CheckDevice(void);
void INA226_SetConfig(uint16_t config_value);
uint16_t INA226_GetConfig(void);
#ifdef __cplusplus
}
#endif
#endif /* __INA226_H */
4.3 新建文件:Core/Src/ina226.c
此文件为INA226电压电流检测模块的驱动实现,包含寄存器读写、校准配置和数据读取等功能。
c
/**
* @file ina226.c
* @brief INA226电压电流检测模块驱动实现
* @author SmartCharger Project
* @date 2025
*/
/* Includes ------------------------------------------------------------------*/
#include "ina226.h"
#include <stdio.h>
#include <math.h>
/* 私有变量 ------------------------------------------------------------------*/
static float shunt_voltage_lsb = 2.5f; /* 分流电压LSB,单位:uV */
static float bus_voltage_lsb = 1.25f; /* 总线电压LSB,单位:mV */
static float current_lsb = 0.0001f; /* 电流LSB,单位:A */
/* 内部函数声明 --------------------------------------------------------------*/
static uint16_t INA226_CalculateCalibValue(float r_shunt, float max_current);
/**
* @brief 计算INA226校准寄存器的值
* @param r_shunt: 采样电阻阻值,单位:欧姆
* @param max_current: 期望测量的最大电流,单位:安培
* @retval 校准寄存器的16位值
* @note 计算公式:CAL = 0.00512 / (Current_LSB * R_SHUNT)
* 其中Current_LSB = Max_Current / 2^15
*/
static uint16_t INA226_CalculateCalibValue(float r_shunt, float max_current)
{
float current_lsb_value;
float calib_value;
/* 计算电流LSB,使用15位分辨率 */
current_lsb_value = max_current / 32768.0f;
/* 根据公式计算校准值 */
calib_value = 0.00512f / (current_lsb_value * r_shunt);
/* 更新全局电流LSB */
current_lsb = current_lsb_value;
/* 返回整数值,确保在0-65535范围内 */
if (calib_value > 65535.0f) {
return 65535;
}
if (calib_value < 1.0f) {
return 1;
}
return (uint16_t)calib_value;
}
/**
* @brief INA226模块初始化
* @retval 0: 初始化成功
* 1: 初始化失败
*/
uint8_t INA226_Init(void)
{
uint8_t retry_count = 0;
uint16_t manuf_id = 0;
uint16_t config_value = 0;
uint16_t calib_value = 0;
uint8_t status = 0;
/* 短暂延时等待模块上电稳定 */
HAL_Delay(10);
/* 检查设备是否存在,读取制造商ID */
while (retry_count < 5) {
status = INA226_ReadRegister(INA226_REG_MANUFACT_ID, &manuf_id);
if (status == 0 && manuf_id == 0x5449) {
/* 制造商ID正确,TI的ID为0x5449 */
break;
}
retry_count++;
HAL_Delay(10);
}
if (retry_count >= 5) {
/* 设备未找到或通信失败 */
return 1;
}
/* 软件复位INA226 */
status = INA226_Reset();
if (status != 0) {
return 1;
}
/* 等待复位完成 */
HAL_Delay(5);
/* 计算校准寄存器值 */
calib_value = INA226_CalculateCalibValue(INA226_SHUNT_RESISTANCE,
INA226_MAX_CURRENT);
/* 写入校准寄存器 */
status = INA226_WriteRegister(INA226_REG_CALIB, calib_value);
if (status != 0) {
return 1;
}
/* 验证校准寄存器写入 */
status = INA226_ReadRegister(INA226_REG_CALIB, &calib_value);
if (status != 0) {
return 1;
}
/* 配置INA226工作模式 */
/* 配置:分流和总线电压连续测量 | 转换时间1.1ms | 采样平均64次 */
config_value = INA226_MODE_SHUNT_BUS_CONT | /* 连续测量模式 */
INA226_CT_1100us | /* 总线转换时间1.1ms */
(INA226_CT_1100us << 3) | /* 分流转换时间1.1ms */
INA226_AVG_64; /* 64次采样平均 */
/* 写入配置寄存器 */
status = INA226_WriteRegister(INA226_REG_CONFIG, config_value);
if (status != 0) {
return 1;
}
/* 验证配置寄存器写入 */
status = INA226_ReadRegister(INA226_REG_CONFIG, &config_value);
if (status != 0) {
return 1;
}
/* 初始化完成 */
return 0;
}
/**
* @brief 软件复位INA226
* @retval 0: 成功
* 1: 失败
*/
uint8_t INA226_Reset(void)
{
uint8_t status = 0;
uint16_t reset_value = 0x8000; /* 复位位,写入后自动清除 */
/* 向配置寄存器写入复位命令 */
status = INA226_WriteRegister(INA226_REG_CONFIG, reset_value);
/* 等待复位完成 */
HAL_Delay(2);
return status;
}
/**
* @brief 向INA226寄存器写入16位数据
* @param reg_addr: 寄存器地址
* @param reg_value: 要写入的16位数据
* @retval 0: 写入成功
* 1: 写入失败
*/
uint8_t INA226_WriteRegister(uint8_t reg_addr, uint16_t reg_value)
{
HAL_StatusTypeDef hal_status;
uint8_t tx_data[3];
/* 构造发送数据:寄存器地址 + 高8位 + 低8位 */
tx_data[0] = reg_addr;
tx_data[1] = (uint8_t)(reg_value >> 8); /* 高字节在前 */
tx_data[2] = (uint8_t)(reg_value & 0xFF); /* 低字节在后 */
/* 通过I2C发送数据 */
hal_status = HAL_I2C_Master_Transmit(&hi2c1,
INA226_ADDR_WRITE,
tx_data,
3,
INA226_I2C_TIMEOUT);
if (hal_status != HAL_OK) {
return 1;
}
return 0;
}
/**
* @brief 从INA226寄存器读取16位数据
* @param reg_addr: 寄存器地址
* @param reg_value: 读取到的16位数据指针
* @retval 0: 读取成功
* 1: 读取失败
*/
uint8_t INA226_ReadRegister(uint8_t reg_addr, uint16_t *reg_value)
{
HAL_StatusTypeDef hal_status;
uint8_t rx_data[2] = {0, 0};
/* 先发送要读取的寄存器地址 */
hal_status = HAL_I2C_Master_Transmit(&hi2c1,
INA226_ADDR_WRITE,
®_addr,
1,
INA226_I2C_TIMEOUT);
if (hal_status != HAL_OK) {
return 1;
}
/* 再读取两个字节的数据 */
hal_status = HAL_I2C_Master_Receive(&hi2c1,
INA226_ADDR_READ,
rx_data,
2,
INA226_I2C_TIMEOUT);
if (hal_status != HAL_OK) {
return 1;
}
/* 组合为16位数据,高字节在前 */
*reg_value = ((uint16_t)rx_data[0] << 8) | (uint16_t)rx_data[1];
return 0;
}
/**
* @brief 读取INA226完整测量数据
* @param ina_data: 数据存储结构体指针
* @retval 0: 读取成功
* 1: 读取失败
*/
uint8_t INA226_ReadData(INA226_DataTypeDef *ina_data)
{
uint16_t raw_value = 0;
int16_t signed_value = 0;
uint8_t status = 0;
if (ina_data == NULL) {
return 1;
}
/* 读取分流电压寄存器 */
status = INA226_ReadRegister(INA226_REG_SHUNT_VOLT, &raw_value);
if (status != 0) {
return 1;
}
/* 分流电压是有符号数,转换为mV */
signed_value = (int16_t)raw_value;
ina_data->shunt_voltage = (float)signed_value * 0.0025f; /* LSB = 2.5uV = 0.0025mV */
/* 读取总线电压寄存器 */
status = INA226_ReadRegister(INA226_REG_BUS_VOLT, &raw_value);
if (status != 0) {
return 1;
}
/* 总线电压,LSB = 1.25mV,转换为V */
ina_data->bus_voltage = (float)raw_value * 0.00125f; /* LSB = 1.25mV = 0.00125V */
/* 读取电流寄存器 */
status = INA226_ReadRegister(INA226_REG_CURRENT, &raw_value);
if (status != 0) {
return 1;
}
/* 电流是有符号数,转换为A */
signed_value = (int16_t)raw_value;
ina_data->current = (float)signed_value * current_lsb;
/* 读取功率寄存器 */
status = INA226_ReadRegister(INA226_REG_POWER, &raw_value);
if (status != 0) {
return 1;
}
/* 功率,LSB = 25 * Current_LSB,转换为W */
ina_data->power = (float)raw_value * current_lsb * 25.0f;
return 0;
}
/**
* @brief 获取总线电压值
* @retval 总线电压值,单位:V
*/
float INA226_GetBusVoltage(void)
{
uint16_t raw_value = 0;
float voltage = 0.0f;
if (INA226_ReadRegister(INA226_REG_BUS_VOLT, &raw_value) == 0) {
voltage = (float)raw_value * 0.00125f;
}
return voltage;
}
/**
* @brief 获取分流电压值
* @retval 分流电压值,单位:mV
*/
float INA226_GetShuntVoltage(void)
{
uint16_t raw_value = 0;
int16_t signed_value = 0;
float voltage = 0.0f;
if (INA226_ReadRegister(INA226_REG_SHUNT_VOLT, &raw_value) == 0) {
signed_value = (int16_t)raw_value;
voltage = (float)signed_value * 0.0025f;
}
return voltage;
}
/**
* @brief 获取电流值
* @retval 电流值,单位:A
*/
float INA226_GetCurrent(void)
{
uint16_t raw_value = 0;
int16_t signed_value = 0;
float current = 0.0f;
if (INA226_ReadRegister(INA226_REG_CURRENT, &raw_value) == 0) {
signed_value = (int16_t)raw_value;
current = (float)signed_value * current_lsb;
}
return current;
}
/**
* @brief 获取功率值
* @retval 功率值,单位:W
*/
float INA226_GetPower(void)
{
uint16_t raw_value = 0;
float power = 0.0f;
if (INA226_ReadRegister(INA226_REG_POWER, &raw_value) == 0) {
power = (float)raw_value * current_lsb * 25.0f;
}
return power;
}
/**
* @brief 检查INA226设备是否存在
* @retval 0: 设备存在
* 1: 设备不存在
*/
uint8_t INA226_CheckDevice(void)
{
uint16_t manuf_id = 0;
uint8_t status = 0;
/* 尝试读取制造商ID寄存器 */
status = INA226_ReadRegister(INA226_REG_MANUFACT_ID, &manuf_id);
if (status == 0 && manuf_id == 0x5449) {
return 0; /* 设备存在 */
}
return 1; /* 设备不存在 */
}
/**
* @brief 设置INA226配置寄存器
* @param config_value: 配置值
*/
void INA226_SetConfig(uint16_t config_value)
{
INA226_WriteRegister(INA226_REG_CONFIG, config_value);
}
/**
* @brief 获取INA226配置寄存器值
* @retval 配置寄存器值
*/
uint16_t INA226_GetConfig(void)
{
uint16_t config_value = 0;
INA226_ReadRegister(INA226_REG_CONFIG, &config_value);
return config_value;
}
4.4 新建文件:Core/Inc/oled.h
此文件为OLED显示屏驱动的头文件,基于SSD1306驱动芯片,使用I2C通信。
c
/**
* @file oled.h
* @brief OLED SSD1306 I2C显示驱动头文件
* @author SmartCharger Project
* @date 2025
*/
#ifndef __OLED_H
#define __OLED_H
#ifdef __cplusplus
extern "C" {
#endif
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "i2c.h"
#include <string.h>
#include <stdio.h>
/* OLED参数宏定义 ------------------------------------------------------------*/
#define OLED_I2C_ADDR 0x78 /* 0x3C左移1位后的地址 */
#define OLED_I2C_TIMEOUT 100 /* I2C超时时间 */
#define OLED_WIDTH 128 /* OLED宽度(像素) */
#define OLED_HEIGHT 64 /* OLED高度(像素) */
#define OLED_PAGES 8 /* OLED页数(每页8像素高度) */
/* OLED控制命令定义 ----------------------------------------------------------*/
#define OLED_CMD_DISPLAY_OFF 0xAE /* 关闭显示 */
#define OLED_CMD_DISPLAY_ON 0xAF /* 开启显示 */
#define OLED_CMD_SET_CONTRAST 0x81 /* 设置对比度 */
#define OLED_CMD_DISPLAY_ALL_ON 0xA5 /* 全屏点亮 */
#define OLED_CMD_DISPLAY_NORMAL 0xA6 /* 正常显示 */
#define OLED_CMD_DISPLAY_INVERT 0xA7 /* 反相显示 */
#define OLED_CMD_SET_MUX_RATIO 0xA8 /* 设置多路复用比 */
#define OLED_CMD_SET_DISPLAY_OFFSET 0xD3 /* 设置显示偏移 */
#define OLED_CMD_SET_DISPLAY_START 0x40 /* 设置显示起始行 */
#define OLED_CMD_SET_SEGMENT_REMAP 0xA1 /* 段重映射(左右翻转) */
#define OLED_CMD_SET_COM_SCAN_DIR 0xC8 /* COM扫描方向(上下翻转) */
#define OLED_CMD_SET_COM_PINS 0xDA /* 设置COM引脚配置 */
#define OLED_CMD_SET_CLOCK_DIV 0xD5 /* 设置显示时钟分频 */
#define OLED_CMD_SET_PRECHARGE 0xD9 /* 设置预充电周期 */
#define OLED_CMD_SET_VCOM_DESELECT 0xDB /* 设置VCOMH电压 */
#define OLED_CMD_SET_CHARGE_PUMP 0x8D /* 设置电荷泵 */
#define OLED_CMD_SET_MEMORY_MODE 0x20 /* 设置内存寻址模式 */
/* 全局变量声明 --------------------------------------------------------------*/
extern const uint8_t F6x8[][6]; /* 6x8点阵ASCII字库 */
extern const uint8_t F8X16[]; /* 8x16点阵ASCII字库(此项目未使用完整字库) */
/* 函数声明 ------------------------------------------------------------------*/
void OLED_Init(void);
void OLED_Clear(void);
void OLED_ClearBuffer(void);
void OLED_Refresh(void);
void OLED_DisplayOn(void);
void OLED_DisplayOff(void);
void OLED_SetCursor(uint8_t x, uint8_t y);
void OLED_ShowChar(uint8_t x, uint8_t y, char ch, uint8_t font_size);
void OLED_ShowString(uint8_t x, uint8_t y, const char *str, uint8_t font_size);
void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t font_size);
void OLED_ShowFloat(uint8_t x, uint8_t y, float num, uint8_t int_len,
uint8_t dec_len, uint8_t font_size);
void OLED_DrawPoint(uint8_t x, uint8_t y, uint8_t color);
void OLED_DrawLine(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t color);
void OLED_DrawRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t color);
void OLED_FillRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t color);
void OLED_ShowBattery(uint8_t x, uint8_t y, uint8_t percent);
void OLED_Printf(uint8_t x, uint8_t y, uint8_t font_size, const char *fmt, ...);
#ifdef __cplusplus
}
#endif
#endif /* __OLED_H */
4.5 新建文件:Core/Src/oled.c
此文件为OLED显示屏驱动的实现,包含初始化、基本绘图和字符显示功能。
c
/**
* @file oled.c
* @brief OLED SSD1306 I2C显示驱动实现
* @author SmartCharger Project
* @date 2025
*/
/* Includes ------------------------------------------------------------------*/
#include "oled.h"
#include <stdarg.h>
/* 全局变量 ------------------------------------------------------------------*/
/* OLED显存缓冲区,128*64/8 = 1024字节 */
static uint8_t OLED_Buffer[OLED_WIDTH * OLED_HEIGHT / 8];
/* 6x8点阵ASCII字符字库(部分常用字符) */
/* 每个字符占用6字节,每字节对应一列8个像素(从上到下) */
const uint8_t F6x8[][6] = {
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 空格 (32)
{0x00, 0x00, 0x5F, 0x00, 0x00, 0x00}, // !
{0x00, 0x07, 0x00, 0x07, 0x00, 0x00}, // "
{0x14, 0x7F, 0x14, 0x7F, 0x14, 0x00}, // #
{0x24, 0x2A, 0x7F, 0x2A, 0x12, 0x00}, // $
{0x23, 0x13, 0x08, 0x64, 0x62, 0x00}, // %
{0x36, 0x49, 0x55, 0x22, 0x50, 0x00}, // &
{0x00, 0x05, 0x03, 0x00, 0x00, 0x00}, // '
{0x00, 0x1C, 0x22, 0x41, 0x00, 0x00}, // (
{0x00, 0x41, 0x22, 0x1C, 0x00, 0x00}, // )
{0x08, 0x2A, 0x1C, 0x2A, 0x08, 0x00}, // *
{0x08, 0x08, 0x3E, 0x08, 0x08, 0x00}, // +
{0x00, 0x50, 0x30, 0x00, 0x00, 0x00}, // ,
{0x08, 0x08, 0x08, 0x08, 0x08, 0x00}, // -
{0x00, 0x60, 0x60, 0x00, 0x00, 0x00}, // .
{0x20, 0x10, 0x08, 0x04, 0x02, 0x00}, // /
{0x3E, 0x51, 0x49, 0x45, 0x3E, 0x00}, // 0
{0x00, 0x42, 0x7F, 0x40, 0x00, 0x00}, // 1
{0x42, 0x61, 0x51, 0x49, 0x46, 0x00}, // 2
{0x21, 0x41, 0x45, 0x4B, 0x31, 0x00}, // 3
{0x18, 0x14, 0x12, 0x7F, 0x10, 0x00}, // 4
{0x27, 0x45, 0x45, 0x45, 0x39, 0x00}, // 5
{0x3C, 0x4A, 0x49, 0x49, 0x30, 0x00}, // 6
{0x01, 0x71, 0x09, 0x05, 0x03, 0x00}, // 7
{0x36, 0x49, 0x49, 0x49, 0x36, 0x00}, // 8
{0x06, 0x49, 0x49, 0x29, 0x1E, 0x00}, // 9
{0x00, 0x36, 0x36, 0x00, 0x00, 0x00}, // :
{0x00, 0x56, 0x36, 0x00, 0x00, 0x00}, // ;
{0x00, 0x08, 0x14, 0x22, 0x41, 0x00}, // <
{0x14, 0x14, 0x14, 0x14, 0x14, 0x00}, // =
{0x41, 0x22, 0x14, 0x08, 0x00, 0x00}, // >
{0x02, 0x01, 0x51, 0x09, 0x06, 0x00}, // ?
{0x32, 0x49, 0x79, 0x41, 0x3E, 0x00}, // @
{0x7E, 0x11, 0x11, 0x11, 0x7E, 0x00}, // A
{0x7F, 0x49, 0x49, 0x49, 0x36, 0x00}, // B
{0x3E, 0x41, 0x41, 0x41, 0x22, 0x00}, // C
{0x7F, 0x41, 0x41, 0x22, 0x1C, 0x00}, // D
{0x7F, 0x49, 0x49, 0x49, 0x41, 0x00}, // E
{0x7F, 0x09, 0x09, 0x01, 0x01, 0x00}, // F
{0x3E, 0x41, 0x41, 0x51, 0x32, 0x00}, // G
{0x7F, 0x08, 0x08, 0x08, 0x7F, 0x00}, // H
{0x00, 0x41, 0x7F, 0x41, 0x00, 0x00}, // I
{0x20, 0x40, 0x41, 0x3F, 0x01, 0x00}, // J
{0x7F, 0x08, 0x14, 0x22, 0x41, 0x00}, // K
{0x7F, 0x40, 0x40, 0x40, 0x40, 0x00}, // L
{0x7F, 0x02, 0x04, 0x02, 0x7F, 0x00}, // M
{0x7F, 0x04, 0x08, 0x10, 0x7F, 0x00}, // N
{0x3E, 0x41, 0x41, 0x41, 0x3E, 0x00}, // O
{0x7F, 0x09, 0x09, 0x09, 0x06, 0x00}, // P
{0x3E, 0x41, 0x51, 0x21, 0x5E, 0x00}, // Q
{0x7F, 0x09, 0x19, 0x29, 0x46, 0x00}, // R
{0x46, 0x49, 0x49, 0x49, 0x31, 0x00}, // S
{0x01, 0x01, 0x7F, 0x01, 0x01, 0x00}, // T
{0x3F, 0x40, 0x40, 0x40, 0x3F, 0x00}, // U
{0x1F, 0x20, 0x40, 0x20, 0x1F, 0x00}, // V
{0x7F, 0x20, 0x18, 0x20, 0x7F, 0x00}, // W
{0x63, 0x14, 0x08, 0x14, 0x63, 0x00}, // X
{0x03, 0x04, 0x78, 0x04, 0x03, 0x00}, // Y
{0x61, 0x51, 0x49, 0x45, 0x43, 0x00}, // Z
{0x00, 0x00, 0x7F, 0x41, 0x41, 0x00}, // [
{0x02, 0x04, 0x08, 0x10, 0x20, 0x00}, // 反斜杠
{0x41, 0x41, 0x7F, 0x00, 0x00, 0x00}, // ]
{0x04, 0x02, 0x01, 0x02, 0x04, 0x00}, // ^
{0x40, 0x40, 0x40, 0x40, 0x40, 0x00}, // _
{0x00, 0x01, 0x02, 0x04, 0x00, 0x00}, // `
{0x20, 0x54, 0x54, 0x54, 0x78, 0x00}, // a
{0x7F, 0x48, 0x44, 0x44, 0x38, 0x00}, // b
{0x38, 0x44, 0x44, 0x44, 0x20, 0x00}, // c
{0x38, 0x44, 0x44, 0x48, 0x7F, 0x00}, // d
{0x38, 0x54, 0x54, 0x54, 0x18, 0x00}, // e
{0x08, 0x7E, 0x09, 0x01, 0x02, 0x00}, // f
{0x08, 0x14, 0x54, 0x54, 0x3C, 0x00}, // g
{0x7F, 0x08, 0x04, 0x04, 0x78, 0x00}, // h
{0x00, 0x44, 0x7D, 0x40, 0x00, 0x00}, // i
{0x20, 0x40, 0x44, 0x3D, 0x00, 0x00}, // j
{0x00, 0x7F, 0x10, 0x28, 0x44, 0x00}, // k
{0x00, 0x41, 0x7F, 0x40, 0x00, 0x00}, // l
{0x7C, 0x04, 0x18, 0x04, 0x78, 0x00}, // m
{0x7C, 0x08, 0x04, 0x04, 0x78, 0x00}, // n
{0x38, 0x44, 0x44, 0x44, 0x38, 0x00}, // o
{0x7C, 0x14, 0x14, 0x14, 0x08, 0x00}, // p
{0x08, 0x14, 0x14, 0x18, 0x7C, 0x00}, // q
{0x7C, 0x08, 0x04, 0x04, 0x08, 0x00}, // r
{0x48, 0x54, 0x54, 0x54, 0x20, 0x00}, // s
{0x04, 0x3F, 0x44, 0x40, 0x20, 0x00}, // t
{0x3C, 0x40, 0x40, 0x20, 0x7C, 0x00}, // u
{0x1C, 0x20, 0x40, 0x20, 0x1C, 0x00}, // v
{0x3C, 0x40, 0x30, 0x40, 0x3C, 0x00}, // w
{0x44, 0x28, 0x10, 0x28, 0x44, 0x00}, // x
{0x0C, 0x50, 0x50, 0x50, 0x3C, 0x00}, // y
{0x44, 0x64, 0x54, 0x4C, 0x44, 0x00}, // z
};
/**
* @brief 向OLED发送命令
* @param cmd: 命令字节
*/
static void OLED_WriteCmd(uint8_t cmd)
{
uint8_t tx_data[2];
tx_data[0] = 0x00; /* 控制字节:0x00表示接下来是命令 */
tx_data[1] = cmd;
HAL_I2C_Master_Transmit(&hi2c1, OLED_I2C_ADDR, tx_data, 2, OLED_I2C_TIMEOUT);
}
/**
* @brief 向OLED发送数据
* @param data: 数据字节
*/
static void OLED_WriteData(uint8_t data)
{
uint8_t tx_data[2];
tx_data[0] = 0x40; /* 控制字节:0x40表示接下来是数据 */
tx_data[1] = data;
HAL_I2C_Master_Transmit(&hi2c1, OLED_I2C_ADDR, tx_data, 2, OLED_I2C_TIMEOUT);
}
/**
* @brief 向OLED批量发送数据
* @param data: 数据缓冲区指针
* @param len: 数据长度
*/
static void OLED_WriteMultiData(uint8_t *data, uint16_t len)
{
/* 注意:HAL库单次I2C传输最大255字节,需要分批发送 */
uint8_t tx_buffer[129]; /* 1字节控制 + 最多128字节数据 */
uint16_t remaining = len;
uint16_t offset = 0;
uint8_t chunk_size;
while (remaining > 0) {
chunk_size = (remaining > 128) ? 128 : remaining;
tx_buffer[0] = 0x40; /* 控制字节:数据模式 */
memcpy(&tx_buffer[1], &data[offset], chunk_size);
HAL_I2C_Master_Transmit(&hi2c1, OLED_I2C_ADDR,
tx_buffer, chunk_size + 1, OLED_I2C_TIMEOUT);
offset += chunk_size;
remaining -= chunk_size;
}
}
/**
* @brief OLED初始化
*/
void OLED_Init(void)
{
/* 等待OLED上电稳定 */
HAL_Delay(100);
/* SSD1306初始化命令序列 */
OLED_WriteCmd(OLED_CMD_DISPLAY_OFF); /* 关闭显示 */
OLED_WriteCmd(OLED_CMD_SET_CLOCK_DIV); /* 设置时钟分频 */
OLED_WriteCmd(0x80); /* 分频因子=1,振荡频率=默认 */
OLED_WriteCmd(OLED_CMD_SET_MUX_RATIO); /* 设置多路复用比 */
OLED_WriteCmd(0x3F); /* 复用比=64(64行) */
OLED_WriteCmd(OLED_CMD_SET_DISPLAY_OFFSET); /* 设置显示偏移 */
OLED_WriteCmd(0x00); /* 偏移=0 */
OLED_WriteCmd(OLED_CMD_SET_DISPLAY_START); /* 设置显示起始行(0x40) */
OLED_WriteCmd(OLED_CMD_SET_SEGMENT_REMAP); /* 段重映射(左右镜像) */
OLED_WriteCmd(OLED_CMD_SET_COM_SCAN_DIR); /* COM扫描方向(上下镜像) */
OLED_WriteCmd(OLED_CMD_SET_COM_PINS); /* 设置COM引脚配置 */
OLED_WriteCmd(0x12); /* 替代COM引脚配置 */
OLED_WriteCmd(OLED_CMD_SET_CONTRAST); /* 设置对比度 */
OLED_WriteCmd(0x7F); /* 对比度值(0-255) */
OLED_WriteCmd(OLED_CMD_DISPLAY_ALL_ON); /* 全屏点亮(用于测试) */
OLED_WriteCmd(OLED_CMD_DISPLAY_NORMAL); /* 恢复正常显示 */
OLED_WriteCmd(OLED_CMD_SET_MEMORY_MODE); /* 设置内存寻址模式 */
OLED_WriteCmd(0x00); /* 水平寻址模式 */
OLED_WriteCmd(0x8D); /* 设置电荷泵 */
OLED_WriteCmd(0x14); /* 使能电荷泵(7.5V) */
OLED_WriteCmd(OLED_CMD_SET_PRECHARGE); /* 设置预充电周期 */
OLED_WriteCmd(0xF1); /* Phase1=15, Phase2=1 */
OLED_WriteCmd(OLED_CMD_SET_VCOM_DESELECT); /* 设置VCOMH电压 */
OLED_WriteCmd(0x40); /* VCOMH = ~0.77*VCC */
OLED_WriteCmd(OLED_CMD_DISPLAY_ON); /* 开启显示 */
/* 清空显存和屏幕 */
OLED_Clear();
}
/**
* @brief 清空OLED屏幕
*/
void OLED_Clear(void)
{
memset(OLED_Buffer, 0x00, sizeof(OLED_Buffer));
OLED_Refresh();
}
/**
* @brief 清空显存缓冲区(不更新屏幕)
*/
void OLED_ClearBuffer(void)
{
memset(OLED_Buffer, 0x00, sizeof(OLED_Buffer));
}
/**
* @brief 刷新屏幕(将缓冲区内容写入OLED)
*/
void OLED_Refresh(void)
{
/* 设置写入范围:全屏 */
OLED_WriteCmd(0x21); /* 设置列地址范围 */
OLED_WriteCmd(0); /* 起始列 = 0 */
OLED_WriteCmd(127); /* 结束列 = 127 */
OLED_WriteCmd(0x22); /* 设置页地址范围 */
OLED_WriteCmd(0); /* 起始页 = 0 */
OLED_WriteCmd(7); /* 结束页 = 7 */
/* 写入显存数据 */
OLED_WriteMultiData(OLED_Buffer, sizeof(OLED_Buffer));
}
/**
* @brief 开启OLED显示
*/
void OLED_DisplayOn(void)
{
OLED_WriteCmd(OLED_CMD_DISPLAY_ON);
}
/**
* @brief 关闭OLED显示
*/
void OLED_DisplayOff(void)
{
OLED_WriteCmd(OLED_CMD_DISPLAY_OFF);
}
/**
* @brief 设置显示光标位置
* @param x: 列坐标(0-127)
* @param y: 页坐标(0-7),对应行坐标0-63,每页8行
*/
void OLED_SetCursor(uint8_t x, uint8_t y)
{
if (x > 127) x = 127;
if (y > 7) y = 7;
OLED_WriteCmd(0xB0 + y); /* 设置页地址 */
OLED_WriteCmd(x & 0x0F); /* 设置列低4位 */
OLED_WriteCmd(0x10 | (x >> 4)); /* 设置列高4位 */
}
/**
* @brief 在指定位置显示字符
* @param x: 列坐标(像素)
* @param y: 行坐标(像素)
* @param ch: 要显示的字符
* @param font_size: 字体大小(6或8)
*/
void OLED_ShowChar(uint8_t x, uint8_t y, char ch, uint8_t font_size)
{
uint8_t i, j;
uint8_t temp;
uint16_t page_offset;
uint8_t char_index;
/* 计算字符在字库中的索引 */
if (ch >= ' ' && ch <= 'z') {
char_index = ch - ' ';
} else {
char_index = 0; /* 非打印字符显示空格 */
}
if (font_size == 6) {
/* 6x8字体,字符宽度6像素,高度8像素(1页) */
if (x > 122) x = 122; /* 防止超出边界 */
if (y > 56) y = 56;
page_offset = (y / 8) * OLED_WIDTH; /* 计算该行在缓冲区中的偏移 */
for (i = 0; i < 6; i++) {
temp = F6x8[char_index][i];
/* 写入缓冲区,注意不能覆盖该页中其他行的数据 */
if ((y % 8) == 0) {
OLED_Buffer[page_offset + x + i] = temp;
} else {
/* 处理字符跨页的情况 */
uint8_t y_offset = y % 8;
OLED_Buffer[page_offset + x + i] |= (temp << y_offset);
if ((y / 8) < 7) {
OLED_Buffer[page_offset + OLED_WIDTH + x + i] |= (temp >> (8 - y_offset));
}
}
}
}
}
/**
* @brief 在指定位置显示字符串
* @param x: 起始列坐标
* @param y: 起始行坐标
* @param str: 要显示的字符串
* @param font_size: 字体大小
*/
void OLED_ShowString(uint8_t x, uint8_t y, const char *str, uint8_t font_size)
{
uint8_t char_width = (font_size == 6) ? 6 : 8;
while (*str != '\0') {
/* 换行处理 */
if (*str == '\n') {
y += font_size;
x = 0;
str++;
continue;
}
/* 显示单个字符 */
OLED_ShowChar(x, y, *str, font_size);
/* 移动到下一个字符位置 */
x += char_width;
/* 超出屏幕宽度时换行 */
if (x + char_width > OLED_WIDTH) {
x = 0;
y += font_size;
}
str++;
}
}
/**
* @brief 显示无符号整数
* @param x: 起始列坐标
* @param y: 起始行坐标
* @param num: 要显示的数字
* @param len: 数字位数
* @param font_size: 字体大小
*/
void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t font_size)
{
char str[12] = {0};
uint8_t i;
/* 转换为字符串并补齐位数 */
for (i = 0; i < len; i++) {
str[len - 1 - i] = (num % 10) + '0';
num /= 10;
}
str[len] = '\0';
/* 显示字符串 */
OLED_ShowString(x, y, str, font_size);
}
/**
* @brief 显示浮点数
* @param x: 起始列坐标
* @param y: 起始行坐标
* @param num: 要显示的浮点数
* @param int_len: 整数部分位数
* @param dec_len: 小数部分位数
* @param font_size: 字体大小
*/
void OLED_ShowFloat(uint8_t x, uint8_t y, float num, uint8_t int_len,
uint8_t dec_len, uint8_t font_size)
{
char str[20] = {0};
uint32_t int_part;
uint32_t dec_part;
uint32_t multiplier = 1;
uint8_t i;
uint8_t pos = 0;
/* 处理负数 */
if (num < 0) {
str[pos++] = '-';
num = -num;
}
/* 分离整数和小数部分 */
int_part = (uint32_t)num;
/* 计算小数部分 */
for (i = 0; i < dec_len; i++) {
multiplier *= 10;
}
dec_part = (uint32_t)((num - int_part) * multiplier + 0.5f);
/* 处理四舍五入进位 */
if (dec_part >= multiplier) {
int_part++;
dec_part = 0;
}
/* 格式化整数部分 */
char int_str[12] = {0};
for (i = 0; i < int_len; i++) {
int_str[int_len - 1 - i] = (int_part % 10) + '0';
int_part /= 10;
}
/* 去掉前导零 */
i = 0;
while (i < int_len - 1 && int_str[i] == '0') {
int_str[i] = ' ';
i++;
}
/* 复制整数部分到输出字符串 */
for (i = 0; i < int_len; i++) {
str[pos++] = int_str[i];
}
/* 添加小数点 */
str[pos++] = '.';
/* 格式化小数部分 */
for (i = 0; i < dec_len; i++) {
str[pos + dec_len - 1 - i] = (dec_part % 10) + '0';
dec_part /= 10;
}
pos += dec_len;
str[pos] = '\0';
/* 显示字符串 */
OLED_ShowString(x, y, str, font_size);
}
/**
* @brief 在缓冲区中绘制一个像素点
* @param x: 列坐标(0-127)
* @param y: 行坐标(0-63)
* @param color: 1点亮,0熄灭
*/
void OLED_DrawPoint(uint8_t x, uint8_t y, uint8_t color)
{
uint16_t page_offset;
uint8_t bit_mask;
if (x > 127 || y > 63) return;
page_offset = (y / 8) * OLED_WIDTH + x;
bit_mask = 1 << (y % 8);
if (color) {
OLED_Buffer[page_offset] |= bit_mask;
} else {
OLED_Buffer[page_offset] &= ~bit_mask;
}
}
/**
* @brief 绘制直线(Bresenham算法)
* @param x0, y0: 起点坐标
* @param x1, y1: 终点坐标
* @param color: 1点亮
*/
void OLED_DrawLine(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t color)
{
int16_t dx, dy;
int16_t sx, sy;
int16_t err, e2;
dx = (x1 > x0) ? (x1 - x0) : (x0 - x1);
dy = (y1 > y0) ? (y1 - y0) : (y0 - y1);
sx = (x0 < x1) ? 1 : -1;
sy = (y0 < y1) ? 1 : -1;
err = dx - dy;
while (1) {
OLED_DrawPoint(x0, y0, color);
if (x0 == x1 && y0 == y1) break;
e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
}
}
/**
* @brief 绘制矩形边框
* @param x, y: 左上角坐标
* @param w: 宽度
* @param h: 高度
* @param color: 1点亮
*/
void OLED_DrawRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t color)
{
OLED_DrawLine(x, y, x + w - 1, y, color); /* 上边 */
OLED_DrawLine(x, y + h - 1, x + w - 1, y + h - 1, color); /* 下边 */
OLED_DrawLine(x, y, x, y + h - 1, color); /* 左边 */
OLED_DrawLine(x + w - 1, y, x + w - 1, y + h - 1, color); /* 右边 */
}
/**
* @brief 绘制填充矩形
* @param x, y: 左上角坐标
* @param w: 宽度
* @param h: 高度
* @param color: 1填充
*/
void OLED_FillRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t color)
{
uint8_t i, j;
for (i = 0; i < h; i++) {
for (j = 0; j < w; j++) {
OLED_DrawPoint(x + j, y + i, color);
}
}
}
/**
* @brief 显示电池图标及电量百分比
* @param x, y: 左上角坐标
* @param percent: 电量百分比(0-100)
*/
void OLED_ShowBattery(uint8_t x, uint8_t y, uint8_t percent)
{
uint8_t bar_width;
/* 绘制电池外框 */
OLED_DrawRect(x, y, 30, 12, 1);
OLED_DrawRect(x + 30, y + 2, 3, 8, 1); /* 电池正极突起 */
/* 限制百分比范围 */
if (percent > 100) percent = 100;
/* 计算填充宽度(总共24像素) */
bar_width = (uint8_t)((float)percent / 100.0f * 24.0f);
/* 绘制电量条 */
OLED_FillRect(x + 3, y + 3, bar_width, 6, 1);
/* 在电池下方显示百分比数字 */
char str[5];
sprintf(str, "%d%%", percent);
OLED_ShowString(x, y + 14, str, 6);
}
/**
* @brief 格式化输出函数(类似printf)
* @param x, y: 坐标
* @param font_size: 字体大小
* @param fmt: 格式化字符串
*/
void OLED_Printf(uint8_t x, uint8_t y, uint8_t font_size, const char *fmt, ...)
{
char buffer[64];
va_list args;
va_start(args, fmt);
vsnprintf(buffer, sizeof(buffer), fmt, args);
va_end(args);
OLED_ShowString(x, y, buffer, font_size);
}
4.6 新建文件:Core/Inc/key.h
此文件为按键驱动头文件。
c
/**
* @file key.h
* @brief 按键驱动头文件
* @author SmartCharger Project
* @date 2025
*/
#ifndef __KEY_H
#define __KEY_H
#ifdef __cplusplus
extern "C" {
#endif
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "gpio.h"
/* 按键引脚定义 --------------------------------------------------------------*/
#define KEY_SET_PIN GPIO_PIN_10
#define KEY_SET_PORT GPIOB
#define KEY_UP_PIN GPIO_PIN_11
#define KEY_UP_PORT GPIOB
#define KEY_DOWN_PIN GPIO_PIN_12
#define KEY_DOWN_PORT GPIOB
/* 按键状态定义 --------------------------------------------------------------*/
#define KEY_NONE 0x00 /* 无按键按下 */
#define KEY_SET_PRESS 0x01 /* 设置键按下 */
#define KEY_UP_PRESS 0x02 /* 上调键按下 */
#define KEY_DOWN_PRESS 0x03 /* 下调键按下 */
#define KEY_SET_LONG 0x11 /* 设置键长按 */
#define KEY_UP_LONG 0x12 /* 上调键长按 */
#define KEY_DOWN_LONG 0x13 /* 下调键长按 */
/* 按键消抖时间定义 ----------------------------------------------------------*/
#define KEY_DEBOUNCE_MS 20 /* 消抖时间(ms) */
#define KEY_LONG_PRESS_MS 1500 /* 长按判定时间(ms) */
#define KEY_REPEAT_MS 200 /* 长按重复间隔(ms) */
/* 按键结构体 ----------------------------------------------------------------*/
typedef struct {
uint8_t press_flag; /* 按键按下标志 */
uint8_t long_press_flag; /* 长按标志 */
uint8_t repeat_flag; /* 重复触发标志 */
uint32_t press_time; /* 按下时刻计时 */
uint32_t repeat_time; /* 重复计时 */
uint8_t state; /* 当前状态 */
} Key_TypeDef;
/* 函数声明 ------------------------------------------------------------------*/
void KEY_Init(void);
uint8_t KEY_Scan(void);
uint8_t KEY_GetValue(void);
void KEY_Process(void);
uint8_t KEY_IsAnyPressed(void);
#ifdef __cplusplus
}
#endif
#endif /* __KEY_H */
4.7 新建文件:Core/Src/key.c
此文件为按键驱动实现,包含消抖和长按检测。
c
/**
* @file key.c
* @brief 按键驱动实现
* @author SmartCharger Project
* @date 2025
*/
/* Includes ------------------------------------------------------------------*/
#include "key.h"
/* 全局变量 ------------------------------------------------------------------*/
static Key_TypeDef key_set;
static Key_TypeDef key_up;
static Key_TypeDef key_down;
static uint32_t key_tick = 0; /* 按键扫描计时 */
/**
* @brief 按键初始化
*/
void KEY_Init(void)
{
/* 清空按键结构体 */
memset(&key_set, 0, sizeof(Key_TypeDef));
memset(&key_up, 0, sizeof(Key_TypeDef));
memset(&key_down, 0, sizeof(Key_TypeDef));
}
/**
* @brief 读取单个按键的原始电平状态
* @param key: 按键结构体指针
* @param port: GPIO端口
* @param pin: GPIO引脚
* @return 0: 按下(低电平), 1: 释放(高电平)
*/
static uint8_t ReadKeyPin(GPIO_TypeDef *port, uint16_t pin)
{
/* 按键按下为低电平(内部上拉) */
if (HAL_GPIO_ReadPin(port, pin) == GPIO_PIN_RESET) {
return 0; /* 按下 */
}
return 1; /* 释放 */
}
/**
* @brief 处理单个按键的状态机
* @param key: 按键结构体指针
* @param raw_state: 原始电平状态
* @param key_value: 按键返回值
* @return 处理后的按键状态
*/
static uint8_t ProcessSingleKey(Key_TypeDef *key, uint8_t raw_state, uint8_t key_value)
{
uint8_t result = KEY_NONE;
switch (key->state) {
case 0: /* 空闲状态,等待按下 */
if (raw_state == 0) { /* 检测到低电平 */
key->state = 1; /* 进入消抖状态 */
key->press_time = HAL_GetTick();
}
break;
case 1: /* 消抖确认状态 */
if (raw_state == 0) { /* 仍然为低电平 */
if ((HAL_GetTick() - key->press_time) >= KEY_DEBOUNCE_MS) {
key->state = 2; /* 确认按下 */
key->press_flag = 1;
key->press_time = HAL_GetTick(); /* 记录按下时间用于长按判断 */
}
} else {
key->state = 0; /* 抖动,返回空闲 */
}
break;
case 2: /* 按下保持状态 */
if (raw_state == 0) { /* 持续按下 */
/* 检查是否达到长按时间 */
if ((HAL_GetTick() - key->press_time) >= KEY_LONG_PRESS_MS) {
key->state = 3; /* 进入长按状态 */
key->long_press_flag = 1;
result = key_value + 0x10; /* 长按返回值 */
}
} else { /* 按键释放 */
key->state = 4; /* 进入释放消抖 */
key->press_time = HAL_GetTick();
}
break;
case 3: /* 长按状态 */
if (raw_state == 0) { /* 持续长按 */
/* 检查是否需要重复触发 */
if ((HAL_GetTick() - key->repeat_time) >= KEY_REPEAT_MS) {
key->repeat_flag = 1;
key->repeat_time = HAL_GetTick();
result = key_value + 0x10; /* 重复长按返回值 */
}
} else { /* 长按释放 */
key->state = 4;
key->press_time = HAL_GetTick();
}
break;
case 4: /* 释放消抖状态 */
if (raw_state == 1) { /* 确认为高电平 */
if ((HAL_GetTick() - key->press_time) >= KEY_DEBOUNCE_MS) {
key->state = 0; /* 返回空闲 */
key->press_flag = 0;
key->long_press_flag = 0;
key->repeat_flag = 0;
}
} else {
/* 仍在低电平,回到之前的状态 */
if (key->long_press_flag) {
key->state = 3;
} else {
key->state = 2;
}
}
break;
default:
key->state = 0;
break;
}
/* 处理短按返回 */
if (key->press_flag && key->state == 2 && !key->long_press_flag) {
/* 短按在释放时才返回 */
if (raw_state == 1) {
key->press_flag = 0;
result = key_value;
}
}
return result;
}
/**
* @brief 按键扫描函数,每10ms调用一次
* @return 按键值
*/
uint8_t KEY_Scan(void)
{
uint8_t key_value = KEY_NONE;
uint8_t raw_set, raw_up, raw_down;
/* 控制扫描频率(每10ms扫描一次) */
if ((HAL_GetTick() - key_tick) < 10) {
return KEY_NONE;
}
key_tick = HAL_GetTick();
/* 读取按键原始电平 */
raw_set = ReadKeyPin(KEY_SET_PORT, KEY_SET_PIN);
raw_up = ReadKeyPin(KEY_UP_PORT, KEY_UP_PIN);
raw_down = ReadKeyPin(KEY_DOWN_PORT, KEY_DOWN_PIN);
/* 处理各个按键 */
key_value = ProcessSingleKey(&key_set, raw_set, KEY_SET_PRESS);
if (key_value != KEY_NONE) return key_value;
key_value = ProcessSingleKey(&key_up, raw_up, KEY_UP_PRESS);
if (key_value != KEY_NONE) return key_value;
key_value = ProcessSingleKey(&key_down, raw_down, KEY_DOWN_PRESS);
if (key_value != KEY_NONE) return key_value;
return KEY_NONE;
}
/**
* @brief 获取当前按键值(非阻塞)
* @return 按键值
*/
uint8_t KEY_GetValue(void)
{
return KEY_Scan();
}
/**
* @brief 按键处理任务(在主循环中调用)
*/
void KEY_Process(void)
{
/* 此函数保留用于未来扩展,当前使用KEY_Scan即可 */
}
/**
* @brief 检查是否有按键被按下
* @return 0: 无按键按下, 1: 有按键按下
*/
uint8_t KEY_IsAnyPressed(void)
{
if (ReadKeyPin(KEY_SET_PORT, KEY_SET_PIN) == 0 ||
ReadKeyPin(KEY_UP_PORT, KEY_UP_PIN) == 0 ||
ReadKeyPin(KEY_DOWN_PORT, KEY_DOWN_PIN) == 0) {
return 1;
}
return 0;
}
4.8 新建文件:Core/Inc/charger.h
此文件为充电器主控制逻辑的头文件。
c
/**
* @file charger.h
* @brief 智能充电器控制逻辑头文件
* @author SmartCharger Project
* @date 2025
*/
#ifndef __CHARGER_H
#define __CHARGER_H
#ifdef __cplusplus
extern "C" {
#endif
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "ina226.h"
#include "oled.h"
#include "key.h"
/* 充电参数定义 --------------------------------------------------------------*/
/* 电池类型:3.7V标称锂电池(4.2V满充) */
#define BATTERY_FULL_VOLTAGE 4.20f /* 满充电压(V) */
#define BATTERY_CV_VOLTAGE 4.20f /* 恒压充电电压(V) */
#define BATTERY_PRE_CHARGE_THRES 3.00f /* 预充电阈值(V) */
#define BATTERY_RECHARGE_THRES 4.00f /* 复充阈值(V) */
/* 充电电流设置(单位:A) */
#define CHARGE_CURRENT_PRE 0.10f /* 预充电电流100mA */
#define CHARGE_CURRENT_CC 0.50f /* 恒流充电电流500mA(默认) */
#define CHARGE_CURRENT_MAX 1.00f /* 最大充电电流1A */
#define CHARGE_CURRENT_TERM 0.05f /* 终止充电电流50mA */
/* 保护参数 */
#define OVER_VOLTAGE_THRES 4.30f /* 过压保护阈值(V) */
#define OVER_CURRENT_THRES 1.20f /* 过流保护阈值(A) */
#define CHARGE_TIMEOUT_MS 36000000UL /* 充电超时10小时(ms) */
/* PWM参数 */
#define PWM_FREQUENCY 20000 /* PWM频率20kHz */
#define PWM_PERIOD 50 /* ARR值(72M/72/20000=50) */
#define PWM_MAX_DUTY 48 /* 最大占空比96% */
#define PWM_MIN_DUTY 1 /* 最小占空比2% */
#define PWM_OFF_DUTY 0 /* 关闭PWM */
/* 充电状态枚举 --------------------------------------------------------------*/
typedef enum {
CHARGE_STATE_IDLE = 0, /* 空闲等待 */
CHARGE_STATE_CHECK, /* 电池检测中 */
CHARGE_STATE_PRE_CHARGE, /* 预充电阶段 */
CHARGE_STATE_CC_CHARGE, /* 恒流充电阶段 */
CHARGE_STATE_CV_CHARGE, /* 恒压充电阶段 */
CHARGE_STATE_DONE, /* 充电完成 */
CHARGE_STATE_ERROR, /* 错误状态 */
CHARGE_STATE_PAUSE /* 暂停充电 */
} ChargeState_TypeDef;
/* 充电器系统数据结构体 ------------------------------------------------------*/
typedef struct {
/* 测量数据 */
float bus_voltage; /* 总线电压(V) */
float battery_voltage; /* 电池电压(V) */
float charge_current; /* 充电电流(A) */
float charge_power; /* 充电功率(W) */
float temperature; /* 温度(预留) */
/* 设定参数 */
float set_voltage; /* 设定充电电压(V) */
float set_current; /* 设定充电电流(A) */
/* 状态信息 */
ChargeState_TypeDef state; /* 充电状态 */
uint32_t state_start_time; /* 状态开始时间 */
uint32_t charge_start_time; /* 充电开始时间 */
uint32_t total_charge_time; /* 总充电时间(秒) */
float total_charge_capacity;/* 累计充电容量(mAh) */
/* PWM控制 */
uint8_t pwm_duty; /* 当前PWM占空比(0-50) */
/* 错误标志 */
uint8_t error_code; /* 错误代码 */
uint8_t fault_occurred; /* 故障发生标志 */
/* UI相关 */
uint8_t display_page; /* 显示页面 */
uint8_t setting_mode; /* 设置模式 */
uint32_t last_display_update; /* 上次显示更新时间 */
} Charger_DataTypeDef;
/* 错误代码定义 */
#define ERROR_NONE 0x00 /* 无错误 */
#define ERROR_OVERVOLTAGE 0x01 /* 过压错误 */
#define ERROR_OVERCURRENT 0x02 /* 过流错误 */
#define ERROR_TIMEOUT 0x03 /* 超时错误 */
#define ERROR_SENSOR_FAIL 0x04 /* 传感器故障 */
#define ERROR_REVERSE_POLARITY 0x05 /* 反接错误 */
#define ERROR_OVERTEMP 0x06 /* 过温错误 */
/* 函数声明 ------------------------------------------------------------------*/
void Charger_Init(void);
void Charger_Task(void);
void Charger_UpdateMeasurements(void);
void Charger_StateMachine(void);
void Charger_SetPWM(uint8_t duty);
void Charger_StopCharge(void);
void Charger_DisplayTask(void);
void Charger_KeyHandler(uint8_t key_value);
void Charger_CheckProtection(void);
float Charger_PID_Calculate(float target, float current);
#ifdef __cplusplus
}
#endif
#endif /* __CHARGER_H */
4.9 新建文件:Core/Src/charger.c
此文件为充电器主控制逻辑实现,包含状态机、PID控制和保护功能。
c
/**
* @file charger.c
* @brief 智能充电器控制逻辑实现
* @author SmartCharger Project
* @date 2025
*/
/* Includes ------------------------------------------------------------------*/
#include "charger.h"
#include <stdio.h>
#include <math.h>
/* 全局变量 ------------------------------------------------------------------*/
Charger_DataTypeDef charger;
extern TIM_HandleTypeDef htim2;
/* 私有变量 ------------------------------------------------------------------*/
static float pid_integral = 0.0f;
static float pid_last_error = 0.0f;
static const float pid_kp = 0.8f; /* 比例系数 */
static const float pid_ki = 0.15f; /* 积分系数 */
static const float pid_kd = 0.05f; /* 微分系数 */
static const float pid_max_integral = 20.0f; /* 积分限幅 */
/**
* @brief 充电器系统初始化
*/
void Charger_Init(void)
{
/* 初始化充电器数据结构 */
memset(&charger, 0, sizeof(Charger_DataTypeDef));
/* 设置默认参数 */
charger.set_voltage = BATTERY_CV_VOLTAGE; /* 4.2V */
charger.set_current = CHARGE_CURRENT_CC; /* 500mA */
charger.state = CHARGE_STATE_IDLE;
charger.pwm_duty = PWM_OFF_DUTY; /* 关闭PWM输出 */
charger.display_page = 0;
charger.setting_mode = 0;
/* 关闭PWM输出 */
Charger_SetPWM(PWM_OFF_DUTY);
/* 初始化PID变量 */
pid_integral = 0.0f;
pid_last_error = 0.0f;
/* 关闭蜂鸣器和LED */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); /* 蜂鸣器关闭 */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET); /* 红色LED灭(共阳) */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_SET); /* 绿色LED灭 */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, GPIO_PIN_SET); /* 蓝色LED灭 */
printf("Charger System Initialized.\r\n");
printf("Default: %.2fV, %.3fA\r\n", charger.set_voltage, charger.set_current);
}
/**
* @brief 更新测量数据
*/
void Charger_UpdateMeasurements(void)
{
INA226_DataTypeDef ina_data;
/* 读取INA226数据 */
if (INA226_ReadData(&ina_data) == 0) {
charger.bus_voltage = ina_data.bus_voltage;
charger.battery_voltage = ina_data.bus_voltage; /* 总线电压即电池电压 */
charger.charge_current = ina_data.current;
charger.charge_power = ina_data.power;
/* 电流为负表示放电,取绝对值 */
if (charger.charge_current < 0) {
charger.charge_current = 0.0f;
}
} else {
/* 读取失败,可能是传感器故障 */
charger.error_code = ERROR_SENSOR_FAIL;
charger.fault_occurred = 1;
}
}
/**
* @brief 设置PWM占空比
* @param duty: 占空比值(0-50),对应0%-100%
*/
void Charger_SetPWM(uint8_t duty)
{
if (duty > PWM_MAX_DUTY) {
duty = PWM_MAX_DUTY;
}
charger.pwm_duty = duty;
/* 设置TIM2通道2的比较值 */
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, duty);
}
/**
* @brief 停止充电
*/
void Charger_StopCharge(void)
{
/* 设置PWM占空比为0,关闭输出 */
Charger_SetPWM(PWM_OFF_DUTY);
charger.state = CHARGE_STATE_IDLE;
/* LED指示 */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET); /* 红灯灭 */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_SET); /* 绿灯灭 */
printf("Charge Stopped.\r\n");
}
/**
* @brief 检查保护条件
*/
void Charger_CheckProtection(void)
{
/* 过压保护 */
if (charger.battery_voltage > OVER_VOLTAGE_THRES) {
charger.error_code = ERROR_OVERVOLTAGE;
charger.fault_occurred = 1;
charger.state = CHARGE_STATE_ERROR;
Charger_SetPWM(PWM_OFF_DUTY);
/* 蜂鸣器报警 */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_RESET); /* 红灯亮 */
printf("ERROR: Overvoltage! Vbat=%.3fV\r\n", charger.battery_voltage);
}
/* 过流保护 */
if (charger.charge_current > OVER_CURRENT_THRES) {
charger.error_code = ERROR_OVERCURRENT;
charger.fault_occurred = 1;
charger.state = CHARGE_STATE_ERROR;
Charger_SetPWM(PWM_OFF_DUTY);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_RESET);
printf("ERROR: Overcurrent! I=%.3fA\r\n", charger.charge_current);
}
/* 充电超时保护 */
if (charger.state != CHARGE_STATE_IDLE &&
charger.state != CHARGE_STATE_ERROR &&
charger.state != CHARGE_STATE_DONE) {
if ((HAL_GetTick() - charger.charge_start_time) > CHARGE_TIMEOUT_MS) {
charger.error_code = ERROR_TIMEOUT;
charger.fault_occurred = 1;
charger.state = CHARGE_STATE_ERROR;
Charger_SetPWM(PWM_OFF_DUTY);
printf("ERROR: Charge Timeout!\r\n");
}
}
}
/**
* @brief PID控制器
* @param target: 目标值
* @param current: 当前值
* @return PID输出值
*/
float Charger_PID_Calculate(float target, float current)
{
float error;
float derivative;
float output;
/* 计算误差 */
error = target - current;
/* 积分项累加(带抗积分饱和) */
pid_integral += error;
if (pid_integral > pid_max_integral) {
pid_integral = pid_max_integral;
} else if (pid_integral < -pid_max_integral) {
pid_integral = -pid_max_integral;
}
/* 微分项 */
derivative = error - pid_last_error;
pid_last_error = error;
/* PID输出 */
output = pid_kp * error + pid_ki * pid_integral + pid_kd * derivative;
/* 输出限幅 */
if (output > (float)PWM_MAX_DUTY) {
output = (float)PWM_MAX_DUTY;
/* 输出饱和时停止积分 */
pid_integral -= error;
} else if (output < 0.0f) {
output = 0.0f;
pid_integral -= error;
}
return output;
}
/**
* @brief 充电状态机
*/
void Charger_StateMachine(void)
{
static uint32_t last_state_time = 0;
uint32_t current_time = HAL_GetTick();
/* 检查保护条件 */
Charger_CheckProtection();
/* 如果发生故障,停止状态机 */
if (charger.fault_occurred) {
return;
}
switch (charger.state) {
case CHARGE_STATE_IDLE:
/* 空闲状态,等待电池接入 */
charger.pwm_duty = PWM_OFF_DUTY;
Charger_SetPWM(PWM_OFF_DUTY);
/* 检测电池是否接入(电压>0.5V认为有电池) */
if (charger.battery_voltage > 0.5f &&
charger.battery_voltage < OVER_VOLTAGE_THRES) {
/* 有电池接入,进入检测状态 */
charger.state = CHARGE_STATE_CHECK;
charger.charge_start_time = current_time;
charger.total_charge_capacity = 0.0f;
charger.fault_occurred = 0;
charger.error_code = ERROR_NONE;
/* 绿灯闪烁表示开始检测 */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_RESET);
printf("Battery Detected: %.3fV\r\n", charger.battery_voltage);
}
break;
case CHARGE_STATE_CHECK:
/* 检测电池状态,延时2秒确认 */
if ((current_time - charger.state_start_time) > 2000) {
if (charger.battery_voltage < BATTERY_PRE_CHARGE_THRES) {
/* 电压过低,进入预充电 */
charger.state = CHARGE_STATE_PRE_CHARGE;
charger.set_current = CHARGE_CURRENT_PRE;
printf("Enter Pre-Charge Mode.\r\n");
} else if (charger.battery_voltage < BATTERY_CV_VOLTAGE) {
/* 进入恒流充电 */
charger.state = CHARGE_STATE_CC_CHARGE;
charger.set_current = CHARGE_CURRENT_CC;
/* 初始化PID */
pid_integral = 0.0f;
pid_last_error = 0.0f;
printf("Enter CC Charge Mode.\r\n");
} else {
/* 电池已满,直接完成 */
charger.state = CHARGE_STATE_DONE;
printf("Battery Already Full.\r\n");
}
charger.state_start_time = current_time;
}
break;
case CHARGE_STATE_PRE_CHARGE:
/* 预充电:小电流恒流充电 */
/* 设置目标电流为预充电电流 */
{
float pid_output = Charger_PID_Calculate(CHARGE_CURRENT_PRE,
charger.charge_current);
uint8_t duty = (uint8_t)pid_output;
if (duty < PWM_MIN_DUTY && charger.charge_current > 0.001f) {
duty = PWM_MIN_DUTY;
}
Charger_SetPWM(duty);
}
/* 累计充电容量 */
charger.total_charge_capacity += charger.charge_current * 0.1f / 3.6f; /* mAh */
/* 检查是否达到恒流充电条件 */
if (charger.battery_voltage >= BATTERY_PRE_CHARGE_THRES) {
charger.state = CHARGE_STATE_CC_CHARGE;
charger.set_current = CHARGE_CURRENT_CC;
charger.state_start_time = current_time;
pid_integral = 0.0f;
pid_last_error = 0.0f;
printf("Switch to CC Charge Mode.\r\n");
}
break;
case CHARGE_STATE_CC_CHARGE:
/* 恒流充电:保持设定电流 */
{
float pid_output = Charger_PID_Calculate(charger.set_current,
charger.charge_current);
uint8_t duty = (uint8_t)pid_output;
if (duty < PWM_MIN_DUTY && charger.charge_current > 0.001f) {
duty = PWM_MIN_DUTY;
}
if (duty > PWM_MAX_DUTY) {
duty = PWM_MAX_DUTY;
}
Charger_SetPWM(duty);
}
/* 累计充电容量 */
charger.total_charge_capacity += charger.charge_current * 0.1f / 3.6f;
/* 检查是否达到恒压充电条件 */
if (charger.battery_voltage >= BATTERY_CV_VOLTAGE) {
charger.state = CHARGE_STATE_CV_CHARGE;
charger.state_start_time = current_time;
pid_integral = 0.0f;
pid_last_error = 0.0f;
printf("Switch to CV Charge Mode.\r\n");
}
break;
case CHARGE_STATE_CV_CHARGE:
/* 恒压充电:保持电压不变,电流逐渐减小 */
{
float pid_output = Charger_PID_Calculate(BATTERY_CV_VOLTAGE,
charger.battery_voltage);
uint8_t duty = (uint8_t)pid_output;
if (duty > PWM_MAX_DUTY) {
duty = PWM_MAX_DUTY;
}
Charger_SetPWM(duty);
}
/* 累计充电容量 */
charger.total_charge_capacity += charger.charge_current * 0.1f / 3.6f;
/* 检查是否充电完成(电流小于终止电流) */
if (charger.charge_current <= CHARGE_CURRENT_TERM) {
charger.state = CHARGE_STATE_DONE;
charger.state_start_time = current_time;
Charger_SetPWM(PWM_OFF_DUTY);
/* 充电完成指示 */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_RESET); /* 绿灯常亮 */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); /* 蜂鸣器短鸣 */
HAL_Delay(200);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
printf("Charge Complete! Capacity: %.1fmAh\r\n",
charger.total_charge_capacity);
}
break;
case CHARGE_STATE_DONE:
/* 充电完成,持续监测电池电压 */
/* 如果电压下降到复充阈值以下,重新开始充电 */
if (charger.battery_voltage < BATTERY_RECHARGE_THRES) {
charger.state = CHARGE_STATE_CC_CHARGE;
charger.charge_start_time = current_time;
charger.fault_occurred = 0;
charger.error_code = ERROR_NONE;
pid_integral = 0.0f;
pid_last_error = 0.0f;
printf("Recharge Triggered. Vbat=%.3fV\r\n", charger.battery_voltage);
}
break;
case CHARGE_STATE_ERROR:
/* 错误状态,保持PWM关闭 */
Charger_SetPWM(PWM_OFF_DUTY);
break;
case CHARGE_STATE_PAUSE:
/* 暂停状态 */
Charger_SetPWM(PWM_OFF_DUTY);
break;
default:
charger.state = CHARGE_STATE_IDLE;
break;
}
/* 更新总充电时间 */
if (charger.state != CHARGE_STATE_IDLE &&
charger.state != CHARGE_STATE_ERROR) {
charger.total_charge_time = (current_time - charger.charge_start_time) / 1000;
}
}
/**
* @brief 显示任务,更新OLED
*/
void Charger_DisplayTask(void)
{
static uint32_t last_refresh = 0;
uint32_t current_time = HAL_GetTick();
char str_buf[32];
uint8_t progress;
/* 每500ms刷新一次显示 */
if ((current_time - last_refresh) < 500) {
return;
}
last_refresh = current_time;
/* 清空缓冲区 */
OLED_ClearBuffer();
/* 显示标题栏 */
OLED_ShowString(0, 0, "STM32 Smart Charger", 6);
OLED_DrawLine(0, 9, 127, 9, 1); /* 分隔线 */
/* 根据状态显示不同内容 */
if (charger.fault_occurred) {
/* 错误状态显示 */
OLED_ShowString(10, 16, "FAULT!", 6);
OLED_ShowString(10, 28, "Code:", 6);
switch (charger.error_code) {
case ERROR_OVERVOLTAGE:
OLED_ShowString(50, 28, "Over Voltage", 6);
break;
case ERROR_OVERCURRENT:
OLED_ShowString(50, 28, "Over Current", 6);
break;
case ERROR_TIMEOUT:
OLED_ShowString(50, 28, "Time Out", 6);
break;
case ERROR_SENSOR_FAIL:
OLED_ShowString(50, 28, "Sensor Fail", 6);
break;
default:
OLED_ShowString(50, 28, "Unknown", 6);
break;
}
OLED_ShowString(10, 48, "Press SET to Reset", 6);
} else {
/* 正常状态显示 */
/* 电压显示 */
OLED_ShowString(0, 14, "V:", 6);
OLED_ShowFloat(16, 14, charger.battery_voltage, 1, 3, 6);
OLED_ShowString(64, 14, "V", 6);
/* 电流显示 */
OLED_ShowString(0, 24, "I:", 6);
OLED_ShowFloat(16, 24, charger.charge_current * 1000.0f, 3, 0, 6);
OLED_ShowString(64, 24, "mA", 6);
/* 功率显示 */
OLED_ShowString(0, 34, "P:", 6);
OLED_ShowFloat(16, 34, charger.charge_power, 1, 2, 6);
OLED_ShowString(64, 34, "W", 6);
/* 状态显示 */
switch (charger.state) {
case CHARGE_STATE_IDLE:
OLED_ShowString(72, 14, "IDLE", 6);
break;
case CHARGE_STATE_CHECK:
OLED_ShowString(72, 14, "CHECK", 6);
break;
case CHARGE_STATE_PRE_CHARGE:
OLED_ShowString(72, 14, "PRE-CHG", 6);
break;
case CHARGE_STATE_CC_CHARGE:
OLED_ShowString(72, 14, "CC-CHG", 6);
break;
case CHARGE_STATE_CV_CHARGE:
OLED_ShowString(72, 14, "CV-CHG", 6);
break;
case CHARGE_STATE_DONE:
OLED_ShowString(72, 14, "DONE", 6);
break;
case CHARGE_STATE_PAUSE:
OLED_ShowString(72, 14, "PAUSE", 6);
break;
default:
break;
}
/* PWM占空比 */
OLED_ShowString(72, 24, "PWM:", 6);
OLED_ShowNum(108, 24, charger.pwm_duty * 2, 3, 6); /* 显示百分比 */
OLED_ShowString(126, 24, "%", 6);
/* 充电时间 */
uint32_t hours = charger.total_charge_time / 3600;
uint32_t minutes = (charger.total_charge_time % 3600) / 60;
uint32_t seconds = charger.total_charge_time % 60;
sprintf(str_buf, "%02lu:%02lu:%02lu", hours, minutes, seconds);
OLED_ShowString(72, 34, str_buf, 6);
/* 充电进度条 */
if (charger.state != CHARGE_STATE_IDLE &&
charger.state != CHARGE_STATE_CHECK) {
/* 根据电压计算进度 */
if (charger.battery_voltage < BATTERY_PRE_CHARGE_THRES) {
progress = (uint8_t)(charger.battery_voltage / BATTERY_PRE_CHARGE_THRES * 10.0f);
} else if (charger.state == CHARGE_STATE_DONE) {
progress = 100;
} else {
progress = (uint8_t)((charger.battery_voltage - BATTERY_PRE_CHARGE_THRES) /
(BATTERY_CV_VOLTAGE - BATTERY_PRE_CHARGE_THRES) * 80.0f + 10.0f);
}
if (progress > 100) progress = 100;
/* 绘制进度条 */
OLED_DrawRect(0, 48, 100, 8, 1);
OLED_FillRect(2, 50, progress, 4, 1);
sprintf(str_buf, "%d%%", progress);
OLED_ShowString(104, 48, str_buf, 6);
}
/* 累计容量 */
OLED_ShowString(0, 56, "Cap:", 6);
OLED_ShowFloat(36, 56, charger.total_charge_capacity, 4, 0, 6);
OLED_ShowString(72, 56, "mAh", 6);
}
/* 更新显示 */
OLED_Refresh();
}
/**
* @brief 按键处理函数
* @param key_value: 按键返回值
*/
void Charger_KeyHandler(uint8_t key_value)
{
switch (key_value) {
case KEY_SET_PRESS:
/* 设置键短按 */
if (charger.fault_occurred) {
/* 故障状态下按SET复位 */
charger.fault_occurred = 0;
charger.error_code = ERROR_NONE;
charger.state = CHARGE_STATE_IDLE;
Charger_StopCharge();
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); /* 蜂鸣器关 */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET); /* 红灯灭 */
printf("Fault Reset.\r\n");
} else {
/* 进入/退出设置模式 */
charger.setting_mode = !charger.setting_mode;
if (charger.setting_mode) {
printf("Enter Setting Mode.\r\n");
} else {
printf("Exit Setting Mode.\r\n");
}
}
break;
case KEY_UP_PRESS:
if (charger.setting_mode) {
/* 在设置模式下增加充电电流 */
charger.set_current += 0.05f;
if (charger.set_current > CHARGE_CURRENT_MAX) {
charger.set_current = CHARGE_CURRENT_MAX;
}
printf("Set Current: %.3fA\r\n", charger.set_current);
}
break;
case KEY_DOWN_PRESS:
if (charger.setting_mode) {
/* 在设置模式下减小充电电流 */
charger.set_current -= 0.05f;
if (charger.set_current < CHARGE_CURRENT_PRE) {
charger.set_current = CHARGE_CURRENT_PRE;
}
printf("Set Current: %.3fA\r\n", charger.set_current);
}
break;
case KEY_SET_LONG:
/* 设置键长按:强制停止充电 */
if (!charger.fault_occurred && charger.state != CHARGE_STATE_IDLE) {
Charger_StopCharge();
printf("Force Stop Charge.\r\n");
}
break;
default:
break;
}
}
/**
* @brief 充电器主任务,在主循环中每100ms调用一次
*/
void Charger_Task(void)
{
static uint32_t last_task_time = 0;
uint32_t current_time = HAL_GetTick();
uint8_t key_value;
/* 每100ms执行一次 */
if ((current_time - last_task_time) < 100) {
return;
}
last_task_time = current_time;
/* 更新测量数据 */
Charger_UpdateMeasurements();
/* 按键扫描 */
key_value = KEY_Scan();
if (key_value != KEY_NONE) {
Charger_KeyHandler(key_value);
}
/* 运行充电状态机 */
if (!charger.setting_mode) {
Charger_StateMachine();
}
/* 更新显示 */
Charger_DisplayTask();
}
五、主程序main.c修改
接下来修改main.c文件,整合所有模块。以下是在CubeMX生成的main.c基础上进行的修改。
5.1 修改后的main.c
c
/**
* @file main.c
* @brief 智能充电器主程序
* @author SmartCharger Project
* @date 2025
*/
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "gpio.h"
#include "i2c.h"
#include "tim.h"
#include "usart.h"
#include "adc.h"
#include "ina226.h"
#include "oled.h"
#include "key.h"
#include "charger.h"
#include <stdio.h>
/* 私有函数声明 --------------------------------------------------------------*/
void SystemClock_Config(void);
static void MX_NVIC_Init(void);
void System_Init(void);
/**
* @brief 重定向printf到USART1
* @note 需要在KEIL中勾选Use MicroLIB
*/
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
PUTCHAR_PROTOTYPE
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
/**
* @brief 系统初始化
*/
void System_Init(void)
{
/* 初始化显示 */
OLED_Init();
OLED_Clear();
OLED_ShowString(20, 20, "Smart Charger", 6);
OLED_ShowString(15, 35, "Initializing...", 6);
OLED_Refresh();
/* 初始化INA226 */
printf("Initializing INA226...\r\n");
if (INA226_Init() != 0) {
printf("INA226 Init Failed!\r\n");
OLED_ShowString(10, 50, "Sensor Error!", 6);
OLED_Refresh();
HAL_Delay(2000);
} else {
printf("INA226 Init Success!\r\n");
}
/* 初始化按键 */
KEY_Init();
printf("Key Init Done.\r\n");
/* 初始化充电器 */
Charger_Init();
/* 启动PWM输出(初始占空比0,即关闭输出) */
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 0);
printf("\r\n========================================\r\n");
printf(" STM32 Smart Charger System Started\r\n");
printf(" System Clock: %d MHz\r\n", HAL_RCC_GetSysClockFreq() / 1000000);
printf(" PWM Frequency: 20 kHz\r\n");
printf(" I2C Speed: 400 kHz\r\n");
printf("========================================\r\n\r\n");
}
/**
* @brief 主函数
* @retval int
*/
int main(void)
{
/* HAL库初始化 */
HAL_Init();
/* 配置系统时钟 */
SystemClock_Config();
/* 初始化外设 */
MX_GPIO_Init();
MX_I2C1_Init();
MX_TIM2_Init();
MX_USART1_UART_Init();
MX_ADC1_Init();
/* 系统初始化 */
System_Init();
/* 主循环 */
while (1)
{
/* 运行充电器主任务 */
Charger_Task();
/* 板载LED心跳闪烁 */
static uint32_t led_tick = 0;
if ((HAL_GetTick() - led_tick) > 500) {
led_tick = HAL_GetTick();
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
/* 充电状态LED指示 */
if (charger.state == CHARGE_STATE_DONE) {
/* 充电完成:绿灯常亮 */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, GPIO_PIN_RESET);
} else if (charger.state != CHARGE_STATE_IDLE &&
charger.state != CHARGE_STATE_ERROR) {
/* 充电中:蓝灯闪烁 */
static uint32_t blue_tick = 0;
if ((HAL_GetTick() - blue_tick) > 300) {
blue_tick = HAL_GetTick();
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_15);
}
} else {
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, GPIO_PIN_SET); /* 蓝灯灭 */
}
/* 故障状态LED */
if (charger.fault_occurred) {
/* 红灯快闪 */
static uint32_t red_tick = 0;
if ((HAL_GetTick() - red_tick) > 150) {
red_tick = HAL_GetTick();
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_13);
}
}
}
}
/**
* @brief 系统时钟配置
* @note 系统时钟72MHz, APB1=36MHz, APB2=72MHz
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
/* 配置HSE振荡器 */
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/* 配置时钟树 */
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
/* 配置外设时钟 */
PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_ADC;
PeriphClkInit.AdcClockSelection = RCC_ADCPCLK2_DIV6;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
{
Error_Handler();
}
}
/**
* @brief NVIC配置
*/
static void MX_NVIC_Init(void)
{
/* 设置优先级分组为4位抢占优先级 */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
/* 系统定时器中断 */
HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
}
/**
* @brief 错误处理函数
*/
void Error_Handler(void)
{
__disable_irq();
while (1)
{
/* 错误状态:板载LED快速闪烁 */
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
HAL_Delay(100);
}
}
六、编译与下载
6.1 KEIL工程设置
6.1.1 添加源文件
在KEIL的Project窗口中,右键点击Application/User组,选择Add Existing Files to Group,将以下新建的文件添加到工程中:
- ina226.c
- oled.c
- key.c
- charger.c
同时确保所有对应的.h头文件在编译搜索路径中。右键点击工程名,选择Options for Target,在C/C++标签页的Include Paths中添加包含路径。
6.1.2 编译选项设置
在Options for Target窗口中:
-
Target标签页:
- 确认Xtal为8.0MHz
- Code Generation勾选Use MicroLIB(使用微库,支持printf)
-
Debug标签页:
- 选择ST-Link Debugger
- 点击Settings,确认Port为SW,SW Device检测到设备
-
Utilities标签页:
- 勾选Use Debug Driver
- 点击Settings,添加Flash Programming Algorithm:STM32F10x Med-density Flash
6.1.3 编译工程
点击Rebuild按钮(或按F7)编译整个工程。编译成功后应显示:
Program Size: Code=XXXXX RO-data=XXXX RW-data=XXXX ZI-data=XXXX
0 Error(s), 0 Warning(s).
6.2 烧录程序
将ST-Link连接到STM32F103C8T6最小系统板:
- ST-Link SWCLK → STM32 SWCLK (PA14)
- ST-Link SWDIO → STM32 SWDIO (PA13)
- ST-Link GND → STM32 GND
- ST-Link 3.3V → STM32 3.3V
点击Load按钮(或按F8)下载程序到开发板。下载完成后,程序自动运行。
七、系统调试与测试
7.1 串口调试
将USB转串口模块连接到USART1引脚:
- TX → PA10 (RX)
- RX → PA9 (TX)
- GND → GND
打开串口调试助手,设置波特率115200,8数据位,1停止位,无校验。按下复位键后,串口应输出初始化信息。
7.2 功能测试步骤
7.2.1 基本检测测试
- 上电后,OLED应显示"Smart Charger"和"Initializing..."
- 初始化成功后进入等待状态,显示"IDLE"
- 连接一块锂电池(注意正负极),观察OLED电压读数变化
- 系统应自动检测电池并进入相应充电状态
7.2.2 预充电测试
使用电压低于3.0V的电池进行测试:
- 接入电池后,状态应显示"PRE-CHG"
- 充电电流应被限制在100mA左右
- 观察电池电压缓慢上升
- 电压达到3.0V后自动切换到恒流充电
7.2.3 恒流充电测试
- 接入电压在3.0V-4.2V之间的电池
- 状态显示"CC-CHG",电流维持在500mA(或设定值)
- 可以通过按键调整充电电流
- 观察PWM占空比随电池电压变化而自动调节
7.2.4 恒压充电测试
- 当电池电压接近4.2V时自动切换
- 状态显示"CV-CHG",电压维持在4.2V
- 观察充电电流逐渐减小
- 电流降至50mA以下时充电完成
7.2.5 保护功能测试
- 过压保护:使用可调电源模拟电池,将电压调至4.5V以上,系统应切断输出并报警
- 过流保护:使用大功率负载,系统检测到电流超过1.2A时切断输出
- 反接保护:反接电池,系统不输出电流(注意:硬件保护依赖SS34二极管)
7.3 校准指南
7.3.1 电压校准
如果INA226读取的电压与实际万用表测量值有偏差,可以在代码中进行软件校准:
c
/* 在charger.c的Charger_UpdateMeasurements函数中添加 */
float voltage_correction = 0.02f; /* 根据实际偏差调整 */
charger.battery_voltage = ina_data.bus_voltage + voltage_correction;
7.3.2 电流校准
c
/* 电流偏移校准 */
float current_offset = 0.005f; /* 零电流偏移 */
charger.charge_current = ina_data.current - current_offset;
if (charger.charge_current < 0.0f) charger.charge_current = 0.0f;
7.4 常见问题排查
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| OLED不显示 | I2C未初始化或接线错误 | 检查PB6/PB7连接,确认地址0x3C |
| 电压显示为0 | INA226通信失败 | 检查I2C接线,确认地址0x40 |
| PWM无输出 | 定时器未正确启动 | 检查PA1引脚配置和TIM2初始化 |
| 电流读数不准 | 校准值不正确 | 重新计算校准值或更换采样电阻 |
| 按键无反应 | 引脚配置错误 | 检查PB10-PB12的内部上拉设置 |
| 程序卡死 | 硬件故障触发HardFault | 检查所有外设初始化是否成功 |
八、扩展与优化建议
8.1 功能扩展方向
- 温度监测:添加NTC热敏电阻或DS18B20温度传感器,实现过温保护
- 数据记录:通过串口将充电数据发送到上位机,绘制充电曲线
- 多槽充电:利用多路ADC和PWM,实现多节电池独立充电
- 电池内阻测量:通过瞬间加载测量电池内阻,评估电池健康状态
- 蓝牙监控:添加HC-05蓝牙模块,实现手机APP远程监控
8.2 性能优化建议
- FreeRTOS移植:将充电任务、显示任务、通信任务分离到不同线程
- DMA传输:使用DMA进行I2C数据读取,减少CPU占用
- 双缓冲显示:使用双显存缓冲区,避免显示撕裂
- 数字滤波器:对ADC采样数据使用滑动平均或卡尔曼滤波
九、总结
本教程详细介绍了基于STM32F103C8T6的智能充电器完整开发过程,涵盖硬件设计、外设驱动、充电算法和系统集成。通过本项目的实践,可以掌握以下核心技能:
- STM32CubeMX图形化工程配置方法
- I2C总线设备驱动开发(INA226、OLED)
- 定时器PWM输出控制
- 状态机程序设计模式
- PID闭环控制算法实现
- 嵌入式系统保护机制设计
按照步骤操作即可成功构建一个实用的智能充电器。希望本教程能够帮助你深入理解STM32嵌入式开发,并在此基础上开发更多有趣的项目。