程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)

龙芯2k0300 - 智能车走马观碑组编码器驱动移植

----------------------------------------------------------------------------------------------------------------------------

开发板 :久久派开发板
eMMC8GB
DDR4512MB
u-bootu-boot 2022.04
linux6.12
rootfsbuildroot-2024.08
----------------------------------------------------------------------------------------------------------------------------

在《龙芯2k0300 - 走马观碑组第21届智能汽车竞赛软硬件设计》中我们使用了LQ_1024线方向mini编码器。

一、LQ_1024线方向mini编码器

龙邱科技mini系列编码器具有以下特点:

  • 分辨率有:A/B相正交输出256线、A/B相正交输出1024线、步进脉冲 + 方向输出512线、......;
  • 旋转速度高,最高转速可达10000rpm
  • 宽广的工作温度范围:-40℃ ~ +125℃
  • 抗扰性好。本产品采用霍尔检测技术,属于无接触检测,传感器运行不受灰尘或其它杂物影响,很好克服了基于光学检测原理的缺点;
  • 体积小巧。直径D:14mm、高H:18mm、轴径:3mm

如果你对编码器一点都不了解的话可以先参考这篇文章:《STM32F103霍尔编码器测速》。

龙邱科技提供的编码器,有带方向输出引脚和不带方向输出引脚两种,这里我使用的是正交A/B1024线方向mini编码器,也就是说我使用的这个编码器除了有A/B输出引脚外,还有单独的方向输出引脚,这样我们就不用通过A/B相信号变化的先后顺序来判断运动方向。

1.1 工作原理

编码器的线数,是说编码器转一圈输出多少个脉冲,如果一个编码器是1024线,说明这个编码器转一圈对应的信号线会输出1024个脉冲,A/B两相转一圈发出的脉冲数一样,不过存在90°相位差。

由于我使用的编码器带有方向输出引脚,因此:

  • 通过对A相脉冲计数,通过读取单位时间t的脉冲信号数量,就可以计算出转速;
  • 通过判断方向引脚的高低电平,就可以知道是正转还是反转。

编码器码盘旋转一周A/B相输出的脉冲数目为N,在时间T内统计到的有效脉冲数目为S,那么每秒转数为:

\[RPS = \frac{S}{N T} \]

这里驱动直接返回RPS,也就是每秒转数。相比RPMRPS不会再被放大60倍,更适合在智能车控制算法中直接作为速度环反馈量使用。

如果需要换算成RPM,可以在应用层自行乘以60

\[RPM = RPS \times 60 \]

1.2 硬件接线

img

1.2.1 左电机编码器

编码器一共引出6个引脚,左电机编码器与龙芯2K0300开发板的连接关系需严格对应,连接表如下:

引脚编号 编码器引脚 含义 连接的龙芯GPIO
PIN1 GND 必须可靠接地,否则可能出现显示异常 GND
PIN2 3.3~5V 直流供电 3.3V
PIN3 A A 相输出,CMOS 电平 GPIO67(SPI2_CS)
PIN4 Dir 方向输出 GPIO72(CAN2_RX)
PIN5 B B 相输出,CMOS 电平 ——
PIN6 nc 悬空 ——

电机正转编码器引脚输出电平为0,电机反转编码器引脚输出电平为1

1.2.2 右电机编码器

右电机编码器与龙芯2K0300开发板的连接关系需严格对应,连接表如下:

引脚编号 编码器引脚 含义 连接的龙芯GPIO
PIN1 GND 必须可靠接地,否则可能出现显示异常 GND
PIN2 3.3~5V 直流供电 3.3V
PIN3 A A 相输出,CMOS 电平 GPIO64(SPI2_CLK)
PIN4 Dir 方向输出 GPIO73(CAN2_TX)
PIN5 B B 相输出,CMOS 电平 ——
PIN6 nc 悬空 ——

在上一节《龙芯2k0300 - 走马观碑组PWM驱动移植》我们讲到GPIO64可以复用为PWM0GPIO67可以复用为PWM3

1.3 PWM控制器脉冲测量功能

使用编码器,需要使用到2K0300PWM控制器的脉冲测量功能,通过查阅手册《龙芯2K0300处理器用户手册》。

1.3.1 访问地址及引脚复用

PWM控制器内部寄存器的物理地址构成如下:

地址 设备 备注
0x1611_b000 PWM0~3 每个 PWM 占用 16B 寄存器配置空间:
0x1611_b000-pwm0,
0x1611_b010-pwm1,
0x1611_b020-pwm2,
0x1611_b030-pwm3

对于PWM模块,使用时要注意将对应的引脚设置为相应的功能。与PWM相关的引脚复用设置可查询PWM功能引脚复用关系,并配置相应GPIO引脚复用配置寄存器实现。

1.3.2 寄存器描述

每路控制器共有5个寄存器,本文主要使用如下几个寄存器:

名称 地址 宽度 访问 说明
low_buffer Base + 0x4 32 R/W 低脉冲缓冲寄存器
full_buffer Base + 0x8 32 R/W 脉冲周期缓冲寄存器
CTRL Base + 0xC 11 R/W 控制寄存器

此外还有Low_levelfull_pulse寄存器:这是两个内部硬件计数器,它们以系统时钟为基准进行自增,用来测量时间间隔。

  • Low_level计数器:负责测量低电平的时间;
  • full_pulse计数器:负责测量一个完整脉冲周期的时间。

low_bufferfull_buffer寄存器是两个“影子寄存器”(缓冲寄存器)。当特定事件发生时,硬件会立刻将计数器的当前值“快照”并存入对应的缓冲寄存器中,供软件读取。

1.3.3 脉冲测量

整个测量过程是由硬件自动完成的,逻辑如下:

① 初始化与启动:设置好控制寄存器后,计数器在系统时钟驱动下开始工作;

② 一次完整的脉冲周期测量(从下跳变开始):

  • 下跳变(高->低)触发:
    • 当检测到信号由高变低(下降沿)时,本轮测量开始;
    • 硬件会将full_pulse计数器的当前值,也就是上一个完整周期的长度,传送到full_buffer寄存器;
    • 然后,将Low_levelfull_pulse这两个计数器重置为1,并开始新一轮计数;
  • 低电平期间:Low_levelfull_pulse计数器从1开始同步自增;
  • 上跳变(低->高)触发:
    • 当检测到信号由低变高(上升沿)时,低电平阶段结束;
    • 硬件将此时Low_level计数器的值,也就是低电平持续时间,传送到low_buffer寄存器;
    • Low_level计数器被重置为1,但full_pulse计数器继续自增,因为它还在测量整个周期;
  • 下一个下跳变触发:
    • 当再次检测到下降沿时,表示一个完整周期结束;
    • 硬件将full_pulse计数器,此时已累加高电平和低电平时间,存入full_buffer,并重置计数器。

