犀牛派 X1 GPIO 入门实战 从 C++ 点亮 LED 到 ROS 2 节点

Ambrose42 2026-09-20 23:42:41

犀牛派 X1 GPIO 入门实战 01
从 C++ 点亮 LED 到 ROS 2 节点

拿到犀牛派后,很多人会先尝试视觉识别、定位或导航。这次我们先从一个更小的实验开始:用 GPIO 点亮一颗 LED,再把控制逻辑放进 ROS 2 节点。

这篇文章以犀牛派 X1 为例,使用 Ubuntu 22.04、ROS 2 Humble 和 libgpiod 1.x。我们会依次完成接线、命令行验证、C++ 控制和 ROS 2 定时闪烁。学完后,你应该能分清物理针脚编号与 GPIO line offset,也能在自己的节点中完成 GPIO 的申请、写入和释放。

 

图 1 犀牛派 X1 开发板。

1 实验环境与材料

软件环境

项目

本文环境

开发板

犀牛派 X1

操作系统

Ubuntu 22.04

ROS 2

Humble

GPIO 库

libgpiod 1.x C API

编译工具

g++、CMake、colcon

编辑器

VS Code,可换成自己习惯的编辑器

 

本文默认已经安装 ROS 2 Humble。GPIO 部分先独立运行,后面再接入 ROS 2。

Linux 用户态程序适合本例的低频 LED 控制,但普通 sleep 和 ROS 2 定时器都不提供硬实时保证。对于高频 PWM、精确时序等需求,需要结合硬件外设或 MCU 设计,不能把本例的控制方式直接套过去。

硬件材料

  • 犀牛派 X1 一块,以及匹配的电源。
  • 普通 LED 一颗。
  • 限流电阻一个。
  • 面包板一块、杜邦线若干。

裸 LED 必须串联限流电阻。 电阻值需要结合 LED 的正向压降、目标电流和板卡允许的 GPIO 电流选择。如果使用自带限流电阻的 LED 模块,则按模块说明接线。

可以先用下面的关系估算电阻:

R ≈ (供电电压 − LED 正向压降) / LED 电流

例如,仅作计算演示:假设供电为 3.3 V、LED 正向压降为 2.0 V、目标电流为 1 mA,则计算得到约 1.3 kΩ。这个例子不代表 X1 的 GPIO 额定电流;实际选择还要核对官方电气规格和 GPIO 带载时的输出电压。

2 确认引脚并完成接线

分清三种编号

本例涉及三种容易混淆的编号:

名称

含义

本文示例

物理针脚编号

排针上实际的位置

PIN7

板卡功能名称

官方引脚图中的标注

GPIO_00

GPIO line offset

某个 gpiochip 内的偏移编号

gpiochip0 中的 0

 

 

图 2 X1 部分排针定义。接线前请同时核对官方完整引脚图和板上针脚方向。

按照图中的排针定义,本文用到 PIN1 的 3.3V OUT、PIN6 的 GND,以及 PIN7 的 GPIO_00。程序沿用本次实验中的 /dev/gpiochip0、offset 0

物理 PIN7 并不意味着程序里要填 offset 7。 不同硬件版本或系统镜像的映射可能不同;运行前应结合对应版本的官方资料,确认控制器与 line offset 确实对应目标排针。后面的 gpioinfo 能显示系统识别的 line 和占用情况,但不能单独证明物理接线位置。

先检查 LED 再接 GPIO

接线或改线前先断电。LED 正极通常为长脚,负极通常为短脚;引脚剪短或器件不同的情况下,以器件标记或数据手册为准。

可以先用 3.3 V 电源检查 LED 的方向和连接:

 

图 3 使用 3.3 V 电源检查 LED 方向和连接。

确认 LED 能亮后,再断电改为 GPIO 推挽输出接法:

 

图 4 GPIO 推挽输出接线,写入 1 点亮,写入 0 熄灭。

在后面的普通输出示例中,GPIO 写入 1 时点亮 LED,写入 0 时熄灭 LED。不要把旁边的 5 V 电源针脚接入 GPIO。

3 安装依赖并验证 GPIO

进入开发板的终端,安装编译工具和 GPIO 库:

sudo apt update

