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

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

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

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

在《龙芯2k0300 - 走马观碑组第21届智能汽车竞赛软硬件设计》中我们使用了DRV8701双路电机驱动,DRV8701双路电机驱动采用门极驱动芯片DRV8701E + N-MOSTPH1R403NL方案,输入电源5.9V ~ 28V均可使用,带过流保护功能。

一、DRV8701双路电机驱动

其中

  • PH-6P接口用来隔离供电以及信号输入;
  • 红色端子同来提供直流电压输入,输入电压5.9-28V,一般大小为电机可承受的最大电压;
  • 蓝色端子是直流电压输出,输出电压与输入PWM值正相关,连接到电机上。

1.1 电路原理图

电路原理图如下:

1.1.1 DRV8701E

DRV8701E是德州仪器 (TI) 的一款单H桥栅极驱动器,专门用来控制外部的4N沟道MOSFET,关键特性;

  • 控制接口:它采用PH/EN (相位/使能)逻辑接口。这意味着你只需要两个GPIO信号就能控制电机的正转、反转、刹车和滑行,逻辑非常清晰。
  • 自举供电:它内置了一个电荷泵 (Charge Pump),可以产生驱动高侧N-MOS管所需的高于电源电压的栅极驱动电压(通常为 9.5V),这使得它可以高效地驱动高侧开关。
  • 可调驱动强度:它有一个 IDRIVE 引脚,可以通过一个电阻来调节栅极的驱动电流(从6mA150mA)。在龙邱方案中(5.9V-28V 输入),这个功能非常有用,可以根据电源电压调整开关速度,平衡开关损耗和电磁干扰 (EMI)。
  • 电流检测与保护:它内置了电流检测放大器,配合外部采样电阻,可以实现 PWM电流斩波(限制启动或堵转电流)。同时具备欠压锁定 (UVLO)、过流保护 (OCP) 和热关断 (TSD) 功能。
1.1.2 TPH1R403NL

这是一款ToshibaN沟道MOSFET,这里会用到4颗(H桥结构),关键特性;

  • 超低导通电阻:它的\(R_{DS(ON)}\)典型值仅为1.7 mΩ (在\(V_{GS}=4.5V\)时)。这是一个非常惊人的低数值,意味着导通损耗极低,发热小,效率极高;
  • 大电流能力:单颗芯片的漏极电流可达60A-150A(取决于测试条件),配合DRV8701E的强驱能力,足以应对大扭矩电机的启动电流。
  • 低门槛电压:它支持4.5V的低电压逻辑驱动。这与DRV8701E在电池供电(如5.9V-12V)时的输出电压完美匹配,确保在电池电压下降时MOS管依然能完全导通,避免烧管。
1.1.3 74HC125PW

74HC125PW是一款非常经典的数字逻辑芯片,它的核心身份是“四路缓冲器/线路驱动器”,并且带有三态输出功能。

可以把它想象成4个独立的电子开关;

  • 缓冲/驱动(增强信号):当开关“打开”时,它会将输入的信号(01)原样输出,但输出能力更强。比如,单片机的引脚电流很小,带不动大功率负载,通过74HC125后,就能驱动更重的负载(比如长导线或更多的逻辑门);
  • 三态输出(关键功能): 这是它最特别的地方。普通的芯片输出只有“高电平(1)”和“低电平(0)”。而74HC125有第三种状态高阻态,高阻态相当于开关断开,这根线就像悬空一样,既不输出高也不输出低,对电路没有任何影响。

它还可以用来隔离信号,防止不同模块之间的信号干扰。

1.2 控制原理

DRV8701有两种控制模式:PH/EN模式以及PWM模式,DRV8701芯片表面印的字,最后会有一个字母,决定了它的工作逻辑:

  • DRV8701E(后缀是 E)芯片内部电路被设计为PH/EN模式,此时,15号引脚被定义为PH(方向),14号引脚被定义为EN(使能);
  • DRV8701P(后缀是 P):芯片内部电路被设计为 PWM (IN1/IN2) 模式。此时,15号引脚被定义为IN114 号引脚被定义为 IN2
1.2.1 PH/EN模式

这种模式使用PH(Phase/相位)和EN(Enable/使能)两个信号来控制电机。逻辑非常直观,适合单片机控制。

1.2.2 PWM模式

这种模式使用IN1IN2两个信号直接控制H桥的上下臂,通常用于直接输入PWM波的场景。

1.2.3 H桥工作原理

更多H桥工作原理内容可以参考《H桥工作原理》。

1.3 电机控制

1.3.1 控制逻辑

经过前面的分析,不难得出右电机控制逻辑;

nSLEEP PWM1(EN/IN2) GPIO1(PH/IN1) 电机状态
0 X X 电机停止
1 0 X 刹车(续流)
1 1 0 反转
1 1 1 正转

所以:

  • 给引脚GPIO1为高电平,然后引脚PWM1输入PWM就可以进行右电机正转调速控制;
  • 给引脚GPIO1为低电平,然后引脚PWM1输入PWM就可以进行右电机反转调速控制;

同理,右电机控制逻辑:

  • 给引脚GPIO2为高电平,然后引脚PWM2输入PWM就可以进行左电机正转调速控制。
  • 给引脚GPIO2为低电平,然后引脚PWM2输入PWM就可以进行左电机反转调速控制。

注意:龙邱驱动开发板OU1/OUT2输出位于驱动板的右侧,对应的PWM1控制;开发板OUT3/OUT4输出位于驱动板的左侧,对应的PWM2控制。

PWM波的占空比越大时,蓝色端子输出的电压值与红色端子的电压值越接近,反之则与0V越接近。

PWM波的频率不可以随意设置,频率太低会导致电机运转不畅,振动大,噪音大;频率太高会导致驱动器开关损耗较大,甚至有电机会啸叫而不转的情况。一般1k~30kPWM频率较为普遍,几百Hz的也有,实际上需要根据电机功率在测试时确定合适的PWM频率范围为宜。

1.3.2 硬件接线

久久派底版控制口原理图如下:

img

DRV8701双路电机驱动板与龙芯2K0300开发板的连接关系需严格对应,连接表如下:

电机驱动板 底版引脚 连接的龙芯GPIO 说明
3.3V 3.3V 3.3V 接3.3V
PWM1 MT1_P GPIO65(SPI2_MISO) 右电机PWM控制
GPIO1 MT1_N GPIO75(CAN3_TX) 右电机方向引脚
PWM2 MT2_P GPIO66(SPI2_MOSI) 左电机PWM控制
GPIO2 MT2_N GPIO74(CAN3_RX) 左电机方向引脚
GND GND GND 必须可靠接地,否则可能出现显示异常

在《龙芯2k0300 - 久久派开发环境搭建及内核升级(上)》中我们介绍到龙芯2K0300集成4PWM控制器,支持输入/输出,根据《龙芯2K0300处理器用户手册》手册查询到芯片功能引脚复用关系表;

