1-Wire implementation for AVR

Brief tutorial how to add 1-Wire devices to your MCU.

Introduction

The 1-Wire protocol developed by Dallas Semiconductor (now Maxim Integrated) allows numerous sensors or peripheral components to be connected to a master via two or three lines. The special feature of this interface is that the data line (DQ) can be used simultaneously for the power supply (parasitic power supply), whereby the bus can work with only two lines:

  • The data line DQ

  • A Ground connection

If the data line is connected to the power supply via a pull-up resistor, bus lengths of 100 - 300 m are possible, depending on the cable used.

Let's take a look at how a 1-wire bus can be implemented on an AVR microcontroller (here an XMega256A3BU). Then let´s add a DS18B20 1-Wire temperature sensor as a 1-Wire device so we can test out the implementation.

1-Wire basics

The 1-Wire bus does not use a separate clock line for the data signals. Therefore, similar to the USART, a given timing has to be adhered to so that a functioning communication can take place. The 1-Wire has only one master at a time and the data line is provided with a weak pull-up resistor (usually a voltage of 3V or 5V is used).

The data line is designed as a bidirectional open-drain I / O port on a slave device, allowing both master and slaves to send data. The use of an open-drain driver for the data line creates a wired AND connection, which means that the master can only communicate with a single slave at a time. Also the master must also be able to generate a delay of 1 μs or 0.25 μs for the so-called overdrive mode, a fast mode for a higher data transfer rate of up to 142 Kbps.

A 1-Wire communication consists of four basic operations:

  • Transmit a logical 1

  • Transmit a logical 0

  • Read one bit

  • Perform a Reset

For every data transmission initiated by the master (regardless of whether reading or writing), a reset pulse is triggered first. Each connected slave outputs a so-called presence pulse at the end of the reset by pulling the data line low. This presence pulse signals to the master that a device is connected to the bus. If no pulse occur, no device is connected. After a reset, a general ROM command is sent to either identify the bus users or select a bus user. As soon as the participant has been selected, a device-specific command is issued.

Each of the basic operations consists of a fixed sequence of high / low levels and must also correspond to specific time specifications.

This results in the following sequence:

Let´s take a look at the implementation of the driver.

Implementation of the 1-Wire driver

For the implementation I use my XMEGA-A3BU Xplained development board and as slaves a DS18B20 1-Wire temperature sensor. The implementation should be realized in general, so that the final driver can be used on different plattforms like the ATmega32 or the Raspberry Pi.

In the first step, the data line DQ, in this example the pin 0 of port E, must be initialized. The I / O must be switched as an output and set to high state to put a high level on the bus.

GPIO_SetDirection(GET_PERIPHERAL(ONEWIRE_DQ), GET_INDEX(ONEWIRE_DQ), GPIO_DIRECTION_OUT);
GPIO_SetPullConfig(GET_PERIPHERAL(ONEWIRE_DQ), GET_INDEX(ONEWIRE_DQ), GPIO_OUTPUTCONFIG_WIREDANDUP);
GPIO_Set(GET_PERIPHERAL(ONEWIRE_DQ), GET_INDEX(ONEWIRE_DQ));

I use the internal pull-up resistor of the microcontroller as the required resistor for the 1-Wire bus.

Depending on the microcontroller used, the pull-up resistors are switched on differently. This line must be adapted accordingly to the microcontroller used.

After initialization, I give the bus participants some time to process the internal initialization. Then I perform a complete reset of all bus participants:

_delay_us(100);
OneWire_Reset();

The structure of the reset function results from the timings of the specification and the sequence of a 1-wire reset:

OneWire_Error_t OneWire_Reset(void)
{
	uint8_t State = 0x00;
 
	uint8_t Reg = CPU_IRQSave();
	_delay_us(ONEWIRE_DELAY_G);
	GPIO_Clear(GET_PERIPHERAL(ONEWIRE_DQ), GET_INDEX(ONEWIRE_DQ));
	_delay_us(ONEWIRE_DELAY_H);
	GPIO_Set(GET_PERIPHERAL(ONEWIRE_DQ), GET_INDEX(ONEWIRE_DQ));
	_delay_us(ONEWIRE_DELAY_I);
	GPIO_SetDirection(GET_PERIPHERAL(ONEWIRE_DQ), GET_INDEX(ONEWIRE_DQ), GPIO_DIRECTION_IN);
	State = GPIO_Read(GET_PERIPHERAL(ONEWIRE_DQ), GET_INDEX(ONEWIRE_DQ));
	_delay_us(ONEWIRE_DELAY_J);
	GPIO_SetDirection(GET_PERIPHERAL(ONEWIRE_DQ), GET_INDEX(ONEWIRE_DQ), GPIO_DIRECTION_OUT);
	CPU_IRQRestore(Reg);
 
	if(State != 0x00)
	{
		return ONEWIRE_RESET_ERROR;
	}
 
	return ONEWIRE_NO_ERROR;
}

In order to avoid timing problems due to interrupts, the SREG is stored immediately after the function call and the global interrupts are deactivated. After delay time I, DQ is switched as input and the bus status is read. If the bus is not low, the reset has failed and an appropriate error message is returned.

Analogous to the reset function, the functions for sending a bit / byte

void OneWire_WriteBit(const uint8_t Bit)
{
	uint8_t Reg = CPU_IRQSave();
 
	GPIO_Clear(GET_PERIPHERAL(ONEWIRE_DQ), GET_INDEX(ONEWIRE_DQ));
 
	if(Bit) _delay_us(ONEWIRE_DELAY_A);
	else _delay_us(ONEWIRE_DELAY_C);
 
	GPIO_Set(GET_PERIPHERAL(ONEWIRE_DQ), GET_INDEX(ONEWIRE_DQ));
	
	if(Bit)	_delay_us(ONEWIRE_DELAY_B);
	else _delay_us(ONEWIRE_DELAY_D);
	
	CPU_IRQRestore(Reg);
}
 
void OneWire_WriteByte(const uint8_t Data)
{
	for(uint8_t i = 0x01; i != 0x00; i <<= 0x01)
	{
		OneWire_WriteBit(Data & i);
	}
}

or to read a bit / byte:

uint8_t OneWire_ReadBit(void)
{
	uint8_t State = 0x00;
	uint8_t Reg = CPU_IRQSave();
 
	GPIO_Clear(GET_PERIPHERAL(ONEWIRE_DQ), GET_INDEX(ONEWIRE_DQ));
	_delay_us(ONEWIRE_DELAY_A);
	GPIO_Set(GET_PERIPHERAL(ONEWIRE_DQ), GET_INDEX(ONEWIRE_DQ));
	_delay_us(ONEWIRE_DELAY_E);
 
	GPIO_SetDirection(GET_PERIPHERAL(ONEWIRE_DQ), GET_INDEX(ONEWIRE_DQ), GPIO_DIRECTION_IN);
	State = GPIO_Read(GET_PERIPHERAL(ONEWIRE_DQ), GET_INDEX(ONEWIRE_DQ));
	_delay_us(ONEWIRE_DELAY_F);
 
	GPIO_SetDirection(GET_PERIPHERAL(ONEWIRE_DQ), GET_INDEX(ONEWIRE_DQ), GPIO_DIRECTION_OUT);
 
	CPU_IRQRestore(Reg);
 
	return State;
}
 
uint8_t OneWire_ReadByte(void)
{
	uint8_t Data = 0x00;
 
	for(uint8_t i = 0x00; i < 0x08; i++)
	{
		Data = Data | (OneWire_ReadBit() << i);
	}
 
	return Data;
}

This completes the initialization of the bus and the functions for writing or reading the bus have been created. The master can now start to get the addresses of the connected bus subscribers so he can address individual bus subscribers and start to communicate with them.

A transmission via 1-wire strictly follows the same procedure and a wrong sequence automatically leads to a transmission error:

  • Initialize the bus participants with a reset

  • Transmit the ROM command (1-Wire specific command)

  • Transmit the function command (device specific command)

