[quote][size=2][url=forum.php?mod=redirect&goto=findpost&pid=55759&ptid=6349][color=#999999]manzunzu 发表于 2024-1-30 09:35[/color][/url][/size]
大神们帮忙看看什么问题[/quote]
好的,我已经查阅了STC官方论坛和手册资料。下面我来为你分析这个问题,并提供一个完整的解决方案。
问题分析
你遇到的问题非常典型:DS18B20 的单总线通信对时序要求极其严格(微秒级),而 Modbus RTU 的串口中断会随时打断 DS18B20 的读写时序,导致通信失败。
核心矛盾在于:
- DS18B20 的读写时序需要精确的微秒级延时(如复位脉冲 480μs、读时隙 60μs 等)
- Modbus 串口中断(尤其是接收中断)随时可能触发,打断正在进行的 DS18B20 时序
- 即使关中断操作 DS18B20,如果关中断时间过长,又会丢失 Modbus 数据帧
解决方案:状态机 + 分步执行
你的思路完全正确——将 read_temp() 拆分为多个步骤,用状态机在主循环中分时执行,这样每个步骤的执行时间很短(微秒级),不会长时间关中断影响 Modbus 通信。
整体架构设计
主循环:
1. 处理 Modbus RTU 协议(串口中断接收,主循环解析)
2. 调用 DS18B20 状态机(每次执行一个步骤,耗时 < 200μs)
3. 更新温度值供 Modbus 读取
DS18B20 状态机设计
DS18B20 的一次完整温度转换+读取流程分为以下步骤:
| 步骤 |
操作 |
说明 |
最大耗时 |
| 0 |
复位 + 发送 Skip ROM (0xCC) + 启动转换 (0x44) |
启动温度转换 |
~1ms (复位) |
| 1 |
等待转换完成(约 750ms) |
此步骤不阻塞,只是等待定时器计数 |
750ms |
| 2 |
复位 + 发送 Skip ROM (0xCC) + 读暂存器 (0xBE) |
准备读取温度 |
~1ms |
| 3 |
读取温度低字节 |
逐个位读取 |
~1ms |
| 4 |
读取温度高字节 |
逐个位读取 |
~1ms |
| 5 |
计算温度值,标记完成 |
纯计算 |
< 10μs |
关键优化:步骤 1(等待 750ms 转换)完全不需要阻塞 CPU,只需让状态机等待定时器计数到时间后再进入下一步。
完整代码实现
以下是基于 STC8H 系列(兼容 STC8G/STC8A)的完整代码,包含 Modbus RTU 从站 + DS18B20 非阻塞读取。
文件 1: main.c
/*---------------------------------------------------------------------*/
/* --- STC MCU International Limited ----------------------------------*/
/* --- Web: www.STCAI.com ---------------------------------------------*/
/* --- BBS: www.STCAIMCU.com -----------------------------------------*/
/* --- 功能: Modbus RTU 从站 + DS18B20 非阻塞读取 */
/* --- 主时钟: 24MHz (用户可自行修改) */
/* --- MCU: STC8H8K64U / STC8G / STC8A 系列通用 */
/*---------------------------------------------------------------------*/
#include "stc8h.h"
#include "intrins.h"
#define MAIN_Fosc 24000000L // 定义主时钟频率
typedef unsigned char u8;
typedef unsigned int u16;
typedef unsigned long u32;
/**************************** 引脚定义 *********************************/
sbit DQ = P3^3; // DS18B20 数据引脚
/*************************** Modbus 参数 *******************************/
#define MODBUS_SLAVE_ID 0x01 // 本机 Modbus 地址
#define MODBUS_BAUD 9600 // Modbus 波特率
/*************************** DS18B20 状态机 ****************************/
#define DS18B20_IDLE 0 // 空闲状态
#define DS18B20_START_CONV 1 // 启动温度转换
#define DS18B20_WAIT_CONV 2 // 等待转换完成
#define DS18B20_START_READ 3 // 开始读取温度
#define DS18B20_READ_LSB 4 // 读取低字节
#define DS18B20_READ_MSB 5 // 读取高字节
#define DS18B20_CALC_DONE 6 // 计算完成
#define DS18B20_CONV_TIME 750 // 转换等待时间 (ms)
/**************************** 全局变量 *********************************/
// DS18B20 相关
u8 ds18b20_state = DS18B20_IDLE; // 状态机当前状态
u8 ds18b20_bit_count; // 位计数器
u8 ds18b20_byte_val; // 当前读取的字节值
u8 ds18b20_temp_lsb; // 温度低字节
u8 ds18b20_temp_msb; // 温度高字节
u16 ds18b20_conv_timer; // 转换等待计时器
bit ds18b20_data_ready = 0; // 温度数据就绪标志
int ds18b20_temperature = 0; // 最终温度值 (实际温度 * 10)
// Modbus 相关
u8 modbus_rx_buf[32]; // 串口接收缓冲区
u8 modbus_rx_len; // 接收数据长度
u8 modbus_rx_index; // 接收索引
bit modbus_rx_flag = 0; // 接收完成标志
u8 modbus_tx_buf[32]; // 串口发送缓冲区
u8 modbus_tx_len; // 发送数据长度
// 定时器相关
volatile u16 sys_tick = 0; // 系统心跳 (1ms)
/************************ 函数声明 *************************************/
void Timer0_Init(void);
void UART1_Init(void);
void DS18B20_StateMachine(void);
void Modbus_Poll(void);
void Modbus_SendResponse(u8 *buf, u8 len);
// DS18B20 底层操作函数
void DS18B20_Delay1us(u8 n);
bit DS18B20_Reset(void);
void DS18B20_WriteByte(u8 dat);
u8 DS18B20_ReadByte(void);
/************************ 主函数 ***************************************/
void main(void)
{
P0M0 = 0x00; P0M1 = 0x00; // 设置所有IO为准双向口
P1M0 = 0x00; P1M1 = 0x00;
P2M0 = 0x00; P2M1 = 0x00;
P3M0 = 0x00; P3M1 = 0x00;
P4M0 = 0x00; P4M1 = 0x00;
P5M0 = 0x00; P5M1 = 0x00;
Timer0_Init(); // 初始化定时器0 (1ms节拍)
UART1_Init(); // 初始化串口1 (Modbus)
EA = 1; // 开启总中断
while(1)
{
DS18B20_StateMachine(); // 处理 DS18B20 状态机 (非阻塞)
Modbus_Poll(); // 处理 Modbus 协议
}
}
/*********************** 定时器0初始化 (1ms) ***************************/
void Timer0_Init(void)
{
// 定时器0, 16位自动重装, 1ms中断
// 24MHz, 12T模式: 计数脉冲 = 24MHz/12 = 2MHz
// 重装值 = 65536 - 2000 = 63536 = 0xF830
AUXR |= 0x80; // 定时器0为1T模式 (更快)
// 若使用12T模式: AUXR &= ~0x80;
TMOD &= ~0x0F; // 定时器0模式0 (16位自动重装)
TMOD |= 0x00;
// 24MHz 1T模式: 计数脉冲 = 24MHz, 1ms = 24000 计数
// 重装值 = 65536 - 24000 = 41536 = 0xA240
u16 reload = 65536UL - (MAIN_Fosc / 1000);
TH0 = (u8)(reload >> 8);
TL0 = (u8)(reload);
ET0 = 1; // 允许定时器0中断
TR0 = 1; // 启动定时器0
}
/*********************** 串口1初始化 (Modbus) **************************/
void UART1_Init(void)
{
// 串口1, 模式1 (8位UART, 可变波特率), 使用定时器2做波特率发生器
// 波特率 = 9600bps @ 24MHz
S1_S1 = 0; // 串口1选择P3.0/P3.1 (默认)
S1_S0 = 0;
SCON = 0x50; // 模式1, 允许接收
// 定时器2做波特率发生器
// 波特率 = 定时器2溢出率 / 4
// 定时器2溢出率 = MAIN_Fosc / (65536 - RL2)
// RL2 = 65536 - MAIN_Fosc / 4 / 波特率
// RL2 = 65536 - 24000000 / 4 / 9600 = 65536 - 625 = 64911 = 0xFD8F
u16 baud_reload = 65536UL - MAIN_Fosc / 4 / MODBUS_BAUD;
T2L = (u8)(baud_reload);
T2H = (u8)(baud_reload >> 8);
AUXR |= 0x14; // 定时器2开始运行, 1T模式
ES = 1; // 允许串口1中断
}
/*********************** 串口1中断服务函数 *****************************/
void UART1_ISR(void) interrupt 4
{
if(RI)
{
RI = 0;
u8 dat = SBUF;
// 接收超时处理: 如果超过3.5字符时间没有新数据, 认为一帧结束
// 这里简化处理: 接收缓冲区满或收到足够数据后置标志
if(modbus_rx_len < sizeof(modbus_rx_buf))
{
modbus_rx_buf[modbus_rx_len++] = dat;
}
// 简单超时判断: 每收到一个字节重置定时器
// 实际项目中建议使用定时器做3.5字符超时判断
// 这里使用一个简单的计数方式
modbus_rx_index = 0; // 重置超时计数
}
if(TI)
{
TI = 0;
}
}
/*********************** 定时器0中断服务函数 ***************************/
void Timer0_ISR(void) interrupt 1
{
sys_tick++; // 1ms 递增
// Modbus 接收超时处理 (3.5字符时间)
// 9600bps: 1字符 ≈ 1ms, 3.5字符 ≈ 3.5ms
if(modbus_rx_len > 0)
{
modbus_rx_index++;
if(modbus_rx_index >= 4) // 约4ms超时
{
modbus_rx_flag = 1; // 标记接收完成
modbus_rx_index = 0;
}
}
}
/********************** DS18B20 微秒延时函数 ***************************/
void DS18B20_Delay1us(u8 n)
{
// 24MHz下, 1个nop约0.0417us
// 延时 n 微秒
while(n--)
{
_nop_(); _nop_(); _nop_(); _nop_(); _nop_();
_nop_(); _nop_(); _nop_(); _nop_(); _nop_();
_nop_(); _nop_(); _nop_(); _nop_(); _nop_();
_nop_(); _nop_(); _nop_(); _nop_(); _nop_();
_nop_(); _nop_(); _nop_(); _nop_(); _nop_();
_nop_(); _nop_();
}
}
/********************** DS18B20 复位 ***********************************/
bit DS18B20_Reset(void)
{
bit presence;
EA = 0; // 关中断保护时序
DQ = 0; // 拉低总线
DS18B20_Delay1us(480); // 保持480us
DQ = 1; // 释放总线
DS18B20_Delay1us(60); // 等待60us
presence = DQ; // 读取存在脉冲
DS18B20_Delay1us(420); // 等待剩余时间
EA = 1; // 开中断
return presence; // 0=存在, 1=不存在
}
/********************** DS18B20 写一个字节 *****************************/
void DS18B20_WriteByte(u8 dat)
{
u8 i;
EA = 0; // 关中断保护时序
for(i = 0; i < 8; i++)
{
DQ = 0; // 拉低总线
_nop_(); // 延时约1us
DQ = dat & 0x01; // 输出数据位 (低位在前)
DS18B20_Delay1us(60); // 保持60us
DQ = 1; // 释放总线
_nop_(); // 恢复时间
dat >>= 1;
}
EA = 1; // 开中断
}
/********************** DS18B20 读一个字节 *****************************/
u8 DS18B20_ReadByte(void)
{
u8 i, dat = 0;
EA = 0; // 关中断保护时序
for(i = 0; i < 8; i++)
{
dat >>= 1;
DQ = 0; // 拉低总线启动读时隙
_nop_(); // 延时约1us
DQ = 1; // 释放总线
_nop_(); // 等待约1us
if(DQ) dat |= 0x80; // 读取数据位
DS18B20_Delay1us(60); // 等待时隙结束
}
EA = 1; // 开中断
return dat;
}
/********************** DS18B20 状态机 (非阻塞) ************************/
void DS18B20_StateMachine(void)
{
static u8 temp_lsb, temp_msb;
switch(ds18b20_state)
{
case DS18B20_IDLE:
// 空闲状态, 启动新一轮温度转换
ds18b20_state = DS18B20_START_CONV;
break;
case DS18B20_START_CONV:
// 步骤1: 复位 + 发送Skip ROM + 启动温度转换
DS18B20_Reset();
DS18B20_WriteByte(0xCC); // Skip ROM
DS18B20_WriteByte(0x44); // 启动温度转换
ds18b20_conv_timer = 0; // 重置等待计时器
ds18b20_state = DS18B20_WAIT_CONV;
break;
case DS18B20_WAIT_CONV:
// 步骤2: 等待转换完成 (非阻塞, 靠定时器计数)
// 每次进入此状态只检查计时, 不阻塞
if(ds18b20_conv_timer >= DS18B20_CONV_TIME)
{
ds18b20_state = DS18B20_START_READ;
}
// 否则什么都不做, 等待下次主循环调用
break;
case DS18B20_START_READ:
// 步骤3: 复位 + 发送Skip ROM + 发送读暂存器命令
DS18B20_Reset();
DS18B20_WriteByte(0xCC); // Skip ROM
DS18B20_WriteByte(0xBE); // 读暂存器
ds18b20_bit_count = 0;
ds18b20_byte_val = 0;
ds18b20_state = DS18B20_READ_LSB;
break;
case DS18B20_READ_LSB:
// 步骤4: 读取温度低字节 (8位)
// 每次主循环读1位, 分8次读完
if(ds18b20_bit_count < 8)
{
u8 bit_val;
EA = 0;
DQ = 0; // 拉低启动读时隙
_nop_();
DQ = 1;
_nop_();
bit_val = DQ ? 1 : 0;
DS18B20_Delay1us(60);
EA = 1;
ds18b20_byte_val >>= 1;
if(bit_val) ds18b20_byte_val |= 0x80;
ds18b20_bit_count++;
}
else
{
temp_lsb = ds18b20_byte_val;
ds18b20_bit_count = 0;
ds18b20_byte_val = 0;
ds18b20_state = DS18B20_READ_MSB;
}
break;
case DS18B20_READ_MSB:
// 步骤5: 读取温度高字节 (8位)
if(ds18b20_bit_count < 8)
{
u8 bit_val;
EA = 0;
DQ = 0;
_nop_();
DQ = 1;
_nop_();
bit_val = DQ ? 1 : 0;
DS18B20_Delay1us(60);
EA = 1;
ds18b20_byte_val >>= 1;
if(bit_val) ds18b20_byte_val |= 0x80;
ds18b20_bit_count++;
}
else
{
temp_msb = ds18b20_byte_val;
ds18b20_state = DS18B20_CALC_DONE;
}
break;
case DS18B20_CALC_DONE:
// 步骤6: 计算温度值
{
u16 temp_raw = ((u16)temp_msb << 8) | temp_lsb;
// DS18B20 分辨率12位: 0.0625°C/LSB
// 温度值 = temp_raw * 0.0625
// 这里用整数表示, 实际温度 * 10
if(temp_raw & 0x8000) // 负温度
{
temp_raw = (~temp_raw) + 1;
ds18b20_temperature = -((int)(temp_raw * 625L / 1000));
}
else
{
ds18b20_temperature = (int)(temp_raw * 625L / 1000);
}
ds18b20_data_ready = 1;
}
ds18b20_state = DS18B20_IDLE; // 回到空闲, 开始新一轮
break;
default:
ds18b20_state = DS18B20_IDLE;
break;
}
}
/********************** Modbus CRC16 计算 ******************************/
u16 Modbus_CRC16(u8 *buf, u8 len)
{
u16 crc = 0xFFFF;
u8 i;
while(len--)
{
crc ^= *buf++;
for(i = 0; i < 8; i++)
{
if(crc & 0x0001)
crc = (crc >> 1) ^ 0xA001;
else
crc >>= 1;
}
}
return crc;
}
/********************** Modbus 协议处理 ********************************/
void Modbus_Poll(void)
{
u8 i;
u16 crc, crc_recv;
if(!modbus_rx_flag) return; // 没有收到完整帧
modbus_rx_flag = 0;
// 检查帧长度 (至少4字节: 地址+功能码+CRC低+CRC高)
if(modbus_rx_len < 4)
{
modbus_rx_len = 0;
return;
}
// 检查CRC
crc = Modbus_CRC16(modbus_rx_buf, modbus_rx_len - 2);
crc_recv = (u16)modbus_rx_buf[modbus_rx_len - 1] << 8 |
modbus_rx_buf[modbus_rx_len - 2];
if(crc != crc_recv)
{
modbus_rx_len = 0;
return; // CRC错误, 丢弃
}
// 检查从机地址
if(modbus_rx_buf[0] != MODBUS_SLAVE_ID && modbus_rx_buf[0] != 0x00)
{
modbus_rx_len = 0;
return; // 不是发给本机
}
// 解析功能码
switch(modbus_rx_buf[1])
{
case 0x03: // 读保持寄存器
// 读取 DS18B20 温度值
{
u16 reg_addr = (u16)modbus_rx_buf[2] << 8 | modbus_rx_buf[3];
u16 reg_count = (u16)modbus_rx_buf[4] << 8 | modbus_rx_buf[5];
// 只支持读取地址0x0000的寄存器 (温度值)
if(reg_addr == 0x0000 && reg_count == 1)
{
modbus_tx_buf[0] = MODBUS_SLAVE_ID;
modbus_tx_buf[1] = 0x03;
modbus_tx_buf[2] = 2; // 数据字节数
modbus_tx_buf[3] = (u8)(ds18b20_temperature >> 8); // 高字节
modbus_tx_buf[4] = (u8)(ds18b20_temperature); // 低字节
crc = Modbus_CRC16(modbus_tx_buf, 5);
modbus_tx_buf[5] = (u8)(crc);
modbus_tx_buf[6] = (u8)(crc >> 8);
Modbus_SendResponse(modbus_tx_buf, 7);
}
else
{
// 非法地址, 返回异常
modbus_tx_buf[0] = MODBUS_SLAVE_ID;
modbus_tx_buf[1] = 0x83; // 异常功能码
modbus_tx_buf[2] = 0x02; // 非法数据地址
crc = Modbus_CRC16(modbus_tx_buf, 3);
modbus_tx_buf[3] = (u8)(crc);
modbus_tx_buf[4] = (u8)(crc >> 8);
Modbus_SendResponse(modbus_tx_buf, 5);
}
}
break;
case 0x06: // 写单个寄存器 (预留)
// 可在此添加写寄存器功能
{
// 返回异常: 不支持
modbus_tx_buf[0] = MODBUS_SLAVE_ID;
modbus_tx_buf[1] = 0x86;
modbus_tx_buf[2] = 0x01; // 非法功能码
crc = Modbus_CRC16(modbus_tx_buf, 3);
modbus_tx_buf[3] = (u8)(crc);
modbus_tx_buf[4] = (u8)(crc >> 8);
Modbus_SendResponse(modbus_tx_buf, 5);
}
break;
default:
// 不支持的功能码
modbus_tx_buf[0] = MODBUS_SLAVE_ID;
modbus_tx_buf[1] = modbus_rx_buf[1] | 0x80;
modbus_tx_buf[2] = 0x01; // 非法功能码
crc = Modbus_CRC16(modbus_tx_buf, 3);
modbus_tx_buf[3] = (u8)(crc);
modbus_tx_buf[4] = (u8)(crc >> 8);
Modbus_SendResponse(modbus_tx_buf, 5);
break;
}
modbus_rx_len = 0;
}
/********************** Modbus 发送响应 ********************************/
void Modbus_SendResponse(u8 *buf, u8 len)
{
u8 i;
for(i = 0; i < len; i++)
{
SBUF = buf[i];
while(!TI); // 等待发送完成
TI = 0;
}
}
文件 2: stc8h.h 头文件
请使用 STC 官方提供的头文件 stc8h.h,可在 STC-ISP 下载软件中找到,或从 STC 官网下载。
关键设计要点说明
1. 状态机分步执行
| 步骤 |
每次执行耗时 |
说明 |
DS18B20_START_CONV |
~1ms (关中断) |
复位+写命令,关中断保护 |
DS18B20_WAIT_CONV |
< 1μs |
仅检查计时器,不阻塞 |
DS18B20_START_READ |
~1ms (关中断) |
复位+写命令 |
DS18B20_READ_LSB |
~65μs/位 (关中断) |
每次主循环读1位 |
DS18B20_READ_MSB |
~65μs/位 (关中断) |
每次主循环读1位 |
DS18B20_CALC_DONE |
< 10μs |
纯计算,无需关中断 |
每次关中断时间最长约 65μs(读一个位),远小于 Modbus 的字符间隔(9600bps 下约 1ms),不会导致 Modbus 帧丢失。
2. 主循环周期
DS18B20_START_CONV → (750ms等待) → START_READ → READ_LSB(8次) → READ_MSB(8次) → CALC_DONE
- 等待转换的 750ms 完全不占用 CPU
- 读取阶段每次主循环读 1 位,主循环周期约 2~3ms
- Modbus 随时可以响应,不会被长时间阻塞
3. Modbus 寄存器映射
| 寄存器地址 |
内容 |
类型 |
说明 |
| 0x0000 |
温度值 |
只读 |
实际温度 × 10 (如 25.3°C → 253) |
使用说明
-
硬件连接:
- DS18B20 的 DQ 接 P3.3(可修改
sbit DQ = P3^3 更改)
- DS18B20 的 VDD 接 3.3V/5V,GND 接 GND
- DQ 引脚需接 4.7kΩ 上拉电阻到 VCC
- 串口1 (P3.0 TXD, P3.1 RXD) 接 Modbus 总线
-
Modbus 通信参数:
- 波特率:9600
- 数据位:8
- 停止位:1
- 校验:无
- 从机地址:0x01
-
读取温度:
- 发送 Modbus 命令:
01 03 00 00 00 01 84 0A
- 返回:
01 03 02 [温度高字节] [温度低字节] [CRC]
注意事项
- DS18B20 上拉电阻必须加,否则通信会失败
- 关中断时间:每次读位操作关中断约 65μs,在 9600bps 下完全安全。如果使用更高的 Modbus 波特率(如 115200),字符间隔更短,建议进一步优化——将读位操作也拆分为更细的步骤
- DS18B20 的 750ms 转换等待是在
WAIT_CONV 状态中通过 sys_tick 计时实现的,完全非阻塞
- 如果使用 STC8G/STC8A 系列,代码完全兼容,只需更换对应的头文件
这个架构可以轻松扩展到其他对时序敏感的外设(如 DHT11/DHT22、单总线 LED 等),配合 Modbus 使用。