芯片引脚 GPIO 复用 主功能复用 第一复用 第二复用
SPI2_CLK GPIO64 spi2_clk PWM0 uart0_dcd
SPI2_MISO GPIO65 spi2_miso PWM1 uart0_ri
SPI2_MOSI GPIO66 spi2_mosi PWM2 uart1_rts
SPI2_CS GPIO67 spi2_cs PWM3 uart1_cts

注:除芯片启动相关的引脚(SPI0SDIO0/eMMC0在相应启动模式下对应引脚为主功能) 外,以上复用引脚上电默认状态都复用为GPIO 功能,其中GPIO0~63默认为输入状态,GPIO64~105默认为输出低电平状态。

因此我们可以知道GPIO65GPIO66引脚是可以服用为PWM功能的。

PWM设备驱动

PWM(脉冲宽度调制)通过改变输出方波的占空比来调节电机两端的平均电压,从而控制转速。结合DRV8701E驱动芯片的PH/EN模式,可以用两个GPIO控制一个电机:一个PWM引脚(调速),一个方向引脚(换向)。

2.1 内核配置

进入内核配置界面:

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 menuconfig

依次进入以下菜单:

Device Drivers → 
	[*] Pulse-Width Modulation (PWM) Support  --->  → 
         <*>   Loongson PWM support

默认会生成配置:

CONFIG_PWM_LOONGSON=y

arch/loongarch/configs/loongson_2k300_defconfig文件已经配置了这个,所以我们不用修改任何配置。

2.2 PWM驱动

驱动源码位于drivers/pwd/pwm-loongson.c

点击查看详情
// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (C) 2017-2024 Loongson Technology Corporation Limited.
 *
 * Loongson PWM driver
 *
 * For Loongson's PWM IP block documentation please refer Chapter 11 of
 * Reference Manual: https://loongson.github.io/LoongArch-Documentation/Loongson-7A1000-usermanual-EN.pdf
 *
 * Author: Juxin Gao <gaojuxin@loongson.cn>
 * Further cleanup and restructuring by:
 *         Binbin Zhou <zhoubinbin@loongson.cn>
 *
 * Limitations:
 * - If both DUTY and PERIOD are set to 0, the output is a constant low signal.
 * - When disabled the output is driven to 0 independent of the configured
 *   polarity.
 */

#include <linux/acpi.h>
#include <linux/clk.h>
#include <linux/device.h>
#include <linux/init.h>
#include <linux/io.h>
#include <linux/iopoll.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/pwm.h>
#include <linux/units.h>
#include <linux/delay.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/delay.h>

/* Loongson PWM registers */
#define LOONGSON_PWM_REG_DUTY		0x4 /* Low Pulse Buffer Register */
#define LOONGSON_PWM_REG_PERIOD		0x8 /* Pulse Period Buffer Register */
#define LOONGSON_PWM_REG_CTRL		0xc /* Control Register */

/* Control register bits */
#define LOONGSON_PWM_CTRL_EN		BIT(0)  /* Counter Enable Bit */
#define LOONGSON_PWM_CTRL_OE		BIT(3)  /* Pulse Output Enable Control Bit, Valid Low */
#define LOONGSON_PWM_CTRL_SINGLE	BIT(4)  /* Single Pulse Control Bit */
#define LOONGSON_PWM_CTRL_INTE		BIT(5)  /* Interrupt Enable Bit */
#define LOONGSON_PWM_CTRL_INT		BIT(6)  /* Interrupt Bit */
#define LOONGSON_PWM_CTRL_RST		BIT(7)  /* Counter Reset Bit */
#define LOONGSON_PWM_CTRL_CAPTE		BIT(8)  /* Measurement Pulse Enable Bit */
#define LOONGSON_PWM_CTRL_INVERT	BIT(9)  /* Output flip-flop Enable Bit */
#define LOONGSON_PWM_CTRL_DZONE		BIT(10) /* Anti-dead Zone Enable Bit */

/* default input clk frequency for the ACPI case */
#define LOONGSON_PWM_FREQ_DEFAULT	50000000 /* Hz */

struct pwm_loongson_suspend_store {
	u32 ctrl;
	u32 duty;
	u32 period;
};

struct pwm_loongson_ddata {
	struct clk *clk;
	void __iomem *base;
	u32 irq;
	u32 int_count;
	u64 clk_rate;
	struct pwm_loongson_suspend_store lss;
    struct wait_queue_head capture_wait_queue;
};

static inline struct pwm_loongson_ddata *to_pwm_loongson_ddata(struct pwm_chip *chip)
{
	return pwmchip_get_drvdata(chip);
}

static inline u32 pwm_loongson_readl(struct pwm_loongson_ddata *ddata, u32 offset)
{
	return readl(ddata->base + offset);
}

static inline void pwm_loongson_writel(struct pwm_loongson_ddata *ddata,
				       u32 val, u32 offset)
{
	writel(val, ddata->base + offset);
}

static irqreturn_t pwm_loongson_isr(int irq, void *dev)
{
	u32 val;
	struct pwm_chip *chip = dev_get_drvdata(dev);
	struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip);

	val = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_CTRL);
	if ((val & LOONGSON_PWM_CTRL_INT) == 0) {
		return IRQ_NONE;
	}
	val |= LOONGSON_PWM_CTRL_INT;
	pwm_loongson_writel(ddata, val, LOONGSON_PWM_REG_CTRL);

	ddata->int_count++;

	dev_info(dev, "pwm_loongson_isr count %u\n", ddata->int_count);

	return IRQ_HANDLED;
}

static int pwm_loongson_capture(struct pwm_chip *chip, struct pwm_device *pwm,
				struct pwm_capture *result,
				unsigned long timeout)
{
	u32 val;
	struct device *dev = pwmchip_parent(chip);
	struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip);
	int ret;

	pwm_loongson_writel(ddata, 0, LOONGSON_PWM_REG_PERIOD);

	val = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_CTRL);
	val |= LOONGSON_PWM_CTRL_EN | LOONGSON_PWM_CTRL_CAPTE |
	       LOONGSON_PWM_CTRL_OE;
	pwm_loongson_writel(ddata, val, LOONGSON_PWM_REG_CTRL);
	ret = readl_poll_timeout(ddata->base + LOONGSON_PWM_REG_PERIOD, val,
				 val, 10, timeout * 1000);
	if (ret < 0)
		return -ETIMEDOUT;

	dev_dbg(dev, "get first period: %u\n", val);

	usleep_range(val * 4 / 100 + 1, val * 4 / 100 + 2);

	val = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_PERIOD);
	result->period = DIV64_U64_ROUND_UP((u64)val * NSEC_PER_SEC, ddata->clk_rate);
	dev_dbg(dev, "get second period: %u\n", val);
	val = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_DUTY);
	result->duty_cycle = DIV64_U64_ROUND_UP((u64)val * NSEC_PER_SEC, ddata->clk_rate);

	val = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_CTRL);
	val &= ~(LOONGSON_PWM_CTRL_EN | LOONGSON_PWM_CTRL_CAPTE |
		 LOONGSON_PWM_CTRL_OE);
	pwm_loongson_writel(ddata, val, LOONGSON_PWM_REG_CTRL);

	return 0;
}

