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

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

在《龙芯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,那么转速为:

\[n = \frac{S}{NT} \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 悬空 ——
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微秒。

1.3.5 转速计算

已知参数:

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

则脉冲周期:

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

脉冲频率计算:

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

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

\[转速 RPM = \frac{f_{pulse}}{PPR} × 60 = \frac{60 \times f_{clk}}{N \times PPR} \]

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/encoder_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/of_gpio.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>

#define DEVICE_NAME "pulse_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)

struct encoder_counts {
    long long count;        /* 脉冲计数 (已转换为 RPM) */
    int direction;          /* 旋转方向 (1: 正转, -1: 反转) */
    unsigned int raw_count; /* 原始计数值 (调试用) */
};

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

struct encoder_device {
    void __iomem *base;      /* 定时器寄存器基地址 */
    int dir_gpio;            /* 方向引脚 */
    int ppr;                 /* 编码器每转脉冲数 */
    unsigned long clk_freq;  /* 时钟频率 */
    struct miscdevice miscdev;
    struct device *dev;      /* 设备指针,用于日志 */
};

/**
 * encoder_calc_rpm() - 根据原始计数值计算转速
 * @enc: 编码器设备
 * @raw_count: 硬件计数值
 *
 * Return: 转速 (RPM)
 */
static long long encoder_calc_rpm(struct encoder_device *enc, u32 raw_count)
{
    unsigned long long temp;
    
    /* 防止除零和溢出 */
    if (unlikely(raw_count == 0 || raw_count > OVERFLOW_THRESHOLD)) {
        dev_warn_ratelimited(enc->dev, "Invalid raw_count: %u\n", raw_count);
        return 0;
    }
    
    /* RPM = (60 * clk_freq) / (raw_count * ppr) */
    temp = 60ULL * enc->clk_freq;
    do_div(temp, raw_count);
    do_div(temp, enc->ppr);
    
    return (long long)temp;
}

/**
 * encoder_read_raw() - 读取硬件计数值
 * @enc: 编码器设备
 * @raw_count: 输出原始计数值
 *
 * Return: 0 成功,负数表示错误
 */
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;
}

/**
 * encoder_get_direction() - 获取旋转方向
 * @enc: 编码器设备
 *
 * Return: 1 正转, -1 反转
 */
static int encoder_get_direction(struct encoder_device *enc)
{
    int val;
    
    if (unlikely(!enc))
        return 1;
    
    val = gpio_get_value_cansleep(enc->dir_gpio);
    return (val > 0) ? 1 : -1;
}

/* ioctl 处理 - 通过 filp->private_data 获取设备实例 */
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;
    unsigned long flags;
    int ret = 0;
    
    if (unlikely(!enc))
        return -ENODEV;
    
    switch (cmd) {
    case ENCODER_GET_COUNT:
    {
        struct encoder_counts cnt = {0};
        u32 raw_count;
        
        local_irq_save(flags);
        ret = encoder_read_raw(enc, &raw_count);
        if (ret == 0) {
            cnt.raw_count = raw_count;
            cnt.direction = encoder_get_direction(enc);
            cnt.count = encoder_calc_rpm(enc, raw_count);
        }
        local_irq_restore(flags);
        
        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:
        /* 重置计数器:通过重新配置实现 */
        local_irq_save(flags);
        writel(0, enc->base + PWM_ENCODER_CTRL);
        writel(PWM_ENCODER_EN | PWM_ENCODER_CAPTE, enc->base + PWM_ENCODER_CTRL);
        local_irq_restore(flags);
        dev_info(enc->dev, "Counter reset\n");
        break;
    
    default:
        return -ENOTTY;
    }
    
    return 0;
}

/* open 函数:将设备实例保存到 filp->private_data */
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);
    filp->private_data = enc;
    return 0;
}

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

/* 获取时钟频率 */
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;
}

