【Linux驱动开发】PWM子系统-servo

一、设备树的编写

设备树需要描述两个关键信息:

  1. PWM 控制器硬件(舵机依赖 PWM 信号,需先定义 PWM 控制器);

  2. 舵机设备本身(关联到 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";                                 
    };                                                   

关键属性详解(直接影响驱动能否正常工作):

  1. compatible = "servo"

    • 必须与舵机驱动中**pwm_of_match** 的compatible完全一致(驱动中定义为"servo"),否则驱动不会匹配该设备,probe函数不会执行。
  2. pwm-names

    • 定义 PWM 通道的名称列表,与驱动中**devm_pwm_get(&pdev->dev, pwm_name)** 的pwm_name对应。

    • 驱动中循环获取"servo0""servo1",因此这里必须按顺序定义这两个名称,否则pwm_get会失败(提示 "can't find PWM device")。

  3. 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 控制器
相关推荐
A-花开堪折15 小时前
Qemu 嵌入式Linux驱动开发
linux·运维·驱动开发
wan5555cn16 小时前
当代社会情绪分类及其改善方向深度解析
大数据·人工智能·笔记·深度学习·算法·生活
陈增林16 小时前
基于 PyQt5 的多算法视频关键帧提取工具
开发语言·qt·算法
郝学胜-神的一滴16 小时前
Linux系统函数stat和lstat详解
linux·运维·服务器·开发语言·c++·程序人生·软件工程
Mr.亮先生17 小时前
常用、高效、实用的 Linux 服务器监控与运维工具清单
linux·运维·服务器
poemyang17 小时前
单线程如何撑起百万连接?I/O多路复用:现代网络架构的基石
linux·rpc·i/o 模式
极客先躯17 小时前
高可用巡检脚本实战:一键掌握服务、网络、VIP、资源状态
运维·网络·金融
鹿鸣天涯18 小时前
Wine 10.15 发布!Linux 跑 Windows 应用更丝滑了
linux·运维·windows
C嘎嘎嵌入式开发18 小时前
【机器学习算法篇】K-近邻算法
算法·机器学习·近邻算法