static int pwm_loongson_set_polarity(struct pwm_chip *chip, struct pwm_device *pwm,
				     enum pwm_polarity polarity)
{
	u16 val;
	struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip);

	val = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_CTRL);

	if (polarity == PWM_POLARITY_INVERSED)
		/* Duty cycle defines LOW period of PWM */
		val &= ~LOONGSON_PWM_CTRL_INVERT;
	else
		/* Duty cycle defines HIGH period of PWM */
		val |= LOONGSON_PWM_CTRL_INVERT;

	pwm_loongson_writel(ddata, val, LOONGSON_PWM_REG_CTRL);

	return 0;
}

static void pwm_loongson_disable(struct pwm_chip *chip, struct pwm_device *pwm)
{
	u32 val;
	u32 duty;
	u32 period;
	u32 period_1000ns;
	struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip);

	duty = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_DUTY);
	period = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_PERIOD);

	period_1000ns = mul_u64_u64_div_u64(1000, ddata->clk_rate, NSEC_PER_SEC);
	pwm_loongson_writel(ddata, 1, LOONGSON_PWM_REG_DUTY);
	pwm_loongson_writel(ddata, period_1000ns, LOONGSON_PWM_REG_PERIOD);
	val = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_CTRL);
	val |= LOONGSON_PWM_CTRL_RST;
	pwm_loongson_writel(ddata, val, LOONGSON_PWM_REG_CTRL);
	val ^= LOONGSON_PWM_CTRL_RST;
	pwm_loongson_writel(ddata, val, LOONGSON_PWM_REG_CTRL);

	ndelay(1000);

	val = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_CTRL);
	val &= ~LOONGSON_PWM_CTRL_EN;
	pwm_loongson_writel(ddata, val, LOONGSON_PWM_REG_CTRL);

	pwm_loongson_writel(ddata, duty, LOONGSON_PWM_REG_DUTY);
	pwm_loongson_writel(ddata, period, LOONGSON_PWM_REG_PERIOD);
}

static int pwm_loongson_enable(struct pwm_chip *chip, struct pwm_device *pwm)
{
	u32 val;
	struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip);

	val = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_CTRL);
	val |= LOONGSON_PWM_CTRL_EN;
	pwm_loongson_writel(ddata, val, LOONGSON_PWM_REG_CTRL);

	return 0;
}

static int pwm_loongson_config(struct pwm_chip *chip, struct pwm_device *pwm,
			       u64 duty_ns, u64 period_ns)
{
	u64 duty, period;
	struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip);

	/* duty = duty_ns * ddata->clk_rate / NSEC_PER_SEC */
	duty = mul_u64_u64_div_u64(duty_ns, ddata->clk_rate, NSEC_PER_SEC);
	pwm_loongson_writel(ddata, duty, LOONGSON_PWM_REG_DUTY);

	/* period = period_ns * ddata->clk_rate / NSEC_PER_SEC */
	period = mul_u64_u64_div_u64(period_ns, ddata->clk_rate, NSEC_PER_SEC);
	pwm_loongson_writel(ddata, period, LOONGSON_PWM_REG_PERIOD);

	return 0;
}

static int pwm_loongson_apply(struct pwm_chip *chip, struct pwm_device *pwm,
			      const struct pwm_state *state)
{
	int ret;
	u64 period, duty_cycle;
	bool enabled = pwm->state.enabled;

	if (!state->enabled) {
		if (enabled)
			pwm_loongson_disable(chip, pwm);
		return 0;
	}

	ret = pwm_loongson_set_polarity(chip, pwm, state->polarity);
	if (ret)
		return ret;

	period = min(state->period, NSEC_PER_SEC);
	duty_cycle = min(state->duty_cycle, NSEC_PER_SEC);

	ret = pwm_loongson_config(chip, pwm, duty_cycle, period);
	if (ret)
		return ret;

	if (!enabled && state->enabled)
		ret = pwm_loongson_enable(chip, pwm);

	return ret;
}

static int pwm_loongson_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
				  struct pwm_state *state)
{
	u32 duty, period, ctrl;
	struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip);

	duty = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_DUTY);
	period = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_PERIOD);
	ctrl = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_CTRL);

	/* duty & period have a max of 2^32, so we can't overflow */
	state->duty_cycle = DIV64_U64_ROUND_UP((u64)duty * NSEC_PER_SEC, ddata->clk_rate);
	state->period = DIV64_U64_ROUND_UP((u64)period * NSEC_PER_SEC, ddata->clk_rate);
	state->polarity = (ctrl & LOONGSON_PWM_CTRL_INVERT) ? PWM_POLARITY_NORMAL :
			  PWM_POLARITY_INVERSED;
	state->enabled = (ctrl & LOONGSON_PWM_CTRL_EN) ? true : false;

	return 0;
}

static const struct pwm_ops pwm_loongson_ops = {
	.capture = pwm_loongson_capture,
	.apply = pwm_loongson_apply,
	.get_state = pwm_loongson_get_state,
};

static int pwm_loongson_probe(struct platform_device *pdev)
{
	int ret;
	struct pwm_chip *chip;
	struct pwm_loongson_ddata *ddata;
	struct device *dev = &pdev->dev;
	struct device_node *np = dev->of_node;
	u32 of_clk_freq = 0;
	u32 irq;
	u32 init_polarity = 0;

	irq = platform_get_irq(pdev, 0);
	dev_info(dev, "irq get:%u\n", irq);
	if (irq <= 0) {
	    dev_err(&pdev->dev, "no irq resource?\n");
	    return -ENODEV;
	}

	chip = devm_pwmchip_alloc(dev, 1, sizeof(*ddata));
	if (IS_ERR(chip))
		return PTR_ERR(chip);
	ddata = to_pwm_loongson_ddata(chip);

    // 初始化等待队列
    init_waitqueue_head(&ddata->capture_wait_queue);

	ddata->base = devm_platform_ioremap_resource(pdev, 0);
	if (IS_ERR(ddata->base))
		return PTR_ERR(ddata->base);

	ddata->clk = devm_clk_get_optional_enabled(dev, NULL);
	if (IS_ERR(ddata->clk))
		return dev_err_probe(dev, PTR_ERR(ddata->clk),
				     "failed to get pwm clock\n");
	ddata->clk_rate = LOONGSON_PWM_FREQ_DEFAULT;
	if (ddata->clk) {
		ret = devm_clk_rate_exclusive_get(dev, ddata->clk);
		if (ret)
			return dev_err_probe(dev, ret,
						"Failed to get exclusive rate\n");

		ddata->clk_rate = clk_get_rate(ddata->clk);
		if (!ddata->clk_rate)
			return dev_err_probe(dev, -EINVAL,
						"Failed to get frequency\n");
	} else {
#ifdef CONFIG_OF
		if (!of_property_read_u32(np, "clock-frequency", &of_clk_freq))
			ddata->clk_rate = of_clk_freq;
		of_property_read_u32(np, "init-polarity", &init_polarity);
#endif
	}

	ddata->irq = irq;

	ret = devm_request_irq(dev, ddata->irq, pwm_loongson_isr, IRQF_SHARED,
			       dev_name(dev), dev);

	if (ret)
		dev_err(dev, "failure requesting irq %d\n", ret);

	/* Explicitly initialize the CTRL register */
	pwm_loongson_writel(ddata, 0, LOONGSON_PWM_REG_CTRL);

	chip->ops = &pwm_loongson_ops;
	chip->atomic = true;
	dev_set_drvdata(dev, chip);

	pwm_loongson_set_polarity(chip, NULL, init_polarity ? PWM_POLARITY_INVERSED : PWM_POLARITY_NORMAL);
	if (init_polarity) {
		pwm_loongson_config(chip, NULL, 0, 100000);
		pwm_loongson_enable(chip, NULL);
		pwm_loongson_disable(chip, NULL);
	}

	ret = devm_pwmchip_add(dev, chip);
	if (ret < 0)
		return dev_err_probe(dev, ret, "failed to add PWM chip\n");

	return 0;
}