③ 读取结果:

  • 经过两个完整的脉冲周期后,low_bufferfull_buffer中的数据才会稳定下来,成为可靠的测量值;
  • low_buffer的计数值代表低电平的持续时间,以系统时钟周期数为单位;
  • full_buffer的计数值代表整个脉冲信号的周期,以系统时钟周期数为单位;
  • 因此,高电平时间 = full_buffer - low_buffer
1.3.4 案例

假设系统时钟频率为1MHz,即每个时钟周期为1微秒。输入一个周期为200微秒,高电平50微秒、低电平150微秒的方波;一次测量流程结束后:

  • low_buffer中读出的值应为150,代表150微秒的低电平;
  • full_buffer中读出的值应为200,代表200微秒的完整周期。

通过计算可得:

\[高电平时间 = 200 - 150 = 50\mu s \]

1.3.5 转速计算

已知参数:

  • 系统时钟频率:f_clk,单位Hz
  • 编码器每转脉冲数:PPR
  • full_buffer寄存器的值:N,表示一个完整脉冲周期对应的系统时钟周期数。

则脉冲周期为:

\[T_{pulse} = \frac{N}{f_{clk}} \]

脉冲频率计算:

\[f_{pulse} = \frac{1}{T_{pulse}} = \frac{f_{clk}}{N} \]

电机每转一圈产生PPR个脉冲,因此每秒转数为:

\[RPS = \frac{f_{pulse}}{PPR} = \frac{f_{clk}}{N \times PPR} \]

如果需要换算成每分钟转数:

\[RPM = RPS \times 60 = \frac{60 \times f_{clk}}{N \times PPR} \]

本文中的驱动直接返回RPS,不在内核驱动中乘以60。这样可以避免速度反馈数值过大,后续速度环控制也统一使用RPS作为单位。

1.3.6 PWM控制器时钟频率

龙芯2K0300CPU核心主频为1GHz,但是PWM控制器的时钟频率为200MHz,我们通过处理器用户手册可以获取到时钟结构图;

片内主要由一路外部时钟输入,120MHz固定频率时钟作为系统参考时钟输入,供片内PLL使用。芯片内部共有3个独立的PLL,其中每个PLL最多可以提供3组频率上相互依赖的时钟输出;

① 一个NODE PLL用于产生NODEGMACI2S模块时钟,该时钟为芯片的主要时钟,分为三组频率时钟;

  • 一路供CPU核、二级Cache、一二级交叉开关以及IO子网络使用;
  • 一路供GMAC模块使用;
  • 一路供I2S设备控制器使用;

② 一个DDR PLL同时产生DDRNETWORK以及USBAPBBOOTSDIO等模块的时钟;

③ 一个PIX PLL同时产生DC显示、GMAC控制器备份的内部时钟。

DDR PLL输出三个时钟,分别为:

ddr_pllclk用于内存控制器,频率范围:1.0GHz

network_pllclk用于NETWORK模块,频率范围:500MHz

dev_pllclk用 于USBAPB ( 除SDIO外其他APB低 速 接口控制器,包括UART/I2C/PWM/CAN/ADC/I2S/SPI2/SPI3/HPET/GPIO/TIMER/ENCRYPT/RTC/WDT )、BOOTConfbus/SPI0/SPI1/LocalIO控制器)、SDIO等模块,频率范围:200MHz

以上三个时钟共用一个PLL,通过设置各自的divout值来实现不同的频率输出。同样在调整其中一个模块时钟时,如果对公用PLL的倍频系数进行调整,需要注意对其他时钟的影响。

PWM控制器挂接在APB总线上,时钟频率来自时钟dev_pllclk,时钟频率为200MHz,当然我们也可以通过 clk_summary 查看时钟树;

[root@LS-GD ~]# cat /sys/kernel/debug/clk/clk_summary
                                 enable  prepare  protect                                duty  hardware                            connection
   clock                          count    count    count        rate   accuracy phase  cycle    enable   consumer                         id
---------------------------------------------------------------------------------------------------------------------------------------------
 ref_clk                             1       1        0        120000000   0          0     50000      Y   deviceless                      ref_clk
                                                                                                           clock-controller@0x16000400     ref_clk
                                                                                                           deviceless                      no_connection_id
    pix_pll_clk                      0       0        0        519272727   0          0     50000      Y      deviceless                      no_connection_id
       gmacbp_clk                    0       0        0        4088761     0          0     50000      Y         deviceless                      no_connection_id
       pix_clk                       0       0        0        4595333     0          0     50000      Y         deviceless                      no_connection_id
    ddr_pll_clk                      1       1        0        1600000000  0          0     50000      Y      deviceless                      no_connection_id
       devs_clk                      1       1        0        200000000   0          0     50000      Y         deviceless                      no_connection_id
          sdio_clk                   0       0        0        200000000   0          0     50000      Y            deviceless                      no_connection_id
          boot_clk                   0       0        0        200000000   0          0     50000      Y            deviceless                      no_connection_id
          apb_clk                    3       4        0        200000000   0          0     50000      Y            syscon@0x16124000               no_connection_id
                                                                                                                    16100800.serial                 no_connection_id
                                                                                                                    16100400.serial                 no_connection_id
                                                                                                                    16100000.serial                 no_connection_id
                                                                                                                    deviceless                      no_connection_id
          usb_clk                    0       0        0        200000000   0          0     50000      Y            deviceless                      no_connection_id
       net_clk                       0       0        0        200000000   0          0     50000      Y         deviceless                      no_connection_id
       ddr_clk                       0       0        0        800000000   0          0     50000      Y         deviceless                      no_connection_id
    node_pll_clk                     0       0        0        2000000000  0          0     50000      Y      deviceless                      no_connection_id
       i2s_clk                       0       0        0        100000000   0          0     50000      Y         deviceless                      no_connection_id
       gmac_clk                      0       0        0        125000000   0          0     50000      Y         deviceless                      no_connection_id
       iodma_clk                     0       0        0        1000000000  0          0     50000      Y         deviceless                      no_connection_id
       scache_clk                    0       0        0        1000000000  0          0     50000      Y         deviceless                      no_connection_id
       cpu_clk                       0       0        0        1000000000  0          0     50000      Y         deviceless                      cpu_clk
                                                                                                                 deviceless                      no_connection_id