sudo apt install build-essential cmake pkg-config gpiod libgpiod-dev

确认命令行工具和开发库的版本:

gpioset --version

pkg-config --modversion libgpiod

本文后续命令和 C++ 代码使用 libgpiod 1.x。 如果查到的是 2.x,不要直接照搬:2.x 的命令行参数和 C API 都有变化。另一个容易混淆的地方是,Ubuntu 的 libgpiod2 软件包名中的 2 是 ABI 标识,并不表示它提供 libgpiod 2.x API。

查看 GPIO 控制器和 line:

gpiodetect

gpioinfo gpiochip0

如果提示没有权限,可以使用 sudo 重试;如果系统中的控制器名称不同,则先确认映射,再替换命令中的 gpiochip0。还要检查目标 line 是否已被其他程序或驱动占用。

确认接线和映射后,先从命令行点亮 LED:

# libgpiod 1.x:保持申请,直到收到退出信号

sudo gpioset --mode=signal gpiochip0 0=1

命令保持运行时,程序会持续占用这条 line。按 Ctrl+C 结束后,可以再执行以下命令验证低电平:

sudo gpioset --mode=signal gpiochip0 0=0

验证完成后再次按 Ctrl+C,释放 line,再运行下一节的 C++ 程序。

这里的 --mode=signal 很关键:libgpiod 1.x 的 gpioset 默认设置后就退出,退出后会释放 GPIO 请求,释放后的电平不保证保持。因此,不能把一条瞬间退出的 gpioset 命令当作永久开关。如果程序提示 Device or resource busy,先检查是否还有 gpioset 在运行。

4 用 C++ 点亮 LED

完整代码

在自己的目录中新建 led_on.cpp,写入以下代码。程序申请 GPIO 后点亮 LED,等待约 5 秒,再写入低电平并释放资源。

#include <gpiod.h>

 

#include <chrono>

#include <csignal>

#include <cstdio>

#include <thread>

 

namespace {

volatile std::sig_atomic_t stop_requested = 0;

 

void handle_signal(int)

{

    stop_requested = 1;

}

}  // namespace

 

int main()

{

    // 示例采用 libgpiod 1.x;运行前确认芯片、line offset 和实际接线。

    const char* chip_path = "/dev/gpiochip0";

    const unsigned int line_offset = 0;

    const int kLedOn = 1;

    const int kLedOff = 0;

 

    if (std::signal(SIGINT, handle_signal) == SIG_ERR ||

        std::signal(SIGTERM, handle_signal) == SIG_ERR) {

        std::perror("signal");

        return 1;

    }

 

    gpiod_chip* chip = gpiod_chip_open(chip_path);

    if (chip == nullptr) {

        std::perror("gpiod_chip_open");

        return 1;

    }

 

    gpiod_line* line = gpiod_chip_get_line(chip, line_offset);

    if (line == nullptr) {

        std::perror("gpiod_chip_get_line");

        gpiod_chip_close(chip);

        return 1;

    }

 

    // 默认是推挽输出;申请时先输出低电平。

    if (gpiod_line_request_output(line, "x1-led-demo", kLedOff) < 0) {

        std::perror("gpiod_line_request_output");

        gpiod_chip_close(chip);

        return 1;

    }

 

    int exit_code = 0;

    if (!stop_requested) {

        if (gpiod_line_set_value(line, kLedOn) < 0) {

            std::perror("gpiod_line_set_value(on)");

            exit_code = 1;

        } else {

            const auto deadline = std::chrono::steady_clock::now() +

                                  std::chrono::seconds(5);

            while (!stop_requested &&

                   std::chrono::steady_clock::now() < deadline) {

                std::this_thread::sleep_for(std::chrono::milliseconds(100));

            }

        }

    }

 

    // 信号处理函数不操作 GPIO;由主线程在退出前尽力熄灯。

    if (gpiod_line_set_value(line, kLedOff) < 0) {

        std::perror("gpiod_line_set_value(off)");

        exit_code = 1;

    }

    gpiod_line_release(line);

    gpiod_chip_close(chip);

    return exit_code;

}

