Skip to contentSkip to conversation

mfd: pm8xxx-pwm: add pm8xxx PWM driver

PATCH In review Willie Ruan 2 participants 4 messages Apr 22, 2011 – May 4, 2011

Subsystems (2)
lore
Share & tools
lore
#0 lore raw

PATCH 1/2 mfd: pm8xxx-pwm: add pm8xxx PWM driver

Qualcomm PM8xxx chips, such as PM8058 and PM8921, have 8 channels of PWM, also called LPG (Light Pulse Generator) in HW specs. All PWM channels can be used as simple PWM machine or as a more advanced PWM pattern generator using programmed lookup table.

This patch supports all APIs listed in <linux/pwm.h> with a small difference. The two parameters (duty_ns and period_ns) in pwm_config() are used as values in microseconds instead of nanoseconds. Otherwise a 32-bit integer can't fit for a range of 7 us to 300+ seconds.

---
 drivers/mfd/Kconfig            |   10 +
 drivers/mfd/Makefile           |    1 +
 drivers/mfd/pm8xxx-pwm.c       |  819 ++++++++++++++++++++++++++++++++++++++++
 include/linux/mfd/pm8xxx/pwm.h |   88 +++++
 4 files changed, 918 insertions(+), 0 deletions(-)
 create mode 100644 drivers/mfd/pm8xxx-pwm.c
 create mode 100644 include/linux/mfd/pm8xxx/pwm.h
drivers/mfd/Kconfig +10 -0
index 38320e2..8d97996 100644
@@ -699,6 +699,16 @@ config MFD_PM8XXX_IRQ
 	  This is required to use certain other PM 8xxx features, such as GPIO
 	  and MPP.
 
+config MFD_PM8XXX_PWM
+	tristate "Support for Qualcomm PM8xxx PWM feature"
+	depends on MFD_PM8XXX
+	default y if MFD_PM8XXX
+	help
+	  This is the Pulse Width Modulation (PWM) driver for Qualcomm
+	  PM 8xxx PMIC chips. It can driver 8 channels of PWM output, and
+	  has a lookup table with size of 64 to be shared by any of the
+	  8 channels.
+
 endif # MFD_SUPPORT
 
 menu "Multimedia Capabilities Port drivers"
drivers/mfd/Makefile +1 -0
index f4afcc8..7975466 100644
@@ -90,3 +90,4 @@ obj-$(CONFIG_MFD_CS5535)	+= cs5535-mfd.o
 obj-$(CONFIG_MFD_OMAP_USB_HOST)	+= omap-usb-host.o
 obj-$(CONFIG_MFD_PM8921_CORE) 	+= pm8921-core.o
 obj-$(CONFIG_MFD_PM8XXX_IRQ) 	+= pm8xxx-irq.o
+obj-$(CONFIG_MFD_PM8XXX_PWM)	+= pm8xxx-pwm.o
drivers/mfd/pm8xxx-pwm.c new file +819 -0

819 lines not on this page · raw message

include/linux/mfd/pm8xxx/pwm.h new file +88 -0

88 lines not on this page · raw message

Signature · 5 lines
1.6.5.2

--
Sent by an employee of the Qualcomm Innovation Center, Inc.
The Qualcomm Innovation Center, Inc. is a member of the Code Aurora Forum.
#1 lore raw

PATCH 2/2 mfd: pm8921-core: add pwm device for PM8921

Qualcomm PM8921 has a PWM module which can output 8 channels of PWM signals.

---
 drivers/mfd/pm8921-core.c         |   11 +++++++++++
 include/linux/mfd/pm8xxx/pm8921.h |    1 +
 2 files changed, 12 insertions(+), 0 deletions(-)
drivers/mfd/pm8921-core.c +11 -0
index 2fbd42d..47b1189 100644
@@ -107,6 +107,11 @@ static const struct resource mpp_cell_resources[] __devinitconst = {
 	},
 };
 
+static struct mfd_cell pwm_cell __devinitdata = {
+	.name           = PM8XXX_PWM_DEV_NAME,
+	.id             = -1,
+};
+
 static struct mfd_cell mpp_cell __devinitdata = {
 	.name		= PM8XXX_MPP_DEV_NAME,
 	.id		= -1,
@@ -160,6 +165,12 @@ static int __devinit pm8921_add_subdevices(const struct pm8921_platform_data
 		}
 	}
 
