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
悪戦苦闘
この接続の場合、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のみ確認するよう提案があった。
プログラムは、以下のとおり。
#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
だた、何度も実行するとエラー、成功と安定しない。最終的には、リトライを入れて対応
以下が、コードです。
#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
裏にヒートシンク