DHT11 Gather Data without library

Here's a timer-based approach for a Mega2560 that you can adapt to a Nano by using pin D8 and Timer1 instead of Timer4.

/*
 * Sketch:  sketch_dec06a
 * Target:  Mega2560 (can be adapted to Nano)
 * 
 */
 
#define SNSR_READ_INT   1500ul          //mS
#define SNSR_TIMEOUT    50ul            //mS
//
#define MIN_ZERO_WIDTH  1040            //#     uS = N*62.5nS       65uS    nominal is 77uS
#define MAX_ZERO_WIDTH  1520            //#     uS = N*62.5nS       95uS
//
#define MIN_ONE_WIDTH   1600            //#     uS = N*62.5nS       100uS   nominal is 120uS
#define MAX_ONE_WIDTH   2240            //#     uS = N*62.5nS       140uS
//
#define PP_MIN_WIDTH    1120            //#     uS = N*62.5nS       70uS
#define PP_MAX_WIDTH    1440            //#     uS = N*62.5nS       90uS

enum eisrStates
{
    ICP_PP_START = 0,
    ICP_PP_TIMEPP,
    ICP_DATA
        
};

const uint8_t
    pinDbg = 7,
    pinDHT11 = 49;      //ICP4

uint32_t
    tTimeout;    
volatile uint8_t        //5 bytes is 40 bits, enough to hold DHT11 data packet
    grDHT11Data[5];
volatile bool
    bDataFlag = false;
        
void setup() 
{
    Serial.begin(115200);
    pinMode( pinDHT11, INPUT );
    pinMode( pinDbg, OUTPUT );
    digitalWrite( pinDbg, LOW );    

    //set-up input capture on pin 49 (ICP4), prescaler to /1
    TCCR4A = 0;
    //TCCR4B = _BV(ICNC4) | _BV(CS40);
    TCCR4B = _BV(CS40);
    
}//setup

void loop() 
{
    static uint8_t
        stateSensor = 0;
    static uint32_t
        tSensor,
        tDHT = 0ul;
    uint32_t
        tNow = millis();

    switch( stateSensor )
    {
        case    0:
            //periodically read the sensor
            if( (tNow - tDHT) >= SNSR_READ_INT )
            {
                //start by sending the start pulse
                dht11Start();
                //save the time now for the next reading
                tDHT = tNow;                
                stateSensor++;  
                
            }//if
            
        break;

        case    1:
            //either data finishes or we time out
            if( (tNow - tTimeout) >= SNSR_TIMEOUT )
            {
                //timed out waiting for the sensor
                Serial.println( "ERROR: Sensor time-out" );
                stateSensor = 0;
                
            }//if
            else if( bDataFlag )
            {
                //data ready
                showDHTData();
                stateSensor = 0;
                bDataFlag = false;
            
            }//else

        break;
        
    }//switch
    
}//loop

void showDHTData( void )
{
    char
        szT[20];

#if 1
    //human readable
    sprintf( szT, "RH=%d.%d%% T=%d.%doC", grDHT11Data[0], grDHT11Data[1], grDHT11Data[2], grDHT11Data[3] );
    Serial.println( szT );
#else    
    //raw data
    for( uint8_t i=0; i<5; i++ )
    {
        if( grDHT11Data[i] < 0x10 )
            Serial.write( '0' );
        Serial.print( grDHT11Data[i], HEX );
        Serial.write( ' ' );
    }//for

    uint8_t sum = 0;
    for( uint8_t i=0; i<4; i++ )
        sum = sum + grDHT11Data[i];
    if( sum == grDHT11Data[4] )
        Serial.print( "  (CRC OK)" );
    else
        Serial.print( "  (CRC BAD)" );

    Serial.write( '\n' );
#endif
        
}//showDHTData

void dht11Start( void )
{
    //set the data pin to output and drive it low
    pinMode( pinDHT11, OUTPUT );
    digitalWrite( pinDHT11, LOW );
    //give it a start pulse >= 18mS
    delay( 30 );
    //set the pin high, then revert to pull-up to release the line
    digitalWrite( pinDHT11, HIGH );
    pinMode( pinDHT11, INPUT );

    //set up the input capture 4
    noInterrupts();
    TCCR4B &= ~(_BV(ICES4));    //look for falling edge
    TIFR4 |= _BV( ICF4 );       //clear any existing flags
    TIMSK4 |= _BV( ICIE4 );     //enable the input capture
    tTimeout = millis();        //so we can check for timeout in loop()
    interrupts();            
    
}//dht11IssueStart

