
??想了解更多關于開源的內容,請訪問:??
??51CTO 開源基礎軟件社區(qū)??
??https://ost.51cto.com??
本文簡要介紹對比基于linux內核開發(fā)PWM平臺驅動的方案,在平臺驅動開發(fā)完成后可以合入HDF框架作為Openharmony底層驅動方案,之前寫完驅動GPIO方案LINUX驅動基礎以及合入openharmony的文章后有同學問其他的外設怎么合入,有沒有更簡單易用的方法開發(fā)陌生的linux開發(fā)板和系統(tǒng),本章接著介紹PWM接口技術,以及設備樹構造技術來進行簡單解析。
本次實踐部分使用九聯UnionPi開發(fā)板演示。
PWM技術基礎
PWM全稱Pulse Width Modulation:脈沖寬度調制(簡稱脈寬調制,通俗的講就是調節(jié)脈沖的寬度),是電子電力應用中非常重要的一種控制技術。
通過PWM可以控制實現原本閾值電壓范圍內電壓值的控制.PWM有非常廣泛的應用,比如直流電機的無極調速,開關電源、逆變器等等。

上述公式可以帶來的效果包括但不限于希望一個點燈以50%亮度點亮時可以使用該方法完成,設置一個占空比為50%的PWM波輸出即可,而在用PWM控制電機轉速時,占空比的降低可以使得電機轉速降低,而占空比的增加會使得電機轉速的增加。
PWM技術中需要關注的參數包括:
- PWM周期:PWM周期是指載波信號重復的時間。它的值取決于系統(tǒng)中使用的計時器和計數器的設置,可以根據需求進行調整。通常情況下,PWM周期越短,電機或LED的響應速度越快。
- 占空比:占空比是指PWM信號中高電平的時間與周期時間的比例。例如,50%的占空比意味著高電平時間等于周期時間的一半。占空比越高,電機或LED的亮度或速度越高。
- 分辨率:分辨率是指PWM信號能夠表達的離散級別數量。它取決于系統(tǒng)的計時器和計數器分辨率,以及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輸出引腳的模式和狀態(tài)。通過IO控制寄存器,可以選擇PWM輸出引腳的功能和輸出模式,例如單極性輸出或雙極性輸出。還可以設置引腳的電平、電流和電壓等參數。
- 中斷寄存器:中斷寄存器用于設置PWM中斷功能。當PWM周期結束時,可以通過中斷寄存器來觸發(fā)中斷事件,并在中斷服務程序中執(zhí)行一些操作。
下圖為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;
按照基本寄存器操作邏輯整體計算占空比的公式如下:

頻率計算公式為:
*(psc%2B1)%7D)
如果使用文中的九聯UnionPi開發(fā)板,將會用到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
- 通過閱讀開發(fā)板原理圖和開發(fā)板手冊了解PWM口物理連接接口
- 創(chuàng)建PWM設備節(jié)點: 創(chuàng)建一個新的PWM設備節(jié)點,可以使用misc設備驅動程序或platform_device框架。在創(chuàng)建設備節(jié)點時,需要指定設備ID、名稱和相關的設備數據。
- 初始化PWM設備: 在PWM設備初始化期間,需要設置PWM時鐘、周期、占空比和極性等參數。可以通過PWM子系統(tǒng)提供的函數進行操作。
- 實現PWM設備控制函數: 這些函數提供了PWM設備的讀寫訪問??梢允褂胹ysfs接口或misc設備驅動程序提供的ioctl函數進行實現。
- 注冊PWM設備: 注冊PWM設備以啟用設備??梢允褂胢isc設備驅動程序或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子系統(tǒng)提供的接口來訪問PWM控制器的寄存器。PWM子系統(tǒng)提供了一組API函數,用于初始化PWM、設置PWM周期和占空比、開啟/關閉PWM輸出等操作,這些API函數會自動設置PWM控制器的寄存器。
例如,在上面的PWM驅動程序示例中,使用了PWM子系統(tǒng)提供的函數pwm_request()、pwm_init()、pwm_enable()等,這些函數會自動配置PWM控制器的寄存器以實現PWM輸出。因此,可以通過使用PWM子系統(tǒng)提供的接口,避免手動配置PWM控制器的寄存器,從而簡化驅動程序的編寫。
PWM子系統(tǒng)中的具體接口函數屬性如下:
- struct pwm_device:表示一個PWM通道,定義在include/linux/pwm.h文件中。
- struct pwm_state:表示一個PWM通道的狀態(tài),包括周期、占空比等參數,定義在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
常規(guī)在LINUX內核環(huán)境中進行驅動調用的方式有兩種,一種是重寫底層硬件配置調用platform_device_register()函數來完成注冊操作,以及可以使用文件系統(tǒng)調用內核中已經寫好的驅動,如GPIO章節(jié)描述的方法類似,另一種是通過設備樹來完成底層硬件和內核態(tài)程序間的隔離和連接。
如果想調用剛剛寫好的驅動,通過設備樹的方式可以將節(jié)點注冊到設備樹中。
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芯片而言。
重啟系統(tǒng)剛剛注冊的設備和驅動可以在前臺通過讀寫文件的方式進行控制
$ 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
};
// 創(chuàng)建一個平臺設備
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開發(fā)板設備樹中已經配置完成具備兩個硬件PWM引腳,分別對應在:
PWM1 /sys/class/pwm/pwmchip0/pwm0
PWM2/sys/class/pwm/pwmchip2/pwm0
案例實戰(zhàn)
接下來通過一個實際案例演示PWM操作呼吸燈。//代碼和工程文件加在附件中。
通過文件系統(tǒng)和終端操作已經完成驅動配置的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); //使能狀態(tài)
呼吸燈實現原理:
更改脈沖電平寬度,不斷連續(xù)減小。
(代碼為邏輯示意代碼,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口演示使用。
??想了解更多關于開源的內容,請訪問:??
??51CTO 開源基礎軟件社區(qū)??
??https://ost.51cto.com??