叠个甲:代码有点乱,硬件设计小菜鸡,用stm32g431只是为了学习(可以用便宜单片机)
方案设计 软件链接:电能表: STM32G431主控、TI的INA226采样。能够在电压5V-30V、电流0~6A的范围内,准确显示电压、电流数值,并保持误差不高于0.5%。UI界面、电能累计、短信通知、蓝牙、FDCAN和PD诱骗。 (gitee.com) B站介绍视频:https://www.bilibili.com/video/BV1bv411F7vi/?share_source=copy_web&vd_source=202523ed8844f5cefcdcbca3e33b33c7
基础功能: (1)接入电能表的电压 24V,电流 2A,电能表可显示电压、电流数值,误差不高于 0.5%。 (2)接入任意电压(5V~24V),电流(0~5A),电能表可正确显示电压、电流数值,误差不高于 1%。 (3)接入任意电压(5V~24V),电流(0~5A),电能表可正常显示功率值,误差不高于 1%。 拓展功能: (1)具备电能累计功能,并可正常显示。可以显示mAh和mWh (2)具备短信通知功能,可将测量数值和电能通过SIM900A模块,以短信的形式发送到手机中(支持定时发送和触发发送两种模式) (3)具有蓝牙通讯接口,手机端能够通过蓝牙连接实时获取电能表的数据。 (4)具有完整的UI交互界面,可以通过按键实时交互,并控制电能表各个控件的状态。 (5)支持PD诱骗协议,能通过c-c线选择诱骗电压5V,9V,15V,20V,支持多种接口输入和输出,包括XT30,typec接口,USB接口等。 (6)具有纹波测量功能,可测量直流电源的纹波电压,可以显示mV。
整体方案:
主控MCU选用STM的高性价比STM32G431CBT6,电压和电流采样模块选用TI的INA226模块,显示模块选用中景圆下接FPC的0.91寸SPI接口OLED,蓝牙模块选用海凌科的BLE5.0,FDCAN模块选用芯特力的SIT65HVD230DR,PD诱骗芯片为沁恒CH224K,按键选择三档拨轮开关,供电输入和输出接口选用12pin的typeC、5A耐流的usb和XT30UPB-M接口,主控供电选用南京微盟的ME3116经过DC-DC降压至5V后再使用ME6211进行LDO降压至3.3V后给主控供电,板子上有两颗用于debug的LED指示灯和蜂鸣器。 方案分析:
在设计电能表的硬件方案时,我们的目标是选择性能稳定、功耗低、成本合理的组件,以确保电能表的准确度、可靠性和用户友好性。 (1)主控MCU:STM32G431CBT6 性能与成本比较:STM32G431CBT6是STMicroelectronics的一款高性价比的STM32系列微控制器,具有足够的性能来处理电能测量和通信任务,同时在成本方面相对较低。广泛的生态系统:STM32系列在开发者社区和相关工具方面有着强大的支持,有助于简化开发流程,提高开发效率。 稳定性:STM32系列以其稳定性和广泛的生态系统而闻名,能够满足电能表的实时性要求。 (2)电压和电流采样模块:INA226 高精度:TI的INA226模块提供了高精度的电压和电流测量,能够满足电能表对准确度的要求。通用性:INA226具有通用性,适用于不同电压和电流范围的测量,保证了电能表在不同工作环境下的灵活性。I2C接口:INA226采用I2C接口,与STM32G431CBT6主控MCU兼容性良好,方便集成和通信。 (3)显示模块:0.91寸SPI接口OLED 中景圆下接FPC:中景圆的0.91寸SPI接口OLED是一款小巧且高分辨率的显示器,适用于电能表的紧凑设计。SPI接口:使用SPI接口实现与主控的高速数据传输,确保显示的实时性和清晰度。低功耗:OLED在显示时仅点亮需要的像素,相比传统液晶显示器,具有更低的功耗。 (4)蓝牙模块:BLE5.0 海凌科BLE5.0:选择海凌科的BLE5.0模块,提供了稳定的蓝牙通信连接,适用于与手机端的实时数据传输。低功耗:BLE5.0在功耗方面有显著的优势,有助于延长电能表的使用寿命。广泛兼容性: BLE5.0兼容性良好,可以与大多数现代智能手机和设备无缝连接。 (5)FDCAN模块:SIT65HVD230DR 芯特力SIT65HVD230DR:作为FDCAN模块的选择,芯特力的产品在工业通信领域有良好的口碑,确保电能表的可靠通信。高速通信:FDCAN支持高速数据传输,适用于需要快速响应的电能表应用。适用于工业环境:SIT65HVD230DR设计用于工业应用,具有较强的抗干扰能力,确保在复杂电磁环境下仍能保持可靠通信。 (6)PD诱骗芯片:CH224K 沁恒CH224K: PD诱骗芯片的选择考虑到其对不同电压的支持,满足电能表对充电协议的多样性需求。可以通过选择诱骗电阻实现5V、9V、12V、15V和20V电压诱骗。稳定性和灵活性: CH224K的性能和灵活性使其成为实现充电控制的可靠选择。 (7)按键选择:三档拨轮开关 直观控制:三档拨轮开关提供了用户友好的直观控制方式,简化了用户与电能表的交互。机械结构:机械式拨轮开关相对于电容触摸等技术更为稳定,避免了误操作和灵敏度不足的问题。 (8)供电输入和输出接口:typeC、USB、XT30UPB-M 接口多样性:选择12pin的typeC、5A耐流的USB和XT30UPB-M接口,确保了电能表在不同场景下的适用性。耐流性能:USB和XT30UPB-M接口的耐流性能适用于电能表在高电流环境下的可靠供电和输出。 (9)电源供电设计:ME3116 + ME6211 南京微盟的ME3116:ME3116作为DC-DC降压模块,有效地将电能表的输入电压降至合适的水平。ME6211进行LDO降压:ME6211用于LDO降压至3.3V,以给主控供电,保证了系统的稳定性。 (10) 调试和指示模块:LED指示灯和蜂鸣器 可视化调试:LED指示灯提供了可视化的调试方式,有助于迅速定位问题。 蜂鸣器:蜂鸣器用于提供音频反馈,增强用户对电能表工作状态的感知。 通过以上方案的综合比较与选择,我们确保了电能表在硬件设计上的稳定性、性能和灵活性。这一设计不仅满足基础功能的要求,还引入了多项拓展功能,使电能表更加智能、便捷,并提供了广泛的应用场景。
理论分析和计算 接口选型 由于接口需要有24V的电压耐压值和6A电流耐流值,因此选择拥有4A耐流的XKB(星坤)生产的USB接口、拥有5A耐流值首韩生产的16PIN Type-C、拥有500V耐压值和15A耐流值的AMASS(艾迈斯)生产的公头XT30UPB-M+顶层和底层同时铺铜+开窗堆锡处理以实现走大电流和高电压的能力。 开窗堆锡主要目的是: (1)开窗堆锡能够提高导线的横截面积,从而降低电流密度,减少线路的电阻和热量产生。这对于需要传输大电流的电路,如功率电子器件和电源电路,可以有效减少热损耗,防止线路过热。 (2)堆锡处理可以提高导线的导电性能,降低线路的电阻。这对于需要承受大电流的电路来说,有助于减小电压降,提高整个电路的效率。 (3)通过通过增加绝缘层的厚度,提高电路之间的绝缘性能。对于高电压电路,这样的处理可以减小线路之间的电场强度,降低击穿的风险,确保电路的稳定运行。 一般线路板厂家以OZ表示铜箔厚度,1OZ的厚度表示将1OZ重量的铜均匀铺在1平方英尺面积上达到的铜箔厚度,约为0.035mm。所以35um,50um,70um,对应的以oz为计量单位的厚度为1OZ,1.5OZ,2OZ。 为降低PCB板生产成本,本项目采用的是1OZ铜厚的四层板,线宽为1.900mm,由上表可知1.9mm线宽大约能走3.8A左右电流,加上外部2mm宽度左右的堆锡处理和PCB多网路增加电流(正反双面均布同样线路),电流理论上能远超6A。 采样电阻选型 为满足上三式对电流采样电阻功率等限制,此处电阻选择封装为2512、阻值为10mΩ、功率为3W、精度误差为1%的封体合金采样电阻,根据P=IIR,当电阻上流经的电流为6A时,功率P为0.36W,在电阻的功率限制3W范围内。 电流采样选用的芯片为TI公司的INA226,INA226是一款分流/功率监视器,具有IIC或SMBUS兼容接口。该器件监视分流压降和总线电源电压。可编程校准值、转换时间和取平均值功能与内部乘法器相结合,可实现电流值(单位为安培)和功率值(单位为瓦)的直接读取。INA226可在0V至36V的共模总线电压范围内感测电流,与电源电压无关。该器件由一个2.7V至5.5V的单电源供电,电源电流典型值为330uA。该器件的额定工作温度范围为-40°℃至125°c,IIC兼容接口上具有多达16个可编程地址。 INA226具有 I2C 接口的单芯片解决方案支持电流、电压以及电源测量,无需外部多路复用器或 ADC 即可简化电路板设计,应对布局局限性。INA226具有以下优点: (1)具有 10 uV 最大失调电压与 0.1% 最大增益误差,不但可在低电流下实现更高精度,而且还可使用更小的分流电阻器,从而可降低 I*R 损耗。 (2)独立可编程转换时间与采样平均技术不但可简化每个系统的速度需求定制,而且还可降低对软件及存储器的需求。 (3)420 uA 最大静态电流与 2 uA 最大关断电流支持高效工作,即便 INA226 集成 ADC 与电源多路复用器,功耗也比同类竞争产品的分立式解决方案低 3.5%。 (4)140 dB 共模抑制比 (CMRR) 与 36 V 共模电压 (CMV) 可在整个工作范围内确保变化极小或根本无变化的失调电压,从而可简化误差分析。 供电电压选择 ME3116 是内部集成了 MOSFET 的异步整流降压稳压器,有宽输入电压范围内(4.75V-40V1A 的负载能力。ME3116 系统采用 PWM 控制模式,具有很好的瞬态响和逐周期限流功能。同时,在轻载条件下系统自动切换到 PF模式,保证较高的轻载效率。ME3116 内置功率管具有较低的 RDSON (典型值0.9Ω),典型情况下效率高达 90% 。它内置了软启动和环补偿,以及550K 固定工作频率,保证了产品性能的同时也幅度降低了产品应用所需的外围器件ME3116 还具有过热关断、输入欠压保护、 BS 欠压保和短路保护。
电路与程序设计 采样电阻 INA226是一款由德州仪器生产的高精度电流/电压/功率监测器芯片,广泛用于电源管理系统中。在设计INA226采样电路时,需要考虑以下几个关键因素: (1)采样电阻: 采样电阻的选择与计算在理论计算与分析部分,阻值会直接影响测量的精度和范围。此外,还应选择低温度系数的电阻可以减少温度变化对测量精度的影响。 (2)布局: 接口:采样电阻应尽可能靠近INA226,并且其地线应该短且粗,以减少电阻和INA226之间的电压降。 走线:INA226的Kelvin(四线)连接方式可以减少走线电阻对测量的影响。 (3)滤波: 电源线:在INA226的电源引脚上加入旁路电容可以减少电源噪声对测量的影响。 差分信号线:在采样电阻两端的差分信号线上加入滤波电容可以减少高频噪声。 (4)量程选择: INA226可以通过编程设置不同的量程,以适应不同的测量需求。需要根据实际应用中的最大电流和电压来选择合适的量程。 下面是ina226_diff.h文件中定义的一些宏,主要是和硬件联系在一起,根据INA226 datasheet的描述定义了如下宏,方便后续根据项目需求快速修改。其中ina226 IIC器件地址为0x80(因为A0和A1两个地址位下拉接地,修改A0和A1连接方式即可改变地址) #define MODE_INA226 0X456F //0100_010_101_101_111 //16次平均,2.116ms,2.116ms,连续测量分流电压和总线电压
#define CFG_REG 0X00 //配置寄存器
#define SV_REG 0X01 //分流电压
#define BV_REG 0X02 //总线电压
#define PWR_REG 0X03 //电源功率
#define CUR_REG 0X04 //电流
#define CAL_REG 0X05 //校准,设定满量程范围以及电流和功率测数的
#define ONFF_REG 0X06 //屏蔽 使能 警报配置和转换准备就绪
#define AL_REG 0X07 //包含与所选警报功能相比较的限定值
#define INA226_GET_ADDR 0XFF //包含唯一的芯片标识号
#define INA226_ADDR1 0x80 //ina226地址
#define INA226_GETALADDR 0X14
//定义配置数据
#define R_SHUNT 10 //采样电阻 10mR (+-) 1% Rshunt是外部分流器——两引脚间电阻的值
#define INA226_VAL_LSB 2.5f //分流电压 LSB 2.5uV
#define Voltage_LSB 1.25f //总线电压 LSB 1.25mV
#define CURRENT_LSB 1.0f //电流LSB 1mA =Maximum Expected Current / 2^15
#define POWER_LSB 25*CURRENT_LSB
#define CAL 512 //0.00512*1000/(Current_LSB*R_SHUNT) = 512 //电流偏大改小 =0.00512/(CURRENT_LSB*R_SHUNT)
使用如下结构体,方便外围程序进行相应的数据调用 typedef struct
{
bool mode; //模式
float voltageVal; //mV
float Shunt_voltage; //uV
float Shunt_Current; //mA
float Power_Val; //功率
float Power; //功率mW
float voltage; //V
float current; //A
float power; //W
float QUA_mAh; //电量
float QUA_mWh; //功率
float QUA_time; //电量计算周期(ms)
uint32_t ina226_id;
bool valid;
bool rectify_valid; //校准数据有效标志
} INA226;
其他一些函数可以去附件的工程中进行查看 通讯电路: (1)FDCAN数据发送和接收 SIT65HVD230 是一款应用于 CAN 协议控制器和物理总线之间的接口芯片,与具有 CAN 控制器 的 3.3V 微处理器、微控制器 (MCU) 和数字信号处理器 (DSP)或者等效协议控制器结合使用,应用 于工业自动化、控制、传感器和驱动系统,电机和机器人控制,楼宇和温度控制,电信和基站控制及 状态等领域。适用于采用符合 ISO 11898 标准的 CAN 串行通信物理层的应用。 FDCAN通讯电路中,使用120欧姆用于端对端通讯阻抗匹配,PESD用于防止静电,差分布线满足高速需求。 首先使用HAL库中的函数对FDCAN的GPIO进行初始化,初始化后再进行滤波器的配置:
void GPIO_FDCAN_Init(void) //FDCAN GPIO初始化
{
/*引脚初始化*/
GPIO_InitTypeDef GPIO_InitStruct = {0};
/*时钟初始化*/
RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_FDCAN;
PeriphClkInit.FdcanClockSelection = RCC_FDCANCLKSOURCE_PCLK1;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
{
Error_Handler();
}
/* FDCAN1 clock enable */
MYCURR_FDCAN_CLK_ENABLE();
FDCANTX_GPIO_CLK_ENABLE();
FDCANRX_GPIO_CLK_ENABLE();
/*FDCAN1 GPIO Configuration*/
GPIO_InitStruct.Pin = FDCANTX_GPIO_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
GPIO_InitStruct.Alternate = MYCURR_FDCAN_ALTERNATE;
HAL_GPIO_Init(FDCANTX_GPIO_Port, &GPIO_InitStruct);
GPIO_InitStruct.Pin = FDCANRX_GPIO_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
GPIO_InitStruct.Alternate = MYCURR_FDCAN_ALTERNATE;
HAL_GPIO_Init(FDCANRX_GPIO_Port, &GPIO_InitStruct);
/* FDCAN1 interrupt Init */
HAL_NVIC_SetPriority(FDCAN1_IT0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(FDCAN1_IT0_IRQn);
//FDCAN1初始化
hfdcan1.Instance = MYCURR_FDCAN;
hfdcan1.Init.ClockDivider = FDCAN_CLOCK_DIV1; //时钟分频器
hfdcan1.Init.FrameFormat = FDCAN_FRAME_CLASSIC; //用于设置CAN帧格式
hfdcan1.Init.Mode = FDCAN_MODE_NORMAL; //用于设置CAN操作模式
hfdcan1.Init.AutoRetransmission = DISABLE; //关闭自动重传!
hfdcan1.Init.TransmitPause = DISABLE; //关闭传输暂停
hfdcan1.Init.ProtocolException = DISABLE; //关闭协议异常处理
hfdcan1.Init.NominalPrescaler = 20; //用于CAN FD仲裁阶段分频设置,产生标称位时间量,参数范围1-512
hfdcan1.Init.NominalSyncJumpWidth = 1; //设置FD CAN仲裁阶段最大支持的时间量来加长或者缩短一个bit来实现再同步,参数范围1-128
hfdcan1.Init.NominalTimeSeg1 = 10; //设置仲裁阶段Bit Segment 1的时间量,范围2–256
hfdcan1.Init.NominalTimeSeg2 = 5; //设置仲裁阶段Bit Segment 2的时间量,范围2–128
hfdcan1.Init.DataPrescaler = 20; //用于CAN FD数据阶段分频设置,范围1-32
hfdcan1.Init.DataSyncJumpWidth = 1; //设置FD CAN数据阶段最大支持的时间量来加长或者缩短一个bit来实现数据再同步,参数范围1-16
hfdcan1.Init.DataTimeSeg1 = 10; //设置数据阶段Data Bit Segment 1的时间量,范围1–32
hfdcan1.Init.DataTimeSeg2 = 5; //设置数据阶段Data Bit Segment 2的时间量,范围1–16
hfdcan1.Init.StdFiltersNbr = 1; //标准信息ID滤波器个数
hfdcan1.Init.ExtFiltersNbr = 1; //扩展信息ID滤波器个数
hfdcan1.Init.TxFifoQueueMode = FDCAN_TX_FIFO_OPERATION; //发送FIFO序列模式
if(HAL_FDCAN_Init(&MYCURR_FDCAN_GET_CAN) != HAL_OK) //初始化FDCAN
{
Error_Handler();
}
//配置数据帧滤波器
FDCAN_FilterTypeDef sFilterConfig;
sFilterConfig.IdType = FDCAN_STANDARD_ID; //设置标准ID或者扩展ID
sFilterConfig.FilterIndex = 0; //用于过滤索引,如果是标准ID,范围0到127。如果是扩展ID,范围0到64
sFilterConfig.FilterType = FDCAN_FILTER_RANGE; //过滤器采样 Range filter from FilterID1 to FilterID2
sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO0; //如果过滤匹配,将数据保存到Rx FIFO 0
sFilterConfig.FilterID1 = 0x0000; //32位ID
sFilterConfig.FilterID2 = 0x0000; //如果FDCAN配置为传统模式的话,这里是32位掩码
if(HAL_FDCAN_ConfigFilter(&MYCURR_FDCAN_GET_CAN, &sFilterConfig)!= HAL_OK)
{
Error_Handler();
}
HAL_FDCAN_Start(&MYCURR_FDCAN_GET_CAN); //开启FDCAN
HAL_FDCAN_ActivateNotification(&MYCURR_FDCAN_GET_CAN, FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0);
}
初始化后,定义以下两个函数进行FDCAN数据的发送和接收: /*----------------- FDCAN -----------------*/
//can发送一组数据(固定格式:ID为0X12,标准帧,数据帧)
//len:数据长度(最大为8),可设置为FDCAN_DLC_BYTES_2~FDCAN_DLC_BYTES_8
//msg:数据指针,最大为8个字节.
//返回值:0,成功;
// 其他,失败;
uint8_t FDCAN1_Send_Msg(uint8_t* msg)
{
curr_fdcan1.Curr_txheader.Identifier = 0x001;
curr_fdcan1.Curr_txheader.IdType = FDCAN_STANDARD_ID;
curr_fdcan1.Curr_txheader.TxFrameType = FDCAN_DATA_FRAME;
curr_fdcan1.Curr_txheader.DataLength = FDCAN_DLC_BYTES_8;
curr_fdcan1.Curr_txheader.ErrorStateIndicator = FDCAN_ESI_ACTIVE;
curr_fdcan1.Curr_txheader.BitRateSwitch = FDCAN_BRS_OFF;
curr_fdcan1.Curr_txheader.FDFormat = FDCAN_CLASSIC_CAN;
curr_fdcan1.Curr_txheader.TxEventFifoControl = FDCAN_NO_TX_EVENTS;
curr_fdcan1.Curr_txheader.MessageMarker = 0x52; //由于网上借鉴该函数,我也不太明白为什么是0x52,不过我测试改成0好像也没问题
if(HAL_FDCAN_AddMessageToTxFifoQ(&hfdcan1,&curr_fdcan1.Curr_txheader,msg)!=HAL_OK) return 1;//发送
return 0;
}
//can口接收数据查询
//buf:数据缓存区;
//返回值:0,无数据被收到;
//其他,接收的数据长度;
uint8_t FDCAN1_Receive_Msg(uint8_t *buf, uint16_t *Identifier,uint16_t *len)
{
if(HAL_FDCAN_GetRxMessage(&hfdcan1,FDCAN_RX_FIFO0,&curr_fdcan1.Curr_rxheader,buf)!=HAL_OK)return 0;//接收数据
*Identifier = curr_fdcan1.Curr_rxheader.Identifier;
*len=curr_fdcan1.Curr_rxheader.DataLength>>16;
return curr_fdcan1.Curr_rxheader.DataLength>>16;
}
FDCAN的具体发送和接收的业务逻辑还没写,可以自行进行扩展。 (2)USART数据发送和接收 串口有两个引出,一个连接在HLK-B25蓝牙模块上(用于手机端蓝牙数据接收,可以正常收发),另外一个引出未使用,可以用于外挂短信模块SIM900A,目前有一些小BUG(DMA发送时串口有概率卡死或者异常)
串口使用DMA的形式进行发送,空闲中断+DMA进行不定长数据的接收(有BUG慎用),引脚初始化和波特率配置: void GPIO_USART_Init(void) //USART GPIO初始化
{
/* DMA controller clock enable */
__HAL_RCC_DMAMUX1_CLK_ENABLE();
__HAL_RCC_DMA1_CLK_ENABLE();
/* DMA interrupt init */
/* DMA1_Channel1_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
/* DMA1_Channel2_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA1_Channel2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel2_IRQn);
/* DMA1_Channel3_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA1_Channel3_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel3_IRQn);
/* DMA1_Channel4_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* USART2 clock enable */
USART2_COM_USART_CLK_ENABLE();
USART2_COM_TX_GPIO_CLK_ENABLE();
USART2_COM_RX_GPIO_CLK_ENABLE();
/**USART2 GPIO Configuration
PA2 ------> USART2_TX
PA3 ------> USART2_RX
*/
GPIO_InitStruct.Pin = USART2_COM_TX_GPIO_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART2;
HAL_GPIO_Init(USART2_COM_TX_GPIO_Port, &GPIO_InitStruct);
GPIO_InitStruct.Pin = USART2_COM_RX_GPIO_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART2;
HAL_GPIO_Init(USART2_COM_RX_GPIO_Port, &GPIO_InitStruct);
/* USART2 interrupt Init */
HAL_NVIC_SetPriority(USART2_COM_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(USART2_COM_IRQn);
/* USART3 clock enable */
USART3_BLE_USART_CLK_ENABLE();
USART3_BLE_TX_GPIO_CLK_ENABLE();
USART3_BLE_RX_GPIO_CLK_ENABLE();
/**USART3 GPIO Configuration
PB10 ------> USART3_TX
PB11 ------> USART3_RX
*/
GPIO_InitStruct.Pin = USART3_BLE_TX_GPIO_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART3;
HAL_GPIO_Init(USART3_BLE_TX_GPIO_Port, &GPIO_InitStruct);
GPIO_InitStruct.Pin = USART3_BLE_RX_GPIO_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART3;
HAL_GPIO_Init(USART3_BLE_RX_GPIO_Port, &GPIO_InitStruct);
/* USART3 interrupt Init */
HAL_NVIC_SetPriority(USART3_BLE_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(USART3_BLE_IRQn);
/* USART2_TX Init */
USART2_COM_TX_HDMA.Instance = USART2_COM_TX_DMA_CH;
USART2_COM_TX_HDMA.Init.Request = DMA_REQUEST_USART2_TX;
USART2_COM_TX_HDMA.Init.Direction = DMA_MEMORY_TO_PERIPH;
USART2_COM_TX_HDMA.Init.PeriphInc = DMA_PINC_DISABLE;
USART2_COM_TX_HDMA.Init.MemInc = DMA_MINC_ENABLE;
USART2_COM_TX_HDMA.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
USART2_COM_TX_HDMA.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
USART2_COM_TX_HDMA.Init.Mode = DMA_CIRCULAR;
USART2_COM_TX_HDMA.Init.Priority = DMA_PRIORITY_VERY_HIGH;
if (HAL_DMA_Init(&USART2_COM_TX_HDMA) != HAL_OK)
{
Error_Handler();
}
__HAL_LINKDMA(&USART2_SIM_Get_HUART,hdmatx,USART2_COM_TX_HDMA);
/* USART2 interrupt Init */
HAL_NVIC_SetPriority(USART2_COM_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(USART2_COM_IRQn);
/* USART3 DMA Init */
USART3_BLE_TX_HDMA.Instance = USART3_BLE_TX_DMA_CH;
USART3_BLE_TX_HDMA.Init.Request = DMA_REQUEST_USART3_TX;
USART3_BLE_TX_HDMA.Init.Direction = DMA_MEMORY_TO_PERIPH;
USART3_BLE_TX_HDMA.Init.PeriphInc = DMA_PINC_DISABLE;
USART3_BLE_TX_HDMA.Init.MemInc = DMA_MINC_ENABLE;
USART3_BLE_TX_HDMA.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
USART3_BLE_TX_HDMA.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
USART3_BLE_TX_HDMA.Init.Mode = DMA_CIRCULAR;
USART3_BLE_TX_HDMA.Init.Priority = DMA_PRIORITY_LOW;
if (HAL_DMA_Init(&USART3_BLE_TX_HDMA) != HAL_OK)
{
Error_Handler();
}
__HAL_LINKDMA(&USART3_BLE_Get_HUART,hdmatx,USART3_BLE_TX_HDMA);
/* USART3_RX Init */
USART3_BLE_RX_HDMA.Instance = USART3_BLE_RX_DMA_CH;
USART3_BLE_RX_HDMA.Init.Request = DMA_REQUEST_USART3_RX;
USART3_BLE_RX_HDMA.Init.Direction = DMA_PERIPH_TO_MEMORY;
USART3_BLE_RX_HDMA.Init.PeriphInc = DMA_PINC_DISABLE;
USART3_BLE_RX_HDMA.Init.MemInc = DMA_MINC_ENABLE;
USART3_BLE_RX_HDMA.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
USART3_BLE_RX_HDMA.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
USART3_BLE_RX_HDMA.Init.Mode = DMA_CIRCULAR;
USART3_BLE_RX_HDMA.Init.Priority = DMA_PRIORITY_LOW;
if (HAL_DMA_Init(&USART3_BLE_RX_HDMA) != HAL_OK)
{
Error_Handler();
}
__HAL_LINKDMA(&USART3_BLE_Get_HUART,hdmatx,USART3_BLE_RX_HDMA);
}
串口进行DMA形式的数据发送:
//串口1的DMA发送
void USART2_COM_DMA_Send(uint8_t *buffer, uint16_t length)
{
//等待上一次的数据发送完毕
while(HAL_DMA_GetState(&hdma_usart2_tx) != HAL_DMA_STATE_READY)
{};
//while(__HAL_DMA_GET_COUNTER(&hdma_usart1_tx));
//关闭DMA
__HAL_DMA_DISABLE(&hdma_usart2_tx);
//开始发送数据
HAL_UART_Transmit_DMA(&huart2, buffer, length);
}
void USART3_BLE_DMA_Send(uint8_t *buffer, uint16_t length)
{
//等待上一次的数据发送完毕
while(HAL_DMA_GetState(&hdma_usart3_tx) != HAL_DMA_STATE_READY)
{};
//while(__HAL_DMA_GET_COUNTER(&hdma_usart1_tx));
//关闭DMA
__HAL_DMA_DISABLE(&hdma_usart3_tx);
//开始发送数据
HAL_UART_Transmit_DMA(&huart3, buffer, length);
}
//串口2的DMA发送printf
void Debug2_SIM_printf(const char *format, ...)
{
uint32_t length = 0;
va_list args;
__va_start(args, format);
length = vsnprintf((char*)curr_usart.Curr_sim_txdata, sizeof(curr_usart.Curr_sim_txdata), (char*)format, args);
USART2_COM_DMA_Send(curr_usart.Curr_sim_txdata, length);
}
void Debug3_BLE_printf(const char *format, ...)
{
uint32_t length = 0;
va_list args;
__va_start(args, format);
length = vsnprintf((char*)curr_usart.Curr_ble_txdata, sizeof(curr_usart.Curr_ble_txdata), (char*)format, args);
USART3_BLE_DMA_Send(curr_usart.Curr_ble_txdata, length);
}
手机端蓝牙数据显示:
显示电路: 选用中景圆15pin FPC下接的0.91寸SPI OLED,三线SPI通讯协议被广泛用于驱动OLED等设备,主要原因是其简洁性和高效性。三线SPI通信只需要使用三条线:串行时钟线(SCK)、主机输出/从机输入数据线MOSI和低电平有效的从机选择线C/S。这种连接方式减少了需要接插的线路数量,降低了硬件的复杂性,提高了系统的稳定性。 在三线SPI通讯中,各线路的功能如下: - 串行时钟线(SCK):提供了同步的时钟信号,控制数据的传输速率。 - 主机输出/从机输入数据线MOSI:由主设备输出数据到从设备。 - 低电平有效的从机选择线C/S:这是一个控制线,由主设备发出低电平信号来激活指定的从设备。 三线SPI通讯协议相较于其他协议如I2C,有其独特的优势。其中一个显著优点是其通信速度快,具体可以达到1M以上。这种高速度对于显示刷新等需要快速响应的应用非常重要。另一个优势是三线SPI只需要使用三条线:串行时钟线(SCK)、主机输出/从机输入数据线MOSI和低电平有效的从机选择线C/S。这样的设计减少了接线数量,降低了硬件的复杂性,提高了系统的稳定性。 驱动方式为硬件SPI驱动SSD1306(较为常见,就不再赘述),一些宏和功能函数:
#define Quantity_Row 128 //行
#define Quantity_Column 32 //列
//息屏时间配置
#define Screen_Sleep_Time 30000 //蜂鸣器长响 ms
typedef struct{
bool sleep_status; //息屏状态
uint32_t sleep_time; //息屏时间
uint32_t light; //背光强度
}Screen_Typedef;
//实例
extern uint8_t ssd1306_gram[(Quantity_Row) * (Quantity_Column/8)]; //GRAM空间
typedef enum{
CharFont_0806 = 0x01, //0806字符
CharFont_1206, //1206字符
CharFont_1608, //1608字符
CharFont_2412, //1608字符
}CharFont;
//读写操作
void SSD1306_Write_Data(uint8_t _data); //写数据
void SSD1306_Write_Comm(uint8_t _comm); //写指令
//基本指令操作
void SSD1306_Display_On(void); //屏幕显示开
void SSD1306_Display_Off(void); //屏幕显示关
void SSD1306_Set_Bright(uint8_t brightness); //设置屏幕亮度
void SSD1306_Color_Reversal(bool _switch); //颜色反转
void SSD1306_Orien_Reversal(bool _switch); //方向反转
//基本数据操作
void SSD1306_Refresh(uint8_t *_gram); //更新显示
//读写GRAM空间操作
void GRAM_Clear( uint8_t *_gram ); //GRAM清空
void GRAM_DrawPoint( uint8_t *_gram, uint8_t _x, uint8_t _y, bool _switch); //GRAM打点
void GRAM_DrawLine( uint8_t *_gram, uint8_t _x, uint8_t _y, uint8_t _xe, uint8_t _ye, bool _switch); //GRAM打线
void GRAM_DrawCircle( uint8_t *_gram, uint8_t _x, uint8_t _y, uint8_t _radius, bool _switch); //GRAM打圆
void GRAM_ShowPicture( uint8_t *_gram, uint8_t _x, uint8_t _y, uint8_t _xs, uint8_t _ys, uint8_t* pic, bool _switch); //GRAM显示图片
void GRAM_ShowChar( uint8_t *_gram, uint8_t _x, uint8_t _y, uint8_t _char, CharFont _format, bool _switch); //GRAM显示字符
void GRAM_ShowString( uint8_t *_gram, uint8_t _x, uint8_t _y, uint8_t *_char, CharFont _format, bool _switch); //GRAM显示字符串
void GRAM_ShowNum_LU_Max10( uint8_t *_gram, uint8_t _x, uint8_t _y, uint32_t _num, uint8_t _width, CharFont _format, bool _switch); //GRAM显示LU型数字
void GRAM_ShowNum_LI_Max11( uint8_t *_gram, uint8_t _x, uint8_t _y, int32_t _num, uint8_t _width, CharFont _format, bool _switch); //GRAM显示LI型数字
void GRAM_ShowNum_Float_Max15(uint8_t *_gram, uint8_t _x, uint8_t _y, float _num, uint8_t _width, CharFont _format, bool _switch); //GRAM显示Float型数字
void GRAM_ShowNum_Float_Max4(uint8_t *_gram, uint8_t _x, uint8_t _y, float _num, uint8_t _width, CharFont _format, bool _switch); //GRAM显示Float型数字
//接口函数
void SSD1306_Init(void); //SSD1306初始化
主要是UI界面框架的设计,电能表整体的UI如下图:
使用双向链表进行UI框架的构建:
/******************** 通用数据 ********************/
/******************** 通用数据 ********************/
/******************** 通用数据 ********************/
/**
* UI数据包
**/
typedef struct{
uint32_t period_ms; //周期
uint32_t flicker_timer_100ms; //闪烁计时器
uint32_t flicker_timer_500ms; //闪烁计时器
bool flicker_flag_100ms; //闪烁标志
bool flicker_flag_500ms; //闪烁标志
}REINui_Data_Typedef;
/******************** 控件 ********************/
/******************** 控件 ********************/
/******************** 控件 ********************/
/**
* 基本控件
**/
typedef struct{
//用户辨识
uint8_t *name; //控件名称
uint8_t *icon_3232; //图标(32*32)
//拓扑
void *source_widget; //源控件指针(指向来源控件)
void *linked_widget; //链表指针(指向同级别下一个控件)
//绘制
//动态刷新
bool update_drawing; //绘制刷新
void (*drawing_function)(void* widget); //绘制函数
//高级功能包(由于C无法实现继承,故使用该方法实现在统一引用下定义不同数据)
void *feature_pack; //高级功能包
}REINui_Widget_Typedef;
/**
* 高级包(过度动画)
**/
typedef struct{
REINui_Widget_Typedef *begin_widget; //起始控件
REINui_Widget_Typedef *end_widget; //结束控件
uint32_t duration_ms; //过度时间
}REINui_Switches_Typedef;
/**
* 高级包(目录)
**/
typedef struct{
//项目指示
void *target_widget; //目标控件指针(指向列表的第一个单元)
uint32_t list_size; //目录列表大小
uint32_t pitch_number; //选中的项目编号(0开始)
//过程变量(无需全局)
REINui_Widget_Typedef *up_widget; //上项目
REINui_Widget_Typedef *middle_widget; //中项目
REINui_Widget_Typedef *down_widget; //下项目
}REINui_Directory_Typedef;
通过每20ms进行回调,查阅按键状态,然后进行UI界面的索引
/**
* [url=home.php?mod=space&uid=247401]@brief[/url] UI任务回调
* @param NULL
* @retval NULL
**/
void XDrive_REINui_Callback_ms(uint32_t _time)
{
//记录刷新周期
ui_data.period_ms = _time;
ui_data.flicker_timer_100ms += _time;
if(ui_data.flicker_timer_100ms > 100){
ui_data.flicker_timer_100ms = 0;
ui_data.flicker_flag_100ms = !ui_data.flicker_flag_100ms;
}
ui_data.flicker_timer_500ms += _time;
if(ui_data.flicker_timer_500ms > 500){
ui_data.flicker_timer_500ms = 0;
ui_data.flicker_flag_500ms = !ui_data.flicker_flag_500ms;
}
//按键扫描
Button_Scan_ms(_time);
//调用图形控件
if(ui_pitch){
ui_activated = ui_pitch;
ui_activated->drawing_function(ui_activated);
}
}
电压诱骗电路:
使用CH224K的PD诱骗芯片,用于电压诱骗。CH224K 单芯片集成 USB PD 等多种快充协议,支持 PD3.0/2.0,BC1.2 等升压快充协议,自动检测 VCONN 及模拟 E-Mark 芯片,最高支持 100W 功率,内置 PD 通讯模块,集成度高,外围精简。集成输出电 压检测功能,并且提供过温、过压保护等功能。
可以通过拨码开关选择诱骗出的电压大小(支持9V,12V,15V和20V电压档位调节)
测量结果
|