Connecting MicroChip’s PIC18F4550 to a K-30 10,000ppm CO2 Sensor is easy and fast to do. This posting contains all of the source codes, schematics and application notes. The K-30 is used in a real application and the code is directly out of projects. Application of K-30 within CELSS The method presented here uses the “bit bang” method for I2C communication. A CCS C compiler is used in conjunction with MPLAB and an ICD3 programmer. The code presented can be ported over to other compilers without I2C built-in functions. The code can be ported to Assembly or to another CPU platform(s). Remember to mind the delay timings.
If you want this running quickly, connect the K-30 I2C pins to…
#define DATACO2 PIN_B3 /*set DATACO2 to PortB3 and SCKCO2 to portB2*/
#define SCKCO2 PIN_B2
on your PIC CPU; copy and paste the source code listed below and compile.
Because of the low cost of the K30 sensor, it became very popular for any application. For less than $100, a programmer could build a sophisticated grow room controller or human presence detector.
Application of K-30 CO2 Sensor
![K-30-SE-19_1024x1024[1]](http://www.gardeningrhythms.com/wp-content/uploads/K-30-SE-19_1024x10241.jpg)
CO2 Sensor Measuring CO2 produced by mushrooms for grow rooms
Basic Application of CO2 sensor in CELSS (Closed Environmental Life Supporting System)
Below is the schematic for a basic PIC application. It connects directly to an ICD3 programmer. Everything runs on +5volts DC. Make sure to install all bypass capacitors or your CPU may not work right. This application calls for 20MHz crystal. One can put up to 48 MHz. The PIC 18F46k22 can also be substituted.

This application uses more than just on gas sensor. The schematic below shows how to connect a K-30 to the PIC CPU. Pull ups are required. The K-30 can be used with a 5 volts or 3.3 volts supply. If using 3.3 volts, make sure the logic levels are adjusted by applying 3.3 volts to the level input pin on the K-30. The K-30 contains a level adjuster.
For this CO2 application ignore all of the other sensors.

Put all of the source code in your project and compile. Use the routine at the bottom to get a finished CO2 reading. This routine reads the CO2 values and converts it into a hex number. There are a few words of caution. The K-30 can be addressed at one certain address and/or all addresses (for applications where you don’t care which device responses back ). The addresses are 0x7E or 0xD0 respectfully. These addresses need to be shifted and padded with zeros. See application examples.
It is very important to insert/use I2C bit stretching routines. When data is sent to the K-30, the routine needs to check if the cycle needs stretching. The K-30 gets busy and needs time to process measurements. Last, when receiving data from the K-30, the CPU sends the clock, but the CPU becomes the slave with respect to ACKs. For an example, when data is written to the K-30, the K-30 sends an ACK bit at the end of each byte. The CPU takes this byte and checks if everything is OK. When the CPU is reading a byte from the K-30, the CPU needs to send an ACK bit back to the K-30 or the K-30 will assume an error and refuse to send the rest of the bytes for the given command. This information is not presented clearly in the data sheets and example application notes.
#define DATACO2 PIN_B3 /*set DATACO2 to PortB3 and SCKCO2 to portB2*/
#define SCKCO2 PIN_B2
#include <18F4550.h>
#DEVICE ICD=TRUE
#include <stdlib.h>
#include <string.h>
#include <math.h>
#fuses HS, NOWDT, NOLVP, NOBROWNOUT, NOPROTECT, PUT
#use delay( crystal=20mhz, clock=20mhz )
#use rs232(baud=9600, xmit=PIN_C6, rcv=PIN_C7, STREAM=COM_A)
void main(){
//*******************************************************************
//Added Oct 4, 2012
//The routines below are used for inting I2C devices.
//*******************************************************************
initControls();
InitCO2(); //Inits CO2 sensor
while (1){
//********************************************************
//Added Oct 11, 2012
// O2 sensor is working for the inside tank sensor.
// The resulting retuened value can be a -1 if the information is not current.
// If the returned value is not a -1 and is indeed a number, then that number is update on the system.
//********************************************************
returnedCOTwo = ReadCOSensor();
if (returnedCOTwo != -1)
{
CarbonDioxide = returnedCOTwo; //If the returned values is not -1, or error, then update the value.
}
checkforcommands(); //This is an event driven operating system. This checks for other processes.
}
}
void InitCO2(void)
//******************************************************************************************
// Added Jan 26, 2012
// This routine sets up the two control bits for output.
//******************************************************************************************
{
output_high(DATACO2);
output_high(SCKCO2);
}
void CO2SendStopBit(void)
//****************************************************************************************
// Added Jan 28, 2012
// This routine sends a start bit only.
//Starting with the clock and data lines low for data SDA change.
//Leaving Data lines high.
//****************************************************************************************
{
output_high(DATACO2);
delay_us(6);
output_high(SCKCO2);
delay_us(6);
output_low(DATACO2);
delay_us(10);
output_high(DATACO2);
}
void CO2SendStartBit(void)
//****************************************************************************************
// Added Jan 24, 2012
// This routine sends a start bit only.
//Leaving the clock low for data SDA change.
//Leaving Data high.
//This routine assumes all lines are high for a while.
//****************************************************************************************
{
output_high(SCKCO2);
output_high(DATACO2);
delay_ms(20);
output_low(DATACO2); //High to low on the data line Start Condition
delay_us(10);
}
//****************************************************************************************
// Added Jan 28, 2012
// This routine is used to send the timing for reading one Byte of data from the I2c.
//****************************************************************************************
BYTE CO2ReadByte(void)
{
int i;
BYTE lVal1; //Add September 3, 2012. This def needed to be added.
long lTmp,lValue;
lVal1=0;
output_low(SCKCO2);
delay_us(5);
output_float(DATACO2);
for (i=0; i<8; i++) //Read CO2 sensor without ACK.
{
lVal1<<=1;
output_high(SCKCO2); //Clock High is when the data is legal.
delay_us(10);
lTmp = input(DATACO2);
delay_us(10);
output_low(SCKCO2);
delay_us(10);
if (lTmp) lVal1|=1;
}
output_low(DATACO2);
delay_us(5);
output_high(SCKCO2); //Clock High is when the data is legal.
delay_us(5);
output_low(SCKCO2);
delay_us(5);
output_float(DATACO2);
delay_us(5);
return(lVal1);
}
//****************************************************************************************
// Added Jan 28, 2012
// This routine is used to send the timing to send one Byte of data to the CO2 sensor.
//This routines makes about 10 attempts of writing one byte of data with ACK. If the ACk is not present, then it tries again until 10 times are up.
//The routine returns a 0 ieverything is OK, it returns a 1 if it failed.
//This routines assumes a startbit has occured.
//****************************************************************************************
int CO2WriteByte(BYTE command)
{
int i,ii,oo;
int lTmp;
int state;
//Make sure the start bit is active.
// All data lines are coming in Low
//Setup first bit of data to write.
//for (ii=0; ii<10; ii++) //loop through 10 tries
//****************************************************************
//Added 2/13/2012
//* data is sent to the CO2 sensor in a backwards way. For instance the command address 0×68 is b01101000. The LSB is send firt and the last bit is the MSB.
//* The wave form looks like b00010110. All numbers are like that for the CO2 sensor.
//* To receive data from the CO2 sensor.
//* Master Transmit:
//* <68> <04> <00> <03> <00> <01> <C8> <F3>
//* This means
//* 0×68 is the command. In the I2C world this needs to be shifted to 0xD0 or 0×68 = b01101000 to 11010000 padded 2 shifts left.
//* 0×04 is the register
//* 0×00 and 0×03 is the register. It is sent in this order.
//* 0×00 and 0×01 is the number of bytes read back in this case 2.
//* The last two are the check Sum where the LSByte is send first and the high one last.
//* All data is send backwards. That is why CCS’s I2C routines don’t work easily.
//* Slave Reply:
//* <68> <04> <02> <03> <01> <24> <09> This gives the number of 0×301 ppm.
output_low(SCKCO2);
delay_us(5);
ii = 0×80;
for (i=1; i<=8; i++) //Routine for writing bits.
{if (ii & command)
{
output_high(DATACO2);
}
else
{
output_low(DATACO2);
}
ii/=2; //Shift over to the next bit.
delay_us(10);
output_high(SCKCO2);
delay_us(10);
output_low(SCKCO2);
}
delay_us(7);
output_float(DATACO2); //Wait to see if the K-30 is ready.
delay_us(7);
output_high(SCKCO2);
delay_us(10);
state = 0;
lTmp=input(DATACO2); //Check to see if the Data line is high. Low means sensor can take a command. A Low means sensor is not ready.
if(lTmp != 0)
{
state = 0; //This means the device is not ready.
}
else
{
state = 1; //This means the device is ready or ACK worked!
}
output_low(SCKCO2);
output_high(DATACO2);
return(state); //If it returns a 0, then is failed. If it reutrns a 1, it worked.
}
Getting a CO2 Reading
int16 ReadCOSensor(void)
//******************************************************************************************
// Added Jan 24, 2012
// This routine sends the sequence of bytes to the CO2 device and then waits for the return data line to go low.
// At the time, the program starts to read the data coming back from the CO2.
//******************************************************************************************
{
int16 lTmp;
int i,kk,lTmp2;
BYTE OTmp;
BYTE first,second,third,forth, checksum;
lTmp = 0;
CO2SendStartBit();
//lTmp=CO2WriteByte(0x7E); //This Sensor address that is 0×68 shifted left one with a zoer padded 0b01101000 -> 0b11010000
lTmp=CO2WriteByte(0xD0); //This Sensor address that is 0×68 shifted left one with a zoer padded 0b01101000 -> 0b11010000
if (lTmp == 1)
{
lTmp = 0;
lTmp=CO2WriteByte(0×22);
if (lTmp == 1)
{
lTmp = 0;
lTmp=CO2WriteByte(0×00);
if (lTmp == 1)
{
lTmp = 0;
lTmp=CO2WriteByte(0×08);
if (lTmp == 1)
{
lTmp = 0;
lTmp=CO2WriteByte(0x2A);
if (lTmp == 1)
{
lTmp = 5;
}
}
}
}
}
CO2SendStopBit();
if (lTmp == 5)
{
delay_ms(10); //Wait for sensor to come back with numbers and then send command.
//******************************************************************************
//Added Feb 13, 2012
//This part waits for a start bit from the Slave.
//******************************************************************************
//Slave Reply:
//* <68> <04> <02> <03> <01> <24> <09> This gives the nuimger of 0×301 ppm.
lTmp=0;
CO2SendStartBit();
lTmp=CO2WriteByte(0xD1); //This Sensor address is 0×68 shifted left one with a zero padded 0b01101000 -> 0b11010000
if (lTmp == 1)
{
first = CO2ReadByte();
second = CO2ReadByte();
third = CO2ReadByte();
forth = CO2ReadByte();
CO2SendStopBit();
lTmp = lTmp+1;
}
else //This is the else command for writing succes the first read command
{
CO2SendStopBit();
lTmp = -1;
return(lTmp); //If is does not work, then return failed.
}
}
else //This is the else command for writing succes the first read command
{
CO2SendStopBit();
lTmp = -1;
return(lTmp); //If is does not work, then return failed.
}
//******************************************************
//Added Oct. 11, 2012
//This section does a checksum of the returned three values and tells the calling routine if the data
// is valid or returns the new CO2 measurement.
//******************************************************
checksum = first + second + third;
if (checksum == forth & first == 0×21)
{
//This sectoin agress that check sum is OK
//Added Oct 11, 2012
// Second is the upper part of the integer.
lTmp = second;
lTmp = lTmp * 256;
lTmp = lTmp + third;
}
else
{
lTmp = -1; // This routines an error if the check sum does not come out.
}
return(lTmp);
}