1-Wire总线上挂载多个DS18B20温度传感器驱动程序

问题背景

DS18B20 是一款温度传感器,单片机可以通过 1-Wire 协议与 DS18B20 进行通信,最终将温度读出。1-Wire 总线的硬件接口很简单,只需要把 DS18B20 的数据引脚和单片机的一个 IO 口接上就可以了。硬件的简单,随之而来的,就是软件时序的复杂。

在每个 DS18B20 内部都有一个唯一的 64 位长的序列号,这个序列号值就存在 DS18B20内部的 ROM 中。开始的 8 位是产品类型编码(DS18B20 是 0x28),接着的 48 位是每个器件唯一的序号,最后的 8 位是 CRC 校验码。DS18B20 可以引出去很长的线,最长可以到几十米,测不同位置的温度。单片机可以通过和 DS18B20 之间的通信,获取每个传感器所采集到的温度信息,也可以同时给所有的 DS18B20 发送一些指令。

当一根很长的线上挂了很多个DS18B20测不同位置的温度时,由于是同一个总线上,需要根据每个器件的唯一序列号分别对每个器件进行温度值采样,DS18B20的通信协议有对此进行考虑,可以发序列号进行匹配,总线上所有器件在收到序列号后只有和自身序列号匹配的才能响应。

但怎么能事先知道每个器件的序列号?有些人说要先单个读取每个器件的序列号,把读到的序列号在程序的数组中表示,然后实际应用时就能对总线上每个DS18B20器件单个进行匹配读取。

这种方法在实际应用场景是完全不可能的,存在以下问题:
1.实际场景每个控制板对应不同的多个DS18B20,怎么单独读每个DS18B20的序列号是个大问题,严重降低生产效率
2.即使读到了每个控制板对应的多个DS18B20的序列号,那么需要把这些序列号发送给控制板进行保存,这个过程也很繁杂
3.即使上面2点都这样做了,那么如果产品使用中总线上某个DS18B20坏了,那维护过程也很困难,需要单独读取用于更换的DS18B20的序列号,然后发送给控制板使其改变内部的扫描列表

所以事先单个读取每个器件的序列号进行保存这样的方法肯定是行不通的,只能开机就能自动扫描总线上所有器件的序列号,然后再根据序列号去读取才是正常的逻辑。好在DS18B20的 1-Wire 通讯协议设计了扫描序列号的方法。有专用于扫描的指令,细节可继续往下看。

解决思路

首先了解一下单总线的硬件结构:
在这里插入图片描述
这个图里说明,DS18B20在输出时,是靠拉低DQ通信线实现低电平输出,靠释放DQ通信线(被上拉电阻拉高)来实现高电平输出。

也就是说它可以实现“线与”。多个DS18B20同时输出时,只要有一个低电平,则DQ通信线就会被拉低,只有低电平能被识别,只有都为高电平时,总线上才是高电平。单总线上识别不同的DS18B20就是靠“线与”来解决通信冲突的。(这个特点类似I2C总线)

DS18B20的datasheet中有这样一部分流程图:
在这里插入图片描述
F0是搜索ROM的指令,上面的流程图可以看出通过单总线通过F0指令获取DS18B20内部序列号ID的方法,访问每个bit时,分为三步:

先读取第一位;

再读取第一位的补码;

通过读取的数据和补码,和下表对应的结论,来判断总线上的器件状态:
在这里插入图片描述
然后写一位数据,告诉从器件,主机选择了哪些器件继续通信;如果从机当前ROM码全为0,则写0;如果从机当前ROM码全为1,则写1;如果从机当前ROM码有0也有1,则主机可以先写0,选择为0的器件继续通信;等本次后续的bit位都搜索完后,再回到第一个位冲突处,选择为1的器件,到另一个分支搜索。

主机通过不断循环、回溯,完成所有器件的搜索,获取所有器件的序列号。

下面举一个详细的例子来帮助理解:
(以下转载自https://blog.csdn.net/little_grapes/article/details/121917737)

假如总线上挂载了三个DS18B20,我们命名为a、b、c器件,为简化处理,假设器件ID只有两位,分别是:01、00、11;MCU从低位开始读取。

a)第一轮通信

