3. 车载字符设备驱动注册与文件操作实现

车载字符设备驱动注册与文件操作实现 学习卡片

任务目标

在车载Linux环境中实现一个PWM蜂鸣器驱动,支持通过ioctl设置PWM频率与占空比,从而控制蜂鸣器音调与音量。驱动基于Linux PWM子系统,适用于带有PWM控制器(如SoC内置PWM或外部PWM芯片)的硬件平台。

环境与依赖

  • Linux内核版本:4.19+(使用标准PWM子系统API)
  • 硬件:任意PWM输出通道连接蜂鸣器驱动电路(需有源蜂鸣器或通过H桥驱动无源蜂鸣器)
  • 设备树:需在设备树中声明PWM蜂鸣器节点,并指定使用的PWM通道、默认频率与占空比

核心实现

驱动通过标准字符设备接口暴露给用户空间,注册miscdevice以简化设备节点管理。核心功能包括:open时初始化PWM并启动输出;ioctl支持设置频率(以Hz为单位)与占空比(以百分比表示,0~100);release时停止PWM并释放资源。PWM频率通过pwm_config的周期参数控制,占空比通过duty_cycle控制。

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/pwm.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/slab.h>

#define PWM_BUZZER_IOC_MAGIC   'B'
#define PWM_BUZZER_SET_FREQ    _IOW(PWM_BUZZER_IOC_MAGIC, 1, unsigned int)
#define PWM_BUZZER_SET_DUTY    _IOW(PWM_BUZZER_IOC_MAGIC, 2, unsigned int)

struct pwm_buzzer_dev {
    struct pwm_device *pwm;
    unsigned int freq;      /* Hz */
    unsigned int duty;      /* 0~100 */
};

static struct pwm_buzzer_dev *buzzer_dev;

static void pwm_buzzer_update(struct pwm_buzzer_dev *dev)
{
    unsigned int period_ns;
    unsigned int duty_ns;

    if (!dev->pwm)
        return;

    if (dev->freq == 0) {
        pwm_disable(dev->pwm);
        return;
    }

    period_ns = NSEC_PER_SEC / dev->freq;
    duty_ns = (period_ns / 100) * dev->duty;

    pwm_config(dev->pwm, duty_ns, period_ns);
    pwm_enable(dev->pwm);
}

static int pwm_buzzer_open(struct inode *inode, struct file *file)
{
    if (!buzzer_dev || !buzzer_dev->pwm)
        return -ENODEV;

    file->private_data = buzzer_dev;
    return 0;
}

static long pwm_buzzer_ioctl(struct file *file, unsigned int cmd,
                             unsigned long arg)
{
    struct pwm_buzzer_dev *dev = file->private_data;
    unsigned int val;

    if (copy_from_user(&val, (void __user *)arg, sizeof(val)))
        return -EFAULT;

    switch (cmd) {
    case PWM_BUZZER_SET_FREQ:
        if (val == 0 || val > 100000)   /* 限制范围:1Hz~100kHz */
            return -EINVAL;
        dev->freq = val;
        break;
    case PWM_BUZZER_SET_DUTY:
        if (val > 100)
            return -EINVAL;
        dev->duty = val;
        break;
    default:
        return -ENOTTY;
    }

    pwm_buzzer_update(dev);
    return 0;
}

static int pwm_buzzer_release(struct inode *inode, struct file *file)
{
    struct pwm_buzzer_dev *dev = file->private_data;

    if (dev->pwm)
        pwm_disable(dev->pwm);
    return 0;
}

static const struct file_operations pwm_buzzer_fops = {
    .owner          = THIS_MODULE,
    .open           = pwm_buzzer_open,
    .unlocked_ioctl = pwm_buzzer_ioctl,
    .release        = pwm_buzzer_release,
};

static struct miscdevice pwm_buzzer_misc = {
    .minor  = MISC_DYNAMIC_MINOR,
    .name   = "pwm_buzzer",
    .fops   = &pwm_buzzer_fops,
};