static int pwm_loongson_suspend(struct device *dev)
{
	struct pwm_chip *chip = dev_get_drvdata(dev);
	struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip);

	ddata->lss.ctrl = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_CTRL);
	ddata->lss.duty = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_DUTY);
	ddata->lss.period = pwm_loongson_readl(ddata, LOONGSON_PWM_REG_PERIOD);

	clk_disable_unprepare(ddata->clk);

	return 0;
}

static int pwm_loongson_resume(struct device *dev)
{
	int ret;
	struct pwm_chip *chip = dev_get_drvdata(dev);
	struct pwm_loongson_ddata *ddata = to_pwm_loongson_ddata(chip);

	ret = clk_prepare_enable(ddata->clk);
	if (ret)
		return ret;

	pwm_loongson_writel(ddata, ddata->lss.ctrl, LOONGSON_PWM_REG_CTRL);
	pwm_loongson_writel(ddata, ddata->lss.duty, LOONGSON_PWM_REG_DUTY);
	pwm_loongson_writel(ddata, ddata->lss.period, LOONGSON_PWM_REG_PERIOD);

	return 0;
}

static DEFINE_SIMPLE_DEV_PM_OPS(pwm_loongson_pm_ops, pwm_loongson_suspend,
				pwm_loongson_resume);

static const struct of_device_id pwm_loongson_of_ids[] = {
	{ .compatible = "loongson,ls7a-pwm" },
	{ .compatible = "loongson,ls2k-pwm" },
	{ /* sentinel */ },
};
MODULE_DEVICE_TABLE(of, pwm_loongson_of_ids);

static const struct acpi_device_id pwm_loongson_acpi_ids[] = {
	{ "LOON0006" },
	{ }
};
MODULE_DEVICE_TABLE(acpi, pwm_loongson_acpi_ids);

static struct platform_driver pwm_loongson_driver = {
	.probe = pwm_loongson_probe,
	.driver = {
		.name = "loongson-pwm",
		.pm = pm_ptr(&pwm_loongson_pm_ops),
		.of_match_table = pwm_loongson_of_ids,
		.acpi_match_table = pwm_loongson_acpi_ids,
	},
};
module_platform_driver(pwm_loongson_driver);

MODULE_DESCRIPTION("Loongson PWM driver");
MODULE_AUTHOR("Loongson Technology Corporation Limited.");
MODULE_LICENSE("GPL");

内核的PWM驱动pwm-loongson.c已实现标准PWM框架接口,无需编写额外驱动。驱动提供以下功能:

功能 实现方式
周期设置 .apply回调
占空比设置 .apply回调
使能/禁用 .apply回调
捕获输入 .capture回调(部分支持)

驱动加载后,会在/sys/class/pwm/下创建pwmchipX目录。

2.3 新增设备节点

2.3.1 pwm

pwm节点定义在arch/loongarch/boot/dts/loongson-2k0300.dtsi

zhengyang@ubuntu:/opt/2k0300/build-2k0300/workspace/linux-6.12$ vim arch/loongarch/boot/dts/loongson-2k0300.dtsi

我们可以看到pwm0pwm1pwm2pwm3节点,但是我们只关注pwm1pwm2节点;

pwm1: pwm@0x1611b010 {
        compatible = "loongson,ls2k-pwm";
        reg = <0 0x1611b010 0 0xf>;
        interrupt-parent = <&liointc0>;
        interrupts = <16 IRQ_TYPE_LEVEL_HIGH>;
        #pwm-cells = <2>;
        pinctrl-0 = <&pwm1_mux_m1>;
        pinctrl-names = "default";
        clock-frequency = <200000000>;
        status = "disabled";
};

pwm2: pwm@0x1611b020 {
        compatible = "loongson,ls2k-pwm";
        reg = <0 0x1611b020 0 0xf>;
        interrupt-parent = <&liointc0>;
        interrupts = <17 IRQ_TYPE_LEVEL_HIGH>;
        #pwm-cells = <2>;
        pinctrl-0 = <&pwm2_mux_m1>;
        pinctrl-names = "default";
        clock-frequency = <200000000>;
        status = "disabled";
};

其中:

  • pwm1::标签,用于在设备树中其他地方通过&pwm1引用这个节点,方便添加属性或修改状态;
  • pwm@0x1611b010:节点名,格式为 设备名@寄存器基地址。这里pwm是功能名,0x1611b010是该PWM1控制器的物理寄存器基地址。
  • compatible:驱动匹配字符串。内核通过该属性寻找能驱动此设备的驱动程序;
  • reg:寄存器地址范围。格式为 <地址高位 地址低位 长度高位 长度低位>,由于龙芯采用64位寻址,这里用两个32位数表示64位地址;
  • interrupt-parent:指定该设备的中断路由到哪个中断控制器。&liointc0 是龙芯2K0300内部的中断控制器节点(即龙芯I/O中断控制器)的标签;
  • interrupts:描述中断线的具体信息;
    • <16>:硬件中断编号(对应PWM1控制器在中断控制器中的编号);
    • IRQ_TYPE_LEVEL_HIGH:中断触发类型,这里定义为高电平触发。该宏在 <dt-bindings/interrupt-controller/irq.h> 中定义。
  • pinctrl-0:指定设备使用的第一组引脚配置,&pwm1_mux_m1 是另一个设备树节点的标签,该节点描述了GPIO87引脚复用为PWM1功能;
  • pinctrl-names:为引脚的配置状态命名,与 pinctrl-0 对应。"default" 是默认状态,驱动在probe时会自动应用 pinctrl-0 的配置,引脚配置设置为&pwm1_mux_m1
  • clock-frequencyPWM1控制器的输入时钟频率(单位Hz);
  • status:设备状态,此时处于禁用状态。

更多有关设备树相关的内容可以参考:《linux设备树-基础介绍》。

2.3.2 使能

修改arch/loongarch/boot/dts/ls2k300_99pi.dtsi,使能pwm1pwm2