/* 驱动入口 */
static int encoder_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct device_node *np = dev->of_node;
    struct resource *res;
    struct pinctrl *pinctrl;
    struct encoder_device *enc;
    char devname[32];
    int ret;
    
    dev_info(dev, "Probing pulse encoder driver v%s\n", DRIVER_VERSION);
    
    /* 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(可选,失败不致命) */
    pinctrl = devm_pinctrl_get_select_default(dev);
    if (IS_ERR(pinctrl)) {
        dev_warn(dev, "Failed to get pinctrl: %ld, assuming already configured\n", 
                 PTR_ERR(pinctrl));
    }
    
    /* 3. 获取寄存器资源 */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res) {
        dev_err(dev, "Missing memory resource\n");
        return -ENOENT;
    }
    
    /* 4. 映射寄存器地址 */
    enc->base = devm_ioremap(dev, res->start, resource_size(res));
    if (!enc->base) {
        dev_err(dev, "Failed to ioremap memory\n");
        return -ENOMEM;
    }
    dev_dbg(dev, "Registers mapped at 0x%p (size=0x%llu)\n", 
            enc->base, (unsigned long long)resource_size(res));
    
    /* 5. 获取方向 GPIO */
    enc->dir_gpio = of_get_named_gpio(np, "dir-gpios", 0);
    if (!gpio_is_valid(enc->dir_gpio)) {
        dev_err(dev, "Invalid or missing dir-gpios\n");
        return -EINVAL;
    }
    
    ret = devm_gpio_request(dev, enc->dir_gpio, "encoder_dir");
    if (ret) {
        dev_err(dev, "Failed to request dir GPIO %d: %d\n", 
                enc->dir_gpio, ret);
        return ret;
    }
    
    ret = gpio_direction_input(enc->dir_gpio);
    if (ret) {
        dev_err(dev, "Failed to set dir GPIO %d as input: %d\n", 
                enc->dir_gpio, ret);
        return ret;
    }
    dev_dbg(dev, "Direction GPIO %d configured\n", enc->dir_gpio);
    
    /* 6. 获取 PPR(带范围检查) */
    if (of_property_read_u32(np, "rotary-encoder,steps-per-period", &enc->ppr) != 0)
        enc->ppr = DEFAULT_PPR;
    
    if (enc->ppr < MIN_PPR || enc->ppr > MAX_PPR) {
        dev_warn(dev, "Invalid PPR %u, clamping to [%d,%d]\n", 
                 enc->ppr, MIN_PPR, MAX_PPR);
        enc->ppr = clamp(enc->ppr, MIN_PPR, MAX_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");
    
    /* 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. 创建唯一的设备名称 */
    const char *label = of_get_property(np, "label", NULL);
    if (label)
        snprintf(devname, sizeof(devname), "pulse_encoder_%s", label);
    else
        snprintf(devname, sizeof(devname), "pulse_encoder_%s", dev_name(dev));
    
    /* 10. 注册 misc 设备 */
    enc->miscdev.minor = MISC_DYNAMIC_MINOR;
    enc->miscdev.name = devname;
    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",
             devname, enc->ppr, enc->clk_freq);
    return 0;
}

static void encoder_remove(struct platform_device *pdev)
{
    struct encoder_device *enc = platform_get_drvdata(pdev);
    if (enc) {
        misc_deregister(&enc->miscdev);
        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 = "pulse-dir-encoder" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, encoder_of_match);

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

module_platform_driver(encoder_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Pulse/Direction Encoder Driver using PWM Timer (Multi-instance)");
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
SRC_DIR:=src


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

# 编译规则
ll: prepare compile move_files

# 提前创建目录
prepare:
	@mkdir -p $(BUILD_DIR) $(KO_DIR)
	@echo "📂 \033[32m已创建目录\033[0m"

compile:
	@echo "📂 开始编译驱动模块"
	# 关键:指定内核路径并传递正确的编译参数
	make -C $(KERNELDIR) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) M=$(PWD) modules

# 移动文件
move_files:
	# 移动中间文件(.o/.mod.o/.mod.c/.cmd等)到build目录
	@find . -type f \
                -not -path "./$(BUILD_DIR)/*" -not -path "./$(KO_DIR)/*" \
                \( -name '*.o' -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)/ {} +                
	# 移动 ko 模块到 ko 目录
	@find . -type f \
                -not -path "./$(BUILD_DIR)/*" -not -path "./$(KO_DIR)/*" \
                -name '*.ko' -exec mv -t $(KO_DIR)/ {} +

clean:
	@echo "🧹 清理编译产物"
	make -C $(KERNELDIR) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) M=$(PWD) clean
	rm -rf *.ko *.o *.mod.o *.mod.c *.symvers *.order .*.cmd .tmp_versions build ko

