0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NANOPI NEO3 GPIOでDHT11, DHT22を接続

0
Last updated at Posted at 2025-05-21

NANOPI NEO3 GPIOにDHT11を接続してみた

はじめに

ここ数か月、空き時間を見ながら調べていたが、やっと動いたので同じことを探している人に役立つかと思い、記録しておきます。
NANOPI NEO3、いくら検索しても情報が少ない。これまでRaspberry Piを参考に解決してきたが、DHT11となると、何も情報がない!
WiringNPも動かない、gpiodも動かない。RPi.GPIO_NPも動かない、どうする?

ChatGTPさんの助けをかりた

次のような基本的な提案がいくつかあった。

gpiodを使った pythonサンプル, cサンプル...一緒に考えてくれた...そして...解決!

環境

NANOPI NEO3
イメージ: rk3328-sd-friendlycore-lite-focal-5.10-arm64-20210830.img

Distributor ID: Ubuntu
Description: Ubuntu 20.04.6 LTS
Release: 20.04
Codename: focal

必要なパッケージ

gpiodなどは不要。開発環境のみインストール

sudo apt-get update
sudo apt-get install build-essential
sudo apt-get install gdb
sudo apt-get install rt-tests

DHT11の接続方法

接続方法は、以下の様にした。

PIN 2 5V
PIN 6 GND
PIN 7 DATA

NENOPI.jpg

GPIO.jpg

悪戦苦闘

この接続の場合、Linux GPIO66, GPIOチップ名: gpiochip2, GPIOピン番号: 2(gpiochip2内の相対番号)でgpiod で動くはずだがDHT11が全く応答していない。
DHT22やI2C式センサーなら動くよ、との提案もあったが、DHT22が入手困難 (EOL)

gpiodよりmmap

chatGPTさんからの提案で

mmap + C言語で直接GPIO制御する場合
NanoPi NEO3のGPIO2のレジスタベースは 0xFF780000(RK3328の場合)ですが、GPIO2_A2がピン2に対応するなら
GPIO_PIN = 2 (bit 2 を操作)
で問題ありません。

ときたので、gpiodを諦めmmapでアクセスすることにした。サンプルコードを作成してもらう。
コンパイルして、実行したが動かない。

GPIOベースアドレスとピン番号の確認

指示とおりに確認してみた。

pi@NanoPi-NEO3:~/.vscode-server$ sudo cat /proc/iomem | grep gpio
ff210000-ff2100ff : ff210000.gpio gpio@ff210000
ff220000-ff2200ff : ff220000.gpio gpio@ff220000
ff230000-ff2300ff : ff230000.gpio gpio@ff230000
ff240000-ff2400ff : ff240000.gpio gpio@ff240000

サンプルコードの値を正しく修正
修正前

#define RK3328_GPIO2_BASE 0xFF780000 

修正後

#define RK3328_GPIO2_BASE 0xFF230000  // GPIO2 base address corrected

GPIOが動作した

HDT11は、まだ動かない。ただ、初期のGPIO信号の書き込みが成功して、信号がHIGHになったことを確認した。
のでDEBUGの手段として、GPIOのみ確認するよう提案があった。
プログラムは、以下のとおり。

gpio.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <time.h>

#define RK3328_GPIO2_BASE 0xFF230000  

//#define GPIO_MEM_SIZE     0x10000     // 64KB
#define GPIO_MEM_SIZE 0x100
#define GPIO_PIN          2           // GPIO2_A2 = GPIO66
#define DATA_BITS         40
#define DELAY         30
#define TiMEOUT         2500

#define GET_BIT(value, bit) (((value) >> (bit)) & 1)

volatile uint32_t *gpio_base = NULL;

