[RA4 & RA6] 【瑞萨RA × Zephyr评测】6、基于Zephyr的GPIO直流电机遥控小车

[复制链接]
4208|6
sujingliang 发表于 2026-4-23 15:14 | 显示全部楼层 |阅读模式
[i=s] 本帖最后由 sujingliang 于 2026-4-23 19:26 编辑 [/i]

一、项目背景

因为参加本次fpb_ra6e2评测,最近一直在研究Zephyr RTOS,加上之前用蓝牙、2.4G、WIFI、RX2遥控驱动小车,正好手头有自制小车底盘。就用这些实现一个Zephyr+fpb_ra6e2+红外+GPIO控制电机的遥控小车吧。TC1508A双路电机驱动4轮,遥控部分用了VS1838B红外接收头,配合普通红外遥控器控制。

二、硬件方案

主控:fpb_ra6e2
电机驱动:TC1508A × 2(每片控制2路电机)
电机:4个直流减速电机
遥控接收:VS1838B红外接收头

引脚分配

  • 第一片TC1508A:P100,P101,P111,P110
  • 第二片TC1508A:P109, P301, P403, P006
  • 红外接收:P304(带内部上拉)

tc1508a.png

红外接收.jpg

7.jpg

6.jpg

三、软件架构

3.1 Devicetree配置

用Zephyr标准的Devicetree描述硬件,关键部分如下:

tc1508a_1: tc1508a_1 {
    compatible = "gpio-motor";
    in1-gpios = <&ioport1 0 GPIO_ACTIVE_HIGH>;
    in2-gpios = <&ioport1 1 GPIO_ACTIVE_HIGH>;
    in3-gpios = <&ioport1 11 GPIO_ACTIVE_HIGH>;
    in4-gpios = <&ioport1 10 GPIO_ACTIVE_HIGH>;
};

ir_receiver {
    compatible = "vishay,vs1838b";
    data-gpios = <&ioport3 4 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
};

3.2 驱动层

  • GPIO驱动:标准Zephyr GPIO API
  • 红外驱动:使用 INPUT_VS1838B子系统,通过回调处理按键

3.3 应用层逻辑

电机控制抽象

  • 每个电机两个GPIO:IN1/IN2控制正反转
  • 实现了 motor_forward()motor_reverse()motor_stop()

运动控制

  • 4个电机独立控制,实现前进、后退、左转、右转、原地转向
  • 左转时左侧轮反转、右侧轮正转

红外遥控映射

  • 0x07:前进
  • 0x13:后退
  • 0x03:停止
  • 0x50:左转
  • 0x0F:右转
  • 0x0B:回正方向

四、核心代码解析

4.1 电机驱动封装

void motor_forward(uint8_t wheel) {
    gpio_pin_set_dt(motors[wheel].in1, 1);
    gpio_pin_set_dt(motors[wheel].in2, 0);
}

void car_forward(void)
{
    if(car_direction==MIDDLE)
    {
        motor_forward(LEFTFRONT);
        motor_forward(LEFTREAR);
        motor_forward(RIGHTFRONT);
        motor_forward(RIGHTREAR);
    }else if(car_direction==LEFT)
    {
        motor_reverse(LEFTFRONT);
        motor_reverse(LEFTREAR);
        motor_forward(RIGHTFRONT);
        motor_forward(RIGHTREAR);
    }else if(car_direction==RIGHT)
    {
        motor_forward(LEFTFRONT);
        motor_forward(LEFTREAR);
        motor_reverse(RIGHTFRONT);
        motor_reverse(RIGHTREAR);
    }
}

4.2 红外回调处理

static void ir_input_callback(struct input_event *evt, void *user_data) {
    if (evt->type == INPUT_EV_DEVICE && evt->code == INPUT_MSC_SCAN) {
        uint16_t value = (uint16_t)evt->value;
        uint8_t command = value & 0xFF;
        // 根据command执行对应动作
    }
}

4.3 主程序流程

  1. 初始化GPIO,配置为输出
  2. 初始化红外设备
  3. 注册红外回调
  4. 进入无限循环,等待遥控指令

五、调试时注意事项

1. GPIO配置问题
部分IO口默认有其他功能,需要先在Devicetree中enable对应的ioport节点。

2. 红外驱动
Zephyr的VS1838B驱动通过Input子系统上报事件,需要用 INPUT_CALLBACK_DEFINE宏注册回调,不是直接函数调用。

六、效果演示

整体代码量不大,但完整实现了Zephyr设备树配置→驱动→应用的全流程。

