龙芯2k0300 - 走马观碑组编码器驱动移植
在《龙芯2k0300- 走马观碑组第21届智能汽车竞赛软硬件设计》中我们使用了LQ_1024线方向mini编码器。一、LQ_1024线方向mini编码器龙邱科技mini系列编码器具有以下特点分辨率有A/B相正交输出256线、A/B相正交输出1024线、步进脉冲 方向输出512线、......旋转速度高最高转速可达10000rpm宽广的工作温度范围-40℃ ~ 125℃。抗扰性好。本产品采用霍尔检测技术属于无接触检测传感器运行不受灰尘或其它杂物影响很好克服了基于光学检测原理的缺点体积小巧。直径D14mm、高H18mm、轴径3mm。如果你对编码器一点都不了解的话可以先参考这篇文章《STM32F103霍尔编码器测速》。龙邱科技提供的编码器有带方向输出引脚和不带方向输出引脚两种这里我使用的是正交A/B相1024线方向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 硬件接线1.2.1 左电机编码器编码器一共引出6个引脚左电机编码器与龙芯2K0300开发板的连接关系需严格对应连接表如下引脚编号编码器引脚含义连接的龙芯GPIOPIN1GND必须可靠接地否则可能出现显示异常GNDPIN23.3~5V直流供电3.3VPIN3AA 相输出CMOS 电平GPIO67(SPI2_CS)PIN4Dir方向输出GPIO72(CAN2_RX)PIN5BB 相输出CMOS 电平——PIN6nc悬空——1.2.2 右电机编码器右电机编码器与龙芯2K0300开发板的连接关系需严格对应连接表如下编码器引脚含义连接的龙芯GPIOPIN1GND必须可靠接地否则可能出现显示异常GNDPIN23.3~5V直流供电3.3VPIN3AA 相输出CMOS 电平GPIO64(SPI2_CLK)PIN4Dir方向输出GPIO73(CAN2_TX)PIN5BB 相输出CMOS 电平——PIN6nc悬空——在上一节《龙芯2k0300- 走马观碑组PWM驱动移植》我们讲到GPIO64可以复用为PWM0、GPIO67可以复用为PWM3。1.3 脉冲测量功能使用编码器需要使用到2K0300的PWM控制器的脉冲测量功能通过查阅手册《龙芯2K0300处理器用户手册》。1.3.1 访问地址及引脚复用PWM控制器内部寄存器的物理地址构成如下地址设备备注0x1611_b000PWM0~3每个 PWM 占用 16B 寄存器配置空间0x1611_b000-pwm0,0x1611_b010-pwm1,0x1611_b020-pwm2,0x1611_b030-pwm3对于PWM模块使用时要注意将对应的引脚设置为相应的功能。与PWM相关的引脚复用设置可查询PWM功能引脚复用关系并配置相应GPIO引脚复用配置寄存器实现。1.3.2 寄存器描述每路控制器共有5个寄存器具体描述如下名称地址宽度访问说明low_bufferBase 0x432R/W低脉冲缓冲寄存器full_bufferBase 0x832R/W脉冲周期缓冲寄存器CTRLBase 0xC11R/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微秒的完整周期)通过计算可得高电平时间 200 - 150 50微秒。1.3.5 转速计算已知参数系统时钟频率f_clk(Hz)编码器每转脉冲数PPRfull_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} \]二、编码器设备驱动2.1 编码器驱动接下来我们在driver目录下创建子目录encoder_driverzhengyangubuntu:/opt/2k0300/loongson_2k300_lib/driver$ mkdir encoder_driver zhengyangubuntu:/opt/2k0300/loongson_2k300_lib/encoder_driver$ cd encoder_driver目录结构如下zhengyangubuntu:/opt/2k0300/loongson_2k300_lib/driver/encoder_driver$ tree . . ├── Makefile └── encoder_driver.c2.1.1encoder_driver.cencoder_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 #define DEVICE_NAME pulse_encoder #define DRIVER_VERSION 1.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 100000000UL /* 100 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; /* 设备指针用于日志 */ }; static struct encoder_device *g_encoder; /** * 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 处理 */ static long encoder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct encoder_device *enc g_encoder; 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; } static const struct file_operations encoder_fops { .owner THIS_MODULE, .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; 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; } /* 尝试从时钟框架获取 */ struct clk *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; int ret; dev_info(dev, Probing pulse encoder driver v%s\n, DRIVER_VERSION); /* 1. 分配设备结构体 */ g_encoder devm_kzalloc(dev, sizeof(*g_encoder), GFP_KERNEL); if (!g_encoder) return -ENOMEM; g_encoder-dev dev; /* 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. 映射寄存器地址 */ g_encoder-base devm_ioremap(dev, res-start, resource_size(res)); if (!g_encoder-base) { dev_err(dev, Failed to ioremap memory\n); return -ENOMEM; } dev_dbg(dev, Registers mapped at 0x%p (size0x%llu)\n, g_encoder-base, (unsigned long long)resource_size(res)); /* 5. 获取方向 GPIO */ g_encoder-dir_gpio of_get_named_gpio(np, dir-gpios, 0); if (!gpio_is_valid(g_encoder-dir_gpio)) { dev_err(dev, Invalid or missing dir-gpios\n); return -EINVAL; } ret devm_gpio_request(dev, g_encoder-dir_gpio, encoder_dir); if (ret) { dev_err(dev, Failed to request dir GPIO %d: %d\n, g_encoder-dir_gpio, ret); return ret; } ret gpio_direction_input(g_encoder-dir_gpio); if (ret) { dev_err(dev, Failed to set dir GPIO %d as input: %d\n, g_encoder-dir_gpio, ret); return ret; } dev_dbg(dev, Direction GPIO %d configured\n, g_encoder-dir_gpio); /* 6. 获取 PPR(带范围检查) */ if (of_property_read_u32(np, rotary-encoder,steps-per-period, g_encoder-ppr) ! 0) g_encoder-ppr DEFAULT_PPR; if (g_encoder-ppr MIN_PPR || g_encoder-ppr MAX_PPR) { dev_warn(dev, Invalid PPR %u, clamping to [%d,%d]\n, g_encoder-ppr, MIN_PPR, MAX_PPR); g_encoder-ppr clamp(g_encoder-ppr, MIN_PPR, MAX_PPR); } dev_info(dev, PPR %d\n, g_encoder-ppr); /* 7. 获取时钟频率 */ ret encoder_get_clk_freq(g_encoder, pdev); if (ret) dev_warn(dev, Failed to get clock frequency, using default\n); /* 8. 配置定时器为编码器模式 */ writel(0, g_encoder-base PWM_ENCODER_CTRL); /* 先禁用 */ writel(PWM_ENCODER_EN | PWM_ENCODER_CAPTE, g_encoder-base PWM_ENCODER_CTRL); dev_dbg(dev, Timer configured in encoder mode\n); /* 9. 注册 misc 设备 */ g_encoder-miscdev.minor MISC_DYNAMIC_MINOR; g_encoder-miscdev.name DEVICE_NAME; g_encoder-miscdev.fops encoder_fops; ret misc_register(g_encoder-miscdev); if (ret) { dev_err(dev, Failed to register misc device: %d\n, ret); /* 清理硬件配置 */ writel(0, g_encoder-base PWM_ENCODER_CTRL); return ret; } dev_info(dev, Encoder driver loaded successfully (PPR%d, clk%luHz)\n, g_encoder-ppr, g_encoder-clk_freq); return 0; } static void encoder_remove(struct platform_device *pdev) { if (g_encoder) { misc_deregister(g_encoder-miscdev); writel(0, g_encoder-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 (No Interrupt)); MODULE_VERSION(DRIVER_VERSION);2.1.2Makefile# 内核源码路径 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 ko2.2 新增设备节点zhengyangubuntu:/opt/2k0300/build-2k0300/workspace/linux-6.12$2.2.1encoder_leftencoder_right修改arch/loongarch/boot/dts/ls2k300_99pi.dtsi节点/新增如下节点encoder_left { compatible pulse-dir-encoder; 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; 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.2pwm0_mux_m0pwm3_mux_m0pwm0_mux_m0、pwm3_mux_m0定义在arch/loongarch/boot/dts/loongson-2k0300.dtsipinmux: pinmux16000490 { ...... 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_appzhengyangubuntu:/opt/2k0300/loongson_2k300_lib/example$ mkdir encoder_app zhengyangubuntu:/opt/2k0300/loongson_2k300_lib/example$ cd encoder_app目录结构如下zhengyangubuntu:/opt/2k0300/loongson_2k300_lib/example/encoder_app$ tree . . ├── main.c ├── Makefile3.1main.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) i1 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) i1 argc) { max_samples atoi(argv[i]); } } /* 打开设备 */ fd open(/dev/pulse_encoder, O_RDWR); if (fd 0) { fprintf(stderr, Failed to open /dev/pulse_encoder: %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 (CtrlC 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.2Makefileall: loongarch64-linux-gnu-gcc -o main main.c clean: rm -rf *.o main3.3 编译应用程序zhengyangubuntu:/opt/2k0300/loongson_2k300_lib/example/encoder_app$ make loongarch64-linux-gnu-gcc -o main main.c zhengyangubuntu:/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内核根目录执行如下命令zhengyangubuntu:~$ cd /opt/2k0300/build-2k0300/workspace/linux-6.12 zhengyangubuntu:/opt/2k0300/build-2k0300/workspace/linux-6.12$ source ../set_env.sh make arch/loongarch/boot/dts/ls2k300_99pi_wifi.dts dtbs V14.1.2 更新设备树将设备树拷贝到久久派的/opt目录zhengyangubuntu:/opt/2k0300/build-2k0300/workspace/linux-6.12$ scp arch/loongarch/boot/dts/ls2k300_99pi_wifi.dtb root172.23.34.188:/opt在久久派使用dd命令烧写设备树到SPI Nor Flash的dtb分区[rootLS-GD opt]# dd if/opt/ls2k300_99pi_wifi.dtb of/dev/mtdblock3 bs1 221240 records in 221240 records out 22124 bytes (22 kB, 22 KiB) copied, 0.500342 s, 44.2 kB/s [rootLS-GD opt]# reboot4.2 安装驱动由于我们并没有将驱动源码放到内核中因此需要单独编译安装。4.2.1 编译驱动ubuntu宿主接重新编译驱动hengyangubuntu:~$ cd /opt/2k0300/loongson_2k300_lib/driver/encoder_driver zhengyangubuntu:/opt/2k0300/loongson_2k300_lib/driver/encoder_driver$ make 已创建目录 开始编译驱动模块 # 关键指定内核路径并传递正确的编译参数 make -C /opt/2k0300/build-2k0300/workspace/linux-6.12 ARCHloongarch CROSS_COMPILEloongarch64-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 目录 zhengyangubuntu:/opt/2k0300/loongson_2k300_lib/driver/encoder_driver$ scp ./ko/encoder.ko root172.23.34.188:/opt4.2.2 安装驱动久久派安装驱动[rootLS-GD opt]# ls encoder.ko -l -rw-r--r-- 1 root root 21104 Jul 24 22:23 encoder.ko [rootLS-GD opt]# insmod encoder.ko [rootLS-GD opt]# lsmod Module Size Used by encoder 65536 0查看内核输出信息[rootLS-GD opt]# dmesg ...... [ 44.748954] encoder: loading out-of-tree module taints kernel. [ 44.756862] pulse_dir_encoder 1611b030.encoder_left: Probing pulse encoder driver v1.0 [ 44.765085] pulse_dir_encoder 1611b030.encoder_left: PPR 1024 [ 44.772085] pulse_dir_encoder 1611b030.encoder_left: Using default clock frequency: 100000000 Hz [ 44.781224] pulse_dir_encoder 1611b030.encoder_left: Encoder driver loaded successfully (PPR1024, clk100000000Hz)4.3 应用程序测试接着我们进行测试[rootLS-GD opt]# scp zhengyang172.23.34.187:/opt/2k0300/loongson_2k300_lib/example/encoder_app/main /opt # 运行(基本模式) [rootLS-GD opt]# ./main # 运行(50ms 采样间隔) [rootLS-GD opt]# ./main 50 # 运行(显示详细信息) [rootLS-GD opt]# ./main -i参考文字[1] 龙邱科技Mini编码器.pdf[2] 龙芯2K0300数据手册[3] 龙芯2K0300处理器用户手册