clk_summary 输出可以清晰地看到龙芯2K0300的完整时钟树,我们重点关注与 PWM控制器相关的时钟。PWM控制器挂接在APB总线上,时钟源是apb_clk,频率为200 MHz

二、编码器设备驱动

2.1 编码器驱动

接下来我们在driver目录下创建子目录encoder_driver

zhengyang@ubuntu:~$ cd /opt/2k0300/loongson_2k300_lib/driver
zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/driver$ mkdir encoder_driver
zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/driver$ cd encoder_driver

目录结构如下:

zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/driver/encoder_driver$ tree .
.
├── Makefile
└── encoder_driver.c
2.1.1 encoder_driver.c

encoder_driver.c源码实现如下:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/io.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/pinctrl/consumer.h>
#include <linux/err.h>
#include <linux/bitops.h>
#include <linux/clk.h>
#include <linux/slab.h>
#include <linux/gpio/consumer.h>
#include <linux/math64.h>
#include <linux/mutex.h>
#include <linux/completion.h>

#define DEVICE_NAME "ls2k300_99pi_encoder"
#define DRIVER_VERSION "2.0"

/* 寄存器定义 */
#define PWM_ENCODER_LOW_BUFFER   0x04
#define PWM_ENCODER_FULL_BUFFER  0x08
#define PWM_ENCODER_CTRL         0x0C

/* 控制寄存器位定义 */
#define PWM_ENCODER_EN           BIT(0)   /* 计数器使能 */
#define PWM_ENCODER_CAPTE        BIT(8)   /* 测量脉冲使能 (编码器模式) */

/* 默认配置 */
#define DEFAULT_PPR              1024
#define MAX_PPR                  100000
#define MIN_PPR                  1
#define DEFAULT_CLK_FREQ         200000000UL  /* 200 MHz */
#define OVERFLOW_THRESHOLD       0xFFFFF000   /* 溢出警告阈值 */

/* ioctl 命令 */
#define ENCODER_IOC_MAGIC        'E'
#define ENCODER_GET_COUNT        _IOR(ENCODER_IOC_MAGIC, 1, struct encoder_counts)
#define ENCODER_GET_INFO         _IOR(ENCODER_IOC_MAGIC, 2, struct encoder_info)
#define ENCODER_RESET_COUNT      _IO(ENCODER_IOC_MAGIC, 3)

/*
 * 用户态读取一次编码器采样值。
 * rps:根据 PWM 捕获计数和 PPR 换算出的转速,单位 RPS。
 * direction:方向 GPIO 对应的旋转方向,1 表示正转,-1 表示反转,0 表示读取失败。
 * raw_count:PWM 捕获到的原始计数值,主要用于调试和校准。
 */
struct encoder_counts {
    long long rps;          /* 转速 (RPS),即每秒转数 */
    int direction;          /* 旋转方向 (1: 正转, -1: 反转) */
    unsigned int raw_count; /* 原始计数值 (调试用) */
};

/*
 * 编码器静态参数。
 * ppr:每转脉冲数,来自设备树 rotary-encoder,steps-per-period 或默认值。
 * clk_freq:PWM/计数器时钟频率,单位 Hz。
 * max_count:驱动认为接近溢出的最大有效计数值。
 */
struct encoder_info {
    int ppr;                /* 每转脉冲数 */
    unsigned long clk_freq; /* 时钟频率 */
    unsigned int max_count; /* 最大计数值 */
};

/* 单个编码器实例的运行上下文,每个设备树 encoder 节点对应一个实例。 */
struct encoder_device {
    void __iomem *base;      /* 定时器寄存器基地址 */
    struct gpio_desc *dir_gpiod; /* 方向 GPIO 描述符 */
    int ppr;                 /* 编码器每转脉冲数 */
    unsigned long clk_freq;  /* 时钟频率 */
    struct mutex lock;        /* 保护寄存器读写和 reset 操作 */
    struct mutex file_lock;   /* 保护 open_count/removing */
    struct completion no_users; /* remove 等待已打开 fd 释放 */
    struct miscdevice miscdev;
    struct device *dev;      /* 设备指针,用于日志 */
    char name[32];            /* misc 设备名,生命周期必须覆盖 miscdev */
    unsigned int open_count;  /* 当前打开的用户态 fd 数量 */
    bool removing;            /* 设备正在移除,阻止新的 open */
};

/* 移除全局变量 g_encoder,每个设备独立管理 */

/**
 * @brief 根据原始计数值计算转速。
 * @param enc 编码器设备上下文,必须已经完成 clk_freq 和 ppr 初始化。
 * @param raw_count PWM 捕获寄存器读出的原始计数值。
 *
 * @return 转速,单位 RPS;无有效脉冲或接近溢出时返回 0。
 */
static long long encoder_calc_rps(struct encoder_device *enc, u32 raw_count)
{
    unsigned long long temp;
    
    /*
     * raw_count 为 0 通常表示电机停转、刚启动尚未捕获到完整周期,或输入脉冲暂时缺失。
     * 这是正常运行状态,不应打印内核日志;高频 ioctl 下打印会显著拖慢系统。
     */
    if (unlikely(raw_count == 0))
        return 0;

    /* 接近硬件溢出的计数才视为异常,保留限频日志便于排查接线/时钟问题。 */
    if (unlikely(raw_count > OVERFLOW_THRESHOLD)) {
        dev_warn_ratelimited(enc->dev, "Invalid raw_count: %u\n", raw_count);
        return 0;
    }
    
    /* RPS = clk_freq / (raw_count * ppr) */
    temp = enc->clk_freq;
    do_div(temp, raw_count);
    do_div(temp, enc->ppr);
    
    return (long long)temp;
}

/**
 * @brief 读取硬件计数值。
 * @param enc 编码器设备上下文。
 * @param raw_count 输出参数,成功时写入 PWM_ENCODER_FULL_BUFFER 的原始计数值。
 *
 * @return 0 表示成功;-EINVAL 表示入参无效;-EOVERFLOW 表示硬件溢出。
 */