这段代码的关键步骤如下:

  1. gpiod_chip_open() 打开 GPIO 控制器。
  2. gpiod_chip_get_line() 获取指定 offset 的 line。
  3. gpiod_line_request_output() 申请输出,并指定初始电平。
  4. gpiod_line_set_value() 改变输出值。
  5. 结束前尝试写入低电平,再释放 line、关闭控制器。

这里通过 gpiod.h 在 C++ 中调用 libgpiod 的 C API,后续 ROS 2 节点也会使用相同的接口。

编译和运行

led_on.cpp 所在目录执行:

g++ -std=c++14 -Wall -Wextra -pthread led_on.cpp -o led_on \

  $(pkg-config --cflags --libs libgpiod)

sudo ./led_on

pkg-config 会给出 libgpiod 所需的编译与链接选项。仅包含 gpiod.h 还不够;如果忘记链接库,通常会在链接阶段看到 undefined reference to gpiod_*

预期现象是 LED 点亮约 5 秒,随后程序在释放 GPIO 前写入低电平。

 

图 5 实验中的 LED 点亮效果。完整接线以本节文字说明为准,裸 LED 需要串联限流电阻。

程序正常退出时会尽力把 LED 置于熄灭状态,但这不等于 GPIO 释放后仍能保证电平。强制终止、断电等情况也不能依赖软件清理;如果项目要求确定的默认状态,需要配合合适的硬件电路。

VS Code 的可选配置

如果你使用 VS Code,可以安装 Microsoft 的 C/C++ 扩展。Code Runner 只是可选的运行工具;建议先在终端完成一次编译和运行,方便区分代码、链接和权限问题。

想通过 Code Runner 一键运行时,可以在工作目录的 .vscode/settings.json 中加入以下配置。如果文件已有配置,只合并对应字段,不要覆盖其他设置。

{

  "code-runner.executorMap": {

    "cpp": "cd \"$dir\" && g++ -std=c++14 -Wall -Wextra -pthread \"$fileName\" -o \"$fileNameWithoutExt\" -lgpiod && sudo \"./$fileNameWithoutExt\""

  },

  "code-runner.runInTerminal": true

}

其中 -lgpiod 用于链接 GPIO 库,-pthread 配合示例中的线程等待,runInTerminal 让程序在集成终端中运行,便于查看输出和输入 sudo 密码。这里使用 sudo 是为了完成本地 GPIO 实验;长期使用时,应按系统的设备权限机制给相应用户配置访问权限。

5 理解开漏输出

普通推挽输出可以主动输出高、低电平。开漏输出则只主动拉低;写入逻辑 1 时,GPIO 停止主动驱动,呈高阻态。以下说明均以没有设置 ACTIVE_LOW 为前提。

如果要尝试开漏输出,先断电,将接线改为:

 

图 6 GPIO 开漏输出接线,写入 0 点亮,写入 1 时释放输出。

此时 GPIO 拉低后,电流从 3.3 V 电源经过电阻和 LED 流入 GPIO,LED 点亮。GPIO 释放为高阻态时,LED 熄灭。

申请开漏输出的关键调用是:

// line 已经通过 gpiod_chip_get_line() 获得。

// 初始写入 1,表示释放输出,LED 保持熄灭。

int ret = gpiod_line_request_output_flags(

    line, "led_open_drain",

    GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN, 1);

 

// 必须检查 ret;申请失败时不能继续读写这条 line。

这是一段接口说明,不能只替换申请语句就直接运行原来的推挽示例。接线、点亮值和退出时的熄灭值都需要同步修改:

操作

推挽接法

本节开漏接法

初始熄灭值

0

1

点亮 LED

写入 1

写入 0

熄灭 LED

写入 0

写入 1

 

如果开漏申请失败,应检查平台支持情况和错误信息,不能直接把普通输出当作等价替代。

开漏模式不保证 LED 更亮,也不会自动提供上拉。 LED 亮度取决于实际电流,不能只凭测得约 3.3 V 就确定亮度差异的原因;需要结合电阻、LED 特性和带载电压判断。即使使用开漏接法,电流仍经过 GPIO,仍受允许灌电流的限制。需要驱动更大负载时,应使用合适的三极管、MOSFET 或专用驱动器。

6 把 GPIO 控制放进 ROS 2 节点

下面回到第 2 节的推挽接法PIN7 → 限流电阻 → LED → PIN6。如果刚做过开漏实验,请先断电改线。

