1. Linux PWM驱动开发概述
在嵌入式Linux系统中,PWM(脉冲宽度调制)是一种常用的硬件接口技术,广泛应用于电机控制、LED调光、舵机驱动等场景。本文将基于i.MX6ULL平台,详细介绍如何开发一个完整的PWM设备驱动,用于控制常见的SG90舵机。
1.1 PWM技术基础
PWM通过调节脉冲的占空比来控制输出信号的平均电压。对于SG90舵机而言:
- 工作周期固定为20ms(50Hz)
- 脉冲宽度范围0.5ms-2.5ms对应0°-180°转角
- 控制精度可达±1°
在Linux内核中,PWM子系统提供了统一的接口来管理各种PWM控制器。开发者需要理解三个核心数据结构:
pwm_chip:抽象PWM控制器pwm_device:代表单个PWM输出通道pwm_ops:PWM控制器的操作函数集
1.2 开发环境准备
本次实验基于以下硬件/软件环境:
- 开发板:正点原子i.MX6ULL Alpha
- Linux内核版本:4.1.15
- 目标PWM:i.MX6ULL的PWM3(GPIO1_IO04)
- 外设:SG90舵机(工作电压4.8-6V)
需要准备的开发工具:
- 交叉编译工具链:arm-linux-gnueabihf-
- 内核源码:linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
- 设备树编译器(dtc)
2. 设备树配置
2.1 引脚复用配置
i.MX6ULL的PWM3对应GPIO1_IO04引脚,需要在设备树中正确配置复用功能。修改imx6ull-alientek-emmc.dts文件:
c复制&iomuxc {
pinctrl_pwm3: pwm3grp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO04__PWM3_OUT 0x110b0
>;
};
};
其中0x110b0是引脚电气特性配置:
- 0x10:100K下拉
- 0xb0:驱动能力中等,速度中等,开漏禁用
2.2 PWM控制器配置
在设备树中启用PWM3控制器:
c复制&pwm3 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_pwm3>;
status = "okay";
};
2.3 自定义设备节点
为SG90舵机添加专用节点:
c复制sg90 {
compatible = "sg90";
pwms = <&pwm3 0 20000000>; // 使用PWM3,周期20ms
status = "okay";
};
关键参数说明:
pwms属性格式:<&pwm控制器 ID 周期(ns)>- 周期20ms(20000000ns)是SG90的标准控制周期
- ID为0表示使用PWM3的第一个通道
注意:检查设备树中是否有其他节点占用了PWM3或GPIO1_IO04,如有冲突需要注释掉相关配置。
3. 驱动开发详解
3.1 驱动框架设计
采用Platform驱动框架,主要包含以下组件:
- 字符设备接口(供用户空间控制)
- PWM子系统接口(与硬件交互)
- 设备树匹配机制
驱动文件结构:
code复制pwm_driver/
├── Makefile
├── pwm.c # 驱动核心代码
└── pwmAPP.c # 测试应用程序
3.2 关键数据结构
3.2.1 设备私有结构体
c复制struct sg90_struct {
dev_t devid; // 设备号
struct cdev cdev; // 字符设备
struct class *class; // 设备类
struct device *device; // 设备节点
struct device_node *nd; // 设备树节点
struct pwm_device *pwm_dev; // PWM设备指针
};
3.2.2 文件操作集
c复制static const struct file_operations sg90_fops = {
.owner = THIS_MODULE,
.open = sg90_open,
.release = sg90_release,
.write = sg90_write,
};
3.3 PWM核心操作实现
3.3.1 PWM设备获取
c复制sg90dev.nd = dev->dev.of_node;
sg90dev.pwm_dev = devm_of_pwm_get(&dev->dev, node, NULL);
if (IS_ERR(sg90dev.pwm_dev)) {
pr_err("Failed to get PWM device\n");
return PTR_ERR(sg90dev.pwm_dev);
}
3.3.2 PWM参数配置
c复制// 配置周期和占空比
ret = pwm_config(sg90dev.pwm_dev, 500000, 20000000);
if (ret < 0) {
pr_err("PWM config failed\n");
return ret;
}
// 使能PWM输出
ret = pwm_enable(sg90dev.pwm_dev);
if (ret < 0) {
pr_err("PWM enable failed\n");
return ret;
}
3.3.3 角度控制实现
c复制static ssize_t sg90_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
int angle, duty_ns;
char databuf[32];
// 从用户空间获取角度值(0-180)
if (copy_from_user(databuf, buf, count))
return -EFAULT;
angle = simple_strtol(databuf, NULL, 10);
angle = clamp(angle, 0, 180); // 限制角度范围
// 计算对应占空比(0.5ms-2.5ms)
duty_ns = 500000 + (2000000 * angle) / 180;
// 更新PWM参数
ret = pwm_config(sg90dev.pwm_dev, duty_ns, 20000000);
if (ret < 0)
return ret;
return count;
}
3.4 驱动初始化和退出
3.4.1 驱动初始化
c复制static int sg90_probe(struct platform_device *pdev)
{
// 1. 注册字符设备
alloc_chrdev_region(&sg90dev.devid, 0, 1, "sg90");
cdev_init(&sg90dev.cdev, &sg90_fops);
cdev_add(&sg90dev.cdev, sg90dev.devid, 1);
// 2. 创建设备节点
sg90dev.class = class_create(THIS_MODULE, "sg90");
sg90dev.device = device_create(sg90dev.class, NULL,
sg90dev.devid, NULL, "sg90");
// 3. 初始化PWM
// ... PWM操作代码见上文 ...
return 0;
}
3.4.2 驱动卸载
c复制static int sg90_remove(struct platform_device *pdev)
{
// 1. 禁用PWM
pwm_disable(sg90dev.pwm_dev);
// 2. 销毁设备节点
device_destroy(sg90dev.class, sg90dev.devid);
class_destroy(sg90dev.class);
// 3. 注销字符设备
cdev_del(&sg90dev.cdev);
unregister_chrdev_region(sg90dev.devid, 1);
return 0;
}
4. 应用层测试程序
4.1 测试程序实现
c复制#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char **argv)
{
int fd;
if (argc != 2) {
printf("Usage: %s <angle(0-180)>\n", argv[0]);
return -1;
}
fd = open("/dev/sg90", O_WRONLY);
if (fd < 0) {
perror("Open device failed");
return -1;
}
if (write(fd, argv[1], strlen(argv[1])) < 0) {
perror("Write failed");
close(fd);
return -1;
}
printf("Set angle to %d degrees\n", atoi(argv[1]));
close(fd);
return 0;
}
4.2 编译与测试
- 编译驱动:
bash复制make -C /path/to/kernel/source M=$(pwd) modules
- 编译测试程序:
bash复制arm-linux-gnueabihf-gcc pwmAPP.c -o pwmAPP
- 在开发板上测试:
bash复制insmod pwm.ko # 加载驱动
./pwmAPP 90 # 设置舵机到90度位置
./pwmAPP 0 # 回到0度位置
rmmod pwm # 卸载驱动
5. 常见问题与调试技巧
5.1 极性设置失败问题
在测试中发现pwm_set_polarity()返回-38错误,这是因为:
- i.MX6ULL的PWM控制器硬件不支持运行时极性切换
- 需要在设备树中固定配置极性
- 解决方案:注释掉极性设置代码或修改硬件设计
5.2 占空比不准确
可能原因及解决方法:
-
时钟源精度不足:
- 检查PWM控制器的时钟源配置
- 确保父时钟(如ipg_clk)频率稳定
-
计算误差:
c复制// 改进后的占空比计算公式(避免整数除法截断) duty_ns = 500000 + (2000000 * angle + 90) / 180; -
硬件滤波影响:
- 在PWM输出线上添加RC滤波电路(如1kΩ+100nF)
- 但注意滤波会降低响应速度
5.3 系统负载影响PWM输出
在高系统负载下可能出现PWM波形抖动:
- 解决方案:
- 提高PWM控制器的时钟优先级
- 使用RT-Preempt内核补丁
- 避免在PWM控制进程中使用sleep()
5.4 设备树调试技巧
- 检查PWM配置是否生效:
bash复制cat /sys/kernel/debug/pwm
- 验证引脚复用:
bash复制cat /sys/kernel/debug/pinctrl/pinctrl-handles
- 测量实际输出频率:
bash复制# 需要示波器或逻辑分析仪
# 或使用内核的PWM调试接口
echo 1 > /sys/class/pwm/pwmchip0/pwm0/enable
6. 进阶优化建议
6.1 添加sysfs接口
除了字符设备,可以实现sysfs接口更方便控制:
c复制static ssize_t angle_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
return sprintf(buf, "%d\n", current_angle);
}
static ssize_t angle_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
int angle;
sscanf(buf, "%d", &angle);
// 更新PWM占空比
return count;
}
static DEVICE_ATTR_RW(angle);
6.2 支持多路PWM
修改驱动结构支持多个舵机:
- 在设备树中定义多路PWM
- 使用数组管理多个pwm_device
- 添加次设备号区分不同通道
6.3 平滑运动控制
实现舵机平滑转动:
c复制void smooth_move(int target_angle, int speed)
{
int current = get_current_angle();
int step = (target > current) ? 1 : -1;
while (current != target) {
current += step;
set_angle(current);
msleep(1000/speed); // 控制运动速度
}
}
6.4 功耗优化
- 空闲时关闭PWM输出:
c复制static int sg90_suspend(struct device *dev)
{
pwm_disable(sg90dev.pwm_dev);
return 0;
}
- 动态调整PWM频率:
- 静止时降低刷新率
- 运动时恢复50Hz
在实际项目中,PWM驱动开发需要综合考虑硬件特性、系统实时性和应用需求。本文介绍的SG90驱动实现方案可以扩展到其他PWM设备控制场景,如直流电机调速、LED调光等。关键是要深入理解PWM子系统的工作机制,并根据具体硬件调整实现细节。