static int encoder_read_raw(struct encoder_device *enc, u32 *raw_count)
{
    u32 ctrl;
    
    if (unlikely(!enc || !raw_count))
        return -EINVAL;
    
    /* 检查计数器是否溢出 */
    ctrl = readl(enc->base + PWM_ENCODER_CTRL);
    if (unlikely(ctrl & BIT(6))) {  /* INT 位 */
        dev_warn_ratelimited(enc->dev, "Counter overflow detected\n");
        /* 清除溢出标志(写1清除) */
        writel(ctrl | BIT(6), enc->base + PWM_ENCODER_CTRL);
        return -EOVERFLOW;
    }
    
    *raw_count = readl(enc->base + PWM_ENCODER_FULL_BUFFER);
    return 0;
}

/**
 * @brief 获取旋转方向。
 * @param enc 编码器设备上下文。
 *
 * @return 1 表示正转;-1 表示反转;0 表示方向 GPIO 读取失败。
 */
static int encoder_get_direction(struct encoder_device *enc)
{
    int val;
    
    if (unlikely(!enc))
        return 1;
    
    /*
     * gpiod_get_value_cansleep() 返回设备树 active-low/high 转换后的逻辑值。
     * 本驱动约定逻辑 1 为正转,逻辑 0 为反转;实际电平极性由 dir-gpios 配置。
     */
    val = gpiod_get_value_cansleep(enc->dir_gpiod);
    if (val < 0) {
        dev_warn_ratelimited(enc->dev, "Failed to read direction GPIO: %d\n", val);
        return 0;
    }
    return (val > 0) ? 1 : -1;
}

/**
 * @brief 处理用户态 ioctl 命令。
 * @param filp 用户态打开 /dev/ls2k300_99pi_encoder_* 后对应的 file 对象。
 * @param cmd ioctl 命令,支持 ENCODER_GET_COUNT、ENCODER_GET_INFO、ENCODER_RESET_COUNT。
 * @param arg 用户态参数地址,GET 类命令会把结构体复制到该地址。
 *
 * @return 0 表示命令执行成功;负 errno 表示设备移除、命令非法、硬件读取失败
 * 或 copy_to_user() 失败。
 *
 * filp->private_data 在 encoder_open() 中被替换为 struct encoder_device,
 * 因此 ioctl 可以直接拿到当前 misc 设备对应的编码器实例。
 */
static long encoder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    struct encoder_device *enc = filp->private_data;
    void __user *argp = (void __user *)arg;
    int ret = 0;
    
    if (unlikely(!enc))
        return -ENODEV;

    if (_IOC_TYPE(cmd) != ENCODER_IOC_MAGIC)
        return -ENOTTY;

    mutex_lock(&enc->file_lock);
    if (enc->removing) {
        mutex_unlock(&enc->file_lock);
        return -ENODEV;
    }
    mutex_unlock(&enc->file_lock);
    
    switch (cmd) {
    case ENCODER_GET_COUNT:
    {
        struct encoder_counts cnt = {0};
        u32 raw_count;
        
        /*
         * 不能使用 local_irq_save() 包住整个读取流程,因为方向 GPIO 通过
         * gpio_get_value_cansleep() 读取,底层控制器可能睡眠。这里用 mutex
         * 只保证 reset/read 不并发破坏寄存器状态。
         */
        mutex_lock(&enc->lock);
        ret = encoder_read_raw(enc, &raw_count);
        if (ret == 0) {
            cnt.raw_count = raw_count;
            cnt.direction = encoder_get_direction(enc);
            cnt.rps = encoder_calc_rps(enc, raw_count);
        }
        mutex_unlock(&enc->lock);
        
        if (ret)
            return ret;
        
        if (copy_to_user(argp, &cnt, sizeof(cnt)))
            return -EFAULT;
        break;
    }
    
    case ENCODER_GET_INFO:
    {
        struct encoder_info info = {
            .ppr = enc->ppr,
            .clk_freq = enc->clk_freq,
            .max_count = OVERFLOW_THRESHOLD,
        };
        if (copy_to_user(argp, &info, sizeof(info)))
            return -EFAULT;
        break;
    }
    
    case ENCODER_RESET_COUNT:
        /* 重置计数器:通过重新配置实现 */
        mutex_lock(&enc->lock);
        writel(0, enc->base + PWM_ENCODER_CTRL);
        writel(PWM_ENCODER_EN | PWM_ENCODER_CAPTE, enc->base + PWM_ENCODER_CTRL);
        mutex_unlock(&enc->lock);
        dev_info(enc->dev, "Counter reset\n");
        break;
    
    default:
        return -ENOTTY;
    }
    
    return 0;
}

/**
 * @brief 打开 misc 设备。
 * @param inode VFS inode,当前未直接使用。
 * @param filp 打开的 file 对象,misc 框架会先把 private_data 指向 miscdevice。
 *
 * @return 0 表示打开成功;-ENODEV 表示设备正在卸载。
 *
 * 打开成功后把 filp->private_data 从 miscdevice 替换为 encoder_device,
 * 这样后续 ioctl/release 可以直接访问当前编码器实例。
 */
static int encoder_open(struct inode *inode, struct file *filp)
{
    struct miscdevice *miscdev = filp->private_data;
    struct encoder_device *enc = container_of(miscdev, struct encoder_device, miscdev);

    mutex_lock(&enc->file_lock);
    if (enc->removing) {
        mutex_unlock(&enc->file_lock);
        return -ENODEV;
    }
    enc->open_count++;
    mutex_unlock(&enc->file_lock);

    filp->private_data = enc;
    return 0;
}

/**
 * @brief 关闭 misc 设备。
 * @param inode VFS inode,当前未直接使用。
 * @param filp 待关闭的 file 对象,private_data 指向 encoder_device。
 *
 * @return 固定返回 0。
 *
 * 该函数减少 open_count。若 remove 已经开始并且最后一个 fd 被关闭,
 * 则唤醒 encoder_remove(),避免模块卸载后用户态仍访问已释放资源。
 */
static int encoder_release(struct inode *inode, struct file *filp)
{
    struct encoder_device *enc = filp->private_data;

    if (!enc)
        return 0;

    mutex_lock(&enc->file_lock);
    if (enc->open_count > 0)
        enc->open_count--;
    if (enc->removing && enc->open_count == 0)
        complete(&enc->no_users);
    mutex_unlock(&enc->file_lock);

    filp->private_data = NULL;
    return 0;
}