+	ret = mfd_add_devices(pmic->dev, 0, &pwm_cell, 1, NULL, 0);
+	if (ret) {
+		pr_err("Failed to add pwm subdevice ret=%d\n", ret);
+		goto bail;
+	}
+
 	return 0;
 bail:
 	if (pmic->irq_chip) {
include/linux/mfd/pm8xxx/pm8921.h +1 -0
index 19cffb2..8132325 100644
@@ -22,6 +22,7 @@
 #include <linux/mfd/pm8xxx/irq.h>
 #include <linux/mfd/pm8xxx/gpio.h>
 #include <linux/mfd/pm8xxx/mpp.h>
+#include <linux/mfd/pm8xxx/pwm.h>
 
 #define PM8921_NR_IRQS		256
 
Signature · 5 lines
1.6.5.2

--
Sent by an employee of the Qualcomm Innovation Center, Inc.
The Qualcomm Innovation Center, Inc. is a member of the Code Aurora Forum.
#2 lore raw
In reply to Willie Ruan

Hi Willie,

Quoted 9 lines · Willie Ruan

Qualcomm PM8xxx chips, such as PM8058 and PM8921, have 8 channels of PWM, also called LPG (Light Pulse Generator) in HW specs. All PWM channels can be used as simple PWM machine or as a more advanced PWM pattern generator using programmed lookup table.

This patch supports all APIs listed in <linux/pwm.h> with a small difference. The two parameters (duty_ns and period_ns) in pwm_config() are used as values in microseconds instead of nanoseconds. Otherwise a 32-bit integer can't fit for a range of 7 us to 300+ seconds.

I can only lament the fact that we're still lacking a proper PWM API. Besides that, I have a couple comments:

Quoted 58 lines
drivers/mfd/pm8xxx-pwm.c new file +58 -0
+static void pm8xxx_pwm_calc_period(unsigned int period_us,
+					   struct pm8xxx_pwm_config *pwm_conf)
+{
+	int	n, m, clk, div;
+	int	best_m, best_div, best_clk;
+	int	last_err, cur_err, better_err, better_m;
+	unsigned int	tmp_p, last_p, min_err, period_n;
+
+	/* PWM Period / N */
+	if (period_us < (40 * USEC_PER_SEC)) {  /* ~6-bit max */
+		period_n = (period_us * NSEC_PER_USEC) >> 6;
+		n = 6;
+	} else if (period_us < (274 * USEC_PER_SEC)) { /* overflow threshold */
+		period_n = (period_us >> 6) * NSEC_PER_USEC;
+		if (period_n >= MAX_MPT) {
+			n = 9;
+			period_n >>= 3;
+		} else
+			n = 6;
+	} else {
+		period_n = (period_us >> 9) * NSEC_PER_USEC;
+		n = 9;
+	}
+
+	min_err = MAX_MPT;
+	best_m = 0;
+	best_clk = 0;
+	best_div = 0;
+	for (clk = 0; clk < NUM_CLOCKS; clk++) {
+		for (div = 0; div < NUM_PRE_DIVIDE; div++) {
+			tmp_p = period_n;
+			last_p = tmp_p;
+			for (m = 0; m <= PM8XXX_PWM_M_MAX; m++) {
+				if (tmp_p <= pt_t[div][clk]) {
+					/* Found local best */
+					if (!m) {
+						better_err = pt_t[div][clk] -
+							tmp_p;
+						better_m = m;
+					} else {
+						last_err = last_p -
+							pt_t[div][clk];
+						cur_err = pt_t[div][clk] -
+							tmp_p;
+
+						if (cur_err < last_err) {
+							better_err = cur_err;
+							better_m = m;
+						} else {
+							better_err = last_err;
+							better_m = m - 1;
+						}
+					}
+
+					if (better_err < min_err) {
+						min_err = better_err;
+						best_m = better_m;
+						best_clk = clk;

drivers/mfd/pm8xxx-pwm.c line 339

Quoted 10 lines
drivers/mfd/pm8xxx-pwm.c new file +10 -0
+						best_div = div;
+					}
+					break;
+				} else {
+					last_p = tmp_p;
+					tmp_p >>= 1;
+				}
+			}
+		}
+	}

This loop needs to be splitted up into 2-3 different routines. Your code will be much more readable.

Quoted 102 lines
drivers/mfd/pm8xxx-pwm.c new file +102 -0
+static int pm8xxx_pwm_configure(struct pwm_device *pwm,
+			 struct pm8xxx_pwm_config *pwm_conf)
+{
+	int	i, rc, len;
+	u8	reg, ramp_enabled = 0;
+
+	reg = (pwm_conf->pwm_size > 6) ? PM8XXX_PWM_SIZE_9_BIT : 0;
+	pwm->pwm_ctl[5] = reg;
+
+	reg = ((pwm_conf->clk + 1) << PM8XXX_PWM_CLK_SEL_SHIFT)
+		& PM8XXX_PWM_CLK_SEL_MASK;
+	reg |= (pwm_conf->pre_div << PM8XXX_PWM_PREDIVIDE_SHIFT)
+		& PM8XXX_PWM_PREDIVIDE_MASK;
+	reg |= pwm_conf->pre_div_exp & PM8XXX_PWM_M_MASK;
+	pwm->pwm_ctl[4] = reg;
+
+	if (pwm_conf->bypass_lut) {
+		pwm->pwm_ctl[0] &= PM8XXX_PWM_PWM_START; /* keep enabled */
+		pwm->pwm_ctl[1] = PM8XXX_PWM_BYPASS_LUT;
+		pwm->pwm_ctl[2] = 0;
+
+		if (pwm_conf->pwm_size > 6) {
+			pwm->pwm_ctl[3] = pwm_conf->pwm_value
+						& PM8XXX_PWM_VALUE_BIT7_0;
+			pwm->pwm_ctl[4] |= (pwm_conf->pwm_value >> 1)
+						& PM8XXX_PWM_VALUE_BIT8;
+		} else {
+			pwm->pwm_ctl[3] = pwm_conf->pwm_value
+						& PM8XXX_PWM_VALUE_BIT5_0;
+		}
+
+		len = 6;
+	} else {
+		int	pause_cnt, j;
+
+		/* Linear search for duty time */
+		for (i = 0; i < PM8XXX_PWM_1KHZ_COUNT_MAX; i++) {
+			if (duty_msec[i] >= pwm_conf->lut_duty_ms)
+				break;
+		}
+
+		ramp_enabled = pwm->pwm_ctl[0] & PM8XXX_PWM_RAMP_GEN_START;
+		pwm->pwm_ctl[0] &= PM8XXX_PWM_PWM_START; /* keep enabled */
+		pwm->pwm_ctl[0] |= (i << PM8XXX_PWM_1KHZ_COUNT_SHIFT) &
+					PM8XXX_PWM_1KHZ_COUNT_MASK;
+		pwm->pwm_ctl[1] = pwm_conf->lut_hi_index &
+					PM8XXX_PWM_HIGH_INDEX_MASK;
+		pwm->pwm_ctl[2] = pwm_conf->lut_lo_index &
+					PM8XXX_PWM_LOW_INDEX_MASK;
+
+		if (pwm_conf->flags & PM_PWM_LUT_REVERSE)
+			pwm->pwm_ctl[1] |= PM8XXX_PWM_REVERSE_EN;
+		if (pwm_conf->flags & PM_PWM_LUT_RAMP_UP)
+			pwm->pwm_ctl[2] |= PM8XXX_PWM_RAMP_UP;
+		if (pwm_conf->flags & PM_PWM_LUT_LOOP)
+			pwm->pwm_ctl[2] |= PM8XXX_PWM_LOOP_EN;
+
+		/* Pause time */
+		if (pwm_conf->flags & PM_PWM_LUT_PAUSE_HI_EN) {
+			/* Linear search for pause time */
+			pause_cnt = (pwm_conf->lut_pause_hi + duty_msec[i] / 2)
+					/ duty_msec[i];
+			for (j = 0; j < PM8XXX_PWM_PAUSE_COUNT_MAX; j++) {
+				if (pause_count[j] >= pause_cnt)
+					break;
+			}
+			pwm->pwm_ctl[5] = (j <<
+					   PM8XXX_PWM_PAUSE_COUNT_HI_SHIFT) &
+						PM8XXX_PWM_PAUSE_COUNT_HI_MASK;
+			pwm->pwm_ctl[5] |= PM8XXX_PWM_PAUSE_ENABLE_HIGH;
+		} else
+			pwm->pwm_ctl[5] = 0;
+
+		if (pwm_conf->flags & PM_PWM_LUT_PAUSE_LO_EN) {
+			/* Linear search for pause time */
+			pause_cnt = (pwm_conf->lut_pause_lo + duty_msec[i] / 2)
+					/ duty_msec[i];
+			for (j = 0; j < PM8XXX_PWM_PAUSE_COUNT_MAX; j++) {
+				if (pause_count[j] >= pause_cnt)
+					break;
+			}
+			pwm->pwm_ctl[6] = (j <<
+					   PM8XXX_PWM_PAUSE_COUNT_LO_SHIFT) &
+						PM8XXX_PWM_PAUSE_COUNT_LO_MASK;
+			pwm->pwm_ctl[6] |= PM8XXX_PWM_PAUSE_ENABLE_LOW;
+		} else
+			pwm->pwm_ctl[6] = 0;
+
+		len = 7;
+	}
+
+	pm8xxx_pwm_bank_sel(pwm);
+
+	for (i = 0; i < len; i++) {
+		rc = pm8xxx_writeb(pwm->chip->dev->parent,
+				   SSBI_REG_ADDR_LPG_CTL(i),
+				   pwm->pwm_ctl[i]);
+		if (rc) {
+			pr_err("pm8xxx_write(): rc=%d (PWM Ctl[%d])\n", rc, i);
+			break;
+		}
+	}

drivers/mfd/pm8xxx-pwm.c line 458

Quoted 10 lines
drivers/mfd/pm8xxx-pwm.c new file +10 -0
+
+	if (ramp_enabled) {
+		pwm->pwm_ctl[0] |= ramp_enabled;
+		pm8xxx_writeb(pwm->chip->dev->parent,
+			      SSBI_REG_ADDR_LPG_CTL(0),
+			      pwm->pwm_ctl[0]);
+	}
+
+	return rc;
+}

I would also appreciate if this routine could be made smaller by having it calling sub routines.

Cheers,
Samuel.

Signature · 2 lines
Intel Open Source Technology Centre
http://oss.intel.com/
#3 lore raw

Hi Samuel,

Thank you for reviewing my patches. I'll make the two functions smaller by creating sub routines as you suggested.

pm8xxx_pwm_calc_period()
pm8xxx_pwm_configure()

Other comments below.

Quoted 8 lines

Hi Willie,

Quoted 5 lines · Willie Ruan

Qualcomm PM8xxx chips, such as PM8058 and PM8921, have 8 channels of PWM, also called LPG (Light Pulse Generator) in HW specs. All PWM channels can be used as simple PWM machine or as a more advanced PWM pattern generator using programmed lookup table.

Quoted 7 lines
Quoted 4 lines

This patch supports all APIs listed in <linux/pwm.h> with a small difference. The two parameters (duty_ns and period_ns) in pwm_config() are used as values in microseconds instead of nanoseconds. Otherwise a 32-bit integer can't fit for a range of 7 us to 300+ seconds.

I can only lament the fact that we're still lacking a proper PWM API. Besides
that, I have a couple comments:

I agree. Hopefully someone would create a better PWM API soon for us to use. Fortunately PWM driver and API are not hard for our customers.

Quoted 64 lines
Quoted 64 lines
drivers/mfd/pm8xxx-pwm.c new file +13 -0
+static void pm8xxx_pwm_calc_period(unsigned int period_us,
+					   struct pm8xxx_pwm_config *pwm_conf)
+{
+	int	n, m, clk, div;
+	int	best_m, best_div, best_clk;
+	int	last_err, cur_err, better_err, better_m;
+	unsigned int	tmp_p, last_p, min_err, period_n;
+
+	/* PWM Period / N */
+	if (period_us < (40 * USEC_PER_SEC)) {  /* ~6-bit max */
+		period_n = (period_us * NSEC_PER_USEC) >> 6;
+		n = 6;
+	} else if (period_us < (274 * USEC_PER_SEC)) { /* overflow threshold

*/

drivers/mfd/pm8xxx-pwm.c new file +49 -0
+		period_n = (period_us >> 6) * NSEC_PER_USEC;
+		if (period_n >= MAX_MPT) {
+			n = 9;
+			period_n >>= 3;
+		} else
+			n = 6;
+	} else {
+		period_n = (period_us >> 9) * NSEC_PER_USEC;
+		n = 9;
+	}
+
+	min_err = MAX_MPT;
+	best_m = 0;
+	best_clk = 0;
+	best_div = 0;
+	for (clk = 0; clk < NUM_CLOCKS; clk++) {
+		for (div = 0; div < NUM_PRE_DIVIDE; div++) {
+			tmp_p = period_n;
+			last_p = tmp_p;
+			for (m = 0; m <= PM8XXX_PWM_M_MAX; m++) {
+				if (tmp_p <= pt_t[div][clk]) {
+					/* Found local best */
+					if (!m) {
+						better_err = pt_t[div][clk] -
+							tmp_p;
+						better_m = m;
+					} else {
+						last_err = last_p -
+							pt_t[div][clk];
+						cur_err = pt_t[div][clk] -
+							tmp_p;
+
+						if (cur_err < last_err) {
+							better_err = cur_err;
+							better_m = m;
+						} else {
+							better_err = last_err;
+							better_m = m - 1;
+						}
+					}
+
+					if (better_err < min_err) {
+						min_err = better_err;
+						best_m = better_m;
+						best_clk = clk;
+						best_div = div;
+					}
+					break;
+				} else {
Quoted 8 lines

drivers/mfd/pm8xxx-pwm.c line 339

Quoted 6 lines
drivers/mfd/pm8xxx-pwm.c new file +6 -0
+					last_p = tmp_p;
+					tmp_p >>= 1;
+				}
+			}
+		}
+	}

This loop needs to be splitted up into 2-3 different routines. Your code will be much more readable.

Sure I can change this.

Quoted 107 lines
Quoted 107 lines
drivers/mfd/pm8xxx-pwm.c new file +106 -0
+static int pm8xxx_pwm_configure(struct pwm_device *pwm,
+			 struct pm8xxx_pwm_config *pwm_conf)
+{
+	int	i, rc, len;
+	u8	reg, ramp_enabled = 0;
+
+	reg = (pwm_conf->pwm_size > 6) ? PM8XXX_PWM_SIZE_9_BIT : 0;
+	pwm->pwm_ctl[5] = reg;
+
+	reg = ((pwm_conf->clk + 1) << PM8XXX_PWM_CLK_SEL_SHIFT)
+		& PM8XXX_PWM_CLK_SEL_MASK;
+	reg |= (pwm_conf->pre_div << PM8XXX_PWM_PREDIVIDE_SHIFT)
+		& PM8XXX_PWM_PREDIVIDE_MASK;
+	reg |= pwm_conf->pre_div_exp & PM8XXX_PWM_M_MASK;
+	pwm->pwm_ctl[4] = reg;
+
+	if (pwm_conf->bypass_lut) {
+		pwm->pwm_ctl[0] &= PM8XXX_PWM_PWM_START; /* keep enabled */
+		pwm->pwm_ctl[1] = PM8XXX_PWM_BYPASS_LUT;
+		pwm->pwm_ctl[2] = 0;
+
+		if (pwm_conf->pwm_size > 6) {
+			pwm->pwm_ctl[3] = pwm_conf->pwm_value
+						& PM8XXX_PWM_VALUE_BIT7_0;
+			pwm->pwm_ctl[4] |= (pwm_conf->pwm_value >> 1)
+						& PM8XXX_PWM_VALUE_BIT8;
+		} else {
+			pwm->pwm_ctl[3] = pwm_conf->pwm_value
+						& PM8XXX_PWM_VALUE_BIT5_0;
+		}
+
+		len = 6;
+	} else {
+		int	pause_cnt, j;
+
+		/* Linear search for duty time */
+		for (i = 0; i < PM8XXX_PWM_1KHZ_COUNT_MAX; i++) {
+			if (duty_msec[i] >= pwm_conf->lut_duty_ms)
+				break;
+		}
+
+		ramp_enabled = pwm->pwm_ctl[0] & PM8XXX_PWM_RAMP_GEN_START;
+		pwm->pwm_ctl[0] &= PM8XXX_PWM_PWM_START; /* keep enabled */
+		pwm->pwm_ctl[0] |= (i << PM8XXX_PWM_1KHZ_COUNT_SHIFT) &
+					PM8XXX_PWM_1KHZ_COUNT_MASK;
+		pwm->pwm_ctl[1] = pwm_conf->lut_hi_index &
+					PM8XXX_PWM_HIGH_INDEX_MASK;
+		pwm->pwm_ctl[2] = pwm_conf->lut_lo_index &
+					PM8XXX_PWM_LOW_INDEX_MASK;
+
+		if (pwm_conf->flags & PM_PWM_LUT_REVERSE)
+			pwm->pwm_ctl[1] |= PM8XXX_PWM_REVERSE_EN;
+		if (pwm_conf->flags & PM_PWM_LUT_RAMP_UP)
+			pwm->pwm_ctl[2] |= PM8XXX_PWM_RAMP_UP;
+		if (pwm_conf->flags & PM_PWM_LUT_LOOP)
+			pwm->pwm_ctl[2] |= PM8XXX_PWM_LOOP_EN;
+
+		/* Pause time */
+		if (pwm_conf->flags & PM_PWM_LUT_PAUSE_HI_EN) {
+			/* Linear search for pause time */
+			pause_cnt = (pwm_conf->lut_pause_hi + duty_msec[i] / 2)
+					/ duty_msec[i];
+			for (j = 0; j < PM8XXX_PWM_PAUSE_COUNT_MAX; j++) {
+				if (pause_count[j] >= pause_cnt)
+					break;
+			}
+			pwm->pwm_ctl[5] = (j <<
+					   PM8XXX_PWM_PAUSE_COUNT_HI_SHIFT) &
+						PM8XXX_PWM_PAUSE_COUNT_HI_MASK;
+			pwm->pwm_ctl[5] |= PM8XXX_PWM_PAUSE_ENABLE_HIGH;
+		} else
+			pwm->pwm_ctl[5] = 0;
+
+		if (pwm_conf->flags & PM_PWM_LUT_PAUSE_LO_EN) {
+			/* Linear search for pause time */
+			pause_cnt = (pwm_conf->lut_pause_lo + duty_msec[i] / 2)
+					/ duty_msec[i];
+			for (j = 0; j < PM8XXX_PWM_PAUSE_COUNT_MAX; j++) {
+				if (pause_count[j] >= pause_cnt)
+					break;
+			}
+			pwm->pwm_ctl[6] = (j <<
+					   PM8XXX_PWM_PAUSE_COUNT_LO_SHIFT) &
+						PM8XXX_PWM_PAUSE_COUNT_LO_MASK;
+			pwm->pwm_ctl[6] |= PM8XXX_PWM_PAUSE_ENABLE_LOW;
+		} else
+			pwm->pwm_ctl[6] = 0;
+
+		len = 7;
+	}
+
+	pm8xxx_pwm_bank_sel(pwm);
+
+	for (i = 0; i < len; i++) {
+		rc = pm8xxx_writeb(pwm->chip->dev->parent,
+				   SSBI_REG_ADDR_LPG_CTL(i),
+				   pwm->pwm_ctl[i]);
+		if (rc) {
+			pr_err("pm8xxx_write(): rc=%d (PWM Ctl[%d])\n", rc, i);
+			break;
+		}
+	}
+
+	if (ramp_enabled) {
+		pwm->pwm_ctl[0] |= ramp_enabled;
+		pm8xxx_writeb(pwm->chip->dev->parent,
Quoted 8 lines

drivers/mfd/pm8xxx-pwm.c line 458

Quoted 6 lines
drivers/mfd/pm8xxx-pwm.c new file +6 -0
+			      SSBI_REG_ADDR_LPG_CTL(0),
+			      pwm->pwm_ctl[0]);
+	}
+
+	return rc;
+}

I would also appreciate if this routine could be made smaller by having it calling sub routines.

I'll add subroutines for this function.

Thank you,
~Willie

Quoted 13 lines

Cheers,
Samuel.

--
Intel Open Source Technology Centre
http://oss.intel.com/
--
To unsubscribe from this list: send the line "unsubscribe linux-arm-msm" in
the body of a message to [email protected]
More majordomo info at http://vger.kernel.org/majordomo-info.html

Signature · 2 lines
Sent by an employee of the Qualcomm Innovation Center, Inc.
The Qualcomm Innovation Center, Inc. is a member of the Code Aurora Forum.