ISR( TIMER4_CAPT_vect )
{
    static bool
        bEdge = false;
    static uint16_t
        tStart;
    static uint8_t
        stateDHT = ICP_PP_START,
        mask,
        index;
    uint16_t
        tDur,
        tEdge = ICR4;
        
    switch( stateDHT )
    {
        case    ICP_PP_START:            
            //after the start pulse, we set ICP4 to falling edge to detect the beginning of the presence pulse
            //save the time of this interrupt and set the edge to rising so we can measure its width
            tStart = tEdge;
            TCCR4B |= _BV(ICES4);       //set rising edge interrupt
            stateDHT = ICP_PP_TIMEPP;   //move to time the PP width
            
        break;

        case    ICP_PP_TIMEPP:
            //if here, we got a rising edge
            //is the duration of that pulse within spec?
            tDur = tEdge - tStart;            
            if( (tDur >= PP_MIN_WIDTH) && (tDur <= PP_MAX_WIDTH) )
            {
                //PP was a good width; we are ready to start receiving data
                //set up for falling edge interrupts
                bEdge = false;              //first falling edge is "special"
                TCCR4B &= ~(_BV(ICES4));    //falling edges
                mask = 0x80;                //data us received msb first
                index = 0;                  //index into our 5-byte array of data bytes
                memset( grDHT11Data, 0, sizeof( grDHT11Data ) );    //clear the buffer
                stateDHT = ICP_DATA;        //and move to collect data
                
            }//if
            else
            {
                //invalid presence pulsewidth ;turn off ints and go back to start
                //loop() will timeout as recovery
                TIMSK4 &= ~_BV( ICIE4 );
                stateDHT = ICP_PP_START;
                
            }
            
        break;

        case    ICP_DATA:            
            //collecting data here
            //is this the first falling edge after the presence pulse?
            switch( bEdge )
            {
                case    false:
                    //yes; just save the time and set the flag true so we go to the next state
                    //on the following interrupts
                    //log time of first falling edge
                    tStart = tEdge;
                    bEdge = true;
                    
                break;    

                case    true:
                    //get the time between falling edges
                    //for a 0:
                    //
                    // _____a   ___b        a-b time is nominally 77uS
                    //      |__|   |_
                    //
                    //for a 1:                    
                    //
                    // _____a   ______b     a-b time is nominally 120uS
                    //      |__|      |_
                    //                    
                    tDur = tEdge - tStart;
                    //we need to save the time of this falling edge because it's actually the start of the next bit
                    tStart = tEdge;
                    //for readability; if the duration between falling edges is within the realm of '0'
                    if( (tDur >= MIN_ZERO_WIDTH) && (tDur <= MAX_ZERO_WIDTH) )
                    {                        
                        grDHT11Data[index] |= 0;    //OR nothing in
                        
                    }//if
                    else if( (tDur >= MIN_ONE_WIDTH) && (tDur <= MAX_ONE_WIDTH) )
                    {
                        //duration was within the window for a '1' so we or the mask in here
                        grDHT11Data[index] |= mask;                        
                        
                    }//else

                    //for each falling edge, we bump the mask one-bit to the right
                    mask >>= 1;
                    //when it hits zero, we're done this byte so...
                    if( mask == 0 )
                    {
                        //reset the mask...
                        mask = 0x80;
                        //and move to the next byte index
                        index++;
                        //when we've done all five bytes...
                        if( index == 5 )
                        {
                            //set a flag so loop() knows
                            bDataFlag = true;           
                            //reset the state variable back to the start                 
                            stateDHT = ICP_PP_START;
                            //and disable further input capture interrupts
                            TIMSK4 &= ~_BV( ICIE4 );
                            
                        }//if
                        
                    }//if
                        
                break;
                
            }//switch
            
            
        break;
        
    }//switch
    
}//ISR CAPT4

You'll find a low-cost Logic Analyzer to be invaluable for this type of work:
https://www.amazon.com/gp/product/B077LSG5P2/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1

Sorry for dropping off the grid here. The website would not let me post any more replies on my first day. Some kind of anti-spam feature. I scoped the output and compared between my program and the DHT11 library and compared to code posted above, thank you all for your help so far. I confirmed that I am getting a response but only one response. I do not know why it does not repeatedly transmit data.