static const struct file_operations encoder_fops = {
    .owner = THIS_MODULE,
    .open = encoder_open,
    .release = encoder_release,
    .unlocked_ioctl = encoder_ioctl,
};

/**
 * @brief 获取 PWM/计数器时钟频率。
 * @param enc 编码器设备上下文,函数会写入 enc->clk_freq。
 * @param pdev platform device,用于读取设备树和 clk 框架。
 *
 * @return 当前固定返回 0。优先级为设备树 clock-frequency,其次 clk API,
 * 最后使用 DEFAULT_CLK_FREQ 兜底。
 */
static int encoder_get_clk_freq(struct encoder_device *enc, struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;
    struct clk *clk;
    u32 clk_freq = 0;
    
    /* 优先从设备树获取 */
    if (of_property_read_u32(np, "clock-frequency", &clk_freq) == 0) {
        enc->clk_freq = clk_freq;
        dev_info(&pdev->dev, "Using clock-frequency from DT: %lu Hz\n", enc->clk_freq);
        return 0;
    }
    
    /* 尝试从时钟框架获取 */
    clk = clk_get(&pdev->dev, NULL);
    if (!IS_ERR(clk)) {
        enc->clk_freq = clk_get_rate(clk);
        clk_put(clk);
        dev_info(&pdev->dev, "Using clock rate from clk API: %lu Hz\n", enc->clk_freq);
        return 0;
    }
    
    /* 使用默认值 */
    enc->clk_freq = DEFAULT_CLK_FREQ;
    dev_warn(&pdev->dev, "Using default clock frequency: %lu Hz\n", enc->clk_freq);
    return 0;
}

/**
 * @brief platform driver probe 入口。
 * @param pdev 由设备树 compatible = "ls2k300-99pi-encoder" 匹配到的设备。
 *
 * @return 0 表示当前编码器实例初始化成功;负 errno 表示设备树、内存、
 * 寄存器、GPIO 或 misc 设备注册失败。
 *
 * 每个设备树 encoder 节点都会进入一次 probe,并注册一个独立 misc 设备:
 * label = "left"  -> /dev/ls2k300_99pi_encoder_left
 * label = "right" -> /dev/ls2k300_99pi_encoder_right
 */
static int encoder_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct device_node *np = dev->of_node;
    struct pinctrl *pinctrl;
    struct encoder_device *enc;
    const char *label = NULL;
    u32 ppr;
    int dir_value;
    int ret;
    
    dev_info(dev, "Probing pulse encoder driver v%s\n", DRIVER_VERSION);
    if (!np) {
        dev_err(dev, "Missing device tree node\n");
        return -ENODEV;
    }
    
    /* 1. 分配设备结构体(每个实例独立) */
    enc = devm_kzalloc(dev, sizeof(*enc), GFP_KERNEL);
    if (!enc)
        return -ENOMEM;
    
    enc->dev = dev;
    platform_set_drvdata(pdev, enc);  /* 保存到 platform_device */
    
    /* 2. 配置 pinctrl。方向 GPIO 与 CAN/PWM 等外设复用,复用失败时不能继续加载。 */
    pinctrl = devm_pinctrl_get_select_default(dev);
    if (IS_ERR(pinctrl)) {
        ret = PTR_ERR(pinctrl);
        dev_err(dev, "Failed to select default pinctrl: %d\n", ret);
        return ret;
    }
    
    /* 3. 获取并映射寄存器资源,devm 接口会同时申请资源范围并自动释放。 */
    enc->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(enc->base))
        return PTR_ERR(enc->base);
    dev_dbg(dev, "Registers mapped at 0x%p\n", enc->base);
    
    /* 5. 获取方向 GPIO。设备树属性名为 dir-gpios,对应 gpiod consumer 名称 "dir"。 */
    enc->dir_gpiod = devm_gpiod_get(dev, "dir", GPIOD_IN);
    if (IS_ERR(enc->dir_gpiod)) {
        ret = PTR_ERR(enc->dir_gpiod);
        dev_err(dev, "Failed to get dir-gpios: %d\n", ret);
        return ret;
    }

    /*
     * probe 阶段读一次方向 GPIO,用于确认设备树复用是否真正生效。
     * 若外部已拉高但这里仍为 0,应优先检查 pinctrl/DTB/物理引脚编号。
     */
    dir_value = gpiod_get_value_cansleep(enc->dir_gpiod);
    if (dir_value < 0) {
        dev_err(dev, "Failed to read initial direction GPIO: %d\n", dir_value);
        return dir_value;
    }
    dev_info(dev, "Direction GPIO configured, initial logic value=%d\n", dir_value);
    
    /* 6. 获取 PPR(带范围检查) */
    if (of_property_read_u32(np, "rotary-encoder,steps-per-period", &ppr) != 0)
        ppr = DEFAULT_PPR;
    
    if (ppr < MIN_PPR || ppr > MAX_PPR) {
        dev_warn(dev, "Invalid PPR %u, clamping to [%d,%d]\n", 
                 ppr, MIN_PPR, MAX_PPR);
        ppr = clamp_val(ppr, MIN_PPR, MAX_PPR);
    }
    enc->ppr = (int)ppr;
    dev_info(dev, "PPR = %d\n", enc->ppr);
    
    /* 7. 获取时钟频率 */
    ret = encoder_get_clk_freq(enc, pdev);
    if (ret)
        dev_warn(dev, "Failed to get clock frequency, using default\n");

    mutex_init(&enc->lock);
    mutex_init(&enc->file_lock);
    init_completion(&enc->no_users);
    
    /* 8. 配置定时器为编码器模式 */
    writel(0, enc->base + PWM_ENCODER_CTRL);  /* 先禁用 */
    writel(PWM_ENCODER_EN | PWM_ENCODER_CAPTE, 
           enc->base + PWM_ENCODER_CTRL);
    dev_dbg(dev, "Timer configured in encoder mode\n");
    
    /* 9. 创建唯一的设备名称 */
    if (!of_property_read_string(np, "label", &label))
        snprintf(enc->name, sizeof(enc->name), DEVICE_NAME "_%s", label);
    else
        snprintf(enc->name, sizeof(enc->name), DEVICE_NAME "_%s", dev_name(dev));
    
    /* 10. 注册 misc 设备 */
    enc->miscdev.minor = MISC_DYNAMIC_MINOR;
    enc->miscdev.name = enc->name;
    enc->miscdev.fops = &encoder_fops;
    
    ret = misc_register(&enc->miscdev);
    if (ret) {
        dev_err(dev, "Failed to register misc device: %d\n", ret);
        /* 清理硬件配置 */
        writel(0, enc->base + PWM_ENCODER_CTRL);
        return ret;
    }
    
    dev_info(dev, "Encoder driver loaded successfully, device /dev/%s (PPR=%d, clk=%luHz)\n",
             enc->name, enc->ppr, enc->clk_freq);
    return 0;
}