The individual bus subscribers can then be addressed via the addresses. Each 1-Wire slave has a 64-bit ROM which identifies the individual slaves and which is used by the master to address individual devices. This ROM includes

  • A 8 bit CRC (Bit [64:56])

  • A 48 bit serial number (Bit [55:8])

  • A 8 bit family code (Bit [7:0])

Two methods are provided to allow the master to access the information in the ROM:

  • Search ROM command (code 0xF0): A search algorithm that identifies the individual users of the bus and determines the user addresses.

  • Read ROM command (code 0x33): A simplier requests for the identification of a single bus participant. Significantly easier than Search ROM, but works only with a single bus participant.

The third point in the transmission differs depending on the family of the addressed target device and is not needed for the determination of the ROM codes.

Thus, if the ROM code of a single bus user is to be requested, the master must perform a reset, send the Read ROM command and then read the information from the ROM:

OneWire_Error_t OneWire_ReadROM(const OneWire_ROM_t* ROM)
{
	if(ROM == NULL)
	{
		return ONEWIRE_PARAMETER_ERROR;
	}
 
	uint8_t* pROM = (uint8_t*)ROM;
 
	OneWire_Error_t ErrorCode = OneWire_Reset();
	if(ErrorCode == ONEWIRE_NO_DEVICE)
	{
		return ErrorCode;
	}
 
	OneWire_WriteByte(ONEWIRE_READ_ROM);
 
	for(uint8_t i = 0x00; i < sizeof(OneWire_ROM_t); i++)
	{
		*(pROM++) = OneWire_ReadByte();
	}
 
	return ONEWIRE_NO_ERROR;
}

Where OneWire_ROM_t is a structure to store the information from the ROM:

typedef struct
{
	uint8_t FamilyCode;
	uint8_t SerialNumber[6];
	uint8_t Checksum;
} __attribute__((packed)) OneWire_ROM_t;

The master is now able to identify a single 1-wire device on the bus. Here with a DS18B20 temperature sensor as an example:

If there are several (even different) subscribers on the bus, the Search ROM command must be used to identify the individual subscribers. How the search algorithm works is explained in Maxim Integrated in application note 187.

For the implementation of the search algorithm in the 1-Wire driver, I use a customized version of Maxim Integrated's reference implementation to meet my requirements:

static OneWire_Error_t OneWire_SearchROM(const OneWire_ROM_t* ROM)
{
	OneWire_Error_t ErrorCode = ONEWIRE_NO_ERROR;
	uint8_t* pROM = (uint8_t*)ROM;
	uint8_t id_bit;
	uint8_t cmp_id_bit;
	uint8_t ROM_Byte = 0x00;
	uint8_t CRC8 = 0x00;
	uint8_t search_direction = 0x00;
	uint8_t last_zero = 0x00;
	uint8_t ROM_Mask = 0x01;
	uint8_t id_bit_number = 0x01;

	if(ROM == NULL)
	{
		return ONEWIRE_PARAMETER_ERROR;
	}

	if(!__LastDevice)
	{
		ErrorCode = OneWire_Reset();
		if(ErrorCode != ONEWIRE_NO_ERROR)
		{
			__LastDiscrepancy = 0x00;
			__LastDevice = FALSE;
			__LastDiscrepancy = 0x00;

			return ErrorCode;
		}
		if(isAlarm == TRUE)
		{
			OneWire_WriteByte(ONEWIRE_ALARM_SEARCH);
		}
		else
		{
			OneWire_WriteByte(ONEWIRE_SEARCH_ROM);
		}

		do
		{
			id_bit = OneWire_ReadBit();
			cmp_id_bit = OneWire_ReadBit();

			if((id_bit == 0x01) && (cmp_id_bit == 0x01))
			{
				break;
			}
			else
			{
				if(id_bit == cmp_id_bit)
				{
					if(id_bit_number == __LastDiscrepancy)
					{
						search_direction = 0x01;
					}
					else
					{
						if(id_bit_number > __LastDiscrepancy)
						{
							search_direction = 0x00;
						}
						else
						{
							search_direction = (((*(pROM + ROM_Byte)) & ROM_Mask) > 0);
						}
					}
					
					if(search_direction == 0x00)
					{
						last_zero = id_bit_number;
						if(last_zero < 0x09)
						{
							__LastFamilyDiscrepancy = last_zero;
						}
					}
				}
				else
				{
					search_direction = id_bit;
				}

				if(search_direction == 0x01)
				{
					*(pROM + ROM_Byte) |= ROM_Mask;
				}
				else
				{
					*(pROM + ROM_Byte) &= ~ROM_Mask;
				}

				OneWire_WriteBit(search_direction);
				id_bit_number++;
				ROM_Mask <<= 0x01;
				if(ROM_Mask == 0x00)
				{
					CRC8 = __OneWire_CRCTable[CRC8 ^ *(pROM + ROM_Byte)];
					ROM_Byte++;
					ROM_Mask = 0x01;
				}
			}
		}while(ROM_Byte < 0x08);

		if(!((id_bit_number < 65) || (CRC8 != 0x00)))
		{
			__LastDiscrepancy = last_zero;
			if(__LastDiscrepancy == 0x00)
			{
				__LastDevice = TRUE;
				__SearchActive = FALSE;
			}
		}
	}

	if((ErrorCode != ONEWIRE_NO_ERROR) || !(*pROM))
	{
		__LastDiscrepancy = 0x00;
		__LastDevice = FALSE;
		__LastFamilyDiscrepancy = 0x00;
	
		if(!(*pROM))
		{
			ErrorCode = ONEWIRE_CRC_ERROR;
		}
	}

	return ErrorCode;
}