2.2 新增设备节点

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

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

encoder_left {
    compatible = "pulse-dir-encoder";
    label = "left";
    pinctrl-names = "default";
    // 请根据你的硬件连接,正确配置引脚复用
    pinctrl-0 = <&pwm3_mux_m0>;

    // 方向引脚:PIN4 (Dir) 连接到 GPIO72 (CAN2_RX)
    dir-gpios = <&gpio 72 GPIO_ACTIVE_HIGH>;

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

    // 可选:编码器每转脉冲数 (PPR),应用程序会用到
    rotary-encoder,steps-per-period = <1024>;
};

encoder_right {
    compatible = "pulse-dir-encoder";
    label = "right";
    pinctrl-names = "default";
    // 请根据你的硬件连接,正确配置引脚复用
    pinctrl-0 = <&pwm0_mux_m0>;

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

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

    // 可选:编码器每转脉冲数 (PPR),应用程序会用到
    rotary-encoder,steps-per-period = <1024>;
};
2.2.2 pwm0_mux_m0&pwm3_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>;
            };
            pwm0_mux_m1: pinmux_G86_as_pwm0 {
                    pinctrl-single,bits = <0x14 0x00002000 0x00003000>;
            };
            pwm0_mux_m2: pinmux_G102_as_pwm0 {
                    pinctrl-single,bits = <0x18 0x00003000 0x00003000>;
            };
    };
    pwm3_pins: pwm3-pins {
            pwm3_mux_m0: pinmux_G67_as_pwm3 {
                    pinctrl-single,bits = <0x10 0x00000040 0x000000c0>;
            };
            pwm3_mux_m1: pinmux_G89_as_pwm3 {
                    pinctrl-single,bits = <0x14 0x00080000 0x000c0000>;
            };
            pwm3_mux_m2: pinmux_G105_as_pwm3 {
                    pinctrl-single,bits = <0x18 0x000c0000 0x000c0000>;
            };
    };
	......
}	

三、应用程序

接下来我们在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

#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 count;        /* 转速 (RPM) */
    int direction;          /* 方向 (1: 正转, -1: 反转) */
    unsigned int raw_count; /* 原始计数值 (调试用) */
};

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

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");
}