/**
 * @brief platform driver remove 入口。
 * @param pdev 正在解绑的 platform device。
 *
 * 注销 misc 设备、阻止新的 open/ioctl,等待已有 fd 关闭后关闭硬件计数器。
 */
static void encoder_remove(struct platform_device *pdev)
{
    struct encoder_device *enc = platform_get_drvdata(pdev);
    if (enc) {
        misc_deregister(&enc->miscdev);
        mutex_lock(&enc->file_lock);
        enc->removing = true;
        if (enc->open_count == 0)
            complete(&enc->no_users);
        mutex_unlock(&enc->file_lock);
        wait_for_completion(&enc->no_users);
        writel(0, enc->base + PWM_ENCODER_CTRL);
        dev_info(&pdev->dev, "Encoder driver removed\n");
    }
}

static const struct of_device_id encoder_of_match[] = {
    { .compatible = "ls2k300-99pi-encoder" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, encoder_of_match);

static struct platform_driver encoder_driver = {
    .probe = encoder_probe,
    .remove = encoder_remove,
    .driver = {
        .name = "ls2k300_99pi_encoder",
        .of_match_table = encoder_of_match,
    },
};

module_platform_driver(encoder_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("zhengyang");
MODULE_DESCRIPTION("LS2K300 99Pi pulse/direction encoder driver");
MODULE_VERSION(DRIVER_VERSION);
2.1.2 Makefile
# 内核源码路径
KERNELDIR ?= /opt/2k0300/build-2k0300/workspace/linux-6.12
# 当前驱动目录
PWD := $(shell pwd)
# 交叉编译工具链
CROSS_COMPILE ?= loongarch64-linux-gnu-
# 架构
ARCH := loongarch

# 编译产物目录
BUILD_DIR := build
KO_DIR := ko

# 编译目标
obj-m := ls2k300_99pi_encoder.o
ls2k300_99pi_encoder-y := encoder_driver.o

all: prepare compile move_files

prepare:
	@mkdir -p $(BUILD_DIR) $(KO_DIR)
	@echo "encoder_driver: prepare build directories"

compile:
	@echo "encoder_driver: build kernel module"
	make -C $(KERNELDIR) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) M=$(PWD) modules

move_files:
	@find . -type f \
		-not -path "./$(BUILD_DIR)/*" -not -path "./$(KO_DIR)/*" \
		\( -name '*.o' -o -name '*.mod' -o -name '*.mod.o' -o -name '*.mod.c' -o -name '.*.cmd' -o -name 'modules.order' -o -name 'Module.symvers' \) \
		! -name '*.ko' -exec mv -t $(BUILD_DIR)/ {} +
	@find . -type f \
		-not -path "./$(BUILD_DIR)/*" -not -path "./$(KO_DIR)/*" \
		-name '*.ko' -exec cp -f {} $(KO_DIR)/ \; -exec rm -f {} \;

clean:
	make -C $(KERNELDIR) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) M=$(PWD) clean
	rm -rf *.ko *.o *.mod *.mod.o *.mod.c *.symvers *.order .*.cmd .tmp_versions build ko

.PHONY: all prepare compile move_files clean

2.2 新增设备节点

zhengyang@ubuntu:~$ cd /opt/2k0300/build-2k0300/workspace/linux-6.12
2.2.1 encoder_leftencoder_right

修改arch/loongarch/boot/dts/ls2k300_99pi.dtsi,在根节点下新增如下节点:

encoder_left {
    compatible = "ls2k300-99pi-encoder";
    label = "left";
    pinctrl-names = "default";
	pinctrl-0 = <&pwm3_mux_m0>;
	
    /* 方向引脚:PIN4 (Dir) 连接到 GPIO72 (CAN2_RX) */
    dir-gpios = <&gpio 72 GPIO_ACTIVE_LOW>;

    /* 左电机编码器的 A 相信号连接到 PWM3 控制器 */
    reg = <0 0x1611b030 0 0x10>;

    /* 编码器每转脉冲数 */
    rotary-encoder,steps-per-period = <1024>;
};

encoder_right {
    compatible = "ls2k300-99pi-encoder";
    label = "right";
    pinctrl-names = "default";
	pinctrl-0 = <&pwm0_mux_m0>;

    /* 方向引脚:PIN4 (Dir) 连接到 GPIO73 (CAN2_TX) */
    dir-gpios = <&gpio 73 GPIO_ACTIVE_LOW>;

    /* 右电机编码器的 A 相信号连接到 PWM0 控制器 */
    reg = <0 0x1611b000 0 0x10>;

    /* 编码器每转脉冲数 */
    rotary-encoder,steps-per-period = <1024>;
};

同时禁用CAN2:

&can2 {
        status = "disabled";   // GPIO72/73 用作编码器方向输入,不能同时启用 CAN2
};
2.2.2 pwm0_mux_m0pwm3_mux_m0

pwm0_mux_m0pwm3_mux_m0定义在arch/loongarch/boot/dts/loongson-2k0300.dtsi

pinmux: pinmux@16000490 {
    ......
    pwm0_pins: pwm0-pins {
        pwm0_mux_m0: pinmux_G64_as_pwm0 {
            pinctrl-single,bits = <0x10 0x00000001 0x00000003>;
        };
    };

    pwm3_pins: pwm3-pins {
        pwm3_mux_m0: pinmux_G67_as_pwm3 {
            pinctrl-single,bits = <0x10 0x00000040 0x000000c0>;
        };
    };
    ......
}

三、应用程序

接下来我们在example目录下创建子目录encoder_app

zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/example$ mkdir encoder_app
zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/example$ cd encoder_app

目录结构如下:

zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/example/encoder_app$ tree .
.
├── main.c
└── Makefile

3.1 main.c

应用程序中的结构体也同步改为RPS

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