void delay_us(unsigned int us) {
    struct timespec ts_start, ts_now;
    clock_gettime(CLOCK_MONOTONIC_RAW, &ts_start);
    do {
        clock_gettime(CLOCK_MONOTONIC_RAW, &ts_now);
    } while (((ts_now.tv_sec - ts_start.tv_sec) * 1000000 +
             (ts_now.tv_nsec - ts_start.tv_nsec) / 1000) < us);
}

// GPIO register macros (RK3328)
#define GPIO_SWPORTA_DR      0x00
#define GPIO_SWPORTA_DDR     0x04
#define GPIO_EXT_PORTA       0x50

void set_gpio_output() {
    uint32_t val = gpio_base[GPIO_SWPORTA_DDR / 4];
    val |= (1 << GPIO_PIN);
    gpio_base[GPIO_SWPORTA_DDR / 4] = val;
}

void set_gpio_input() {
    uint32_t val = gpio_base[GPIO_SWPORTA_DDR / 4];
    val &= ~(1 << GPIO_PIN);
    gpio_base[GPIO_SWPORTA_DDR / 4] = val;
}

void gpio_write(int val) {
    uint32_t reg = gpio_base[GPIO_SWPORTA_DR / 4];
    if (val)
        reg |= (1 << GPIO_PIN);
    else
        reg &= ~(1 << GPIO_PIN);
    gpio_base[GPIO_SWPORTA_DR / 4] = reg;
}

int gpio_read() {
    return (gpio_base[GPIO_EXT_PORTA / 4] & (1 << GPIO_PIN)) != 0;
}


int main() {
    int fd = open("/dev/mem", O_RDWR | O_SYNC);
    gpio_base = mmap(NULL, GPIO_MEM_SIZE, PROT_READ | PROT_WRITE,
                     MAP_SHARED, fd, RK3328_GPIO2_BASE);

    set_gpio_output();

    while (1) {
        gpio_write(0);  // LOW
        printf("write 0\n");
        usleep(500000);
        gpio_write(1);  // HIGH
        printf("write 1\n");
        usleep(500000);
    }
}

ビルド

$gcc -O2 -o gpio gpio.c

実行

pi@NanoPi-NEO3:~/.vscode-server$ sudo ./gpio
write 0
write 1
write 0
write 1
write 0
write 1
write 0
write 1
write 0
write 1

この間、PIN 7をテスターで確認するとLOW/HIGHが切り替わっていることを確認!
GPIOのLOW/HIGH切り替えが正しく動いているのが確認できた。

DHT11信号タイミングの概要

恐らく、信号のタイミングの問題ではないかとの推測がchatGPTさんからあった。

スタート信号
MCUがDATAラインをLOWにして18ms以上保持 → DHT11が応答を返す準備
DHT11応答
80μs LOW
80μs HIGH
データ送信(40ビット)
1ビットはLOWで始まり、その後のHIGHの長さで0か1か判別
LOWは約50μs固定
HIGHの長さ
約26〜28μs → 0
約70μs → 1

DEBUGメッセージを入れて確認すると、DHT11からの読み取りが40バイト前にタイムアウトしてしまうことが分かった。

mmapサイズを256に小さくする。もとは 0xFF230000

#define GPIO_MEM_SIZE 0x100

完成!

何度かタイミングの修正の提案を反映しながらテストしていると、あれ? 動いた!


pi@NanoPi-NEO3:~/.vscode-server$ sudo ./dht11_mmap 
成功: 湿度 50%, 温度 26°C

だた、何度も実行するとエラー、成功と安定しない。最終的には、リトライを入れて対応
以下が、コードです。

dht11_mmap.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <time.h>
#include <string.h>

// GPIO2 Base アドレス(GPIO2_Ax)
#define RK3328_GPIO2_BASE 0xFF230000
#define GPIO_MEM_SIZE     0x100
#define GPIO_PIN          2  // GPIO2_A2 = GPIO66
#define DATA_BITS         40

volatile uint32_t *gpio_base = NULL;

// GPIO レジスタ
#define GPIO_SWPORTA_DR   0x00
#define GPIO_SWPORTA_DDR  0x04
#define GPIO_EXT_PORTA    0x50

