#创作者激励#【FFH】openharmony南向研究(5)-linux驱动框架-PWM 原创 精华
【本文正在参加2023年第一期优质创作者激励计划】
本文简要介绍对比基于linux内核开发PWM平台驱动的方案,在平台驱动开发完成后可以合入HDF框架作为Openharmony底层驱动方案,之前写完驱动GPIO方案LINUX驱动基础以及合入openharmony的文章后有同学问其他的外设怎么合入,有没有更简单易用的方法开发陌生的linux开发板和系统,本章接着介绍PWM接口技术,以及设备树构造技术来进行简单解析。
本次实践部分使用九联UnionPi开发板演示
PWM技术基础
PWM全称Pulse Width Modulation:脉冲宽度调制(简称脉宽调制,通俗的讲就是调节脉冲的宽度),是电子电力应用中非常重要的一种控制技术
通过PWM可以控制实现原本阈值电压范围内电压值的控制.PWM有非常广泛的应用,比如直流电机的无极调速,开关电源、逆变器等等。
$$
U_{Real}=U_{幅值} \times T_{占空比}
$$
上述公式可以带来的效果包括但不限于希望一个点灯以50%亮度点亮时可以使用该方法完成,设置一个占空比为50%的PWM波输出即可,而在用PWM控制电机转速时,占空比的降低可以使得电机转速降低,而占空比的增加会使得电机转速的增加。
PWM技术中需要关注的参数包括:
- PWM周期:PWM周期是指载波信号重复的时间。它的值取决于系统中使用的计时器和计数器的设置,可以根据需求进行调整。通常情况下,PWM周期越短,电机或LED的响应速度越快。
- 占空比:占空比是指PWM信号中高电平的时间与周期时间的比例。例如,50%的占空比意味着高电平时间等于周期时间的一半。占空比越高,电机或LED的亮度或速度越高。
- 分辨率:分辨率是指PWM信号能够表达的离散级别数量。它取决于系统的计时器和计数器分辨率,以及PWM周期的长度。较高的分辨率意味着我们可以更精细地控制PWM信号的占空比和输出精度。
在通常使用的芯片中通过对以下几个寄存器的配置可以实现PWM的控制。
- TMR(计时器)寄存器:TMR寄存器用于设置PWM信号的载波周期。通过设置TMR寄存器的计数值和预置值,可以确定PWM信号的频率。TMR寄存器的计数值决定了PWM周期的长度,预置值决定了载波信号周期的长度。通过调整计数值和预置值,可以调整PWM周期和频率。
- PR(预置器)寄存器:PR寄存器用于设置PWM信号的载波周期的预置值。预置值决定了载波信号周期的长度,它是TMR寄存器计数值的上限。当TMR寄存器的计数值达到预置值时,计数器会重新计数并产生一个新的PWM周期。
- PWM占空比寄存器:PWM占空比寄存器用于设置PWM信号的占空比。在PWM周期内的高电平时间和低电平时间是由PWM占空比寄存器的值决定的。通过改变PWM占空比寄存器的值,可以调整PWM信号的占空比,从而控制输出的电压或电流大小。
- IO控制寄存器:IO控制寄存器用于设置PWM输出引脚的模式和状态。通过IO控制寄存器,可以选择PWM输出引脚的功能和输出模式,例如单极性输出或双极性输出。还可以设置引脚的电平、电流和电压等参数。
- 中断寄存器:中断寄存器用于设置PWM中断功能。当PWM周期结束时,可以通过中断寄存器来触发中断事件,并在中断服务程序中执行一些操作。
下图为PR寄存器值为8,TMR寄存器值为4,最终输出信号位OCXREF,占空比为44.4%。
在常用的芯片中对应不同的寄存器,但是核心逻辑还是在于TMR寄存器和PR寄存器,例如在STM32中预置器寄存器又叫做TIMx_ARR定时器重载控制器,需要读者仔细甄别。
寄存器具体作用方式如下:
首先在定时器以PWM模式工作时会将计数器计数模式设置为向上计数模式,并使能计时器。(此处为示意代码)
// 将定时器设置为PWM模式
TMRx->CCP |= TMR_CCP_PWM;
// 设置计数模式为向上计数模式
TMRx->CON &= ~TMR_CON_CM_MASK;
TMRx->CON |= TMR_CON_CM_UP;
// 使能定时器
TMRx->CON |= TMR_CON_TEN;
再配置定时器相关参数,设定定时器的分频值,PWM的周期长度和PWM的占空比
// 设置定时器的时钟分频系数
TMRx->PSC = prescaler_value;
// 设置PWM周期的长度
TMRx->PR = period_value;
// 设置PWM波形的占空比
TMRx->PWM = pulse_value;
prescaler_value为定时器的时钟分频系数,用于设置定时器的时钟频率,period_value为PWM周期的长度,即PR寄存器的值,pulse_value为PWM波形的占空比,即TMR寄存器的值。
最后启动PWM输出
// 启动定时器
TMRx->CON |= TMR_CON_TEN;
按照基本寄存器操作逻辑整体计算占空比的公式如下
$$
P =\frac{CCR-1}{PR}100%
$$
频率计算公式为
$$
f_p=\frac{f_c}{(pr+1)(psc+1)}
$$
如果使用文中的九联UnionPi开发板,将会用到Amlogic A311D芯片,以下例程可以单独通过寄存器实现占空比为50%,频率为4000Hz的PWM输出,
// 定义定时器TMRx
struct meson_pwm *pwm_dev = (struct meson_pwm *)PWM_BASE;
// 计算PWM周期和占空比的值
uint32_t period_value;
uint32_t pulse_value;
period_value = PWM_CLOCK_RATE / 4000 - 1; // PWM周期 = PWM时钟频率 / 频率 - 1
pulse_value = period_value / 2; // PWM波形的占空比 = Pulse / (Period + 1)
// 配置PWM时钟频率
pwm_dev->pre_divider = 0; // PWM时钟频率 = 24MHz / (2 * (pre_divider + 1))
pwm_dev->divider = 0;
// 配置PWM输出通道
pwm_dev->enables = 1 << PWM_CHANNEL; // 使能PWM输出
pwm_dev->hi_time[PWM_CHANNEL] = pulse_value;
pwm_dev->lo_time[PWM_CHANNEL] = pulse_value;
// 启动PWM输出
pwm_dev->config = 1; // 启动PWM输出
其中,PWM_BASE表示PWM控制器的基地址;PWM_CHANNEL表示要使用的PWM输出通道,取值为0、1、2或3;period_value和pulse_value分别表示PWM周期和占空比的值,根据占空比为50%、频率为4000Hz可以计算出period_value=PWM_CLOCK_RATE/4000-1,pulse_value=period_value/2。
另外,需要注意的是,在Amlogic A311D芯片中,PWM波形的占空比是由hi_time和lo_time寄存器的值决定的。在上面的代码中,我们通过修改pwm_dev->hi_time[PWM_CHANNEL]和pwm_dev->lo_time[PWM_CHANNEL]参数来设置占空比的值,实际上是将该值写入hi_time和lo_time寄存器中。因此,在不同的通道上设置占空比时,需要根据具体的寄存器名和通道号来设置pwm_dev->hi_time和pwm_dev->lo_time参数。
LINUX驱动中配置PWM
- 通过阅读开发板原理图和开发板手册了解PWM口物理连接接口
- 创建PWM设备节点: 创建一个新的PWM设备节点,可以使用misc设备驱动程序或platform_device框架。在创建设备节点时,需要指定设备ID、名称和相关的设备数据。
- 初始化PWM设备: 在PWM设备初始化期间,需要设置PWM时钟、周期、占空比和极性等参数。可以通过PWM子系统提供的函数进行操作。
- 实现PWM设备控制函数: 这些函数提供了PWM设备的读写访问。可以使用sysfs接口或misc设备驱动程序提供的ioctl函数进行实现。
- 注册PWM设备: 注册PWM设备以启用设备。可以使用misc设备驱动程序或platform_device框架提供的函数进行操作。
#include <linux/module.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/pwm.h>
#define DRIVER_NAME "my_pwm_driver" // 驱动程序名称
static struct pwm_device *pwm_dev; // PWM设备指针
static int duty_cycle_percent = 50; // 占空比(默认50%)
static int my_pwm_driver_probe(struct platform_device *pdev) // 驱动程序的probe函数
{
struct device *dev = &pdev->dev;
struct pwm_args args; // PWM参数结构体
args.period = 1000000; // PWM周期(1ms)
args.duty_cycle = 500000; // PWM占空比(50%)
args.polarity = PWM_POLARITY_NORMAL; // PWM极性
// 请求PWM设备,返回PWM设备指针
pwm_dev = pwm_request(dev, 0, DRIVER_NAME);
if (IS_ERR(pwm_dev)) { // 如果请求PWM设备失败
dev_err(dev, "Failed to request PWM device\n"); // 打印错误信息
return PTR_ERR(pwm_dev); // 返回错误码
}
pwm_init(pwm_dev, &args); // 初始化PWM设备
pwm_enable(pwm_dev); // 启用PWM设备
return 0; // 返回成功
}
static int my_pwm_driver_remove(struct platform_device *pdev) // 驱动程序的remove函数
{
pwm_disable(pwm_dev); // 关闭PWM输出
pwm_free(pwm_dev); // 释放PWM设备
return 0; // 返回成功
}
static struct platform_driver my_pwm_driver = { // 平台驱动程序结构体
.probe = my_pwm_driver_probe, // 驱动程序的probe函数
.remove = my_pwm_driver_remove, // 驱动程序的remove函数
.driver = {
.name = DRIVER_NAME, // 驱动程序名称
},
};
module_platform_driver(my_pwm_driver); // 注册平台驱动程序
MODULE_LICENSE("GPL"); // 许可证信息
MODULE_DESCRIPTION("PWM driver for my platform"); // 驱动程序描述信息
在编写PWM驱动程序时,需要配置PWM控制器的寄存器以实现PWM输出。PWM控制器的寄存器地址和控制位的具体定义因芯片而异,因此需要根据具体的硬件平台进行编写。
在Linux内核中,通常使用PWM子系统提供的接口来访问PWM控制器的寄存器。PWM子系统提供了一组API函数,用于初始化PWM、设置PWM周期和占空比、开启/关闭PWM输出等操作,这些API函数会自动设置PWM控制器的寄存器。
例如,在上面的PWM驱动程序示例中,使用了PWM子系统提供的函数pwm_request()、pwm_init()、pwm_enable()等,这些函数会自动配置PWM控制器的寄存器以实现PWM输出。因此,可以通过使用PWM子系统提供的接口,避免手动配置PWM控制器的寄存器,从而简化驱动程序的编写。
PWM子系统中的具体接口函数属性如下:
-
struct pwm_device
:表示一个PWM通道,定义在include/linux/pwm.h文件中。 -
struct pwm_state
:表示一个PWM通道的状态,包括周期、占空比等参数,定义在include/linux/pwm.h文件中。 -
struct pwm_ops
:表示PWM驱动程序的操作函数,包括配置PWM通道、使能/禁用PWM输出等,定义在include/linux/pwm.h文件中。 -
struct platform_device
:表示一个平台设备,包含设备名、设备资源等信息,定义在include/linux/platform_device.h文件中。 -
pwm_get
:获取一个PWM通道,定义在drivers/pwm/core.c文件中。 -
pwm_request
:申请一个PWM通道,定义在drivers/pwm/core.c文件中。 -
pwm_free
:释放一个PWM通道,定义在drivers/pwm/core.c文件中。 -
pwm_config
:配置PWM通道的参数,包括周期、占空比等,定义在drivers/pwm/core.c文件中。 -
pwm_enable
:使能PWM输出,定义在drivers/pwm/core.c文件中。 -
pwm_disable
:禁用PWM输出,定义在drivers/pwm/core.c文件中。 -
pwm_apply_state
:将PWM通道的配置应用到硬件上,定义在drivers/pwm/core.c文件中。这些函数和结构体的实现可能会因为Linux内核版本不同而有所变化,因此在编写设备驱动程序时需要仔细阅读对应内核版本的源代码和文档。
在上述代码中我们已经完成了标注的PWM驱动编写,接下来要对其进行调用。
对其进行编译合并入内核
$ make
$ insmod my_pwm_driver.ko
常规在LINUX内核环境中进行驱动调用的方式有两种,一种是重写底层硬件配置调用platform_device_register()函数来完成注册操作,以及可以使用文件系统调用内核中已经写好的驱动,如GPIO章节描述的方法类似,另一种是通过设备树来完成底层硬件和内核态程序间的隔离和连接。
如果想调用刚刚写好的驱动,通过设备树的方式可以将节点注册到设备树中。
my_pwm_device {
compatible = "my_pwm_driver"; //关键的设备树配置 需要和驱动中的驱动名一致
};
pwm_ef : pwm@86c0 {
compatible ="amlogic,meson8-pwm","amlogic,meson8b-pwm";
reg=<0x86c0 0x10>;
#pwm-cells =<3>;
status ="disabled";
} //完整的设备树配置
设备树的参数属性详细解释如下所示。
pwm1: pwm@f0038000 {
compatible = "fsl,imx6q-pwm", "fsl,imx27-pwm"; // 兼容性字符串
reg = <0x0 0xf0038000 0x0 0x4000>; // PWM控制器寄存器地址和大小
interrupts = <0x0 0x38 0x4>; // PWM控制器的中断号
clocks = <0x1 0x7d>; // PWM控制器的时钟
clock-names = "ipg"; // PWM控制器时钟名称
#pwm-cells = <0x2>; // 每个PWM单元包含两个细胞
pinctrl-names = "default"; // 引脚控制器名称
pinctrl-0 = <0x41>; // 引脚控制器配置编号
};
我们需要将设备树写入到dtsi文件当中,该文件目录位于 kernel/linux/linux-5.10/arch/arm/boot/dts/meson8.dtsi。同时通过搜索PWM,PINCTRL可以查看到厂商以及合入的内核设备树,我们可以在此处添加或修改新的硬件对于amlogic a311d芯片而言。
重启系统刚刚注册的设备和驱动可以在前台通过读写文件的方式进行控制
$ echo 1 > /sys/class/pwm/pwmchip0/export
$ echo 5000000 > /sys/class/pwm/pwmchip0/pwm0/period
$ echo 2500000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle
$ echo 1 > /sys/class/pwm/pwmchip0/pwm0/enable
第二种不通过设备树的驱动编写方案如下,其核心思路是注册该设备,即将该设备添加到Linux内核中的设备列表中,可以使用platform_device_register()
函数来完成注册操作。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/pwm.h>
static struct pwm_device *pwm_dev;
static int __init my_pwm_init(void)
{
int ret;
struct platform_device *pdev;
struct pwm_args args = {
.period = 1000000, // 1MHz
.duty_cycle = 500000, // 50% duty cycle
};
// 创建一个平台设备
pdev = platform_device_alloc("my_pwm_device", -1);
if (!pdev) {
printk(KERN_ERR "Failed to allocate platform device\n");
return -ENOMEM;
}
// 注册平台设备
ret = platform_device_add(pdev);
if (ret) {
printk(KERN_ERR "Failed to add platform device\n");
goto err_free_device;
}
// 获取PWM设备
pwm_dev = pwm_request(0, "my_pwm_driver");
if (IS_ERR(pwm_dev)) {
ret = PTR_ERR(pwm_dev);
printk(KERN_ERR "Failed to request PWM device: %d\n", ret);
goto err_remove_device;
}
// 配置PWM设备
ret = pwm_apply_args(pwm_dev, &args);
if (ret) {
printk(KERN_ERR "Failed to apply PWM arguments: %d\n", ret);
goto err_free_pwm;
}
// 使能PWM输出
ret = pwm_enable(pwm_dev);
if (ret) {
printk(KERN_ERR "Failed to enable PWM device: %d\n", ret);
goto err_free_pwm;
}
printk(KERN_INFO "PWM driver loaded successfully\n");
return 0;
err_free_pwm:
pwm_free(pwm_dev);
err_remove_device:
platform_device_unregister(pdev);
err_free_device:
platform_device_put(pdev);
return ret;
}
static void __exit my_pwm_exit(void)
{
// 禁用PWM输出
pwm_disable(pwm_dev);
// 释放PWM设备
pwm_free(pwm_dev);
// 移除平台设备
platform_device_unregister(pwm_dev->chip->pdev);
printk(KERN_INFO "PWM driver unloaded successfully\n");
}
module_init(my_pwm_init);
module_exit(my_pwm_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Example PWM driver");
目前可以在.dtsi文件中查找到A311D开发板设备树中已经配置完成具备两个硬件PWM引脚,分别对应在
PWM1
/sys/class/pwm/pwmchip0/pwm0
PWM2
/sys/class/pwm/pwmchip2/pwm0
案例实战
接下来通过一个实际案例演示PWM操作呼吸灯。//代码和工程文件加在附件中
通过文件系统和终端操作已经完成驱动配置的PWM引脚
$ echo 0 > /sys/class/pwm/pwmchip0
$ echo 0 > /sys/class/pwm/pwmchip2
以上两行指令在pwmchip0和pwmchip2中生成引脚目录pwm0
也可以进入pwm0目录查看内涵属性
打开pwm和关闭pwm
$ echo 1 > /sys/class/pwm/pwmchip0/pwm0/enabled
$ echo 0 > /sys/class/pwm/pwmchip0/pwm0/enabled
设置周期值
$ echo 10000000 > /sys/class/pwm/pwmchip0/pwm0/period
设置高电平时间
$ echo 5000000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle
此时占空比=duty_cycle/period=50%
设置PWM极性
$ echo normal > /sys/class/pwm/pwmchip0/pwm0/polarity
$ echo inversed > /sys/class/pwm/pwmchip0/pwm0/polarity
## 代码实现版本-实现简单呼吸灯
详细代码见附件
```C
//接口函数如下
int init_pwm(int channel);
int set_period(int channel,int period); //设置周期
int set_duty_cycle(int channel,int dutycycle); //设置高电平
int set_pwm_polarity(int channel, int polarity); //设置极性
int set_enable(int channel,int enable_s); //使能状态
呼吸灯实现原理:
更改脉冲电平宽度,不断连续减小
(代码为逻辑示意代码,STM32版本,具体参见附件)
uint32 T = 1600; // 周期(脉冲宽度)
uint32 i=0,m=0,n=0,t=0;
pwm1ES= 1; // 输出使能
pwm1AT = 1; // 灭
while (1)
{
for (i=0;i<T;i++)
{
pwm1AT = 0; // 亮
for (m=0;m<t;m++);
pwm1AT = 1; // 灭
for (n=0;n<T-t;n++);
t++;
if (t >= T)
{
for (i=0;i<T;i++)
{
pwm1AT = 0; // 亮
for (m=0;m<t;m++);
pwm1AT = 1; // 灭
for (n=0;n<T-t;n++);
t--;
}
}
}
}
该方法不适用精准定时器,只做PWM口演示使用。
大佬求一份呼吸灯的代码和工程文件
同求代码
大佬好强
九联UnionPi开发板使用效果可以分享下吗
下周找时间再复现一次效果,之前做出来的时候没有录效果视频和照片下周更新这篇
代码文件已更新
感谢更新