#define ENCODER_IOC_MAGIC 'E'
#define ENCODER_GET_COUNT _IOR(ENCODER_IOC_MAGIC, 1, struct encoder_counts)
#define ENCODER_GET_INFO  _IOR(ENCODER_IOC_MAGIC, 2, struct encoder_info)
#define ENCODER_RESET_COUNT _IO(ENCODER_IOC_MAGIC, 3)

struct encoder_counts {
    long long rps;          /* 转速 (RPS) */
    int direction;          /* 方向 (1: 正转, -1: 反转) */
    unsigned int raw_count; /* 原始计数值 (调试用) */
};

struct encoder_info {
    int ppr;                /* 每转脉冲数 */
    unsigned long clk_freq; /* 时钟频率 */
    unsigned int max_count; /* 最大计数值 */
};

/*
 * @brief 打印编码器测试程序的命令行帮助。
 * @param prog 当前程序名,通常来自 argv[0]。
 * @return 无。
 */
static void print_usage(const char *prog)
{
    printf("Usage: %s [options]\n", prog);
    printf("Options:\n");
    printf("  -h, --help      Show this help\n");
    printf("  -i, --info      Show encoder info\n");
    printf("  -r, --reset     Reset encoder counter\n");
    printf("  -s, --sample N  Set sample interval (ms), default 50\n");
    printf("  -c, --count N   Number of samples, default infinite\n");
}

/*
 * @brief 编码器测试程序入口。
 * @param argc 命令行参数数量。
 * @param argv 命令行参数数组。
 * @return 0 表示执行成功,负数表示打开设备或 ioctl 操作失败。
 */
int main(int argc, char *argv[])
{
    int fd;
    int sample_ms = 50;
    int max_samples = -1;
    int show_info = 0;
    int reset_counter = 0;
    
    /* 简单命令行参数解析 */
    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
            print_usage(argv[0]);
            return 0;
        } else if (strcmp(argv[i], "-i") == 0 || strcmp(argv[i], "--info") == 0) {
            show_info = 1;
        } else if (strcmp(argv[i], "-r") == 0 || strcmp(argv[i], "--reset") == 0) {
            reset_counter = 1;
        } else if ((strcmp(argv[i], "-s") == 0 || strcmp(argv[i], "--sample") == 0) && i+1 < argc) {
            sample_ms = atoi(argv[++i]);
            if (sample_ms <= 0) sample_ms = 50;
        } else if ((strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--count") == 0) && i+1 < argc) {
            max_samples = atoi(argv[++i]);
        }
    }
    
    /* 打开设备 */
    fd = open("/dev/ls2k300_99pi_encoder_left", O_RDWR);
    if (fd < 0) {
        fprintf(stderr, "Failed to open /dev/ls2k300_99pi_encoder_left: %s\n", strerror(errno));
        fprintf(stderr, "Make sure the driver is loaded: insmod ls2k300_99pi_encoder.ko\n");
        return -1;
    }
    
    /* 显示编码器信息 */
    if (show_info) {
        struct encoder_info info;
        if (ioctl(fd, ENCODER_GET_INFO, &info) == 0) {
            printf("Encoder Info:\n");
            printf("  PPR (pulses per revolution): %d\n", info.ppr);
            printf("  Clock frequency: %lu Hz\n", info.clk_freq);
            printf("  Max count: %u\n", info.max_count);
        } else {
            perror("ioctl GET_INFO");
        }
    }
    
    /* 重置计数器 */
    if (reset_counter) {
        if (ioctl(fd, ENCODER_RESET_COUNT) == 0) {
            printf("Counter reset successfully\n");
        } else {
            perror("ioctl RESET_COUNT");
        }
    }
    
    /* 如果只是查看信息或重置,不进行采样 */
    if (show_info || reset_counter) {
        close(fd);
        return 0;
    }
    
    /* 采样循环 */
    struct encoder_counts cnt;
    long long last_rps = 0;
    int sample_count = 0;
    
    printf("Sampling encoder at %d ms intervals (Ctrl+C to stop)\n", sample_ms);
    printf("%-10s %-10s %-10s %-10s %-10s\n", 
           "Sample", "RPS", "Direction", "Raw", "Delta");
    printf("--------------------------------------------------------\n");
    
    while (max_samples < 0 || sample_count < max_samples) {
        if (ioctl(fd, ENCODER_GET_COUNT, &cnt) < 0) {
            perror("ioctl GET_COUNT");
            break;
        }
        
        long long delta = cnt.rps - last_rps;
        last_rps = cnt.rps;
        
        printf("%-10d %-10lld %-10d %-10u %-10lld\n",
               sample_count++, cnt.rps, cnt.direction,
               cnt.raw_count, delta);
        
        usleep(sample_ms * 1000);
    }
    
    close(fd);
    return 0;
}

3.2 Makefile

TOOLCHAIN_DIR ?= ../../cross_lib/loongarch64-linux-gnu-gcc13.3/bin
CROSS_COMPILE ?= $(TOOLCHAIN_DIR)/loongarch64-linux-gnu-
ifeq ($(origin CC),default)
CC := $(CROSS_COMPILE)gcc
endif
CFLAGS ?= -Wall -Wextra -O2
TARGET := main

all:
	$(CC) $(CFLAGS) -o $(TARGET) main.c

clean:
	rm -rf *.o $(TARGET)

.PHONY: all clean

3.3 编译应用程序

zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/example/encoder_app$ make
zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/example/encoder_app$ ll
-rwxrwxr-x 1 zhengyang zhengyang 20680  4月  4 11:02 main*
-rw-rw-r-- 1 zhengyang zhengyang   863  4月  4 11:01 main.c
-rw-rw-r-- 1 zhengyang zhengyang    71  4月  4 11:02 Makefile

四、测试

4.1 烧录设备树

4.1.1 编译设备树

如果需要单独编译设备树,在linux内核根目录执行如下命令:

zhengyang@ubuntu:~$ cd /opt/2k0300/build-2k0300/workspace/linux-6.12
zhengyang@ubuntu:/opt/2k0300/build-2k0300/workspace/linux-6.12$ source ../set_env.sh && make dtbs V=1
4.1.2 更新设备树

将设备树拷贝到久久派的/opt目录:

zhengyang@ubuntu:/opt/2k0300/build-2k0300/workspace/linux-6.12$ scp arch/loongarch/boot/dts/ls2k300_99pi_wifi.dtb root@172.23.34.188:/opt