&pwm1 {
        status = "okay";
        pinctrl-0 = <&pwm1_mux_m0>;
        pinctrl-names = "default";
};

&pwm2 {
        status = "okay";
        pinctrl-0 = <&pwm2_mux_m0>;
        pinctrl-names = "default";
};

// PWM1和PWM2(GPIO65/66)与SPI2冲突,需要禁用
&spi2 {
    status = "disabled";
};

// GPIO75和GPIO74与CAN3,需要禁用
&can3 {
        status = "disabled";
};

通过 &pwm1pwm2 引用这个节点,将 status 改为 "okay",同时根据实际硬件修改引脚配置。

2.3.3 pwm1_mux_m1&pwm2_mux_m1

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

pinmux: pinmux@16000490 {
	......
	pwm1_pins: pwm1-pins {
            pwm1_mux_m0: pinmux_G65_as_pwm1 {
                    pinctrl-single,bits = <0x10 0x00000004 0x0000000c>;
            };
            pwm1_mux_m1: pinmux_G87_as_pwm1 {
                    pinctrl-single,bits = <0x14 0x00008000 0x0000c000>;
            };
            pwm1_mux_m2: pinmux_G103_as_pwm1 {
                    pinctrl-single,bits = <0x18 0x0000c000 0x0000c000>;
            };
    };

    pwm2_pins: pwm2-pins {
            pwm2_mux_m0: pinmux_G66_as_pwm2 {
                    pinctrl-single,bits = <0x10 0x00000010 0x00000030>;
            };
            pwm2_mux_m1: pinmux_G88_as_pwm2 {
                    pinctrl-single,bits = <0x14 0x00020000 0x00030000>;
            };
            pwm2_mux_m2: pinmux_G104_as_pwm2 {
                    pinctrl-single,bits = <0x18 0x00030000 0x00030000>;
            };
    };
	......
}

这个设备树节点是使用pinctrl-single驱动来配置引脚复用功能的典型写法,pwm1_mux_m1这行代码会在寄存器0x160004a4的位[15:14]位写入 10b,而其它位保持不变,用于将芯片的GPIO87这个引脚设置为PWM1功能。

实际上对于右电机我们使用的是GPIO65引脚,即我们需要使用的是pwm1_mux_m0pwm1_mux_m0这行代码会在寄存器0x160004a0的位[3:2]位写入 01b,而其它位保持不变,用于将芯片的GPIO65这个引脚设置为PWM1功能。

同理对于右电机我们使用的是GPIO66引脚,即我们需要使用的是pwm2_mux_m0pwm2_mux_m0这行代码会在寄存器0x160004a0的位[5:4]位写入 01b,而其它位保持不变,用于将芯片的GPIO66这个引脚设置为PWM1功能。

注:pinctrl-single 是一个通用的引脚控制驱动,适用于那些引脚复用寄存器是“单寄存器位域”的芯片。当SoC没有提供复杂的pinctrl框架时,可以直接用这种方式“裸写”寄存器。

三、应用程序

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

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

目录结构如下:

zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/example/pwm_app$ tree .
.
├── inc
│   ├── motor_control.h    # 电机控制
│   └── platform.h         # 平台抽象层接口
├── main.c
├── Makefile
└── src
    ├── motor_control.c    # 电机控制逻辑
    └── platform_linux.c   # Linux 平台实现

该案例通过用户层提供的GPIOPWM接口实现了电机控制。

3.1 源码

3.1.1 platform.h
#ifndef PLATFORM_H
#define PLATFORM_H

#include <stdint.h>
#include <stdbool.h>

// ==================== 引脚类型定义 ====================
typedef enum {
    PIN_MODE_INPUT,
    PIN_MODE_OUTPUT,
    PIN_MODE_PWM
} pin_mode;

// ==================== PWM 配置结构体 ====================
typedef struct {
    int chip;           // PWM 控制器编号
    int channel;        // PWM 通道号
    int period_ns;      // 周期(纳秒)
    int duty_ns;        // 占空比(纳秒)
    int direction;      // 方向 1:正转  0:未初始化 -1:反转
    bool enabled;       // 是否使能
    void *priv;         // 平台私有数据
} pwm_handle;

// ==================== GPIO 配置结构体 ====================
typedef struct {
    int pin;            // 引脚号(平台相关)
    pin_mode mode;       // 引脚模式
    void *priv;         // 平台私有数据
} gpio_handle;

// ==================== 平台操作接口 ====================
typedef struct platform_ops {
    // GPIO 操作
    int (*gpio_init)(gpio_handle *gpio);
    void (*gpio_set)(gpio_handle *gpio, int value);
    int (*gpio_get)(gpio_handle *gpio);
    void (*gpio_deinit)(gpio_handle *gpio);
    
    // PWM 操作
    int (*pwm_init)(pwm_handle *pwm);
    void (*pwm_set_duty)(pwm_handle *pwm, int duty_ns);
    void (*pwm_enable)(pwm_handle *pwm, bool enable);
    void (*pwm_deinit)(pwm_handle *pwm);
    
    // 延时操作(毫秒/微秒)
    void (*delay_ms)(uint32_t ms);
    void (*delay_us)(uint32_t us);
} platform_ops;

// 全局平台操作接口(由平台适配层实现)
extern const platform_ops platform;

#endif // PLATFORM_H   
3.1.2 motor_control.h
#ifndef MOTOR_CONTROL_H
#define MOTOR_CONTROL_H

#include "platform.h"

// ==================== 电机配置结构体 ====================
typedef struct {
    pwm_handle pwm;          // PWM 配置
    gpio_handle dir_gpio;    // 方向控制 GPIO
    const char *name;       // 电机名称(调试用)
} motor_config;

// ==================== 电机控制 API ====================
int motor_init(motor_config *motor);
void motor_set_duty(motor_config *motor, int duty);
void motor_stop(motor_config *motor);
void motor_deinit(motor_config *motor);

#endif // MOTOR_CONTROL_H
3.1.3 platform_linux.c

这是一个基于linux平台的实现,其通过用户空间接口实现对GPIOPWM的控制;

  • GPIO子系统提供了通过sysfs控制GPIO的方式;
  • PWM子系统提供了通过sysfs控制PWM的方式。

更多细节可以参考: 《Rockchip RK3399 - GPIO&PWM风扇调试》。

具体实现如下:

点击查看详情
#include "platform.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <errno.h>   // [修改] 引入 errno 以获取错误码
#include <syslog.h>  // [修改] 引入 syslog 用于更规范的日志输出

// ==================== Linux GPIO 私有数据 ====================
typedef struct {
    int pin;
    int fd_value;
    char path_value[64];
} linux_gpio_priv;

// ==================== Linux PWM 私有数据 ====================
typedef struct {
    int chip;
    int channel;
    char base_path[128];
} linux_pwm_priv;

