[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(带内部上拉)




三、软件架构
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 主程序流程
- 初始化GPIO,配置为输出
- 初始化红外设备
- 注册红外回调
- 进入无限循环,等待遥控指令
五、调试时注意事项
1. GPIO配置问题
部分IO口默认有其他功能,需要先在Devicetree中enable对应的ioport节点。
2. 红外驱动
Zephyr的VS1838B驱动通过Input子系统上报事件,需要用 INPUT_CALLBACK_DEFINE宏注册回调,不是直接函数调用。
六、效果演示
整体代码量不大,但完整实现了Zephyr设备树配置→驱动→应用的全流程。


七、全部代码
-
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、回到我们的项目
理解了这个机制,再看我的代码就清晰了:
- YAML:定义了
gpio-motor设备有 in1-gpios等4个属性
- Overlay:创建了两个
gpio-motor设备(两片TC1508A),填入了实际的GPIO引脚
- prj.conf:启用了GPIO和INPUT子系统
- C代码:用
GPIO_DT_SPEC_GET获取配置,通过 gpio_pin_set_dt操作
如果我想把电机换到别的引脚,只需要改overlay文件,main.c一行都不用动。这就是Zephyr设备树机制的威力。