一、设备树的编写
设备树需要描述两个关键信息:
-
PWM 控制器硬件(舵机依赖 PWM 信号,需先定义 PWM 控制器);
-
舵机设备本身(关联到 PWM 控制器的具体通道,指定驱动匹配信息)。
理解设备树基本语法
设备树以.dts
文件(设备树源文件)编写,编译后生成.dtb
(设备树二进制文件)供内核加载。
基本语法:
-
节点(Node):用
节点名@地址
表示,描述一个硬件设备(如pwm@12340000
表示地址为 0x12340000 的 PWM 控制器); -
属性(Property):
键=值
形式,描述节点的具体参数(如compatible = "vendor,pwm-controller"
表示硬件兼容的驱动); -
子节点:节点内部可以嵌套子节点,描述从属设备(如舵机是 PWM 控制器的子设备)。
步骤 1:编写 PWM 控制器节点
舵机需要 PWM 信号驱动,因此必须先在设备树中定义PWM 控制器的硬件信息。
cpp
&pwm6 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_gslpwm6>;
clocks = <&clks IMX6UL_CLK_PWM6>,<&clks IMX6UL_CLK_PWM6>;
clock-names = "ipg","per";
status = "okay";
};
&pwm7 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_gslpwm7>;
clocks = <&clks IMX6UL_CLK_PWM7>,<&clks IMX6UL_CLK_PWM7>;
clock-names = "ipg","per";
status = "okay";
};
关键属性解释:
-
compatible
:PWM 控制器驱动通过该属性匹配硬件(必须与 PWM 控制器驱动的of_match_table
一致); -
pinctrl-0:包含PWM 控制器的寄存器物理地址和大小,还有电器属性;
-
clocks
:PWM 控制器依赖的时钟源(内核时钟子系统提供,需在时钟树中定义);
步骤 2:编写 PWM 引脚
cpp
pinctrl_gslpwm6: pwm6grp {
fsl,pins = <
MX6UL_PAD_JTAG_TDI__PWM6_OUT 0x110b0
>;
};
pinctrl_gslpwm7: pwm7grp {
fsl,pins = <
MX6UL_PAD_JTAG_TCK__PWM7_OUT 0x110b0
>;
};
步骤 3:编写舵机设备节点(关联 PWM 与驱动)
舵机设备节点需要关联到 PWM 控制器 ,并指定与我们编写的舵机驱动匹配的信息(compatible = "servo"
)。
cpp
/*
servo {
compatible = "servo";
pwms = <&pwm6 0 20000000>;
pwm-names = "servo";
};
*/
servo {
compatible = "servo";
pwms = <&pwm6 0 20000000>, <&pwm7 0 20000000>;
pwm-names = "servo0", "servo1";
status = "okay";
};
关键属性详解(直接影响驱动能否正常工作):
-
compatible = "servo"
- 必须与舵机驱动中**
pwm_of_match
** 的compatible
完全一致(驱动中定义为"servo"
),否则驱动不会匹配该设备,probe
函数不会执行。
- 必须与舵机驱动中**
-
pwm-names
-
定义 PWM 通道的名称列表,与驱动中**
devm_pwm_get(&pdev->dev, pwm_name)
** 的pwm_name
对应。 -
驱动中循环获取
"servo0"
和"servo1"
,因此这里必须按顺序定义这两个名称,否则pwm_get
会失败(提示 "can't find PWM device")。
-
-
pwms
-
描述每个舵机使用的 PWM 通道参数,格式由 PWM 控制器的
#pwm-cells
决定(此处为 3 个参数):-
第一个参数:
&pwm
→ 引用 PWM 控制器节点(与步骤 2 中的 PWM 节点标签一致); -
第二个参数:
0
/1
→ PWM 通道号(硬件上 PWM 控制器的通道,如 TIM1_CH1 对应通道 0);(这里我设置 0 也是可以没啥影响) -
第三个参数:
20000000
→ PWM 周期(单位:ns),必须与驱动中的DEFAULT_PERIOD
一致(20ms=20000000ns),否则舵机无法正常工作; -
第四个参数(可选):
0
→ 极性(0 表示高电平有效,1 表示低电平有效,根据舵机硬件手册设置)。(我没设置)
-
-
二、驱动的编写
步骤 1:搭建内核模块基础框架
先实现一个 "空模块",确保能编译、加载、卸载,理解内核模块的基本结构。
cpp
#include <linux/module.h>
#include <linux/platform_device.h>
#define DRIVER_NAME "servo"
// 模块信息(必须)
MODULE_LICENSE("GPL"); // 许可协议(GPL是最常见的)
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("PWM Servo Driver");
// 平台驱动结构体(暂为空)
static struct platform_driver pwm_driver = {
.driver = {
.name = DRIVER_NAME,
},
};
// 注册平台驱动(简化入口/出口)
module_platform_driver(pwm_driver);
步骤 2:定义数据结构与核心参数
根据需求设计状态存储结构(struct servo_data
),并定义 PWM 关键参数(周期、脉冲范围)。
cpp
// 舵机PWM参数(核心!根据硬件手册调整)
#define DEFAULT_PERIOD 20000000 // 20ms周期(50Hz)
#define MIN_PULSE 500000 // 0.5ms(0°)
#define MAX_PULSE 2500000 // 2.5ms(180°)
// 状态数据结构
struct servo_data {
struct pwm_device *pwm[2]; // 2个舵机的PWM设备
int current_angle[2]; // 记录当前角度
int selected_servo; // 当前选中的舵机
};
static struct servo_data servo_data; // 全局实例
为什么这样设计?
-
舵机的 PWM 参数是硬件决定的:大多数舵机要求 20ms 周期,0.5-2.5ms 脉冲对应 0-180°
-
数据结构需要保存 "硬件句柄"(
pwm_device
)和 "运行时状态"(角度、选中的舵机),方便后续操作
步骤 3:实现 PWM 设备初始化(probe 函数)
probe 函数是驱动 "启动" 的入口,负责获取硬件资源并初始化。
cpp
static int pwm_probe(struct platform_device *pdev)
{
int ret;
int i;
pr_info("Servo driver probe start\n");
// 1. 获取2个PWM设备
for (i = 0; i < 2; i++) {
char pwm_name[20];
snprintf(pwm_name, sizeof(pwm_name), "servo%d", i); // PWM名称(与设备树匹配)
// 获取PWM设备(devm_前缀:自动管理资源,卸载时释放)
servo_data.pwm[i] = devm_pwm_get(&pdev->dev, pwm_name);
if (IS_ERR(servo_data.pwm[i])) {
ret = PTR_ERR(servo_data.pwm[i]);
pr_err("Failed to get PWM %s: %d\n", pwm_name, ret);
return ret; // 获取失败则退出
}
// 2. 启用PWM
ret = pwm_enable(servo_data.pwm[i]);
if (ret) {
pr_err("Failed to enable PWM %s: %d\n", pwm_name, ret);
return ret;
}
// 3. 初始角度设为90°
servo_data.current_angle[i] = 90;
// (后续补充set_servo_angle函数来配置PWM)
}
pr_info("PWM initialized successfully\n");
return 0;
}
// 更新platform_driver,添加probe
static struct platform_driver pwm_driver = {
.probe = pwm_probe,
.driver = {
.name = DRIVER_NAME,
},
};
关键说明:
-
devm_pwm_get
:从内核获取 PWM 设备,第二个参数是 PWM 名称(需与设备树中定义的一致) -
错误处理:内核编程必须检查每一步的返回值,失败时及时退出并清理资源
-
pwm_enable
:启用 PWM 输出(不启用则无信号输出)
步骤 4:实现角度控制逻辑(set_servo_angle)
根据角度计算 PWM 占空比,并调用pwm_config
配置硬件。
cpp
static int set_servo_angle(int servo_id, int angle)
{
unsigned int duty_cycle;
int ret;
// 1. 检查舵机ID合法性
if (servo_id < 0 || servo_id > 1) {
pr_err("Invalid servo ID: %d\n", servo_id);
return -EINVAL; // 内核错误码:无效参数
}
// 2. 限制角度范围(0-180°)
if (angle < 0) angle = 0;
if (angle > 180) angle = 180;
// 3. 计算占空比(核心公式)
// 原理:角度与脉冲宽度线性映射
duty_cycle = MIN_PULSE + (angle * (MAX_PULSE - MIN_PULSE) / 180);
// 4. 配置PWM(占空比+周期)
ret = pwm_config(servo_data.pwm[servo_id], duty_cycle, DEFAULT_PERIOD);
if (ret) {
pr_err("Failed to config PWM: %d\n", ret);
return ret;
}
// 5. 更新状态
servo_data.current_angle[servo_id] = angle;
return 0;
}
// 在probe函数中调用,初始化角度
// 替换步骤3中的"初始角度设为90°"部分:
ret = set_servo_angle(i, 90);
if (ret) {
pr_err("Failed to set initial angle for servo %d\n", i);
return ret;
}
参数为什么这样算?
-
舵机的角度由 "脉冲宽度" 决定:0.5ms 对应 0°,2.5ms 对应 180°,中间线性变化
-
公式推导:
脉冲宽度 = 0.5ms + (角度/180°)*(2.5ms-0.5ms)
-
单位:内核 PWM 函数要求纳秒(ns),所以 0.5ms=500000ns,20ms=20000000ns
步骤 5:添加用户空间接口(混杂设备)
通过miscdevice
创建/dev
下的设备文件,让用户程序能访问驱动。
cpp
// 1. 定义文件操作结构体(用户空间调用的接口)
static const struct file_operations fops = {
.owner = THIS_MODULE,
.open = pwm_open, // 打开设备
.release = pwm_release, // 关闭设备
.unlocked_ioctl = pwm_ioctl, // 处理IOCTL命令
};
// 2. 混杂设备结构体
static struct miscdevice servo_miscdev = {
.minor = MISC_DYNAMIC_MINOR, // 动态分配次设备号
.name = "misc-servo", // 设备文件名:/dev/misc-servo
.fops = &fops, // 关联文件操作
};
// 3. 实现open/release(简单打印日志)
static int pwm_open(struct inode *node, struct file *fp)
{
pr_info("Servo device opened\n");
return 0;
}
static int pwm_release(struct inode *node, struct file *fp)
{
pr_info("Servo device closed\n");
return 0;
}
// 4. 在probe函数末尾注册混杂设备
ret = misc_register(&servo_miscdev);
if (ret) {
pr_err("Failed to register misc device: %d\n", ret);
return ret;
}
pr_info("Device created: /dev/misc-servo\n");
为什么用混杂设备?
-
普通字符设备需要手动申请主设备号,混杂设备(miscdevice)简化了这一过程(自动分配)
-
适合功能简单的设备(如舵机控制),无需复杂的设备管理
步骤 6:实现 IOCTL 命令(用户 - 内核交互)
定义 IOCTL 命令,让用户程序能设置角度、获取角度和切换舵机。
cpp
// 1. 定义IOCTL命令(内核规定格式)
#define SERVO_MAGIC 'S' // 幻数(唯一标识一组命令)
#define SERVO_SET_ANGLE _IOW(SERVO_MAGIC, 1, int) // 写命令:设置角度
#define SERVO_GET_ANGLE _IOR(SERVO_MAGIC, 2, int) // 读命令:获取角度
#define SERVO_SELECT _IOW(SERVO_MAGIC, 3, int) // 写命令:选择舵机
// 2. 实现IOCTL处理函数
static long pwm_ioctl(struct file *fp, unsigned int cmd, unsigned long arg)
{
int value; // 存储用户空间传来的值
switch (cmd) {
case SERVO_SET_ANGLE:
// 从用户空间复制角度值(内核不能直接访问用户空间内存)
if (copy_from_user(&value, (int __user *)arg, sizeof(value)))
return -EFAULT; // 复制失败
return set_servo_angle(servo_data.selected_servo, value);
case SERVO_GET_ANGLE:
// 获取当前角度,复制到用户空间
value = servo_data.current_angle[servo_data.selected_servo];
if (copy_to_user((int __user *)arg, &value, sizeof(value)))
return -EFAULT;
return 0;
case SERVO_SELECT:
// 选择舵机(0或1)
if (copy_from_user(&value, (int __user *)arg, sizeof(value)))
return -EFAULT;
if (value != 0 && value != 1)
return -EINVAL; // 无效舵机ID
servo_data.selected_servo = value;
return 0;
default:
return -ENOTTY; // 未知命令
}
}
// 3. 在probe中初始化默认选中的舵机
servo_data.selected_servo = 0; // 默认选中舵机0
关键说明:
-
IOCTL 命令格式:
_IOW(幻数, 命令号, 数据类型)
表示 "从用户空间写入数据",_IOR
表示 "向用户空间读取数据" -
用户空间与内核空间内存隔离:必须用
copy_from_user
/copy_to_user
传输数据,直接访问会导致崩溃
步骤 7:完善设备树匹配与卸载逻辑
-
设备树匹配:让驱动能找到对应的硬件(通过
compatible
属性) -
remove 函数:驱动卸载时清理资源
cpp
// 1. 设备树匹配表(与设备树中的compatible对应)
static const struct of_device_id pwm_of_match[] = {
{ .compatible = "servo" }, // 设备树中需定义compatible = "servo"
{} // 哨兵(结束标志)
};
MODULE_DEVICE_TABLE(of, pwm_of_match); // 向内核注册匹配表
// 2. 实现remove函数
static int pwm_remove(struct platform_device *pdev)
{
int i;
misc_deregister(&servo_miscdev); // 注销混杂设备
for (i = 0; i < 2; i++) {
pwm_disable(servo_data.pwm[i]); // 禁用PWM
}
pr_info("Servo driver removed\n");
return 0;
}
// 3. 更新platform_driver
static struct platform_driver pwm_driver = {
.probe = pwm_probe,
.remove = pwm_remove,
.driver = {
.name = DRIVER_NAME,
.of_match_table = pwm_of_match, // 关联设备树匹配表
},
};
设备树作用?
- 设备树(Device Tree)是描述硬件的文件,驱动通过
compatible
属性找到对应的硬件(如 PWM 控制器