先前要使用rock5a rk3588s芯片中的pwm通道作为两个电调的输入信号使用。

查阅资料,在rsetup中打开相对应的pwm-gpio后,发现可以通过用户空间直接操控pwm频率,占空比。

其用作电调的控制是足够的,但是想更进一步,把pwm编写为字符设备驱动程序。

esc-pwm驱动程序编写

这里简要说明一下,该处编写的esc-pwm程序分为左右两个,驱动程序中实现的有

  1. 映射pwmchip到字符设备文件

  2. 向字符设备写入pwm高电平持续时间(1000-2000us)即可

  3. pwm周期为20ms即50Hz

  4. 字符设备释放后,pwm高电平持续时间自动复位1.5ms(1500us)

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/pwm.h>
#include <linux/uaccess.h>
#include <linux/device.h>

#define DEVICE_NAME "esc_left_pwm"
#define PWM_CHIP "pwmchip0"  // PWM 控制器名称
#define PWM_CHANNEL 0        // PWM 通道号

#define PWM_PERIOD 20000000
#define PWM_DUTY_CYCLE 1500000

static struct pwm_device *esc_left_pwm;
static int major_number;
static struct class *esc_left_pwm_class;

static int device_open(struct inode *inode, struct file *file) {
    printk(KERN_INFO "ESC LEFT PWM device opened\n");
    return 0;
}

static int device_release(struct inode *inode, struct file *file) {
    printk(KERN_INFO "ESC LEFT PWM device closed\n");
    pwm_config(esc_left_pwm, PWM_DUTY_CYCLE, PWM_PERIOD); // 初始占空比为 1.5ms
    return 0;
}

static ssize_t device_write(struct file *file, const char __user *buffer, size_t length, loff_t *offset) {
    char data[32];
    unsigned long duty_cycle_us;

    if (copy_from_user(data, buffer, length))
        return -EFAULT;

    data[length] = '\0';
    duty_cycle_us = simple_strtoul(data, NULL, 10);

    // 设置 PWM 占空比,将微秒转换为纳秒
    pwm_config(esc_left_pwm, duty_cycle_us * 1000, PWM_PERIOD); // 输入为微秒,转换为纳秒

    return length;
}

static struct file_operations fops = {
    .open = device_open,
    .release = device_release,
    .write = device_write,
};

static int __init pwm_init(void) {
    // 注册字符设备
    major_number = register_chrdev(0, DEVICE_NAME, &fops);
    if (major_number < 0) {
        printk(KERN_ALERT "ESC LEFT PWM: Failed to register device\n");
        return major_number;
    }

    // 创建设备类
    esc_left_pwm_class = class_create(THIS_MODULE, "esc_left_pwm_class");
    if (IS_ERR(esc_left_pwm_class)) {
        printk(KERN_ALERT "ESC LEFT PWM: Failed to create device class\n");
        unregister_chrdev(major_number, DEVICE_NAME);
        return PTR_ERR(esc_left_pwm_class);
    }

    // 创建设备文件
    device_create(esc_left_pwm_class, NULL, MKDEV(major_number, 0), NULL, DEVICE_NAME);
    printk(KERN_INFO "ESC LEFT PWM device registered with major number %d\n", major_number);

    // 初始化 PWM 设备
    esc_left_pwm = pwm_request(PWM_CHANNEL, PWM_CHIP);
    if (IS_ERR(esc_left_pwm)) {
        printk(KERN_ALERT "ESC LEFT PWM: Failed to request PWM\n");
        device_destroy(esc_left_pwm_class, MKDEV(major_number, 0));
        class_destroy(esc_left_pwm_class);
        unregister_chrdev(major_number, DEVICE_NAME);
        return PTR_ERR(esc_left_pwm);
    }

    // 设置 PWM 周期为 20ms
    pwm_config(esc_left_pwm, PWM_DUTY_CYCLE, PWM_PERIOD); // 初始占空比为 15ms
    pwm_set_polarity(esc_left_pwm, PWM_POLARITY_NORMAL);
    pwm_enable(esc_left_pwm);

    printk(KERN_INFO "ESC LEFT PWM: PWM device initialized\n");
    return 0;
}

static void __exit pwm_exit(void) {
    // 禁用 PWM
    pwm_disable(esc_left_pwm);
    pwm_free(esc_left_pwm);

    // 删除设备文件
    device_destroy(esc_left_pwm_class, MKDEV(major_number, 0));
    class_destroy(esc_left_pwm_class);
    unregister_chrdev(major_number, DEVICE_NAME);
    printk(KERN_INFO "PWM device unregistered\n");
}

module_init(pwm_init);
module_exit(pwm_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("DeepSeek");
MODULE_DESCRIPTION("A simple PWM character device driver");

同理稍微改动就是对应侧的驱动

驱动程序编译成ko内核模块

编写Makefile文件

obj-m += esc_left_pwm_driver.o
obj-m += esc_right_pwm_driver.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

编译

make

加载内核模块

sudo insmod pwm_driver.ko

查看字符设备是否正常出现

ls /dev/esc_left_pwm
ls /dev/esc_right_pwm

编写测试程序验证驱动可行性

#include <iostream>
#include <fstream>
#include <cstdlib>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <cmath>
#include <iomanip>

#define MIN_PULSE_US 1000  // 最小脉冲宽度(us)
#define MAX_PULSE_US 2000  // 最大脉冲宽度(us)

// 将0-1的油门值转换为脉冲宽度(us)
int throttle_to_pulse(float throttle) {
    if(throttle < 0) throttle = 0;
    if(throttle > 1) throttle = 1;
    return MIN_PULSE_US + throttle * (MAX_PULSE_US - MIN_PULSE_US);
}

int main() {
    std::ofstream fd_left("/dev/esc_left_pwm");
    std::ofstream fd_right("/dev/esc_right_pwm");
    std::string buf;
    float t = 0;
    float throttle;
    
    if (!fd_left.is_open()) {
        std::cerr << "无法打开左侧ESC设备" << std::endl;
        return -1;
    }
    
    if (!fd_right.is_open()) {
        std::cerr << "无法打开右侧ESC设备" << std::endl;
        return -1;
    }

    std::cout << "开始PWM输出测试..." << std::endl;
    
    // 生成动态变化的油门值
    while(true) {
        // 使用正弦函数生成0-1之间变化的油门值
        throttle = (std::sin(t) + 1) / 2.0;
        
        // 转换为脉冲宽度并写入设备
        int pulse_width = throttle_to_pulse(throttle);
        
        fd_left << pulse_width;
        fd_left.flush();
        fd_right << pulse_width;
        fd_right.flush();
        
        std::cout << "当前油门值: " << std::fixed << std::setprecision(4) 
                 << throttle << ", 脉冲宽度: " << pulse_width << " us" << std::endl;
        
        t += 0.1;  // 增加时间步长
        usleep(100000);  // 延时100ms
    }

    // 文件流会在析构时自动关闭
    return 0;
}

将内核模块导入内核模块目录

  1. .ko 文件复制到内核模块目录

sudo cp esc_left_pwm_driver.ko /lib/modules/$(uname -r)/kernel/drivers/pwm/esc_pwm/
  1. 更新模块依赖关系

sudo depmod
  1. 创建模块加载配置文件

echo "esc_left_pwm_driver" | sudo tee /etc/modules-load.d/esc_left_pwm_driver.conf
  1. 重启系统

esc_right_pwm_driver(O) esc_left_pwm_driver(O)后面会有(O):未签名的模块,非官方模块,不影响使用

山和山不相遇,人与人要相逢