Interrupt driven OneWire bus using USART hardware

I’ve written an implementation of the OneWire bus which uses the USART to offload all of the bus timing requirements on the hardware and is fully interrupt driven for read, write and search.

The implementation follows Maxim’s App 214 - Using a UART to Implement a 1-Wire Bus Master. A compile time option for table based or bit-by-bit OneWire CRC provides a tradeoff between speed and code space utilization.

An open drain drive circuit is required to adapt the push-pull TX output to the open drain + 4.7k pullup of the onewire bus. You can either build the external circuit in App 214 which uses n-channel mosfets, or if you have common 2N3904 (or another NPN transistor) handy, the following circuit can be built on the breadboard:

ONEWIRE_TX and ONEWIRE_RX are connected to the core’s TX and RX pins respectively. ONEWIRE_RX_TX is connected to the onewire bus pin of your onewire devices. R1 and R2 provide biasing and current control of the NPN transistors. R3 is the required external pullup of 4.7K. R4 provides input current limiting to the RX pin of the core which allows the bus voltage to exceed the 3.3V IO voltage limit. The +5V can either be connected to VIN (5V from usb) or the 3.3V digital power rail, depending on how you wish to power your onewire devices. D1 and D2 provide optional excessive voltage protection.

Here is how I’ve implemented this circuit using the included breadboard, two 2N3904, and a ds18b20 temperature sensor on a pigtail (sorry for the lousy cellphone quality photo):

If there is interest I can submit a pull request to merge this into the baseline Spark core software or when external modules are more defined, build a module for this work. For the time being, interested parties can pull my github branches, mattande/core-firmware:onewirehw and mattande/core-common-lib:onewirehw. The onewirehw class is fully documented in the onewirehw.h header file. There is also included a (very simplified) ds18b20 interface class, onewire_ds18b20.cpp and sample application.cpp which uses the cloud API.

Future work, which I intend to take on once the core software settles down a bit includes:

  • If the RX pin is unconnected or there is a problem with the open drain output circuit, the bus will always report as “busy” after the first doWriteRead(). This would require an API to tie into the SysTick interrupt to perform some receive timeout processing.
  • There is no built in support for “parasite power” devices. The onwirehw class could be extended to optionally take an IO pin as the driver for a parasite power circuit which would be energized after doWriteRead() for a specified number of milliseconds. The bus would remain in the “busy” state while the parasite power devices are powered. This requires external hardware and a tie in to the SysTick interrupt.
  • During search, a CRC calculation is performed in the interrupt context. While the default table based CRC is quite fast, it would be best to break this out to occur in a user or system task.
  • This software uses USART2 and the USART2 interrupt. There is also a Spark Wiring class (Serial1) which uses USART2 and the USART2 interrupt. I’ve put in some code to allow this interrupt to be claimed by one class or the other. If the Spark guys like this approach then we’re cool… otherwise some changes need to be made.

Feedback and comments are welcome.

7 Likes

Nice, although a pity it requires hardware mods. What’s the advantage of this over the pure software implementation?

Speed and reliability, I’d say. OneWire is a very timing critical protocol.

@mattande Very, very, very nice job with this! Hmmm, so could you use one of the two I2C interfaces for this instead? At least those ports are physically setup for an open drain bus. If the STM32 is just using hardware timer based USCI, I’d think it might be possible.

The advantage of offloading timing to the USART is CPU utilization and interrupt latency. If you are using a bit-banged OneWire, while each bit is read or written the CPU has to use a busy-wait (ie, delay) to approximate the bit timing. During the busy-wait, your ARM core is tied up doing nothing (CPU utilization). If you want to ensure this timing is always correct, the CPU must lockout interrupts (Interrupt latency, boo!) which could prevent the delay from ending (and therefore the next GPIO operation) at the time its expected.

A more concrete example is the sampling the DS18B20 sensor and running a control algorithm in realtime. Say you have 10 sensors. Each time you sample the temperature it takes 232 OneWire Read+Write slots. At 87usec per slot (not counting the presence pulse), that’s ~20ms per sensor and ~200ms to sample all of your sensors. Now say you need to run the control algorithm at 100Hz. Using a software implementation of OneWire its impossible to run the control algorithm every 10ms AND ensure that each sensor is sampled at the required rate.

Some people also dont like to use delay() :smile:

The external hardware had advantages and disadvantages. Using an external driver is much more robust to harsh conditions, ie, the 2N3904 can withstand up to 40V, while if the onewire bus was connected directly to the STM32 IO pin, voltages over 3.3V will cause damage. If your sending your onewire bus out in the “real world”, ie. not outside of your own PCB, its a good idea to provide some input protection circuitry or you’ll inevitably end up with blown up GPIO ports and a non-functional product.

On the other hand, one of the advantages of the OneWire bus is not needing any more external circuitry than a 4.7K pull-up resistor.

So @timb got me thinking - well, you could probably get the I2C peripheral to do the same thing, but its less suitable because I2C does not perform any clock recovery (ie, there is always a SCL clock toggling and you don’t need it for this application). But all the STM32 uses the same GPIO circuit on each pin, so why cant you use the USART with an open drain driver like with the I2C? Turns out you can - using alternate function open-drain IO instead of AF push-pull. Also, the STM32 USART provides a “half-duplex” mode, where the USART RX is tied internally to the TX pin, enabling a “true” single-pin-only-needs-4.7K-pullup on TX solution.

I’ve pushed updates to my onewirehw branches which adds a drive circuit option to select either the internal STM32 open drain drive on TX, or an external drive circuit solution on TX and RX. If you test this out, be sure to pull my onewirehw branch versions of all core-firmware, core-common-lib and core-communications-lib. Just be sure your onewire voltage does not exceed the core’s 3.3V VCC voltage.