主机MCU先读取第1位,a的最低位为1、b的最低位为0、c的最低位为1,由于最低位有0,所以总线数据会被识别为0;

再读取第1位的补码,a的最低位补码为0、b的最低位补码为1、c的最低位补码为0,则总线数据也会被识别为0(有0就为0,全1才为1);

主机MCU识别到两次都为0,则知道总线上有器件为0、也有器件为1;则主机MCU需要选择一条路径继续访问;如果我们的算法选择优先访问0的路径;则向总线上写入一个0位;三个从器件收到发送来的0后,只有b器件的对应位符合,则下一次通信时,a、c器件都不会再响应;

b)第二轮通信

主机MCU开始读取第2位,由于只有b器件响应,则读到0;

再读取第2位的补码,由于只有b器件响应,可以读到1;

主机MCU两次读取到0、1,则可以判定总线上的器件该位为0;

又因为,上一轮选择的是0路径,加上本次识别到的0,则可以断定,总线上有一个器件的ID是00;识别完ID的总长度(2位)后,本次搜索结束。

c)回溯后再次通信

与第一轮通信类似,最后写入的位改为1;则只有a、c器件的对应位符合,后续只有a、c器件会继续通信,b器件不再响应;

主机MCU开始读取第二位,读取到0;

读取补码,读到0;

则主机可以断定,第二位有0、也有1;再加上上一轮通信中选择的1路径,则可以断定,总线上有两个器件:10和11。

至此,所有ID搜索完毕,搜索的示意见下图:
在这里插入图片描述
实际的DS18B20的ID有64bit,上面的例子只有2bit,搜索的原理是一样的,以此可以获取总线上所有器件的ID,之后就可以通过ID来访问特定器件了。

按上述方法实现搜索ID的代码如下所示:

#define MaxSensorNum 8
unsigned char DS18B20_ID[MaxSensorNum][8];	// 存检测到的传感器DS18B20_ID的数组,前面的维数代表单根线传感器数量上限
unsigned char DS18B20_SensorNum;			// 检测到的传感器数量(从1开始,例如显示1代表1个,8代表8个)

// 自动搜索ROM
void DS18B20_Search_Rom(void)
{
    
    
	uint8_t k, l, chongtuwei, m, n, num;
	uint8_t zhan[64+1];//按顺序保存冲突位的位号
	uint8_t ss[64];//保存64个bit
	l = 0;
	num = 0;
	do
	{
    
    
		DS18B20_Rst(); //注意:复位的延时不够
		delay_us(750); //480、720
		DS18B20_Write_Byte(0xf0);
		for (m = 0; m < 8; m++)//8 * 8 = 64bit ID
		{
    
    
			uint8_t s = 0;//用于缓存当前读到的字节位
			for (n = 0; n < 8; n++)//8bit
			{
    
    
				k = DS18B20_Read_2Bit();//读两位数据
				k = k & 0x03;
				s >>= 1;//低位开始
				if (k == 0x01)//01读到的数据为0 写0 此位为0的器件响应
				{
    
    
					DS18B20_Write_Bit(0);
					ss[(m * 8 + n)] = 0;
				}
				else if (k == 0x02)//10读到的数据为1 写1 此位为1的器件响应
				{
    
    
					s = s | 0x80;
					DS18B20_Write_Bit(1);
					ss[(m * 8 + n)] = 1;
				}
				else if (k == 0x00)//读到的数据为00 有冲突位 判断冲突位
				{
    
    
					//如果冲突位大于栈顶写0 小于栈顶写以前数据 等于栈顶写1
					chongtuwei = m * 8 + n + 1;
					if (chongtuwei > zhan[l])
					{
    
    
						DS18B20_Write_Bit(0);
						ss[(m * 8 + n)] = 0;
						zhan[++l] = chongtuwei;//记录该新的冲突位
					}
					else if (chongtuwei < zhan[l])
					{
    
    
						s = s | ((ss[(m * 8 + n)] & 0x01) << 7);
						DS18B20_Write_Bit(ss[(m * 8 + n)]);
					}
					else if (chongtuwei == zhan[l])
					{
    
    
						s = s | 0x80;
						DS18B20_Write_Bit(1);
						ss[(m * 8 + n)] = 1;
						l = l - 1;
					}
				}
				else
				{
    
    
					//没有搜索到
					break;
				}
			}
			DS18B20_ID[num][m] = s; // 保存搜索到的ID字节
		}
		num = num + 1;// 保存搜索到的个数
	} while (zhan[l] != 0 && (num < MaxSensorNum));//测试完所有冲突位,或搜索到的个数大于最大数则退出
	DS18B20_SensorNum = num;
}

