龙芯2k0300 - 智能车走马观碑组编码器驱动移植
----------------------------------------------------------------------------------------------------------------------------
开发板 :久久派开发板
eMMC :8GB
DDR4 :512MB
u-boot :u-boot 2022.04
linux :6.12
rootfs:buildroot-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/B相1024线方向mini编码器,也就是说我使用的这个编码器除了有A/B输出引脚外,还有单独的方向输出引脚,这样我们就不用通过A/B相信号变化的先后顺序来判断运动方向。
1.1 工作原理
编码器的线数,是说编码器转一圈输出多少个脉冲,如果一个编码器是1024线,说明这个编码器转一圈对应的信号线会输出1024个脉冲,A/B两相转一圈发出的脉冲数一样,不过存在90°相位差。
由于我使用的编码器带有方向输出引脚,因此:
- 通过对
A相脉冲计数,通过读取单位时间t的脉冲信号数量,就可以计算出转速; - 通过判断方向引脚的高低电平,就可以知道是正转还是反转。
编码器码盘旋转一周A/B相输出的脉冲数目为N,在时间T内统计到的有效脉冲数目为S,那么每秒转数为:
这里驱动直接返回RPS,也就是每秒转数。相比RPM,RPS不会再被放大60倍,更适合在智能车控制算法中直接作为速度环反馈量使用。
如果需要换算成RPM,可以在应用层自行乘以60:
1.2 硬件接线


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可以复用为PWM0、GPIO67可以复用为PWM3。
1.3 PWM控制器脉冲测量功能
使用编码器,需要使用到2K0300的PWM控制器的脉冲测量功能,通过查阅手册《龙芯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_level和full_pulse寄存器:这是两个内部硬件计数器,它们以系统时钟为基准进行自增,用来测量时间间隔。
Low_level计数器:负责测量低电平的时间;full_pulse计数器:负责测量一个完整脉冲周期的时间。
low_buffer和full_buffer寄存器是两个“影子寄存器”(缓冲寄存器)。当特定事件发生时,硬件会立刻将计数器的当前值“快照”并存入对应的缓冲寄存器中,供软件读取。
1.3.3 脉冲测量
整个测量过程是由硬件自动完成的,逻辑如下:
① 初始化与启动:设置好控制寄存器后,计数器在系统时钟驱动下开始工作;
② 一次完整的脉冲周期测量(从下跳变开始):
- 下跳变(高->低)触发:
- 当检测到信号由高变低(下降沿)时,本轮测量开始;
- 硬件会将
full_pulse计数器的当前值,也就是上一个完整周期的长度,传送到full_buffer寄存器; - 然后,将
Low_level和full_pulse这两个计数器重置为1,并开始新一轮计数;
- 低电平期间:
Low_level和full_pulse计数器从1开始同步自增; - 上跳变(低->高)触发:
- 当检测到信号由低变高(上升沿)时,低电平阶段结束;
- 硬件将此时
Low_level计数器的值,也就是低电平持续时间,传送到low_buffer寄存器; Low_level计数器被重置为1,但full_pulse计数器继续自增,因为它还在测量整个周期;
- 下一个下跳变触发:
- 当再次检测到下降沿时,表示一个完整周期结束;
- 硬件将
full_pulse计数器,此时已累加高电平和低电平时间,存入full_buffer,并重置计数器。
③ 读取结果:
- 经过两个完整的脉冲周期后,
low_buffer和full_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微秒的完整周期。
通过计算可得:
1.3.5 转速计算
已知参数:
- 系统时钟频率:
f_clk,单位Hz; - 编码器每转脉冲数:
PPR; full_buffer寄存器的值:N,表示一个完整脉冲周期对应的系统时钟周期数。
则脉冲周期为:
脉冲频率计算:
电机每转一圈产生PPR个脉冲,因此每秒转数为:
如果需要换算成每分钟转数:
本文中的驱动直接返回RPS,不在内核驱动中乘以60。这样可以避免速度反馈数值过大,后续速度环控制也统一使用RPS作为单位。
1.3.6 PWM控制器时钟频率
龙芯2K0300的CPU核心主频为1GHz,但是PWM控制器的时钟频率为200MHz,我们通过处理器用户手册可以获取到时钟结构图;

片内主要由一路外部时钟输入,120MHz固定频率时钟作为系统参考时钟输入,供片内PLL使用。芯片内部共有3个独立的PLL,其中每个PLL最多可以提供3组频率上相互依赖的时钟输出;
① 一个NODE PLL用于产生NODE、GMAC、I2S模块时钟,该时钟为芯片的主要时钟,分为三组频率时钟;
- 一路供
CPU核、二级Cache、一二级交叉开关以及IO子网络使用; - 一路供
GMAC模块使用; - 一路供
I2S设备控制器使用;
② 一个DDR PLL同时产生DDR、NETWORK以及USB、APB、BOOT、SDIO等模块的时钟;
③ 一个PIX PLL同时产生DC显示、GMAC控制器备份的内部时钟。
DDR PLL输出三个时钟,分别为:
①ddr_pllclk用于内存控制器,频率范围:1.0GHz;
②network_pllclk用于NETWORK模块,频率范围:500MHz;
③ dev_pllclk用 于USB 、 APB ( 除SDIO外其他APB低 速 接口控制器,包括UART/I2C/PWM/CAN/ADC/I2S/SPI2/SPI3/HPET/GPIO/TIMER/ENCRYPT/RTC/WDT )、BOOT(Confbus/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_left和encoder_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_m0和pwm3_mux_m0
pwm0_mux_m0、pwm3_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 Flash的dtb分区:
[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(如20kHz → 50000ns),占空比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 脉冲测量
将GPIO65和GPIO67通过杜邦线连接起来,接着进行测试:
[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:
此时脉冲频率为:
编码器配置的PPR = 1024,因此:
由于当前驱动返回的是整数,所以测试程序中看到的结果约为19 RPS。如果换算成RPM:
这也对应旧版驱动中看到的1171 RPM。新版驱动不再乘以60,直接返回RPS。
五、代码下载
参考文章
[1] 龙邱科技Mini编码器.pdf
[2] 龙芯2K0300数据手册
[3] 龙芯2K0300处理器用户手册

浙公网安备 33010602011771号