With I2C Interfacing
Argent Data Systems makes a pole-mounted sensor unit for collecting wind speed, wind direction and rainfall. It’s a great weather measurement system with which you can do whatever fancy interfacing you choose. In this project article, Jeff creates an I2C slave that does all the sensor work and presents the results in table form that can be accessed with a simple I2C connection.
Anyone who delves into a weather project has most likely run across what I like to call the “weather “tree”—a pole-mounted sensor system for collecting wind speed, wind direction and rainfall. One unit is available from Argent Data Systems for about $70 (Figure 1). Nothing else on the market gives an experimenter these kinds of weather tools at such a reasonable price. The interface is simple for each sensor. All the sensors use reed switches to detect their specific properties.
The wind speed and rain gauge both employ a single reed switch. Reed switches require a magnetic field to change state. These particular switches are open until a magnet comes within range. With a magnet attached to a moving mechanism, the reed switch closes each time the magnet and switch come into close proximity. No actual touching is necessary. The reed switch, itself, is contained within a glass package and can be easily damaged, so some care is necessary. Only two wires are required for each sensor. Connecting each sensor pair between an input pin with a pull-up resistor and ground, the pin will be at logic high, unless the magnet is closing the reed switch, in which case the input is grounded.
When closed, depending on the value resistor in series with that switch, the input voltage may or may not be pulled below the threshold for the input to see a logic low. We must use an analog-to-digital (ADC) to detect direction with this sensor. In fact, the design is such that one reed switch will always be closed. It is possible for two switches to be closed while the vane is between reed switches! All resistors in the sensor are 1%, so their actual values are dependable enough that with the values selected, each of the 16 points (N, NNE, NE, ENE, E, ESE, SE SSE, S, SSW, SW, WSW, W, WNW, NW and NNW) can be determined from the divider’s voltage.
What could be simpler than counting pulses and measuring voltages? Not much really. So why fiddle with simplicity? Why climb a mountain? Cross an ocean in a row boat? Because you can. It’s not the feat, it’s the journey! In this journey, I want to create an I2C slave that will do all the sensor work and present the results in table form that can be accessed with a simple I2C connection. If you’ve used I2C in the past, you know how easy it is to use. If not, then you ought to add this as another tool in your kit of experience.
THE I2C BUS
The interface is simple—four wires, power, ground, clock and data. Both signal lines are open collector with external pull-ups. Although I2C was originally designed for use among devices on the same PCB board, the bus is applicable for wiring together devices at length from one another. For instance, I have a system that polls multiple I2C devices using twisted cables of over 120′, using 2.2kΩ resistors on both clock and data lines.
The open collector nature of the bus connections allows multiple Master devices to take hold of the bus and request data from multiple Slaves. Bus arbitration is required when using multiple Masters. In most situations, you will have a single Master requesting data from one or more Slave devices. Each device is given a 7-bit address. (10-bit addresses are available in extended addressing, but I’ve never seen one or needed one.) If you were to purchase an I2C device, it would already have a fixed address. (Some devices have alternate addresses, so you can use multiple devices on the same bus.) There cannot be more than one device on a bus using the same address, otherwise they would both try to answer a request.
The first byte of any I2C transmission contains the 7-bit address plus a read/write bit. So, in reality, a 7-bit address is shifted left 1 bit. The least significant bit becomes 0 (even) to write to the device, and 1 (odd) to read from the device. The 7-bit address 0x13 (00010011) becomes address 0x26 (00100110 = write) and 0x27 (00100111 = read). This is probably the most confusing part of the protocol. I’ll be calling this new address the “address value,” so as not to be confused with the 7-bit address.
A Master is responsible for driving the clock line. The clock line is used to determine when to interpret the data line. The data is allowed to change state only while the clock is low, and are stable when the clock line is high. Should the data line fall while the clock is low, this indicates a start bit (communication will commence). Should the data line rise while the clock is low, this indicates a stop bit (communication has ended). These rules are easily understood by examining Figure 2.
The first data byte is always sent by the Master and is the address value. The hardware in all the I2C devices wakes upon seeing a start bit on the I2C bus. All devices read the address value clocked by the Master. Each device has been given a unique 7-bit address. If an address match occurs, that device continues to pay attention, while all other devices disregard further communications. We’ve already discussed that the LSBit of the address value indicates the mode of the transmission—1 for a read and 0 for a write.
At this point, only one device (or none if there were no device match) is still paying attention and knows whether the Master will be sending more data (writing) or will be expecting data from the Slave (reading). So, a Slave either continues collecting data, reading it from the I2C register when it is full after each 8 clock bits, or places data into the I2C register before the next 8 clock bits. Where these received bytes go or from where these bytes come can be specific to each device. The standard approach is to read from a table where they are written. The table pointer is reset at each start bit and increments for each new byte. Let’s leave it at that for now.
Since the bus is open collector (pulled high when no device is driving either line low), any device can pull a line low. For the data line, it’s easy to see that if SDA (the data signal) is left undriven, any device looking at SDA will interpret this as a 1 (logic high), and if driven (pulled) low, any device looking at SDA will interpret this as a 0 (logic low). For the clock line, the Master will control this, but also pays attention to it. When it stops driving the clock line low, it expects it to go high, since Slaves are not in charge of the clock. However, when a Master wants to read data from a Slave by toggling SCL, it must wait until the SCL is high before clocking in that data from the Slave. In this way if a Slave isn’t ready to load its data into the I2C register, it can hold down the clock line, which causes the Master to wait while it prepares its data.
If the clock line doesn’t return high when the Master stops driving it, the Master pauses until it is released by the Slave. This acts as a handshake that data is ready!
SETTING THE TABLE
The Slave device we are creating for this project will do all kinds of data computations, so that a Master can receive weather data without having to interpret the reed switch functions of each sensor. Here’s where we decide what that data will be. For the rain gauge, each time we get a low pulse, the tip bucket has flip-flopped, passing its magnet past the reed switch that momentarily grounds the pull-up on the sensor’s input. According to the datasheet , each tip equals 0.011″ of liquid. While 1 byte (0-255) could cover 0 × 0.011″ (0.000″) – 255 × 0.011″ (2.800″), this is probably sufficient for most locations. However, the Master would still need to do the conversion to inches from the byte of data. Instead, we’ll use 2 bytes. The high byte is whole inches and the low byte is hundredths of an inch. This covers up to 256.99″ and requires no external computations, other than placing a decimal between the upper and lower bytes. In most cases, this may never exceed an inch if you are resetting the values every hour or day (writing the values of 0 to the table).
The wind speed sensor works on the same principle: a magnet passes a reed switch every complete rotation of the anemometer. The datasheet says that one rotation/s = 1.492mph, so we must include time in our computations for wind speed. I began thinking that we can use the same 2 table bytes as those that we used in the rain gauge, and be able to show wind speeds of up to 255.99mph using this format. However, I decided that fractions of miles per hour were unnecessary. Instead, I could show meaningful wind speed data in a single byte 0-255mph. This led me to realize that other wind speed data might be useful, so I set aside 3 bytes in the table for present, average and peak miles per hour.
Wind direction, as we’ve previously seen, is an analog value that can be boiled down to one of 16 directions. Each direction can have from one to three characters made up of the same four characters (N, E, S and W). Alternately, we might want to show degrees, again three characters—in this case digits (000-337). Remember, degrees are in increments of 22.5 degrees, which is the best we can do with this sensor. I’ll stick to the 16 cardinal directions, thus, three table entries. Table 1 is the complete table.
READING THE SENSORS
Sensor data for rainfall and wind speed are based on changes in the reed relay’s state, and each state change has an opposite edge. The interrupt structure of this microcontroller (MCU), the Microchip Technology PIC16F18313, is two-fold. A dedicated interrupt input pin and a COS (change of state) interrupt can be enabled for any input pin. COS interrupts are all lumped together, so additional registers are required to see through the trees. Separate registers—IOCAP (positive or rising edge) and IOCAN (negative or falling edge)—for each input pin define what will trigger an interrupt. All inputs that have one or both edges enabled will affect the IOCAF register.
The COS interrupt is triggered when the reed switch of the rain sensor and/or wind sensor changes state, due to the position of its associated magnet. The input will drop and rise as the magnet passes the reed switch. We need to recognize only one of these edges. When the IOC (interrupt on change) interrupt is set, we must consult the IOCAF register to determine which input has caused it, and therefore, which routine to run. When the rain bucket tips, its interrupt will increment the variable RainBucket (word). When the wind speed rotates, its interrupt will increment the variable SpeedTick (word).}
The wind direction is handled in the main loop. Execution of any routines in the main loop are based on the passage of time. A separate timer interrupt handles this. It sets a flag once a set amount of time has passed, in this case 1 second. Back in the main loop, we wait to see this flag set before proceeding. An A-D conversion measures the resultant voltage that is presented to the RA5 analog input. The external 10kΩ 1% pull-up to VCC on the RA5 input creates a voltage divider with any resistors that are enabled by the wind direction’s vane magnet. All vane resistors are connected to the RA5 analog input, but only those grounded by a reed switch closure become part of the voltage divider. The resultant measurement is transferred to the ADCDIR register. A conversion flag can be polled to determine when it is appropriate to read this value.
CONVERTING SENSOR READINGS
Rainfall is by far the easiest sensor reading to convert. All conversions use fixed-point, multiply and divide routines. Each bucket tip is equal to 11 thousandths of an inch, so we multiply the number of counts in RainBucket by 11. A 16-bit by 8-bit multiply gives a potential 24-bit result, in thousandths. By dividing this by 1,000 we get both the whole and fractional parts of the result. The whole part will be transferred to the table offset 0 (as 0-255 inches), and the fractional part/10 will be transferred to the table offset 1 (as hundredths of an inch).
Converting wind speed is similar. Each anemometer rotation/s is equal to 1.49mph, so we multiply the number of counts per second in SpeedTick by 149. A 16-bit by 8-bit multiply gives a potential 24-bit result, which is in hundredths of mph. By dividing this result by 100, we lob off the fractional part and can transfer the whole part to the table offset 2 (as present MPH). Detecting the peak speed is just a matter of comparing the present value (table offset 2) to the peak value MPH_P (table offset 3), and saving the larger back into the peak value (table offset 3).
The average speed is another story entirely. Like rainfall, average wind speed will most likely be reset at some point, on an hourly or daily basis, so we’ll want to be able to keep samples until this reset occurs. If samples are taken every second for 86,400 seconds (1 day), then we must be able to total a possible 255 mph × 86,400 seconds/day. That’s a 32-bit value for the MPHTotal and a 24-bit value for the seconds count MPHSeconds. These registers allow the running average MPH_A to be calculated using a 24-bit divide routine.
Calculating wind direction from the sensor’s voltage divider requires several comparisons. See Table 2 for what we can expect from the 8 reed switch-resistor combinations. The voltage presented to the 10-bit ADC will convert to a 10-bit value. We will be using only the most significant 8-bits of each conversion. Note that the values are not in ascending order. A quick sorting of the list into Table 3 allows Check values to be calculated based on the closest neighbor (conversion-wise), which will then be used in comparisons to determine the Cardinal direction characters to be placed into the table at offsets 5, 6 and 7 (Table 2).
I2C addresses were once assigned by the developer of the I2C bus, Philips Semiconductor (now NXP Semiconductor), in the 1980s. Little did they realize how widespread the use of I2C would become outside of its intended local use. Even with its expanded world, the fact remains that each I2C device on a bus must have a unique address. In addition, the end user must know how each I2C Slave device operates, to use it effectively. The data sent to each device must follow the specific rules for that device.
I’ve discussed the simple transfers in which a Slave can have multiple registers where data can come from and go to. In this case, an internal pointer always begins at offset 0 at the beginning of any communications. This pointer automatically increments for every data byte sent or requested. The only issue here is that you can’t get or put any arbitrary byte of data, without first getting or putting all the data up to that arbitrary offset. You can stop at any point, but you must always begin at offset 0.
Around 1995, Intel wanted to improve some of the shortcomings of the I2C bus, and the SMBus (System Management Bus) was developed. The SMB is based mainly on the principles of operation of the I2C bus. The SMBus protocols expand the usefulness of the bus without interfering with the simple Read/Write protocols of I2C. I encourage you to investigate more about these by reading the SMB specifications .
One of the improvements adds a “command” byte as the first byte following any write. This could be used for many things, as you will see in looking through the specs. I could have explained this without bringing SMB into the picture, but if you like I2C, SMB is worth checking out.
When a Slave device follows this format, the special use of the first write data byte in any I2C write, the data can be assigned to the register pointer. This allows a user to point to any register, prior to additional data transfers. You can therefore zero in on the register you want, and not have to begin with offset 0 every time. Writing to a specific register would require only a write function of the format:
To read from a specific register would require a write function (to set the pointer) and a read function of the format:
You may still write or read multiple bytes, but the total number of bytes can be significantly less, depending on the offset. I’m sure you can see the efficiencies of using this format.
REGISTER READS & WRITES
I implemented registered addressing (using the first write to set the table pointer) for this project. This means the MCU’s pointer is set by the first write data byte, as opposed to resetting it automatically to zero for each new read or write. Let’s use an Arduino to read the data table from this MCU. The complete application is in Listing 1.
Now we can look at the I2C interrupt routine for this project (Listing 2), and see how it handles the Arduino requests. The first Arduino routine,
checkI2C(), checks to see if the I2C device is at a specific address by looking to see if the device acknowledges its address. This is handled by the I2C hardware in our project’s MCU when its address (register
SSP1ADD) matches what is sent by the Master.
When the Arduino requests data,
requestData(), this read must be preceded by a write to set the pointer to a specific offset. In this case the
writePointer() routine always is zero. I’ve colored various sections of Listing 2 for reference. Looking at Listing 2, note that in the MCU’s interrupt routine, when an address match has been made, the
Flag.Pointer is set (red). The next time through, when the first data byte is received, this flag is tested. The branch here either uses this value as the new pointer, or on subsequent data it is stored into the table at the pointer (blue). The second part of the request data routine actually retrieves data from the table. Note that the MCU’s hardware automatically holds down the SCL line on a read function, so your routine can have time to fetch the required data. This is released by setting CKP in the
SSP1CON1 register as the code exits (orange) for each byte received. It’s not necessary for writes, but it doesn’t hurt, either.
To clear data, separate Arduino routines are used to handle data at different positions in the table—
resetrainfall() at offset 0 and
resetwindspeed() at offset 2. Your application should keep track of time and selectively clear registers as needed to give stats by hour or day. Our Slave device must recognize when writes are requested to specific registers (blue). Any writes to these will call routines that actually clear the appropriate registers required.
Resetrainfall clears the counter
Resetaveragewind clears totals
MPHSecondsU:H:L, and Resetpeakwind clears table register MPH_P. It can be noted for this example that the actual data values written are not used. In this case, the location written to triggers the clearing routines.
AND IN THE END…
As shown in Figure 3, I’ve built the circuitry on a protoboard that sits inside the rain gauge (Figure 1). If this were a PCB, I could spray it with coating like FINE-L-KOTE AR aerosol conformal coating, to protect it from moisture. For my hand-wired prototype, I plan to put a small plastic bag over it, leaving the bottom open. Note the two threaded standoffs epoxied to the PCB. This creates a firm mounting connection to the rain gauge base, keeping it from interfering with the tipping bucket. Since I had to debug each function separately on alternate I/O pins, RA0:1 are required for debugging/programming. The jumpers allow the sensors to be temporarily rerouted until the code is debugged. Figure 4 shows the circuit for debugging compared to the circuit for a code-ready PCB.
The kind of work that can be done with an 8-pin MCU continues to amaze me. Many sensor modules available today use one of the serial protocols—UART, SPI or I2C—for communications. I hope you can put this knowledge of I2C into practice in your projects. Like a physical tool in your workshop, it never hurts to add a new protocol to your repertoire. My “weather tree” is no longer dumb. The “smarts” added here help distribute the computing power of your system. This allows you to do more, because you need to do less. So little time, so much to do!
PUBLISHED IN CIRCUIT CELLAR MAGAZINE • MAY 2020 #358 – Get a PDF of the issue