简介:这份资源是一份面向电子信息、自动化及电气工程专业学生与嵌入式开发初学者的完整毕业设计/课设文档,围绕单片机智能电表抄表系统的软硬件实现展开,适合需要参考课题方案、撰写论文或搭建电力载波通信实验的读者。压缩包内为1个docx文件,约1.57MB,涵盖摘要、绪论、总体方案设计、硬件电路设计、软件代码编写与调试等章节,涉及STM32F103C8T6主控、ST7540电力线载波modem、MAX232电平转换等核心器件的选型与外围电路分析。文档结合FSK调制解调通信技术,给出电路原理图绘制、各功能模块解析及软硬件联调思路,可帮助读者理解抄表电路板从方案论证到调试落地的完整流程。该系统具备可靠性高、可扩展性强、成本低、计量准确等特点,主要应用于居民住宅电量的自动检测、收费与管理。目前已有84人学习下载,适合作为嵌入式电力抄表方向课题设计、论文写作与实验参考的材料。
1. 从一户一表到一台 PC:抄表链路上的断点在哪
一个 600 户的老小区,抄表员每月要爬十几栋楼,抄完还得回去手工录入。问题不在于爬楼累,而在于数据链路是断的——电表读数和收费系统之间,中间靠人当"传输介质",错抄、漏抄、抄多抄少都从这里来。
这套设计给的答案很直接:STM32F103C8T6 当控制核心,ST7540 当电力线载波调制解调器,把电量数据调制成载波信号直接耦合到既有的 220V 电力线上,不额外架设通信线。终端侧电表把读数存进 EEPROM,掉电不丢;PC 端串口助手当管理平台,按一对多主从轮询的方式挨个点名取数。
它的适用边界要说清楚:电力载波信号被配电变压器阻隔,一套系统只能覆盖同一个台区;三相电力线之间存在 10dB~30dB 的信号损失,工程上一般只走单相。适合做单片机课程设计、毕业设计,或者想低成本验证远传抄表原型的人。
2. STM32F103C8T6 与 ST7540 的载波前端电路设计
2.1 主控与载波芯片的分工与选型理由
STM32F103C8T6 是 LQFP48 封装、Cortex-M3 内核的 32 位 RISC 处理器,最高 72MHz,片内 64K Flash 加 20K SRAM。放到这套系统里,真正被看中的是外设数量:3 个 USART、2 个 SPI、2 个 I2C、3 个 16 位定时器加 1 个高级定时器。串口刚好够分——一路给 PC 侧的电平转换芯片,一路给 ST7540,I2C 一路挂 EEPROM,不用做软件模拟。
ST7540 的定位是电力线载波 modem。单电源 7.5~12.5V 供电,内部集成了线路驱动器和 5V、3.3V 两个线性调整器,最大输出电流 50mA,静态电流约 5mA。它自带 8 个可编程发送频率、最高 4800bps 的可编程波特率、500μV(rms) 的接收灵敏度,向下兼容 ST7538 和 ST7537,做升级替换时改动小。内部功放已经集成,外围不需要再搭大功率驱动级,这是选它而不是"MCU + 独立调制电路 + 功放"三段式方案的主要原因。
引脚资源的实际分配如下,这张表在画原理图前最好先定死,后面改线很麻烦。
| 功能 | STM32 引脚 | 外设/模式 | 关键参数 |
|---|---|---|---|
| PC 通信 | PA9 / PA10 | USART1_TX / RX | 接 MAX232,最高 4.5Mbit/s |
| ST7540 通信 | PA2 / PA3 | USART2_TX / RX | 接载波模块,本设计跑 4800bps |
| EEPROM | PB6 / PB7 | I2C1_SCL / SDA | 开漏输出,外部上拉 |
| 程序烧录调试 | PA13 / PA14 | SWDIO / SWCLK | 优先 SWD,不用 JTAG |
| 复位 | NRST | 异步复位 | 低电平有效 |
| 启动选择 | BOOT0 / PB2 | BOOT1 | 决定启动映射 |
2.2 最小系统:8M 晶振、复位、启动模式与 SWD 烧录
外部 8M 晶振接在 OSC_IN / OSC_OUT 上,经 PLL 九倍频得到 72MHz。如果为了省料去掉外部晶振改用内部 RC,48 脚封装下有两个处理方式:把 OSC_IN、OSC_OUT 各串 10K 电阻接地来提高 EMC 性能,或者把这两个脚重映射成 PD0、PD1 并配置为推挽输出低电平,后者能省掉两个电阻。代价是内部 RC 的温漂和精度都差一档,跑 4800bps 串口还凑合,再往上就容易出误码。
复位电路的作用是在上电时产生足够宽的低电平,NRST 被拉低的这段时间里,内部寄存器和片内 SRAM 被重设;电平回升后 PC 指针从 0 地址取指。看门狗溢出时也是往这里灌高电平强制复位,所以复位脚不要挂大电容,否则看门狗复位会被拖慢。
BOOT0 的接法是最容易踩坑的一处。常见做法是经 10K 电阻接地,从片内 Flash 启动,程序烧完直接跑;要烧录时才把 BOOT0 拉到 3.3V 进系统存储器,走串口 ISP 下载。如果 BOOT0 长期拉高又没接对 BOOT1,上电后不会进你的应用程序,现象是"程序明明烧进去了却不动"。
时钟树配置建议直接操作寄存器,逻辑比库函数清楚:
/* 8MHz 外部晶振 -> PLL 9 倍频 -> 72MHz 系统时钟 */ RCC->CR |= RCC_CR_HSEON; while (!(RCC->CR & RCC_CR_HSERDY)); /* 等 HSE 稳定 */ FLASH->ACR |= FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY_2; /* 72MHz 需 2 个等待周期 */ RCC->CFGR &= ~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLMULL); RCC->CFGR |= RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL9; /* HSE 不分频,9 倍频 */ RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; /* APB1 最高 36MHz */ RCC->CFGR |= RCC_CFGR_PPRE2_DIV1; /* APB2 保持 72MHz */ RCC->CR |= RCC_CR_PLLON; while (!(RCC->CR & RCC_CR_PLLRDY)); RCC->CFGR |= RCC_CFGR_SW_PLL; /* 切系统时钟到 PLL */ while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);参数上要盯住 APB1 和 APB2 的分频。USART1 挂在 APB2 上跑 72MHz,USART2 挂在 APB1 上跑 36MHz,两者的 BRR 计算基准不同。以 USART2 跑 4800bps 为例,BRR = 36 000 000 / 4800 = 7500,写成 USART2->BRR = 0x1D4C。如果分频配错成 72MHz,同一个 BRR 值算出来就是 9600bps,对端收到的全是乱码。
烧录口用 PA13(SWDIO)、PA14(SWCLK) 的 SWD 模式。JTAG 在大数据量下载时失败率明显更高,SWD 占用引脚少、高速下更稳,调试器接四根线(3.3V、GND、SWDIO、SWCLK)就能用。
2.3 ST7540 外围:滤波网络与电流限制
ST7540 到电力线之间不是直连,中间有三级滤波,顺序和作用不能随意调换。
| 滤波级 | 位置 | 作用 | 调整时的观察点 |
|---|---|---|---|
| 发送有源滤波 | PA_OUT 之后 | 抑制功放输出的谐波,防止污染其他频段 | 输出波形是否削顶 |
| 发送无源滤波 | 耦合变压器前 | 阻抗匹配 + 进一步滤除带外分量 | 耦合端电压幅度 |
| 接收无源滤波 | RX_IN 之前 | 窄带选频,压低带外噪声 | 弱信号下的接收误码率 |
发送通路最终通过耦合变压器并到电力线上。接收通路经滤波后直接进 RX_IN,芯片内部再做三次混频滤波,在低信噪比条件下保证动态范围。24 脚的 CL 与 SVSS 之间接一个限流设定电阻,这个电阻决定功放的最大输出电流——阻值取得太小,轻载时波形失真;取得太大,重载时驱动不足。常用做法是先按典型值装上去,用示波器看耦合端的波形,边调边看,不要一上来就按计算值赌。
还有两处必须接地:TEST1 直接接数字地,TEST2 接模拟地(SVSS)。这两个脚悬空时芯片行为不确定,表现是收发时好时坏,很难查。
2.4 TTL/RS232 电平转换与 I2C EEPROM 电路
STM32 的 USART1 输出是 CMOS/TTL 电平,PC 的串口是 RS232 电平,两者之间用 MAX232 转换。MAX232 靠内部电荷泵把 5V 泵成 ±10V 左右,需要外接 4 个 1μF 电容,容值不足会直接导致通信距离变短或者完全不通。接线记住是交叉:单片机 TX 接 MAX232 的 T1IN、对应输出 T1OUT 接 DB9 的 RXD;单片机 RX 接 R1OUT,R1IN 接 DB9 的 TXD。
终端电表侧的数据存放在 EEPROM 里,STM32 通过 I2C1(PB6/PB7) 读写。SDA 和 SCL 都是开漏输出,必须外接上拉电阻,4.7K 是常用值,线长超过 20cm 可以降到 2.2K 提高上升沿速度。器件地址的低三位由 A0~A2 引脚决定,同一块板上挂多片时靠这三根线区分。
/* I2C1 初始化:100kHz 标准模式,PB6=SCL,PB7=SDA */ RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; RCC->APB1ENR |= RCC_APB1ENR_I2C1EN; GPIOB->CRL &= ~(0xFF << 24); GPIOB->CRL |= (0xEE << 24); /* PB6/PB7 复用开漏输出,50MHz */ I2C1->CR2 = 36; /* APB1 36MHz,单位 MHz */ I2C1->CCR = 180; /* 标准模式:36MHz/(2*100kHz) */ I2C1->TRISE = 37; /* 36MHz * 1us + 1 */ I2C1->CR1 |= I2C_CR1_PE; /* 使能 I2C */这里有个隐蔽的坑:APB1 一旦从 36MHz 改成 72MHz,CCR 和 TRISE 必须同步重算,否则 I2C 时序错乱,表现为能写进去但读出来是 0xFF,或者干脆卡在等待 ACK 的循环里。
3. ST7540 寄存器配置与载波数据收发驱动
3.1 REG_DATA 与 RxTx 的四种组合
ST7540 靠两根控制线切换工作状态,这两根线的组合决定了芯片此刻是收数据、发数据,还是在读写内部寄存器。程序里第一件事就是把这张真值表封成函数,不然后面到处直接操作 IO 口,出错很难定位。
| REG_DATA | RxTx | 工作状态 | CLR/T 角色 |
|---|---|---|---|
| 0 | 0 | 数据传送 | 发送时钟输入 |
| 0 | 1 | 数据接收 | 恢复时钟输出 |
| 1 | 1 | 控制寄存器读出 | 寄存器读时钟 |
| 1 | 0 | 控制寄存器写入 | 寄存器写时钟 |
状态切换后有一段 TCC 时间的空闲期,数据线上不做任何事,等芯片内部 PLL 稳定。很多"发送时机不对导致丢首字节"的问题,就是把 TCC 等待省掉了。
#define MODE_TX_DATA 0x00 /* REG_DATA=0, RxTx=0 */ #define MODE_RX_DATA 0x02 /* REG_DATA=0, RxTx=1 */ #define MODE_REG_READ 0x03 /* REG_DATA=1, RxTx=1 */ #define MODE_REG_WRITE 0x01 /* REG_DATA=1, RxTx=0 */ static void st7540_set_mode(uint8_t mode) { /* REG_DATA 接 PB0,RxTx 接 PB1 */ GPIOB->BSRR = (mode & 0x01) ? GPIO_BSRR_BS0 : GPIO_BSRR_BR0; GPIOB->BSRR = (mode & 0x02) ? GPIO_BSRR_BS1 : GPIO_BSRR_BR1; if (mode & 0x01) { /* 进入寄存器模式才需要时钟配合 */ delay_us(50); } }3.2 控制寄存器初始化:频率、波特率、看门狗
芯片上电后必须先写控制寄存器把发送频率、波特率、看门狗等参数配好,再切到数据模式。寄存器写入是同步串行:CLR/T 提供时钟,TXD 上先送寄存器地址再送数据,上升沿采样。初始化序列的顺序建议固定为:先复位、再配频率、再配波特率、最后开看门狗。
static void st7540_init(void) { st7540_set_mode(MODE_REG_WRITE); clr_t_low(); /* 拉低进入空闲,等待 TCC */ delay_us(TCC_US); st7540_wr_reg(REG_CTRL_1, 0x00); /* 先清零控制位,避免残留配置 */ st7540_wr_reg(REG_FREQ_SEL, FREQ_CHx); /* 选择载波通道,8 档可选 */ st7540_wr_reg(REG_BAUD_SEL, BAUD_4800);/* 波特率必须与从机一致 */ st7540_wr_reg(REG_WD_CTRL, WD_EN); /* 使能看门狗,掉线时自动复位输出 */ clr_t_high(); st7540_set_mode(MODE_RX_DATA); /* 初始化完先进接收态待命 */ }参数上有两个硬约束:发送频率必须落在允许的载波频段内,且同一台区内所有节点用同一档;波特率两端必须完全一致,ST7540 最高支持 4800bps,超了直接不通信。看门狗建议开,载波信道噪声重,芯片偶尔会卡在某个状态,看门狗能把发送通路复位回来。
3.3 发送与接收:UART 模式下的数据流
UART/SPI 引脚拉高时,RxD、TxD 走 UART 类型,ST7540 作为通信主体提供 CLR/T 参考时钟;拉低则走 SPI 类型。本设计用 UART 模式,STM32 侧的 USART2 只需按普通串口收发,不用自己造时钟。
发送流程是:切到 MODE_TX_DATA,拉低 CLR/T 进空闲,等 TCC,然后往 USART2->DR 里写字节;接收流程是:切到 MODE_RX_DATA,CLR/T 由芯片输出恢复时钟,RxD 上的数据在上升沿稳定,STM32 用中断接收。接收态下芯片会关掉内部发送模块,发送态下关掉接收模块,这是它静态电流能压到 5mA 的原因,不用手动去管。
void st7540_send(const uint8_t *buf, uint16_t len) { st7540_set_mode(MODE_TX_DATA); clr_t_low(); delay_us(TCC_US); for (uint16_t i = 0; i < len; i++) { while (!(USART2->SR & USART_SR_TXE)); /* 等发送寄存器空 */ USART2->DR = buf[i]; } while (!(USART2->SR & USART_SR_TC)); /* 等最后一字节发完 */ st7540_set_mode(MODE_RX_DATA); /* 立刻切回接收,准备应答 */ }发送完立刻切回接收态很关键。主从轮询下从机应答通常只有几毫秒的窗口,如果发送后还停在发送态,应答帧会直接丢掉,表现为"能发不能收"。
3.4 数据帧解析:前导码、地址、命令与校验
载波信道上电噪声大,帧头必须有足够长的前导码让接收侧的窄带滤波器锁定。帧结构上建议固定字段顺序,长度用显式字段而不是靠结束符判断。
uint8_t frame_parse(const uint8_t *buf, uint16_t n, meter_frame_t *out) { uint16_t i = 0; while (i + 1 < n && !(buf[i] == 0xAA && buf[i+1] == 0x55)) i++; /* 找帧头 */ if (i + 6 > n) return 0; /* 长度不够,直接丢 */ out->addr = buf[i+2]; /* 电表地址 */ out->cmd = buf[i+3]; /* 命令码 */ out->len = buf[i+4]; /* 数据长度 */ if (out->len > sizeof(out->data) || i + 5 + out->len + 2 > n) return 0; /* 越界保护,别省这一步 */ for (uint8_t k = 0; k < out->len; k++) out->data[k] = buf[i+5+k]; uint16_t crc_rx = buf[i+5+out->len] | (buf[i+6+out->len] << 8); if (crc_rx != crc16_modbus(&buf[i+2], out->len + 3)) return 0; /* 校验不过,交给上层重发 */ return 1; }解析函数里最不能省的是长度越界保护。载波信道误码是常态,收到半截帧时如果直接按 len 拷贝,会踩到缓冲区外面的内存,现场表现是"跑几个小时就死机",比通信失败难查得多。
4. PC 主节点协议与电表数据读写实现
4.1 一对多轮询:为什么不用侦听冲突
系统拓扑是 PC 管理平台作主节点,所有终端电表作从节点,控制命令一律由主节点发出,从节点只应答不主动发。这样一来就不存在多节点同时抢信道的问题,也就不需要载波侦听多路访问那套机制——在电力线这种高噪声、高衰减的信道上做冲突检测本来就不可靠,退回到主从轮询反而稳。
代价是轮询周期。4800bps 下一帧 20 字节大约 40ms,加上 TCC 等待和从机响应时间,单节点一轮按 100ms 估,128 户一个小区的完整轮询约 13 秒。对抄表这种一天一次的业务完全够用,但别指望它做实时负荷监控。
4.2 串口数据帧格式与 CRC16
PC 与主板卡之间、主板卡与从板卡之间用同一套帧格式,字段定义要一次定死,后面扩功能只加命令码不动结构。
| 字段 | 长度 | 说明 |
|---|---|---|
| 前导码 | 2 字节 | 固定 0xAA 0x55 |
| 地址 | 1 字节 | 0x00 为广播,0x01~0xFE 为电表编号 |
| 命令码 | 1 字节 | 0x01 读电量,0x02 写参数,0x03 校时 |
| 长度 | 1 字节 | 后续数据域字节数 |
| 数据域 | N 字节 | 电量为 4 字节 BCD 或小端整数 |
| CRC16 | 2 字节 | Modbus 多项式 0xA001,低字节在前 |
uint16_t crc16_modbus(const uint8_t *p, uint16_t len) { uint16_t crc = 0xFFFF; while (len--) { crc ^= *p++; for (uint8_t i = 0; i < 8; i++) crc = (crc & 0x01) ? (crc >> 1) ^ 0xA001 : (crc >> 1); } return crc; } /* 组帧:返回整帧长度,buf 由调用者保证足够大 */ uint16_t frame_build(uint8_t *buf, uint8_t addr, uint8_t cmd, const uint8_t *data, uint8_t len) { uint16_t i = 0; buf[i++] = 0xAA; buf[i++] = 0x55; buf[i++] = addr; buf[i++] = cmd; buf[i++] = len; for (uint8_t k = 0; k < len; k++) buf[i++] = data[k]; uint16_t crc = crc16_modbus(&buf[2], len + 3); /* 从地址域开始算 */ buf[i++] = crc & 0xFF; /* 低字节在前 */ buf[i++] = crc >> 8; return i; }CRC 的起算位置必须和解析端保持一致,都从地址域算起,把前导码排除在外。这一处对不上,整个系统一帧都收不下来,但单独测 CRC 函数又是对的,很容易在两个模块之间来回甩锅。
4.3 PC 侧串口收发与超时重发
STM32 接 MAX232 的那路串口优先用中断接收,配合一个环形缓冲和空闲超时状态机,把不定长帧切出来。
void USART1_IRQHandler(void) { if (USART1->SR & USART_SR_RXNE) { uint8_t b = USART1->DR; /* 读 DR 自动清 RXNE */ rx_ring_push(b); /* 入环形缓冲 */ last_rx_tick = tick_ms; /* 刷新帧间超时计时 */ } if (USART1->SR & USART_SR_ORE) { /* 溢出错误必须显式清 */ (void)USART1->DR; } }主循环里判断tick_ms - last_rx_tick > 5就认为一帧结束。PC 侧发送命令后启动 200ms 超时,超时未收到合法应答就重发,连续三次失败把该节点标记为离线。这个重发次数不要设太大,载波信道一次轮询本来就不长,重发太多次会拖垮整个轮询周期。
4.4 读电表数据:从 EEPROM 取电量并回帧
终端侧电表把电量存在 EEPROM 里,主板卡收到读命令后按地址取出再回帧。读操作走标准 I2C 时序:起始、器件地址+写、内存地址、重启、器件地址+读、连续读、非应答、停止。
uint8_t ee_read(uint8_t dev, uint16_t off, uint8_t *buf, uint8_t len) { i2c_start(); if (!i2c_write((dev << 1) | 0)) return 0; /* 器件地址 + 写 */ if (!i2c_write(off >> 8)) return 0; if (!i2c_write(off & 0xFF)) return 0; i2c_start(); /* 重复起始,不释放总线 */ if (!i2c_write((dev << 1) | 1)) return 0; /* 器件地址 + 读 */ for (uint8_t i = 0; i < len; i++) buf[i] = i2c_read(i == len - 1); /* 最后一字节回 NACK */ i2c_stop(); return 1; }注意最后一字节必须回非应答再发停止位,回应答会让 EEPROM 继续输出下一字节,把总线拉死。EEPROM 写周期约 5ms,写完立刻读会拿到旧数据,两次写之间要留够等待时间,或者用应答轮询确认写完。
4.5 写电表数据与掉电保护
写电表参数(比如费率、表号)时,单次写不超过 EEPROM 的页大小,跨页要拆成多次。更重要的是顺序:先把新数据写到一个备份区,校验通过后再更新主区标志位。抄表系统最怕的是写一半掉电,主区数据变成半新半旧,电量对不上账。
5. 联调顺序与串口、载波的故障定位
联调不要一上来就把 PC、载波、电表全接上,按链路分段验证,出问题时才能定位到具体一段。
第一步,只接 STM32 和 PC,用串口助手 4800-8-N-1 发固定字符串,确认收发正常;第二步,示波器看 ST7540 的 CLR/T 有没有时钟输出,判断芯片是否正常上电工作;第三步,两台板子之间用短线直接连 TX_OUT 和 RX_IN,跳过电力线验证调制解调本身;第四步,接耦合变压器和电力线,短距离通信;第五步,接电表终端,跑完整轮询。
常见故障的定位路径如下。
| 现象 | 高概率原因 | 定位手段 |
|---|---|---|
| 串口收到乱码 | 波特率基准算错、晶振未起振 | 量 OSC_OUT 有无 8MHz,核对 APB 分频 |
| 完全收不到数据 | TX/RX 未交叉、MAX232 电容不足 | 短接 MAX232 两端做回环测试 |
| 能发不能收 | 发送后未及时切回接收态 | 抓 RxTx 引脚电平变化时序 |
| 通信时好时坏 | 限流电阻取值不当、TEST 脚悬空 | 示波器看耦合端波形有无削顶 |
| 跨楼层就断 | 跨配电变压器、三相耦合损失 | 确认两端在同一台区同一相 |
| 跑数小时死机 | 帧解析越界、串口溢出未清 | 加看门狗,检查 ORE 标志处理 |
有两个技巧值得单独说。一是做一条"回环线",把 ST7540 的 TX_OUT 直接短接到 RX_IN,写一段自发自收的测试程序,能把调制解调部分和电力线耦合部分彻底隔离开,省掉大量来回换板子的时间。二是所有串口和 I2C 的等待循环都加超时退出,载波信道噪声重,硬件偶尔不响应是正常的,卡死在 while 里等标志位,比通信失败更难查——现场表现就是"设备死机,断电重启又好"。
最后调限流电阻时,别按计算值一次到位。先在耦合端接示波器,从小阻值慢慢往上调,看波形从削顶变成完整正弦那个点,再往上留一点余量。这个值跟实际线路阻抗相关,换个台区可能要重调。
本文还有配套的精品资源,点击获取