- 打卡等级:初来乍到
- 打卡总天数:1
- 最近打卡:2026-01-17 13:38:48
已绑定手机
新手上路
- 积分
- 5
|
[13:36:24.185]发送→01 03 00 00 00 03 05 CB
[13:36:24.260]接收←01 03 06 FF FF FF FF 0C E8 24 04
[13:36:29.706]发送→01 06 00 01 07 D0 DB A6
[13:36:29.796]接收←01 06 00 01 07 D0 DB A6
[13:36:32.908]发送→01 03 00 00 00 03 05 CB
[13:36:32.995]接收←01 03 06 FF FF 07 D0 0C EA A5 6C
[13:36:47.223]发送→01 10 00 00 00 02 04 00 01 86 5F 80 37
[13:36:47.340]接收←45 6E 74 65 72 20 30 78 31 30 20 70 61 72 73 65
0D 0A 62 79 74 65 5F 6C 65 6E 3D 31 30 32 36 2C
20 72 65 67 5F 6E 75 6D 3D 30 2C 20 72 65 67 5F
61 64 64 72 3D 30 0D 0A 72 65 63 76 5F 63 72 63
3D 30 78 33 37 38 30 2C 20 63 61 6C 63 5F 63 72
63 3D 30 78 33 37 38 30 0D 0A 77 72 69 74 65 5F
62 75 66 5B 30 5D 3D 30 78 30 31 38 30 0D 0A 77
72 69 74 65 5F 62 75 66 5B 33 39 30 5D 3D 30 78
35 66 38 30 0D 0A 01 10 00 00 00 02 41 C8
[13:36:51.205]发送→01 03 00 00 00 03 05 CB
[13:36:51.287]接收←01 03 06 00 01 86 5F 0C E8 00 A1
[13:37:03.412]发送→01 06 00 04 00 02 49 CA
[13:37:03.498]接收←01 06 00 04 00 02 49 CA
[13:37:09.230]发送→02 03 00 00 00 03 05 F8
[13:37:09.312]接收←02 03 06 00 01 86 5F 0C E8 14 51
/**************************************************************************************
* 程序名称:水表计量主控程序 - 精简版(仅保留03/06/16功能码)
* 核心功能:1.双霍尔正向/反向计数 2.Modbus 03读/06单写/16批量写 3.EEPROM掉电存储 4.低功耗休眠 5.ADC电压采样 6.站号修改
* 硬件平台:STC8H单片机 | 波特率:9600bps 8N1 | 晶振:11.0592MHz/12MHz通用
* 通信指令:03读/06单写/16批量写 均支持标准CRC校验
**************************************************************************************/
#include <STC8H.H> // STC8H寄存器定义头文件
//#include <intrins.h> // _nop_()函数依赖
#include <stdio.h> // printf函数依赖的头文件
/**************************************************************************************
* putchar重定向函数(放在头文件之后,其他函数之前,核心位置!)
**************************************************************************************/
char putchar(char c)
{
// 和你现有的uart_send_byte函数逻辑一致,实现字符发送
SBUF = c;
while (!TI); // 等待发送完成
TI = 0; // 清除发送完成标志
return c;
}
/**************************************************************************************
* 引脚定义区 - 硬件接线映射(请勿随意修改)
**************************************************************************************/
sbit INT0_PIN = P3^2; // 正向霍尔信号输入引脚 -> INT0外部中断0
sbit INT1_PIN = P3^3; // 反向霍尔信号输入引脚 -> INT1外部中断1
/**************************************************************************************
* 数据类型重定义 - 简化代码书写,提升可读性
**************************************************************************************/
typedef unsigned char u8; // 无符号字符型,1字节,0~255
typedef unsigned int u16; // 无符号整型,2字节,0~65535
typedef unsigned long u32; // 无符号长整型,4字节,0~4294967295
/**************************************************************************************
* 全局变量声明 - 跨函数共用变量,全局生效
**************************************************************************************/
unsigned int VDDA; // ADC采样得到的系统电压值,单位:毫伏(mV)
unsigned int BGV; // 单片机内部基准电压值,用于ADC校准,单位:毫伏(mV)
u8 modbus_slave_addr = 1; // === 新增 === 动态站号变量(默认1,支持修改)
/**************************************************************************************
* 功能宏定义区 - 核心参数配置,可按需修改
**************************************************************************************/
// ADC相关宏定义(STC8H标准,头文件内置宏映射)
#define ADC_POWER_EN ADC_POWER // ADC电源使能位
#define ADC_START_CONV ADC_START // ADC转换启动位
#define ADC_CONV_DONE ADC_FLAG // ADC转换完成标志位
// EEPROM配置宏(掉电存储水度值+站号)
#define IAP_ADDR 0x0000 // EEPROM水度值起始地址
#define IAP_ADDR_ADDR 0x0008 // === 新增 === EEPROM站号存储地址(和水度值分离)
#define IAP_DATA_LEN 4 // 存储数据长度:4字节存储u32水度值
#define MAIN_Fosc 11059200L // 晶振频率,和官方代码一致
#define IAP_PAGE_ERASE 0x04 // EEPROM页擦除命令
#define IAP_READ_CMD 0x01 // EEPROM字节读命令
#define IAP_WRITE_CMD 0x02 // EEPROM字节写命令
#define IAP_EXEC_CMD 0x03 // EEPROM命令执行位
#define IAP_POWER_EN IAPEN // IAP模块电源使能位
#define IAP_TPS_VAL 12 // 晶振12MHz,参考代码配置值
// 水流计数时序配置
#define FLOW_TIMEOUT 5 // 霍尔时序超时阈值(5×10ms=50ms),超时放弃计数
// 低功耗休眠配置
#define SLEEP_TIMEOUT 1000 // 空闲休眠阈值(1000×10ms=10秒),无操作休眠
// Modbus核心配置(通信关键,请勿随意修改)
#define SLAVE_ADDR modbus_slave_addr // === 修改 === 宏映射到动态站号变量
#define HOLD_REG_COUNT 0x0008 // Modbus保持寄存器总数(0000~0007)
#define MAX_READ_NUM 16 // 单次最大读取寄存器数量
/**************************************************************************************
* 系统运行状态变量 - 计数/休眠/通信状态管理
**************************************************************************************/
u8 sleep_flag; // 休眠标志位:0=工作,1=休眠
u8 wakeup_count; // 空闲计时变量,累计达阈值触发休眠
u32 xdata water_meter; // 核心水度计数值,1单位=0.001m³(1升),掉电保存
u8 xdata flow_dir_state; // 水流方向状态机:0=初始,1=正向触发中,2=反向触发中
u16 xdata flow_timer; // 霍尔时序计时变量,超时复位状态机
u8 xdata valid_count_flag; // 计数有效标志:0=无效,1=正向,2=反向
/**************************************************************************************
* Modbus通信缓冲区 - 保障数据收发稳定
**************************************************************************************/
u16 xdata holding_regs[HOLD_REG_COUNT]; // Modbus保持寄存器数组
u8 xdata uart_rx_buf[32]; // 串口接收缓冲区(扩容适配16功能码)
u8 xdata modbus_resp_buf[64]; // Modbus应答缓冲区
u8 rx_len; // 串口接收字节数计数
bit rx_complete; // 指令接收完成标志位:0=未完成,1=完成
u16 xdata modbus_timeout; // Modbus接收超时计数器(防粘包)
/**************************************************************************************
* 函数声明区 - 声明所有自定义函数,避免编译报错
**************************************************************************************/
// 系统初始化函数
void init_sys(void); // 系统总初始化
void init_ext_int(void); // 外部中断初始化(霍尔计数)
void init_adc(void); // ADC初始化(电压采样)
void init_timer0(void); // 定时器0初始化(10ms中断)
void init_uart(void); // 串口初始化(Modbus RTU通信)
// 低功耗函数
void sleep_mode(void); // 进入掉电休眠模式
void wakeup_proc(void); // 休眠唤醒处理
// EEPROM函数(掉电存储)
void eeprom_read(void); // 上电读取EEPROM水度值
void eeprom_write(void) reentrant; // 水度更新后写入EEPROM
void eeprom_page_erase(u16 addr); // EEPROM页擦除
u8 eeprom_read_byte(u16 addr); // EEPROM单字节读取
void eeprom_write_byte(u16 addr, u8 dat); // EEPROM单字节写入
void iap_idle(void); // IAP模块空闲复位
void eeprom_read_addr(void); // === 新增 === 上电读取EEPROM站号
void eeprom_write_addr(void) reentrant; // === 新增 === 站号更新后写入EEPROM
// 硬件功能函数
void battery_check(void); // 电池电压采样计算
void delay_ms(u16 ms); // 毫秒级延时函数
u16 ADCRead(u8 channel); // ADC采样函数(12位精度)
// Modbus通信核心函数
void uart_send_byte(u8 dat); // 串口单字节发送
void uart_send_buf(u8 *buf, u8 len); // 串口多字节发送(RS485控制)
u16 crc16(u8 *buf, u8 len); // Modbus标准CRC16校验
void modbus03_response(u8 addr, u16 reg_addr, u8 reg_num); // 03功能码应答
void modbus06_write(u8 addr, u16 reg_addr, u16 write_val); // 06功能码单写
void modbus10_write(u8 addr, u16 reg_addr, u8 reg_num, u16 *write_buf); //16批量写
void modbus_parse(void); // Modbus指令解析
void update_holding_regs(void);// 更新保持寄存器数据
void limit_slave_addr(void); // === 新增 === 站号合法性校验
/**************************************************************************************
* 主函数 - 程序入口,执行总逻辑
**************************************************************************************/
void main(void)
{
P_SW2 |= 0x80; // 开启STC8H扩展寄存器访问权限(必须)
init_sys(); // 初始化所有外设
eeprom_read(); // 上电读取EEPROM中历史水度值
eeprom_read_addr(); // === 新增 === 上电读取EEPROM中保存的站号
limit_slave_addr(); // === 新增 === 站号合法性兜底校验
printf("System init ok! Slave addr=%d\r\n", modbus_slave_addr); // === 修改 === 打印当前站号
while(1) // 主循环,永不退出
{
if(sleep_flag) // 休眠标志置1,进入低功耗
{
sleep_mode();
}
else // 工作模式,执行业务逻辑
{
battery_check(); // 实时采样电池电压
update_holding_regs(); // 更新Modbus寄存器数据
if (rx_complete) // 接收到完整Modbus指令,执行解析
{
modbus_parse();
}
// 空闲计时达阈值,触发休眠
if(wakeup_count > SLEEP_TIMEOUT)
{
wakeup_count = 0;
sleep_flag = 1;
}
}
}
}
/**************************************************************************************
* 系统总初始化函数 - 统一初始化所有外设,便于管理
**************************************************************************************/
void init_sys(void)
{
P0 = 0xFF;P1 = 0xFF;P2 = 0xFF;P3 = 0xFF; // 所有IO口置高,防止误触发
init_ext_int(); // 初始化外部中断(霍尔)
init_adc(); // 初始化ADC(电压采样)
init_timer0(); // 初始化定时器0(10ms中断)
init_uart(); // 初始化串口(Modbus通信)
EA = 1; // 开启总中断
}
/**************************************************************************************
* 外部中断初始化函数 - 配置INT0/INT1为下降沿触发,适配霍尔信号
**************************************************************************************/
void init_ext_int(void)
{
P3PU |= 0x0C; // P3^2(INT0)、P3^3(INT1)使能内部上拉电阻,抗干扰
IT0 = 1; // INT0配置为下降沿触发
EX0 = 1; // 使能INT0中断
IT1 = 1; // INT1配置为下降沿触发
EX1 = 1; // 使能INT1中断
}
/**************************************************************************************
* ADC初始化函数 - 配置12位精度,校准内部基准电压
**************************************************************************************/
void init_adc(void)
{
// 读取内部基准电压值(CHIPID7/8为STC8H内置基准电压寄存器)
BGV = ((unsigned int)CHIPID7 << 8) | (unsigned int)CHIPID8;
ADCCFG = 0x2F; // ADC配置:12位精度、右对齐、分频适配
delay_ms(1); // 延时稳定ADC模块
ADC_CONTR |= ADC_POWER_EN; // 开启ADC电源
}
/**************************************************************************************
* 定时器0初始化函数 - 16位模式,11.0592MHz晶振,精准10ms中断
**************************************************************************************/
void init_timer0(void)
{
TMOD |= 0x01; // 定时器0配置为16位定时器模式
TH0 = 0xDC; // 定时器高8位初值
TL0 = 0x00; // 定时器低8位初值
ET0 = 1; // 使能定时器0中断
TR0 = 1; // 启动定时器0
}
/**************************************************************************************
* 串口初始化函数 - 9600bps波特率,8N1格式,适配Modbus RTU通信
**************************************************************************************/
void init_uart(void)
{
ES = 1; // 使能串口中断
PCON &= 0x7F; // 关闭串口倍速模式,保证波特率稳定
SCON = 0x50; // 串口配置:8位数据位、1位停止位、接收使能
AUXR &= 0xBF; // 定时器1分频配置
AUXR &= 0xFE; // 定时器1为12T模式,适配波特率
TMOD &= 0x0F; // 清空定时器1模式
TMOD |= 0x20; // 定时器1配置为8位自动重装模式
TL1 = 0xFD; // 9600bps波特率初值(11.0592MHz/12MHz通用)
TH1 = 0xFD; // 定时器1重装值
ET1 = 0; // 关闭定时器1中断(仅用于波特率生成)
TR1 = 1; // 启动定时器1
}
/**************************************************************************************
* 低功耗休眠函数 - 进入掉电模式,最低功耗,霍尔/串口可唤醒
**************************************************************************************/
void sleep_mode(void)
{
TR0 = 0; // 停止定时器0
PCON |= PD; // 开启STC8H掉电休眠模式(最低功耗)
_nop_();_nop_(); // 空操作,保证休眠指令执行完成
TR0 = 1; // 唤醒后重启定时器0
}
/**************************************************************************************
* 休眠唤醒处理函数 - 退出休眠,复位状态,稳定外设
**************************************************************************************/
void wakeup_proc(void)
{
sleep_flag = 0; // 清除休眠标志,进入工作模式
wakeup_count = 0; // 复位空闲计时器
delay_ms(10); // 延时稳定外设,避免误触发
}
/**************************************************************************************
* ADC采样函数 - 12位精度采样指定通道,返回0~4095采样值
**************************************************************************************/
u16 ADCRead(u8 channel)
{
ADC_RES = 0;ADC_RESL = 0; // 清空采样结果寄存器
// 配置采样通道+启动ADC转换
ADC_CONTR = (ADC_CONTR & 0xF0) | 0x40 | (channel & 0x0F);
_nop_();_nop_();_nop_();_nop_(); // 等待转换启动
while((ADC_CONTR & 0x20) == 0); // 等待转换完成
ADC_CONTR &= ~0x20; // 清除转换完成标志
return ((((u16)ADC_RES << 8) | ADC_RESL) & 0x0FFF); // 返回12位采样值
}
/**************************************************************************************
* 电池电压采样函数 - 计算实际电压值(mV),每次采样重启ADC电源
**************************************************************************************/
void battery_check(void)
{
u8 i;u16 res = 0;
ADC_CONTR |= ADC_POWER_EN; // 每次采样前重启ADC电源,保证稳定
ADCRead(15);ADCRead(15); // 丢弃前两次不稳定采样值
for(i=0; i<8; i++) res += ADCRead(15); // 8次采样取平均,滤除噪声
res >>= 3; // 平均值计算(除以8)
// 根据基准电压计算实际电压值:VDDA = BGV * 4096 / 采样值
VDDA = (unsigned int)((unsigned long)BGV * 4096UL / (unsigned long)res);
}
/**************************************************************************************
* 毫秒级延时函数 - 适配11.0592MHz晶振,精准延时,用于外设稳定/RS485时序
**************************************************************************************/
void delay_ms(u16 ms)
{
u16 i, j;
for(i = ms; i > 0; i--)
for(j = 110; j > 0; j--);
}
/**************************************************************************************
* 正向霍尔中断服务函数 - INT0,下降沿触发,硬件消抖+反向计数逻辑
**************************************************************************************/
void int0_isr(void) interrupt INT0_VECTOR
{
u8 temp=0,i;
// 硬件消抖:连续检测10次,低电平≥8次判定为有效信号
for(i=0;i<10;i++){if(INT0_PIN==0)temp++;_nop_();}
if(temp<8) return; // 消抖失败,直接退出
wakeup_proc(); // 触发唤醒,退出休眠
switch(flow_dir_state)
{
case 0: // 初始状态,标记为正向触发中
flow_dir_state = 1;flow_timer=0;valid_count_flag=0;break;
case 2: // 反向触发中,接收到正向信号 → 反向计数
if(water_meter>0) water_meter--;
eeprom_write(); // 水度更新,写入EEPROM
flow_dir_state=0;flow_timer=0;valid_count_flag=2;break;
default: // 异常状态,复位状态机
flow_dir_state=0;flow_timer=0;valid_count_flag=0;break;
}
}
/**************************************************************************************
* 反向霍尔中断服务函数 - INT1,下降沿触发,硬件消抖+正向计数逻辑
**************************************************************************************/
void int1_isr(void) interrupt INT1_VECTOR
{
u8 temp=0,i;
// 硬件消抖:连续检测10次,低电平≥8次判定为有效信号
for(i=0;i<10;i++){if(INT1_PIN==0)temp++;_nop_();}
if(temp<8) return; // 消抖失败,直接退出
wakeup_proc(); // 触发唤醒,退出休眠
switch(flow_dir_state)
{
case 0: // 初始状态,标记为反向触发中
flow_dir_state = 2;flow_timer=0;valid_count_flag=0;break;
case 1: // 正向触发中,接收到反向信号 → 正向计数
water_meter++;
eeprom_write(); // 水度更新,写入EEPROM
flow_dir_state=0;flow_timer=0;valid_count_flag=1;break;
default: // 异常状态,复位状态机
flow_dir_state=0;flow_timer=0;valid_count_flag=0;break;
}
}
/**************************************************************************************
* 定时器0中断服务函数 - 10ms一次,处理:霍尔时序超时/空闲计时/Modbus超时
**************************************************************************************/
void timer0_isr(void) interrupt TMR0_VECTOR
{
TH0=0xDC;TL0=0x00; // 重装定时器初值,保证10ms中断精度
// 1. 霍尔时序超时处理:超时复位状态机
if(flow_dir_state!=0)
{
flow_timer++;
if(flow_timer>=FLOW_TIMEOUT)
{flow_dir_state=0;flow_timer=0;valid_count_flag=0;}
}
// 2. 空闲计时:工作模式下累计,触发休眠
if(sleep_flag == 0) wakeup_count++;
// 3. Modbus超时:50ms无数据,判定指令接收完成(防粘包)
if(rx_len>0)
{
modbus_timeout++;
if(modbus_timeout>5) // 5×10ms=50ms,符合Modbus RTU标准帧间隙
{rx_complete=1;modbus_timeout=0;}
}
else modbus_timeout=0;
}
/**************************************************************************************
* Modbus通信核心函数(03/06/16功能码支撑)
**************************************************************************************/
// 串口单字节发送函数 - 底层发送接口,保证字节发送完成
void uart_send_byte(u8 dat)
{
SBUF = dat;
while (!TI); // 等待发送完成
TI = 0; // 清除发送完成标志
}
// RS485发送函数(精简时序,9600bps最优适配)
void uart_send_buf(u8 *buf, u8 len)
{
u8 i;
// DE_RE = 1; // RS485切换为发送模式
for (i = 0; i < len; i++) uart_send_byte(buf[i]);
delay_ms(1); // 1ms延时,确保最后1字节发送完成
// DE_RE = 0; // RS485切回接收模式
}
// Modbus标准CRC16校验函数(低位在前、高位在后)
u16 crc16(u8 *buf, u8 len)
{
u16 crc = 0xFFFF;u8 i, j;
for (i = 0; i < len; i++)
{
crc ^= buf[i];
for (j = 0; j < 8; j++)
{
if (crc & 0x0001) crc = (crc >> 1) ^ 0xA001;
else crc >>= 1;
}
}
return crc;
}
// 03功能码应答函数(读保持寄存器)
void modbus03_response(u8 addr, u16 reg_addr, u8 reg_num)
{
u16 crc;u8 i, idx = 0;
modbus_resp_buf[idx++] = addr; // 站号
modbus_resp_buf[idx++] = 0x03; // 功能码
modbus_resp_buf[idx++] = reg_num * 2; // 返回数据字节数
// 填充寄存器数据:高字节在前
for (i = 0; i < reg_num; i++)
{
modbus_resp_buf[idx++] = (holding_regs[reg_addr + i] >> 8) & 0xFF;
modbus_resp_buf[idx++] = holding_regs[reg_addr + i] & 0xFF;
}
// CRC校验:低位在前
crc = crc16(modbus_resp_buf, idx);
modbus_resp_buf[idx++] = (u8)(crc & 0xFF);
modbus_resp_buf[idx++] = (u8)((crc >> 8) & 0xFF);
uart_send_buf(modbus_resp_buf, idx);
}
// 06功能码单寄存器写函数
void modbus06_write(u8 addr, u16 reg_addr, u16 write_val)
{
u16 crc;u8 idx = 0;
if(addr != SLAVE_ADDR) return; // 站号不匹配,直接返回
// 寄存器写入权限控制
switch(reg_addr)
{
case 0x0000: // 水度高16位
water_meter = (water_meter & 0x0000FFFF) | ((u32)write_val << 16);
eeprom_write();
break;
case 0x0001: // 水度低16位
water_meter = (water_meter & 0xFFFF0000) | (u32)write_val;
eeprom_write();
break;
// === 新增 === 站号寄存器分支(0004H)
case 0x0004: // 站号寄存器(可写,掉电保存)
modbus_slave_addr = (u8)write_val; // 站号为单字节,强制转换
limit_slave_addr(); // 合法性校验
eeprom_write_addr(); // 写入EEPROM,掉电保存
break;
// === 新增结束 ===
case 0x0002: // 电压寄存器(只读)
case 0x0003: // 状态寄存器(只读)
return;
default: // 预留寄存器
holding_regs[reg_addr] = write_val;
break;
}
// 组装应答帧(与下发指令一致)
modbus_resp_buf[idx++] = addr;
modbus_resp_buf[idx++] = 0x06;
modbus_resp_buf[idx++] = (reg_addr >> 8) & 0xFF;
modbus_resp_buf[idx++] = reg_addr & 0xFF;
modbus_resp_buf[idx++] = (write_val >> 8) & 0xFF;
modbus_resp_buf[idx++] = write_val & 0xFF;
// CRC校验
crc = crc16(modbus_resp_buf, idx);
modbus_resp_buf[idx++] = (u8)(crc & 0xFF);
modbus_resp_buf[idx++] = (u8)((crc >> 8) & 0xFF);
uart_send_buf(modbus_resp_buf, idx);
}
// 16功能码批量写寄存器函数(核心校零功能)
void modbus10_write(u8 addr, u16 reg_addr, u8 reg_num, u16 *write_buf)
{
u16 crc;u8 idx = 0,i;
if(addr != SLAVE_ADDR) return;
// 批量写入寄存器
for(i=0;i<reg_num;i++)
{
if((reg_addr + i) == 0x0000)
{
water_meter = (water_meter & 0x0000FFFF) | ((u32)write_buf[i] << 16);
eeprom_write();
}
else if((reg_addr + i) == 0x0001)
{
water_meter = (water_meter & 0xFFFF0000) | (u32)write_buf[i];
eeprom_write();
}
// === 新增 === 支持16功能码批量写站号
else if((reg_addr + i) == 0x0004)
{
modbus_slave_addr = (u8)write_buf[i];
limit_slave_addr();
eeprom_write_addr();
}
// === 新增结束 ===
else
{
holding_regs[reg_addr + i] = write_buf[i];
}
}
// 组装应答帧(站号+功能码+起始地址+寄存器数量+CRC)
modbus_resp_buf[idx++] = addr;
modbus_resp_buf[idx++] = 0x10;
modbus_resp_buf[idx++] = (reg_addr >> 8) & 0xFF;
modbus_resp_buf[idx++] = reg_addr & 0xFF;
modbus_resp_buf[idx++] = (reg_num >> 8) & 0xFF;
modbus_resp_buf[idx++] = reg_num & 0xFF;
crc = crc16(modbus_resp_buf, idx);
modbus_resp_buf[idx++] = (u8)(crc & 0xFF);
modbus_resp_buf[idx++] = (u8)((crc >> 8) & 0xFF);
uart_send_buf(modbus_resp_buf, idx);
}
// 更新保持寄存器数据
void update_holding_regs(void)
{
holding_regs[0] = (u16)(water_meter >> 16); // 水度高16位
holding_regs[1] = (u16)(water_meter & 0xFFFF); // 水度低16位
holding_regs[2] = VDDA; // 电池电压(mV)
holding_regs[3] = valid_count_flag; // 水流状态
holding_regs[4] = modbus_slave_addr; // === 新增 === 0004H寄存器映射为当前站号
holding_regs[5] = 0x0000; // 预留
holding_regs[6] = 0x0000;
holding_regs[7] = 0x0000;
}
/**************************************************************************************
* Modbus指令解析函数(仅解析03/06/16功能码)
**************************************************************************************/
void modbus_parse(void)
{
u16 recv_crc, calc_crc;
u16 reg_addr, write_val;
u8 reg_num, byte_len, i;
u16 write_buf[8]; // 批量写数据缓冲区
// 指令长度校验:03/06固定8字节,16功能码长度可变
if(uart_rx_buf[1] == 0x03 || uart_rx_buf[1] == 0x06)
{
if(rx_len != 8) goto parse_end; // 03/06固定8字节
}
else if(uart_rx_buf[1] == 0x10)
{
if(rx_len < 8) goto parse_end; // 16功能码长度≥8,具体由数据长度决定
}
else
{
goto parse_end; // 其他功能码直接丢弃
}
// 站号校验
if(uart_rx_buf[0] != SLAVE_ADDR && uart_rx_buf[0] != 0x00)
goto parse_end;
switch(uart_rx_buf[1])
{
case 0x03: // 读保持寄存器
recv_crc = uart_rx_buf[6] | (uart_rx_buf[7] << 8);
calc_crc = crc16(uart_rx_buf, 6);
if(recv_crc != calc_crc) goto parse_end;
reg_addr = (uart_rx_buf[2] << 8) | uart_rx_buf[3];
reg_num = (uart_rx_buf[4] << 8) | uart_rx_buf[5];
// 寄存器地址合法性校验
if(reg_addr >= HOLD_REG_COUNT || (reg_addr + reg_num) > HOLD_REG_COUNT || reg_num == 0 || reg_num > MAX_READ_NUM)
{
// 返回地址越界错误帧
modbus_resp_buf[0] = SLAVE_ADDR;
modbus_resp_buf[1] = 0x83;
modbus_resp_buf[2] = 0x02;
recv_crc = crc16(modbus_resp_buf, 3);
modbus_resp_buf[3] = recv_crc & 0xFF;
modbus_resp_buf[4] = (recv_crc >> 8) & 0xFF;
uart_send_buf(modbus_resp_buf, 5);
goto parse_end;
}
modbus03_response(SLAVE_ADDR, reg_addr, reg_num);
break;
case 0x06: // 单寄存器写
recv_crc = uart_rx_buf[6] | (uart_rx_buf[7] << 8);
calc_crc = crc16(uart_rx_buf, 6);
if(recv_crc != calc_crc) goto parse_end;
reg_addr = (uart_rx_buf[2] << 8) | uart_rx_buf[3];
write_val = (uart_rx_buf[4] << 8) | uart_rx_buf[5];
modbus06_write(SLAVE_ADDR, reg_addr, write_val);
break;
case 0x10: // 批量写寄存器
printf("Enter 0x10 parse\r\n"); // 调试打印:确认进入16功能码分支
// 第一步:先提取正确的基础参数(顺序不能错!)
reg_addr = (uart_rx_buf[2] << 8) | uart_rx_buf[3]; // 起始地址:字节2=高,字节3=低
reg_num = (uart_rx_buf[4] << 8) | uart_rx_buf[5]; // 寄存器数量:字节4=高,字节5=低
byte_len = uart_rx_buf[6]; // 数据字节数:字节6(关键!顺序不能反)
// 打印提取的参数,验证正确性
printf("byte_len=%d, reg_num=%d, reg_addr=%d\r\n", byte_len, reg_num, reg_addr);
// 第二步:校验参数合法性(新增,防止越界)
if(byte_len != reg_num * 2 || reg_num == 0 || (reg_addr + reg_num) > HOLD_REG_COUNT)
{
printf("Param error: byte_len=%d, reg_num=%d\r\n", byte_len, reg_num);
goto parse_end;
}
// 第三步:正确提取CRC值(范围=站号~最后1字节数据)
// CRC位置 = 7 + byte_len (数据起始位7) → 7+4=11 → uart_rx_buf[11] = 80, uart_rx_buf[12] = 37
recv_crc = uart_rx_buf[7 + byte_len] | (uart_rx_buf[8 + byte_len] << 8);
// CRC计算范围 = 0 ~ 6 + byte_len (站号~最后1字节数据) → 0~10
calc_crc = crc16(uart_rx_buf, 7 + byte_len); // 7+byte_len = 11(0~10共11字节)
// 打印CRC值,验证正确性
printf("recv_crc=0x%04x, calc_crc=0x%04x\r\n", recv_crc, calc_crc);
if(recv_crc != calc_crc)
{
printf("CRC check fail!\r\n"); // 打印CRC失败提示
goto parse_end;
}
// 第四步:正确提取批量写入数据
for(i=0;i<reg_num;i++)
{
// 数据起始位=7 → 7+i*2=高字节,8+i*2=低字节
write_buf[i] = (uart_rx_buf[7 + i*2] << 8) | uart_rx_buf[8 + i*2];
printf("write_buf[%d]=0x%04x\r\n", i, write_buf[i]); // 打印写入值
}
// 执行批量写操作
modbus10_write(SLAVE_ADDR, reg_addr, reg_num, write_buf);
break;
default: // 非法功能码
modbus_resp_buf[0] = SLAVE_ADDR;
modbus_resp_buf[1] = uart_rx_buf[1] | 0x80;
modbus_resp_buf[2] = 0x01; // 错误码:非法功能码
recv_crc = crc16(modbus_resp_buf, 3);
modbus_resp_buf[3] = recv_crc & 0xFF;
modbus_resp_buf[4] = (recv_crc >> 8) & 0xFF;
uart_send_buf(modbus_resp_buf, 5);
break;
}
parse_end:
rx_len = 0;
rx_complete = 0;
}
/**************************************************************************************
* 串口中断服务函数 - 接收Modbus指令,触发唤醒
**************************************************************************************/
void uart_isr(void) interrupt 4
{
if(RI) // 检测接收中断
{
RI = 0;
if(rx_len < sizeof(uart_rx_buf))
{
uart_rx_buf[rx_len++] = SBUF;
modbus_timeout = 0;
wakeup_proc(); // 接收到数据,唤醒设备
}
}
}
/**************************************************************************************
* EEPROM相关函数 - 基于参考代码重构(STC8H 12MHz晶振适配)
**************************************************************************************/
// IAP空闲函数(完全复用官方代码)
void iap_idle(void)
{
IAP_CONTR = 0; // 关闭IAP功能
IAP_CMD = 0; // 清除命令寄存器
IAP_TRIG = 0; // 清除触发寄存器
IAP_ADDRH = 0x80; // 将地址设置到非IAP区域,防止误操作
IAP_ADDRL = 0;
}
// 读取EEPROM一个字节(参考官方IapRead函数)
u8 eeprom_read_byte(u16 addr)
{
u8 dat;
EA = 0; // 关闭总中断,防止操作被打断
IAP_CONTR = 0x80; // 使能IAP(官方代码固定值)
IAP_CMD = 1; // 设置IAP读命令(官方代码0x01)
IAP_ADDRL = addr; // 设置IAP低地址
IAP_ADDRH = addr >> 8; // 设置IAP高地址
IAP_TRIG = 0x5a; // 官方代码:写触发命令1
IAP_TRIG = 0xa5; // 官方代码:写触发命令2
_nop_(); // 等待操作完成
dat = IAP_DATA; // 读取数据
iap_idle(); // 关闭IAP功能
EA = 1; // 恢复总中断
return dat;
}
// 写入一个字节到EEPROM(参考官方IapProgram函数)
void eeprom_write_byte(u16 addr, u8 dat)
{
EA = 0; // 关闭总中断
IAP_CONTR = 0x80; // 使能IAP
IAP_CMD = 2; // 设置IAP写命令(官方代码0x02)
IAP_ADDRL = addr; // 设置IAP低地址
IAP_ADDRH = addr >> 8; // 设置IAP高地址
IAP_DATA = dat; // 写入数据
IAP_TRIG = 0x5a; // 触发指令1
IAP_TRIG = 0xa5; // 触发指令2
_nop_(); // 等待写入完成
iap_idle(); // 关闭IAP
EA = 1; // 恢复总中断
}
// 擦除EEPROM扇区(参考官方IapErase函数)
void eeprom_page_erase(u16 addr)
{
EA = 0; // 关闭总中断
IAP_CONTR = 0x80; // 使能IAP
IAP_CMD = 3; // 设置IAP擦除命令(官方代码0x03,关键修正!)
IAP_ADDRL = addr; // 设置IAP低地址
IAP_ADDRH = addr >> 8; // 设置IAP高地址
IAP_TRIG = 0x5a; // 触发指令1
IAP_TRIG = 0xa5; // 触发指令2
_nop_(); // 等待擦除完成
iap_idle(); // 关闭IAP
EA = 1; // 恢复总中断
}
// 上电读取EEPROM中的水度值
void eeprom_read(void)
{
u8 i;
u8 data_buf[IAP_DATA_LEN];
// 初始化IAP等待参数(官方代码核心步骤,之前缺失!)
IAP_TPS = 11; // 适配11.0592MHz晶振的等待参数,仅需初始化一次
// 从IAP_ADDR读取4字节水度值
for(i=0; i<IAP_DATA_LEN; i++)
{
data_buf[i] = eeprom_read_byte(IAP_ADDR + i);
}
// 拼接为32位水度值
water_meter = ((u32)data_buf[0] << 24) |
((u32)data_buf[1] << 16) |
((u16)data_buf[2] << 8) |
data_buf[3];
}
// 水度值更新后写入EEPROM(参考官方擦除+写入流程)
void eeprom_write(void) reentrant
{
u8 i;
u8 data_buf[IAP_DATA_LEN];
// 初始化IAP等待参数
IAP_TPS = 11;
// 拆分32位水度值为4个字节
data_buf[0] = (u8)(water_meter >> 24) & 0xFF;
data_buf[1] = (u8)(water_meter >> 16) & 0xFF;
data_buf[2] = (u8)(water_meter >> 8) & 0xFF;
data_buf[3] = (u8)water_meter & 0xFF;
// 先擦除扇区(官方代码流程:擦除→写入)
eeprom_page_erase(IAP_ADDR);
// 逐个写入4字节数据
for(i=0; i<IAP_DATA_LEN; i++)
{
eeprom_write_byte(IAP_ADDR + i, data_buf[i]);
}
}
// === 新增 === 上电读取EEPROM中保存的站号
void eeprom_read_addr(void)
{
IAP_TPS = 11; // 初始化IAP等待参数
modbus_slave_addr = eeprom_read_byte(IAP_ADDR_ADDR);
}
// === 新增 === 站号更新后写入EEPROM
void eeprom_write_addr(void) reentrant
{
IAP_TPS = 11; // 初始化IAP等待参数
eeprom_page_erase(IAP_ADDR_ADDR); // 先擦除扇区
eeprom_write_byte(IAP_ADDR_ADDR, modbus_slave_addr); // 写入站号
}
// === 新增 === 站号合法性校验(兜底保障)
void limit_slave_addr(void)
{
// Modbus标准:站号范围1~247,非法值重置为1
if(modbus_slave_addr < 1 || modbus_slave_addr > 247)
{
modbus_slave_addr = 1;
}
}
|
|