龙芯2k0300走马观碑组PWM驱动移植,如何实现?
摘要:在《龙芯2k0300 - 走马观碑组第21届智能汽车竞赛软硬件设计》中我们使用了DRV8701双路电机驱动,DRV8701双路电机驱动采用门极驱动芯片DRV8701E + N-MOS管TPH1R403NL方案,输入电源5.
在《龙芯2k0300 - 走马观碑组第21届智能汽车竞赛软硬件设计》中我们使用了DRV8701双路电机驱动,DRV8701双路电机驱动采用门极驱动芯片DRV8701E + N-MOS管TPH1R403NL方案,输入电源5.9V ~ 28V均可使用,带过流保护功能。
一、DRV8701双路电机驱动
其中
PH-6P接口用来隔离供电以及信号输入;
红色端子同来提供直流电压输入,输入电压5.9-28V,一般大小为电机可承受的最大电压;
蓝色端子是直流电压输出,输出电压与输入PWM值正相关,连接到电机上。
1.1 电路原理图
电路原理图如下:
1.1.1 DRV8701E
DRV8701E是德州仪器 (TI) 的一款单H桥栅极驱动器,专门用来控制外部的4个N沟道MOSFET,关键特性;
控制接口:它采用PH/EN (相位/使能)逻辑接口。这意味着你只需要两个GPIO信号就能控制电机的正转、反转、刹车和滑行,逻辑非常清晰。
自举供电:它内置了一个电荷泵 (Charge Pump),可以产生驱动高侧N-MOS管所需的高于电源电压的栅极驱动电压(通常为 9.5V),这使得它可以高效地驱动高侧开关。
可调驱动强度:它有一个 IDRIVE 引脚,可以通过一个电阻来调节栅极的驱动电流(从6mA到150mA)。在龙邱方案中(5.9V-28V 输入),这个功能非常有用,可以根据电源电压调整开关速度,平衡开关损耗和电磁干扰 (EMI)。
电流检测与保护:它内置了电流检测放大器,配合外部采样电阻,可以实现 PWM电流斩波(限制启动或堵转电流)。同时具备欠压锁定 (UVLO)、过流保护 (OCP) 和热关断 (TSD) 功能。
1.1.2 TPH1R403NL
这是一款Toshiba的N沟道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个独立的电子开关;
缓冲/驱动(增强信号):当开关“打开”时,它会将输入的信号(0或1)原样输出,但输出能力更强。比如,单片机的引脚电流很小,带不动大功率负载,通过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号引脚被定义为IN1,14 号引脚被定义为 IN2。
1.2.1 PH/EN模式
这种模式使用PH(Phase/相位)和EN(Enable/使能)两个信号来控制电机。逻辑非常直观,适合单片机控制。
1.2.2 PWM模式
这种模式使用IN1和IN2两个信号直接控制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就可以进行右电机反转调速控制。
当PWM波的占空比越大时,蓝色端子输出的电压值与红色端子的电压值越接近,反之则与0V越接近。
PWM波的频率不可以随意设置,频率太低会导致电机运转不畅,振动大,噪音大;频率太高会导致驱动器开关损耗较大,甚至有电机会啸叫而不转的情况。一般1k~30k的PWM频率较为普遍,几百Hz的也有,实际上需要根据电机功率在测试时确定合适的PWM频率范围为宜。
1.3.2 硬件接线
久久派底版控制口原理图如下:
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集成4路PWM控制器,支持输入/输出,根据《龙芯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
注:除芯片启动相关的引脚(SPI0或SDIO0/eMMC0在相应启动模式下对应引脚为主功能) 外,以上复用引脚上电默认状态都复用为GPIO 功能,其中GPIO0~63默认为输入状态,GPIO64~105默认为输出低电平状态。
因此我们可以知道GPIO65、GPIO66引脚是可以服用为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
我们可以看到pwm0、pwm1、pwm2、pwm3节点,但是我们只关注pwm1、pwm2节点;
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-frequency:PWM1控制器的输入时钟频率(单位Hz);
status:设备状态,此时处于禁用状态。
更多有关设备树相关的内容可以参考:《linux设备树-基础介绍》。
2.3.2 使能
修改arch/loongarch/boot/dts/ls2k300_99pi.dtsi,使能pwm1、pwm2;
&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";
};
通过 &pwm1、pwm2 引用这个节点,将 status 改为 "okay",同时根据实际硬件修改引脚配置。
2.3.3 pwm1_mux_m1&pwm2_mux_m1
pwm1_mux_m1、pwm2_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_m0,pwm1_mux_m0这行代码会在寄存器0x160004a0的位[3:2]位写入 01b,而其它位保持不变,用于将芯片的GPIO65这个引脚设置为PWM1功能。
同理对于右电机我们使用的是GPIO66引脚,即我们需要使用的是pwm2_mux_m0,pwm2_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 平台实现
该案例通过用户层提供的GPIO和PWM接口实现了电机控制。
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平台的实现,其通过用户空间接口实现对GPIO和PWM的控制;
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 = 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
}
};
motor_config motor_right = {
.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
}
};
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 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 Flash的dtb分区;
[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
注意: pwmchip0、pwmchip1 只是逻辑编号,它们并不直接对应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(如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 ~]# 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.
如果这个时候我们接了驱动板,并且连接了小车的电机,我们就可以验证电机是否按照我们的设定运行;
使用逻辑分析仪捕获到的GPIO65、GPIO75、GPIO66、GPIO74引脚信号如下;
可以看到前半段波形是Forward时的,后半段是Turn left时的。