void delay_us(unsigned int us) {
    struct timespec ts_start, ts_now;
    clock_gettime(CLOCK_MONOTONIC_RAW, &ts_start);
    do {
        clock_gettime(CLOCK_MONOTONIC_RAW, &ts_now);
    } while (((ts_now.tv_sec - ts_start.tv_sec) * 1000000 +
             (ts_now.tv_nsec - ts_start.tv_nsec) / 1000) < us);
}

void set_gpio_output() {
    uint32_t val = gpio_base[GPIO_SWPORTA_DDR / 4];
    val |= (1 << GPIO_PIN);
    gpio_base[GPIO_SWPORTA_DDR / 4] = val;
}

void set_gpio_input() {
    uint32_t val = gpio_base[GPIO_SWPORTA_DDR / 4];
    val &= ~(1 << GPIO_PIN);
    gpio_base[GPIO_SWPORTA_DDR / 4] = val;
}

void gpio_write(int val) {
    uint32_t reg = gpio_base[GPIO_SWPORTA_DR / 4];
    if (val)
        reg |= (1 << GPIO_PIN);
    else
        reg &= ~(1 << GPIO_PIN);
    gpio_base[GPIO_SWPORTA_DR / 4] = reg;
}

int gpio_read() {
    return (gpio_base[GPIO_EXT_PORTA / 4] & (1 << GPIO_PIN)) != 0;
}

int read_dht11_data(int *humidity, int *temperature) {
    uint8_t data[5] = {0};

    // スタートシーケンス
    set_gpio_output();
    gpio_write(0);
    delay_us(20000);  // 20ms
    gpio_write(1);
    delay_us(40);     // 40us
    set_gpio_input();

    // 応答待ち: LOW → HIGH
    int timeout = 0;
    while (gpio_read()) {
        delay_us(1);
        if (++timeout > 100) {
            printf("Timeout waiting for LOW (start)\n");
            return -1;
        }
    }

    timeout = 0;
    while (!gpio_read()) {
        delay_us(1);
        if (++timeout > 100) {
            printf("Timeout waiting for HIGH (response)\n");
            return -1;
        }
    }

    timeout = 0;
    while (gpio_read()) {
        delay_us(1);
        if (++timeout > 100) {
            printf("Timeout waiting for LOW (data start)\n");
            return -1;
        }
    }

    // ビット読み取り(40ビット = 5バイト)
    for (int i = 0; i < 40; i++) {
        // LOW: 開始ビット (約50us)
        timeout = 0;
        while (!gpio_read()) {
            delay_us(1);
            if (++timeout > 100) {
                printf("Timeout waiting for HIGH at bit %d\n", i);
                return -1;
            }
        }

        // HIGHパルス長で0/1を判別
        delay_us(35);  // 約26〜28us: 0, 70us: 1

        if (gpio_read()) {
            data[i / 8] |= (1 << (7 - (i % 8)));
        }

        // HIGHが続く場合、LOWに戻るまで待つ
        timeout = 0;
        while (gpio_read()) {
            delay_us(1);
            if (++timeout > 100) break;
        }
    }

    // チェックサム確認
    uint8_t sum = data[0] + data[1] + data[2] + data[3];
    if (data[4] != sum) {
        printf("Checksum error: got %d, expected %d\n", data[4], sum);
        return -1;
    }

    *humidity = data[0];
    *temperature = data[2];
    return 0;
}

