
简介这是基于STM32F103与ADE7763电能测量芯片的SPI通信工程源码面向嵌入式开发者和电能计量应用设计人员解决交流电流有效值读取与监测的问题。压缩包共20个文件全部为C语言源码含10个头文件与10个C文件整体仅26KB轻量易读。工程按模块划分清晰涵盖SPI底层驱动、ADE7763寄存器读写、电流有效值采样、LED/LCD显示以及定时器与外部中断等功能模块便于对照学习或直接移植。已有267人学习适合需要掌握STM32 SPI外设配置、ADE7763数据手册操作以及嵌入式C语言驱动开发的读者。通过阅读源码和模块组织可以快速理解电能计量芯片的通信流程与电流有效值计算思路为智能电表或电力监测项目提供可复用的代码基础。1. 一块 STM32 加 ADE7763把电流有效值读出来为什么容易翻车做电能监测项目时最常见的方案是 MCU 用 ADC 直接采样电压/电流波形再自己算有效值。这条路要兼顾采样率、滤波和标定代码越写越重。更省力的是把计量交给专用芯片ADE7763 内部完成 ADC、平方、平均、开方MCU 只需要通过 SPI 在合适的时机把寄存器里的 16 位结果搬回来。于是问题收敛成两个SPI 能不能稳定读写寄存器数据怎么换算成物理量。这里分享的 HARDWARE 驱动包正好把这两条路的代码都补齐了。这套驱动包针对 STM32F103 平台标准外设库工程组织里面 ade7763.c/h 提供寄存器操作spi.c 是硬件 SPImspi.c 是软件模拟 SPICurrSample 封装电流采样流程T12864、JLX12832 这类 LCD 驱动则负责把结果显示出来。如果你也在做电流检测、智能电表、机房配电监测或者只是想把 ADE7763 玩明白这份代码能省掉不少翻数据手册的时间。不过省事的前提是理解它为什么这么写。下面从芯片的测量链路说起再把我调的参数和排错过程过一遍最后给出几个能直接用上的抗干扰技巧。2. ADE7763 的测量链路与 SPI 通信边界2.1 有效值为什么在芯片内部算而不是靠软件ADE7763 内部有两条同步采样通道电流通道和电压通道。电流信号经过电流互感器或采样电阻进入芯片后先被可编程增益放大器PGA做量程匹配然后进入 24 位 ADC。注意这里不是逐次逼近型而是高精度 delta-sigma ADC过采样率很高所以对前端的抗混叠滤波要求没那么苛刻。ADC 后面是一串数字信号处理模块高通滤波器去掉直流偏置乘法器和低通滤波器组合出瞬时有功功率再经过平方、平均、开方得到电压有效值和电流有效值。这些运算全在芯片内部完成MCU 不需要保存整周波采样点。这种方案的本质是把模拟域难题转换成数字寄存器访问。MCU 要做的只是设置好 MODE 寄存器选择工作模式然后周期性地读 IRMS、VRMS、WATTHR 等寄存器。相比用 STM32 自带 ADC 连续采样一个工频周期比如 20ms 内采样 128 点再做 RMS专用芯片在动态范围和温漂上的优势很明显而且不影响 MCU 做别的任务。所以理解这套代码时不要先去抠 SPI 波形先分清哪些是芯片自己在算的哪些是 MCU 必须参与的。MCU 只负责初始化 SPI、初始化 ADE7763、按时读寄存器、把原始码换算成工程量。下面看 SPI 这部分。2.2 SPI 主模式配置与 ADE7763 的时序要求ADE7763 的 SPI 是从机STM32 做主模式通信以寄存器命令为单元。一次读操作通常是CS 拉低主机发送 8 位命令字节包含读写方向和寄存器地址然后主机继续输出时钟从机把 16 位寄存器数据分成两个字节移到 MISO 上主机读回最后 CS 拉高。写操作则是命令字节后跟着 16 位数据数据在 MOSI 上发出。这就引出了三个关键参数时钟极性 CPOL、时钟相位 CPHA、时钟频率。不同版本手册给出的时序图可能让人犹豫但我这个项目里用的组合是 CPOL 低电平空闲、CPHA 第一个边沿采样。如果读回来的寄存器值表现为全 0 或全 1或者第 2 个字节错位优先尝试 CPOL/CPHA 的另外三种组合同时用逻辑分析仪确认 CS 是否在字节边界上行动。时钟频率上STM32F103 的 SPI1 在 PCLK272MHz 时分频系数可选 2 到 256。ADE7763 的 SCLK 上限一般在 10MHz 量级所以 72MHz/16 4.5MHz 是很安全的起点。分频太大比如 256也能工作但每次读寄存器的时间变长配合显示刷新时可能出现肉眼可见的卡顿。2.3 驱动包的模块划分硬件 SPI 与软件 SPI 并存这个 HARDWARE 包里有两个 SPI 相关目录SPI 和 M_SPI。从命名看前者是 STM32 硬件 SPI1/SPI2 的驱动后者是软件模拟 SPImspi.c。为什么保留两套常见原因是硬件 SPI 明明初始化了却在某些板子上读回的数据不对可能是引脚复用错误、某个复用功能被其他外设抢占了也可能是从机时序要求特殊。这时用软件模拟 SPI 可以绕开硬件外设的隐藏状态先验证 ADE7763 本身是好的。M_SPI 的原理是使用普通 GPIO 拉 SCLK、MISO、MOSI、CS按位翻转 GPIO 来实现时序。它的优点是引脚任意、调试直观缺点是频率低、占 CPU。我在采样电流有效值这种低频读取场景里更喜欢初始化时用硬件 SPI失败时立刻切到 mspi.c 对比波形这也解释了为什么两个驱动都放进来了。寄存器地址方面不同 ADE7763 手册版本略有差异。我常用的几个关键寄存器整理如下实际使用前请和你的数据手册核对。寄存器名参考地址读写类型作用典型读取时机MODE0x09读/写选择采样模式、滤波、SPI 行为上电初始化IRMS0x1F只读电流有效值原始码每个采样节拍VRMS0x1E只读电压有效值原始码按需WATTHR0x0D只读有功电能累计定时或累计清0TIME0x0B只读内部时间基准校准用注意地址不是绝对的最稳妥的方式是打开 ade7763.h 看宏定义对照你所用手册的寄存器图。如果发现同一个寄存器你的芯片版本里没有千万别硬读否则只能得到 0xFFFF 之类的结果。3. 动手实现 STM32 SPI 驱动与 ADE7763 寄存器读写3.1 引脚分配从芯片引脚到 STM32 GPIO以 SPI1 为例常用的引脚映射是PA5-SCKPA6-MISOPA7-MOSI。片选 NSS 没有直接用硬件 NSS而是选 PA4 当作普通 GPIO 驱动称为软件片选。原因有两个ADE7763 的 CS 是低有效访问寄存器期间必须保持低电平硬件 NSS 在主模式下如果配置不当会被自动拉高造成时序截断另外后续如果板上接了多个 SPI 从机软件片选可以按业务逻辑自由选通。硬件连接上需要注意 ADE7763 的数字供电和 IO 电平。很多计量板用 3.3V 供电和 STM32F103 的 IO 电平一致可以直接连如果开发板给 ADE7763 提供的是 5V就要检查 MISO 上是否加电平转换或电阻分压。下面表格是我这边验证过的接法。STM32F103ADE7763说明PA5 (SPI1_SCK)SCLKSPI 时钟PA6 (SPI1_MISO)DOUT从机数据输出主机输入PA7 (SPI1_MOSI)DIN主机数据输出从机输入PA4 (GPIO)CS低有效片选软件控制GNDGND必须共地3.3VDVDD按芯片手册供电连接完成后先用万用表量一遍 CS 到 GND 的电压确认没有虚焊。SPI 问题里有相当大比例是杜邦线接触不良尤其是 MISO 上只有主机读时才有信号非常容易误判成软件问题。3.2 标准外设库下 SPI1 主模式初始化这个源码使用 STM32 标准外设库初始化时要同时打开 GPIOA 和 SPI1 的时钟把 SCK/MISO/MOSI 配成复用推挽输出CS 配成推挽输出。下面是我整理出的最小初始化函数。void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); /* SCK: PA5 MOSI: PA7 MISO: PA6 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); /* 软件片选PA4 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_4); /* SPI1 主机、8 位数据、MSB 先行 */ SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; // 空闲低电平 SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; // 第一个边沿采样 SPI_InitStructure.SPI_NSS SPI_NSS_Soft; // 禁止硬件 NSS SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_16; // 72/164.5MHz SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }这段代码里SPI_Direction_2Lines_FullDuplex是必须的全双工模式因为哪怕只是读操作主机也必须继续发出 SCKSCK 由发送空数据产生SPI_DataSize_8b对应 ADE7763 命令/数据的字节长度SPI_NSS_Soft让片选完全交给 GPIO 控制。SPI_BaudRatePrescaler可根据实测波形调整如果发现 MISO 采样点靠近跳变沿就应该降到 32 或 64保证数据稳定。初始化完成后可以做一个自检连续给 SPI1 发送 0xA5用逻辑分析仪观察 MOSI 波形确认 MSB 先出、时钟极性和目标一致。不要直接去读 ADE7763先确认主机这边波形干净。3.3 寄存器读函数命令字节、16 位数据与有效值换算ADE7763 的寄存器数据是 16 位SPI 传输时按高字节在前。读取函数的核心是先把 CS 拉低发出带读写方向的命令字节再连续读两个字节拼成 u16最后把 CS 拉高。以读命令为例#define ADE7763_READ (0x80) /* 读标志位具体位以手册为准 */ #define ADE7763_REG_IRMS (0x1F) /* 电流有效值寄存器地址 */ void ADE7763_CS_Low(void) { GPIO_ResetBits(GPIOA, GPIO_Pin_4); } void ADE7763_CS_High(void) { GPIO_SetBits(GPIOA, GPIO_Pin_4); } uint16_t ADE7763_ReadReg(uint8_t reg) { uint16_t value 0; ADE7763_CS_Low(); /* 先发命令字节读方向 寄存器地址 */ SPI1_ReadWriteByte(ADE7763_READ | (reg 0x7F)); /* 从高字节到低字节读取 16 位数据 */ value (uint16_t)SPI1_ReadWriteByte(0x00) 8; value | (uint16_t)SPI1_ReadWriteByte(0x00); ADE7763_CS_High(); return value; }SPI1_ReadWriteByte是标准外设库的SPI_I2S_SendData和SPI_I2S_ReceiveData的封装发送 0x00 的目的只是产生 SCK 时钟同时把 MISO 上移出的数据收回来。如果省略第二个字节的发送很多 SPI 外设在连续传输时不会继续输出时钟导致低位数据丢失。拿到 16 位原始码后电流有效值的物理量换算需要结合外部互感器或采样电阻的缩比。更通用的做法是在写代码时保留比例因子#define CURRENT_SCALE (0.001f) /* 每 LSB 对应的电流单位 A需实际校准 */ float CurrSample_GetIRMS_Amps(void) { uint16_t raw ADE7763_ReadReg(ADE7763_REG_IRMS); return (float)raw * CURRENT_SCALE; }CURRENT_SCALE怎么定根据 ADE7763 的满量程输入和电流互感器变比计算或者用一个已知有效值的恒流源整体标定。项目源码里 CurrSample.c 的注释一般会写明这个系数的来源如果没有建议先用万用表比对几个点的实际电流和 ADC 原始码做线性拟合。不要跳过这一步否则显示出的电流值只能看趋势不能作为计量依据。3.4 写寄存器与上电自检初始化阶段必然涉及 MODE 等寄存器的写入。写函数的时序和读函数类似只是命令字节不带读标志并且随后发送的是 16 位数据。void ADE7763_WriteReg(uint8_t reg, uint16_t value) { ADE7763_CS_Low(); SPI1_ReadWriteByte(reg 0x7F); /* 写命令 */ SPI1_ReadWriteByte((uint8_t)(value 8)); /* 高字节 */ SPI1_ReadWriteByte((uint8_t)(value 0xFF)); /* 低字节 */ ADE7763_CS_High(); }写完寄存器后最好的验证方式是立刻读回。比如写完 MODE 寄存器可以先从 ade7763.h 里拿到期望值再用ADE7763_ReadReg比对不一致就在串口打印错误而不是继续悄悄跑。这样能够把硬件问题暴露在初始化阶段避免后面读电流时出现一串看似正常但其实是乱码的数据。4. 定时器节拍、LCD 显示与 SPI 联调实战4.1 用定时中断驱动电流采样任务电流有效值不需要每个峰值时刻都去读通常 100ms 到 500ms 刷新一次就够了。TimerXX 模块在这个工程里承担的就是节拍作用。比如配置 TIM2 每 200ms 进入一次中断在中断里置一个标志主循环看到标志后调用 CurrSample 读取函数。volatile uint8_t g_irmsFlag 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); g_irmsFlag 1; } } int main(void) { /* ... 初始化 SPI、ADE7763、LCD、TIM2 ... */ while (1) { if (g_irmsFlag) { g_irmsFlag 0; float current CurrSample_GetIRMS_Amps(); LCD_ShowCurrent(current); } } }这里的关键是不要在中断里执行 SPI 传输因为 ADE7763 的寄存器读取要维持 CS 低电平至少 3 字节时间如果中途被更高优先级中断打断片选时序会乱。把工作放到主循环即使读取稍慢也不会破坏时序。CurrSample_GetIRMS_Amps内部还会对多次读值做平均这样单次毛刺不会直接反映到显示上。定时器分频和重装载值取决于你的时钟树。例如 72MHz 时钟PSC7199ARR199就是 72MHz/(7200*200)50Hz正好 20ms想刷成 200ms就把 ARR 改成 1999。工程里 TimerXX.c 里应该已经写好了参数换到其他频率的板子时按这个公式反推即可。4.2 在 LCD12864 和 JLX12832 上显示电流值T12864 和 JLX12832 是两个常见的 LCD 型号前者是 128x64 点阵后者是 128x32 点阵。它们在初始化命令上有差异但显示字符串的接口可以统一。这里以 JLX12832 为例把换算后的电流值格式化成字符串再显示。void LCD_ShowCurrent(float current) { char str[16]; if (current 0) { current 0; } if (current 10.0f) { sprintf(str, I %.2f A, current); } else { sprintf(str, I %.1f A, current); } LCD_ShowString(8, 2, str); }LCD_ShowString的第二个参数是行号需要根据 LCD 驱动的字库行高调整。12832 通常只有 4 行把电流放在第二行比较合适12864 可以同时显示电压、电流、功率等多项参数。sprintf 会引入浮点库开销如果编译后 Flash 空间紧张可以换成把整数和小数分开处理的整数拼接方式比如 current 的整数值 . 小数两位。显示刷新最常见的问题是 SPI 总线和 LCD 冲突。如果 LCD 也挂在同一个 SPI 总线上每次切换从机时必须保证 CS 之间有至少一个字节的空闲时间避免 ADE7763 把 LCD 的显示数据误当成自己的命令。如果 LCD 用的是软件模拟接口那就完全没有这个问题。4.3 用串口和逻辑分析仪验证读取结果调 SPI 最怕看到一个稳定但错误的数据。我的习惯是先在串口打印寄存器原始码而不是换算后的浮点数因为浮点换算可能掩盖问题。printf(IRMS raw 0x%04X\r\n, ADE7763_ReadReg(ADE7763_REG_IRMS));把串口波特率设为 115200打开串口助手观察输出。如果 IRMS raw 在输入端电流不变时上下跳动不到十几个 LSB说明 SPI 时序和寄存器地址都是对的。如果连续读同一寄存器数值完全一致但明显不合理比如永远 0xFFFF那多半是 MISO 没有正确连接到 PA6或者 ADE7763 没有进入 SPI 模式。这个时候逻辑分析仪的判断速度更快。把 CS、SCLK、MOSI、MISO 四根线勾上抓一次完整的读时序重点看 CS 低电平期间是否正好出现 24 个时钟边沿前 8 个是命令字节后 16 个是数据字节。如果只有 8 个时钟说明你的读函数在发完命令字节后没有再发 0x00芯片根本没有把数据移出来。如果时钟数正确但 MISO 上电平变化出现在错误的边沿就要调整 CPOL/CPHA 或者降低 SPI 频率。这里给一个判断表格方便对照现象可能原因优先排查读回全 0CS 时序错误 / 从机未使能确认 CS 拉低时长读回全 1MISO 接线断 / ADE7763 供电异常万用表测 MISO 电平低字节恒为 0时钟数量不够或相位不对用逻辑分析仪数边沿数据偶发跳变干扰或分频系数过高降低 SPI 分频并加平均滤波数据偶发跳变不能只靠软件滤波先检查 SPI 线是否过长、是否和继电器线走在一起。对计量用途模拟地和数字地要单点连接SPI 线上可以加 33Ω 的串联电阻抑制振铃。5. 进阶SPI 参数边界与电流采样抗干扰技巧5.1 当 CPOL/CPHA 和手册对不上时怎么办有些板卡的 ADE7763 周围有上拉电阻或者 STM32 的复用推挽设置不当会导致波形沿变缓。时序图上写的是 CPOL1/CPHA1Edge实际读出来却不对此时可以把初始化里的组合从默认值改成互补项然后用串口打印原始码看符号位是否落在正确的数据位。这里不是玄学而是 PCB 走线延迟让采样子沿落在了数据跳变附近。通常 4.5MHz 下不会这样但当你有意把分频提高到 9MHz 或 18MHz 时这个问题就明显了。所以稳妥做法是先按低速率把功能跑通再逐渐提升到 4.5MHz别一上来就追求极限。5.2 多次读取取中值比取平均更能抗脉冲干扰电流互感器二次侧如果有大电机启动容易串入窄脉冲。传统平均值会把一个尖峰平均到结果里而中值滤波可以完全去掉单个异常样本。在读取 IRMS 时连续读 5 次排序后取中间值。uint16_t IRMS_GetMedian(uint8_t count) { uint16_t buf[5]; uint8_t i, j; uint16_t tmp; if (count 5) count 5; for (i 0; i count; i) buf[i] ADE7763_ReadReg(ADE7763_REG_IRMS); for (i 0; i count - 1; i) for (j i 1; j count; j) if (buf[i] buf[j]) { tmp buf[i]; buf[i] buf[j]; buf[j] tmp; } return buf[count / 2]; }这段代码用冒泡排序count 最多取 5排序耗时可以忽略。要注意的是连续读寄存器本身会占用 SPI 总线如果板子上还挂了 LCD 或 Flash需要在采样函数前后加临界区保护或者接受偶尔一次显示卡顿。更优雅的做法是把 5 次读取分散到 5 个采样周期每周期读 1 次并放入环形缓冲取中值时用最近的 5 个历史样本这样既不阻塞 SPI也能滤掉慢速扰动。最后还有一个小细节ADE7763 在上电后要等电源稳定后再去写 MODE 寄存器否则芯片可能处于复位态。初始化序列里最好延迟 50ms 以上并用写后读回校验。这样即使 SPI 参数有微小偏移也能在日志里暴露出来而不是默默跑在错误的数据上。本文还有配套的精品资源点击获取