ROS 2 示例每隔 1 秒翻转一次输出,LED 亮 1 秒、灭 1 秒,完整周期约为 2 秒。GPIO 初始化只在节点启动时执行一次,定时器回调负责更新输出。

创建工作空间和功能包

确认已安装 ROS 2 Humble 后,在普通用户终端中执行:

source /opt/ros/humble/setup.bash

sudo apt install python3-colcon-common-extensions

 

mkdir -p ~/gpio_ws/src

cd ~/gpio_ws/src

ros2 pkg create gpio_led --build-type ament_cmake --dependencies rclcpp

接下来新建 gpio_led/src/gpio_led_node.cpp,并将包内的 CMakeLists.txt 和 package.xml 替换为下文内容。最终目录结构如下:

gpio_ws/

└── src/

    └── gpio_led/

        ├── CMakeLists.txt

        ├── package.xml

        └── src/

            └── gpio_led_node.cpp

节点代码

src/gpio_led_node.cpp

#include <gpiod.h>

#include <rclcpp/rclcpp.hpp>

 

#include <cerrno>

#include <chrono>

#include <cstdint>

#include <cstdio>

#include <cstring>

#include <limits>

#include <memory>

#include <stdexcept>

#include <string>

#include <system_error>

 

class GpioLedNode : public rclcpp::Node

{

public:

    GpioLedNode() : Node("gpio_led_node")

    {

        // 构造失败时不会调用本类析构函数,必须在 catch 中释放资源。

        try {

            const auto chip_path =

                declare_parameter<std::string>("chip_path", "/dev/gpiochip0");

            const auto line_offset =

                declare_parameter<std::int64_t>("line_offset", 0);

            if (line_offset < 0 ||

                static_cast<std::uint64_t>(line_offset) >

                    std::numeric_limits<unsigned int>::max()) {

                throw std::invalid_argument("line_offset 超出 unsigned int 范围");

            }

 

            chip_ = gpiod_chip_open(chip_path.c_str());

            if (chip_ == nullptr) {

                throw std::system_error(errno, std::generic_category(),

                                        "gpiod_chip_open");

            }

 

            line_ = gpiod_chip_get_line(

                chip_, static_cast<unsigned int>(line_offset));

            if (line_ == nullptr) {

                throw std::system_error(errno, std::generic_category(),

                                        "gpiod_chip_get_line");

            }

 

            // 默认推挽输出,初始低电平;适用于高电平点亮的接线。

            if (gpiod_line_request_output(line_, "ros2-gpio-led", 0) < 0) {

                throw std::system_error(errno, std::generic_category(),

                                        "gpiod_line_request_output");

            }

            line_requested_ = true;

 

            timer_ = create_wall_timer(std::chrono::seconds(1), [this]() {

                const int next_value = current_value_ == 0 ? 1 : 0;

                if (gpiod_line_set_value(line_, next_value) < 0) {

                    const int saved_errno = errno;

                    timer_->cancel();

                    RCLCPP_ERROR(get_logger(),

                                 "GPIO 写入失败,已停止定时器:%s",

                                 std::strerror(saved_errno));

                    return;

                }

                current_value_ = next_value;

                RCLCPP_INFO(get_logger(), "GPIO 电平:%d", current_value_);

            });

 

            RCLCPP_INFO(get_logger(), "已申请 %s,line offset=%lld",

                        chip_path.c_str(), static_cast<long long>(line_offset));

        } catch (...) {

            release_gpio();

            throw;

        }

    }

 

    ~GpioLedNode() override

    {

        // main 使用单线程 spin,退出 spin 后才销毁节点。

        timer_.reset();

        release_gpio();

    }

 

private:

    void release_gpio() noexcept

    {

        if (line_requested_) {

            if (gpiod_line_set_value(line_, 0) < 0) {

                // 此时 ROS 上下文可能已关闭,因此直接输出到标准错误。

                std::perror("退出时设置 GPIO 低电平失败");

            }

            gpiod_line_release(line_);

            line_requested_ = false;

        }

        line_ = nullptr;

        if (chip_ != nullptr) {

            gpiod_chip_close(chip_);

            chip_ = nullptr;

        }

    }

 