CRC校验

DS18B20 在读取8字节ROM和9字节暂存器时,最后一个字节都是前面所有字节的CRC校验值。我们在读ROM时,最好要校验一下CRC是否正确,否则要认为读取的数据有误。

计算CRC的等效多项式为(这是datasheet中的式子, 并非幂运算, 要结合后面的流程图理解)

CRC=X8+X5+X4+1

1-Wire总线的CRC计算由移位寄存器和异或门组成的多项式发生器来执行: 移位寄存器位初始化为0, 然后从第一个字节的最低位开始, 一次移入一位, 根据计算结果决定是否与第4, 第5位作异或, 然后CRC也往右移, 最后移位寄存器的值就是CRC.
在这里插入图片描述
从实现方法上可以分为两个方向,一个是查表法,还有一个是计算法。
通过计算:运行速度相对慢,占用内存相对小。以时间换空间
通过查表:运行速度相对快,占用内存相对大。以空间换时间
最终方法的使用,酌情考虑。

通过计算得到8位CRC:

unsigned char DS18B20_Crc8(unsigned char* dat, unsigned char len)
{
    
    
    unsigned char i, x; 
    unsigned char crcbuff;

    unsigned char crc = 0;
    for (x = 0; x < len; x++)
    {
    
    
        crcbuff = dat[x];
        for (i = 0; i < 8; i++)
        {
    
    
            if (((crc ^ crcbuff) & 0x01) == 0)
                crc >>= 1;
            else {
    
    
                crc ^= 0x18; //CRC=X8+X5+X4+1
                crc >>= 1;
                crc |= 0x80;
            }
            crcbuff >>= 1;
        }
    }
    return crc;
}

通过查表得到8位CRC:

unsigned char dscrc_table[] = {
    
    
    0x00, 0x5e, 0xbc, 0xe2, 0x61, 0x3f, 0xdd, 0x83,
    0xc2, 0x9c, 0x7e, 0x20, 0xa3, 0xfd, 0x1f, 0x41,
    0x9d, 0xc3, 0x21, 0x7f, 0xfc, 0xa2, 0x40, 0x1e,
    0x5f, 0x01, 0xe3, 0xbd, 0x3e, 0x60, 0x82, 0xdc,
    0x23, 0x7d, 0x9f, 0xc1, 0x42, 0x1c, 0xfe, 0xa0,
    0xe1, 0xbf, 0x5d, 0x03, 0x80, 0xde, 0x3c, 0x62,
    0xbe, 0xe0, 0x02, 0x5c, 0xdf, 0x81, 0x63, 0x3d,
    0x7c, 0x22, 0xc0, 0x9e, 0x1d, 0x43, 0xa1, 0xff,
    0x46, 0x18, 0xfa, 0xa4, 0x27, 0x79, 0x9b, 0xc5,
    0x84, 0xda, 0x38, 0x66, 0xe5, 0xbb, 0x59, 0x07,
    0xdb, 0x85, 0x67, 0x39, 0xba, 0xe4, 0x06, 0x58,
    0x19, 0x47, 0xa5, 0xfb, 0x78, 0x26, 0xc4, 0x9a,
    0x65, 0x3b, 0xd9, 0x87, 0x04, 0x5a, 0xb8, 0xe6,
    0xa7, 0xf9, 0x1b, 0x45, 0xc6, 0x98, 0x7a, 0x24,
    0xf8, 0xa6, 0x44, 0x1a, 0x99, 0xc7, 0x25, 0x7b,
    0x3a, 0x64, 0x86, 0xd8, 0x5b, 0x05, 0xe7, 0xb9,
    0x8c, 0xd2, 0x30, 0x6e, 0xed, 0xb3, 0x51, 0x0f,
    0x4e, 0x10, 0xf2, 0xac, 0x2f, 0x71, 0x93, 0xcd,
    0x11, 0x4f, 0xad, 0xf3, 0x70, 0x2e, 0xcc, 0x92,
    0xd3, 0x8d, 0x6f, 0x31, 0xb2, 0xec, 0x0e, 0x50,
    0xaf, 0xf1, 0x13, 0x4d, 0xce, 0x90, 0x72, 0x2c,
    0x6d, 0x33, 0xd1, 0x8f, 0x0c, 0x52, 0xb0, 0xee,
    0x32, 0x6c, 0x8e, 0xd0, 0x53, 0x0d, 0xef, 0xb1,
    0xf0, 0xae, 0x4c, 0x12, 0x91, 0xcf, 0x2d, 0x73,
    0xca, 0x94, 0x76, 0x28, 0xab, 0xf5, 0x17, 0x49,
    0x08, 0x56, 0xb4, 0xea, 0x69, 0x37, 0xd5, 0x8b,
    0x57, 0x09, 0xeb, 0xb5, 0x36, 0x68, 0x8a, 0xd4,
    0x95, 0xcb, 0x29, 0x77, 0xf4, 0xaa, 0x48, 0x16,
    0xe9, 0xb7, 0x55, 0x0b, 0x88, 0xd6, 0x34, 0x6a,
    0x2b, 0x75, 0x97, 0xc9, 0x4a, 0x14, 0xf6, 0xa8,
    0x74, 0x2a, 0xc8, 0x96, 0x15, 0x4b, 0xa9, 0xf7,
    0xb6, 0xe8, 0x0a, 0x54, 0xd7, 0x89, 0x6b, 0x35
};

