目录
前言
提示:老板让调HART然后就调了
提示:读红字
一、关键词
示例:HART 调制 解调 4-20mA hart协议 仪表通讯
二、什么是HART
三、HART协议
一、什么是HART协议
二、物理层
1200波特率频移键控(FSK)将数字信息叠加到传统的4-20mA模拟信号上,以1200 Hz 代表逻辑“1”,2200 Hz 代表逻辑“0”,在4~20 mA 电流上叠加幅度为0. 5mA的正弦调制波。
三、数据链路层
1.概述
2. 数据帧格式
Preamble | Delimiter | Address | Expansion bytes | Command | Bytes Count | Data | Check Byte |
---|---|---|---|---|---|---|---|
前导码 | 定界符 | 地址 | 扩展字节 | 命令 | 数据个数 | 数据 | 校验 |
5-20个字节 (固定FF) | 1个字节 | 短帧1字节 长帧5字节 | 0-3个字节 | 1个字节 | 1个字节 | n个字节 | 1个字节 |
①.Preamble 前导码
导言字节,一般是 5~20 个 FF 十六进制字节。他实际上是同步信号,各通讯设备可以据此略做调整,保证信息的同步。在开始通讯的时候,使用的是 20 个 FF 导言,从机应答 0信号时将告之主机他“希望”接收几个字节的导言,另外主机也可以用 59 号命令告诉从机应答时应用几位导言。
②.Delimiter 定界符
起始字节,他将告之使用的结构为“长”还是“短”、消息源、是否是“突发”模式消息。主机到从机为短结构时,起始位为 02,长帧时为 82。从机到主机的短结构值为 06,长结构值为86。而为“突发”模式的短结构值为 01,长结构为 81。一般设备进行通讯接收到 2 个 FF 字节后,就将侦听起始位。
Bit7 | Bit6 | Bit5 | Bit4 | Bit3 | Bit2 | Bit1 | Bit0 |
地址类型 | 扩展字节数 | 物理层类型 | 帧类型(传输方向) | ||||
0:短帧1个字节 1:长帧5个字节 | 00 | 00:异步(FSK) 01:同步(ACK) | 001:突发模式 010:主机向从机请求数据(STX) 110:从机向主机回复数据(ACK) |
0x02 | 主机到从机的短帧 |
0x82 | 主机到从机的长帧 |
0x06 | 从机到主机的短帧 |
0x86 | 从机到主机的长帧 |
0x01 | 突发模式短帧 |
0x81 | 突发模式长帧 |
③.Address 地址
短帧结构包含了主机地址和从机地址,短结构中占 1 字节,长结构中占 5 字节。无论长结构还是短结构,因为 HART 协议中允许 2 个主机存在,所以我们用首字节的最高位来进行区分,值为 1 表示第一主机地址,第二主机用 0 表示。“突发”模式是特例,0,1 值将交替出现,也就是说,在该模式下,赋予 2 个主机的机会均等。次高位为 1 表示为“突发”模式,短结构用首字节的 0~4 位表示值为 0~15 的从机地址,第 5,6 位赋 0;而长结构用后 6 位表示从机的生产厂商的代码,第 2 个字节表示从机设备型号代码,后 3~5 个字节表示从机的设备序列号,构成“唯一”标志码。另外,长结构的低 38 位如果都是 0 的话表示的是广播地址,即消息发送给所有的设备。
1. 短帧地址结构
Bit7 | Bit6 | Bit5 | Bit4 | Bit3 | Bit2 | Bit1 | Bit0 |
地址类型 | 模式 | 00 | 00 | 短帧地址 | |||
0:第一主机 1:第二主机 | 0:正常模式 1:从机在突发模式 | 保留 | 轮询从机地址 |
• Bit4和Bit5保留必须为00
• 最低有效4位指定轮询地址0-15
2. 长帧地址结构
Byte0 | Byte1 | Byte2 | Byte3 | Byte4 | |||||||
Bit7 | Bit6 | Bit5 | Bit4 | Bit3 | Bit2 | Bit1 | Bit0 | 从机设备型号 | 从机设备序列号 | ||
0:第一主机 1:第二主机 | 0:正常模式 1:从机在突发模式 | 从机生产厂商代码 |
• 制造商ID的最低有效6位。有效的制造商ID可以是公司(制造商)识别码。
• 1字节设备类型代码。该代码由制造商因为只有制造商ID的较低6位用于地址,设备类型代码应按表1所示分配。进一步的有关分配设备类型代码的规范可以在命令中找到摘要规范:第6.1节兼容性规则
• 一个3字节的设备标识符。这与序列号相似,因为每个设备使用相同设备类型代码制造的设备必须具有不同的设备标识符。
④.Command 命令
功能码,指明一个数据帧的具体实现功能,命令有通用命令、普通应用命令、设备专用命令三大类。
⑤.Bytes Count 数据字节数
指实际的数据 Data 的数据。他的值表示的是 BCNT 下一个字节到最后(不包括校验字节)的字节数。接收设备用他可以鉴别出校验字节,也可以知道消息的结束。因为规定数据最多为 25 字节,所以他的值是从 0~27。
⑥.Data 数据
设置或读取指定从机的参数数据(通信的最终结果)。
⑦.Check Byte 校验字节
从定界符到数据的所有字节的“异或”值,即纵向校验。
协议解析的例子百度很多,后面上传的资料中也有很多。
四、HART硬件方案
硬件方案使用了SDIC(晶华微)SD2057方案.
SD2057datasheet
手册上很详细强烈建议精读
HART_OUT:hart芯片根据RXD的数据调制出的模拟信号,叠加在4-20mA环路中。
HART_IN:接收带有调制信号的环路模拟信号后在hart芯片中解调通过TXD输出。
H_OUT内部有0.75v直流偏置,调制信号幅值±250mv,最终出来的信号就是0.75±0.25=0.5-1v正弦波
HART_OUT芯片脚的波形如上图所示,但是要叠加在控制环路电流的芯片控制脚或者运放脚时,需要将直流分量滤掉,所以需要低通加高通滤波,最终H_OUT波形如下图:
H_IN发送与回复幅值(前面发送后面应答):硬件电路如果没错的话,一般调制出来的波形应该正常,反正我是硬件一次OK,但是软件卡了两个星期(后面发现是个小问题,但容易忽略)。
电流环方案一大堆,可以用集成芯片也可以用运放三极管去做,hart硬件没有任何难度,别人都做好了方案。重点介绍下软件协议。
五、OPEN_HART
hart protocol implement
一.帧结构体定义
按照数据链路层协议格式构造数据结构:
typedef struct
{
unsigned char preamble_num;
unsigned char delimiter;
unsigned char address_size;
unsigned char cmd;
unsigned char byte_count;
unsigned char data_buf[50];
unsigned char check_byte;
}frame_type;
typedef struct
{
unsigned char manufacturer_id;
unsigned char device_type;
unsigned char unique_device_id[3];
}long_addr_type;
long_addr_type long_addr = {
MANUFACTURER_ID,DEVICE_TYPE,UNIQUE_DEVICE_ID0,UNIQUE_DEVICE_ID1,UNIQUE_DEVICE_ID2,
};
二.状态机
两个原始状态机,一个接收机和一个传输机。它们在数据传输和接收时进行状态转换,由“xmt_msg”表示和“rcv_msg”两个中断函数去动作。通常这些状态的切换是通过中断去实现的。
void USART3_IRQHandler(void)
{
if(USART_GetITStatus(USART3,USART_IT_RXNE))
{
hart_rcv_msg();
}
else if(USART_GetITStatus(USART3,USART_IT_TXE))
{
hart_xmt_msg();
}
}
1.接收状态机
定义接收状态:
/* receive state machine */
typedef enum
{
RCV_WAIT_IDLE,
RCV_WAIT_START,
RCV_READ,
RCV_DONE,
}rsm_state;
没有通信时状态机处于空闲状态,当收到前导码FF后进入开始接收状态,此时判断前导字节个数,每个字节间隔时间GAP,用定时器去标记(Set GAP)
定义定时器,去定时器中断里累加判断,置标志位
#define TMR_CNT 3
typedef struct soft_timer
{
unsigned int cnt;
unsigned char flg;
}soft_timer;
因为源码比较多且复杂,挑点重要的讲讲思路,大家有时间自己琢磨下
#define GAP_TIMER 0 //帧空隙
#define SLAVE_TIMER 1 //从机响应最大时间 28个 CHaracter times 256.7ms
#define BT_TIMER 2 // 突发模式响应时间
此处顺带提一下从机响应最大时间 ,刚开始移植的时候忽略这个时间导致通讯失败
若超时接收状态为错误
/* STX : a master to slave message */
/* ACK : a slave to master message */
/* BACK : a slave message transmited to a master without an STX*/
/* the type of message received */
typedef enum
{
RCV_ERR = 0xff,
RCV_COMM_ERR = 0xee,
RCV_STX = 0x02, //根据数据帧结构定界符
RCV_ACK = 0x06,
RCV_BACK = 0x01,
}rcv_msg_type;
接收中断服务函数:顺着协议,顺着状态转换图
void hart_rcv_msg(void)
{
static unsigned char PreambleNum = 0;
static unsigned int ByteCount;
unsigned char Byte;
serical_get_byte(&Byte);
switch(g_RcvState)
{
case RCV_WAIT_IDLE:
if(Byte == PREAMBLE)
{
PreambleNum = 1;
g_RcvState = RCV_WAIT_START;
set_delay_time(GAP_TIMER,HRT_GAPT);
}
break;
case RCV_WAIT_START:
if(is_timeout_id(GAP_TIMER))
{
g_RcvMsgType = RCV_ERR;
}
else
{
if(Byte == PREAMBLE)
{
PreambleNum++;
}
else
{
switch(Byte)
{
case (SHORT_FRAME|RCV_STX):
if(PreambleNum > 1)
{
g_Rx.address_size = SHORT_ADDR_SIZE;
g_RcvMsgType = RCV_STX;
s_RcvBufferPos = 0;
g_Rx.data_buf[s_RcvBufferPos++] = Byte;
g_RcvState = RCV_READ;
}
else
{
g_RcvMsgType = RCV_ERR;
}
break;
case (SHORT_FRAME|RCV_ACK):
if(PreambleNum > 1)
{
g_Rx.address_size = SHORT_ADDR_SIZE;
g_RcvMsgType = RCV_ACK;
s_RcvBufferPos = 0;
g_Rx.data_buf[s_RcvBufferPos++] = Byte;
g_RcvState = RCV_READ;
}
else
{
g_RcvMsgType = RCV_ERR;
}
break;
case (SHORT_FRAME|RCV_BACK):
if(PreambleNum > 1)
{
g_Rx.address_size = SHORT_ADDR_SIZE;
g_RcvMsgType = RCV_BACK;
s_RcvBufferPos = 0;
g_Rx.data_buf[s_RcvBufferPos++] = Byte;
g_RcvState = RCV_READ;
}
else
{
g_RcvMsgType = RCV_ERR;
}
break;
case (LONG_FRAME|RCV_STX):
if(PreambleNum > 1)
{
g_Rx.address_size = LONG_ADDR_SIZE;
g_RcvMsgType = RCV_STX;
s_RcvBufferPos = 0;
g_Rx.data_buf[s_RcvBufferPos++] = Byte;
g_RcvState = RCV_READ;
}
else
{
g_RcvMsgType = RCV_ERR;
}
break;
case (LONG_FRAME|RCV_ACK):
if(PreambleNum > 1)
{
g_Rx.address_size = LONG_ADDR_SIZE;
g_RcvMsgType = RCV_ACK;
s_RcvBufferPos = 0;
g_Rx.data_buf[s_RcvBufferPos++] = Byte;
g_RcvState = RCV_READ;
}
else
{
g_RcvMsgType = RCV_ERR;
}
break;
case (LONG_FRAME|RCV_BACK):
if(PreambleNum > 1)
{
g_Rx.address_size = LONG_ADDR_SIZE;
g_RcvMsgType = RCV_BACK;
s_RcvBufferPos = 0;
g_Rx.data_buf[s_RcvBufferPos++] = Byte;
g_RcvState = RCV_READ;
}
else
{
g_RcvMsgType = RCV_ERR;
}
break;
default:
g_RcvMsgType = RCV_ERR;
g_RcvState = RCV_DONE;
break;
}
}
}
set_delay_time(GAP_TIMER,HRT_GAPT);
break;
case RCV_READ:
if(is_timeout_id(GAP_TIMER))
{
g_RcvMsgType = RCV_ERR;
}
else
{
g_Rx.data_buf[s_RcvBufferPos++] = Byte;
if(g_Rx.address_size == SHORT_ADDR_SIZE)
{
if(s_RcvBufferPos == HRT_SHORTF_LEN_OFF+1)
{
ByteCount = Byte;
}
if(s_RcvBufferPos > HRT_SHORTF_LEN_OFF+ByteCount+1)
{
g_RcvState = RCV_DONE;
}
}
else
{
if(s_RcvBufferPos == HRT_LONGF_LEN_OFF+1)
{
ByteCount = Byte;
}
if(s_RcvBufferPos > HRT_LONGF_LEN_OFF+ByteCount+1)
{
g_RcvState = RCV_DONE;
}
}
}
set_delay_time(GAP_TIMER,HRT_GAPT);
if(g_RcvState == RCV_DONE)
{
is_timeout_id(GAP_TIMER);
g_HartState = HRT_WAIT;
g_RcvState = RCV_WAIT_IDLE;
PreambleNum = 0;
ByteCount = 0;
set_rcv_frame_count();
serical_enable(FALSE,FALSE);
}
break;
default:
break;
}
}
想了一下,这样大篇幅的粘贴代码没什么用,还是自己去打读源码打断点调试,思路要自己去体会。
2.发送状态机
3.从机/突发模式状态机
4.主机状态机
重点就是几张状态转换图。给大家个源码链接GITHUB上搜HART。
OpenHart开源是真的好。作者是断断续续调了两周左右,其实是卡在一个小问题,顺利的话应该三四天就可以通上。
三.命令
定义函数指针
void (*command)(unsigned char *data);
成帧的时候选择调用相应函数
g_Tx.byte_count = cmd_function(cmd,data);
g_Tx.data_buf[HRT_LONGF_LEN_OFF] = g_Tx.byte_count;
写不同命令入口函数,以comm0为例子
case 0: command = C0_RdUniqueId; break;
0命令是响应设备信息
在这里插入代码片
/* the same as C11 */
void C0_RdUniqueId(unsigned char *data)
{
set_respose_code(data);
if(!HrtResposeCode)
{
set_ID(data);
}
}
HART命令0:读标识码
返回扩展的设备类型代码,版本和设备标识码。
请求:无
响应:
字节0: 254
字节1: 制造商ID(Enum)
字节2: 设备类型(Enum)
字节3: 请求的最小前导符数(主->从)
字节4: 通用命令文档版本号
字节5: 设备规范版本号
字节6: 设备软件版本号
字节7: (前五个bit)设备硬件版本号
(后三个bit)物理信号类型(Enum)
字节8: 设备标志
字节9-11: 设备ID号
void set_ID(unsigned char *data)
{
data[HrtByteCnt++] = 254;
data[HrtByteCnt++] = MANUFACTURER_ID;
data[HrtByteCnt++] = DEVICE_TYPE;
data[HrtByteCnt++] = get_response_preamble_num();
data[HrtByteCnt++] = 5; //hart revision
data[HrtByteCnt++] = 5; //device revision level
data[HrtByteCnt++] = 2; //software revision
data[HrtByteCnt++] = 0x20;
data[HrtByteCnt++] = 0; //flag assignment
data[HrtByteCnt++] = UNIQUE_DEVICE_ID0;
data[HrtByteCnt++] = UNIQUE_DEVICE_ID1;
data[HrtByteCnt++] = UNIQUE_DEVICE_ID2;
//the 12th data byte?
}
类似这样的命令函数有很多。
就先这样吧,太晚了,希望帮助到新人朋友,大家一起少走弯路。最后我把用到的文档,开发工具全部打包上传!大家点点关注涨个粉哈哈哈哈
六、HART资料及工具
参考文章