Projects Research & Design Hub

Upgrading the Weather Tree

Written by Jeff Bachiochi

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.

  • How to use reed switches
  • How to implement sensors
  • How to use wind speed sensors
  • How to make an I2C slave
  • How to use an Arduino to read data from an MCU
  • Arduino board
  • Reed Sensors
  • Microchip Technology PIC16F18313,
  • I2C
  • SMBus

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.

FIGURE 1 – The sensor tree contains tipping rain gauge, anemometer and wind direction sensors. Sensors connect to your circuitry via two 4-conductor phone cable connectors.

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.

— ADVERTISMENT—

Advertise Here

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.

FIGURE 2 – I2C communication requires two signals: SCL (clock) and SDA (data). Except for the start and stop bits (yellow), you can be assured that the data won’t change while the clock line is high (green). After the start bit, an acknowledge ninth bit is added for every 8 data bits. This is feedback that someone is listening to the communication.

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 [1], 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.

— ADVERTISMENT—

Advertise Here

TABLE 1 – This project requires reading 7 bytes from this table to get real data from all three binary weather sensors.

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).

TABLE 2 – Values taken from datasheet. I added the Cardinal and Conversion columns.
TABLE 3 – With the Conversion values sorted in ascending order, the Check column was calculated to find the average value. This value can be used for comparisons in the code, to determine to which Cardinal direction the vane’s position refers.

I2C EVOLUTION
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 [2].

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:

I2CWrite
(7-bit Address*2)+0
Pointer
Data

To read from a specific register would require a write function (to set the pointer) and a read function of the format:

I2CWrite
(7-bit Address*2)+0
Pointer
I2CRead
(7-bit Address*2)+1
Data

— ADVERTISMENT—

Advertise Here

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.

LISTING 1 – The Arduino makes a good “test bed” for interfacing to the project.