Some 1-wire peripherals (such as the DS18B20) have the ability to switch to an alarm state through certain events. The 1-Wire protocol offers the possibility to search all devices with the alarm flag set. The search is carried out in the same way as the ROM search, except that the search for alarm devices with the command code 0xEC is started instead of 0xF0.

A complete search consists of four different functions:

static uint8_t __LastFamilyDiscrepancy;
static uint8_t __LastDiscrepancy;
static Bool_t __LastDevice;
static Bool_t __SearchActive;
static Bool_t __isAlarm;

OneWire_Error_t OneWire_StartSearch(const OneWire_ROM_t* ROM, const Bool_t isAlarm)
{
	__LastFamilyDiscrepancy = 0x00;
	__LastDiscrepancy = 0;
	__LastDevice = FALSE;
	__SearchActive = TRUE;
	__isAlarm = isAlarm;

	if(ROM == NULL)
	{
		return ONEWIRE_PARAMETER_ERROR;
	}

	return OneWire_SearchROM(ROM, __isAlarm);
}

Bool_t OneWire_IsLast(void)
{
	return __LastDevice;
}

OneWire_Error_t OneWire_SearchNext(const OneWire_ROM_t* ROM)
{
	if(__SearchActive == TRUE)
	{
		return OneWire_SearchROM(ROM, __isAlarm);
	}

	return ONEWIRE_INACTIVE_SEARCH;
}

OneWire_Error_t OneWire_StopSearch(void)
{
	__SearchActive = FALSE;

	return OneWire_Reset();
}

After each byte received, a CRC is calculated to detect transmission errors. Since the calculation of the CRC 1-Wire is specific, each 1-Wire device uses the same CRC polynomial. Therefore, as in the reference implementation, the calculation of the CRC can be done via a fixed lookup table.

A complete search can be implemented as follows (again with a DS18B20 temperature sensor):

static OneWire_Error_t DS18B20_SearchDevices(uint8_t* Found, uint8_t Search, OneWire_ROM_t* ROM, const Bool_t isAlarm)
{
	uint8_t DevicesFound = 0x00;

	if(Found == NULL)
	{
		return ONEWIRE_PARAMETER_ERROR;
	}

	OneWire_Error_t ErrorCode = OneWire_StartSearch(ROM, isAlarm);
	if(ErrorCode == ONEWIRE_NO_ERROR)
	{
		if(ROM->FamilyCode == DS18B20_ID)
		{
			DevicesFound++;
			ROM++;
		}

		while((!OneWire_IsLast()) && (DevicesFound < Search))
		{
			ErrorCode = OneWire_SearchNext(ROM);
			if(ErrorCode != ONEWIRE_NO_ERROR)
			{
				OneWire_StopSearch();
				break;
			}
			
			if(ROM->FamilyCode == DS18B20_ID)
			{
				DevicesFound++;
				ROM++;
			}
		}
	}

	*Found = DevicesFound;

	return ErrorCode;
}

