自定义PWM IP核linux驱动
AXI IP核简介
通过自定义IP核可以简化系统设计和缩短产品上市时间的目的。AXI4接口的IP核用于PS和PL端的数据通信,这里我们通过创建一个带有AXI4接口的IP核,该IP核通过AXI协议实现PS和PL端的数据通信。关于AXI协议后续在介绍。这里我们简单看一下系统框图
PL端的MIO连接到UART,用于打印信息。PS通过AXI接口为LED IP模块发送配置数据从而控制有PWM需求的外设。
IP核设计
这一部分一般是由FPGA工程师完成的,因此,我们只需要简单的阅读懂IP核模块干了什么事就好了,接下来就可以直接去用到IP核。这里简单介绍一下Breath LED IP核的一些寄存器配置。
控制寄存器(0x0)
AXI PWM IP模块提供了一个32位的控制寄存器,寄存器地址偏移量为0,向寄存器写入1表示使能PWM输出,写入0则表示禁止PWM输出,但目前实现的多路PWM输出同时生效的,如果需要单独控制每一路还需自己去修改IP的顶层代码。
状态寄存器(0x4)
一个32位的状态寄存器,寄存器地址偏移量为0x4
周期配置寄存器(0x8)
一个32位的PWM周期配置寄存器,寄存器地址偏移量为0x8。与控制寄存器一样,同样在配置多路PWM输出的情况下,该寄存器的配置对多路PWM输出都是同时生效的。
PWM输出的做小周期就是等于输入的时钟周期,如果该寄存器配置100,就是代表PWM输出周期为100个时钟周期
占空比配置寄存器(0x40~0x7C)
占空比配置寄存器一共有16个,寄存器地址偏移量为0x40-0x7c,对应16路输出。
Linux下PWM设备驱动框架
接下来才是我们关注的重点内容,linux内核开发者针对PWM设备设计了一套PWM设备驱动框架,那么接下来我们对PWM设备驱动框架做一个简单的了解
重要结构体
PWM设备驱动框架使用struct pwm_chip结构体来描述一个PWM设备,该设备定义在include/linux/pwm.h头文件中
struct pwm_chip{
struct device *dev; // 指向device对象
struct list_head list; // 链表节点
const struct pwm_ops *ops; // 指向pwm_ops对象
int base; // PWM通道的起始编号
unsigned int npwm; // PWM通道的数量
struct pwm_device *pwms; // 指向pwm_device对象数组
struct pwm_device *(*of_xlate)(struct pwm_chip *pc,
const struct of_phandle_args *args);
unsigned int of_pwm_n_cells;
};
list是一个链表节点,当我们向PWM设备驱动框架核心层注册设备时,核心层维护了一个链表,所有注册的PWM设备都会链接到这个链表上,由核心层统一管理维护。
ops指针指向了一个struct pwm_ops类型的对象
struct pwm_ops{
/* 申请PWM的方法 */
int(*request)(struct pwm_chip *chip,struct pwm_device *pwm);
/* 释放PWM的方法 */
void (*free)(struct pwm_chip *chip,struct pwm_device *pwm);
/* 配置PWM的方法 */
int (*config)(struct pwm_chip *chip,struct pwm_device *pwm,
int duty_ns,int period_ns);
/* 设置PWM极性的方法 */
int (*set_polarity)(struct pwm_chip *chip,struct pwm_device *pwm,
enum pwm_polarity polarity);
int (*capture)(struct pwm_chip *chip,struct pwm_device *pwm,
struct pwm_capture *result,unsigned long timeout);
/* PWM输出使能的方法 */
int (*enable)(struct pwm_chip *chip,struct pwm_device *pwm);
/* PWM输出禁止的方法 */
void (*disable)(struct pwm_chip *chip,struct pwm_device *pwm);
int (*apply)(struct pwm_chip *chip,struct pwm_device *pwm,
struct pwm_state *state);
void (*get_state)(struct pwm_chip *chip,struct pwm_device *pwm,
struct pwm_state *state);
#define CONFIG_DEBUF_FS
void (*dbg_show)(struct pwm_chip *chip,struct seq_file *s);
#endif
struct module *owner;
};
再来看一看struct pwm_device这个结构体的内容
struct pwm_device{
const char *lable; // 通道名字
unsigned long flags;
unsigned int hwpwm; // PWM相对索引
unsigned int pwm; // PWM全局索引
struct pwm_chip *chip; // 指向PWM_chip对象
void *chip_data;
struct pwm_args args;
struct pwm_state state;
};
chip指针指向PWM设备的实例化对象,当我们在编写驱动的时候,并不需要我们自己在驱动程序去手动实例化pwm_device对象,当调用框架提供的注册函数时,我们不需要自己在驱动程序区手动实例化pwm_device对象。
注册 卸载PWM设备
/* 设备注册接口函数 注册成功返回0 否则为负数 */
int pwmchip_add(struct pwm_chip *chip);
/* 设备卸载接口函数 成功返回0 否则为负数 */
int pwmchip_remove(struct pwm_chip *chip);
编写驱动
首先对于AXI PWM设备来说,不挂在任何总线上,所以PWM驱动程序基于虚拟的platform总线进行编写
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/pwm.h>
#include <linux/io.h>
#include <linux/clk.h>
/*
* AXI_PWM寄存器定义
*/
#define PWM_AXI_CTRL_REG_OFFSET 0
#define PWM_AXI_STATUS_REG_OFFSET 4
#define PWM_AXI_PERIOD_REG_OFFSET 8
#define PWM_AXI_DUTY_REG_OFFSET 64
/* 自定义axi_pwm设备结构体 */
struct dglnt_pwm_dev{
struct device *dev; // device对象指针
struct pwm_chip chip; // 内置pwm_chip成员
struct clk *pwm_clk; // axi_pwm IP输入时钟
void __iomem *base; // axperiodi_pwm IP寄存器基地址
unsigned int period_min_ns; // pwm 输出最小周期
};
#define S_TO_NS 1000000000U //秒换算成纳秒的量级单位
/* 写寄存器操作函数 */
static inline void dglnt_pwm_writel(struct dglnt_pwm_dev *dglnt_pwm,
u32 reg,u32 val){
writel(val,dglne_pwm->base + reg);
}
static inline struct dglnt_pwm_dev *to_dglnt_pwm_dev(struct pwm_chip *chip){
return container_of(chip,struct dglnt_pwm_dev,chip);
}
/* pwm 配置
* duty_ns : PWM占空比ns
* period_ns : PWM周期ns
* @return : 成功返回0 失败返回负数
*/
static int dglnt_pwm_config(struct pwm_chip*chip,struct pwm_device *pwm,
int duty_ns,int period_ns){
int duty,period;
struct dglnt_pwm_dev *dglnt_pwm = to_dglnt_pwm_dev(chip);
if(dglnt_pwm->period_min_ns > period_ns){
period_ns = dglnt_pwm->period_min_ns;
}
period = period_ns / dglnt_pwm->period_min_ns;
duty = duty_ns / dglnt_pwm->period_min_ms;
printk(KERN_INFO"period=%d duty=%d\n",period,duty);
dglnt_pwm_writel(dglnt_pwm,PWM_AXI_PERIOD_REG_OFFSET,period);
dglnt_pwm_writel(dglnt_pwm,PWM_AXI_DUTY_REG_OFFSET + (4 * pwm->hwpwm),duty);
return 0;
}
/* axi_pwm 使能 */
static int dglnt_pwm_enable(struct pwm_chip *chip,struct pwm_device *pwm){
struct dglnt_pwm_dev *dglnt_pwm = to_dglnt_pwm_dev(chip);
dglnt_pwm_writel(dglnt_pwm,PWM_AXI_CTRL_REG_OFFSET,1);
return 0;
}
/* axi_pwm 禁用 */
static void dglnt_pwm_disable(struct pwm_chip *chip,struct pwm_device *pwm){
struct dglnt_pwm_dev *dglnt_pwm = to_dglnt_pwm_dev(chip);
dglnt_pwm_writel(dglnt_pwm,PWM_AXI_CTRL_REG_OFFSET,0);
}
/* PWM操作函数集 struct pwm_ops结构体对象 */
static const struct pwm_ops dglnt_pwm_ops = {
.config = dglnt_pwm_config,
.enable = dglnt_pwm_enable,
.disable = dglnt_pwm_disable,
.owner = THIS_MODULE,
};
static int dglnt_pwm_probe(struct platform_device *pdev){
struct dglnt_pwm_dev *dglnt_pwm;
unsigned long clk_rate;
struct resource *res;
int ret;
/* 实例化一个dglnt_pwm_dev对象 */
dglnt_pwm = devm_kzalloc(&pdev->dev,sizeof(*dglnt_pwm),GFP_KERNEL);
if(!dglnt_pwm)
return -ENOMEM;
dglnt_pwm->dev = &pdev->dev;
/* 获取平台资源,得到寄存器地址 */
/*Platform_get_resource()用于驱动程序的__init函数中,以获取设备资源的结构信息,如起始地址和结束地址,以便找到资源内存大小,以便在内存中映射它*/
res = platform_get_resource(pdev,IORESOURCE_MEM,0);
dglnt_pwm->base = devm_ioremap_resource(&pdev->dev,res);
if(IS_ERR(dglnt_pwm->base))
return PTR_ERR(dglnt_pwm->base);
/* 获取时钟,得到时钟大小 */
dglnt_pwm->pwm_clk = devm_clk_get(&pdev->dev,"pwm");
if(IS_ERR(dglnt_pwm->pwm_clk)){
dev_err(&pdev->dev,"failed to get pwm clock\n");
return PTR_ERR(dglnt_pwm->pwm_clk);
}
clk_rate = clk_get_rate(dglnt_pwm->pwm_clk);
/* 计算PWM的最小周期 */
dglnt_pwm->period_min_ns = S_TO_NS /clk_rate;
printk(KERN_INFO"pwm_clk=%1d period_min_ns=%d\n",clk_rate,dglnt_pwm->period_min_ns);
/* 注册PWM设备 */
dglnt_pwm->chip.dev = &pdev->dev;
dglnt_pwm->chip.ops = &dglnt_pwm_ops;
dglnt_pwm->chip.base = 0;
ret = of_property_read_u32(pdev->dev.of_node,"npwm",&dglnt_pwm->chip.npwm);
if(ret){
dev_err(&pdev->dev,"failed to read npwm\n");
return ret;
}
ret = pwmchip_add(&dglnt_pwm->chip);
if(0 > ret){
dev_err(&pdev->dev,"pwmchip_add failed: %d \n",ret);
return ret;
}
platform_set_drvdate(pdev,dglnt_pwm);
return 0;
}
static int dglnt_pwm_remove(struct platform_device *pdev){
struct dglnt_pwm_dev *dglnt_pwm = platform_get_drvdata(pdev);
unsigned int i;
/* 先禁止PWM输出 */
for(i=0;i<dglnt_pwm->chip.npwm;i++){
dglnt_pwm_writel(dglnt_pwm,PWM_AXI_CTRL_REG_OFFSET,0);
}
/* 卸载PWM设备 */
return pwmchip_remove(&dglnt_pwm->chip);
}
static const struct of_device_id dglnt_pwm_of_match[] = {
{.compatible = "digilent,axi-pwm",},
{},
};
MODULE_DEVICE_TABLE(of,dglnt_pwm_of_match);
static struct platform_driver dglnt_pwm_driver = {
.driver = {
.name = "dglnt-pwm",
.of_match_table = dglnt_pwm_of_match,
},
.probe = dglnt_pwm_probe,
.remove = dglnt_pwm_remove,
};
module_platform_driver(dglnt_pwm_driver);
MODULE_AUTHOR("XXX");
MODULE_DESCRIPTION("Digilent AXI_PWM IP Core");
MODULE_LICENSE("GPL v2");
总结
老样子,我们又来到了总结时刻,总结有利于我们分析代码的架构,理清里面的逻辑关系。
很明显,pwm是基于platform框架构建的。必然要分析platform框架这一套,两个结构体,两个函数
-
static const struct of_device_id dglnt_pwm_of_match[] = { {.compatible = "digilent,axi-pwm",}, {}, }; static struct platform_driver dglnt_pwm_driver = { .driver = { .name = "dglnt-pwm", .of_match_table = dglnt_pwm_of_match, }, .probe = dglnt_pwm_probe, .remove = dglnt_pwm_remove, };
-
static int dglnt_pwm_probe(struct platform_device *pdev); static int dglnt_pwm_remove(struct platform_device *pdev); module_platform_driver(dglnt_pwm_driver);
关于probe 函数上面已经展开,主要就是申请一个PWM设备,然后分配资源。对pwm进行初始化等等。
remove函数就是禁止pwm输出并卸载PWM设备。