unsigned char DS18B20_Crc8(unsigned char* dat, unsigned char len)
{
    
    
    unsigned char crc = 0;
    for (x = 0; x < len; x++)
    {
    
    
        crc = dscrc_table[crc ^ dat[x]];
    }
    return crc;
}

获取温度

经过上面的步骤,扫描了所有的ROM,然后CRC校验也没问题,那么接着就用0x55指令匹配ROM对每个ROM分别进行读取温度的操作即可。
代码示例:

float DS18B20_Get_Temp_ID(uint8_t* ID)
{
    
    
    //u8 flag;
	uint8_t j;//匹配的字节
	uint8_t TL, TH;
	short Temperature;
	float Temperature1;
	DS18B20_Rst();
	DS18B20_Check();
	DS18B20_Write_Byte(0xcc);// skip rom
	DS18B20_Write_Byte(0x44);// convert
	DS18B20_Rst();
	DS18B20_Check();
	// DS18B20_Write_Byte(0xcc);// skip rom
	//匹配ID,i为形参
	DS18B20_Write_Byte(0x55);
	for (j = 0; j < 8; j++)
	{
    
    
		DS18B20_Write_Byte(ID[j]);
	}
	DS18B20_Write_Byte(0xbe);// convert
	TL = DS18B20_Read_Byte(); // LSB
	TH = DS18B20_Read_Byte(); // MSB
	if (TH & 0xfc)
	{
    
    
		//flag=1;
		Temperature = (TH << 8) | TL;
		Temperature1 = (~Temperature) + 1;
		Temperature1 *= 0.0625;
	}
	else
	{
    
    
		//flag=0;
        Temperature1 = ((TH << 8) | TL)*0.0625;
	}
	return Temperature1;
}
		for(int num=0;num<DS18B20_SensorNum;num++)
		{
    
    
            log_debug("ID:");
            for(int i=0;i<8;i++)
            {
    
    
                log_debug("%02x",DS18B20_ID[num][i]);
            }
            
			log_debug("  TM:%.2f\r\n",DS18B20_Get_Temp_ID((uint8_t *)DS18B20_ID[num]));
		}

推荐:

https://blog.csdn.net/little_grapes/article/details/121917737

https://blog.csdn.net/weixin_44457994/article/details/121758826

https://blog.csdn.net/weixin_39827856/article/details/125134565

猜你喜欢

转载自blog.csdn.net/weixin_44788542/article/details/129684743