OneWire_Error_t DS18B20_GetDevices(uint8_t* Found, uint8_t Search, OneWire_ROM_t* ROM)
{
	return DS18B20_SearchDevices(Found, Search, ROM, FALSE);
}

The search will only save participants who have the correct ID code (0x28). The search returns the number of DS18B20 temperature sensors found and stores the ROM codes in an array.

Since a microcontroller (usually) is a system with a static memory allocation, the size of the ROM array must be known. So you must know how many nodes are connected to the bus or how many nodes are searched for should.

The code to detect all connected DS18B20 sensors looks like this:


OneWire_Error_t DS18B20_Init(void)
{
	return OneWire_Init();
}
 
#define DS18B20_BUS_DEVICES			0x03
 
uint8_t Devices;
OneWire_ROM_t DS18B20_ROM[DS18B20_BUS_DEVICES];
 
if(DS18B20_Init() == ONEWIRE_NO_ERROR)
{
	if(DS18B20_GetDevices(&Devices, DS18B20_BUS_DEVICES, DS18B20_ROM) == ONEWIRE_NO_ERROR)
	{
		...
	}
}

At the end of the ROM scan, the master knows the ROM codes of all bus subscribers and can begin to communicate with the individual subscribers.

As a rule, the master only communicates with one participant at a time. There are some exceptions that allow the master to send a command to all users (for example, to start a temperature measurement on a DS18B20), but no communication from slave to master can only be done one at a time. At the beginning of communication, the master must select the target device. The determined ROM codes are used for this purpose.

To do this, the master sends a match ROM command (0x55) followed by the ROM code followed by the device-specific command. The selection of the bus participant can be implemented as follows:

OneWire_Error_t OneWire_SelectDevice(const OneWire_ROM_t* ROM)
{
	OneWire_Error_t ErrorCode = OneWire_Reset();
	if(ErrorCode == ONEWIRE_NO_DEVICE)
	{
		return ErrorCode;
	}
 
	if(ROM == NULL)
	{
		OneWire_WriteByte(ONEWIRE_SKIP_ROM);
	}
	else
	{
		uint8_t* ROM_Temp = (uint8_t*)ROM;
		OneWire_WriteByte(ONEWIRE_MATCH_ROM);
		for(uint8_t i = 0x00; i < 0x08; i++)
		{
			OneWire_WriteByte(*(ROM_Temp++));
		}
	}
 
	return ONEWIRE_NO_ERROR;
}

The function expects a pointer to the corresponding ROM code as the transfer value. If the address of the pointer is NULL, i. e. no ROM structure has been transferred, a skip ROM command is sent to address all bus users. If an address has been transferred, the match ROM instruction and the eight bytes of the ROM code are transmitted.

The target device is then selected and can subsequently be operated by the master with a function code. This function code (and possibly a few additional data bytes) are transmitted by the master one after the other. If the master wants to read the target device, it first sends the corresponding function code and then reads in the required number of data bytes. Here's a short example to write the scratchpad, the clipboard, of a DS18B20:

OneWire_Error_t DS18B20_WriteScratchpad(const OneWire_ROM_t* ROM, const uint8_t TH, const uint8_t TL, const uint8_t Config)
{
	OneWire_Error_t ErrorCode = OneWire_SelectDevice(ROM);
	if(ErrorCode != ONEWIRE_NO_ERROR)
	{
		return ErrorCode;
	}
 
	OneWire_WriteByte(DS18B20_WRITE_SCRATCHPAD);
	OneWire_WriteByte(TH);
	OneWire_WriteByte(TL);
	OneWire_WriteByte(Config);
 
	return ONEWIRE_NO_ERROR;
}

This completes the 1-wire driver and provides basic functionality to communicate with 1-wire devices.

The final 1-Wire driver, including an example implementation for DS18B20 temperature sensors for a XMega256A3BU, can be found in my GitLab repository.

Last updated