static int pwm_buzzer_probe(struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;
    struct pwm_buzzer_dev *dev;
    u32 freq = 1000, duty = 50;

    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    dev->pwm = devm_of_pwm_get(&pdev->dev, np, NULL);
    if (IS_ERR(dev->pwm))
        return PTR_ERR(dev->pwm);

    of_property_read_u32(np, "default-frequency", &freq);
    of_property_read_u32(np, "default-duty", &duty);

    dev->freq = freq;
    dev->duty = duty;

    buzzer_dev = dev;
    pwm_buzzer_update(dev);

    return misc_register(&pwm_buzzer_misc);
}

static int pwm_buzzer_remove(struct platform_device *pdev)
{
    misc_deregister(&pwm_buzzer_misc);
    if (buzzer_dev && buzzer_dev->pwm)
        pwm_disable(buzzer_dev->pwm);
    return 0;
}

static const struct of_device_id pwm_buzzer_of_match[] = {
    { .compatible = "vendor,pwm-buzzer" },
    { }
};
MODULE_DEVICE_TABLE(of, pwm_buzzer_of_match);

static struct platform_driver pwm_buzzer_driver = {
    .probe  = pwm_buzzer_probe,
    .remove = pwm_buzzer_remove,
    .driver = {
        .name = "pwm_buzzer",
        .of_match_table = pwm_buzzer_of_match,
    },
};
module_platform_driver(pwm_buzzer_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("PWM Buzzer Driver");

设备树配置示例

/ {
    pwm_buzzer {
        compatible = "vendor,pwm-buzzer";
        pwms = &pwm0, 0;   /* 使用pwm0通道0 */
        default-frequency = <2000>;  /* 默认2kHz */
        default-duty = <50>;         /* 默认50%占空比 */
    };
};

编译与加载

  1. 将驱动源码放入内核源码树drivers/misc/目录,并添加Makefile条目:obj-$(CONFIG_PWM_BUZZER) += pwm_buzzer.o
  2. 在内核配置中启用CONFIG_PWM_BUZZER=y(或=m)
  3. 编译内核或模块:make modules 或 make
  4. 加载模块(若编译为模块):insmod pwm_buzzer.ko

用户空间测试程序

#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>

#define PWM_BUZZER_IOC_MAGIC   'B'
#define PWM_BUZZER_SET_FREQ    _IOW(PWM_BUZZER_IOC_MAGIC, 1, unsigned int)
#define PWM_BUZZER_SET_DUTY    _IOW(PWM_BUZZER_IOC_MAGIC, 2, unsigned int)

int main(void)
{
    int fd = open("/dev/pwm_buzzer", O_RDWR);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    unsigned int freq = 3000;   /* 3kHz */
    unsigned int duty = 30;     /* 30% 音量 */

    if (ioctl(fd, PWM_BUZZER_SET_FREQ, &freq) < 0)
        perror("set freq");
    if (ioctl(fd, PWM_BUZZER_SET_DUTY, &duty) < 0)
        perror("set duty");

    sleep(2);   /* 发声2秒 */

    freq = 1000;
    duty = 80;
    ioctl(fd, PWM_BUZZER_SET_FREQ, &freq);
    ioctl(fd, PWM_BUZZER_SET_DUTY, &duty);
    sleep(2);

    close(fd);
    return 0;
}

验证方法

  1. 加载驱动后检查设备节点:ls -l /dev/pwm_buzzer,应存在该字符设备。
  2. 编译并运行测试程序,应能听到蜂鸣器先以3kHz、30%占空比发声2秒,随后变为1kHz、80%占空比发声2秒。音调变化对应频率改变,音量变化对应占空比改变。
  3. 使用示波器连接PWM输出引脚,可观察到频率与占空比与设置值一致。
  4. 关闭设备文件后(测试程序退出),蜂鸣器应停止发声,表明release中已禁用PWM输出。

常见问题

  • 设备节点未创建:确认设备树节点匹配compatible属性,且PWM控制器驱动已加载。
  • ioctl返回EINVAL:检查传入的频率是否在1~100000范围内,占空比是否在0~100范围内。
  • 蜂鸣器无声:确认PWM通道硬件连接正确,且蜂鸣器驱动电路支持所设置的频率范围(无源蜂鸣器需在音频范围内)。