Capture of DHT Library output


Capture of my code

After realizing I was getting some output, I started serial monitor and then unplugged the arduino after running briefly and I noticed the same initial output.


serial monitor output

I suspected that my 18ms wait was too specific so I lengthened it to 25ms to give the arduino some error if the sensor was that particular about it but it did not help. I expected to see alternating 1's and 0's for the life of the program until I removed power but it seems I am only getting 1's after the first output.

Below is my current code for your review / advice.

/*
  DHT11 Communication Process
  Arduino Pull Low at least 18ms, then Arduino Pause 20-40microseconds for DHT's response

*/


void setup() {
  Serial.begin(9600);
  pinMode(2, INPUT_PULLUP);

}

void loop() {
  pinMode(2, OUTPUT);
  digitalWrite(2, LOW);
  delay(25);
  pinMode(2, HIGH);
  pinMode(2, INPUT);
  pinMode(2, INPUT_PULLUP);


  while(true)
    Serial.println(digitalRead(2));

}```

Your code gets stuck at the while(true) step.

Put a limit on how long you display the data -- say, 6mS -- and wait 1.5-seconds between start pulses. Maybe like:

void setup() 
{
    Serial.begin(9600);
    pinMode(2, INPUT_PULLUP);

}

void loop() 
{
    uint32_t
        tStart;

    delay(1500ul);    
    pinMode(2, OUTPUT);
    digitalWrite(2, LOW);
    delay(25);
    pinMode(2, HIGH);
    pinMode(2, INPUT);
    pinMode(2, INPUT_PULLUP);

    tStart = micros();
    do
    {
        Serial.println(digitalRead(2));
    }while( micros() - tStart < 6000ul ); 

}

Interesting, I will try that tomorrow. A lot of C language , I am not super familiar with so that code is very helpful in showing me what to learn. Thank you!

Ok I get it now, just easy way to grab current time to use and place limits on conditionals. My only question at the moment with the 32 bit variable is, do we not care about the last 8 bits in the stream? The sensor said it sends 40 bits total.

And I don't understand the purpose for 6000 as an unsigned long vs just putting 6000

You're not storing the data anywhere, just printing the state of pin 2 as fast as you can.

The 32-bit variable tStart is used to store the value returned by micros() (in this case, or millis() in other timing examples.) If you use a variable type that doesn't have that storage capability, you'll experience unexpected behavior of your timing.

The sensor indeed sends 40 bits. A 'zero' bit takes ~77uS and a 'one' bit take 120uS. If we allow at least, say, 150uS for each bit them then we're sure to capture every bit; 40 bits x 150uS = 6000. That's why the do/while waits for 6mS (6000uS) before dropping out. Now, the digitalRead and println within the loop and the 32-bit math to check the time each loop may slow things enough that some bits get missed; you'd need to do some checking with your scope if you're curious.

As mentioned, millis() and micros() and delay() etc all work with 32-bit arguments. Specifiying 'ul' after a value like 6000 (e.g. 6000ul) is good practice and tells the compiler to treat the value as a 32-bit number even if it could actually fit into a smaller type.

Awesome, thank you very much! I will have a fun morning tomorrow playing around with all this new info!

Ok it is working now and I like how you created the solution using micros(). I woke up this morning thinking I had it but had to look at your code so now I think it will really stick in my head to approach these types of problems using the built in system time and simple subtraction.

I would like some clarification on one part though. I am getting confused on sample rate vs println output. Shouldn't the while loop populate a lot of println's a second?

/*
  DHT11 Communication Process
  Arduino Pull Low at least 18ms, then Arduino Pause 20-40microseconds for DHT's response

*/


void setup() {
  Serial.begin(9600);
  pinMode(2, INPUT_PULLUP);
}

void loop() {
  
  delay(5000);
  pinMode(2, OUTPUT);
  digitalWrite(2, LOW);
  delay(25);
  pinMode(2, HIGH);
  pinMode(2, INPUT);
  pinMode(2, INPUT_PULLUP);
  float i = micros();
  int total = 0;
  while(micros() - i < 6000)
    Serial.println(digitalRead(2));
    total++;
    

  Serial.println("Run Complete");
  Serial.println("Total bits: ");
  Serial.print(total);

}

My current code is not incrementing accurately so that adds to the confusion. I count ~25 bits printed but my variable only has a value of 1.

Your baud rate is 9600. With standard N81 serial, a character is 10 bit times: 1 start + 8 data + 1 stop. 9600 bits per second means each bit requires 104.17uS; multiply that by ten bits and you get 1.042mS to send one byte. And because you're using println() you're actually sending three bytes; the pin reading (0 or 1) and a CR (0x0d) and a LF (0x0a) so each serial print requires 3 x 1.042mS or 3.126mS.

When you do rapid-fire serial prints -- that is, you call Serial.println and then call it again before the first bytes have sent, Serial.println blocks; it's not able to send those bytes because there's still bytes from the previous message waiting to go.

While Serial.println() is blocking, you're missing reads of the bit stream. On my scope here, this code:

const uint8_t pin3 = 3;

void setup() 
{
    Serial.begin(9600);
    pinMode( pin3, OUTPUT );    
}

void loop() 
{
    digitalWrite( pin3, HIGH );
    Serial.println( '0' );
    digitalWrite( pin3, LOW );
}

shows that the println function actually blocks for more than 3mS waiting for the characters to go before starting to send the new message.

Keep in mind that the bits from the DHT11 are 77uS and 120uS in width. They're coming whether the processor is looking at pin 2 or waiting for characters to be sent. While you're waiting more than 3mS for your messages to go, thirty nine (39) 0s or twenty five (25) 1s could have come and gone.

So yes, the loop prints a lot of stuff in a second but your data is coming in way faster than that. There's no hardware handshaking like "request to send" and "clear to send"; the DHT11 is just a blabbermouth that talks whether you're listening or not.

If you look at my code in post #21 -- one example of how you might read the sensor without using an established library -- you'll see it's sort of non-trivial. In that example, I time the period between falling edges of the incoming stream (short time between edges is a '0' and long time is '1') and craft a series of bytes from that. I don't do any delays or other blocking stuff while trying to read the data stream. I save the print for after the stream is complete and I've checked the data. Then I send another start and read another stream etc.

This is because you're missing braces around the entirety of the code you want to perform in the while loop. For example, to increment total each pass:

while(micros() - i < 6000)
{
    Serial.println(digitalRead(2));
    total++;
}//while

Keep in mind that you're not reading the stream per se here. You're reading a pin that's wiggling up and down but you're not reading the '1's and '0' encoded in that stream, even if you weren't being blocked by Serial.println().

The stream carries its own timing sync; the falling edges of each bit (this is why I used falling-edge input capture interrupts in my example). You're just reading randomly in time and seeing highs and lows but there's no "data" in what you're reading.

BTW, micros() returns an unsigned long (uint32_t), not a float.

Can't thank you enough, I have a lot more work to do. Thank you for guideing me!!

You can play with this. It tries to read the data without using interrupts, just blocking waiting for bits and, if there's no data it'd get stuck forever. But it seems to work.

The output looks like this:

0001001000000000000111000000010100110011 : 12 00 1C 05 33 
0001001000000000000111000000010100110011 : 12 00 1C 05 33 
0001001000000000000111000000010100110011 : 12 00 1C 05 33 
0010011000000000000111000000010101000111 : 26 00 1C 05 47 
0101001100000000000111000000011001110101 : 53 00 1C 06 75 
0101011100000000000111000000011101111010 : 57 00 1C 07 7A 
0100110100000000000111000000011001101111 : 4D 00 1C 06 6F 
0100010000000000000111000000011101100111 : 44 00 1C 07 67 

The raw binary received is shown on the left. After the ':' is that binary converted to HEX for readability.

If you break down one line, say:

0010011000000000000111000000010101000111 : 26 00 1C 05 47

You can see the RH value (26h == 38%) and the temperature (0x1c with 0x05 as the value after the decimal or 28.5). You can tell the data is valid because 26h + 00h + 1Ch + 05h == 47h, which matches the last value in the received data -- the checksum (0x47).

This code doesn't always work; once in a while I see a bad bit sneak in. I can't be bothered to find out why. Waiting in while() loops, digitalRead(), working with 32-bit unsigned longs etc are probably pushing things timing-wise.

Hopefully this gives you a better idea of how you might interpret the data. BTW, consult the datasheet; it explains the start, presence pulse and 0/1 bit timing better than I can.

The code:

const uint8_t pinDHT = 2;

uint32_t
    tDur,
    tStart,
    tEnd;
uint8_t
    numBits,
    mask,
    ch,
    grBits[40];
    
void setup() 
{
    Serial.begin(9600);
    pinMode( pinDHT, INPUT );    
}

void loop() 
{
    delay(1500);

    //send a start pulse by driving the data line low for >18mS
    pinMode(pinDHT, OUTPUT);
    digitalWrite(pinDHT, LOW);
    delay(25);
    digitalWrite(pinDHT, HIGH);
    pinMode(pinDHT, INPUT);    

    //likely not needed but wait for the data line to go high
    while( digitalRead( pinDHT ) == LOW );

    //wait for the DHT11 presence pulse
    //first, wait for the data line to go low    
    while( digitalRead( pinDHT ) == HIGH );
    //when it does, grab the uS time
    tStart = micros();
    while( digitalRead( pinDHT ) == LOW );
    tEnd = micros();
    tDur = tEnd - tStart;

    //if the presence pulse was ~80uS, it's valid and we can read the data
    if( (tDur > 70ul) && (tDur < 90ul) )
    {            
        //again, likely not needed but double check the line is high
        while( digitalRead( pinDHT ) == HIGH );
        //it's now gone low; grab the micros time now
        tStart = micros();
        //counts the number of bits received
        numBits = 0;

        //wait till we're done all 40 bits
        while( numBits < 40 )
        {        
            //bits always start with a high to low transition and a 50uS low period
            //wait for that to end
            while( digitalRead( pinDHT ) == LOW );
            //after the "bit start bit", the line goes high again
            //wait for it to go low
            while( digitalRead( pinDHT ) == HIGH );
            //line fell
            //  check the duration since the last falling edge
            //      if it's between 70 and 90uS, I'm calling it a '0'
            //      otherwise assuming it's a '1'
            // NOTE to save time I'm just saving each bit in a uint8_t; 0 or 1
            tEnd = micros();
            tDur = tEnd - tStart;
            if( tDur > 70ul && tDur < 90ul )
                grBits[numBits] = 0;
            else
                grBits[numBits] = 1;

            //the end of this bit is actually the start of the next, so set the
            //"start time" to what was the end of the last bit
            tStart = tEnd;

            //we just got one bit; bump the bit counter
            numBits++;
        
        }//while
        
    }//if

    //after we're done 40 bits, print out the bits received
    for( numBits=0; numBits<40; numBits++ )
        Serial.write( (grBits[numBits] == 0) ? '0':'1' );

    //after printing the raw binary, print a delimiter
    Serial.print( " : " );

    //convert that binary to more readable hex
    mask = 0x80;
    ch = 0;
    for( numBits=0; numBits<40; numBits++ )
    {
        if( grBits[numBits] )
            ch |= mask;
        mask >>= 1;
        if( mask == 0 )
        {
            if( ch <= 0x0f )
                Serial.write( '0' );
            Serial.print( ch, HEX );
            Serial.write( ' ' );
            mask = 0x80;
            ch = 0;            
        }//if
        
    }//for

    //after printing the binary and hex, go to the next line
    Serial.println();

}//loop

I was doing a basic test to make sure I had good inputs and data and came across something odd.

void setup() {
  pinMode(2, OUTPUT);
}

void loop() {
  pinMode(2,LOW);
  delay(50);
  pinMode(2, HIGH);
  delay(25);
}

My scope shows a High of 50ms and a Low of 25ms but the code is supposed to be doing the opposite. I signed up for a Microprocessor Architecture class to help give me a better background of some topics I think I can brush up on, I'm just not sure why the code is doing the opposite of what I would expect.

Did you perhaps mean to use digitalWrite() rather than pinMode() in loop() ?

That was it, thank you sir! I'm leaving the thread open because I still have some more work to do.

It's weird that it compiled when I was using pinmode(2, High) and low when I should have been using digitalWrite(). You would think it would have thrown an error.

'HIGH' is a macro that evaluates to 1. That's a perfectly valid second argument for 'pinMode'.

Ok, that is still weird that it does the opposite of what I expected on the scope.

Nope, not weird either.

HIGH and OUTPUT are both macros that evaluate to 1. So, you were setting the pin for OUTPUT and the port's bit in its data register was likely zero. So, the output went low.

LOW and INPUT are both macros that evaluate to 0. So, that statement was setting the pin for INPUT. That's high-z, so the pin could have floated high. Or, perhaps the internal pullup was enabled.

Thank you, that makes sense! I really need to take the microcontroller architecture class. Cant start soon enough!