// ==================== GPIO 实现 ====================
static int linux_gpio_init(gpio_handle *gpio) {
    linux_gpio_priv *priv = malloc(sizeof(linux_gpio_priv));
    if (!priv) {
        perror("[GPIO] Failed to allocate memory"); // [修改]
        return -1;
    }
    
    priv->pin = gpio->pin;
    
    // 导出 GPIO
    char path[64];
    int fd = open("/sys/class/gpio/export", O_WRONLY);
    if (fd >= 0) {
        char buf[16];
        int len = snprintf(buf, sizeof(buf), "%d", gpio->pin);
        if (write(fd, buf, len) < 0) {
            // [修改] 导出失败可能是因为它已经存在,这不一定是致命错误,但值得打印
            // 如果 errno == EBUSY,说明引脚已被占用
            if (errno != EBUSY) {
                fprintf(stderr, "[GPIO] Failed to export GPIO %d: %s\n", gpio->pin, strerror(errno));
            }
        }
        close(fd);
    } else {
        // [修改] 连 export 文件都打不开,通常是权限问题
        fprintf(stderr, "[GPIO] Failed to open /sys/class/gpio/export: %s\n", strerror(errno));
    }

    // [修改] 增加短暂延时,等待 sysfs 节点创建
    usleep(10000); // 10ms

    // 设置方向
    snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/direction", gpio->pin);
    fd = open(path, O_WRONLY);
    if (fd >= 0) {
        const char *dir = (gpio->mode == PIN_MODE_OUTPUT) ? "out" : "in";
        if (write(fd, dir, strlen(dir)) < 0) {
            fprintf(stderr, "[GPIO] Failed to set direction for GPIO %d: %s\n", gpio->pin, strerror(errno));
        }
        close(fd);
    } else {
        fprintf(stderr, "[GPIO] Failed to open direction file for GPIO %d: %s\n", gpio->pin, strerror(errno));
    }
    
    // 保存 value 文件路径
    snprintf(priv->path_value, sizeof(priv->path_value), 
             "/sys/class/gpio/gpio%d/value", gpio->pin);
    
    gpio->priv = priv;
    return 0;
}

static void linux_gpio_set(gpio_handle *gpio, int value) {
    linux_gpio_priv *priv = (linux_gpio_priv*)gpio->priv;
    int fd = open(priv->path_value, O_WRONLY);
    if (fd >= 0) {
        char buf[] = {'0' + (value ? 1 : 0), '\n', '\0'};
        if (write(fd, buf, 1) < 0) {
            fprintf(stderr, "[GPIO] Failed to write value to GPIO %d: %s\n", priv->pin, strerror(errno));
        }
        close(fd);
    } else {
        fprintf(stderr, "[GPIO] Failed to open value file for GPIO %d: %s\n", priv->pin, strerror(errno));
    }
}

static int linux_gpio_get(gpio_handle *gpio) {
    linux_gpio_priv *priv = (linux_gpio_priv*)gpio->priv;
    int fd = open(priv->path_value, O_RDONLY);
    if (fd >= 0) {
        char buf[4];
        ssize_t bytes_read = read(fd, buf, sizeof(buf) - 1); // [修改] 留出空间给 '\0'
        if (bytes_read > 0) {
            buf[bytes_read] = '\0'; // [修改] 确保字符串结束
            close(fd);
            return (buf[0] == '1') ? 1 : 0;
        }
        close(fd);
    } else {
        // 读取失败时打印错误,但在某些高频调用场景下可能会刷屏,按需开启
        // fprintf(stderr, "[GPIO] Failed to open value file for read: %s\n", strerror(errno));
    }
    return 0;
}

static void linux_gpio_deinit(gpio_handle *gpio) {
    linux_gpio_priv *priv = (linux_gpio_priv*)gpio->priv;
    if (priv) {
        // 可选的 unexport 操作
        int fd = open("/sys/class/gpio/unexport", O_WRONLY);
        if (fd >= 0) {
            char buf[16];
            snprintf(buf, sizeof(buf), "%d", priv->pin);
            if (write(fd, buf, strlen(buf)) < 0) {
                fprintf(stderr, "[GPIO] Failed to unexport GPIO %d: %s\n", priv->pin, strerror(errno));
            }
            close(fd);
        }
        free(priv);
        gpio->priv = NULL;
    }
}

// ==================== PWM 实现 ====================
static int linux_pwm_init(pwm_handle *pwm) {
    linux_pwm_priv *priv = malloc(sizeof(linux_pwm_priv));
    if (!priv) {
        perror("[PWM] Failed to allocate memory");
        return -1;
    }
    
    priv->chip = pwm->chip;
    priv->channel = pwm->channel;
    
    // 构建基础路径
    snprintf(priv->base_path, sizeof(priv->base_path),
             "/sys/class/pwm/pwmchip%d/pwm%d", pwm->chip, pwm->channel);
    
    // 导出通道
    char export_path[64];
    snprintf(export_path, sizeof(export_path), 
             "/sys/class/pwm/pwmchip%d/export", pwm->chip);
    
    int fd = open(export_path, O_WRONLY);
    if (fd >= 0) {
        char buf[16];
        int len = snprintf(buf, sizeof(buf), "%d", pwm->channel);
        // [修改] 同样处理导出错误
        if (write(fd, buf, len) < 0) {
             if (errno != EBUSY) {
                fprintf(stderr, "[PWM] Failed to export PWM chip %d channel %d: %s\n", pwm->chip, pwm->channel, strerror(errno));
             }
        }
        close(fd);
    } else {
        fprintf(stderr, "[PWM] Failed to open export file for chip %d: %s\n", pwm->chip, strerror(errno));
    }

    // [修改] 增加延时,等待节点创建
    usleep(10000); // 10ms
    
    // 设置周期
    char period_path[128];
    int ret = snprintf(period_path, sizeof(period_path), "%s/period", priv->base_path);    
    if (ret < 0 || ret >= (int)sizeof(period_path)) {
        fprintf(stderr, "[PWM] Period path too long\n");
        free(priv); // [修改] 记得释放内存
        return -1;
    }

    fd = open(period_path, O_WRONLY);
    if (fd >= 0) {
        char buf[32];
        snprintf(buf, sizeof(buf), "%d", pwm->period_ns);
        if (write(fd, buf, strlen(buf)) < 0) {
            fprintf(stderr, "[PWM] Failed to set period: %s\n", strerror(errno));
        }
        close(fd);
    } else {
        fprintf(stderr, "[PWM] Failed to open period file: %s\n", strerror(errno));
    }
    
    pwm->priv = priv;
    return 0;
}

static void linux_pwm_set_duty(pwm_handle *pwm, int duty_ns) {
    linux_pwm_priv *priv = (linux_pwm_priv*)pwm->priv;
    if (!priv) return;
    
    char duty_path[128];
    int ret = snprintf(duty_path, sizeof(duty_path), "%s/duty_cycle", priv->base_path);
    if (ret < 0 || ret >= (int)sizeof(duty_path)) {
        fprintf(stderr, "[PWM] Duty path too long\n");
        return;
    }
    int fd = open(duty_path, O_WRONLY);
    if (fd >= 0) {
        char buf[32];
        snprintf(buf, sizeof(buf), "%d", duty_ns);
        if (write(fd, buf, strlen(buf)) < 0) {
            fprintf(stderr, "[PWM] Failed to set duty cycle: %s\n", strerror(errno));
        }
        close(fd);
        pwm->duty_ns = duty_ns;
    } else {
        fprintf(stderr, "[PWM] Failed to open duty_cycle file: %s\n", strerror(errno));
    }
}

