此处使用 Raspberry Pi 400 作测试,使用官方 Raspberry Pi OS演示,基本同于 Debian 系其它 Linux。
Linux 上的 GPIO 设备
通过在 Linux 的 /dev 目录查看本机外设,可以看到以下内容。
ls -l /dev/gpiochip*
crw-rw----+ 1 root gpio 254, 0 8月 12 14:15 /dev/gpiochip0
crw-rw----+ 1 root gpio 254, 1 8月 12 14:15 /dev/gpiochip1
lrwxrwxrwx 1 root root 9 8月 12 14:15 /dev/gpiochip4 -> gpiochip0
也可使用 gpiodetect 命令查看 GPIO 设备。
gpiochip0 [pinctrl-bcm2711] (58 lines)
gpiochip1 [raspberrypi-exp-gpio] (8 lines)
此命令显示了当前系统中所有的 GPIO 控制器及其对应的 GPIO 线数。
通过 gpioinfo 命令可以查看 GPIO 控制器的详细信息。
gpiochip0 - 58 lines:
line 0: "ID_SDA" input
line 1: "ID_SCL" input
line 2: "GPIO2" input
line 3: "GPIO3" input
line 4: "GPIO4" output drive=open-drain consumer="onewire@0"
line 5: "GPIO5" input
line 6: "GPIO6" input
line 7: "GPIO7" output active-low consumer="spi0 CS1"
line 8: "GPIO8" output active-low consumer="spi0 CS0"
line 9: "GPIO9" input
line 10: "GPIO10" input
line 11: "GPIO11" input
line 12: "GPIO12" input
line 13: "GPIO13" input
line 14: "GPIO14" input
line 15: "GPIO15" input
line 16: "GPIO16" input
line 17: "GPIO17" input
line 18: "GPIO18" input
line 19: "GPIO19" input
line 20: "GPIO20" input
line 21: "GPIO21" input
line 22: "GPIO22" input
line 23: "GPIO23" input
line 24: "GPIO24" input
line 25: "GPIO25" input
line 26: "GPIO26" input
line 27: "GPIO27" input
line 28: "RGMII_MDIO" input
line 29: "RGMIO_MDC" input
line 30: "CTS0" input
line 31: "RTS0" input
line 32: "TXD0" input
line 33: "RXD0" input
line 34: "SD1_CLK" input
line 35: "SD1_CMD" input
line 36: "SD1_DATA0" input
line 37: "SD1_DATA1" input
line 38: "SD1_DATA2" input
line 39: "SD1_DATA3" input
line 40: "PWM0_MISO" input
line 41: "PWM1_MOSI" input
line 42: "STATUS_LED_G_CLK" output consumer="PWR"
line 43: "SPIFLASH_CE_N" input
line 44: "SDA0" input
line 45: "SCL0" input
line 46: "RGMII_RXCLK" input
line 47: "RGMII_RXCTL" input
line 48: "RGMII_RXD0" input
line 49: "RGMII_RXD1" input
line 50: "RGMII_RXD2" input
line 51: "RGMII_RXD3" input
line 52: "RGMII_TXCLK" input
line 53: "RGMII_TXCTL" input
line 54: "RGMII_TXD0" input
line 55: "RGMII_TXD1" input
line 56: "RGMII_TXD2" input
line 57: "RGMII_TXD3" input
gpiochip1 - 8 lines:
line 0: "BT_ON" output consumer="shutdown"
line 1: "WL_ON" output
line 2: "PWR_LED_OFF" output
line 3: "GLOBAL_RESET" output
line 4: "VDD_SD_IO_SEL" output consumer="vdd-sd-io"
line 5: "GLOBAL_SHUTDOWN" output consumer="gpio-poweroff"
line 6: "SD_PWR_ON" output consumer="regulator-sd-vcc"
line 7: "SHUTDOWN_REQUEST" input
可见外设控制器处于 gpiochip0,gpiochip1 为树莓派主板上控制器。
Shell 控制 GPIO 输出
此处连接一个 LED 到 GPIO 24,通过 shell 控制其亮灭。
gpioset -c gpiochip0 24=1
此命令使线序 24 输出高电平,LED 亮。
gpioset -c gpiochip0 24=0
此命令使线序 24 输出低电平,LED 灭。
Rust 控制 GPIO 输出
引入库 gpio-cdev。
cargo add gpio-cdev
现在可以编写测试来控制 GPIO 输出。
简单写一个初始化函数,用来获取外设并把 GPIO 24 设置为输出。
fn led_init(gpio: u32) -> Result<LineHandle, Error> {
let mut chip = Chip::new(path: "/dev/gpiochip0")?;
let handle = chip
.get_line(offset: gpio)?
.request(LineRequestFlags::OUTPUT, 0, "led");
handle
}
然后分别为 LED 亮和灭写两个测试函数。
#[test]
fn turn_on_led() -> Result<(), Error> {
let handle = led_init(gpio: 24)?;
handle.set_value(1)?;
Ok(())
}
#[test]
fn turn_off_led() -> Result<(), Error> {
let handle = led_init(gpio: 24)?;
handle.set_value(0)?;
Ok(())
}
运行测试:
cargo test turn_on_led
此时能够看到 LED 亮起。
cargo test turn_off_led
此时能够看到 LED 熄灭。
完整代码如下:
use gpio_cdev::{Chip, Error, LineHandle, LineRequestFlags};
fn main() {
let _ = led_init(gpio: 24);
println!("Hello, world!");
}
fn led_init(gpio: u32) -> Result<LineHandle, Error> {
let mut chip = Chip::new(path: "/dev/gpiochip0")?;
let handle: Result<LineHandle, Error> = chip
.get_line(offset: gpio)?
.request(LineRequestFlags::OUTPUT, 0, "led");
handle
}
#[test]
fn turn_on_led() -> Result<(), Error> {
let handle = led_init(gpio: 24)?;
handle.set_value(1)?;
Ok(())
}
#[test]
fn turn_off_led() -> Result<(), Error> {有多少个文件被修改了?
let handle = led_init(gpio: 24)?;
handle.set_value(0)?;
Ok(())
}
读取 DHT11 温湿度传感器的 GPIO 控制方法
DHT11 温湿度传感器使用手册中有对裸传感器接线方式的阐述,此处使用模块。
由于 DHT11 传感器的通信协议比较特殊,直接使用 GPIO 读取数据需要严格按照时序进行操作,因此在 Linux 上直接使用 GPIO 控制 DHT11 传感器不现实。
可以使用内核驱动进行读取,此处不考虑这种方法。
尝试使用 Rust 进行读取
使用 GPIO25 作为数据线,使用 gpio-cdev 进行读取。
同样的,为这个 GPIO 写一个初始化函数。
fn dht11_init(gpio: u32) -> Result<LineHandle, Error> {
let mut chip = Chip::new(path: "/dev/gpiochip0")?;
{
let handle = chip
.get_line(offset: gpio)?
.request(LineRequestFlags::OUTPUT, 0, "dht11")?;
handle.set_value(0)?;
sleep(Duration::from_millis(18));
handle.set_value(1)?;
sleep(Duration::from_micros(40));
}
let handle: Result<LineHandle, Error> = chip
.get_line(offset: gpio)?
.request(LineRequestFlags::INPUT, 0, "dht11");
handle
}
此处初始化需要给 DHT11 发送一个低电平信号,持续 18ms,然后拉高 40us,之后再将 GPIO 设置为输入模式,等待 DHT11 的响应。
注意:此处
handle必须手动加入作用域,否则会因为未释放而导致 GPIO 重用产生EBUSY错误。Error: Error { kind: Ioctl { kind: LineHandle, cause: EBUSY } }
现在写读取数据的函数。
#[test]
fn read_dht11_data() -> Result<(), Error> {
let handle = dht11_init(gpio: 25)?;
let mut data: Vec<u8> = vec![];
let mut timings: Vec<u64> = vec![];
let start = Instant::now();
while start.elapsed() < Duration::from_millis(10) {
let val = handle.get_value()?;
timings.push(start.elapsed().as_micros() as u64);
data.push(val as u8);
}
println!("Sample len: {}", data.len());
println!("Top 100 samples: {:?}", &data[..100.min(data.len())]);
Ok(())
}
查看输出:
Sample len: 9296
Top 100 samples: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
test read_dht11_data ... ok
由于读取时需要确认高低电平的持续时间,我们需要一个 wait_while() 函数来等待高低电平的变化。
fn wait_while(
handle: &LineHandle,
value: u8,
timeout: Duration,
) -> Result<(), Box<dyn std::error::Error>> {
let start = Instant::now();
while handle.get_value()? == value {
if start.elapsed() > timeout {
return Err("dht11: timeout waiting for line".into());
}
}
Ok(())
}
写 read_dht11() 函数,把初始化去掉,直接写成。
fn read_dht11(gpio: u32) -> Result<(f32, f32), Box<dyn std::error::Error>> {
let mut chip = Chip::new(path: "/dev/gpiochip0")?;
// 初始化 DHT11,给定长度的低电平信号和高电平信号激活
{
let out = chip
.get_line(gpio)?
.request(LineRequestFlags::OUTPUT, 0, "dht11")?;
out.set_value(0)?;
sleep(Duration::from_millis(20));
out.set_value(1)?;
sleep(Duration::from_micros(30));
}
// 请求 GPIO 为输入模式,等待 DHT11 响应
let input: LineHandle = chip
.get_line(gpio)?
.request(LineRequestFlags::INPUT, 0, "dht11")?;
// 起始信号,DHT11 响应
wait_while(&input, 1, Duration::from_millis(100))?;
wait_while(&input, 0, Duration::from_millis(100))?;
wait_while(&input, 1, Duration::from_millis(100))?;
// 读取 40 位数据
let mut bits = [0u8; 40];
for bit in bits.iter_mut() {
wait_while(&input, 0, Duration::from_millis(100))?;
let start = Instant::now();
wait_while(&input, 1, Duration::from_millis(100))?;
*bit = if start.elapsed().as_micros() > 40 { 1 } else { 0 };
}
// 读取到的数据为 5 个字节,前 4 个字节为数据,第 5 个字节为校验和
let mut bytes = [0u8; 5];
for (i, &b) in bits.iter().enumerate() {
// 解析方法:`i / 8 << 1 | b`
bytes[i / 8] = (bytes[i / 8] << 1) | b;
}
// 计算校验和
let sum = bytes[..4].iter().fold(init: 0u8, f: |acc, &b| acc.wrapping_add(b));
// 校验和不匹配则返回错误
if sum != bytes[4] {
return Err(format!(
"dht11: checksum mismatch (calc {sum:#04x}, got {:#04x})",
bytes[4]
) String
.into());
}
let humidity = bytes[0] as f32 + bytes[1] as f32 / 10.0;
let temperature = bytes[2] as f32 + bytes[3] as f32 / 10.0;
Ok((humidity, temperature))
}
这个函数接收一个 LineHandle,一个值 value,以及一个超时时间 timeout。它会持续检查 GPIO 的值是否等于 value,如果超过 timeout 时间仍然没有变化,则返回一个错误。
其它外设的控制方法
gpio-cdev 只提供基础 GPIO 外设的控制,想要解析 DHT11 传感器的协议,需要自己实现时序控制和数据解析。对于其它外设,如 I2C、SPI 等,建议使用专门的库来进行控制。
具体参考crates.io 上的 gpio-cdev 文档。
由于本文没有什么含金量,也几乎没有实际使用价值,后面考虑写一篇关于 rpi 上的 HAL 库博客。