int main(int argc, char *argv[])
{
    int fd;
    int sample_ms = 50;
    int max_samples = -1;
    int show_info = 0;
    int reset_counter = 0;
    int opt;
    
    /* 简单命令行参数解析 */
    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/pulse_encoder_left", O_RDWR);
    if (fd < 0) {
        fprintf(stderr, "Failed to open /dev/pulse_encoder_left: %s\n", strerror(errno));
        fprintf(stderr, "Make sure the driver is loaded: insmod encoder_driver.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_count = 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", "RPM", "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.count - last_count;
        last_count = cnt.count;
        
        printf("%-10d %-10lld %-10d %-10u %-10lld\n", 
               sample_count++, cnt.count, cnt.direction, 
               cnt.raw_count, delta);
        
        usleep(sample_ms * 1000);
    }
    
    close(fd);
    return 0;
}

3.2 Makefile

all:
	loongarch64-linux-gnu-gcc -o main main.c
clean:
	rm -rf *.o main

3.3 编译应用程序

zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/example/encoder_app$ make
loongarch64-linux-gnu-gcc -o main main.c
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 arch/loongarch/boot/dts/ls2k300_99pi_wifi.dts  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宿主接重新编译驱动:

hengyang@ubuntu:~$ cd /opt/2k0300/loongson_2k300_lib/driver/encoder_driver
zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/driver/encoder_driver$ make
📂 已创建目录
📂 开始编译驱动模块
# 关键:指定内核路径并传递正确的编译参数
make -C /opt/2k0300/build-2k0300/workspace/linux-6.12 ARCH=loongarch CROSS_COMPILE=loongarch64-linux-gnu- M=/opt/2k0300/loongson_2k300_lib/driver/encoder_driver modules
make[1]: 进入目录“/opt/2k0300/build-2k0300/workspace/linux-6.12”
  CC [M]  /opt/2k0300/loongson_2k300_lib/driver/encoder_driver/encoder_driver.o
  LD [M]  /opt/2k0300/loongson_2k300_lib/driver/encoder_driver/encoder.o
  MODPOST /opt/2k0300/loongson_2k300_lib/driver/encoder_driver/Module.symvers
  CC [M]  /opt/2k0300/loongson_2k300_lib/driver/encoder_driver/encoder.mod.o
  CC [M]  /opt/2k0300/loongson_2k300_lib/driver/encoder_driver/.module-common.o
  LD [M]  /opt/2k0300/loongson_2k300_lib/driver/encoder_driver/encoder.ko
make[1]: 离开目录“/opt/2k0300/build-2k0300/workspace/linux-6.12”
# 移动中间文件(.o/.mod.o/.mod.c/.cmd等)到build目录
# 移动 ko 模块到 ko 目录

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

久久派安装驱动:

[root@LS-GD opt]# ls encoder.ko -l
-rw-r--r-- 1 root root 21104 Jul 24 22:23 encoder.ko

[root@LS-GD opt]# insmod encoder.ko

[root@LS-GD opt]# lsmod
Module                  Size  Used by
encoder                65536  0

查看内核输出信息:

[root@LS-GD opt]# dmesg
......
[   82.115537] pulse_dir_encoder 1611b030.encoder_left: Probing pulse encoder driver v2.0
[   82.123582] pulse_dir_encoder 1611b030.encoder_left: PPR = 1024
[   82.130708] pulse_dir_encoder 1611b030.encoder_left: Using default clock frequency: 200000000 Hz
[   82.139868] pulse_dir_encoder 1611b030.encoder_left: Encoder driver loaded successfully, device /dev/pulse_encoder_left (PPR=1024, clk=200000000Hz)
[   82.167486] pulse_dir_encoder 1611b000.encoder_right: Probing pulse encoder driver v2.0
[   82.193178] pulse_dir_encoder 1611b000.encoder_right: PPR = 1024
[   82.202332] pulse_dir_encoder 1611b000.encoder_right: Using default clock frequency: 200000000 Hz
[   82.214096] pulse_dir_encoder 1611b000.encoder_right: Encoder driver loaded successfully, device /dev/pulse_encoder_right (PPR=1024, clk=200000000Hz)
4.2.3 启动加载

如果希望系统启动自动加载该驱动,我们需要将驱动拷贝到/usr/lib/modules/6.12.0.lsgd+/

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

内核启动的时候,使用/sbin/modprobe加载/lib/modules/{uname -r}下面的模块,modules.order文件指定了模块的加载顺序,因此我们需要拷贝以下文件到久久派;

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

此时开发板/usr/lib/modules/6.12.0.lsgd+路径下:

[root@LS-GD 6.12.0.lsgd+]# ls -l
-rw-r--r-- 1 root root 21104 Jul 24 22:55 encoder.ko
-rw-r--r-- 1 root root 11944 Jul 24 22:54 modules.builtin
-rw-r--r-- 1 root root    63 Jul 24 22:56 modules.order

[root@LS-GD 6.12.0.lsgd+]# touch modules.builtin   
[root@LS-GD 6.12.0.lsgd+]# vi modules.order
encoder.ko

需要在加载模块之前建立该模块的依赖关系,也即必须用depmod来更新一下/lib/modules/$(uname -r)/modules.dep文件;

[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
[root@LS-GD 6.12.0.lsgd+]# ls -l
total 30
-rw-r--r-- 1 root root 21104 Jul 24  2024 encoder.ko
-rw-r--r-- 1 root root   127 Jul 24 20:53 modules.alias
-rw-r--r-- 1 root root    83 Jul 24 20:53 modules.alias.bin
-rw-r--r-- 1 root root     0 Jul 24 20:52 modules.builtin
-rw-r--r-- 1 root root     0 Jul 24 20:53 modules.builtin.alias.bin
-rw-r--r-- 1 root root    12 Jul 24 20:53 modules.builtin.bin
-rw-r--r-- 1 root root    12 Jul 24 20:53 modules.dep
-rw-r--r-- 1 root root    45 Jul 24 20:53 modules.dep.bin
-rw-r--r-- 1 root root     0 Jul 24 20:53 modules.devname
-rw-r--r-- 1 root root    11 Jul 24 20:52 modules.order
-rw-r--r-- 1 root root    55 Jul 24 20:53 modules.softdep
-rw-r--r-- 1 root root    49 Jul 24 20:53 modules.symbols
-rw-r--r-- 1 root root    12 Jul 24 20:53 modules.symbols.bin

我们重新久久派开发板,内核输出相关日志如下:

[root@LS-GD ~]# dmesg | grep encoder
[   17.372939] encoder: loading out-of-tree module taints kernel.
[   17.513670] pulse_dir_encoder 1611b030.encoder_left: Probing pulse encoder driver v2.0
[   17.671224] pulse_dir_encoder 1611b030.encoder_left: PPR = 1024
[   17.761069] pulse_dir_encoder 1611b030.encoder_left: Using default clock frequency: 200000000 Hz
[   17.962302] pulse_dir_encoder 1611b030.encoder_left: Encoder driver loaded successfully, device /dev/pulse_encoder_left (PPR=1024, clk=200000000Hz)
[   18.270242] pulse_dir_encoder 1611b000.encoder_right: Probing pulse encoder driver v2.0
[   18.388887] pulse_dir_encoder 1611b000.encoder_right: PPR = 1024
[   18.521058] pulse_dir_encoder 1611b000.encoder_right: Using default clock frequency: 200000000 Hz
[   18.771180] pulse_dir_encoder 1611b000.encoder_right: Encoder driver loaded successfully, device /dev/pulse_encoder_right (PPR=1024, clk=200000000Hz)

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

4.3 应用程序测试

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

4.3.1 PMW信号输出

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 ~]# cat /sys/class/pwm/pwmchip0/pwm0/enable
0
[root@LS-GD ~]# echo 1 > /sys/class/pwm/pwmchip0/pwm0/enable
4.3.2 脉冲测量

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

[root@LS-GD opt]# scp zhengyang@172.23.34.187:/opt/2k0300/loongson_2k300_lib/example/encoder_app/main /opt

# 运行(基本模式)
[root@LS-GD opt]# ./main
Sampling encoder at 50 ms intervals (Ctrl+C to stop)
Sample     RPM        Direction  Raw        Delta
--------------------------------------------------------
0          1171       -1         10000      1171
1          1171       -1         10000      0
2          1171       -1         10000      0
3          1171       -1         10000      0
4          1171       -1         10000      0
5          1171       -1         10000      0
6          1171       -1         10000      0

# 运行(显示详细信息)
[root@LS-GD opt]# ./main -i
Encoder Info:
  PPR (pulses per revolution): 1024
  Clock frequency: 200000000 Hz
  Max count: 4294963200

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

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

\[理论周期计数 = 50000 ns × (200 MHz / 1e9) = 50000 × 0.2 = 10000 \]

参考文章

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

[2] 龙芯2K0300数据手册

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

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