static void linux_pwm_enable(pwm_handle *pwm, bool enable) {
    linux_pwm_priv *priv = (linux_pwm_priv*)pwm->priv;
    if (!priv) return;
    
    char enable_path[128];
    int ret = snprintf(enable_path, sizeof(enable_path), "%s/enable", priv->base_path);   
    if (ret < 0 || ret >= (int)sizeof(enable_path)) {
        fprintf(stderr, "[PWM] Enable path too long\n");
        return;
    }
    int fd = open(enable_path, O_WRONLY);
    if (fd >= 0) {
        if (write(fd, enable ? "1" : "0", 1) < 0) {
            fprintf(stderr, "[PWM] Failed to enable/disable PWM: %s\n", strerror(errno));
        }
        close(fd);
        pwm->enabled = enable;
    } else {
        fprintf(stderr, "[PWM] Failed to open enable file: %s\n", strerror(errno));
    }
}

static void linux_pwm_deinit(pwm_handle *pwm) {
    linux_pwm_enable(pwm, false);

    linux_pwm_priv *priv = (linux_pwm_priv*)pwm->priv;
    if (priv) {
        // 可选:unexport
        char unexport_path[64];
        snprintf(unexport_path, sizeof(unexport_path), "/sys/class/pwm/pwmchip%d/unexport", priv->chip);
        int fd = open(unexport_path, O_WRONLY);
        if (fd >= 0) {
            char buf[16];
            snprintf(buf, sizeof(buf), "%d", priv->channel);
            write(fd, buf, strlen(buf));
            close(fd);
        }
        free(priv);
        pwm->priv = NULL;
    }
}

// ==================== 延时实现 ====================
static void linux_delay_ms(uint32_t ms) {
    usleep(ms * 1000);
}

static void linux_delay_us(uint32_t us) {
    usleep(us);
}

// ==================== 平台操作接口实例 ====================
const platform_ops platform = {
    .gpio_init = linux_gpio_init,
    .gpio_set = linux_gpio_set,
    .gpio_get = linux_gpio_get,
    .gpio_deinit = linux_gpio_deinit,
    .pwm_init = linux_pwm_init,
    .pwm_set_duty = linux_pwm_set_duty,
    .pwm_enable = linux_pwm_enable,
    .pwm_deinit = linux_pwm_deinit,
    .delay_ms = linux_delay_ms,
    .delay_us = linux_delay_us
};
3.1.4 motor_control.c
#include "motor_control.h"
#include <stdlib.h>
#include <math.h>

extern const platform_ops platform;

// 初始化电机
int motor_init(motor_config *motor) {
    // 初始化方向 GPIO
    if (platform.gpio_init(&motor->dir_gpio) != 0) {
        return -1;
    }
    platform.gpio_set(&motor->dir_gpio, 0);
    
    // 初始化 PWM
    if (platform.pwm_init(&motor->pwm) != 0) {
        return -1;
    }
    platform.pwm_set_duty(&motor->pwm, 0);
    platform.pwm_enable(&motor->pwm, true);
    
    return 0;
}

// 设置电机占空比 (-100 ~ 100,负数反转,正数正转)
void motor_set_duty(motor_config *motor, int duty) {
    // 限幅
    if (duty > 100) duty = 100;
    if (duty < -100) duty = -100;
    
    // 计算占空比
    int duty_ns = (abs(duty) * motor->pwm.period_ns) / 100;
    
    // 方向处理
    int direction = (duty >= 0) ? 1 : -1;
    
    // 更新硬件
    if (motor->pwm.duty_ns != duty_ns) {
        platform.pwm_set_duty(&motor->pwm, duty_ns);
    }
    if (motor->pwm.direction != direction) {
        platform.gpio_set(&motor->dir_gpio, (direction == 1) ? 1 : 0);
    }
}

// 停止电机
void motor_stop(motor_config *motor) {
    platform.pwm_set_duty(&motor->pwm, 0);
}

// 释放资源
void motor_deinit(motor_config *motor) {
    platform.pwm_deinit(&motor->pwm);
    platform.gpio_deinit(&motor->dir_gpio);
}
3.1.5 main.c
#include <stdio.h>
#include "motor_control.h"

extern const platform_ops platform;

// 全局电机配置实例
motor_config motor_left = {
    .pwm = {
        .chip = 1,
        .channel = 0,
        .period_ns = 50000,        // 20kHz
        .duty_ns = 0,
        .direction = 0,
        .enabled = false,
        .priv = NULL
    },
    .dir_gpio = {
        .pin = 74,                 // GPIO74
        .mode = PIN_MODE_OUTPUT,
        .priv = NULL
    }
};

motor_config motor_right = {
    .pwm = {
        .chip = 0,
        .channel = 0,
        .period_ns = 50000,      // 20kHz        
        .duty_ns = 0,
        .direction = 0,          
        .enabled = false,
        .priv = NULL
    },
    .dir_gpio = {
        .pin = 75,               // GPIO75 
        .mode = PIN_MODE_OUTPUT,
        .priv = NULL
    }
};

int main() {
    printf("Initializing motors...\n");
    
    if (motor_init(&motor_left) != 0) {
        printf("Left motor init failed!\n");
        return -1;
    }
    if (motor_init(&motor_right) != 0) {
        printf("Right motor init failed!\n");
        return -1;
    }
    
    printf("Motor control ready!\n");
    
    // 前进 
    printf("Forward...\n");
    motor_set_duty(&motor_left, 30);
    motor_set_duty(&motor_right, 30);
    platform.delay_ms(3000);
    
    // 左转
    printf("Turn left...\n");
    motor_set_duty(&motor_left, -30);
    motor_set_duty(&motor_right, 30);
    platform.delay_ms(2000);
    
    // 停止
    printf("Stop.\n");
    motor_stop(&motor_left);
    motor_stop(&motor_right);
    platform.delay_ms(500);
    
    // 释放资源
    motor_deinit(&motor_left);
    motor_deinit(&motor_right);
    
    return 0;
}

这里左电机先正转后反转,如果学过电机调速控制的话,应该知道电机从正转快速切换到反转,会产生一个很大的瞬时电流,具体原理可以参考《直流有刷电机及机械特性》文章中的反接制动环节。

对于使用DRV8701这类专用电机驱动芯片的系统,这种瞬时大电流是在设计中就被考虑到的正常工况,因此很少会损坏芯片。

此外,如果占空比设置的过小,电机可能不转动。

3.2 Makefile

# 编译器设置
CC = loongarch64-linux-gnu-gcc
CFLAGS = -Wall -Iinc -lm

# 目录设置
SRC_DIR = src
INC_DIR = inc
BUILD_DIR = build
TARGET = main