在久久派使用dd命令烧写设备树到SPI Nor Flashdtb分区:

[root@LS-GD opt]# dd if=/opt/ls2k300_99pi_wifi.dtb of=/dev/mtdblock3 bs=1
22124+0 records in
22124+0 records out
22124 bytes (22 kB, 22 KiB) copied, 0.500342 s, 44.2 kB/s

[root@LS-GD opt]# reboot

4.2 安装驱动

由于我们并没有将驱动源码放到内核中,因此需要单独编译安装。

4.2.1 编译驱动

ubuntu宿主机重新编译驱动:

zhengyang@ubuntu:~$ cd /opt/2k0300/loongson_2k300_lib/driver/encoder_driver
zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/driver/encoder_driver$ make

拷贝驱动到开发板:

zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/driver/encoder_driver$ scp ./ko/ls2k300_99pi_encoder.ko root@172.23.34.188:/opt
4.2.2 安装驱动

久久派安装驱动:

[root@LS-GD opt]# insmod ls2k300_99pi_encoder.ko
[root@LS-GD opt]# lsmod
Module                  Size  Used by
ls2k300_99pi_encoder    65536  0

查看内核输出信息:

[root@LS-GD opt]# dmesg | grep encoder
[   36.932929] ls2k300_99pi_encoder: loading out-of-tree module taints kernel.
[   37.245624] ls2k300_99pi_encoder 1611b030.encoder_left: Probing pulse encoder driver v2.0
[   37.463565] ls2k300_99pi_encoder 1611b030.encoder_left: PPR = 1024
[   37.600145] ls2k300_99pi_encoder 1611b030.encoder_left: Using default clock frequency: 200000000 Hz
[   37.703193] ls2k300_99pi_encoder 1611b030.encoder_left: Encoder driver loaded successfully, device /dev/ls2k300_99pi_encoder_left (PPR=1024, clk=200000000Hz)
[   37.900289] ls2k300_99pi_encoder 1611b000.encoder_right: Probing pulse encoder driver v2.0
[   38.071276] ls2k300_99pi_encoder 1611b000.encoder_right: PPR = 1024
[   38.158601] ls2k300_99pi_encoder 1611b000.encoder_right: Using default clock frequency: 200000000 Hz
[   38.261237] ls2k300_99pi_encoder 1611b000.encoder_right: Encoder driver loaded successfully, device /dev/ls2k300_99pi_encoder_right (PPR=1024, clk=200000000Hz)
4.2.3 启动加载

如果希望系统启动自动加载该驱动,需要将驱动拷贝到/usr/lib/modules/$(uname -r)/

[root@LS-GD ~]# mkdir -p /usr/lib/modules/$(uname -r)/
[root@LS-GD ~]# cd /usr/lib/modules/$(uname -r)/

拷贝驱动:

zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/driver/encoder_driver$ scp ./ko/ls2k300_99pi_encoder.ko root@172.23.34.188:/usr/lib/modules/6.12.0.lsgd+

更新模块依赖关系:

[root@LS-GD 6.12.0.lsgd+]# depmod -a

如果出现如下提示:

depmod: WARNING: could not open modules.builtin.modinfo at /lib/modules/6.12.0.lsgd+: No such file or directory

一般不影响当前这个外部模块的加载。这个警告表示目标板的/lib/modules/$(uname -r)/目录下缺少内核内建模块信息文件,depmod仍然会生成当前外部模块所需的modules.dep等文件。只要后续modprobe ls2k300_99pi_encoder可以正常加载,驱动就可以正常使用。

重启后查看设备节点:

[root@LS-GD ~]# ls /dev/ls2k300_99pi_encoder_* -l
crw------- 1 root root 10, 125 Jul 24 20:49 /dev/ls2k300_99pi_encoder_left
crw------- 1 root root 10, 124 Jul 24 20:49 /dev/ls2k300_99pi_encoder_right

4.3 应用程序测试

这里参考《龙芯2k0300 - 走马观碑组PWM驱动移植》配置GPIO65引脚输出频率20KHz、占空比50%PWM信号,然后通过GPIO67引脚测量PWM信号周期。

4.3.1 PWM信号输出

pwmchip0/export文件写入0,导出PWM通道:

[root@LS-GD ~]# echo 0 > /sys/class/pwm/pwmchip0/export

设置pwm0输出频率20KHz(如20kHz50000ns),占空比50%50%25000ns),极性为正极性:

[root@LS-GD ~]# echo 50000 > /sys/class/pwm/pwmchip0/pwm0/period
[root@LS-GD ~]# echo 25000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle
[root@LS-GD ~]# echo normal > /sys/class/pwm/pwmchip0/pwm0/polarity
[root@LS-GD ~]# echo 1 > /sys/class/pwm/pwmchip0/pwm0/enable
4.3.2 脉冲测量

GPIO65GPIO67通过杜邦线连接起来,接着进行测试:

[root@LS-GD opt]# ./main
Sampling encoder at 50 ms intervals (Ctrl+C to stop)
Sample     RPS        Direction  Raw        Delta
--------------------------------------------------------
0          19          1         10000      19
1          19          1         10000      0
2          19          1         10000      0
3          19          1         10000      0
4          19          1         10000      0
5          19          1         10000      0

这里Raw输出为什么是10000呢?

PWM输出周期为50000 ns,硬件测量到的full_buffer = 10000

\[理论周期计数 = 50000ns \times \frac{200MHz}{1GHz} = 50000 \times 0.2 = 10000 \]

此时脉冲频率为:

\[f_{pulse} = \frac{200000000}{10000} = 20000Hz \]

编码器配置的PPR = 1024,因此:

\[RPS = \frac{20000}{1024} \approx 19.53 \]

由于当前驱动返回的是整数,所以测试程序中看到的结果约为19 RPS。如果换算成RPM

\[RPM = 19.53 \times 60 \approx 1171.8 \]

这也对应旧版驱动中看到的1171 RPM。新版驱动不再乘以60,直接返回RPS

五、代码下载

loongson_2k300_lib

参考文章

[1] 龙邱科技Mini编码器.pdf

[2] 龙芯2K0300数据手册

[3] 龙芯2K0300处理器用户手册

posted @ 2026-04-04 10:34  大奥特曼打小怪兽  阅读(127)  评论(0)    收藏  举报
如果有任何技术小问题,欢迎大家交流沟通,共同进步