// This example code is in the public domain.
String SignOn = "I2C Weather Probe 1/7/2020";
#include <Wire.h>
byte I2CADDRESS = 0x26;
byte BYTECOUNT = 8;
byte rainfall_W;
byte rainfall_F;
byte windspeed;
byte windspeed_A;
byte windspeed_P;
String winddirection = " ";
//
void setup()
{
Wire.begin();
// join i2c bus (address optional for master)
Serial.begin(115200); // start serial for output
Serial.println(SignOn);
}
//
void loop()
{
Serial.print("Checking I2C address " + String(I2CADDRESS));
if(checkI2C()==0)
{
Serial.print(" ... reading");
requestData();
Serial.println(" ... Got it!");
displayData();
}
else
{
Serial.println(" ... No one home at this address!");
}
Serial.println("Resetting rainfall!");
resetrainfall();
Serial.println("Resetting Average and Peak wind speeds!");
resetwindspeed();
Serial.println("Waiting 10 seconds before reading
data again");
delay(10000);
Serial.println();
}
//
boolean checkI2C()
{
Wire.beginTransmission(I2CADDRESS);
return Wire.endTransmission();
}
//
void writePointer()
{
Wire.beginTransmission(I2CADDRESS);
Wire.write(0); // offset pointer=0
Wire.endTransmission();
}
//
void resetrainfall()
{
Wire.beginTransmission(I2CADDRESS);
Wire.write(0); // offset pointer=0
Wire.write(0); // rainfall_W=0
Wire.write(0); // rainfall_F=0
Wire.endTransmission(); 
}
//
void resetwindspeed()
{
Wire.beginTransmission(I2CADDRESS);
Wire.write(2); // offset pointer=2
Wire.write(0); // windspeed_A=0
Wire.write(0); // windspeed_P=0
Wire.endTransmission();
}
//
void requestData()
{
writePointer();
Wire.requestFrom(I2CADDRESS, BYTECOUNT);
while(!Wire.available());
winddirection = "";
for(byte i=0; i<=BYTECOUNT; i++)
{
switch (i)
{
case 0:
rainfall_W = Wire.read();
break;
case 1:
rainfall_F = Wire.read();
break;
case 2:
windspeed = Wire.read();
break;
case 3:
windspeed_A = Wire.read();
break;
case 4:
windspeed_P = Wire.read();
break;
default:
winddirection = winddirection + Wire.readString();
break;
}
}
}
//
void displayData()
{
Serial.println("Today's rainfall " + String(rainfall_W) + "." + String(rainfall_F));
Serial.println("The wind is out of the " + winddirection + " at " + String(windspeed) + " MPH");
Serial.println(" with an average windspeed of " + String(windspeed_A) + " MPH and peak of " + String(windspeed_P) + " MPH");
}

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.

LISTING 2 – The I2C interrupt routine that handles communication requests from a Master (in this case the Arduino). Reading and writing are handled by the indirect pointer FSR0H:L. This pointer is auto-incremented after each data byte.

;***************************************;***************************************
;
; I2C INTERRUPT SERVICE ROUTINE
;
;***************************************;***************************************
;
Interrupt_I2C
banksel PIR1
bcf PIR1, SSP1IF ; clear the interrupt
banksel MyFSR0L_Pointer
movf MyFSR0L_Pointer, W ; retrieve the stored pointer
movwf FSR0L ; use Pointer
clrf FSR0H ; always in Bank 0
banksel SSP1STAT
btfsc SSP1STAT, D_NOT_A ; skip next if received byte was our address
goto Interrupt_I2C_Data ; else it must be a data byte
;
Interrupt_I2C_Address
movf SSP1BUF, W ; dummy read of address
bsf Flags, Pointer ; set Pointer flag

;
btfss SSP1STAT, R_NOT_W ; skip next if Master needs to read data
goto Interrupt_I2C_Exit ; else I need to read data from Master
;
Interrupt_I2C_AddressRead
movf INDF0, W ; get table value
movwf SSP1BUF ; send it to Master
goto Interrupt_I2C_BumpPointer
;
Interrupt_I2C_Data
btfsc SSP1STAT, R_NOT_W ; skip next if Master needs to read data
goto Interrupt_I2C_DataRead ; else I need to read data from Master
;
Interrupt_I2C_DataWrite ; Data from Master
btfss Flags, Pointer ; skip next if the first byte
goto Interrupt_I2C_DataWriteContinue
;
Interrupt_I2C_DataWriteSetPointer
movlw low StartOfI2CData ; bottom of table
movwf FSR0L ; as new Pointer
movf SSP1BUF, W ; get received value
addwf FSR0L, F ; use it as the new Pointer offset
bcf Flags, Pointer ; clear Pointer flag
goto Interrupt_I2C_CheckPointer
;
Interrupt_I2C_DataWriteContinue
movf SSP1BUF, W ; get received value
movwf INDF0 ; save it to the table @ Pointer

;
movlw Rain_F
subwf FSR0L, W ; test for table offset 1
btfss STATUS, Z ; skip next if no match
call resetrainfall ; else reset rainfall
;
movlw Wind_A
subwf FSR0L, W ; test for table offset 3
btfss STATUS, Z ; skip next if no match
call resetaveragewind ; else reset average wind
;
movlw Wind_P
subwf FSR0L, W ; test for table offset 4
btfss STATUS, Z ; skip next if no match
call resetpeakwind ; else reset peak wind 
;
Interrupt_I2C_BumpPointer
incf FSR0L, F ; increment the table Pointer
;
Interrupt_I2C_CheckPointer
movlw low EndOfI2CData ; top of table
subwf FSR0L, W ; subtract top from Pointer
btfss STATUS, Z ; skip next if equal
goto Interrupt_I2C_Exit ; else not a top so we're good to go
;
Interrupt_I2C_PointerRollover
movlw low StartOfI2CData ; bottom of table
movwf FSR0L ; as new Pointer
goto Interrupt_I2C_Exit
;
Interrupt_I2C_DataRead ; data to Master
banksel SSP1CON2
btfsc SSP1CON2, ACKSTAT ; skip if ACK
goto Interrupt_I2C_Exit ; else no more data is wanted
;
movf INDF0, W ; get value from table @ Pointer
movwf SSP1BUF ; send it to Master
goto Interrupt_I2C_BumpPointer
;
Interrupt_I2C_Exit
banksel SSP1CON1
bsf SSP1CON1, CKP ; release clock
movf FSR0L, W ; get the Pointer
banksel MyFSR0L_Pointer
movwf MyFSR0L_Pointer ; save it for next byte
return

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 RainBucket0:1Resetaveragewind clears totals MPHTotalU:H:L and 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.

FIGURE 3 – My “weather tree” outfitted with this project hand-wired PCB. I used a 4-wire twisted cable for the I2C connection to, in this case, the Arduino inside my shed.
FIGURE 4 – The schematic is drawn twice. The circuit on the left allows each sensor to be routed to a couple of different I/Os on the 8-pin microcontroller, so inputs RA0:1 can be dedicated for debugging. The circuit on the right has these sensors assigned to their final I/O, once the use of debugging pins can be released.

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! 

RESOURCES

References:
[1]  https://www.argentdata.com/files/80422_datasheet.pdf
[2]  http://smbus.org/specs

Argent Data Systems | www.argentdata.com
Microchip Technology | www.microchip.com

PUBLISHED IN CIRCUIT CELLAR MAGAZINE • MAY 2020 #358 – Get a PDF of the issue


Don't miss out on upcoming issues of Circuit Cellar. Subscribe today!

 
 
Note: We’ve made the October 2017 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.


Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

Become a Sponsor
Website | + posts

Jeff Bachiochi (pronounced BAH-key-AH-key) has been writing for Circuit Cellar since 1988. His background includes product design and manufacturing. You can reach him at: [email protected] or at: www.imaginethatnow.com.