# 源文件(src 目录下的 .c 文件 + 根目录的 main.c)
SRCS = $(wildcard $(SRC_DIR)/*.c) main.c
OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(SRCS)))
# 处理路径:将 main.c 和 src/*.c 都放到 build 目录
VPATH = $(SRC_DIR):.

# 默认目标
all: $(BUILD_DIR) $(TARGET)

# 创建 build 目录
$(BUILD_DIR):
	mkdir -p $(BUILD_DIR)

# 链接
$(TARGET): $(OBJS)
	$(CC) -o $@ $^ $(CFLAGS)

# 编译(适用于 src 目录和根目录的 .c 文件)
$(BUILD_DIR)/%.o: %.c | $(BUILD_DIR)
	$(CC) $(CFLAGS) -c $< -o $@

# 清理
clean:
	rm -rf $(BUILD_DIR) $(TARGET)

# 查看编译信息
info:
	@echo "SRCS = $(SRCS)"
	@echo "OBJS = $(OBJS)"
	@echo "VPATH = $(VPATH)"

.PHONY: all clean run info

3.3 编译应用程序

zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/example/pwm_app$ make
mkdir -p build
loongarch64-linux-gnu-gcc -Wall -Iinc -lm -c src/motor_control.c -o build/motor_control.o
loongarch64-linux-gnu-gcc -Wall -Iinc -lm -c src/platform_linux.c -o build/platform_linux.o
loongarch64-linux-gnu-gcc -Wall -Iinc -lm -c main.c -o build/main.o
loongarch64-linux-gnu-gcc -o main build/motor_control.o build/platform_linux.o build/main.o -Wall -Iinc -lm

zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/example/pwm_app$ ll

drwxrwxr-x 2 zhengyang zhengyang  4096  4月  2 21:16 build/
drwxrwxr-x 2 zhengyang zhengyang  4096  4月  2 20:40 inc/
-rwxrwxr-x 1 zhengyang zhengyang 21960  4月  2 21:16 main*
-rw-rw-r-- 1 zhengyang zhengyang  1730  4月  2 21:06 main.c
-rw-rw-r-- 1 zhengyang zhengyang   863  4月  2 20:36 Makefile
drwxrwxr-x 2 zhengyang zhengyang  4096  4月  2 20:32 src/

四、测试

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
22292+0 records in
22292+0 records out
22292 bytes (22 kB, 22 KiB) copied, 0.518064 s, 43.0 kB/s

[root@LS-GD opt]# reboot

4.2 sysfs控制PWM

PWM提供了用户层的接口,在 /sys/class/pwm/节点下面,PWM驱动加载成功后,会在/sys/class/pwm/目录下产生pwmchipX目录,

更多细节可以参考: 《Rockchip RK3399 - GPIO&PWM风扇调试》。

4.2.1 确认PWM设备

久久派启动系统后,在/sys/class/目录下可以看到pwm类,查看/sys/class/pwm/

[root@LS-GD ~]# ls -l  /sys/class/pwm/
lrwxrwxrwx 1 root root 0 Jul 24 20:49 pwmchip0 -> ../../devices/platform/2k300-soc/1611b010.pwm/pwm/pwmchip0
lrwxrwxrwx 1 root root 0 Jul 24 20:49 pwmchip1 -> ../../devices/platform/2k300-soc/1611b020.pwm/pwm/pwmchip1

每个pwmchip对应一个PWM控制器,一个SoC可能有多个控制器(如 pwmchip0, pwmchip1)。

[root@LS-GD ~]# ls -l  /sys/class/pwm/pwmchip0/
lrwxrwxrwx 1 root root     0 Jul 24 20:55 device -> ../../../1611b010.pwm
--w------- 1 root root 16384 Jul 24 20:55 export
-r--r--r-- 1 root root 16384 Jul 24 20:55 npwm
drwxr-xr-x 2 root root     0 Jul 24 20:55 power
lrwxrwxrwx 1 root root     0 Jul 24 20:49 subsystem -> ../../../../../../class/pwm
-rw-r--r-- 1 root root 16384 Jul 24 20:49 uevent
--w------- 1 root root 16384 Jul 24 20:55 unexport

注意: pwmchip0pwmchip1 只是逻辑编号,它们并不直接对应GPIO编号。

其中:

  • npwm:这是一个只读文件,读取它可以知道该控制器下共有多少路PWM通道;
  • export:这是入口。在使用某个PWM通道前,必须向这个文件写入通道编号(如 0),系统才会在 pwmchipX 下生成一个 pwmX 目录,供你操作;
  • unexport:使用完毕后,向这个文件写入编号,撤销对该通道的占用,删除对应的目录。

查看通道数量:

[root@LS-GD ~]# cat /sys/class/pwm/pwmchip0/npwm
1
4.2.2 导出PWM通道

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

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

会生成 /sys/class/pwm/pwmchip0/pwm0/目录;

[root@LS-GD ~]# ls -l /sys/class/pwm/pwmchip0/pwm0/
-r--r--r-- 1 root root 16384 Jul 24 21:01 capture
-rw-r--r-- 1 root root 16384 Jul 24 21:01 duty_cycle
-rw-r--r-- 1 root root 16384 Jul 24 21:01 enable
-rw-r--r-- 1 root root 16384 Jul 24 21:01 period
-rw-r--r-- 1 root root 16384 Jul 24 21:01 polarity
drwxr-xr-x 2 root root     0 Jul 24 21:01 power
-rw-r--r-- 1 root root 16384 Jul 24 21:01 uevent

/sys/class/pwm/pwmchip0/pwm0目录下有以下几个文件:

文件名 单位 作用 说明
period 纳秒 (ns) 设置周期 决定 PWM 信号的频率。频率 = 1 / 周期。
duty_cycle 纳秒 (ns) 设置占空比 决定高电平持续的时间。占空比 = duty_cycle / period。
enable 0 或 1 启用/禁用 写入 1 开始输出波形,写入 0 停止输出。
polarity 字符串 设置极性 normal(正常)或 inversed(反转)。
4.2.3 配置PWM参数

设置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

如果此时有逻辑分析仪或者示波器,可以捕获查看GPIO65引脚的输出信号;

4.3 应用程序测试

接着我们进行测试:

[root@LS-GD opt]# scp zhengyang@172.23.34.187:/opt/2k0300/loongson_2k300_lib/example/pwm_app/main /opt
[root@LS-GD opt]# ./main
Initializing motors...
Motor control ready!
Forward...
Turn left...
Stop.

如果这个时候我们接了驱动板,并且连接了小车的电机,我们就可以验证电机是否按照我们的设定运行;

使用逻辑分析仪捕获到的GPIO66GPIO74GPIO65GPIO75、引脚信号如下;

可以看到前半段波形是Forward时的,后半段是Turn left时的。

五、代码下载

loongson_2k300_lib

参考文章:

[1] 基于直流有刷电机的速度闭环控制以及matlab仿真

[2] STM32F103直流有刷电机速度闭环控制

[3] Rockchip RK3399 - GPIO&PWM风扇调试

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