    gpiod_chip* chip_ = nullptr;

    gpiod_line* line_ = nullptr;

    bool line_requested_ = false;

    int current_value_ = 0;

    rclcpp::TimerBase::SharedPtr timer_;

};

 

int main(int argc, char* argv[])

{

    try {

        rclcpp::init(argc, argv);

        {

            auto node = std::make_shared<GpioLedNode>();

            rclcpp::spin(node);

        }

        rclcpp::shutdown();

        return 0;

    } catch (const std::exception& error) {

        std::fprintf(stderr, "gpio_led_node:%s\n", error.what());

        if (rclcpp::ok()) {

            rclcpp::shutdown();

        }

        return 1;

    }

}

与前面的独立程序相比,这里把 GPIO 资源放进节点对象中管理,用 create_wall_timer() 代替主线程中的等待。节点还暴露了 chip_path 和 line_offset 参数,便于在确认映射后调整目标 GPIO。

GPIO 写入失败时,节点会记录错误并停止定时器,不再继续翻转输出。此时 LED 可能保留此前状态,节点也不会自动退出;请查看日志,按 Ctrl+C 停止节点后排查。

配置编译和安装规则

CMakeLists.txt

cmake_minimum_required(VERSION 3.8)

project(gpio_led)

 

set(CMAKE_CXX_STANDARD 14)

set(CMAKE_CXX_STANDARD_REQUIRED ON)

set(CMAKE_CXX_EXTENSIONS OFF)

 

find_package(ament_cmake REQUIRED)

find_package(rclcpp REQUIRED)

find_package(PkgConfig REQUIRED)

pkg_check_modules(GPIOD REQUIRED IMPORTED_TARGET libgpiod)

 

if(NOT GPIOD_VERSION VERSION_LESS "2.0")

  message(FATAL_ERROR

    "This example uses the libgpiod 1.x C API; found ${GPIOD_VERSION}.")

endif()

 

add_executable(gpio_led_node src/gpio_led_node.cpp)

ament_target_dependencies(gpio_led_node rclcpp)

target_link_libraries(gpio_led_node PkgConfig::GPIOD)

 

if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")

  target_compile_options(gpio_led_node PRIVATE -Wall -Wextra -Wpedantic)

endif()

 

install(TARGETS gpio_led_node

  DESTINATION lib/${PROJECT_NAME})

 

ament_package()

这里既要声明 rclcpp 依赖,也要链接 libgpiod。install(TARGETS ...) 则让编译后的可执行文件安装到 ROS 2 能找到的位置,否则可能出现包已构建、ros2 run 却找不到程序的情况。

声明功能包依赖

package.xml

<?xml version="1.0"?>

<package format="3">

  <name>gpio_led</name>

  <version>0.0.1</version>

  <description>犀牛派 X1 GPIO LED 入门示例,使用 libgpiod 1.x 和 ROS 2。</description>

  <maintainer email="your-name@example.com">Your Name</maintainer>

  <!-- 发布代码前按实际情况填写维护者信息和许可证。 -->

  <license>TODO</license>

 

  <buildtool_depend>ament_cmake</buildtool_depend>

  <build_depend>pkg-config</build_depend>

  <depend>rclcpp</depend>

  <depend>libgpiod</depend>

 

  <export>

    <build_type>ament_cmake</build_type>

  </export>

</package>

其中维护者和许可证是示例包的元数据,整理成自己的项目时按实际情况修改。

构建和运行

回到工作空间根目录,以普通用户身份构建:

cd ~/gpio_ws

source /opt/ros/humble/setup.bash

colcon build --packages-select gpio_led

source install/setup.bash

 

ros2 run gpio_led gpio_led_node

如果当前用户没有 /dev/gpiochip0 的访问权限,会出现 Permission denied。本地验证时,可以启动一个临时 root shell,并在这个 shell 中重新加载 ROS 2 和工作空间环境:

# 在普通用户终端中,先进入刚刚构建的工作空间

cd ~/gpio_ws

sudo bash

 

# 以下命令在上面打开的 root shell 中执行

source /opt/ros/humble/setup.bash

source ./install/setup.bash

ros2 run gpio_led gpio_led_node