IMG_20260423_184447.jpg

8.jpg

七、全部代码

  • Devicetree overlay文件(fpb_ra6e2.overlay)

    
    &ioport0 { status = "okay";};
    &ioport1 { status = "okay";};
    &ioport3 { status = "okay";};
    &ioport4 { status = "okay";};
    
    
    / {
        tc1508a_1: tc1508a_1 {
            compatible = "gpio-motor";
            in1-gpios = <&ioport1 0 GPIO_ACTIVE_HIGH>;
            in2-gpios = <&ioport1 1 GPIO_ACTIVE_HIGH>;
            in3-gpios = <&ioport1 11 GPIO_ACTIVE_HIGH>;
            in4-gpios = <&ioport1 10 GPIO_ACTIVE_HIGH>;
            status="okay";
        };
    
        tc1508a_2: tc1508a_2 {
            compatible = "gpio-motor";
            in1-gpios = <&ioport1 9 GPIO_ACTIVE_HIGH>;
            in2-gpios = <&ioport3 1 GPIO_ACTIVE_HIGH>;
            in3-gpios = <&ioport4 3 GPIO_ACTIVE_HIGH>;
            in4-gpios = <&ioport0 6 GPIO_ACTIVE_HIGH>;
            status="okay";
        };
    
        ir_receiver {
            compatible = "vishay,vs1838b";
            data-gpios = <&ioport3 4  (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
        };
    
    };
    
    
  • prj.conf配置

    # 开启你需要的硬件驱动
    CONFIG_GPIO=y
    
    CONFIG_STDOUT_CONSOLE=y
    CONFIG_PRINTK=y
    CONFIG_LOG=y
    CONFIG_CONSOLE=y
    
    # 启用VS1838B驱动
    CONFIG_INPUT=y
    CONFIG_INPUT_VS1838B=y
    
  • main.c主程序

    #include <zephyr/kernel.h>
    #include <zephyr/device.h>
    #include <zephyr/drivers/gpio.h>
    #include <zephyr/input/input.h>
    #include <zephyr/logging/log.h>
    
    LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
    
    
    #define TC1508A_1_NODE DT_NODELABEL(tc1508a_1)
    #define TC1508A_2_NODE DT_NODELABEL(tc1508a_2)
    
    enum wheel{
        LEFTFRONT,
        LEFTREAR,
        RIGHTFRONT,
        RIGHTREAR
    };
    
    enum direction{
        MIDDLE,
        LEFT,
        RIGHT
    };
    
    enum carstatus{
        STOP,
        FORWORD,
        BACKWORD
    };
    
    static const struct gpio_dt_spec motor1_in1 = GPIO_DT_SPEC_GET(TC1508A_1_NODE, in1_gpios);
    static const struct gpio_dt_spec motor1_in2 = GPIO_DT_SPEC_GET(TC1508A_1_NODE, in2_gpios);
    static const struct gpio_dt_spec motor2_in1 = GPIO_DT_SPEC_GET(TC1508A_1_NODE, in3_gpios);
    static const struct gpio_dt_spec motor2_in2 = GPIO_DT_SPEC_GET(TC1508A_1_NODE, in4_gpios);
    
    static const struct gpio_dt_spec motor3_in1 = GPIO_DT_SPEC_GET(TC1508A_2_NODE, in1_gpios);
    static const struct gpio_dt_spec motor3_in2 = GPIO_DT_SPEC_GET(TC1508A_2_NODE, in2_gpios);
    static const struct gpio_dt_spec motor4_in1 = GPIO_DT_SPEC_GET(TC1508A_2_NODE, in3_gpios);
    static const struct gpio_dt_spec motor4_in2 = GPIO_DT_SPEC_GET(TC1508A_2_NODE, in4_gpios);
    
    struct
    {
        const struct gpio_dt_spec *in1;
        const struct gpio_dt_spec *in2;
    }motors[4]={
        {&motor1_in1,&motor1_in2},
        {&motor2_in1,&motor2_in2},
        {&motor3_in1,&motor3_in2},
        {&motor4_in1,&motor4_in2},
    };
    
    static uint8_t car_direction=MIDDLE;
    static uint8_t car_status=STOP;
    
    void motor_stop(uint8_t wheel)
    {
        gpio_pin_set_dt(motors[wheel].in1,0);
        gpio_pin_set_dt(motors[wheel].in2,0);
    }
    
    void motor_forward(uint8_t wheel)
    {
        gpio_pin_set_dt(motors[wheel].in1,1);
        gpio_pin_set_dt(motors[wheel].in2,0);
    }
    
    void motor_reverse(uint8_t wheel)
    {
        gpio_pin_set_dt(motors[wheel].in1,0);
        gpio_pin_set_dt(motors[wheel].in2,1);
    }
    
    
    void car_stop(void)
    {
        motor_stop(LEFTFRONT);
        motor_stop(LEFTREAR);
        motor_stop(RIGHTFRONT);
        motor_stop(RIGHTREAR);
    }
    
    void car_forward(void)
    {
        if(car_direction==MIDDLE)
        {
            motor_forward(LEFTFRONT);
            motor_forward(LEFTREAR);
            motor_forward(RIGHTFRONT);
            motor_forward(RIGHTREAR);
        }else if(car_direction==LEFT)
        {
            motor_reverse(LEFTFRONT);
            motor_reverse(LEFTREAR);
            motor_forward(RIGHTFRONT);
            motor_forward(RIGHTREAR);
        }else if(car_direction==RIGHT)
        {
            motor_forward(LEFTFRONT);
            motor_forward(LEFTREAR);
            motor_reverse(RIGHTFRONT);
            motor_reverse(RIGHTREAR);
        }
    }
    
    
    void car_reverse(void)
    {
        if(car_direction==MIDDLE)
        {
            motor_reverse(LEFTFRONT);
            motor_reverse(LEFTREAR);
            motor_reverse(RIGHTFRONT);
            motor_reverse(RIGHTREAR);
        }else if(car_direction==LEFT)
        {
            motor_forward(LEFTFRONT);
            motor_forward(LEFTREAR);
            motor_reverse(RIGHTFRONT);
            motor_reverse(RIGHTREAR);
        }else if(car_direction==RIGHT)
        {
            motor_reverse(LEFTFRONT);
            motor_reverse(LEFTREAR);
            motor_forward(RIGHTFRONT);
            motor_forward(RIGHTREAR);
        }
    }
    
    
    /* 输入回调函数 */
    static void ir_input_callback(struct input_event *evt, void *user_data)
    {
        if (evt->sync == 0) {
            return;
        }
    
        if (evt->type == INPUT_EV_DEVICE && evt->code == INPUT_MSC_SCAN) {
            uint16_t value = (uint16_t)evt->value;
            uint8_t address = (value >> 8) & 0xFF;
            uint8_t command = value & 0xFF;
    
            LOG_INF("Received IR - Address: 0x%02X, Command: 0x%02X", address, command);
    
            /* 在这里添加你的业务逻辑 */
            switch(command) {
                case 0x07:
                    LOG_INF("Car forward");
                    car_status=FORWORD;
                    car_forward();
                    break;
                case 0x13:
                    LOG_INF("Car car_reverse");
                    car_status=BACKWORD;
                    car_reverse();
                    break;
                case 0x50:
                    LOG_INF("turn left");
                    car_direction=LEFT;
                    if(car_status==FORWORD){
                        car_forward();
                    }else if(car_status==BACKWORD){
                        car_reverse();
                    }
                    break;
                case 0x03:
                    LOG_INF("Car stop");
                    car_stop();
                    break;
                case 0x0B:
                    LOG_INF("Turn middle");
                    car_direction=MIDDLE;
                    if(car_status==FORWORD){
                        car_forward();
                    }else if(car_status==BACKWORD){
                        car_reverse();
                    }
                    break;
                case 0x0F:
                    LOG_INF("Turn right");
                    car_direction=RIGHT;
                    if(car_status==FORWORD){
                        car_forward();
                    }else if(car_status==BACKWORD){
                        car_reverse();
                    }
                    break;
                default:
                    LOG_INF("Unknown command: 0x%02X", command);
                    break;
            }
        }
    }
    /* 注册回调 - 使用宏,不是函数调用 */
    INPUT_CALLBACK_DEFINE(NULL, ir_input_callback,NULL);
    
    int main(void)
    {
        printk("GPIO Motor Control Started\n");
    
        // 检查设备就绪
        if (!gpio_is_ready_dt(&motor1_in1) || !gpio_is_ready_dt(&motor1_in2)|| !gpio_is_ready_dt(&motor2_in1)|| !gpio_is_ready_dt(&motor2_in2)\
        || !gpio_is_ready_dt(&motor3_in1)|| !gpio_is_ready_dt(&motor3_in2)|| !gpio_is_ready_dt(&motor4_in1)|| !gpio_is_ready_dt(&motor4_in2)) {
            printk("GPIO devices not ready\n");
            return -1;
        }
    
    
    
        // 配置 GPIO 为输出模式
        gpio_pin_configure_dt(&motor1_in1, GPIO_OUTPUT);
        gpio_pin_configure_dt(&motor1_in2, GPIO_OUTPUT);
        gpio_pin_configure_dt(&motor2_in1, GPIO_OUTPUT);
        gpio_pin_configure_dt(&motor2_in2, GPIO_OUTPUT);
        gpio_pin_configure_dt(&motor3_in1, GPIO_OUTPUT);
        gpio_pin_configure_dt(&motor3_in2, GPIO_OUTPUT);
        gpio_pin_configure_dt(&motor4_in1, GPIO_OUTPUT);
        gpio_pin_configure_dt(&motor4_in2, GPIO_OUTPUT);
    
        const struct device *gpio = DEVICE_DT_GET(DT_NODELABEL(ioport0));
    
        gpio_pin_configure(gpio,13,GPIO_INPUT);
    
        const struct device *ir_dev;
        ir_dev = DEVICE_DT_GET_ANY(vishay_vs1838b); 
        /* 获取设备句柄 */
    
        if (ir_dev == NULL) {
            LOG_ERR("Failed to get IR device");
            return -ENODEV;
        }
    
        if (!device_is_ready(ir_dev)) {
            LOG_ERR("IR device not ready");
            return -ENODEV;
        }
    
        LOG_INF("VS1838B IR receiver initialized");
    
    
        // 初始状态:停止
        car_stop();
    
        while (1) {
            k_sleep(K_FOREVER);
        }
        return 0;
    }
    
  • dts/bindings/gpio-motor.yamll

    description: Simple DC motor controlled by GPIOs only
    
    compatible: "gpio-motor"
    
    include: [base.yaml]
    
    properties:
      in1-gpios:
        required: true
        type: phandle-array
        description: GPIO for direction control IN1
    
      in2-gpios:
        required: true
        type: phandle-array
        description: GPIO for direction control IN2
    
      in3-gpios:
        required: true
        type: phandle-array
        description: GPIO for direction control IN3
    
      in4-gpios:
        required: true
        type: phandle-array
        description: GPIO for direction control IN4
    
    

八、Zephyr硬件描述机制(如何搭建gpio-motor)

Zephyr时会被这几个文件搞晕:*.yaml*.dts*.overlay*.conf,它们到底什么关系?我用这个电机项目来解释一下

1、四个文件的分工

YAML (Bindings) 定义"怎么写"描述设备属性 类型/必选项
DTS (设备树源文件)描述"有什么"SoC默认配置 板级默认配置
Overlay 用户修改 覆盖默认 DTS
以上三者合并后的最终DTS ---->生成宏定义 (DT_* 宏)
Kconfig (.conf) → 控制哪些驱动被编译

2、YAML Binding——定义“长什么样”

作用:告诉Zephyr这个设备有哪些属性、每个属性是什么类型、是否必须。

位置:通常在驱动目录下,如 dts/bindings/gpio/gpio-motor.yaml

我的项目中的例子

description: Simple DC motor controlled by GPIOs only
compatible: "gpio-motor"    # 关键:匹配字符串
include: [base.yaml]

properties:
  in1-gpios:
    required: true          # 必须提供
    type: phandle-array     # 类型:GPIO引用
    description: GPIO for direction control IN1
  in2-gpios:
    required: true
    type: phandle-array
  # in3, in4 同理

解读

  • compatible: "gpio-motor" —— 这是设备类型标识,DTS里要用这个名字
  • phandle-array —— 表示属性值是一个GPIO引用,格式如 <&ioport1 0 GPIO_ACTIVE_HIGH>
  • required: true —— 每个电机必须配齐4个GPIO

你可以把它理解为:一张表格模板,规定了每个 gpio-motor设备必须填哪几个字段。

3、DTS——描述“实际有什么”

作用:描述SoC默认有哪些外设、板子上默认有哪些设备。

位置zephyr/dts/ 下,按架构和厂商组织,比如 dts/arm/st/dts/boards/

关键点普通用户不应该修改这些文件,而是用overlay覆盖。

4、Overlay——用户自己的“修改单”

作用:在不修改SoC/板级DTS的前提下,添加或修改自己板子上的设备。

位置:项目根目录下的 *.overlay文件,或 boards/xxx.overlay

我的项目中的例子

/ {
    tc1508a_1: tc1508a_1 {
        compatible = "gpio-motor";
        in1-gpios = <&ioport1 0 GPIO_ACTIVE_HIGH>;
        in2-gpios = <&ioport1 1 GPIO_ACTIVE_HIGH>;
        in3-gpios = <&ioport1 11 GPIO_ACTIVE_HIGH>;
        in4-gpios = <&ioport1 10 GPIO_ACTIVE_HIGH>;
        status = "okay";
    };
  
    tc1508a_2: tc1508a_2 {
        compatible = "gpio-motor";
        in1-gpios = <&ioport1 9 GPIO_ACTIVE_HIGH>;
        in2-gpios = <&ioport3 1 GPIO_ACTIVE_HIGH>;
        in3-gpios = <&ioport4 3 GPIO_ACTIVE_HIGH>;
        in4-gpios = <&ioport0 6 GPIO_ACTIVE_HIGH>;
        status = "okay";
    };
};

// 启用需要使用的GPIO端口
&ioport0 { status = "okay"; };
&ioport1 { status = "okay"; };
&ioport3 { status = "okay"; };
&ioport4 { status = "okay"; };

解读

  • tc1508a_1: tc1508a_1 —— 第一个 tc1508a_1是节点名(label),第二个 "gpio-motor"是compatible
  • &ioport0 { status = "okay"; }; —— 覆盖原有节点,启用这个GPIO端口
  • 每个 in1-gpios都指向一个具体的GPIO:<&ioport1 0 ...> 表示“端口1的第0号引脚”

5、一个类比帮助理解

概念 类比 谁写的
YAML Binding 一份“应聘表模板”,规定了要填姓名、电话、学历 驱动开发者
SoC DTS 芯片厂商说“我这颗芯片有哪些硬件资源” 芯片厂商
Board DTS 板厂说“我这块板子上接了哪些东西” 板卡厂商
Overlay 你自己写“我要在这块板子上外接一个电机” 你(应用开发者)
prj.conf “我需要GPIO功能,请编译进去”

6、回到我们的项目

理解了这个机制,再看我的代码就清晰了:

  1. YAML:定义了 gpio-motor设备有 in1-gpios等4个属性
  2. Overlay:创建了两个 gpio-motor设备(两片TC1508A),填入了实际的GPIO引脚
  3. prj.conf:启用了GPIO和INPUT子系统
  4. C代码:用 GPIO_DT_SPEC_GET获取配置,通过 gpio_pin_set_dt操作

如果我想把电机换到别的引脚,只需要改overlay文件,main.c一行都不用动。这就是Zephyr设备树机制的威力。

劫灰飞尽古今平 发表于 2026-4-26 19:44 | 显示全部楼层
电机驱动的分层封装 + 设备树解耦,改引脚只需要动 overlay 文件,main.c 一行代码都不用碰,这一点真的太香了。我之前用裸机写小车,换个电机引脚要到处改宏定义,改完还要查手册核对寄存器,稍微不注意就写错;现在用 Zephyr 这套,哪怕换个 RA 系列的其他板子,只需要改 overlay 里的 GPIO 映射,核心控制逻辑完全复用,可移植性直接拉满。
不想起床喵星人 发表于 2026-4-28 18:51 | 显示全部楼层
代码结构清晰,从Devicetree配置到驱动层再到应用层逻辑,一步步都很详细。特别是电机控制抽象和运动控制部分,实现得很巧妙。
复古留声机 发表于 2026-5-8 16:55 | 显示全部楼层
很棒的项目!我很好奇,你是如何调试红外遥控部分的?有没有遇到什么特别的问题?
东方曜上镜 发表于 2026-5-8 20:52 | 显示全部楼层
我自己调试红外时踩过两个小坑,分享给大家:
VS1838B 一定要开内部上拉,不然容易飘码,楼主的配置里已经写了,照着用就行。
回调必须用INPUT_CALLBACK_DEFINE注册,不能直接函数调用,一开始我就错在这。
情和欲 发表于 2026-5-15 20:15 | 显示全部楼层
楼主的帖子很有帮助,特别是对于Zephyr RTOS的初学者来说,通过这个项目可以很好地理解Zephyr的设备树配置和GPIO操作。
远山寻你 发表于 2026-6-17 18:44 | 显示全部楼层
楼主的红外遥控映射和电机控制逻辑写得非常清晰,对于我这种初学者来说,是很好的学习材料。感谢分享!
您需要登录后才可以回帖 登录 | 注册

本版积分规则

117

主题

187

帖子

4

粉丝
快速回复 在线客服 返回列表 返回顶部
0