int main() {
    int fd = open("/dev/mem", O_RDWR | O_SYNC);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    gpio_base = (uint32_t *)mmap(NULL, GPIO_MEM_SIZE, PROT_READ | PROT_WRITE,
                                 MAP_SHARED, fd, RK3328_GPIO2_BASE);
    if (gpio_base == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    int humidity = 0, temperature = 0;
    int retries = 5;
    while (retries-- > 0) {
        if (read_dht11_data(&humidity, &temperature) == 0) {
            printf("成功: 湿度 %d%%, 温度 %d°C\n", humidity, temperature);
            break;
        }
        printf("再試行中...\n");
        sleep(1); // DHT11の仕様では1秒以上空ける必要あり
    }

    munmap((void *)gpio_base, GPIO_MEM_SIZE);
    close(fd);
    return 0;
}

ビルドと実行

$gcc -O2 -o dht11_mmap dht11_mmap.c
$sudo ./dht11_mmap

終わりに

chatGPTさん、すごいな。もう無理かと思っていたことで解決できたことに感謝!
NANOPI NEO3でDHT11が使えること分かったし、そもそも GPIOが制御できなかったのみ、これで解決した。

フルソースは、GitHubに置きました。お役に立てれば幸いです。

あとがき

そして、これが現在のNANOPI NEO3です。まだまだ拡張できるかな?

USB3.0 aeroTAP 3D USB GSカメラ
USB2.0 WiFi
GPIO DHT11
LAN Gigabit LAN
裏にヒートシンク

SmartSensor.jpg

追記 DHT22対応について

DHT11のコードを元に、DHT22に対応しました。

項目 DHT11 DHT22 (AM2302)
湿度精度 ±5%RH ±2~5%RH
温度精度 ±2°C ±0.5°C
測定周期 1Hz(1秒に1回) 0.5Hz(2秒に1回)
データ構成 8bit * 5 (整数のみ) 16bit * 2 + checksum

測定間隔の変更とFloat型データに対応が必要です。
また、開始時にセンサーとの接続チェック、保存フォーマットを変更しました。GitHubに追加しておきました。

dht22_mmap.c
//
// gcc -O2 -o dht22_mmap dht22_mmap.c
// sudo ./dht22_mmap
//
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <time.h>

//#define _DEBUG

// GPIO2 Base アドレス(NanoPi NEO3: GPIO2_Ax)
#define RK3328_GPIO2_BASE 0xFF230000
#define GPIO_MEM_SIZE     0x100
#define GPIO_PIN          2  // GPIO2_A2 = GPIO66
#define DATA_BITS         40

volatile uint32_t *gpio_base = NULL;

// GPIO レジスタオフセット
#define GPIO_SWPORTA_DR   0x00
#define GPIO_SWPORTA_DDR  0x04
#define GPIO_EXT_PORTA    0x50

void delay_us(unsigned int us) {
    struct timespec ts_start, ts_now;
    clock_gettime(CLOCK_MONOTONIC_RAW, &ts_start);
    do {
        clock_gettime(CLOCK_MONOTONIC_RAW, &ts_now);
    } while (((ts_now.tv_sec - ts_start.tv_sec) * 1000000 +
              (ts_now.tv_nsec - ts_start.tv_nsec) / 1000) < us);
}

void set_gpio_output() {
    gpio_base[GPIO_SWPORTA_DDR / 4] |= (1 << GPIO_PIN);
}

void set_gpio_input() {
    gpio_base[GPIO_SWPORTA_DDR / 4] &= ~(1 << GPIO_PIN);
}

void gpio_write(int val) {
    if (val)
        gpio_base[GPIO_SWPORTA_DR / 4] |= (1 << GPIO_PIN);
    else
        gpio_base[GPIO_SWPORTA_DR / 4] &= ~(1 << GPIO_PIN);
}

int gpio_read() {
    return (gpio_base[GPIO_EXT_PORTA / 4] & (1 << GPIO_PIN)) != 0;
}

int read_dht22_data(float *humidity, float *temperature) {
    uint8_t data[5] = {0};

    // スタート信号
    set_gpio_output();
    gpio_write(0);
    delay_us(10000); // 10ms
    gpio_write(1);
    delay_us(40);    // 40us
    set_gpio_input();

    // 応答 LOW
    int timeout = 0;
    while (gpio_read()) {
        delay_us(1);
       if (++timeout > 200) {
#ifdef _DEBUG        
        printf("Timeout waiting for LOW (start)\n");
#endif        
        return -1;
       }
    }

    // 応答 HIGH
    timeout = 0;
    while (!gpio_read()) {
        delay_us(1);
        if (++timeout > 200) {
#ifdef _DEBUG        
            printf("Timeout waiting for HIGH (response)\n");
#endif
            return -1;
        }
    }

    // データ開始 LOW
    timeout = 0;
    while (gpio_read()) {
        delay_us(1);
        if (++timeout > 200) {
#ifdef _DEBUG        
            printf("Timeout waiting for LOW (data start)\n");
#endif
            return -1;
        }
    }

    // 40ビット受信
    for (int i = 0; i < 40; i++) {
        timeout = 0;
        while (!gpio_read()) {
            delay_us(1);
            if (++timeout > 200) {
#ifdef _DEBUG        
                printf("Timeout waiting for HIGH at bit %d\n", i);
#endif
                return -1;
            }
        }

        delay_us(45); // パルス幅を判定("0" or "1")

        if (gpio_read()) {
            data[i / 8] |= (1 << (7 - (i % 8)));
        }

        timeout = 0;
        while (gpio_read()) {
            delay_us(1);
            if (++timeout > 200) break;
        }
    }

    // チェックサム確認
    uint8_t sum = data[0] + data[1] + data[2] + data[3];
    if (data[4] != sum) {
#ifdef _DEBUG        
        printf("Checksum error: got %d, expected %d\n", data[4], sum);
#endif
        return -1;
    }

    *humidity = ((data[0] << 8) | data[1]) / 10.0;
    *temperature = (((data[2] & 0x7F) << 8) | data[3]) / 10.0;
    if (data[2] & 0x80) *temperature *= -1;

    return 0;
}

void print_result(float humidity, float temperature) {
    time_t now = time(NULL);
    struct tm *t = localtime(&now);
    char buf[256];
    strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", t);
    printf("%s : Humidity: %.1f%%, Temp: %.1f°C\n", buf, humidity, temperature);
}

int check_sensor_presence() {
    // スタートシーケンスの直前にLOWに引く
    set_gpio_output();
    gpio_write(0);
    delay_us(1000);  // 1ms
    gpio_write(1);
    delay_us(40);
    set_gpio_input();

    // DHTがLOWで応答するか確認
    int timeout = 0;
    while (gpio_read()) {
        delay_us(1);
        if (++timeout > 300) {
            return 0;  // HIGHのまま → 応答なし(未接続の可能性)
        }
    }
    return 1;  // 応答あり
}

int main() {
    int fd = open("/dev/mem", O_RDWR | O_SYNC);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    gpio_base = (uint32_t *)mmap(NULL, GPIO_MEM_SIZE, PROT_READ | PROT_WRITE,
                                 MAP_SHARED, fd, RK3328_GPIO2_BASE);
    if (gpio_base == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

        // センサーの接続チェック
    if (!check_sensor_presence()) {
        fprintf(stderr, "DHTセンサーが接続されていないか、応答がありません。\n");
        munmap((void *)gpio_base, GPIO_MEM_SIZE);
        close(fd);
        return 1;
    }
    
    float humidity = 0, temperature = 0;
    int retries = 5;
    while (retries-- > 0) {
        if (read_dht22_data(&humidity, &temperature) == 0) {
            print_result(humidity, temperature);
            break;
        }
        sleep(1); // DHT22: 最低 1秒インターバル
    }

    munmap((void *)gpio_base, GPIO_MEM_SIZE);
    close(fd);
    return 0;
}

最終的には、crontabに登録して一定時間でログを出力するようにしてシステムに追加します。

*/5 * * * * /usr/bin/sudo /path/to/dht22_mmap >> /path/to/dht22.log 2>&1

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?