Hers an updated breadboard photo, showing only the 4.7K pullup and TX pin!

3 Likes

I was reading the STM32 datasheet in bed last night to figure out if all the I/O was selectable, I come back this evening and you’ve already got it implemented! I really hope they pull this into the main branch and include it as a feature.

I’ve been spending a lot of time over the last few months working with TI’s MSP430 series of microcontrollers, which have an interesting feature. Each uC has X number of timer based USCI (Universal Serial Communications Interfaces) ports, which can be setup to function as SPI, I2C, UART, LIN, IrDA and custom interfaces. So that’s what gave me the idea of being able to either configure the STM32’s USART in open-drain mode, or co-opting the I2C interface. I’m really glad you were able to get this working with just a pull-up resistor!

Will this support Parasite Power devices now?

Thanks so much for the clarification. I can certainly see the benefit of protecting the bus if using external circuitry, as well as avoiding busy waits.

I’m puzzled how the USART helps you run it at 100Hz if all sensors take ~20ms to read? Perhaps using the overdrive mode in this case would be a possible solution, or using bus-wide signalling (e.g.s start temp conversion sent to all sensors.)

In software, we can avoid busy wait and interrupt latency by recoding it as a state machine, and then implement the delays via timers, with the timer ISR calling back into the state machine to perform the next action. This would avoid the busy wait and interrupt latency.

With the new asynchronous interface to OneWire that you made, we could provide two implementations of this - one using the hardware (USART/I2C) and another using just software, working from timer interrupts.

Sorry I should have been more clear in my example. The conversion time for the ds18b20 limits the sample rate to about no faster than about 1Hz. It takes 20ms of bus time (total) to sample one sensor counting the time required to clock out the select rom, target rom, and conversion command - then after the conversion is finished (~750ms later) to clock out the select rom, target rom and read scratchpad. If you have 10 sensors and want to maintain an exact sample rate (at 1Hz) your CPU’s foreground attention is needed for 20ms every 100ms (10 sensors, sampled at 1Hz). This becomes difficult to do if you also have something else time critical (ie, the control loop running at 100Hz that has other high rate inputs or outputs) that needs CPU attention every 10ms.

If you have a need for each sensor to be sampled exactly every 1 second because, say, your trying to determine the rate of change of the temperature for the control algorithm, the timing jitter introduced by the software onewire will degrade your measurements since you can’t sample at a deterministic 1Hz.

You raise a good point in that you could schedule all of the bus reads/writes using a timing interrupt and state machine, but you still have to busy-wait during the software onewire bus reads/writes and you still have to have interrupts locked out during the busy-wait in each read/write slot. Unless you are scheduling each GPIO read and GPIO write in your state machine… but oh man… the complexity. Hardware exists on the chip so lets use it. Using the hardware onewire all your state machine needs to do is kick off the onewire operation which can do read/write hundreds of bits with only a few usec of software time taken to handle the USART interrupt.

Yeah, you’re right the software approach using only timer interrupts for delays would be pretty complex, at least fairly difficult to read. If the hw approach only requires the pullup resistor then it’s an ideal solution given given the hw is there. I thought it required more hardware to make it work.

On your last breadboard pic there’s a transistor, is that used?

Yes you could support parasite power devices without any additional hardware. All that needs to happen is after finishing a doWriteRead(), the GPIO is taken out of AF open-drain, placed in output push-pull and set high for a specified number of ms. Then switch the GPIO back to AF open-drain and declare the bus operation complete.

I’ve thought about implementing this but I’m holding off for now. It requires a mechanism for tapping into a timing interrupt like SysTick… which I could write… but I have a feeling that my changes would not be accepted upstream. Since I don’t need parasite power for my own projects (it slows down the conversion time on ds18b20… boo) and I don’t really feel like doing the changes twice … I’ll wait until the core software gets a little more mature.

If you need parasite power for your own projects I would definitely encourage you to implement it. This is the fun of open source.

That TO-92 is the DS18B20 with its DQ pin connected directly to the cores TX and a 4.7K pullup.

1 Like

Doh! Of course, sorry , long day…! :slight_smile: (And hardware isn’t my thing.)

With the minimal hardware changes, and offloading work the stm32, and protection of the core from higher voltage on the bus, this is cooler than a snowman’s cold bits! :slight_smile: Really, really nice. I look forward to trying it out!

I don’t particularly use Parasite Power either, was just curious if the change to a single pin, uh, changed anything!

Generally, I find the Parasite Powered 1Wire devices to be much less reliable and just a headache to use, especially over longer cable lengths.

It’s funny, I was about to start working on a library for the DS2482+100 1Wire to I2C Bridge. But now, I don’t think I’ll need it!

Thanks for the great discussion, I appreciate the feedback.

This is great work. Please be sure to let us know via this thread when/if this gets merged into master. :smiley:

Just out of curiosity, would it be possible to use the RX pin as parasite power source in “half-duplex” mode?
Does the RX pin acutally get tied to the TX pin or is only the USART RX signal rerouted and the hardware pin still available for things like digitalWrite?

Half duplex only applies to the inputs to the USART peripheral. So, yes the hardware could support this. To drive the bus a low impedance high, the port pin would need to be taken out of alternate function open-drain and set to output push-pull and driven high for the period you want to drive parasite power. Then when the period of parasite power is ended, switch back to alternate function open-drain to drive the pulse train from the USART.

Ideally the API would be updated with a time period to set parasite power after a onewire transaction and run this timing source against an aux clock tick interrupt.

Thanks for that info!