验证完成后按 Ctrl+C 停止节点,再输入 exit 退出 root shell。编译仍使用普通用户完成。正式使用时,建议按照板卡和系统的权限管理方式配置 GPIO 访问权限,不要长期依赖 root 运行节点。

需要调整 GPIO 参数时,在具备设备访问权限并加载了环境的终端中执行:

ros2 run gpio_led gpio_led_node --ros-args \

  -p chip_path:=/dev/gpiochip0 \

  -p line_offset:=0

这些参数只指定软件访问位置,不能代替物理引脚映射核对。

7 常见问题排查

现象或报错

优先检查

找不到 gpiod.h

是否安装 libgpiod-dev;开发库是否为 1.x

undefined reference to gpiod_*

编译命令或 CMake 是否链接 libgpiod

Permission denied

当前用户是否有 /dev/gpiochip* 访问权限

Device or resource busy

是否有 gpioset、其他进程或内核驱动占用目标 line

No such file or directory

控制器路径是否存在,是否误用了其他镜像的编号

命令执行后 LED 一闪或状态不稳定

libgpiod 1.x 的 gpioset 是否指定了保持模式

代码能运行,但 LED 不亮

LED 极性、限流电阻、面包板连接、共地及物理针脚映射

ROS 2 找不到包或可执行文件

是否加载 install/setup.bash,CMake 是否有安装规则

ROS 2 启动后不再闪烁

查看 GPIO 初始化或写入错误日志,确认定时器是否因错误停止

 

排查时建议沿着“接线和限流电阻 → GPIO 映射及权限 → 命令行 → C++ → ROS 2”的顺序逐层确认。前一层能独立工作,再往下一层走,问题通常更容易定位。

这次实验先把 GPIO 控制的基本流程跑通。后续需要控制其他外设时,可以继续沿用资源管理和错误处理的思路,再根据设备的电气要求选择合适的驱动电路。

参考资料

感谢阿加犀官方社区的 GPIO 实操帖子,它是这次实验的参考起点:

GPIO 的电气规格和排针映射,请以所用 X1 硬件版本及系统镜像对应的官方资料为准。

...全文
71 回复 打赏 收藏 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复
下载代码方式:https://pan.quark.cn/s/c66ecb4d06ce 同源策略:从安全角度出发,浏览器会对脚本发起的跨站请求施加限制,要求JavaScript或Cookie仅能获取同源(即协议、域名和端口完全一致)下的资源。正因如此,不同项目间的调用会受到浏览器的阻碍。以常见情境为例:WebApi作为数据服务层,它是一个独立的项目,而MVC项目则承担Web的展示功能,此时MVC项目需要调用WebApi中的接口以获取数据并在页面上呈现。由于WebApi与MVC属于两个独立的项目,运行后便会产生前面提及的跨域问题。WebApi的跨域问题主要源于浏览器的同源策略,这是一种安全措施,旨在限制JavaScript或Cookie仅能访问同一源(包括协议、域名和端口)下的内容。在实际开发过程中,当WebApi作为一个独立服务,例如数据服务层,而MVC项目作为前端展示层时,两者运行在不同的项目和端口下,浏览器将阻止MVC对WebApi的跨域请求,从而影响数据的正常获取。为了应对这一问题,我们可以采用CORS(跨域资源共享)机制。CORS通过在HTTP请求与响应头中嵌入特定标识,向浏览器明确哪些跨域请求是被允许的。例如,服务器可以在响应头中添加`Access-Control-Allow-Origin:http://localhost:8081`,表示允许来自http://localhost:8081的请求访问资源。解决WebApi跨域问题的具体实施步骤如下: 1. 构建一个包含MVC项目(Web)与Web API项目(WebApiCORS)的解决方案。 2. 在MVC项目中,例如Home控制器的Index视图,通过Ajax向WebApiCORS发起跨域请求。 3...

7,696

社区成员

发帖
与我相关
我的任务
社区描述
本论坛以AI、IoT、PC 、XR、Auto等核心板块组成,为开发者提供便捷及高效的学习和交流平台。 高通开发者专区主页:https://qualcomm.csdn.net/
物联网人工智能开源 企业社区 北京·东城区
社区管理员
  • csdnsqst0050
  • chipseeker
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